{"id":"1f10afd0-65bd-4510-a954-bc317f2fb0e0","slug":"apsa","kind":"mcp","name":"apsa","summary":"Evidence-first Android and iOS security audits with CLI, TUI, MCP, public vulnerability intelligence, and OWASP-aligned evidence.","owner":"ictechgy","repoUrl":"https://github.com/ictechgy/apsa","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-06T01:59:20.000Z","addedAt":"2026-10-14T05:38:10.309Z","logoUrl":"https://avatars.githubusercontent.com/u/39452092?u=9f713c98df9d5f518accbfb35259f184846d0281&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["utilities"],"tags":["android","cli","ios","mobile-security","owasp","security-audit","tui"],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-14T16:06:30.111Z","sample":false,"url":"https://conduits.dev/tools/apsa","language":"Python","transports":["stdio"],"install":{"pypi":"apsa"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# APSA\n\n**Evidence-first security audits for Android and iOS.**\n\n[English](https://github.com/ictechgy/apsa/blob/main/README.md) · [한국어](https://github.com/ictechgy/apsa/blob/main/README.ko.md)\n\nThis English README is the source of truth. The Korean README follows it.\n\n<!-- mcp-name: io.github.ictechgy/apsa -->\n\nAPSA helps developers and security teams audit their own mobile apps. It inspects source code and APK/AAB/IPA builds, correlates public vulnerability information, and keeps evidence, coverage, and report history together. Use the CLI, terminal UI, or the same audit engine through MCP and reusable skills.\n\nPronounced **“ap-sah”**; Korean name **앱사**. The name connects “app + audit” with **App Security Audit**. APSA combines the earlier Quaygate lint engine and Mobile Audit workflows in one package.\n\nAPSA 1.1 adds bounded model report pages, portable approved baselines, exported policy decisions, and explicit source module/configuration selection. See [model and team workflows](docs/MODEL_WORKFLOWS.md) for the contract and [release sequence](docs/ROADMAP.md) for later analysis milestones.\n\nMixed unsupported source languages and truncated pattern results are incomplete. Declared Gradle versions remain candidates until resolved-build evidence is supplied. OS-CVE correlation reports a bounded recent window, so feed freshness alone cannot satisfy historical coverage.\n\nAPSA 1.2 adds AAB base-manifest/module DEX analysis, embedded IPA Mach-O\nmetadata, bounded Objective-C `.m` candidates and parser OS isolation that is\non by default where it can start.\nEvery AAB audit stays partial, and embedded IPA metadata does not authenticate\nsignatures. See [analysis scope and limits](https://github.com/ictechgy/apsa/blob/main/docs/NEXT_ANALYSIS.md) and the\n[independent holdout](https://github.com/ictechgy/apsa/blob/main/benchmarks/NEXT_RESULTS.md).\n\n**Upgrading from 1.1.** With the default `APSA_PARSER_SANDBOX=auto`, the parser\nruns under macOS Seatbelt or Linux bubblewrap after an input-free activation\nprobe succeeds. Without a backend, the report records\n`parser_isolation.state: unavailable` and parses with resource limits only. If\nthe backend is present but cannot start (for example inside another sandbox or\nwith blocked user namespaces), the report also records the attempted backend\nand probe reason and adds an inventory warning. `required` refuses such audits; `off` skips the backend. A parser\nfailure after isolation has started aborts the audit without an unsandboxed\nretry. Dependencies declared only in a Gradle version catalog no longer receive\nCVE matches until build usage or resolved versions are supplied. New rules\n(`AST-CRYPTO-ECB`, `AST-CRYPTO-WEAK-HASH`, `AST-SQL-CONCAT`,\n`OBJC-CRYPTO-WEAK-HASH`, `OBJC-WEBVIEW-UNTRUSTED-REQUEST`) can add findings\nrelative to existing 1.1 baselines.\n\nAPSA 1.3 links Gradle version-catalog aliases to the shipped dependency\nconfigurations that use them and reads application-module `gradle.lockfile`\ncoordinates as resolved versions. It also decodes AAB feature-module manifests,\nrecomputes Mach-O CodeDirectory page and entitlement hashes without\nauthenticating the signature, adapts more Swift and Objective-C syntax, and can\nbackfill an explicit range of Android security bulletins with SoC vendor and\nvendor patch-level context. See the\n[dependency evidence results](https://github.com/ictechgy/apsa/blob/main/benchmarks/DEPENDENCY_RESULTS.md),\nthe [independent source pairs](https://github.com/ictechgy/apsa/blob/main/benchmarks/FPFN_RESULTS.md)\nand [analysis scope and limits](https://github.com/ictechgy/apsa/blob/main/docs/NEXT_ANALYSIS.md).\n\n**Upgrading from 1.2.**\n\n- Catalog aliases referenced from shipped configurations become `declared`\n  candidates and can receive CVE matches again; unreferenced, test-only and\n  build-tooling aliases stay unresolved. Application-module lockfile coordinates\n  are exact, so their matches are `version-affected`, and the default policy\n  fails on high and critical ones; other modules' lockfile coordinates stay\n  `declared` candidates. A declared candidate that every shipping application\n  resolved is superseded and leaves CVE correlation. Partial inputs keep every\n  candidate, and with unparsed project references only an application's own\n  declarations can be superseded.\n- Lockfile assumptions: APSA does not check that dependency locking is enabled\n  or that the lockfile is current, and it merges the release runtime classpaths\n  of every application module (flavors and wear, TV or automotive apps).\n  `--source-module` scans only that module's directory, so a root version\n  catalog and library-module declarations outside it are not read.\n- Baselines and waivers: a lockfile-resolved finding has a new location\n  (`gradle.lockfile`) and status, so `only_new` treats it as new and waivers for\n  the old finding ID no longer match. Re-approve baselines after upgrading.\n- `--online` sends catalog aliases used in shipped configurations and the\n  release-runtime lockfile coordinates of every module, transitive ones and\n  private group IDs included, to OSV; there is no exclusion list. One run\n  queries at most 100 packages: those without an OSV result from the last day\n  first, and among them build-file declarations before lockfile coordinates in\n  lockfile order. Measured application lockfiles hold 179–412 shipped\n  coordinates, so a single run leaves the rest `not-run`: a required\n  `DEPENDENCY-CVE` rule cannot pass, the online audit is incomplete and the\n  default `fail_on_partial` policy exits 3. Repeated runs within a day advance\n  through the remainder, and a package with an OSV result from the last day\n  counts as checked. Unresolved catalog aliases and ecosystems OSV does not\n  support are reported as online errors on every run, so they keep the online\n  audit incomplete however many runs follow.\n- IPA reports gain `BINARY-IOS-CODE-INTEGRITY`. Consistent hashes do not prove\n  authenticity, because a modified binary that was re-signed is consistent. A\n  mismatch adds a warning, not a finding; App Store encrypted and unsigned\n  executables stay `not-run`.\n- `intel sync` refreshes only the recent Android bulletin window; use\n  `intel backfill` for older months. Records cached by 1.2 derive their vendor\n  scope from the component name; entries outside chipset and kernel sections use\n  the platform patch level only. AAB feature-module installation stays unknown.\n- Adapted Swift and Objective-C files stay partial, and an Objective-C function\n  overlapping a normalized preprocessor conditional is skipped as uncertain.\n  Unmodified 1.2 skills upgrade with `apsa skill install`.\n\nAPSA 1.4 is easier to put in front of developers and coding agents. A GitHub\nAction uploads SARIF to code scanning with stable fingerprints, security\nseverity, MASWE tags and evidence status; every report carries an\n[OWASP MASWE v1.0](https://mas.owasp.org/MASWE/) coverage matrix that names the\nweaknesses APSA did not assess; and the MCP server is packaged for the MCP\nRegistry and as a Claude Code plugin, with a documented\n[MCP security model](https://github.com/ictechgy/apsa/blob/main/docs/MCP_SECURITY.md).\n\n**Upgrading from 1.3.**\n\n- SARIF exports now list a descriptor for every rule with results (help,\n  `security-severity`, precision, MASVS/MASWE tags), add `partialFingerprints`\n  from the finding identity, and report not-run and partial coverage as tool\n  execution notifications. Candidate findings are `warning` rather than `error`\n  and their rules are tagged `candidate`. `--sarif-root` makes locations\n  repository-relative; URIs are percent-encoded, and APK/AAB/IPA members and\n  files outside the target appear as logical locations on the target. Code\n  scanning may therefore show re-keyed alerts once.\n- New source checks (exported components, backup, `targetSdk`, privacy\n  manifests, ATS exceptions, hard-coded secrets, insecure randomness, trust-all\n  TLS, external storage, Java deserialization, more SQL sinks, mutable\n  `PendingIntent`) add findings and coverage entries. A policy with\n  `required_rules` or a severity gate can fail where 1.3 passed; review the first\n  1.4 report before tightening gates.\n- Rule metadata gains MASWE v1.0 identifiers; model context and `capabilities`\n  gain a `maswe` section, and Markdown reports a MASWE coverage table. Reports\n  derived by `reports ingest` add `external_inputs` and findings with\n  `origin: external`; existing fields keep their meaning.\n- The MCP server adds `verify_finding`, `specs_validate`,\n  `reports_ingest_sarif`, `reports_checklist` and `reports_history`.\n  `tool_manifest_sha256` now covers a normalized APSA-owned surface, so its\n  value changes; update any pinned hash. Unmodified 1.3 skills upgrade with\n  `apsa skill install`.\n\nAPSA 1.5 widens what a source scan looks for. New checks cover the OWASP\nMASWE weaknesses behind most MASTG demo misses: broken ciphers and implicit\nECB, key material and IVs built from constants, short (512–1536-bit) RSA, DSA\nand DH key sizes and CryptoKit `Insecure` digests; biometric results not bound to a key,\ndevice-credential fallback and keys that survive new enrollment; TLS below\n1.2, iOS connections outside App Transport Security, and user CAs or cleartext\nin the network security configuration; unverified App Links, intent\nredirection and implicit internal Intents; path traversal and keyed\nunarchiving without secure coding; and Safe Browsing turned off, UIWebView and\nWKWebView file access. On a blind holdout of 24 public vulnerable/fixed pairs\nlabeled by MASWE weakness, APSA 1.5.0 found 7 of the 21 scored in-scope pairs\nat the labeled lines (Wilson 95% 17–55%). Two of the seven come from rules new\nin 1.5 and five from rules 1.4 already had; the fixed-side FP upper bound is 4\nof 21, and a 22nd in-scope pair failed to scan. See the\n[blind pair results](https://github.com/ictechgy/apsa/blob/main/benchmarks/BLIND_PAIRS_RESULTS.md)\nand [analysis scope and limits](https://github.com/ictechgy/apsa/blob/main/docs/NEXT_ANALYSIS.md).\n\n**Upgrading from 1.4.**\n\n- New rules add findings and coverage entries: `AST-CRYPTO-WEAK-CIPHER`,\n  `IOS-CRYPTO-WEAK-CIPHER`, `AST-CRYPTO-HARDCODED-KEY`, `AST-CRYPTO-STATIC-IV`,\n  `SOURCE-WEAK-KEY-SIZE`, `SOURCE-BIOMETRIC-EVENT-BOUND`,\n  `SOURCE-BIOMETRIC-FALLBACK`, `SOURCE-BIOMETRIC-ENROLLMENT`,\n  `SOURCE-WEAK-TLS-VERSION`, `IOS-ATS-BYPASS-API`, `ANDROID-NSC-USER-CA`,\n  `ANDROID-DEEPLINK-AUTOVERIFY`, `AST-INTENT-REDIRECTION`,\n  `AST-IMPLICIT-INTENT`, `IOS-WEBVIEW-FILE-ACCESS`,\n  `WEBVIEW-SAFE-BROWSING-OFF`, `IOS-UIWEBVIEW`, `AST-PATH-TRAVERSAL` and\n  `IOS-INSECURE-UNARCHIVE`. Candidates are not gated by default. The new\n  configuration-confirmed findings (`ANDROID-NSC-USER-CA`, and\n  `ANDROID-CLEARTEXT` from the network security configuration) count under the\n  default `allowed_statuses`, but medium and low thresholds are off by default.\n  Policies that gate candidates or medium/low findings, and `only_new`\n  baselines, will see them.\n- The MASWE matrix changes: MASWE-0003, 0013, 0020, 0021 and 0022 leave\n  not-assessed (they are not-run for APK and IPA, which these source checks do\n  not read), and `ANDROID-NSC-CONFIG` adds coverage to MASWE-0026 and 0027.\n- Existing rules find more: `AST-CRYPTO-ECB` also reports a bare `\"AES\"`\n  transformation (its title is now \"AES in ECB mode is selected\"; fingerprints\n  are unchanged), `AST-CRYPTO-WEAK-HASH` reports CryptoKit `Insecure.MD5` and\n  `Insecure.SHA1`, `AST-SQL-CONCAT` reads `SQLiteQueryBuilder.query`, the\n  groupBy/having/orderBy/limit arguments and nested exported providers, and\n  `SOURCE-INSECURE-RANDOM` runs on Swift and Objective-C (its coverage on\n  iOS-only trees changes from not-applicable to checked).\n- The AST engine now follows Kotlin `let`, `also`, `use`, `apply`, `run` and\n  `with` lambdas, so every structural rule can report code inside them. A\n  `?.let` lambda, or one with `return@`, merges like a branch that may not run;\n  a reassignment inside a lambda that always runs can now also remove a 1.4\n  finding. Other lambdas and Swift closures are still not analyzed.\n- `ANDROID-CLEARTEXT` also comes from the referenced network security\n  configuration (configuration-confirmed, so code scanning rates it). The\n  manifest's `usesCleartextTraffic` candidate is dropped when that\n  configuration was parsed, minSdk is 24 or higher, and the configuration states\n  `cleartextTrafficPermitted` or targetSdk is 28 or higher. Findings from a\n  configuration that several source sets define are candidates.\n- New coverage states can make an audit incomplete: `ANDROID-NSC-USER-CA` and\n  `ANDROID-NSC-CONFIG` are partial when a referenced configuration cannot be\n  parsed, is missing from the scanned sources or is a build placeholder, and\n  checks that stop at their match budget report partial. With the default\n  `fail_on_partial`, such a scan fails the gate and cannot be exported as a\n  baseline. Compiled AAB configurations are not-run.\n- Text rules no longer count binary XML from packages as checked, so\n  `WEBVIEW-SAFE-BROWSING-OFF` is not-run on APKs. The rules resource may carry\n  `case_sensitive` and `once_per_file`; inventory gains `deep_links[].browsable`\n  and `network_security_parsed`. Unmodified 1.4 skills upgrade with\n  `apsa skill install`; the MCP tool manifest hash is unchanged.\n\n**Upgrading from 1.5.0.** 1.5.1 audits large source trees partially instead\nof refusing them and fixes a slowdown on Android translations.\n\n- A source tree over the text staging budgets (64 MiB, 12,000 files) no longer\n  fails with an input-budget error (exit 1). Files are staged by kind: app\n  code and configuration, other shipped text, localized or qualified Android\n  `res/values-*` files, then tests. `inventory.input_snapshot.omitted` counts\n  what was left out, and the audit is incomplete (exit 3). With the default\n  `fail_on_partial` the gate still fails, and a partial report cannot become a\n  baseline. The 100,000-entry and 64-level budgets also stop reading instead\n  of refusing.\n- If app code, configuration or other shipped text is left out\n  (`app_scope_complete: false`), not-applicable coverage becomes partial too\n  and `DEPENDENCY-CVE` gains a partial entry (unless `--sbom` supplies the\n  dependencies), so required rules cannot pass\n  even with `fail_on_partial = false`.\n- SARIF from such an audit carries a warning notification and\n  `run.properties.inputSnapshot`. A workflow with `fail-on-incomplete: \"false\"`\n  now passes with a warning and uploads partial results where 1.5.0 failed;\n  code scanning sees no results for omitted files. Scan a narrower `path`\n  (one module per job, with its own `category`) for complete results. MCP\n  report context gains `input.input_snapshot`.\n- In 1.5.0, `strings.xml` files with many escaped apostrophes (`\\'`), common in\n  French and Italian translations, made `WEBVIEW-SAFE-BROWSING-OFF` slow enough\n  to hit the parser timeout (about 150 s for a 694 KiB file). XML files are\n  now cleaned of `<!-- -->` comments only, so `//` in XML text no longer hides\n  a match. Comment cleaning also stays linear on unterminated strings and\n  comments; a `/*` with no closing `*/` after it is read as text.\n- Unmodified 1.5.0 skills upgrade with `apsa skill install`; the MCP tool\n  manifest hash is unchanged.\n\n## What it checks\n\n| Area | Available checks |\n| --- | --- |\n| Source code | Java/Kotlin/Swift AST analysis; bounded Objective-C `.m` candidates; WebView and deep-link patterns; Manifest, Info.plist, storage, and dependency inspection, including Gradle catalog usage and application lockfiles; exported components, backup and targetSdk; Apple required-reason APIs against privacy manifests; known credential formats (masked); weak random for security values; mutable implicit PendingIntents; trust-all TrustManagers and hostname verifiers, unevaluated iOS server trust and weakened ATS exception domains; external storage writes; Java deserialization; (1.5) broken ciphers, constant keys and IVs, short RSA keys, biometric binding and fallback, TLS versions, iOS APIs outside ATS, network security configuration, App Links, intent redirection and implicit Intents, path traversal, keyed unarchiving, Safe Browsing, UIWebView and WKWebView file access |\n| Android builds | DEX calls and constant flow; AAB base and feature-module manifests and module DEX (always partial); resources and network configuration; exported components/providers; signing-block and v1 certificate evidence; ELF hardening |\n| iOS builds | Mach-O headers, including embedded framework/extension metadata; CodeDirectory page and entitlement hash integrity (not signature authentication); limited entitlement/configuration checks (embedded XML entitlements, ATS exceptions, provisioning indicators); PIE, canary, and string evidence |\n| Public intelligence | Apple/Android advisories with bounded Android bulletin backfill, CVE, CISA KEV, OWASP guidance, and OSV dependency correlation |\n| Reports and CI | SQLite history, comparison and reassessment, JSON/Markdown/SARIF export, coverage requirements, and expiring waivers |\n| Runtime | Prepared scenarios for owned Android test apps and iOS simulator apps; physical iOS devices are unsupported |\n| Model integration | stdio MCP tools/resources and packaged skills, without a required model provider or LLM API key |\n\nFindings distinguish `candidate`, `configuration-confirmed`, `version-affected`, and `runtime-confirmed` evidence. `coverage` and warnings show what actually ran. A signing block does not prove signature authenticity, and an affected dependency version does not prove exploitability. A scan with no findings does not establish that the whole app is secure.\n\nOWASP mappings describe relevant checks; APSA does not certify MASVS compliance or implement every MASTG test. Public advisories cannot reveal undisclosed zero-days. App files alone cannot establish a device's OS patch state. See [OWASP coverage](https://github.com/ictechgy/apsa/blob/main/docs/OWASP_COVERAGE.md) and the [support matrix](https://github.com/ictechgy/apsa/blob/main/docs/SUPPORTED_MATRIX.md), both currently in Korean, for tested scope and limits.\n\n## Install and run\n\nInstall **uv** on **macOS or Linux**. APSA targets **CPython 3.11 and 3.12**; not every host/Python combination has been tested (see the [support matrix](https://github.com/ictechgy/apsa/blob/main/docs/SUPPORTED_MATRIX.md)). The examples select Python 3.12, which uv can download if needed. Initial installation can use the network; scans can then use local inputs and cached intelligence.\n\nInstall the published package from [PyPI](https://pypi.org/project/apsa/):\n\n```sh\nuv tool install --python 3.12 apsa==1.5.1\napsa --version\napsa doctor --json\napsa demo --out ./apsa-demo\n```\n\nThe package provides `apsa` and the compatibility aliases `quaygate` and `mobile-audit`. If the command is not found, run `uv tool update-shell` and open a new terminal. [GitHub Releases](https://github.com/ictechgy/apsa/releases) provides the wheel, source distribution, checksums, and a verified bundle with locked requirements, attribution, and the build manifest.\n\nFor development or installation with the repository's locked dependencies, use a checkout:\n\n```sh\ngit clone https://github.com/ictechgy/apsa.git\ncd apsa\nuv sync --locked --python 3.12\nuv run --locked apsa doctor --json\nuv run --locked apsa demo --out ./apsa-demo\n```\n\n`doctor` checks parsers, optional device tools, and offline readiness. `demo` writes an intentionally vulnerable example and scans it; choose a new output directory.\n\nTo register the checkout's commands on your PATH:\n\n```sh\nuv tool install --editable . --force --python 3.12 --constraints requirements-release.txt\napsa --version\n```\n\nThis registers `apsa` and the compatibility aliases `quaygate` and `mobile-audit`. `--force` replaces existing tools with those command names. An editable installation depends on this checkout; keep it in place. If the command is not found, run `uv tool update-shell` and open a new terminal. The examples below assume `apsa` is on PATH. Without a global installation, prefix them with `uv run --locked` from the checkout.\n\n```sh\napsa scan /path/to/owned/mobile-project\napsa scan /path/to/owned/app.apk\napsa scan /path/to/owned/app.ipa --sbom /path/to/build.cdx.json\napsa tui\n```\n\nUse a CycloneDX JSON SBOM from the actual build to improve dependency correlation. `tui`, or `apsa` without arguments, opens the terminal interface. Run `apsa COMMAND --help` for options.\n\n## Public intelligence and network use\n\nA default `scan` reads local files and cached intelligence without uploading source code or builds. Public-feed collection and OSV queries use the network:\n\n| Command | Network behavior |\n| --- | --- |\n| `apsa scan TARGET` | Uses local inputs and cached intelligence |\n| `apsa intel sync` | Fetches public vulnerability sources |\n| `apsa intel watch` | Polls public sources and reassesses saved inventories; does not reread app files or implicitly query OSV |\n| `apsa intel backfill --source android --since YYYY-MM` | Fetches an explicit, bounded range of monthly Android security bulletins |\n| `apsa scan TARGET --online` | Sends discovered dependency names and versions to OSV, including Gradle catalog aliases used in shipped configurations and release-runtime lockfile coordinates of every module; at most 100 packages per run, unchecked build-file declarations first |\n| `apsa intel watch --online` | Also sends saved dependency names and versions to OSV, with the same budget |\n\n```sh\napsa intel sync\napsa intel status\napsa intel watch --interval 900\napsa scan /path/to/owned/app --online\n```\n\nWatch polls every 900 seconds by default, with a 60-second minimum; it is not a push stream. It runs until interrupted unless `--cycles` sets a finite number of polls. Reassessment saves a new snapshot when findings, coverage, or intelligence state changes. Inspect feed freshness, failures, and pending CVE processing with `intel status`. Fetching a CVE document and completing its processing are separate states. The default pending-item policy allows no backlog; use `intel_max_pending` to set an explicit allowance.\n\n```sh\napsa reports reassess latest\napsa reports compare audit_BEFORE audit_AFTER\napsa reports export latest --format sarif --out audit.sarif\napsa reports verify\n```\n\nReassessment applies current intelligence to a saved inventory; run `scan` again for changed files or new static checks. If connected to an AI client, that client may send report metadata to its model provider. APSA's MCP context excludes source excerpts, full source files, and screenshot bytes; client-side data handling still depends on the client.\n\n## CI and background jobs\n\n```sh\napsa policy init --out apsa.toml\napsa scan /path/to/owned/app --policy apsa.toml --out audit.json\napsa scan /path/to/owned/app --fail-on high --include-candidates\napsa scan /path/to/owned/app --background --json\napsa jobs status JOB_ID --json\n```\n\nSeverity gates exclude `candidate` findings by default. Opt in with `--include-candidates` or the policy's `allowed_statuses`. Required rules accept only `checked` or `not-applicable` coverage; partial execution and missing required checks do not pass. Waivers need a finding ID, a reason, and an expiry date. A background job being `completed` means it finished; check its `audit_incomplete` flag and report before treating the audit as complete.\n\nUnreadable source directories and files leave warnings and incomplete coverage; a source tree with no readable supported files fails explicitly. A source tree over the text staging budgets (64 MiB, 12,000 files) is audited partially rather than refused. Files are staged by kind in this order: app code and configuration (configuration first, then the app's code, then headers and vendored code such as `.build/` or `vendor/`), other shipped text (base resources, JSON, YAML), localized or qualified Android `res/values-*` files, then tests. `inventory.input_snapshot.omitted` counts the files and bytes left out by kind (`code_and_config`, `other_text`, `localized_values`, `tests`), including files over the 8 MiB file limit and files that could not be read or staged. Unreadable directories, and directories past the tree-wide budgets of 100,000 entries (read in name order) or 64 levels, are reported as warnings instead. When app code, configuration or other shipped text is left out, `app_scope_complete` is false and not-applicable coverage also becomes partial. For a complete audit, scan a narrower folder such as the app module. Storage capture failures remain `not-run` and cannot establish that a canary was deleted.\n\n| Exit code | Meaning for the unified CLI |\n| --- | --- |\n| `0` | Command completed or policy passed |\n| `1` | Execution error |\n| `2` | Invalid arguments |\n| `3` | Incomplete audit or policy, failed report verification, or failed or partial intelligence synchronization |\n| `4` | Findings exceeded the configured CI threshold |\n| `130` | Interrupted |\n\n`--json` emits an envelope containing `ok`, `data` or `error`, and `exit_code`; watch emits one JSON envelope per cycle (NDJSON). `ok` is `true` for codes `0` and `4`; code `4` means evaluation succeeded but the CI threshold was exceeded. CI must check `exit_code` and the policy result. Reports may still be produced for codes `3` and `4`.\n\n### GitHub code scanning\n\n```yaml\npermissions:\n  contents: read\n  security-events: write\n  actions: read            # private repositories: lets the upload read the workflow run\nsteps:\n  - uses: actions/checkout@v7\n    with:\n      persist-credentials: false\n  - uses: ictechgy/apsa@<commit-sha> # v1.5.1; pin the full commit SHA\n    with:\n      path: android            # source folder, or a built APK/AAB/IPA in the workspace\n      fail-on-incomplete: \"true\"\n```\n\nThe action installs `apsa` from PyPI at the matching version with the\nrelease's hash-locked dependencies, runs `scan` with `--format sarif\n--sarif-root`, writes a job summary, uploads the SARIF file to code scanning\nand then applies the result. A policy failure or a `fail-on` threshold fails\nthe job even when the audit is also incomplete (exit code `3` can hide a\nthreshold failure, so the action reads the report rather than the exit code\nalone). With a policy, every unmet policy condition (required rules, required\nintelligence, expired waivers, baseline checks) fails the job; only a partial\naudit is left to `fail-on-incomplete`, which fails on exit `3` unless set to\n`\"false\"`. Use\n`policy` (with `baseline-file` and `baseline-sha256`) for team gates, or\n`fail-on` for a severity threshold that excludes candidates.\n\nBy default the action is offline: it does no CVE or dependency-advisory\ncorrelation, and dependency findings need `intel-sync` (fetch public advisories\nfirst; a partial sync is a warning) and `online` (send dependency names and\nversions to OSV, see [network use](#public-intelligence-and-network-use)).\nPrivate repositories need GitHub Code Security to upload SARIF; set\n`upload-sarif: \"false\"` to keep only the file. Pull requests from forks and\nevery Dependabot-triggered run get a read-only token, so the action skips the\nupload there and still writes the file and summary.\n\nEach alert's fingerprint is APSA's finding identity: the rule plus the evidence\nlocation (file, line, function or archive member). Editing the flagged line or\nmoving the file can therefore close one alert and open another. Candidate\nfindings are uploaded at `warning` level and tagged `candidate`, and a rule\nwhose results are all candidates carries no `security-severity`, so it does not\ntrip code scanning's default check for High or Critical alerts. A rule with\nconfirmed results takes its `security-severity` from those, and its candidate\nalerts share that rating. Code scanning\ntracks alerts per `category` (default `apsa`); if you uploaded APSA SARIF\nbefore under another category, keep that value, because a new category starts\nnew alerts and leaves the old category's alerts open until you delete its\nanalyses. GitHub may offer\n[Copilot Autofix](https://docs.github.com/en/code-security/code-scanning/managing-code-scanning-alerts/responsible-use-autofix-code-scanning)\nsuggestions on third-party alerts where your plan enables it; this has not been\ntested with APSA alerts, and APSA's evidence status stays in each alert's\nproperties.\n\nWithout the action:\n\n```sh\napsa scan android --format sarif --out apsa.sarif --sarif-root android\napsa reports export latest --format maswe --out maswe.json\napsa reports export latest --format cyclonedx --out apsa.cdx.json\n```\n\n`reports ingest REPORT --sarif FILE --tool NAME` (MCP `reports_ingest_sarif`)\nimports another tool's SARIF 2.1.0 results, such as Android Lint, CodeQL or\nSemgrep, into a new report. Imported results stay candidates with\n`origin: external`, add no APSA coverage, and are cross-linked with APSA\nfindings of a related weakness at the same location. Their text is sanitized\nand treated as untrusted.\n\n`--format checklist --checklist masvs-v2` (or a TOML file mapping your own\ninspection items to MASWE identifiers and APSA rules) lists, per item, the\nrelated checks' states and findings; statuses never claim an item passed.\n`reports history TARGET` lists when each finding was first and last observed\nand when a later report stopped observing it, as inspection and remediation\nrecords. See [using APSA with Korean inspection standards](https://github.com/ictechgy/apsa/blob/main/docs/KOREA.md) (Korean).\n\n`--format cyclonedx` writes a CycloneDX 1.6 SBOM of the dependencies APSA\ndiscovered, with package URLs and the evidence behind each version, and embeds\nAPSA's dependency matches as VEX entries. Each entry carries the evidence\nstatus, the CISA KEV flag and when APSA first observed it for that target, which\nhelps run vulnerability-handling clocks such as the EU Cyber Resilience Act's.\nEvery analysis state is `in_triage`: APSA never marks a vulnerability\n`not_affected`; record that decision yourself with a justification. The SBOM is\nnot a complete build inventory and does not make an app compliant with any\nregulation.\n\nSee the [CI example](https://github.com/ictechgy/apsa/blob/main/docs/ci-example.yml) and [operations guide](https://github.com/ictechgy/apsa/blob/main/docs/OPERATIONS.md) (Korean) for policies, backup, limits, and troubleshooting.\n\n## MCP and skills\n\n```sh\napsa integrations --root /absolute/path/to/owned-apps\napsa mcp --root /absolute/path/to/owned-apps\napsa skill install\napsa context --report latest --section findings --limit 20 --json\n```\n\nUse `integrations` to generate a configuration with the installed executable path, or a `python -m apsa` fallback when no `apsa` executable is found. The shape below is illustrative; replace both absolute paths:\n\n```json\n{\n  \"mcpServers\": {\n    \"apsa\": {\n      \"command\": \"/absolute/path/to/apsa\",\n      \"args\": [\"mcp\", \"--root\", \"/absolute/path/to/owned-apps\"]\n    }\n  }\n}\n```\n\n### Install for agents\n\n| Client | Install |\n| --- | --- |\n| Claude Code | `/plugin marketplace add ictechgy/apsa`, then `/plugin install apsa@apsa`. The plugin runs `uvx --python 3.12 apsa@VERSION mcp --root <current project>` and adds the skill. |\n| Any MCP client | Use the configuration from `apsa integrations`, or the MCP Registry entry `io.github.ictechgy/apsa` (PyPI package, `uvx`, required `--root`). |\n| Codex and other skill runtimes | `apsa skill install`, plus the `integrations` configuration. |\n\nBefore release, Claude Code 2.1.295 (this plugin loaded from the checkout with\n`--plugin-dir`, root `${CLAUDE_PROJECT_DIR}`) and Codex CLI 0.162.0 were each\nverified calling `capabilities` and `audit_scan` against a local build of this\nversion on a freshly generated synthetic project. Marketplace and MCP Registry\ninstalls are checked after the package is published. Other clients are not yet verified.\n\nMCP uses stdio and requires an explicit `--root`; repeat it for multiple roots. `integrations` defaults to the current directory when no root is supplied. Roots restrict audited targets and access to their reports and jobs. `--allow-any-root` explicitly removes that restriction. APSA does not change model-client configuration automatically; client authentication belongs to the client.\n\n| Purpose | MCP tools |\n| --- | --- |\n| Audits | `capabilities`, `audit_scan`, `audit_start`, `audit_reassess`, `verify_finding`, `specs_validate` |\n| Jobs | `jobs_list`, `jobs_status`, `jobs_cancel` |\n| Reports | `reports_list`, `reports_get`, `reports_compare`, `reports_export_baseline`, `reports_ingest_sarif`, `reports_checklist`, `reports_history` |\n| Intelligence | `intelligence_sync`, `intelligence_search`, `intelligence_get`, `dependency_check` |\n| Policy | `policy_evaluate` |\n| Runtime planning | `runtime_plan`, `runtime_devices` |\n\n`verify_finding` (CLI `apsa verify`) cross-checks a finding claimed by another tool or AI reviewer against APSA's evidence and never refutes it. Project taint specifications name project-specific sources and sinks, such as a deep-link parser or an in-app browser wrapper, for APSA to follow deterministically; see [project specifications](https://github.com/ictechgy/apsa/blob/main/docs/PROJECT_SPECS.md).\n\n`capabilities` reports `tool_manifest_sha256`, a hash of the APSA-owned tool\nsurface (names, descriptions, annotation hints and parameter shapes); the tool\nset never changes within a process. Compare a hash computed from `tools/list`\nwith the published values rather than trusting the server's own report. Read-only tools are annotated read-only and idempotent, network tools\nopen-world. App content and advisory text are treated as untrusted data and\nmodel context omits source excerpts; see the [MCP security model](https://github.com/ictechgy/apsa/blob/main/docs/MCP_SECURITY.md).\n\nResources include `apsa://rules` and `apsa://reports/{report_id}`. The previous `quaygate://` and `mobile-audit://` resource schemes remain compatible.\n\nReport context defaults to 20 records and 64 KiB. Resolve `latest` once, then page with the returned immutable report ID and `page.next_cursor`, keeping section/filters fixed. `partial_response` describes the page; `audit_incomplete` describes execution. Oversized records are explicitly omitted and remain available in local exports. Portable baselines require explicit approval provenance and a SHA-256 pinned in reviewed CI configuration. See [workflow examples](docs/MODEL_WORKFLOWS.md).\n\nThe default server exposes runtime planning. `runtime_execute` and `runtime_start` are registered only with `--allow-runtime` at server startup. Both preview a scenario by default; `execute=true` runs it. `runtime_start` starts a cancellable background device job. Runtime tests need an authorized, prepared test app; the default audit does not boot devices or install apps. See the [operations guide](https://github.com/ictechgy/apsa/blob/main/docs/OPERATIONS.md) (Korean) before running a scenario.\n\n`skill install` copies the packaged [APSA skill](https://github.com/ictechgy/apsa/blob/main/.agents/skills/apsa/SKILL.md) to `~/.codex/skills/apsa`. To install directly into another model runtime's skill directory:\n\n```sh\napsa skill install --name apsa --dest /path/to/runtime/skills/apsa\n```\n\nUse `--name quaygate` or `--name mobile-audit` to update a skill installed under an older default name; custom edits are preserved unless `--force` is supplied. `integrations` reports skill status. For clients without MCP, `context` provides report context that excludes source excerpts, full source files, and screenshot bytes.\n\n## Compatibility and stored data\n\n`quaygate` and `mobile-audit` invoke the same unified CLI. Python entry points `python -m apsa`, `python -m quaygate`, and `python -m mobile_audit` remain available. Existing reports and `QG-*` rule IDs retain their identity.\n\nData is stored by default in `~/.local/share/mobile-audit`. Path precedence is `--home` → `APSA_HOME` → `QUAYGATE_HOME` → `MOBILE_AUDIT_HOME` → that default. Renaming does not copy the database or move history. Existing MCP configurations must include an authorized `--root`.\n\nThe legacy `apk`, `ipa`, and `device` subcommands retain quick lint output and exit codes `0/1/2`; they do not save unified audit history or correlate vulnerability intelligence. Use `scan` for the full workflow. Older cached OSV records without severity need a fresh `scan --online` or `intel watch --online`; reassessment alone cannot recover missing scores.\n\n## Development, validation, and licensing\n\n```sh\nuv sync --locked --extra dev --python 3.12\nmake test benchmark\nmake export-release\nmake release RELEASE_OUT=dist/apsa-local-release\n```\n\nChoose a new or empty release directory. Release verification requires uv **0.12.1** and builds wheel/sdist twice, compares their hashes, and checks a clean installation outside the checkout with offline source/APK scans, MCP, and skill installation. It writes hashes, an SBOM, dependency notices, and a release manifest without publishing. Initial dependency preparation can use the network. The same-host repeat check does not claim byte-identical builds across platforms.\n\nTagged releases use GitHub Actions to publish the verified distributions to PyPI after the supported CI matrix passes. See [release publishing](https://github.com/ictechgy/apsa/blob/main/docs/PUBLISHING.md) for the workflow and download contents.\n\nRecorded product validation is in [RELEASE_READINESS.md](https://github.com/ictechgy/apsa/blob/main/RELEASE_READINESS.md). The [curated benchmark](https://github.com/ictechgy/apsa/blob/main/benchmarks/README.md) is a regression corpus, not a measure of production detection rates. [Integration boundaries](https://github.com/ictechgy/apsa/blob/main/docs/INTEGRATION.md) and the [threat model](https://github.com/ictechgy/apsa/blob/main/docs/THREAT_MODEL.md) are currently in Korean. Historical reviews remain tied to their original snapshots.\n\nA reproducible [APSA/MobSF comparison](benchmarks/COMPETITIVE.md) and its\n[recorded results](benchmarks/COMPETITIVE_RESULTS.md) use freshly generated\nsynthetic source projects and APKs. They measure development candidates\nrecorded before APSA 1.2.0, not the published package; these results are not\nproduction accuracy estimates. A rerun on the APSA 1.3.0 release runtime with\nnewly generated inputs reproduced the candidate figures (see\n[RELEASE_READINESS.md](https://github.com/ictechgy/apsa/blob/main/RELEASE_READINESS.md)).\n\nA separate [public-source and CVE evaluation](benchmarks/REAL_WORLD.md) records\n[initial results](benchmarks/REAL_WORLD_RESULTS.md) on six pinned app source\nsnapshots, selected dependency/CVE units and OS advisory fixtures. It preserves\nextraction gaps, scanner failures and incomplete coverage; it does not measure\nwhole-app security or general production accuracy.\n\nThe subsequent [hardening rerun](benchmarks/HARDENING_RESULTS.md) completes all\nsix source scans and closes four measured integration gaps. It reuses observed\nlabels and reports remaining incomplete coverage; it is a development check.\n\nThe [coverage extension](benchmarks/COVERAGE_RESULTS.md) records narrow\nSwift/Kotlin compatibility, recognized function counts and Apple-evidenced\niOS/iPadOS branch decisions. Adapted source remains partial; the selected\nfixtures do not establish general CVE accuracy.\n\nThe [additional analysis and independent holdout](benchmarks/NEXT_RESULTS.md)\ncovers AAB/IPA metadata, Objective-C, parser isolation and two independently\nlabeled public sources. It records selected CVE boundary agreement, unresolved\ncatalog usage and remaining parser gaps separately.\n\nThe [dependency evidence evaluation](benchmarks/DEPENDENCY_RESULTS.md) compares\nAPSA's Gradle catalog usage and lockfile coordinates with Gradle's own\nresolution on three frozen holdouts of public apps, keeping first blind results\nseparate from reruns after the truth was seen. The\n[independent source pairs](benchmarks/FPFN_RESULTS.md) score vulnerable and\nfixed commits of public projects against truth frozen before scanning. The\n[blind MASWE-labeled pairs](benchmarks/BLIND_PAIRS_RESULTS.md) measure 1.5 on a\nnew holdout labeled by weakness, and the [OWASP MASTG demos](benchmarks/MASTG_RESULTS.md)\nare a development benchmark. All are narrow samples, not general accuracy estimates.\n\nThe source is publicly available on [GitHub](https://github.com/ictechgy/apsa). [LICENSE](https://github.com/ictechgy/apsa/blob/main/LICENSE) preserves the original Quaygate MIT notice. This publication does not declare an additional license for the combined product.\n"}