⚠️ Beta software. APEX is under active development. Findings, the JSON schema, CLI flags, and the on-device (ADB) features may change without notice, and false positives/negatives are expected. Do not rely on it as your sole security gate yet. Feedback and bug reports welcome.
Static analysis for Android/iOS packages (.apk, .aab, .xapk, .ipa)
Extracts the package, runs 15 analyzers, and reports security findings with a weighted score.
brew tap mlab-sh/apex https://github.com/mlab-sh/apex.git
brew install apexGrab a tarball for your platform from the latest release — available for macOS (x86_64, arm64) and Linux (x86_64, arm64) — then:
tar -xzf apex-*-<target>.tar.gz && sudo mv apex-*/apex /usr/local/bin/cargo build --release # binary at target/release/apexRequires a Rust toolchain and a C compiler (libusb is vendored & statically linked for the ADB features, so the binary is self-contained).
apex scan path/to/app.apk # formatted terminal report (default)
apex scan path/to/app.apk --json # also print JSON and write <stem>_report.json
apex scan path/to/app.apk --json -o report.json
apex scan path/to/app.apk --fail-on high # exit 1 if any finding >= high (CI/CD)
apex --versionFlags:
--show— formatted terminal output (implied when no other output flag).--json— emit the JSON report (and write it to a file).-o, --output <PATH>— JSON output path (default<stem>_report.json).--detail— print the proof/evidence (description + metadata) under each finding.--fail-on <critical|high|medium|low|info>— non-zero exit for CI when a finding at that severity or above is present.
Seized phones usually have USB debugging off and a locked screen, so live
ADB isn't an option — investigators work from an extraction that already exists.
apex analyze runs the full static pipeline over a directory tree of package
files, no device required:
apex analyze ./extraction # recurse, scan every .apk/.aab/.xapk/.ipa
apex analyze ./extraction --detail
apex analyze ./extraction --evidence ./case # + hashed, chain-of-custody manifest
apex analyze ./extraction --packages installed.txt # also match an app list vs stalkerware
apex analyze ./extraction --json -o reports/ --fail-on highIt prints a per-package report, an extraction-wide severity summary, a
top-risk triage table (worst packages first, stalkerware flagged), and the
list of stalkerware/spyware indicator matches (by declared package name, and
from an optional --packages installed-apps list). With --evidence it writes
an offline bundle whose manifest.json hashes every input file and every report
(see Forensic evidence bundle below). It reads a directory tree of package
files — unpack a proprietary container (e.g. Cellebrite UFDR) first.
Connect an Android phone in USB-debugging mode (authorize the RSA prompt), then:
apex device list # list installed third-party (non-system) apps
apex device posture # assess the device's own security posture
apex device scan # pull + scan every third-party app
apex device scan --package com.foo.bar # scan a single package
apex device scan --json -o reports/ # write <package>_report.json per app
apex device scan --detail --fail-on high
apex device shell "getprop ro.build.version.sdk" # raw shell passthrough (power-user)Uses adb_client's ADBUSBDevice for a direct USB connection (no adb
server required). pm list packages -3 enumerates non-system packages,
pm path resolves each base APK, which is pulled to a temp dir and run through
the same analyzers as apex scan. Options: --adb-key <PATH> for a custom ADB
private key (default ~/.android/adbkey), plus --json/-o/--detail/--fail-on.
Everything on-device runs over a non-root ADB shell; the few checks that benefit from root degrade gracefully when the data is unreadable.
Collects a structured, hashed extraction for offline analysis (feed it back to
apex analyze). Non-root and read-only; every artifact is recorded in the
evidence bundle.
apex device acquire -o ./case-42
apex device acquire --package com.foo.bar -o ./case-42
apex device acquire --no-logcat --no-usagestats -o ./case-42 # skip the large capturesOutput layout:
case-42/
device/ getprop, identifiers.json (serial, android_id, fingerprint, ABI…),
packages.txt (pm list -f -i -u), usagestats.txt (app-usage timeline),
accounts.txt, wifi.txt, bluetooth.txt, device_policy.txt, logcat.txt
apps/<pkg>/ base.apk + every split_config.*.apk, dumpsys_package.txt
manifest.json / manifest.json.sha256 / audit.jsonl
Deliberately out of scope (do not improvise these): SMS/MMS, call logs and
contacts (privacy-sensitive, legally governed), and app-private data under
/data/data (requires root). Use a validated, warrant-appropriate acquisition
tool for those.
Where scan inspects apps, posture inspects the device: it flags weak
device configuration and abuse vectors, scored with the same weighted model as
a package scan. Checks include Verified Boot / bootloader lock state, SELinux
mode, root indicators, storage encryption, security-patch age, developer
options / ADB / mock-location, a global HTTP proxy (AiTM interception point),
enabled accessibility services and notification listeners (the most-abused
capabilities), active device-administrator apps, and installed stalkerware
(exact package-name match against the
Echap indicator set).
Property-based signals (verified boot, root) are best-effort — they can be
spoofed — so they are reported as indicators, not attestation. Supports
--json/-o/--detail/--fail-on.
device posture and device scan accept --evidence <dir> to write a
tamper-evident record alongside the report, for investigative / chain-of-custody
use:
audit.jsonl— every ADB action taken, in order, each with a UTC timestamp, the SHA-256 of its raw output, byte count and duration.manifest.json— tool name/version, session start/end (UTC), device identifiers (model, serial, build fingerprint), the SHA-256 of every acquired artifact (pulled APKs) and of every report, and an explicit methodology / alteration statement describing what a live ADB acquisition touches.manifest.json.sha256— detached hash so the whole bundle is verifiable.
apex device posture --evidence ./case-42
apex device scan --evidence ./case-42This documents and hashes exactly what APEX did; it is not a forensic image and captures no app-private data, deleted content, or physical/file-system dump — use a validated acquisition tool for evidential imaging.
Re-checks an evidence bundle (from device posture/scan/acquire or offline
analyze) and confirms nothing changed since acquisition — recomputes the
manifest's own hash and re-hashes every file, matching by hash so it survives
the bundle being moved:
apex verify ./case-42 # exit 0 = VERIFIED, exit 1 = tampered/incompleteIt reports matched artifacts, any recorded-but-missing/altered files (integrity failure), extra files not in the manifest, and — for offline bundles — the external source inputs it can't re-check from the bundle alone.
Static analysis reports what an app requests; on device, scan additionally
reports what is actually granted / active — parsed from dumpsys package and
appops: permissions granted (not merely requested), install source
(sideloaded vs a known store), the DEBUGGABLE/TEST_ONLY runtime flags, and
high-risk app-ops (draw-over-other-apps overlay, all-files access, silent
install). A scanned package that matches the stalkerware set is flagged
critical. These findings carry the runtime category and fold into the app's
score alongside the static ones; pass --no-runtime to disable.
manifest, certificate, network, sast, api, permissions, binary
(ELF checksec), secrets (+ entropy), jar_aar, dataflow, cloud,
privacy, binary_advanced, platform, supply_chain.
Binary manifests are decoded with axmldecoder, native libraries with
goblin, and APK signatures / X.509 certificates with the RustCrypto cms /
x509-cert stack. Output (findings, security score, JSON schema) matches the
Python reference implementation, with one additive enrichment: findings whose
detected capability maps to an adversary technique also carry a mitre field
(MITRE ATT&CK for Mobile technique id, e.g. T1430, T1636.003,
T1638) so SOCs can pivot straight into ATT&CK. The mapping lives in one
auditable table (default_mitre in src/models.rs); control
weaknesses with no clean technique equivalent are left untagged rather than
force-mapped. Shown next to CWE/MASVS under --detail.
The privacy analyzer classifies detected third-party trackers with the
Exodus Privacy taxonomy (analytics / advertising / identification /
profiling / crash-reporting / push). The advertising, identification
(cross-app attribution) and profiling (session-replay) classes are surfaced as
high_risk in the PRIV_TRACKERS metadata — that's where the GDPR/CCPA
exposure sits — and the presence of a session-replay tracker escalates the
finding to high. The on-device / offline stalkerware check matches installed packages against a
bundled indicator set (exact package name) sourced from the
Echap stalkerware-indicators
project; refresh it with scripts/update-iocs.sh as the upstream list evolves.
Classifications are a bundled, offline table (no network call at scan time) covering the SDKs APEX fingerprints; extending it to the full Exodus corpus (network signatures, ~450 trackers) is the natural next step. Static analysis reports which ad/tracking SDKs are embedded — proving which are actually contacted at runtime needs dynamic analysis.
Prebuilt binaries are produced by the Release GitHub Actions workflow
(.github/workflows/release.yml), triggered
manually (workflow_dispatch). It:
- runs
cargo testas a gate, - builds a matrix of macOS (x86_64, arm64) and Linux (x86_64, arm64),
- packages each as
apex-<version>-<target>.tar.gz(binary + LICENSE + README), - computes sha256, regenerates
Formula/apex.rb, commits it, and publishes a GitHub Release with the tarballs.
To build/package locally (mirrors the CI):
scripts/build-release.sh # host target
scripts/build-release.sh --all # all four release targets (needs toolchains)libusb is compiled from vendored C sources and statically linked, so the resulting binaries have no external libusb/udev dependency.
