APK/IPA secret + endpoint scanner for vibe-coded mobile apps. Companion to Qualmly (qualmly.dev).
Project description
Qualmly Mobile Scanner — APK/IPA secret extraction
Single-file Python tool that unzips a packaged Android (.apk) or iOS (.ipa) build and scans every embedded text file for hardcoded secrets and backend URLs. Closes the function gap vs AuditYourApp's mobile reverse-engineering feature ($49/scan).
Status
Working v1. Run it on a real APK or IPA today. Catches ~80% of real-world secret leaks via plain-text grep at scale (which is how ~80% of secrets actually ship in mobile apps anyway).
What's done:
- ✅ Auto-detects APK vs IPA format
- ✅ Skips media/fonts/native libs (focuses on JS/JSON/plist/xml/config)
- ✅ Same 9 secret patterns as the qualmly.dev Code Review (Anthropic, OpenAI, Stripe, AWS, GitHub PAT/classic, Slack, Google API, Supabase JWT)
- ✅ Endpoint detection (Supabase, Firebase, localhost fallbacks)
- ✅ Pretty-print + JSON output
- ✅ Score + exit code (1 if any
*_livesecret found — useful for CI) - ✅ Stdlib only (zipfile + re); no install dependencies
What's NOT done (next iteration):
- Smali decompile for APK to reach obfuscated string constants. Add
apktoolintegration. ~1 day. - Mach-O binary string extraction for iOS apps where keys are baked into native binaries (not just JS bundles). Use
nmorstrings. ~1 day. - Web upload UI — wrap as a Cloudflare Worker that accepts file upload, runs the same logic, returns a Qualmly-style report. Enables charging $9/scan via Stripe. ~2 days.
- Integration with qualmly.dev — add a "Mobile" mode tab next to App QA + Code Review. Could be a stub that links to the standalone scanner for now.
Install
# Recommended — installs into an isolated venv, exposes `qualmly-mobile` on PATH
pipx install qualmly-mobile
# Or vanilla pip if you prefer
pip install qualmly-mobile
No native dependencies. Stdlib only. Works on macOS, Linux, and Windows (Python 3.8+).
If you want to run from source instead, just clone this repo and call the
script directly with python3 mobile-scanner/qualmly_mobile.py … — it has
zero install dependencies.
Usage
# Scan an APK
qualmly-mobile /path/to/app.apk
# Scan an IPA
qualmly-mobile /path/to/MyApp.ipa
# JSON output for CI
qualmly-mobile app.apk --json
# Write JSON to file
qualmly-mobile app.apk --report scan.json
(If running from source: replace qualmly-mobile with python3 qualmly_mobile.py.)
Sample output:
🔍 Qualmly Mobile Scan — myapp.apk
─────────────────────────────────────────────────
Format: APK
Size: 14.2 MB
Files scanned: 142 / 1387
Score: 64/100 ██████░░░░
Secrets found: 2
Endpoints found: 4
🔴 SECRETS:
• Stripe publishable key in assets/index-Dp8DBQyP.js
sample: pk_live_51TO… (107 chars total)
• Supabase service-role JWT in assets/index-Dp8DBQyP.js
sample: eyJhbGciOi… (240 chars total)
🔵 ENDPOINTS:
• Supabase project URL → https://abc1234567.supabase.co
in assets/index-Dp8DBQyP.js
• Localhost fallback → http://127.0.0.1:3000
in src/config.json
...
Powered by Qualmly · qualmly.dev
Why a separate Python tool, not in the browser?
Two reasons:
- APK/IPA files are too big to upload to a browser-side scanner reliably. 50–500 MB is typical. A native CLI runs locally on the build artifact without uploading.
- CI/CD friendly. Drop into GitHub Actions / Bitrise / Codemagic. Exit code 1 fails the build on
livekeys.
A hosted version (web upload) is on the roadmap — that gets bundled into the Agency tier or charged $9/scan as a separate Mobile tier.
Roadmap
| Phase | Scope | ETA after launch |
|---|---|---|
| v1 (this scaffold) | CLI tool, secrets + endpoints | — Ready to ship now |
| v1.1 | apktool / smali integration for APK | 1 week |
| v1.2 | Cloudflare Worker hosted upload, $9/scan | 2 weeks |
| v2 | Mach-O binary scanner (iOS native), mobile-specific findings (root detection bypass, certificate pinning checks) | 4–6 weeks |
Publishing a new version (maintainer notes)
Releases are published to PyPI via GitHub Actions Trusted Publishing (OIDC) —
no PYPI_API_TOKEN secret needed once the project is registered.
One-time setup on PyPI:
- Create the project skeleton: build + upload the first release with a
manual API token (
twine upload mobile-scanner/dist/*), OR pre-register it via Trusted Publishing's "pending publisher" flow at https://pypi.org/manage/account/publishing/ → add publisher with:- PyPI Project Name:
qualmly-mobile - Owner:
DarkPixel-Z - Repository name:
qualmly - Workflow name:
publish-mobile-pypi.yml - Environment name:
pypi
- PyPI Project Name:
- In the GitHub repo, create a
pypienvironment (Settings → Environments) so the workflow'senvironment: pypiclause resolves.
Cutting a release:
# bump __version__ in qualmly_mobile.py and version in pyproject.toml
git add mobile-scanner/qualmly_mobile.py mobile-scanner/pyproject.toml
git commit -m "mobile-scanner: bump to v1.0.1"
git tag mobile-v1.0.1
git push origin main --tags
The Publish qualmly-mobile to PyPI workflow fires on the tag, builds
sdist + wheel, runs twine check, then uploads via OIDC.
License
MIT — same as the parent project.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qualmly_mobile-1.0.1.tar.gz.
File metadata
- Download URL: qualmly_mobile-1.0.1.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91c6589d7d0541e16421d1dc8c9a71071c9ee5896a19914e50cda948a9779090
|
|
| MD5 |
27bf91d0d0ae76b81b38652098145283
|
|
| BLAKE2b-256 |
4d7c01ae907283480c0fa7e17cc70912ce13f9609a9723c8e87e66d615863533
|
Provenance
The following attestation bundles were made for qualmly_mobile-1.0.1.tar.gz:
Publisher:
publish-mobile-pypi.yml on DarkPixel-Z/qualmly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qualmly_mobile-1.0.1.tar.gz -
Subject digest:
91c6589d7d0541e16421d1dc8c9a71071c9ee5896a19914e50cda948a9779090 - Sigstore transparency entry: 1437701734
- Sigstore integration time:
-
Permalink:
DarkPixel-Z/qualmly@cc012624b796daa3d2c6fbfcfaa1dd2efba02e8c -
Branch / Tag:
refs/tags/mobile-v1.0.1 - Owner: https://github.com/DarkPixel-Z
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-mobile-pypi.yml@cc012624b796daa3d2c6fbfcfaa1dd2efba02e8c -
Trigger Event:
push
-
Statement type:
File details
Details for the file qualmly_mobile-1.0.1-py3-none-any.whl.
File metadata
- Download URL: qualmly_mobile-1.0.1-py3-none-any.whl
- Upload date:
- Size: 9.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74a62d8b8540e100614f7e985f590343cca583b3d0469f3a469c95f829f20ec6
|
|
| MD5 |
aefa428f47215d1933ca54b7e367359a
|
|
| BLAKE2b-256 |
170a26c6767d72539eba9f0f5f710846cb8b568c7825efa395eadbb78fa22679
|
Provenance
The following attestation bundles were made for qualmly_mobile-1.0.1-py3-none-any.whl:
Publisher:
publish-mobile-pypi.yml on DarkPixel-Z/qualmly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qualmly_mobile-1.0.1-py3-none-any.whl -
Subject digest:
74a62d8b8540e100614f7e985f590343cca583b3d0469f3a469c95f829f20ec6 - Sigstore transparency entry: 1437701742
- Sigstore integration time:
-
Permalink:
DarkPixel-Z/qualmly@cc012624b796daa3d2c6fbfcfaa1dd2efba02e8c -
Branch / Tag:
refs/tags/mobile-v1.0.1 - Owner: https://github.com/DarkPixel-Z
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-mobile-pypi.yml@cc012624b796daa3d2c6fbfcfaa1dd2efba02e8c -
Trigger Event:
push
-
Statement type: