Static analysis, end to end
Rutken's static analysis (SAST) never touches a running device: it unzips an app, parses every DEX and resource, lifts the bytecode to an IR with control-flow graphs, data-flow and taint, runs a fixed rule set, and reports findings. This page is the complete CLI walkthrough — every static verb, the flags that matter, a runnable example, and the output shape you'll actually see. All of it is free in Community; none of it needs an account or a device.
The pipeline
Rutken is a layered, compiler-style pipeline: parser → IR → analysis → rules. Every static verb opens the target once, lets the engine build an intermediate representation (one for APKs, or a native representation for native binaries), and then either prints that IR or runs analysis passes over it. The CLI, GUI, REST API, and MCP server are all thin consumers of the same analysis layer — they produce identical findings for the same app because they call the same code.
The static verbs fall into three jobs:
- Inspection —
info,manifest,strings,classes,search,disasm,dump,stats: read the IR and print it. - Analysis —
rules,sca,compliance,concepts,callgraph,investigate: run passes over the IR. - Attestation —
verify-signature: check the app's cryptographic signature.
One structural detail runs through every example below:
the target path comes first, then the verb —
rutken <app.apk> <verb> [options]. The
file's magic bytes are sniffed, so the same command surface
also accepts native binaries — Linux ELF and Windows PE are
actively building (see Native targets).
Everything on this page is pure static analysis — no device, no emulator, no network (SCA dataset updates aside). It is safe to run on any app you have a copy of. Dynamic analysis (live capture, TLS unpin, runtime intervention) is covered on the main docs page.
Scan: rutken rules
rules is the primary SAST verb — the "scan" of
Rutken. It runs the whole pipeline end to end: decode the APK,
build the IR, run the analysis passes
(XREF, CFG, const-propagation, taint, class hierarchy), apply
the fixed rule registry, and print one finding per line, most
severe first, in a deterministic order.
# The full scan
rutken app.apk rules
Representative output (text, the default format):
[MEDIUM] android-debuggable: Application is debuggable [CWE-489 MASVS-RESILIENCE-4] id=android-debuggable-418a5d3d1fff15ad
[LOW] android-allow-backup: Application allows ADB/cloud backup [CWE-530 MASVS-STORAGE-2] id=android-allow-backup-36e3f0775314510e
[LOW] taint-reaches-sink: Untrusted input reaches Landroid/util/Log;->v (...MotionLayout$Model;->debugLayout @311) [CWE-117 MASVS-CODE-4] id=taint-reaches-sink-86acfea87ddba473
1 finding(s) below the severity floor were hidden (--min-severity; use --format json for everything).
Each line carries the severity, the rule id, a human title,
the CWE/MASVS taxonomy, and a stable per-finding
id= (offset-derived, so it survives re-scans of
the same build — this is what CI baselines key on). A clean app
prints No findings.
The flags that matter
--format <fmt>— output format:text(default),json,sarif,markdown,html,pdf,gitlab-codequality,junit,ndjson. See Formats & CI gating.--fail-on <severity>— CI gate: exit2if any finding is at or above the level (critical,high,medium,low,info).--out <path>— write the report to a file (required for the binarypdfformat).--rule <id>/--exclude-rule <id>— run only, or all-but, the named rules (repeatable; an unknown id is an error, never a silent no-op).--list-rules— print the full rule catalog (id, title, default severity, taxonomy) and exit without opening the APK.--min-severity <level>— floors the text view only; machine formats and--fail-onalways see the full set.--interprocedural/--interprocedural-params— opt into whole-program summary-table flow soweak-crypto-algorithmandtaint-reaches-sinkfollow values through callee return values and parameters. Off by default: this is the most expensive pass in the tool. Truncating its caps only loses findings (sound), never invents them.--baseline <path>/--baseline-out <path>— gate on what is new or escalated versus a committed baseline instead of everything that exists (see below).
Inspect the rule catalog
--list-rules never touches the app, so it's the
fastest way to see what Rutken checks and at what severity:
rutken app.apk rules --list-rules
android-debuggable: Application is debuggable [CWE-489 MASVS-RESILIENCE-4]
severity: MEDIUM
platform: android
android:debuggable="true" in the manifest lets any locally installed app attach a debugger ...
weak-crypto-algorithm: Weak crypto algorithm [CWE-327 MASVS-CRYPTO-1]
severity: HIGH
...
taint-reaches-sink: Untrusted input reaches a sensitive sink
severity: varies
...
A realistic scan
# SARIF for GitHub Code Scanning, written to a file, gating the build on HIGH
rutken app.apk rules --format sarif --out report.sarif --fail-on high
The report always lands in the file; the gate verdict is printed to stderr and the process exit code carries the pass/fail — stdout (and the SARIF file) are identical whether the gate trips or not.
How it works
Findings are not grep hits. Rules like
weak-crypto-algorithm resolve the algorithm name
via constant propagation; taint-reaches-sink walks
a data-flow graph from untrusted Intent/Bundle/Parcel sources
to logging, file-path, and WebView sinks. Severity is a
property of the rule (some, like taint and the WebView bridge,
vary per finding). The result set is sorted by a deterministic
total order so two runs of the same build produce byte-identical
output.
Formats & CI gating
Severity ranking
Every finding carries one of five severities. They rank, high to low:
critical > high > medium > low > info
--fail-on <level> trips when any
finding is at or above that level. The gate reads the full,
unfiltered finding set — --min-severity hides lines
from the text view but never changes what the gate sees.
Output formats
Shared across rules, sca, compliance, and the correlation/container verbs:
- text — one line per finding; the default, human-readable.
- json — a
{schema_version, tool, findings, coverage}envelope (machine-stable). - sarif — SARIF 2.1.0 for GitHub Code
Scanning (
--artifact-uriadds source locations). - markdown / html / pdf — rendered
documents (
pdfis binary, so it needs--out). - gitlab-codequality — GitLab Code Quality
JSON (requires
--artifact-uri). - junit — JUnit XML, one testcase per finding.
- ndjson — line-delimited events for SIEM ingestion.
The JSON envelope (abbreviated):
rutken app.apk rules --format json
{
"schema_version": 1,
"tool": { "name": "rutken", "version": "0.2.3" },
"findings": [
{
"id": "android-debuggable-418a5d3d1fff15ad",
"rule_id": "android-debuggable",
"title": "Application is debuggable",
"severity": "medium",
"confidence": "high",
"description": "android:debuggable=\"true\" ...",
"location": null,
"evidence": ["application.debuggable = true"],
"taxonomy": { "cwe": ["CWE-489"], "masvs": ["MASVS-RESILIENCE-4"] }
}
],
"coverage": { "complete": true, "skipped_dex": [] }
}
Gating in CI
--fail-on alone fails the build for every finding
at or above the threshold — including ones the team has already
triaged. A baseline makes the gate fire only on
what is new or has gotten worse:
# 1. record what is accepted today (commit this file)
rutken app.apk rules --baseline-out ci/rutken-baseline.json
# 2. in CI from then on -- gate on NEW or escalated findings only
rutken app.apk rules --baseline ci/rutken-baseline.json --fail-on medium
A finding whose severity rises keeps its id but is still gated,
and the report marks it: <- ESCALATED from info.
Every baseline run also prints one stderr summary line —
baseline: N new, M escalated, K unchanged, J
remediated — even on a green run.
A green --fail-on result is not, by itself, proof
an app is clean. The gate does not read coverage: an app whose
DEX files could not all be parsed can return zero findings and
exit 0 while coverage.complete is
false. Automation that needs the stronger
guarantee should also assert
coverage.complete == true from
--format json.
rutken dump
dump serializes the analyzed IR — the low-level
parsed structures behind every other verb. Reach for it when
manifest or strings aren't enough and
you need ground truth, not a summary.
# Full IR as JSON -- always redirect to a file
rutken app.apk dump --json > ir.json
Flags:
--json— machine-readable serialization (otherwise a human-readable report).--raw— include raw/unprocessed detail.--include strings— add the string pools to the report.
How it works & when to use it
dump walks the whole IR, not just findings, so its
output scales with the size of the app — it can reach multiple
gigabytes and is streamed to stdout. Always redirect it
to a file; never print it to a terminal. Use it to
diagnose a false positive, confirm what the decoder actually
recovered, or feed another tool. For a quick human look at the
manifest, prefer the dedicated manifest
verb below.
rutken manifest
A focused, human-readable view of the decoded
AndroidManifest.xml: package, components, and
exported surfaces.
rutken app.apk manifest
<manifest package="com.example.loginbypass">
<uses-permission android:name="...DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" />
<application android:label="@string/app_name" android:debuggable="true" android:allowBackup="true">
<activity android:name="com.example.loginbypass.MainActivity" android:exported="true" />
</application>
</manifest>
How it works
It renders the IR's decoded binary AXML back to readable XML.
There is no --format — this verb is for eyes, not
pipelines.
The manifest printer is deliberately
summary-level — it surfaces the application element and
activities/components, not every attribute of every node. If a
manifest attribute you expect is missing from this view, it is a
printer limitation, not a decoding limitation: diagnose from
rutken app.apk dump, which has the full picture.
rutken sca
Software composition analysis: fingerprint the app's bundled
libraries (Java/Kotlin components and native
.so files) and match them against the cached
vulnerability dataset, reporting one finding per matched
component with its known CVEs.
# Fetch/refresh the dataset in the same run, and emit an SBOM alongside
rutken app.apk sca --auto-update --sbom sbom.json
Flags:
--auto-update— runsca updateto completion before scanning. Without it, the scan is fully offline and never touches the network.--cache-dir <dir>— override the dataset cache root.--format <fmt>— same format set asrules.--fail-on <severity>— CI gate, exit2, identical semantics torules.--sbom <path>— also write a CycloneDX 1.6 JSON SBOM of every matched component (a side artifact; it does not change stdout or the gate).--max-age-days <n>— warn on stderr (never error) if the cached dataset is older than this;0disables the check.
How it works
The populated dataset is the input. Prime it once with
rutken sca update (a no-APK verb), or pass
--auto-update to fetch it inline. Dataset
provenance (version, cache age, skipped record counts) is always
printed to stderr, regardless of --out.
An empty cache is a hard error, not "zero
findings" — Rutken refuses to imply an app is dependency-clean
when it simply had nothing to match against. Run
rutken sca update first, or pass
--auto-update.
rutken verify-signature
Check the APK's cryptographic signature. Rutken reports integrity (does the content match what was signed?) and trust (is the signer who you expect?) as two separate claims.
rutken app.apk verify-signature --expect-signer AA:BB:CC:...
Flags:
--expect-signer <SHA256>— the expected signer leaf-certificate fingerprint (hex; repeatable). Without it, trust is reported as "not checked" — Rutken never invents an expected signer.--format text|json.
How it works
It recomputes the content digest, verifies the signer's
signature, and — with --expect-signer — compares
every signer's leaf certificate against your expected
fingerprint(s). A self-signed certificate is the normal,
correct state for a legitimate Android app and is never
reported as a problem. (The info verb runs the same
integrity check as a one-line summary.)
Other inspection verbs
These read the IR and print it — fast ways to orient yourself in
an app before or after a rules scan. None take a
--format beyond what's noted; all take the target
first.
info — one-screen identity
rutken app.apk info
SHA256: eb1ad4b4783f797bbd8266173493c89cc8d286ece6d59a3b408d2bfbbf9f1437
Size: 6MB
DEX files: 5
Classes: 7992
Signing: v1=yes v2=yes v3=yes ...
Integrity: verified
Package: com.example.loginbypass
Min SDK: 26
Target SDK: 36
Hash, size, DEX/class counts, signing scheme, package + SDK levels, and an integrity check in one screen.
stats — analysis diagnostics
rutken app.apk stats
Classes: 7992
Methods: 57876
Instructions: 842201
XREF edges: 278606
CFG blocks: 196702
Analysis time: 1.751s
Peak RAM: 191 MB
Skipped DEX files: none
Coarse counts, timing, and peak RSS (Linux-only; n/a
elsewhere). Human-readable only — the line format is not
machine-stable. This is also where you'll see any DEX files the
decoder had to skip.
strings — the string pool
rutken app.apk strings --grep http
Lists decoded DEX string-pool entries; --grep filters by substring.
classes — every class
rutken app.apk classes
Lists every class across all DEX files.
search — locate a symbol
rutken app.apk search decrypt
Case-insensitive substring search over strings, class names, method names, and disassembled instructions — the fastest way to find where something lives.
disasm — read the bytecode
rutken app.apk disasm LoginActivity
Disassembles every method of classes whose name contains the
query. Its offset format is exactly what the (advanced,
paid-tier) edit-code verb consumes.
Native targets
The same command surface applies the analysis to native binaries — the sniffer routes a non-APK file to the native pipeline automatically. Linux ELF and Windows PE are actively building, and macOS (Mach-O) is on the roadmap.
rutken libfoo.so info
rutken libfoo.so rules --lift --format json
On a native target, rules --lift additionally
recovers functions and CFG so linkage rules are
reachability-qualified; a tripped budget yields a partial result
with the uncovered region recorded, never an error. Native binaries also support
sca for dependency inventory. APK-only flags
(interprocedural, chains, signature) are rejected with a named
error rather than silently ignored, and a Mach-O target is
recognized but deliberately fail-closed.
Exit codes & gotchas
Every verb uses the same three-way exit convention:
0— success.1— an operational or usage error ("the tool could not run").2— a policy outcome: a tripped--fail-ongate (onrules/sca/correlate), averify-signatureverdict, or aclapargument-parsing error.
Exit 2 is not unique to the gate. A malformed
command line (an unknown flag, a bad value) also exits
2, because that is clap's default and
Rutken never overrides it. CI that branches on 2 as
"gate tripped" should also confirm the invocation itself was
well-formed — the gate verdict is always written to stderr
(gate tripped: N finding(s) at or above <level>),
a usage error is not.
A few more things worth knowing:
- Target first. It's
rutken app.apk rules, notrutken rules app.apk— the target path is the first argument, the verb is the second. dumpis huge. Its output scales with the whole IR (potentially gigabytes). Always redirect to a file.scaneeds a dataset. An empty cache is a hard error; runsca updateor pass--auto-update.- Coverage is separate from the gate. Assert
coverage.complete == truefrom--format jsonif you need "clean" to mean "fully analyzed," not just "nothing found." - Machine formats ignore
--min-severityand color. The text floor is a display convenience;json/sarifand--fail-onalways see the full set, and piped/file output is never colored. --helpis authoritative. Runrutken <verb> --helpfor the exact, current flag surface of any verb.