· Droidwatch Security Research team · 7 min read · Static Analysis Deep-Dives ci-cd devsecops apk-scanning
🌐 Leer en español →

Scanning APKs in CI/CD Without Blocking Releases

Where a mobile security scan belongs in the pipeline, which verdicts should fail a build, and how to handle the failures without the team disabling the gate.

Scanning APKs in CI/CD Without Blocking Releases

Most mobile security testing happens after the build that matters has already shipped. A quarterly assessment finds a hardcoded credential that went to production eleven weeks earlier, and the finding arrives as archaeology rather than prevention.

Moving the scan into the pipeline fixes the timing. It also introduces the failure mode that makes teams disable it within a month: a gate that blocks releases for findings nobody intended to block on.

This is about getting the second part right.

Where the scan belongs

The scan runs against the built artifact, after assembly and before distribution. That position matters for a specific reason: what ships is the assembled package, including every dependency the build pulled in.

Source-level scanning misses what arrives through a transitive dependency, and misses anything the build process injects. The APK, AAB or IPA is the artifact users install, so it is the artifact to analyse.

In practice that is one step after assembleRelease, and before the upload to Play, App Store Connect or the distribution channel in use.

What the artifact scan sees that source scanning does not

The argument for scanning the package rather than the repository is concrete rather than philosophical. Three classes of finding exist only in the assembled artifact:

Third-party SDK contents. Droidwatch fingerprints 87 third-party SDKs by signature in the built package. A dependency manifest states what was requested; the artifact states what arrived, including SDKs pulled in transitively by other SDKs.

Alongside the fingerprinting, OSV.dev is queried at analysis time for known vulnerabilities. The honest boundary is worth stating: OSV covers 12 of the 87. Advertising and attribution SDKs are closed-source and absent from public vulnerability databases, so for most of the fingerprint set the pipeline establishes what is in the build but not whether a published CVE exists. A separate module tracks approximately 120 privacy trackers, which is frequently the finding that matters to a privacy review rather than a security one.

Build configuration that only exists post-assembly. android:debuggable="true" surviving into a release build, android:allowBackup="true" alongside sensitive local storage, a permissive networkSecurityConfig, a signing certificate that is not the one the release process was supposed to use. None of these are reliably visible in source.

Manifest merge results. The final permission set is the union of the application's manifest and every library manifest merged into it. A dependency can add a permission the development team never requested and would not approve.

What should fail a build

The default should be narrow. The Droidwatch CI runner takes a fail_on_verdict setting, and it defaults to Malicious alone — the most conservative option available.

That default is deliberate. A gate that fires on anything short of confirmed malware will fire on the organisation's own application, because a legitimate application legitimately requests permissions, legitimately includes analytics SDKs and legitimately obfuscates its code.

The verdict scale runs 0 to 100 where higher is worse, with four bands: 65 and above Malicious, 40 to 64 High Risk, 20 to 39 Suspicious, below 20 Benign.

A reasonable progression:

Stage Gate Rationale
Week 1 Malicious Establish the baseline without blocking anything
After a clean month Malicious\|High Risk The application's own normal score is now known
Mature Add Suspicious on release branches only Feature branches stay fast

The order is the point. Tightening a gate before the application's normal score is known produces a first week of false failures, and a team that turns it off.

Handling the failure

A build that fails on a verdict needs to tell the developer what to do next, and the distinction that matters is between this build introduced something and this application has always scored this way.

The runner emits four outputs — run identifier, verdict, score and a share URL — and the share URL is the one that changes the experience. It is a public link to the full report that a developer can open without an account, which removes the step where they file a ticket asking what the finding actually was.

Three failure categories and what each means:

The score moved. Compare against the previous build's score. A jump usually means a dependency changed. This is the case the gate exists for.

The score has always been this. The gate was tightened past the application's baseline. Either fix the underlying findings or widen the gate deliberately — but as a recorded decision, not by disabling the step.

