Documentation · Static analysis

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: exit 2 if any finding is at or above the level (critical, high, medium, low, info).
  • --out <path> — write the report to a file (required for the binary pdf format).
  • --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-on always see the full set.
  • --interprocedural / --interprocedural-params — opt into whole-program summary-table flow so weak-crypto-algorithm and taint-reaches-sink follow 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-uri adds source locations).
  • markdown / html / pdf — rendered documents (pdf is 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 — run sca update to 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 as rules.
  • --fail-on <severity> — CI gate, exit 2, identical semantics to rules.
  • --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; 0 disables 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-on gate (on rules/sca/correlate), a verify-signature verdict, or a clap argument-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, not rutken rules app.apk — the target path is the first argument, the verb is the second.
  • dump is huge. Its output scales with the whole IR (potentially gigabytes). Always redirect to a file.
  • sca needs a dataset. An empty cache is a hard error; run sca update or pass --auto-update.
  • Coverage is separate from the gate. Assert coverage.complete == true from --format json if you need "clean" to mean "fully analyzed," not just "nothing found."
  • Machine formats ignore --min-severity and color. The text floor is a display convenience; json/sarif and --fail-on always see the full set, and piped/file output is never colored.
  • --help is authoritative. Run rutken <verb> --help for the exact, current flag surface of any verb.