Documentation
Everything you need to scan builds from the dashboard or your pipeline. One idea runs through all of it: DeltaWard tells you what this build introduced — not a 200-item dump of everything that was ever true.
Getting started
DeltaWard analyzes the artifact your CI already produces — the final .apk or .ipa — and diffs every scan against your previous build of the same app.
- Sign in at app.deltaward.com. (DeltaWard is in closed alpha — sign-ups are invite-only for now.)
- Upload a build from the dashboard. The first scan of an app becomes its baseline: everything it finds is new by definition.
- Upload the next build and the report becomes a diff — new, fixed, persisting — with plain-language explanations and concrete fixes.
iOS note: upload developer-exported IPAs (from your own build or an ad-hoc/development export). Store-downloaded IPAs are encrypted and can't be analyzed.
Scans & the delta view
Every finding in a scan carries a delta state relative to your previous build:
| State | Meaning |
|---|---|
| new | Introduced by this build. The default view, and the only thing that can fail a CI gate. |
| fixed | Present in the previous build, gone in this one. |
| persisting | Known from earlier builds. Reported, never re-raised as urgent — and never breaks a build. |
Dismissals carry forward. Dismiss a finding once (false positive, accepted risk) and it stays dismissed across future builds — in the dashboard, in the CLI gate, and in the CI API. Findings come only from deterministic analyzers; severities are never set by an LLM.
Upload keys
CI uploads authenticate with an upload key instead of your account credentials. Mint one on an app space's page in the dashboard.
- Keys look like
dw_ak_…and are shown once, at creation. DeltaWard stores only a SHA-256 hash — a lost key is replaced, never recovered. - A key is scoped to one app space: scans it uploads land in that space, and it can read nothing else — not your other spaces, not dashboard uploads.
- Revocation is immediate. Revoke a key on the space page and every request with it fails from that moment.
- Requests are rate-limited per key. Spread very chatty pipelines across spaces, or contact us.
Store the key as a CI secret (DELTAWARD_KEY). It never needs to be written to disk.
The CLI
The deltaward npm package uploads a build, waits for the scan, prints a delta-first report, and exits with a code your pipeline can act on. Node 20+, no install required:
DELTAWARD_KEY=dw_ak_… npx deltaward scan app-release.apk --wait --fail-on high
Gate semantics: the exit code is non-zero only when this build introduced findings — new, non-dismissed, at or above --fail-on. Persisting findings never re-break a build, and dismissals made in the dashboard are honored in CI.
Flags
| Flag | Default | What it does |
|---|---|---|
| --fail-on <severity> | high | Lowest severity of a NEW finding that fails the build (critical | high | medium | low | info | none). "none" disables the gate — the scan still runs and reports. |
| --wait | — | Poll until the scan finishes, print the delta report, and apply the gate. Without it the CLI uploads, registers, prints the scan URL and exits 0. |
| --json | — | Machine-readable output: one JSON document on stdout, nothing else. |
| --sarif <file> | — | Write the findings as SARIF 2.1.0 to <file> (for GitHub code scanning upload). Requires --wait. |
| --timeout <seconds> | 600 | Maximum time to wait for the scan to complete (only with --wait). |
| --help | — | Show usage and exit. |
| --version | — | Print the CLI version and exit. |
Environment
| Variable | Required | What it does |
|---|---|---|
| DELTAWARD_KEY | yes | Upload key for one app space (dw_ak_…), minted on the space page in the dashboard. Pass it as a CI secret — it is never written to disk or logs. |
| DELTAWARD_API_URL | no | Scanner API base URL. Default: https://scanner.deltaward.com |
| DELTAWARD_APP_URL | no | Dashboard base URL used in finding deep links. Default: https://app.deltaward.com |
Exit codes
| Code | Name | Meaning |
|---|---|---|
| 0 | pass | Scan completed and no new, non-dismissed findings meet --fail-on (or --wait was not given and the upload was accepted). |
| 1 | gate failed | New, non-dismissed findings at or above --fail-on. Persisting findings never cause this — only what this build introduced. |
| 2 | scan failed | The analysis errored or did not complete within --timeout. |
| 3 | usage / auth error | Bad arguments, unreadable file, missing or rejected DELTAWARD_KEY. |
CI snippets
GitHub Actions:
- name: DeltaWard security gate
run: npx deltaward scan app/build/outputs/apk/release/app-release.apk --wait --fail-on high
env:
DELTAWARD_KEY: ${{ secrets.DELTAWARD_KEY }}
On GitHub, the GitHub Action wraps this with a sticky PR comment and SARIF upload.
GitLab CI:
deltaward:
stage: test
image: node:20
script:
- npx deltaward scan app-release.apk --wait --fail-on high
variables:
DELTAWARD_KEY: $DELTAWARD_KEY
Bitrise:
- script@1:
title: DeltaWard security gate
inputs:
- content: npx deltaward scan $BITRISE_APK_PATH --wait --fail-on high
CI API
The CLI is a thin client over a small HTTP API. Use the API directly from any environment that can speak HTTPS. Base URL: https://scanner.deltaward.com; authenticate every request with Authorization: Bearer dw_ak_….
The flow is three steps, in order:
POST /ci/uploadswith your build's filename — returns ascan_idand a one-time signed upload URL.PUTthe binary to the signed URL (plain HTTP PUT, no auth header — the URL itself is the credential).POST /ci/scanswith thescan_id— registers the scan and starts the pipeline. Then pollGET /ci/scans/{id}and fetchGET /ci/scans/{id}/findingswhen it completes.
Endpoints
Mint a scan_id and a one-time signed upload URL for a build.
No scan exists yet: PUT the binary to upload_url (plain HTTP PUT, no auth header — the URL itself is the credential), then register it with POST /ci/scans. The filename's extension (.apk or .ipa) selects the analysis platform; anything else is rejected with 422.
Register an uploaded build and start the scan.
Verifies the binary actually landed in storage (409 if it didn't), creates the queued scan in the key's app space, and starts the pipeline. 409 if the scan_id was already registered. Optional filename is kept as the build's display name.
Status and delta counts for a scan the key can see.
Poll until status is completed or failed. Once completed, new_count / fixed_count / persisting_count carry the diff against the app's previous build. 404 for scans outside the key's space.
Findings for a completed scan (409 while it is still running).
Each finding carries delta (new | persisting) and state (open | dismissed). A faithful CI gate fails the build only on findings with delta == "new" and state == "open" at or above its severity threshold — dismissals made in the dashboard are honored here.
Gating on the response
Each finding in the findings response carries delta (new | persisting) and state (open | dismissed). A faithful gate fails the build only on findings where delta == "new" and state == "open" at or above your severity threshold — that's exactly what the CLI's --fail-on does.
GitHub Action
A composite action wrapping the CLI: scan the artifact your workflow just built, keep one sticky PR comment updated with the delta summary, optionally upload SARIF to GitHub code scanning, and fail the job only when the gate fails. Same exit codes as the CLI; the comment and SARIF upload always run before the job fails.
Usage
name: deltaward
on: pull_request
permissions:
contents: read
pull-requests: write # sticky PR comment
security-events: write # only if upload-sarif: true
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./gradlew assembleRelease
- uses: deltaward/scan-action@v1
with:
artifact: app/build/outputs/apk/release/app-release.apk
key: ${{ secrets.DELTAWARD_KEY }}
fail-on: high
upload-sarif: "true"
Inputs
| Input | Default | What it does |
|---|---|---|
artifact | — (required) | Path to the built .apk or .ipa to scan. |
key | — (required) | DeltaWard upload key (dw_ak_…) — pass a repository secret, never a literal. |
fail-on | high | Lowest severity of a NEW finding that fails the build (critical | high | medium | low | info | none). |
comment | true | Post (and keep updating) a sticky PR comment with the delta summary. Needs pull-requests: write. |
upload-sarif | false | Upload the SARIF log to GitHub code scanning. Needs security-events: write (and GitHub Advanced Security on private repos). |
timeout | 600 | Maximum seconds to wait for the scan to complete. |
cli-version | latest | deltaward npm version to run (passed to npx). |
Outputs
| Output | What it is |
|---|---|
new-count | Findings this build introduced. |
fixed-count | Findings gone since the previous build. |
persisting-count | Findings carried over from previous builds (never gate). |
report-url | Dashboard URL of the full scan report. |
gate | Gate verdict — passed, failed or off (--fail-on none). |
Code scanning (SARIF)
With upload-sarif: "true" here — or the CLI's --sarif <file> flag in any other CI — the full findings snapshot is written as SARIF 2.1.0: severity maps to level, MASVS groups ride along as rule tags, and every alert deep-links back to its dashboard finding. DeltaWard's stable fingerprints go into partialFingerprints so alert identity survives across builds, and dashboard dismissals arrive as suppressions. GitHub diffs consecutive uploads itself — alerts open and close with your builds. Private repos need GitHub Advanced Security for code scanning.
Data handling
- Binaries are deleted after every scan — the uploaded artifact is removed from storage as soon as analysis finishes, success or failure. Findings are kept; your APKs and IPAs are not.
- Analysis is static and runs in an isolated, per-scan sandbox. Your binary is treated as untrusted input.
- Findings come only from deterministic analyzers. The AI explains findings and writes fixes — it never invents findings and never changes a severity.