Getting started
Wardix scans the artifact your CI already produces — the final .apk or .ipa — and diffs every scan against your previous build of the same app. You get a delta, not a wall of findings: what's new, what you fixed, and what's persisting. Findings come only from deterministic analyzers; an AI layer then explains each one in plain language and writes a concrete fix — it never invents a finding and never changes a severity.
Three steps
- Sign in at
https://app.wardix.iowith your email and password, or create an account there. Forgot your password? Use Forgot password? on the sign-in page — we email you a link that lets you set a new one. The link is single-use and expires in one hour; request a fresh one if it has been sitting in your inbox. - Upload a build from the dashboard. The first scan of an app is its baseline: every finding is new by definition, because there is no earlier build to diff against.
- Upload the next build. Now the report is a diff — new / fixed / persisting vs. the previous build of that same app — with a plain-language explanation and a concrete fix for each finding.
That is the whole loop: upload, read the delta, fix what's new. On re-upload each prior finding is reported as fixed or persisting, with anything genuinely new surfaced first.
Platforms
- Android (
.apk) — static analysis, plus optional dynamic analysis with GDPR / consent capture (trackers that fire before consent, cleartext traffic, and more). - iOS (
.ipa) — static analysis. Upload a developer-exported IPA (an ad-hoc or development export). Store-downloaded IPAs are encrypted and cannot be analyzed. iOS dynamic analysis and GDPR / consent capture are in development and not yet available.
Where to next
-
Automate it in CI. Run the same scan on every pull request:
export WARDIX_KEY="wdx_ak_…" npx @wardix/cli scan <path-to.apk|.ipa> # Node 20+, no installOnly new, non-dismissed findings can fail the gate; persisting findings never break a build, and dismissals carry forward across builds. See the CLI section for flags and gate behavior, and Upload keys for the
WARDIX_KEYyour pipeline needs. -
Verify runtime behavior (Android). Go beyond the static artifact to capture what the app actually does at runtime, including trackers that fire before the user consents. See Dynamic & GDPR capture.
Scans & the delta view
A scan takes one build of your app (an Android .apk or a developer-exported iOS .ipa) and produces a set of findings. Wardix never shows you those findings as a flat list. Every scan is compared against the previous build of the same app, and each finding is classified by how it changed since then. This delta — not a raw dump — is the product. It answers the only question that matters in review: did the change I just shipped make things worse?
The first scan of an app is its baseline: there is nothing to compare against, so every finding is new. From the second build onward, findings are diffed against the prior build and land in one of three states.
The three finding states
| State | Meaning | Effect on a CI gate |
|---|---|---|
new | Introduced by this build — not present in the previous build of the same app. This is the default view. | The only state that can fail a gate — and only when the finding is not dismissed. |
fixed | Present in the previous build, gone in this one. Shown as progress. | Never fails a build. |
persisting | Already known from an earlier build and still present. Reported for context, never re-raised as urgent. | Never fails a build. |
The practical consequence: a gate only breaks on regressions you introduced in this change. Pre-existing debt (persisting) is visible but never blocks a merge, so adopting Wardix on a mature app does not paint your pipeline red on day one. fixed findings let you show the delta going the right way. The new view is where you spend your attention.
You get the same three-state delta everywhere a scan surfaces: the dashboard at https://app.wardix.io, the CLI, and the CI API — same fingerprints, same classification. See The dashboard delta view, The CLI, and The CI API for each surface. Fingerprints are stable across builds, so the same underlying issue keeps its identity from one scan to the next; that stability is what makes new vs. persisting meaningful.
A typical CI invocation (Node 20+, no install; upload key in WARDIX_KEY, see Upload keys):
export WARDIX_KEY=wdx_ak_your_key_here
npx @wardix/cli scan ./app-release.apk --wait
--wait polls until the scan finishes and then applies the gate: it fails the build (exit code 1) only on new, non-dismissed findings at or above the --fail-on severity threshold (default high). Persisting and dismissed findings never break the build. Without --wait the CLI uploads, registers the scan, prints its URL, and exits 0 without gating. See The CLI for the full flag set.
Dismissals carry forward
When a finding is a false positive or an accepted risk, dismiss it once. That dismissal is sticky: it carries forward across every future build of the app, keyed to the finding's fingerprint. A dismissed finding stays dismissed in the dashboard, is excluded from the CLI gate, and comes back marked as dismissed in the CI API response — so it never counts toward a gate. You never have to re-triage the same thing on the next build. Because only new, non-dismissed findings can fail a gate, dismissing a finding removes it from the set that can break a build without hiding it from view; it remains inspectable, just no longer urgent. Undismissing is equally durable and takes effect on the next scan.
Where findings come from
Findings are produced only by deterministic analyzers — static analysis for Android and iOS, and Android dynamic/GDPR capture. (iOS dynamic and GDPR/consent capture are in development and not yet available; iOS today is static analysis of developer-exported .ipa files.) Whatever the source, every finding flows into the same schema and the same delta.
The AI layer never creates findings and never sets severity. Its job is to explain each finding in plain language and to write the concrete fix — nothing more. Severities and the finding set itself come from the analyzers alone. If an analyzer didn't raise it, it isn't a finding; if the analyzer rated it high, no model downgrades it. That boundary is deliberate: the delta you gate on is reproducible and trustworthy, not a model's opinion.
Upload keys
CI and other automated uploads authenticate with an upload key instead of your account credentials. A key lets a pipeline submit builds to a single app space without a login, a browser session, or any personal secret. Interactive dashboard uploads keep using your account; upload keys exist for machines.
Minting a key
Keys are created from an app space. Open the space you want uploads to land in from the dashboard (https://app.wardix.io) and mint a key on that space's page. The key is bound to that space at creation and cannot be re-pointed elsewhere.
The key is shown once, at the moment you create it:
- It looks like
wdx_ak_…. - Wardix stores only a SHA-256 hash of the key, never the key itself.
- Copy it immediately into your CI secret store. A lost key cannot be recovered — mint a replacement and revoke the old one.
Scope
An upload key is scoped to exactly one app space:
- Scans it uploads land in that space, following the same delta model as any other upload — the first scan of an app is its baseline, and later builds are compared against the previous build of the same app.
- It can read nothing else: not other app spaces, not the scans you uploaded interactively from the dashboard.
- Use one key per space. To feed several spaces from CI, mint a separate key on each.
Using a key in CI
The CLI and CI API read the key from the WARDIX_KEY environment variable and send it to the scanner at https://scanner.wardix.io. The key never needs to touch disk — keep it in your CI provider's secret store and expose it only as an environment variable for the job.
export WARDIX_KEY="wdx_ak_…"
npx @wardix/cli scan ./app/build/outputs/apk/release/app-release.apk
The CLI needs Node 20+ and no install (npx @wardix/cli scan <path-to.apk|.ipa> [flags]). The upload lands in the key's app space. In GitHub Actions, map the secret into the environment:
env:
WARDIX_KEY: ${{ secrets.WARDIX_KEY }}
For details on the upload flow and gate behavior, see CLI and CI API.
Rate limits
Requests are rate-limited per key. If a single pipeline uploads frequently enough to hit the limit, spread the work across multiple spaces — each with its own key, and each with its own window, so more keys means more aggregate throughput — or contact us to raise the limit.
Revoking a key
Revoke a key from the same app space page where it was minted. Revocation is immediate: every request made with that key fails from that moment on. Revoked keys are never reactivated — if a key is exposed or a pipeline is retired, revoke it and mint a fresh one.
The CLI
@wardix/cli uploads a build, waits for the scan, prints a delta-first report, and exits with a code your pipeline can act on. It runs on Node 20+ with no install — npx fetches it on demand.
WARDIX_KEY=wdx_ak_… npx @wardix/cli scan app-release.apk --wait --fail-on high
The command signature is:
npx @wardix/cli scan <path-to.apk|.ipa> [flags]
Point it at an Android .apk or a developer-exported iOS .ipa (an ad-hoc or development export — store-downloaded IPAs are encrypted and cannot be analyzed). The upload key in WARDIX_KEY binds the scan to one app space; see Upload keys.
Gate semantics
Wardix is delta-first: every scan is compared against the previous build of the same app, and each finding is classed new, fixed, or persisting. The first scan of an app is its baseline, so everything is new.
The gate reacts only to what this build introduced. The CLI exits non-zero only when the build added a new, non-dismissed finding at or above --fail-on. Persisting findings never re-break a build — once a finding exists it cannot fail a later scan. Dismissals made in the dashboard carry forward and are honored in CI, so a triaged finding stays quiet on every future build.
Findings themselves come only from deterministic analyzers. The AI explains each finding and writes the fix; it never invents a finding and never changes a severity.
Without --wait, the CLI uploads, registers the build, prints the scan URL, and exits 0 — the gate is skipped and the scan still runs to completion server-side.
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). |
--dynamic | — | Also run dynamic + GDPR/consent capture after the static scan. Android only — iOS dynamic/GDPR is in development and not yet available. Findings land under the Dynamic/GDPR tabs in the dashboard. Capture runs asynchronously on a worker; --wait does not block on it. |
--intent <text> | — | Custom instruction for the dynamic capture's AI navigation, e.g. "log in and open the account page" (only with --dynamic). Test login credentials are set per app in the dashboard. |
--consent <accept|reject> | reject | Which consent path the dynamic capture tests (only with --dynamic). Default reject — verifies the app doesn't track despite a rejection. |
--help | — | Show usage and exit. |
--version | — | Print the CLI version and exit. |
Environment
| Variable | Required | What it does |
|---|---|---|
WARDIX_KEY | yes | Upload key for one app space (wdx_ak_…), minted on the space page in the dashboard. Pass it as a CI secret — it is never written to disk or logs. |
WARDIX_API_URL | no | Scanner API base URL. Default: https://scanner.wardix.io |
WARDIX_APP_URL | no | Dashboard base URL used in finding deep links. Default: https://app.wardix.io |
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 WARDIX_KEY. |
The Android pipeline and iOS pipeline sections show full CI setups end to end; the GitHub Action section wraps this CLI with a sticky PR comment and SARIF upload.
CI API
The wardix CLI is a thin client over a small HTTP API — you can drive the whole scan flow directly from any environment that speaks HTTPS (a language without a Node runtime, a custom build agent, a script). The CLI, the GitHub Action, and this API all reach the same pipeline and honor the same delta and dismissal rules.
Base URL: https://scanner.wardix.io. Authenticate every request with an upload key (see Upload keys) in an Authorization header:
Authorization: Bearer wdx_ak_…
A key is scoped to one app space. Scans you register land in that space, and the read endpoints only return scans in that space — a scan outside it reads as 404. A malformed or missing key is 401; a revoked key is 401.
The flow
A scan is created in three ordered steps, then polled to completion:
- Mint an upload —
POST /ci/uploadswith the build's filename. Returns ascan_idand a one-time signed upload URL. No scan row exists yet. - Upload the binary —
PUTthe file to that signed URL. No auth header — the signed URL itself is the credential. - Register the scan —
POST /ci/scanswith thescan_id. Verifies the binary landed, creates the queued scan in your key's space, and starts the pipeline.
Then poll GET /ci/scans/{scan_id} until status is completed or failed, and fetch GET /ci/scans/{scan_id}/findings once it completes.
The binary goes straight to storage in step 2, so large builds never proxy through the API. The extension you pass in step 1 (.apk or .ipa) selects the platform — Android (APK) or iOS (a developer-exported .ipa; store-downloaded IPAs are encrypted and cannot be analyzed). iOS gets static analysis; Android additionally supports dynamic and GDPR/consent capture.
POST /ci/uploads
Mint a scan_id and a one-time signed upload URL for a build.
Send the build's filename. The extension selects the analysis platform: .apk → Android, .ipa → iOS. Any other extension is rejected with 422.
curl -sS -X POST https://scanner.wardix.io/ci/uploads \
-H "Authorization: Bearer $WARDIX_KEY" \
-H "Content-Type: application/json" \
-d '{"filename": "app-release.apk"}'
{
"scan_id": "3f2a…",
"artifact_path": "…/3f2a….apk",
"upload_url": "https://…storage…/signed…"
}
PUT the binary to the signed URL
Upload the binary to the signed URL returned above. This is a plain HTTP PUT of the file bytes with no Authorization header — the signed URL is single-use and is itself the credential.
curl -sS -X PUT --upload-file app-release.apk "$UPLOAD_URL"
POST /ci/scans
Register the uploaded build and start the scan.
Send the scan_id from step 1. The API verifies the binary actually landed in storage (409 if it did not — upload it first), inserts the queued scan in the key's space, and nudges the pipeline. Registering the same scan_id twice is 409. An optional filename is kept as the build's display name.
{ "scan_id": "3f2a…" }
Returns { "id": "3f2a…", "status": "queued" }.
To also run Android dynamic + GDPR/consent capture after the static scan, include "dynamic": true (optionally "consent": "accept" | "reject" | "both"). Capture runs asynchronously on a worker. iOS dynamic and GDPR capture is in development and not yet available — an .ipa runs static analysis only.
GET /ci/scans/{scan_id}
Status and delta counts for a scan the key can see.
Poll until status is completed or failed. Once completed, the response carries the diff against the app's previous build: new_count, fixed_count, persisting_count. A scan outside the key's space is 404.
{
"id": "3f2a…",
"status": "completed",
"new_count": 2,
"fixed_count": 1,
"persisting_count": 5
}
GET /ci/scans/{scan_id}/findings
The findings for a completed scan. Returns 409 while the scan is still running — poll status first.
Each finding carries delta (new | persisting) and state (open | dismissed). Findings come only from deterministic analyzers — the AI explains them and writes fixes, but never invents a finding and never changes a severity.
Gating on the response
The delta model is the whole point: every finding is scored new, fixed, or persisting against the previous build of the same app. The first scan of an app is its baseline, so everything is new.
A faithful gate fails the build only on a finding whose delta is new and whose state is open, at or above your severity threshold. That is exactly what the CLI's --fail-on does. persisting findings never break a build — they were already there. Dismissals made in the dashboard carry forward as state == "dismissed" here, so a finding you have accepted or marked a false positive never re-fails CI. See Scans and the delta view for how these states are assigned, and the CLI and GitHub Action sections if you would rather not implement the loop yourself.
GitHub Action
The Wardix GitHub Action is a composite action that wraps the @wardix/cli CLI (see CLI). In one step it scans the artifact your workflow just built, keeps a single sticky comment on the pull request updated with the delta summary, optionally uploads a SARIF log to GitHub code scanning, and fails the job only when the gate fails.
It runs the same command you would run by hand, so it carries the same exit codes as the CLI: 0 gate passed, 1 gate failed, 2 scan failed, 3 usage or auth error. The scan, the sticky comment, and the SARIF upload always run to completion first — the job then exits with the CLI's code as the last step, so a gate failure never robs you of the comment or the code-scanning alerts.
Reference the action by its repository —
wardixio/wardix-scan-action@v1— and pin the major version tagv1. If you would rather not add an action, the plain CLI snippet below runs the identical scan in any CI.
Usage
name: wardix
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: wardixio/wardix-scan-action@v1
with:
artifact: app/build/outputs/apk/release/app-release.apk
key: ${{ secrets.WARDIX_KEY }}
fail-on: high
upload-sarif: "true"
Point artifact at whatever your build produced — an Android .apk or a developer-exported iOS .ipa. The gate reflects the delta against the previous build of the same app: only new, non-dismissed findings at or above fail-on can fail the job; persisting findings never break a build, and dashboard dismissals carry forward automatically. The first scan of an app is its baseline, so everything is new and the gate reads against that.
Plain CLI (no action)
The action is a convenience wrapper; the CLI is the contract. Any CI can do the same thing directly:
npx @wardix/cli scan app/build/outputs/apk/release/app-release.apk \
--wait --fail-on high --sarif wardix.sarif
Node 20+ is required and npx fetches the CLI on demand, so nothing needs to be installed. Pass the upload key as the WARDIX_KEY environment variable (it looks like wdx_ak_…; see Upload keys), never as a literal in the workflow file.
Inputs
| Input | Default | What it does |
|---|---|---|
artifact | (required) | Path to the built .apk or .ipa to scan. |
key | (required) | Upload key (wdx_ak_…). Pass a repository secret, never a literal. |
fail-on | high | Lowest new severity that fails the build: critical | high | medium | low | info | none. |
comment | true | Post and keep updating the sticky PR comment with the delta summary. Needs pull-requests: write. |
upload-sarif | false | Upload the SARIF log to 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 | @wardix/cli npm version to run. |
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 report on https://app.wardix.io. |
gate | Gate verdict: passed | failed | off (off when fail-on: none). |
Sticky comment
With comment: true (the default), the action maintains one comment on the pull request rather than adding a new one per run. Each push rewrites that same comment in place with the current new / fixed / persisting counts, the gate verdict, the list of new findings (dismissed ones flagged as non-gating), and a link to the full report. Requires pull-requests: write and only runs on pull_request events.
SARIF
With upload-sarif: "true" — or the CLI's --sarif <file> in any CI — the findings snapshot is emitted as SARIF 2.1.0. Severity maps to the SARIF level, MASVS groups ride along as rule tags, and each alert deep-links to its finding on the dashboard. Stable finding fingerprints go into partialFingerprints, so an alert keeps its identity across builds; dashboard dismissals are exported as SARIF suppressions. GitHub diffs consecutive uploads itself to open and resolve alerts. Private repositories need GitHub Advanced Security for code scanning.
Dynamic & GDPR capture
Static analysis reads the binary. A dynamic + GDPR/consent scan goes further: Wardix installs and runs the app on an instrumented emulator, records its real network traffic, and inspects consent behaviour — did it fire trackers before the user agreed, does it send data in cleartext, does it honour a rejection. Dynamic findings come from the same deterministic analyzers as everything else (the AI only explains them and drafts fixes — it never invents a finding and never changes a severity), and they land in the scan report under their own Dynamic and GDPR tabs, alongside the static ones, with the same new / fixed / persisting delta view: each finding is compared against the previous build of the same app, dismissals carry forward, and the first scan of an app is its all-new baseline.
Android dynamic + GDPR capture is live today.
iOS dynamic capture — in development. iOS GDPR/consent capture is being built and is NOT yet available. When it lands the flow will mirror Android: upload a developer-exported IPA and add
--dynamic. iOS static analysis is already live for every account.
Run a dynamic scan
Add --dynamic to any scan:
WARDIX_KEY=wdx_ak_… npx @wardix/cli scan app-release.apk --dynamic --consent reject --wait
The invocation is the standard npx @wardix/cli scan <path-to.apk|.ipa> [flags] (Node 20+, nothing to install); --dynamic just adds the capture step on top of the static scan. WARDIX_KEY is your upload key — it looks like wdx_ak_… and is passed via the environment, never on the command line (see Upload keys).
Capture runs asynchronously on a separate worker, so --wait returns as soon as the static result is ready — it does not block on the dynamic run. The Dynamic and GDPR findings appear in the report a short while later, once the worker finishes. For the complete flag list and gate/exit-code behaviour, see the CLI and CI API sections.
Choose the consent path: --consent
--consent selects which consent behaviour the capture exercises:
--consent reject(default) tests the privacy-critical path: the capture rejects the app's consent prompt, then verifies the app respects that choice. Trackers that fire despite the rejection — or before any choice is made at all — become findings.--consent acceptcaptures the accepted path, so you can review what the app sends once the user opts in.
Steer navigation: --intent
The capture drives the app automatically, but some screens it cannot guess its way past — a login wall, or a specific feature you want exercised. Use --intent to describe in plain language where to go:
WARDIX_KEY=wdx_ak_… npx @wardix/cli scan app-release.apk \
--dynamic --consent reject \
--intent "log in and open the account page" --wait
Test login credentials are configured per app in the dashboard, never passed on the command line. When your intent asks the capture to sign in, it uses the credentials saved for that app.
Request a dynamic scan from the dashboard
You do not have to use the CLI. When you upload a build in the dashboard you can request a dynamic scan for that upload directly. You can also save a reusable consent scenario — a consent stance plus a navigation intent — under the app's Scan layers settings, so every future build of that app captures the same way without re-passing the flags.
Android pipeline
Wire Wardix into your Android CI as a security gate: build the release APK, scan it with the CLI, and fail the job on new high-severity findings. The gate is delta-aware — it breaks a build only when a change introduces a new problem, never for issues that already existed.
1. Build the release APK
Wardix scans the compiled artifact your pipeline already produces — the release APK:
./gradlew assembleRelease
# artifact: app/build/outputs/apk/release/app-release.apk
If your pipeline ships an Android App Bundle (.aab), Wardix does not scan the bundle itself — point it at the universal/release APK you also build (the same APK you would sideload to smoke-test the release).
2. Create a space and mint an upload key
In the dashboard (https://app.wardix.io) create an app space for this app, then mint an upload key for it (see Upload keys). The key looks like wdx_ak_…. Store it as a CI secret named WARDIX_KEY — the CLI reads it from that environment variable. Every scan made with a space's key is diffed against the previous build in that space, so keep one space per app.
3. Add the gate
The CLI needs no install (Node 20+):
npx @wardix/cli scan <path-to.apk|.ipa> [flags]
--wait blocks the job until the scan finishes and sets the exit code; --fail-on high makes the job fail when the scan surfaces new findings at or above the high severity threshold. The CLI uploads the build to and polls the CI API at https://scanner.wardix.io, so allow egress to that host if your runners are behind an egress allowlist. Copy-paste for your CI:
GitHub Actions:
- name: Wardix security gate
run: npx @wardix/cli scan app/build/outputs/apk/release/app-release.apk --wait --fail-on high
env:
WARDIX_KEY: ${{ secrets.WARDIX_KEY }}
GitLab CI:
wardix:
stage: test
image: node:20
script:
- npx @wardix/cli scan app-release.apk --wait --fail-on high
variables:
WARDIX_KEY: $WARDIX_KEY
Bitrise:
- script@1:
title: Wardix security gate
inputs:
- content: npx @wardix/cli scan $BITRISE_APK_PATH --wait --fail-on high
fastlane (Fastfile lane):
lane :wardix do
sh("npx @wardix/cli scan ../app/build/outputs/apk/release/app-release.apk --wait --fail-on high")
end
In Bitrise and fastlane the key is read from the same WARDIX_KEY environment variable — expose it as a secret env var in your Bitrise workflow, or via your fastlane environment.
4. Optional: dynamic + GDPR capture
Add --dynamic to also run Android dynamic analysis and GDPR/consent capture on the same build (Android dynamic is live — see Dynamic & GDPR capture):
npx @wardix/cli scan app-release.apk --wait --fail-on high --dynamic
Dynamic capture runs asynchronously on a worker, and --wait does not block on it — so --dynamic does not change this job's exit code. The captured findings surface in the dashboard's Dynamic and GDPR tabs and are diffed build-over-build through the same delta as static findings.
5. Gate behaviour
Wardix compares each scan against the previous build of the same app in the same space:
- The job fails only on new, non-dismissed findings at or above
--fail-on. - Persisting findings — those already present in the prior build — never break the build.
- Dismissals you make in the dashboard carry forward across builds, so a triaged finding stays silent in CI.
- The first scan of an app is its baseline: every finding is new, and it becomes the reference point that later builds diff against.
For a richer GitHub experience — a sticky PR comment summarising the delta and SARIF uploaded to the repository's Security tab — use the Wardix GitHub Action instead of calling the CLI directly (see GitHub Action).
iOS pipeline
Wire iOS scanning into CI the same way you wire Android: mint an upload key, run the wardix CLI on your build, and let the delta gate decide. The only real difference is the artifact — iOS ships a signed .ipa instead of an .apk, and it must be a developer-exported IPA.
What runs today. iOS static analysis is live for every account — upload a developer-exported
.ipaand you get the same deterministic findings, delta view, and CI gate as Android. iOS dynamic capture and GDPR/consent verification are in development and not yet available, so the--dynamicflag is Android-only for now. Do not pass--dynamicto an iOS scan.
Upload keys, the CLI, the CI API, the GitHub Action, SARIF output, and the dashboard delta view all behave identically to the Android pipeline — see Upload keys, The CLI, GitHub Action, and Android pipeline.
1. Produce a developer-exported IPA
Wardix analyzes developer-exported IPAs — a development or ad-hoc export you build yourself. A store-downloaded IPA is encrypted (FairPlay DRM) and cannot be analyzed, so always export the build from your own toolchain.
Raw xcodebuild archive + export:
xcodebuild -scheme MyApp -archivePath build/MyApp.xcarchive archive
xcodebuild -exportArchive -archivePath build/MyApp.xcarchive \
-exportPath build/ipa -exportOptionsPlist ExportOptions.plist
# → build/ipa/MyApp.ipa (use a development or ad-hoc export)
Set method to development or ad-hoc in your ExportOptions.plist.
Or the fastlane gym path — one lane that builds the IPA and scans it:
# fastlane
lane :wardix do
gym(scheme: "MyApp", export_method: "development", output_directory: "build/ipa")
sh("npx @wardix/cli scan ../build/ipa/MyApp.ipa --wait --fail-on high")
end
2. Mint an upload key
Create an upload key scoped to the app's space in the dashboard at https://app.wardix.io, then store it as the CI secret WARDIX_KEY (the value looks like wdx_ak_…). The CLI and CI API read the key from the WARDIX_KEY environment variable and route the scan to the matching space. See Upload keys for how keys are scoped, rotated, and revoked.
3. Add the gate
The CLI is invoked as npx @wardix/cli scan <path-to.apk|.ipa> [flags] (Node 20+, no install). Point it at the .ipa; --wait blocks until the scan finishes, and --fail-on high sets the gate severity. A GitHub Actions step:
- name: Wardix security gate
run: npx @wardix/cli scan build/ipa/MyApp.ipa --wait --fail-on high
env:
WARDIX_KEY: ${{ secrets.WARDIX_KEY }}
The CLI talks to the CI API at https://scanner.wardix.io; no other configuration is needed. For SARIF upload, the full input/output matrix, and the composite Action, see GitHub Action and CI API.
Delta and gate semantics
iOS scans follow the same delta model as Android (see Scans & the delta view):
- Every finding is scored against the previous build of the same app as new, fixed, or persisting.
- The first scan of an app is its baseline — every finding counts as new.
- Only new, non-dismissed findings at or above your
--fail-onthreshold can fail the gate. Persisting findings never break a build. - Dismissals carry forward across builds — a finding you dismiss in the dashboard, CLI, or CI API stays dismissed on later builds and cannot re-fail the gate.
Findings come only from Wardix's deterministic static analyzers. The AI writes the plain-language explanation and the suggested fix; it never invents a finding and never changes a severity.
Data handling
Wardix keeps your findings, not your binaries. This section states exactly what is stored, what is discarded, and what leaves our infrastructure.
Binaries are deleted after every scan
- The uploaded artifact (
.apkor.ipa) is removed from storage as soon as analysis finishes — on success and on failure alike. - We keep the resulting findings and their history across builds. We do not keep your APKs or IPAs.
- Deletion covers both the stored upload object and the scanner's temporary working directory.
Isolated, per-scan sandbox
- Static analysis runs in an isolated, ephemeral sandbox that is created fresh for each scan and torn down afterward.
- Your binary is treated as untrusted input: it never has access to account credentials or to any other tenant's data.
- Each scan runs in its own slot — your binary is not co-processed with anyone else's.
Findings come only from deterministic analyzers
- Every finding is produced by a deterministic analyzer, never by the AI.
- The AI explains findings and drafts fixes. It never invents a finding and never changes a severity.
- Because findings are deterministic, the same build produces the same findings — that is what makes the delta between builds (new / fixed / persisting) trustworthy.
What the AI sees
When you ask the AI to explain a finding, only that finding's metadata is sent to the configured LLM provider (Nebius AI Studio). Your binary, your source code, and your full app are never sent.
The payload is limited to:
- the analyzer rule and finding title
- the severity
- a short evidence snippet
- the package name
- the detected framework
{
"rule": "android_cleartext_traffic",
"title": "Cleartext traffic permitted",
"severity": "high",
"evidence": "android:usesCleartextTraffic=\"true\"",
"package": "com.example.app",
"framework": "native-android"
}
Explanations are cached and reused across builds, so re-explaining a finding that persists into a later build does not send it to the provider again.
Android dynamic capture
For Android dynamic and GDPR/consent capture, the app is installed and exercised inside an isolated capture sandbox. The dynamic findings are derived from the traffic and consent behavior recorded there — not from your binary, which (as above) is deleted after the run.
iOS dynamic and GDPR capture is not yet available. iOS support today is static analysis of developer-exported IPAs, which follows the same deletion and sandbox rules described above.