Remediation, signing & reporting
Finding a problem is half the job. This page covers the other half: Rutken's end-to-end command-line workflow for fixing what the scanner finds, signing the rebuilt app with Rutken's own APK Signature Scheme v2 signer, verifying signatures for trust gating, and turning results into reports and CI/CD gates. Every mutating verb fails closed: it never leaves a partial or corrupt APK behind.
Overview: the remediation loop
Rutken closes the loop from detection to a fixed, installable build entirely from the command line. A typical cycle looks like this:
# 1. Scan and see what's wrong
rutken app.apk scan
# 2. List the suggested fixes (read-only)
rutken app.apk fix
# 3. Apply the safe manifest fixes and re-sign in one shot
rutken app.apk fix --apply --out fixed.apk \
--keystore release.p12 --keystore-pass-file ./keystore-pass.txt
# 4. Confirm the rebuilt app is validly signed
rutken fixed.apk verify-signature
# 5. Re-scan to confirm the findings are gone
rutken fixed.apk scan
The verbs split into two layers. The high-level ones
(fix) map findings to fixes and rebuild for you;
the low-level ones (patch, sign,
edit-code) do exactly one surgical thing each.
All of them share one safety contract:
- Fail closed. No verb ever writes a partial or corrupt APK. If any step of a rebuild cannot complete, nothing is written.
- Unsigned unless you ask. Omit
--keystoreand the output is left unsigned (useful for inspection); pass it and the output is signed with Rutken's in-house v2 signer. - No passwords on argv. There is
deliberately no
--keystore-passwordflag — passwords come from an environment variable or a file so they never show up inps.
rutken fix — the auto-remediation loop
fix is the one-command remediation entry point.
It runs the default rule set, maps each finding to zero or
more concrete fixes, and — with --apply —
rebuilds and re-signs the app.
Read-only by default
Run with no flags and fix only lists
suggestions; it touches nothing on disk. This is the safe way
to preview what an --apply run would do.
rutken app.apk fix
rutken app.apk fix --format json
Each finding is mapped to fixes by the remediation catalog
(the binary remediation catalog), which
currently covers seven mappable rule ids. Fixes come in two
kinds, and the distinction matters for --apply:
- Manifest-shaped fixes — e.g. disabling
android:debuggable,allowBackup, or cleartext traffic. These can be applied automatically. - Advisory DEX code fixes — source/bytecode
changes that need a human. These are always listed and
never applied, even under
--apply.
--apply: rebuild and re-sign
--apply --out <PATH> is opt-in. It translates
the manifest subset of the selected fixes into a single merged
patch plan, rebuilds the APK
through the writer, and — if a keystore is given — re-signs it.
The translation step lives in the CLI layer, not the engine:
the CLI is what turns findings into the merged plan the patch
subsystem then executes.
# Apply every safe fix in one merged patch, unsigned output
rutken app.apk fix --apply --out fixed.apk
# Apply and sign the result with the in-house v2 signer
rutken app.apk fix --apply --out fixed-signed.apk \
--keystore release.p12 --keystore-pass-file ./keystore-pass.txt
Expected output (text): each applied fix is listed with the rule it addresses, the advisory DEX fixes are printed as still-manual, and on a signed run the signing scheme and the certificate's SHA-256 fingerprint are echoed.
All-or-nothing gating
--apply is strictly all-or-nothing. The run
refuses — writing nothing — if any selected fix
either:
- fails to translate into a patch plan, or
- is behaviour-changing and you did not pass
--include-behaviour-changing.
Behaviour-changing fixes are the ones that can break a working
app: disabling android:exported on a component or
android:grantUriPermissions on a provider. To
include them you must opt in explicitly, and Rutken prints the
corresponding risk warning when it applies one:
rutken app.apk fix --apply --include-behaviour-changing \
--out fixed-signed.apk \
--keystore release.p12 --keystore-pass-file ./keystore-pass.txt
Flags
--format text|json— output shape (defaulttext).--apply— opt in to writing a rebuilt APK.--out <PATH>— output path; required by and only meaningful with--apply.--overwrite— allow--applyto overwrite an existing file at--out.--include-behaviour-changing— also apply exported/grant-uri fixes (otherwise they trip the gate).- Signing flags (
--keystore,--keystore-pass-env/--keystore-pass-file,--key-alias,--signing-scheme) — see Signing flags.
Gotcha: fix uses
--out (no short form), whereas the lower-level
patch/sign/edit-code
verbs use -o / --output. Advisory
DEX fixes never get applied here — if you need a bytecode
change, use rutken edit-code (width-preserving
Tier C editing).
rutken patch — the patch subsystem
patch is the surgical, one-fix-per-run
remediation verb. Where fix --apply bundles the
whole manifest subset, patch applies exactly one
safe manifest remediation per invocation — there is no bulk
"apply everything" mode here by design.
Under the hood it finds a finding matching the requested flag, builds the matching transform, and rebuilds the APK through the writer. The AXML and DEX edits it performs are width-preserving: structures are rewritten in place without disturbing surrounding offsets, so the rebuild is lossless apart from the intended change.
The five manifest transforms
Pick exactly one per run:
--disable-debuggable— clearandroid:debuggable.--disable-allow-backup— clearandroid:allowBackup.--disable-cleartext-traffic— clearandroid:usesCleartextTraffic. Only applies when the manifest sets it explicitly totrue; the inferred-default (attribute absent) case has no attribute to patch.--disable-exported <NAME>— clearandroid:exportedon the named component. May break legitimate functionality — review first.--disable-grant-uri <NAME>— clearandroid:grantUriPermissionson the named provider. May break legitimate functionality — review first.
Examples
# Unsigned output
rutken app.apk patch --disable-debuggable -o patched.apk
# Signed output (in-house v2)
rutken app.apk patch --disable-debuggable -o patched-signed.apk \
--keystore release.p12 --keystore-pass-file ./keystore-pass.txt
rutken app.apk patch --disable-allow-backup -o patched.apk
rutken app.apk patch --disable-cleartext-traffic -o patched.apk
rutken app.apk patch --disable-exported com.example.app.SomeActivity -o patched.apk
rutken app.apk patch --disable-grant-uri com.example.app.SomeProvider -o patched.apk
Expected output: a one-line confirmation of
the applied transform and the output path; on a signed run,
the signing scheme and certificate fingerprint. The
--disable-exported/--disable-grant-uri
variants additionally print a risk warning.
Width-preserving edits, and native targets
For advanced bytecode surgery, rutken edit-code
performs a width-preserving instruction patch ("Tier C"): the
replacement must occupy the same number of 16-bit code units
and reference only existing registers/ids, guaranteeing binary
correctness (not semantic correctness). patch
also has a native arm (Linux ELF today) for metadata
transforms, which is consent-gated.
Gotcha: patch requires
-o / --output (not --out).
Exactly one disable-flag is honored per run; passing none
that matches a finding is a no-op refusal, not a silent empty
write.
rutken sign — Rutken's in-house v2 signer
sign (re)signs an APK with no other
change. The signer is Rutken's own
implementation of APK Signature Scheme v2 — it is
not a wrapper around apksigner or the
Android build tools. That matters in practice: Rutken resigns
repacked, obfuscated, and split apps, and Android installs the
result like any other correctly signed APK.
# Re-sign only (no patch), choosing the key by alias
rutken app.apk sign -o signed.apk \
--keystore release.p12 --key-alias release
# Password from a file instead of the environment
rutken app.apk sign -o signed.apk \
--keystore release.p12 --keystore-pass-file ./keystore-pass.txt
Expected output: on success, both
sign and patch print the signing
scheme used and the signing certificate's SHA-256 fingerprint
— the same fingerprint you would later pass to
verify-signature --expect-signer.
Signing flags (shared by patch, sign, fix --apply, embed-gadget, edit-code)
--keystore <path>— a PKCS#12 keystore (.p12/.pfx, or a.jks-named file that is actually PKCS#12 — format is sniffed by magic bytes, never by extension). Legacy JKS files are rejected with akeytoolconversion hint. Required forsign; optional elsewhere (absent ⇒ unsigned output).--keystore-pass-env <VAR>— name of the environment variable holding the password. DefaultRUTKEN_KEYSTORE_PASS.--keystore-pass-file <path>— read the password from a file instead (trailing newline trimmed). Takes precedence over--keystore-pass-env.--key-alias <alias>— which private-key entry to sign with. Defaults to the keystore's sole private-key entry; required (with available aliases listed in the error) when there is more than one. Also determines theMETA-INF/<NAME>.SF/.RSAentry names for v1 signing.--signing-scheme auto|v1|v2|v1+v2— which scheme(s) to emit.auto(default) mirrorsapksigner's own default: v1 (JAR) + v2 when the target'sminSdkVersionis absent or below 24, v2-only otherwise.v1/v2force one scheme (refusing if the target can't install with it);v1+v2forces both.
Why no --keystore-password flag?
Passwords on argv are visible to any other process via
ps, so they're deliberately read from an env var
or a file. PKCS#12 uses one password for both the store and
every key in it, so there is one password flag pair — not
separate store/key passwords. Set it up once in CI:
export RUTKEN_KEYSTORE_PASS='…' # default env var name
rutken app.apk sign -o signed.apk --keystore release.p12
rutken verify-signature — our verifier
verify-signature cryptographically verifies an
APK's own signature and, optionally, checks the signer against
fingerprints you trust. It reports two independent claims:
- Integrity (always checked) — recomputes the content digest for the highest present scheme (v3 > v2 > v1) and verifies the signer's signature. Proves the APK is byte-for-byte unmodified since signing and that the signer held the private key. Says nothing about who the signer is.
- Trust (only with
--expect-signer) — compares each verified signer's leaf-certificate SHA-256 against the supplied set. Without the flag, trust is reported as not checked; Rutken never invents an expected signer. - No X.509 chain validation (never, by design) — APK signing certs are self-signed by convention, which is the normal, correct state and is never flagged.
Trust gating with --expect-signer
--expect-signer <SHA256> is repeatable and
accepts the common apksigner/keytool
presentation forms (upper or lower case, :- or
space-separated groups). Every signer's leaf
cert must match one of the supplied fingerprints for a
MATCHED verdict; any single non-match is a
MISMATCH (no partial credit for multi-signer
APKs).
# Integrity only
rutken app.apk verify-signature
# Trust gating against a known-good release cert
rutken app.apk verify-signature \
--expect-signer AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:…
# Machine-readable
rutken app.apk verify-signature --format json
Expected text output:
Signature: v2
Integrity: VERIFIED
Trust: MATCHED a1b2c3...
Signers:
1. sha256=a1b2c3... self-signed=yes
subject: CN=...
issuer: CN=...
valid: 1690000000 .. 2020000000
Warnings:
- weak digest algorithm used: SHA-1
--format json emits the full
SigningInfo (including verification)
alongside the APK's sha256 and package.
Exit codes
This verb has its own exit convention, independent of
rules --fail-on:
0— every state except a tripped signature problem. This includes anUNVERIFIABLEintegrity result and aNOT CHECKED/ unsigned trust result: "could not check" is never treated as a policy-gate failure.2—IntegrityisFAILED, orTrustisMISMATCH.
The same verification powers rutken app.apk info's
appended Integrity: line (always on, empty anchor
set) and the opt-in
rules --verify-signature /
--expect-signer rules
(apk-signature-invalid /
apk-signature-untrusted).
Report formats
The analysis verbs (rules/scan,
compliance, sca,
correlate, batch,
image, cicd) share one
--format switch:
--format text | json | sarif | markdown | html | pdf
| gitlab-codequality | junit | ndjson
- text — human output, one line per finding; the default.
- json — a
{schema_version, tool, findings, coverage}envelope. - sarif — SARIF 2.1.0 for GitHub Code
Scanning. Pair with
--artifact-urito add aphysicalLocation(Code Scanning silently drops results that carry onlylogicalLocations). Coverage lands underruns[0].properties.analysisCoverage. - markdown — a rendered Markdown report (see below).
- html / pdf — rendered documents; PDF is
binary, so send it to
--outor redirect. - gitlab-codequality — GitLab Code Quality
JSON (requires
--artifact-uri). - junit — JUnit XML, one testcase per finding.
- ndjson — line-delimited SIEM events
(
batch --format ndjsonis the fuller-context path).
The Markdown report
--format markdown renders a self-contained
Markdown document — the format of choice for a PR description,
a wiki page, or a $GITHUB_STEP_SUMMARY. HTML and
PDF render the same content as shareable documents.
rutken app.apk rules --format markdown
rutken app.apk rules --format html > report.html
rutken app.apk rules --format pdf > report.pdf
HTML and PDF are deliberately open. They are rendered documents on purpose, not locked-down exports — you are expected to post-process, re-style, or embed them in your own reporting pipeline.
CI gating with --fail-on
--fail-on <critical|high|medium|low|info>
turns a scan into a build gate. If any finding on the
unfiltered set is at or above the given
level, the command exits 2. The gate verdict is
written to stderr only; stdout (your report) is unchanged, so
you still get the full artifact on a tripped gate.
# Exit 2 (non-zero) if any High or Critical finding exists
rutken app.apk rules --fail-on high || exit 1
# Gate while still producing the SARIF artifact
rutken app.apk rules --fail-on high --format sarif --out rutken.sarif
Related gating flags:
--baseline <path>/--baseline-out <path>/--baseline-delta-out <path>(rules, APK only) — gate only on NEW ∪ severity-escalated findings versus a committed baseline;--baseline-outwrites a fresh one.--min-severity <level>— floors the text view only. Machine formats and--fail-onalways see the full set.- On
correlate,--fail-onis AND-ed with--fail-on-status any|corroborated.
Exit-code map: 0 = clean (or gate
not tripped), 1 = tool/usage error,
2 = policy gate tripped. Keep these distinct in
CI: a 1 is a broken run, a 2 is a
real finding.
Accepting known findings — baselines
# 1. Establish a baseline once, from a known-good scan
rutken app.apk rules --baseline-out rutken-baseline.json
# 2. In CI from then on: gate only on NEW or escalated findings
rutken app.apk rules --baseline rutken-baseline.json --fail-on high
CI/CD platform actions
Rutken ships first-party integrations for the major CI
systems. Each wraps a rutken rules … (or
rutken batch --apk-dir …) invocation, surfaces
--fail-on's exit code as the step's own outcome,
and writes the report artifact before exiting so a
tripped gate never costs you the artifact.
- GitHub Actions — a composite action,
.github/actions/rutken-scan, wrappingrutken rules --format sarif --out <file>plus a Markdown$GITHUB_STEP_SUMMARY. Pairs withgithub/codeql-action/upload-sarif@v3; this is what makes--artifact-uriworth setting. - GitLab CI — a CI/CD Component,
templates/rutken-scan.yml(GitLab 16.6+), emitting Code Quality JSON (works on every tier, including Free).gate-allow-failure: truemakes a tripped gate (exit2) non-blocking via nativeallow_failure: exit_codes: [2], while a tool/usage error (exit1) still fails the pipeline. - Azure Pipelines — a steps template,
templates/azure-pipelines/rutken-scan.yml, emitting SARIF (consumed via the "SARIF SAST Scans Tab" extension) with an optional JUnit fallback.gateAllowFailure: truemaps a tripped gate toSucceededWithIssues. - Jenkins — a shared-library global step,
templates/jenkins/vars/rutkenScan.groovy, with SARIF consumed by the Warnings Next Generation plugin.gateAllowFailure: truemarks the buildUNSTABLErather thanFAILURE.
Each ships its own documentation alongside it with the full input/output reference and both consumption modes.
Putting the reason in front of a reviewer
The platform actions surface the gate as an exit code and a
report artifact, but not the reason inline on a PR.
rutken pr comment closes that last mile for
GitHub: it reads an already-computed baseline delta and upserts
a single marked comment on the pull request. It is fail-soft —
a comment failure never changes the gate verdict (warns, exits
0) unless you pass --require-comment.
# In CI, after the gated scan step:
rutken app.apk rules --baseline base.json --baseline-delta-out delta.json \
--format sarif --out rutken.sarif --fail-on high || GATE=$?
rutken pr comment --delta delta.json --repo me/app --pr 42
exit ${GATE:-0}
Gotchas & FAQ
- Why wasn't my fix applied?
- Three common reasons. (1) It's an advisory DEX code fix —
those are always listed, never auto-applied; use
edit-code. (2) It's behaviour-changing (exported/grant-uri) and you didn't pass--include-behaviour-changing, which trips the all-or-nothing gate and refuses the entire run. (3) No finding matched the requested transform. - Is Rutken's signer just
apksignerunder the hood? - No. It is Rutken's own implementation of APK Signature
Scheme v2. It resigns repacked, obfuscated, and split apps,
and Android installs the result normally. Signing is RSA /
RSASSA-PKCS1-v1_5with SHA2-256. - How do I pass my keystore password?
- Via
--keystore-pass-env(default env varRUTKEN_KEYSTORE_PASS) or--keystore-pass-file. There is deliberately no--keystore-passwordargv flag — passwords on argv leak viaps. A PKCS#12 keystore uses one password for both the store and its keys. - I get
INSTALL_FAILED_VERIFICATION_FAILUREon a signed APK. - A validly signed but modified app can still be
rejected by Play Protect on a stock device
(
apksigner verifypasses, but the on-device verifier blocks it). Turn the adb-install verifier off to sideload:adb shell settings put global verifier_verify_adb_installs 0, install, then restore it. That is the verifier, not a signing bug. - Why did
verify-signatureexit 0 when it couldn't verify? - By design: an
UNVERIFIABLEintegrity result and aNOT CHECKED/unsigned trust result both exit0— "could not check" is never a policy-gate failure. Only aFAILEDintegrity or aMISMATCHtrust exits2. - What's the difference between
fix --applyandpatch? fix --applybundles the whole manifest subset of a finding set into one merged plan (all-or-nothing, uses--out).patchapplies exactly one named manifest transform per run (uses-o). Both rebuild through the same width-preserving writer and sign with the same flags.
Only remediate and resign apps you own or are explicitly authorized to modify. Mutating verbs fail closed and leave the original untouched, but the output is your responsibility.