apsa 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apsa/__init__.py +3 -0
- apsa/__main__.py +3 -0
- apsa/cli.py +3 -0
- apsa-1.0.3.dist-info/METADATA +228 -0
- apsa-1.0.3.dist-info/RECORD +62 -0
- apsa-1.0.3.dist-info/WHEEL +4 -0
- apsa-1.0.3.dist-info/entry_points.txt +4 -0
- apsa-1.0.3.dist-info/licenses/LICENSE +21 -0
- mobile_audit/__init__.py +3 -0
- mobile_audit/__main__.py +3 -0
- mobile_audit/_job_worker.py +72 -0
- mobile_audit/_parser_worker.py +125 -0
- mobile_audit/audit.py +369 -0
- mobile_audit/binary_analysis.py +794 -0
- mobile_audit/cli.py +543 -0
- mobile_audit/core.py +237 -0
- mobile_audit/data/demo/AndroidManifest.xml +13 -0
- mobile_audit/data/demo/Demo.swift +4 -0
- mobile_audit/data/demo/Info.plist +7 -0
- mobile_audit/data/demo/MainActivity.kt +13 -0
- mobile_audit/data/demo/build.gradle.kts +3 -0
- mobile_audit/data/rules.json +174 -0
- mobile_audit/data/skills/apsa/SKILL.md +38 -0
- mobile_audit/data/skills/mobile-audit/SKILL.md +38 -0
- mobile_audit/data/skills/quaygate/SKILL.md +38 -0
- mobile_audit/data/structural-rules.json +280 -0
- mobile_audit/engine.py +66 -0
- mobile_audit/inputs.py +574 -0
- mobile_audit/intel.py +813 -0
- mobile_audit/jobs.py +188 -0
- mobile_audit/mcp_server.py +322 -0
- mobile_audit/output.py +192 -0
- mobile_audit/policy.py +424 -0
- mobile_audit/processes.py +78 -0
- mobile_audit/quaygate_analysis.py +228 -0
- mobile_audit/rules.py +165 -0
- mobile_audit/runtime.py +804 -0
- mobile_audit/skills.py +61 -0
- mobile_audit/source_analysis.py +927 -0
- mobile_audit/store.py +492 -0
- mobile_audit/tools.py +40 -0
- mobile_audit/tui.py +449 -0
- mobile_audit/watch.py +45 -0
- quaygate/__init__.py +3 -0
- quaygate/__main__.py +5 -0
- quaygate/adb.py +55 -0
- quaygate/apk.py +136 -0
- quaygate/appchecks.py +705 -0
- quaygate/apps.py +136 -0
- quaygate/arsc.py +171 -0
- quaygate/axml.py +160 -0
- quaygate/cert.py +91 -0
- quaygate/checks.py +289 -0
- quaygate/cli.py +20 -0
- quaygate/dex.py +37 -0
- quaygate/dexcode.py +271 -0
- quaygate/elf.py +99 -0
- quaygate/indicators.py +148 -0
- quaygate/ipa.py +404 -0
- quaygate/legacy_cli.py +145 -0
- quaygate/network_security.py +141 -0
- quaygate/report.py +128 -0
apsa/__init__.py
ADDED
apsa/__main__.py
ADDED
apsa/cli.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: apsa
|
|
3
|
+
Version: 1.0.3
|
|
4
|
+
Summary: APSA (앱사): evidence-first security audits for Android and iOS
|
|
5
|
+
Project-URL: Homepage, https://github.com/ictechgy/apsa
|
|
6
|
+
Project-URL: Repository, https://github.com/ictechgy/apsa
|
|
7
|
+
Project-URL: Issues, https://github.com/ictechgy/apsa/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/ictechgy/apsa/releases
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: androguard<5,>=4.1
|
|
12
|
+
Requires-Dist: beautifulsoup4<5,>=4.13
|
|
13
|
+
Requires-Dist: cvss<4,>=3.6
|
|
14
|
+
Requires-Dist: defusedxml<1,>=0.7
|
|
15
|
+
Requires-Dist: httpx<1,>=0.28
|
|
16
|
+
Requires-Dist: loguru<1,>=0.7
|
|
17
|
+
Requires-Dist: mcp<2,>=1.20
|
|
18
|
+
Requires-Dist: packaging>=25
|
|
19
|
+
Requires-Dist: psutil==7.2.2
|
|
20
|
+
Requires-Dist: rich<15,>=14
|
|
21
|
+
Requires-Dist: textual<9,>=6
|
|
22
|
+
Requires-Dist: tree-sitter-java==0.23.5
|
|
23
|
+
Requires-Dist: tree-sitter-json==0.24.8
|
|
24
|
+
Requires-Dist: tree-sitter-kotlin==1.1.0
|
|
25
|
+
Requires-Dist: tree-sitter-swift==0.7.4
|
|
26
|
+
Requires-Dist: tree-sitter==0.25.2
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pyright>=1.1.400; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=1; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.12; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# APSA
|
|
35
|
+
|
|
36
|
+
**Evidence-first security audits for Android and iOS.**
|
|
37
|
+
|
|
38
|
+
[English](https://github.com/ictechgy/apsa/blob/main/README.md) · [한국어](https://github.com/ictechgy/apsa/blob/main/README.ko.md)
|
|
39
|
+
|
|
40
|
+
This English README is the source of truth. The Korean README follows it.
|
|
41
|
+
|
|
42
|
+
APSA helps developers and security teams audit their own mobile apps. It inspects source code and APK/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.
|
|
43
|
+
|
|
44
|
+
Pronounced **“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.
|
|
45
|
+
|
|
46
|
+
## What it checks
|
|
47
|
+
|
|
48
|
+
| Area | Available checks |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| Source code | Java/Kotlin/Swift AST analysis; WebView and deep-link patterns; Manifest, Info.plist, storage, and dependency inspection |
|
|
51
|
+
| Android builds | DEX calls and constant flow; resources and network configuration; exported components/providers; signing-block and v1 certificate evidence; ELF hardening |
|
|
52
|
+
| iOS builds | Mach-O headers; limited entitlement/configuration checks (embedded XML entitlements, ATS exceptions, provisioning indicators); PIE, canary, and string evidence |
|
|
53
|
+
| Public intelligence | Apple/Android advisories, CVE, CISA KEV, OWASP guidance, and OSV dependency correlation |
|
|
54
|
+
| Reports and CI | SQLite history, comparison and reassessment, JSON/Markdown/SARIF export, coverage requirements, and expiring waivers |
|
|
55
|
+
| Runtime | Prepared scenarios for owned Android test apps and iOS simulator apps; physical iOS devices are unsupported |
|
|
56
|
+
| Model integration | stdio MCP tools/resources and packaged skills, without a required model provider or LLM API key |
|
|
57
|
+
|
|
58
|
+
Findings 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.
|
|
59
|
+
|
|
60
|
+
OWASP 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.
|
|
61
|
+
|
|
62
|
+
## Install and run
|
|
63
|
+
|
|
64
|
+
Install **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.
|
|
65
|
+
|
|
66
|
+
Install the published package from [PyPI](https://pypi.org/project/apsa/):
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
uv tool install --python 3.12 apsa==1.0.3
|
|
70
|
+
apsa --version
|
|
71
|
+
apsa doctor --json
|
|
72
|
+
apsa demo --out ./apsa-demo
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The 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.
|
|
76
|
+
|
|
77
|
+
For development or installation with the repository's locked dependencies, use a checkout:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
git clone https://github.com/ictechgy/apsa.git
|
|
81
|
+
cd apsa
|
|
82
|
+
uv sync --locked --python 3.12
|
|
83
|
+
uv run --locked apsa doctor --json
|
|
84
|
+
uv run --locked apsa demo --out ./apsa-demo
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`doctor` checks parsers, optional device tools, and offline readiness. `demo` writes an intentionally vulnerable example and scans it; choose a new output directory.
|
|
88
|
+
|
|
89
|
+
To register the checkout's commands on your PATH:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
uv tool install --editable . --force --python 3.12 --constraints requirements-release.txt
|
|
93
|
+
apsa --version
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This 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.
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
apsa scan /path/to/owned/mobile-project
|
|
100
|
+
apsa scan /path/to/owned/app.apk
|
|
101
|
+
apsa scan /path/to/owned/app.ipa --sbom /path/to/build.cdx.json
|
|
102
|
+
apsa tui
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use 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.
|
|
106
|
+
|
|
107
|
+
## Public intelligence and network use
|
|
108
|
+
|
|
109
|
+
A default `scan` reads local files and cached intelligence without uploading source code or builds. Public-feed collection and OSV queries use the network:
|
|
110
|
+
|
|
111
|
+
| Command | Network behavior |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `apsa scan TARGET` | Uses local inputs and cached intelligence |
|
|
114
|
+
| `apsa intel sync` | Fetches public vulnerability sources |
|
|
115
|
+
| `apsa intel watch` | Polls public sources and reassesses saved inventories; does not reread app files or implicitly query OSV |
|
|
116
|
+
| `apsa scan TARGET --online` | Sends discovered dependency names and versions to OSV |
|
|
117
|
+
| `apsa intel watch --online` | Also sends saved dependency names and versions to OSV |
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
apsa intel sync
|
|
121
|
+
apsa intel status
|
|
122
|
+
apsa intel watch --interval 900
|
|
123
|
+
apsa scan /path/to/owned/app --online
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Watch 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.
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
apsa reports reassess latest
|
|
130
|
+
apsa reports compare audit_BEFORE audit_AFTER
|
|
131
|
+
apsa reports export latest --format sarif --out audit.sarif
|
|
132
|
+
apsa reports verify
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Reassessment 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.
|
|
136
|
+
|
|
137
|
+
## CI and background jobs
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
apsa policy init --out apsa.toml
|
|
141
|
+
apsa scan /path/to/owned/app --policy apsa.toml --out audit.json
|
|
142
|
+
apsa scan /path/to/owned/app --fail-on high --include-candidates
|
|
143
|
+
apsa scan /path/to/owned/app --background --json
|
|
144
|
+
apsa jobs status JOB_ID --json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Severity 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.
|
|
148
|
+
|
|
149
|
+
| Exit code | Meaning for the unified CLI |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `0` | Command completed or policy passed |
|
|
152
|
+
| `1` | Execution error |
|
|
153
|
+
| `2` | Invalid arguments |
|
|
154
|
+
| `3` | Incomplete audit or policy, failed report verification, or failed or partial intelligence synchronization |
|
|
155
|
+
| `4` | Findings exceeded the configured CI threshold |
|
|
156
|
+
| `130` | Interrupted |
|
|
157
|
+
|
|
158
|
+
`--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`. See 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.
|
|
159
|
+
|
|
160
|
+
## MCP and skills
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
apsa integrations --root /absolute/path/to/owned-apps
|
|
164
|
+
apsa mcp --root /absolute/path/to/owned-apps
|
|
165
|
+
apsa skill install
|
|
166
|
+
apsa context --report latest --json
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Use `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:
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"mcpServers": {
|
|
174
|
+
"apsa": {
|
|
175
|
+
"command": "/absolute/path/to/apsa",
|
|
176
|
+
"args": ["mcp", "--root", "/absolute/path/to/owned-apps"]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
MCP 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.
|
|
183
|
+
|
|
184
|
+
| Purpose | MCP tools |
|
|
185
|
+
| --- | --- |
|
|
186
|
+
| Audits | `capabilities`, `audit_scan`, `audit_start`, `audit_reassess` |
|
|
187
|
+
| Jobs | `jobs_list`, `jobs_status`, `jobs_cancel` |
|
|
188
|
+
| Reports | `reports_list`, `reports_get`, `reports_compare` |
|
|
189
|
+
| Intelligence | `intelligence_sync`, `intelligence_search`, `intelligence_get`, `dependency_check` |
|
|
190
|
+
| Policy | `policy_evaluate` |
|
|
191
|
+
| Runtime planning | `runtime_plan`, `runtime_devices` |
|
|
192
|
+
|
|
193
|
+
Resources include `apsa://rules` and `apsa://reports/{report_id}`. The previous `quaygate://` and `mobile-audit://` resource schemes remain compatible.
|
|
194
|
+
|
|
195
|
+
The 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.
|
|
196
|
+
|
|
197
|
+
`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:
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
apsa skill install --name apsa --dest /path/to/runtime/skills/apsa
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Use `--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.
|
|
204
|
+
|
|
205
|
+
## Compatibility and stored data
|
|
206
|
+
|
|
207
|
+
`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.
|
|
208
|
+
|
|
209
|
+
Data 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`.
|
|
210
|
+
|
|
211
|
+
The 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.
|
|
212
|
+
|
|
213
|
+
## Development, validation, and licensing
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
uv sync --locked --extra dev --python 3.12
|
|
217
|
+
make test benchmark
|
|
218
|
+
make export-release
|
|
219
|
+
make release RELEASE_OUT=dist/apsa-local-release
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Choose 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.
|
|
223
|
+
|
|
224
|
+
Tagged 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.
|
|
225
|
+
|
|
226
|
+
Recorded 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.
|
|
227
|
+
|
|
228
|
+
The 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.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
apsa/__init__.py,sha256=LkhWhbHT-0QkessgaipnwaroP_hpI1GH9msjzp-xQIg,122
|
|
2
|
+
apsa/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
3
|
+
apsa/cli.py,sha256=nn9B7UEjIJTXduuArAs8cJMjT7Atd4j8t2w21T0oqAM,119
|
|
4
|
+
quaygate/__init__.py,sha256=C_uzOFT-RUKkyZHZ3-VPAaES39WApbprM_VmvrJX24w,104
|
|
5
|
+
quaygate/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
|
|
6
|
+
quaygate/adb.py,sha256=TCUAQE0otx9gVMm6w8pPUV0NLWsNq6F2Q69pBS0jRqo,2140
|
|
7
|
+
quaygate/apk.py,sha256=zv90IBb38kc8oBuwoL9kyyKG4eGJzkBvqxsdOmpM8Cg,5193
|
|
8
|
+
quaygate/appchecks.py,sha256=SF_o3wWtkc61m6STVvs8WCuG5GLZsjVnBGkc1O-cEcU,39157
|
|
9
|
+
quaygate/apps.py,sha256=2ZwVRRPZ1S3QlErvoKtzE36f3QuVzAx_QOAtYQYqtSQ,6266
|
|
10
|
+
quaygate/arsc.py,sha256=4oOH61MqnkFaZDilErSf9WpZDDAOUoLZX75iY9ZMUpw,6666
|
|
11
|
+
quaygate/axml.py,sha256=NeES60Ma5FGoUdoTmCYJ-b3B-k6M-rqWOh6jsIuZZqY,6418
|
|
12
|
+
quaygate/cert.py,sha256=PoDXQxdo6HBQBkv3coX7YyGtHTuxwkfCSS73Tg205kE,3803
|
|
13
|
+
quaygate/checks.py,sha256=_i1AaGTlzqFng1-8-0YuA4wa1P7oI_i5LXnoxBifpHY,13205
|
|
14
|
+
quaygate/cli.py,sha256=bdpr_rmwFta9_FF--EcCNvqN30RVPV_Sy2en0TGzUrk,541
|
|
15
|
+
quaygate/dex.py,sha256=OHk4FEkqFxprfTdduyG0WuoOZ1_1IxDBRlxrPYfpaeE,1330
|
|
16
|
+
quaygate/dexcode.py,sha256=zLikYSt_KKZ1Ou_t9-59sMxJcAxoeNhctkjXoO2M1dQ,11087
|
|
17
|
+
quaygate/elf.py,sha256=xodI04O-eMXGf0J2WF5kVfNutNaIbm1Zb7XtI9-cZ1E,3517
|
|
18
|
+
quaygate/indicators.py,sha256=n_rLQ1wb4RgWR6B3_f_cGa2t-B71AtMNf25xNt1D3fk,7286
|
|
19
|
+
quaygate/ipa.py,sha256=0S3HSbdNwCS5a1E2dGgC8GWDzSK2diIpaLdG6NkG8gQ,17810
|
|
20
|
+
quaygate/legacy_cli.py,sha256=Tgl9larDg6TWLrK-5uAguIyIqoDsLMgAQtWPFoQCSTQ,6325
|
|
21
|
+
quaygate/network_security.py,sha256=62OBN7UyoNt519Kng8VNzeKp6473E7PYjTo5HV0LYqU,6224
|
|
22
|
+
quaygate/report.py,sha256=HB-LQedJY32ygHiHm_8Zj5ZNjA97zzpSQE2En0hnR8o,4768
|
|
23
|
+
mobile_audit/__init__.py,sha256=hU-Du9fv2240GJc1ZYlNFMSaqPEzn_-ZUYdjy43Pk4s,117
|
|
24
|
+
mobile_audit/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
|
|
25
|
+
mobile_audit/_job_worker.py,sha256=X_G4Z8-hJzTlwgEs82sNBgO6tkniT0O2vBEQOr_xV8A,2130
|
|
26
|
+
mobile_audit/_parser_worker.py,sha256=qoVAGkqUbjLYKkvrImqS86tPr2_eqgtkkShd3bE4230,5095
|
|
27
|
+
mobile_audit/audit.py,sha256=jSwMe81eNSE9LNL-5a0e8XlYmxgdXVaZm0eOlY9wCHQ,16343
|
|
28
|
+
mobile_audit/binary_analysis.py,sha256=wi1atK6h8cLjHic5CzYKaEEr00mslcacWjCcp_RquHQ,39124
|
|
29
|
+
mobile_audit/cli.py,sha256=S0RsnYs61f8wqw3hdy7Wa4urWIZJUmuSPpEmGUP_42w,23737
|
|
30
|
+
mobile_audit/core.py,sha256=65Ot8JrMNeLpy4QJwJ7llO48NDNWFdriQs6Vdk2c1uY,8298
|
|
31
|
+
mobile_audit/engine.py,sha256=f-zQHoF4u0ceQcwa1RFTiLOobA8yFR1JTcvTfifcZ4M,2418
|
|
32
|
+
mobile_audit/inputs.py,sha256=DCWrcq6xK5CNsj8xDVBHtcT88sUarpqLlnTBHfOUPAo,24532
|
|
33
|
+
mobile_audit/intel.py,sha256=tMhiUlO9eLZypqKC_PKQP_-3hcHvx_wAHWmkdCybd10,36106
|
|
34
|
+
mobile_audit/jobs.py,sha256=zsFpkb64w8GhJCXSCBvpUubmUQXYIQaCZhuNeKkPEoI,7369
|
|
35
|
+
mobile_audit/mcp_server.py,sha256=SWt8Tkbz_t0Mzad3Af_8fbfwa0uayWT-i5bsSCzlYR4,15089
|
|
36
|
+
mobile_audit/output.py,sha256=3ssS6n0z5G2L3JCYpyWDUfoGwXNAZUy32WOdbTJx3w8,7064
|
|
37
|
+
mobile_audit/policy.py,sha256=H79Ssz2S_HshI2jFht9QLTzpyMgpyywvLtQq61WpkWo,18438
|
|
38
|
+
mobile_audit/processes.py,sha256=gQUv8qqasME9rWkXZZB8fNAMYRC7zSwl-oVLLA-omOw,3213
|
|
39
|
+
mobile_audit/quaygate_analysis.py,sha256=0dJNwQR7MbtCtmoaBd2IlCATU0OjrW7oMqyr0dTeoe0,8165
|
|
40
|
+
mobile_audit/rules.py,sha256=yt9igIEU1cB2-KwYQc0VNCC4qNMBi82x7p_jKsaj4Po,6533
|
|
41
|
+
mobile_audit/runtime.py,sha256=JBRq1_zfkodADq3zkgAnKQ8WgWKHENoJPtI0BMSV5aM,36988
|
|
42
|
+
mobile_audit/skills.py,sha256=EdKb8AIRAlLB7pO_DNrFdBGTDAUXSievP69xb2ftAJQ,2576
|
|
43
|
+
mobile_audit/source_analysis.py,sha256=Bb6x8w2mx9vHpXRFM6zpyNRjjmK9JDvONmWMD5G5MuA,42962
|
|
44
|
+
mobile_audit/store.py,sha256=J6771B1-BMKgKygUmjCwBD2S8nsCg1JAre_gECtWXi8,23191
|
|
45
|
+
mobile_audit/tools.py,sha256=kEvOPLH7zMAlmM_IR85QAVR1gAgY0rO1l-YsTzfA9yc,1422
|
|
46
|
+
mobile_audit/tui.py,sha256=EdYG73naxvLetLPbf8hp2T7Z7qHaxtBN8Jv8ovakZH4,20680
|
|
47
|
+
mobile_audit/watch.py,sha256=mPhM9DNpqqz7CDDJHfRUpp9MBhYI2otygv0r2p4ECfE,1781
|
|
48
|
+
mobile_audit/data/rules.json,sha256=lYWSL8FMY6D3dpfLqTdHeyF1zs9GsPAnB9tEghmHMrA,5782
|
|
49
|
+
mobile_audit/data/structural-rules.json,sha256=B5-rpkQMPpRoLBSV28CznioLwI6tF9WriyWa0HnlSp0,9854
|
|
50
|
+
mobile_audit/data/demo/AndroidManifest.xml,sha256=yeJpWxeJklNaqWv57LyOqn5pzH0f0GdqXz6kMbCs_OU,708
|
|
51
|
+
mobile_audit/data/demo/Demo.swift,sha256=0TY5nE7mmbW3yD9xUtN4ewWcqxrnyybEyD0j_2JeK8Q,111
|
|
52
|
+
mobile_audit/data/demo/Info.plist,sha256=QoY6NtXNV64cT2Bab_GH8ttBKmVNlb27-H5-NVnHYQQ,473
|
|
53
|
+
mobile_audit/data/demo/MainActivity.kt,sha256=t8UsoU33k95OCt6sYcxNLxGKWIhwlb_fHQBRG56bWVg,519
|
|
54
|
+
mobile_audit/data/demo/build.gradle.kts,sha256=ybi0Oi22aHafcOoHCFjkaYMyLnkUpWl9d5S4pxmpkgM,74
|
|
55
|
+
mobile_audit/data/skills/apsa/SKILL.md,sha256=p6rjMpabqYrI2kgxsFohCSs0JXjitLLGyL0nDJyT6Bs,6221
|
|
56
|
+
mobile_audit/data/skills/mobile-audit/SKILL.md,sha256=W90CuC9uceGJEwhR17hRqhNNcneR5Zbad1VFcoyHk2s,6237
|
|
57
|
+
mobile_audit/data/skills/quaygate/SKILL.md,sha256=YcWk3zR4FUJSUXfz_IaOKbzw_-fydX85we8iX4uGswU,6229
|
|
58
|
+
apsa-1.0.3.dist-info/METADATA,sha256=Z66mhiNwwfW2eUKeK00QoyzFFP4ABBCbNKhFJqplkvE,14957
|
|
59
|
+
apsa-1.0.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
60
|
+
apsa-1.0.3.dist-info/entry_points.txt,sha256=swEsVU_KobfUKISAtkD18lqmElkdJpqG1G5ATp1nrXY,101
|
|
61
|
+
apsa-1.0.3.dist-info/licenses/LICENSE,sha256=FbogplrPYWjwJZIGJWxqsWT94pNcZ_qrfEGi_edg_XI,1078
|
|
62
|
+
apsa-1.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 quaygate contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mobile_audit/__init__.py
ADDED
mobile_audit/__main__.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import psutil
|
|
9
|
+
|
|
10
|
+
from . import jobs
|
|
11
|
+
from .audit import scan
|
|
12
|
+
from .core import redact
|
|
13
|
+
from .runtime import run
|
|
14
|
+
from .store import Store
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def interrupted(_signum, _frame):
|
|
18
|
+
raise KeyboardInterrupt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
store = Store(Path(sys.argv[1]))
|
|
23
|
+
identifier = sys.argv[2]
|
|
24
|
+
signal.signal(signal.SIGTERM, interrupted)
|
|
25
|
+
try:
|
|
26
|
+
row = jobs.get(store, identifier, private=True)
|
|
27
|
+
if row["state"] in jobs.TERMINAL:
|
|
28
|
+
return
|
|
29
|
+
jobs.update(
|
|
30
|
+
store,
|
|
31
|
+
identifier,
|
|
32
|
+
state="running",
|
|
33
|
+
pid=os.getpid(),
|
|
34
|
+
process_started=psutil.Process().create_time(),
|
|
35
|
+
)
|
|
36
|
+
payload = row["payload"]
|
|
37
|
+
if payload["kind"] == "scan":
|
|
38
|
+
|
|
39
|
+
def progress(stage, percent):
|
|
40
|
+
jobs.update(store, identifier, stage=stage, progress=percent)
|
|
41
|
+
|
|
42
|
+
report = scan(
|
|
43
|
+
store,
|
|
44
|
+
Path(payload["target"]),
|
|
45
|
+
online=payload.get("online", False),
|
|
46
|
+
sbom=Path(payload["sbom"]) if payload.get("sbom") else None,
|
|
47
|
+
environment=payload.get("environment"),
|
|
48
|
+
progress=progress,
|
|
49
|
+
expected_target=Path(row["target"]),
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
jobs.update(store, identifier, stage="device-scenario", progress=10)
|
|
53
|
+
report = run(
|
|
54
|
+
store,
|
|
55
|
+
Path(payload["scenario"]),
|
|
56
|
+
payload["report_id"],
|
|
57
|
+
screenshots=payload.get("screenshots", False),
|
|
58
|
+
scenario=payload.get("scenario_value"),
|
|
59
|
+
)
|
|
60
|
+
jobs.update(
|
|
61
|
+
store, identifier, state="completed", progress=100, stage="completed", report_id=report["id"]
|
|
62
|
+
)
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
jobs.update(store, identifier, state="cancelled", stage="cancelled")
|
|
65
|
+
except Exception as error:
|
|
66
|
+
jobs.update(store, identifier, state="failed", stage="failed", error=redact(str(error))[:800])
|
|
67
|
+
finally:
|
|
68
|
+
store.close()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
main()
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Internal parser boundary. This process never accesses the report database or network."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from contextlib import contextmanager, nullcontext
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def limits() -> None:
|
|
14
|
+
if sys.platform == "win32":
|
|
15
|
+
return
|
|
16
|
+
import resource
|
|
17
|
+
|
|
18
|
+
resource.setrlimit(resource.RLIMIT_CPU, (45, 46))
|
|
19
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (512 * 1024 * 1024, 512 * 1024 * 1024))
|
|
20
|
+
resource.setrlimit(resource.RLIMIT_NOFILE, (128, 128))
|
|
21
|
+
if sys.platform.startswith("linux"):
|
|
22
|
+
resource.setrlimit(resource.RLIMIT_AS, (2 * 1024**3, 2 * 1024**3))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def stable_input(target: Path, scratch: Path | None = None):
|
|
27
|
+
from .core import MAX_ARCHIVE_TOTAL, open_file
|
|
28
|
+
|
|
29
|
+
if target.is_dir():
|
|
30
|
+
yield target
|
|
31
|
+
return
|
|
32
|
+
fd = open_file(target)
|
|
33
|
+
directory_context = (
|
|
34
|
+
nullcontext(str(scratch)) if scratch else tempfile.TemporaryDirectory(prefix="mobile-audit-parser-")
|
|
35
|
+
)
|
|
36
|
+
with os.fdopen(fd, "rb") as source, directory_context as directory:
|
|
37
|
+
import stat
|
|
38
|
+
|
|
39
|
+
before = os.fstat(source.fileno())
|
|
40
|
+
if not stat.S_ISREG(before.st_mode) or before.st_size > MAX_ARCHIVE_TOTAL:
|
|
41
|
+
raise ValueError("Archive is not a regular input within the size limit")
|
|
42
|
+
snapshot = Path(directory) / ("input" + target.suffix)
|
|
43
|
+
total = 0
|
|
44
|
+
with snapshot.open("wb") as output:
|
|
45
|
+
snapshot.chmod(0o600)
|
|
46
|
+
while chunk := source.read(1024 * 1024):
|
|
47
|
+
total += len(chunk)
|
|
48
|
+
if total > MAX_ARCHIVE_TOTAL:
|
|
49
|
+
raise ValueError("Archive grew beyond the input size limit")
|
|
50
|
+
output.write(chunk)
|
|
51
|
+
after = os.fstat(source.fileno())
|
|
52
|
+
if (before.st_size, before.st_mtime_ns) != (after.st_size, after.st_mtime_ns):
|
|
53
|
+
raise ValueError("Archive changed during inspection; rerun with a stable build")
|
|
54
|
+
yield snapshot
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def analyze(target: Path, sbom: Path | None) -> dict:
|
|
58
|
+
from .inputs import inspect_target
|
|
59
|
+
from .rules import static_checks
|
|
60
|
+
|
|
61
|
+
inventory, sources = inspect_target(target, sbom)
|
|
62
|
+
findings, coverage = static_checks(inventory, sources)
|
|
63
|
+
from .source_analysis import analyze_sources
|
|
64
|
+
|
|
65
|
+
structural = analyze_sources(sources)
|
|
66
|
+
findings.extend(structural["findings"])
|
|
67
|
+
coverage.extend(structural["coverage"])
|
|
68
|
+
inventory["warnings"].extend(structural["warnings"])
|
|
69
|
+
if target.suffix.lower() in {".apk", ".ipa"}:
|
|
70
|
+
from .binary_analysis import analyze_binary
|
|
71
|
+
|
|
72
|
+
binary = analyze_binary(target, inventory)
|
|
73
|
+
findings.extend(binary["findings"])
|
|
74
|
+
coverage.extend(binary["coverage"])
|
|
75
|
+
inventory["warnings"].extend(binary["warnings"])
|
|
76
|
+
inventory["binary"] = binary["metadata"]
|
|
77
|
+
inventory["features"] = sorted(
|
|
78
|
+
set(inventory["features"])
|
|
79
|
+
| {feature for dex in binary["metadata"].get("dex", []) for feature in dex.get("features", [])}
|
|
80
|
+
)
|
|
81
|
+
from .quaygate_analysis import analyze as analyze_lint
|
|
82
|
+
from .quaygate_analysis import merge
|
|
83
|
+
|
|
84
|
+
lint = analyze_lint(target)
|
|
85
|
+
findings = merge(findings, lint["findings"])
|
|
86
|
+
coverage.extend(lint["coverage"])
|
|
87
|
+
inventory["warnings"].extend(lint["warnings"])
|
|
88
|
+
inventory["quaygate"] = lint["metadata"]
|
|
89
|
+
if not lint["metadata"]["complete"]:
|
|
90
|
+
inventory["partial"] = True
|
|
91
|
+
inventory["engines"] = ["mobile-audit"] + (["quaygate-lint"] if "quaygate" in inventory else [])
|
|
92
|
+
if not inventory["fingerprint_complete"]:
|
|
93
|
+
inventory["partial"] = True
|
|
94
|
+
for check in coverage:
|
|
95
|
+
if check["state"] == "checked":
|
|
96
|
+
check["state"] = "partial"
|
|
97
|
+
inventory["warnings"].append(
|
|
98
|
+
"Input coverage is incomplete; the fingerprint identifies inspected source files only."
|
|
99
|
+
)
|
|
100
|
+
return {"inventory": inventory, "findings": findings, "coverage": coverage}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main() -> None:
|
|
104
|
+
limits()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
target = Path(sys.argv[1])
|
|
108
|
+
if not target.exists():
|
|
109
|
+
raise ValueError(f"Input not found: {target}")
|
|
110
|
+
if not target.is_dir() and target.suffix.lower() not in {".apk", ".ipa", ".zip", ".aab"}:
|
|
111
|
+
raise ValueError("Use a source folder, APK, IPA, simulator .app folder, or supported app ZIP")
|
|
112
|
+
expected = Path(sys.argv[3]) if len(sys.argv) > 3 and sys.argv[3] else target
|
|
113
|
+
if target.resolve() != expected:
|
|
114
|
+
raise ValueError("Authorized input moved or became a symlink; audit refused")
|
|
115
|
+
scratch = Path(sys.argv[4]) if len(sys.argv) > 4 else None
|
|
116
|
+
with stable_input(target, scratch) as snapshot:
|
|
117
|
+
result = analyze(snapshot, Path(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2] else None)
|
|
118
|
+
result["inventory"]["target"] = str(target)
|
|
119
|
+
except (ValueError, OSError, TypeError, KeyError) as error:
|
|
120
|
+
result = {"error": f"Input analysis failed ({type(error).__name__}): {error}"}
|
|
121
|
+
sys.stdout.write(json.dumps(result, ensure_ascii=False))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
main()
|