The scan itself failed. This must be distinguished from a policy failure. The runner uses separate exit codes: 1 means the verdict matched the gate, 2 means the analysis failed, 3 means it timed out, 4 means a missing dependency or file, 5 means an HTTP error. Treating an infrastructure failure as a security failure teaches the team to ignore both.

A pipeline that reports exit code 3 as "security scan failed" will, within a quarter, have taught every engineer that the security scan is flaky. Map exit codes 2 through 5 to a distinct status — a warning, an infrastructure alert, a retry — and reserve the red build for exit code 1.

Timing

The analysis takes between two and five minutes for a 50 MB APK, and eight to ten minutes on average above 100 MB. That is long enough to matter on every commit and short enough to be unremarkable on a release branch.

Measured against production reports on 19 September 2026:

Package size Analysis time
48 MB 157 s
97 MB 503 s
111 MB 611 s
165 MB 595 s

Two practical consequences. Run the full gate on release branches and on merges to the main branch, not on every push to a feature branch. And set timeout_seconds above the largest artifact's expected duration — the default of 300 seconds is comfortable for a 50 MB application and tight for a 150 MB one.

There is one shortcut worth building into a pipeline. Where an artifact has been analysed before, the analyse call returns cached: true with a null job identifier and the existing report; there is nothing to poll. Re-running a pipeline on an unchanged artifact therefore costs seconds rather than minutes, which makes a re-run after an unrelated infrastructure failure cheap.

The integrations

Droidwatch ships six CI/CD integrations: GitHub Actions, GitLab CI/CD, Bitrise, Jenkins, CircleCI and Azure Pipelines.

All six call the same runner script, which each wrapper downloads at build time rather than vendoring. The practical effect is that a fix to the runner reaches every pipeline on its next run, with no version bump to chase. The dependencies are curl and jq — no Python, no SDK to install.

A GitHub Actions step is four lines:

- uses: Omar1123/droidwatch-scan@v1
  with:
    apk_path: app/build/outputs/apk/release/app-release.apk
    api_key: ${{ secrets.DROIDWATCH_API_KEY }}

The API key belongs in the CI secret store rather than in the workflow file, and API access begins at the Pro plan. Daily analysis limits apply per plan — 100 on Pro, 500 on Team — which is the number to check against pipeline volume before rolling the gate out to every repository in an organisation.

iOS in the same pipeline

.ipa artifacts are accepted and analysed statically across seven modules: Mach-O binary analysis, Info.plist, entitlements, App Transport Security configuration, class-dump, linked frameworks and declared privacy manifests.

There is no iOS runtime instrumentation, on any plan. For a build gate this matters less than it sounds — the gate exists to catch configuration and dependency regressions, and those are static properties. An ATS exception added to ship a feature, an entitlement that should not be in a release build, a framework nobody reviewed: all visible without execution.

What the iOS gate will not do is establish behaviour. That limitation is worth stating in the internal documentation for the pipeline, because the alternative is a team that assumes coverage it does not have.

What this does not replace

A pipeline gate catches what the build produced. It does not catch what someone else published under the organisation's brand — the repackaged version of a banking application distributed through smishing never passes through the organisation's CI at all.

That is a different workflow: monitoring for packages impersonating the application, and checking their signing certificates against the legitimate one. The triage workflow covers how that analysis runs and what it produces. Worth building, and worth not confusing with the build gate.

Nor does the gate replace reading the report. A verdict is a routing decision; the evidence behind it is in the findings, and the report walkthrough sets out where each part of that evidence sits.


Set up a pipeline scan: see the CI/CD guide.

Threat Research

Droidwatch's research team analyzes Android & iOS malware — banking trojans, spyware, droppers and overlay kits — and writes up the static and dynamic signals that give them away. Every post is grounded in real platform output.

Analyze your first app free — drag an APK or iOS .ipa onto the homepage for a full static report in about a minute. Get started free