Skip to content

feat: 原生吸收 RE 工具能力 — 123 CLI 命令 / 117 MCP 工具 + 统一分发 + 技能文档#1

Open
CC11001100 wants to merge 94 commits into
masterfrom
feat/native-capability-absorption
Open

feat: 原生吸收 RE 工具能力 — 123 CLI 命令 / 117 MCP 工具 + 统一分发 + 技能文档#1
CC11001100 wants to merge 94 commits into
masterfrom
feat/native-capability-absorption

Conversation

@CC11001100

Copy link
Copy Markdown

概述

将约 36 个第三方 Android/移动逆向工具的功能与设计思路原生吸收进 jadx-ai-cli
而非以 monorepo 方式 vendored 它们的代码。产出:一个工具原生完成这些项目所做的静态分析,
通过既有的 CLI + MCP + daemon + 统一 server 四种传输层暴露。第三方项目仅作为移植参考保留在
gitignored 的 reference/ 中,代码不入库。

主要变更

原生静态分析命令(~123 CLI 子命令 / 117 MCP 工具)

按 MASVS 维度吸收:

  • 攻击面/清单:manifest-security-audit、permission-risk-map、dangerous-api-map、
    deep-link-audit、task-hijacking-scan、exported-provider-scan、intent-redirection-scan 等
  • 加密/密钥:crypto-scan、cryptographic-misuse-scan、hardcoded-crypto-scan、
    keystore-scan、token-storage-scan、cert-pinning-scan、secrets-scan(熵过滤+签名)
  • 网络:ioc-extract(--decode-defang 还原 hxxp/[.] defang)、api-endpoint-extract、
    network-traffic-scan、ssl-scan、webview-scan、firebase-scan
  • 注入:sql/command-injection、path-traversal(Zip Slip)、xxe、serialization
  • 存储/隐私:storage-scan、privacy-scan、location-scan、sim-info-scan、account-scan、
    bluetooth-scan、clipboard-scan
  • 韧性/反篡改:tamper-detection-scan、accessibility-scan、notification-listener-scan、
    biometric-scan、bypass-hook(双用途)
  • 原生/JNI:native-bridge-index、native-libs、native-lib-security(ELF checksec+
    加密常量)、il2cpp-metadata-scan、flutter-analysis
  • 结构化查询:find-classes(按父类/接口/注解)、string-xref、call-sites、smali
  • 外部适配器:apktool(graceful ToolNotInstalled + 超时)

统一分发

daemon / server / MCP 三条路径统一路由到 CommandDispatch(单一注册表 + 字段映射),
消除了此前 daemon 注册表遗漏 graph/hook/navigate/comment 的风险。新增命令仅需在
CommandDispatch 注册一处即对全部传输层生效。

磁盘缓存 & 符号索引

移植 GUI 的 DiskCodeCache(--cache-mode DISK)+ 零依赖平面文件 SymbolIndexStore
(index build/status,search --use-index 跳过反编译器加载),降低内存占用;离线安全(无 sqlite)。

技能文档

5 个分析技能(apk-security-audit / apk-reverse-engineer / crypto-analysis /
network-analysis / deobfuscation-workflow)从手动 search 流程改写为调用原生扫描器,
使新命令对 AI 工作流可发现、可编排。

验证

  • :jadx-ai-cli:test 全绿(52 tasks)
  • 跨传输层注册一致性审计:121 个分析命令全部可经 daemon/MCP/server 路由
    (daemon/index/mcp/server 为元命令,设计上不入 CommandDispatch)
  • 各 batch 以合成 dex 探针(正例)+ small.apk/FDex2.apk(负例,零误报)验证
  • 技能文档引用的命令已核验存在于注册表

🤖 Generated with Claude Code

CC11001100 and others added 30 commits July 1, 2026 22:39
…7 MCP tools

Absorbs the capabilities of 34 studied open-source RE/MCP tools as native
jadx-ai-cli features (not a monorepo of vendored code — the reference repos
stay gitignored under /reference/ as porting source only). All new commands
read jadx's parsed model / raw APK bytes via pure Java; no external process
except the apktool/adb/frida adapter trio.

New native static-analysis commands (MASVS-aligned scanners):
- secrets, ioc-extract (URL/IP/domain/endpoint), permission-risk-map,
  native-bridge-index, crypto-scan, manifest-audit, deep-link-audit,
  network-security-config, ssl-scan, webview-scan, framework-detect,
  storage-scan, intent-scan, sql/command-injection-scan, logging-scan,
  path-traversal-scan, serialization-scan, clipboard-scan,
  tamper-detection-scan, tapjacking-scan, dynamic-loading-scan,
  biometric-scan, accessibility-scan, keystore-scan, exported-provider-scan,
  notification-listener-scan, sms-scan, privacy-scan, task-hijacking-scan,
  firebase-scan, xxe-scan, intent-redirection-scan, cert-pinning-scan,
  debug-artifact-scan, deeplink-scan, backup-scan, screen-capture-scan,
  otp-interception-scan, pending-intent-scan, content-provider-scan,
  local-auth-bypass-scan, unsafe-export-scan, insecure-keystore-scan,
  token-storage-scan, webview-url-scan, insecure-api-scan,
  network-traffic-scan, ad-fraud-scan, runtime-integrity-scan,
  hardcoded-crypto-scan, permission-request-scan, data-residue-scan,
  subprocess-scan, cryptographic-misuse-scan, log-info-leak-scan,
  broadcast-scan, fragment-injection-scan, unsafe-encryption-scan,
  screenshot-leak-scan, trust-boundary-scan, insecure-deeplink-handler-scan,
  insecure-file-io-scan, sdk-inventory, bypass-hook, packer-detect,
  capability-report, native-lib-security, api-endpoint-extract,
  google-services-config, dangerous-api-map, manifest-security-audit,
  il2cpp-metadata-scan, flutter-analysis, source-quality-report,
  class-inventory, entrypoint-scan, dex-stat, custom-permission-audit,
  apk-signature, dead-code-report, method-complexity, resource-inventory,
  shared-uid-audit, device-admin-scan, vpn-service-scan, nfc-scan,
  sensor-scan, alarm-wakelock-scan

Batch 6 — hardware-capability deep dives (privacy-scan companion):
- bluetooth-scan, account-scan, location-scan, sim-info-scan

Batch 7 — structured code query + smali (absorbed from dex-analyzer-for-llm):
- smali (JavaClass.getSmali), find-classes (super/interface/annotation),
  string-xref (literal→method reverse lookup), call-sites (any API name),
  ioc-extract --decode-defang (recover hxxp/[.]/[at] indicators)

Infrastructure:
- commands/CommandDispatch.java — single source of truth; daemon/MCP/server
  all delegate here (closes the 4-site duplication + daemon gap)
- util/ExternalTool.java + AbstractExternalCommand — apktool/adb/frida adapters
  with graceful ToolNotInstalled + timeout
- util/ManifestUtil.java — shared manifest/resource loading
- cache/ + index/ — disk code cache + zero-dep flat-file symbol index
- server/ — unified server (TCP/MCP/HTTP) with project persistence
- jadx-ai-gui/ — GUI plugin port; jadx-mcp-server/ — Python bridge port
- docs/design/ — persistent-context + server-mode designs
- .claude/skills/ — workflow skills backed by native commands

Build gotcha: jadx-ai-cli/build.gradle.kts now mirrors jadx-cli's
runtimeOnly(plugin(":jadx-plugins:jadx-*-input")) block — without it jadx
loads 0 classes from any .dex/.apk.

Tests: ~114 CommandsTest methods, all green. Verified positive (synthetic
dex probes) + negative (small.apk/FDex2.apk zero false positives) on real
APKs (UnCrackable-Level3, InsecureBankv2, editor.apk, FDex2.apk).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
将 apk-security-audit / apk-reverse-engineer / crypto-analysis /
network-analysis / deobfuscation-workflow 五个技能从手动 search 流程
改写为调用已吸收的原生扫描器命令,使新增的 ~123 个 CLI 子命令对
AI 工作流可发现、可编排:

- network-analysis: ioc-extract --decode-defang / api-endpoint-extract /
  network-traffic-scan / ssl-scan / cert-pinning-scan / webview-scan /
  secrets-scan / firebase-scan 替换手动字符串搜索
- crypto-analysis: crypto-scan 系列 + keystore/token/cert-pinning 扫描器
  + smali/call-sites/string-xref 深挖
- deobfuscation-workflow: obfuscation-report / source-quality-report /
  packer-detect 分诊 + find-classes 按父类/接口/注解归类重命名
- apk-security-audit: 11 阶段 MASVS 对齐扫描器工作流
- apk-reverse-engineer: framework/packer-detect + smali + 结构化查询

所有引用命令已核验存在于 CommandDispatch 注册表。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
跨传输层注册审计发现 cfg 是唯一有分析价值却未经 MCP 暴露的只读命令
(reload/script 为状态变更/执行任意 JS 故正确排除,signature 与已暴露的
apk-signature 近重复)。补齐时又发现 cfg 存在潜伏 bug:

- CfgCommand 直接调用 DotGraphUtils.dumpToString(methodNode),但方法节点的
  基本块/指令只有在反编译 passes(BlockSplitter、区域构造等)跑过后才存在。
  刚加载的反编译器尚未处理任何方法,导致 getBasicBlocks() 与 getInstructions()
  均为 null,dumpToString 恒返回 null → 命令对任意方法都报 NoGraph(含最简单的
  onCreate/main),即该命令此前从不产出真正的 CFG。
- 修复:新增 ensureProcessed(),dump 前强制反编译父类以建块;abstract/native
  方法降级为 load() 取指令列表走单块 fallback。
- 经 small.apk MainActivity.onCreate 与 hello.dex HelloWorld.main 验证,
  现产出含基本块与指令的真实 dot 图。

同时在 McpToolDefinitions 增加 jadx_cfg 工具定义(class/method 必填,
cfgType basic|raw|region、format dot|text);MCP 派发器为纯名称翻译器,
无需额外接线。新增 CommandsTest.testCfgCommand。

验证::jadx-ai-cli:test 全绿;MCP tools/list 广播 jadx_cfg;
tools/call jadx_cfg 端到端返回 success + CFG。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
审计发现 3 个命令可经 CommandDispatch 路由(daemon/server/mcp 均可执行)
但未在 McpToolDefinitions 广告,MCP 客户端无法发现:
- jadx_signature:apksig v1/v2/v3/v3.1 签名校验 + 证书详情(只读高价值)
- jadx_reload:改名后重编译/卸载类代码,刷新缓存
- jadx_script:对已加载 decompiler 运行 JADX 脚本

同时把 script 的 --engine 接线进 CommandDispatch.script()(此前只读 script 路径,
广告的 engine 参数会被忽略)。

现 MCP 工具数 = KNOWN_COMMANDS = 121,全部可路由分析命令均可发现。
已验证:tools/list 含三者;tools/call jadx_signature 对 small.apk 返回
verified=true 的完整 V2 证书链。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
secrets-scan 只扫 Java 代码与 XML/ARSC/MANIFEST 资源,跳过 lib/<abi>/*.so;
攻击者常把 API key / token / 私钥搬进原生代码正是为了躲开 Java 层扫描器。

- 新增 util/SecretPatterns:21 条规则 + Shannon 熵检测,作为 secrets-scan 与
  native-libs 的唯一真相源;scanText(带熵)/ scanStrings(纯规则,逐串)。
- SecretsScanCommand 重构为委托 SecretPatterns.scanText,输出形状不变。
- NativeLibsCommand 对每个 .so 的 carved strings 跑 SecretPatterns.scanStrings,
  新增 secrets / secretCount 字段。
- SecretPatternsTest:4 项测试守护原生密钥检测(AIza/JWT/PEM/stripe、脱敏、
  零误报、limit 上限)。合成 .so 端到端验证命中 4 个注入密钥。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…round-truth

发现第三个"注册但静默失效"命令(继 cfg 之后):NativeLibSecurityCommand
的 readResourceBytes 无条件返回 null,导致每次运行 libraries 恒为空 —— 整个
ELF/checksec 能力从未真正跑过。旧测试用 hello.dex(无 .so)导致空结果看似正确。

修复:
- readResourceBytes 改用 ResourcesLoader.decodeStream(与 NativeLibsCommand 一致),
  按 ResourceType.LIB 过滤,64MiB 上限;删除 74-80 行死代码。

新增 util/ElfDynamicInfo —— 纯 Java 解析 PT_DYNAMIC 段,产出 readelf -d/checksec
无法用字符串启发式还原的 ground-truth:
- neededLibraries(DT_NEEDED 依赖库)、soname(DT_SONAME)
- runpath/rpath(DT_RUNPATH/DT_RPATH,库劫持面)
- Full vs Partial RELRO —— relroType 由 GNU_RELRO 段 + BIND_NOW
  (DT_BIND_NOW / DT_FLAGS&DF_BIND_NOW / DT_FLAGS_1&DF_1_NOW)判定
- fortifySourceFunctions(.dynstr 中 *_chk 加固导入计数)
通过 PT_LOAD vaddr→file-offset 映射解析 DT_STRTAB 名称;little-endian;永不抛异常。

验证:libcode.so 从恒空 → arch=x86_64/relroType=full/neededLibraries=[libm,libdl,
libc]/soname=libcode.so。新增 ElfDynamicInfoTest(3)直解 .so 字节 + CommandsTest
新增用真实含 .so 的 resources-only.apk 固件,守护读取修复与动态段字段。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
第四个同类 bug:global-metadata.dat 是二进制,magic 0xFAB11BAF 含 0xFA/0x1B/0xB1
等非 ASCII 字节。旧代码经 loadContent().getText().getBytes("ISO-8859-1") 读取 ——
jadx 的 getText() 对二进制要么 NPE(→"not found"),要么有损文本化,破坏所有偏移,
magic 校验恒挂,C# 符号表从未真正被提取。

修复:改用 ResourcesLoader.decodeStream(res,(size,is)->is.readNBytes(cap)) 读原始
字节(128MiB 上限),与 native-libs/native-lib-security 同一 proven idiom。

验证:合成含 magic+version29+字符串表的 global-metadata.dat 注入 APK →
found=true、version=29、classNames 正确恢复(UnityEngine/PlayerController/...);
旧路径下这些字节会被文本解码破坏。Il2cppMetadataScanTest 运行时用 zip 文件系统
注入合成固件,不提交二进制 blob。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e (nm -D)

native-lib-security relied on a strings heuristic for symbols (grep Java_/
known libc names), which misses renamed symbols and is blind on stripped
release libs. Add ElfSymbols: a pure-Java section-header walk of .dynsym +
linked .dynstr that recovers the REAL exported/imported functions. .dynsym
survives strip, so this works where the string scan is blind.

Wires into native-lib-security: exportedCount/importedCount/exportedFunctions/
importedFunctions/jniExports (the actual Java_*/JNI_OnLoad attack surface to
cross-ref native-bridge-index). Upgrades stripped-detection to real empty-
.dynsym truth and flags RegisterNatives-style dynamic JNI hiding (JNI_OnLoad
exported but no Java_* symbols). Canary check now prefers the parsed table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…binding map

New command cross-references Java-declared native methods (regex over the
parsed model) against the REAL Java_* symbols exported by each .so (.dynsym
ground truth via ElfSymbols). Classifies every native method:
- static  — matching mangled Java_* export exists (locatable in the binary)
- dynamic — no export in any lib => wired at runtime via RegisterNatives, the
  common tactic to hide the native surface from static analysis (needs Frida)

Also reports orphan exports (Java_* with no Java declaration). Implements
correct JNI short-name mangling (_→_1, ;→_2, [→_3, non-alnum→_0XXXX, overload
prefix match). Wired through the unified CommandDispatch (CLI + daemon + MCP
jadx_jni_binding_audit + server). Pairs native-bridge-index with
native-lib-security's jniExports into one actionable map.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add util/NativeBehavior: classifies a .so's capabilities from its imported
symbols (.dynsym ground truth) plus high-signal strings — anti-debug (ptrace),
dynamic-code-loading (dlopen/dlsym), self-modifying/unpacking (mprotect),
process-execution (execve/system/fork), networking (socket/getaddrinfo),
tls-or-crypto (EVP_/AES_/SSL_), raw-memory-mapping (mmap), syscall-obfuscation,
plus string-based root/debugger/emulator/tamper detection. Each tag carries
severity + concrete evidence. Imports are the honest signal regardless of
symbol renaming/obfuscation.

Wired into native-lib-security as a 'behaviors' field (parse cap bumped to
4000 so classification sees the full import set; output symbol lists sub-capped
to 200 via capList). JNIEnv ops (RegisterNatives/FindClass) are correctly out
of scope — they go through the function-table pointer, not imports.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
apk-signature already extracted certs + fingerprints + schemes, but stopped
short of a trust verdict. Enrich each cert with: debugCertificate (the fixed
CN=Android Debug SDK key => non-production, private key is public), keyAlgorithm
+ keySize (RSA/DSA modulus, EC field size), weakSignatureAlgorithm (MD5/SHA1),
selfSigned, and a per-cert warnings list. Roll up to top-level debugSigned /
weakSignatureAlgorithm / securityWarnings, plus a v1-only Janus-downgrade
warning. Verified on a real debug-signed fixture (correctly flags debug cert +
SHA1withRSA). No new deps — uses java.security key interfaces.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…AEP, static IV

Closes three high-value MASVS gaps the '/ECB'-only detector missed:
- implicit_ecb_mode: bare Cipher.getInstance("AES") defaults to ECB in JCA
- rsa_without_oaep: bare RSA / RSA/None/NoPadding / RSA/ECB/PKCS1Padding
- static_iv: hardcoded literal IV passed to IvParameterSpec (nonce reuse)

Patterns made package-private and unit-tested directly (positive+negative).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rity-config

An expired <pin-set expiration="..."> silently stops being enforced — Android
falls back to system trust, so pinning that looks configured isn't. Now parses
the expiration date and flags pin_set_expired (high). Also flags a single-pin
set with no backup (pin_set_no_backup, low) — key rotation would brick TLS.
isPastDate helper unit-tested (past/future/unparseable/whitespace).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…webview-scan

The most serious WebView flaw — an onReceivedSslError override that calls
handler.proceed() blindly accepts ANY invalid certificate (trivial MITM) — was
invisible to the per-line rules because the override spans lines. Adds a
class-level check (both onReceivedSslError and .proceed() present), a high
ssl_error_ignored finding, and a top-level sslValidationDisabled rollup.
hasSslBypass helper unit-tested (proceed vs cancel vs unrelated proceed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nalysis

The FLUTTER_VERSION regex claimed to read libflutter.so strings but was run over
decompiled io.flutter.* Java code and took the first x.y.z it saw — highly
false-positive, and the .so was never read. Now reads libapp.so/libflutter.so raw
bytes via the binary-safe decodeStream path (blutter's approach) and extracts the
Dart snapshot version stamp ('3.5.4 (stable) (...)') — ground truth. Adds
engineVersionSource + snapshotEmbeddedInLibApp; Java-code guess demoted to a
version-line-only fallback. asciiStrings/versionFromJava helpers unit-tested.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…intext JS bundle triage

Reads assets/index.android.bundle raw bytes (binary-safe decodeStream) and
parses the Hermes Bytecode File header: magic 0x1F1903C103BC1FC6, bytecode
version (offset 8), function/string counts. Distinguishes Hermes bytecode from
a plaintext JS bundle — the fact that decides the reversing path — and reports
the bytecode version so the right hermes-dec/hbctool build can be picked.

Security value not covered elsewhere: an RN app's logic and secrets live in
index.android.bundle, which secrets-scan/ioc-extract never open (Java-only).
This scans the bundle bytes with the shared SecretPatterns library + a
cleartext-URL pass, surfacing embedded keys and http:// endpoints, and flags a
plaintext bundle as directly-readable source.

Registered across CLI/CommandDispatch/MCP (jadx_react_native_analysis).
Tests: Hermes magic+version parse, plaintext-not-Hermes, URL de-dup/anti-swallow,
string recovery from binary, end-to-end wiring. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…y triage

Reads the actual bytes of assemblies/*.dll and assemblies.blob (binary-safe
decodeStream, 64-byte head) to answer what decides the .NET reversing path:
- XALZ (LZ4) per-assembly wrapper vs raw PE (MZ) — do the DLLs need
  decompressing before ILSpy/dnSpy/dotpeek
- AssemblyStore (XABA magic) blob packing, with entry count
- runtime flavor: classic Xamarin/Mono (mscorlib.dll) vs .NET 5+/MAUI
  (System.Private.CoreLib.dll / CoreCLR .so markers)

framework-detect only name-matches; this is the .NET analog of the
react-native/flutter/il2cpp binary-read commands — the real C#/IL logic lives
in the managed assemblies, invisible to a Java decompiler.

Registered across CLI/CommandDispatch/MCP (jadx_dotnet_analysis).
Tests: XALZ magic+size, PE MZ stub, XABA store, negative/short-buffer cases,
end-to-end wiring. Full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cret triage

Reads config.xml and assets/www/** (HTML/JS) — the files where a Cordova/Ionic/
Capacitor app's real logic, network/navigation whitelist, page CSP, and secrets
live, none of which the Java-only scanners (webview-scan/secrets-scan/
ioc-extract) ever open. framework-detect only name-matches Cordova assets.

Findings: access_origin_wildcard / navigation_wildcard (high — the RCE-pivot
control: remote content into the plugin-privileged WebView), intent_wildcard,
cleartext_whitelist; csp_missing / csp_unsafe (high — unsafe-inline/eval or bare
* source = unmitigated XSS in the privileged WebView). Plus plugins enumerated
from cordova_plugins.js and secrets/URLs scanned from the www JavaScript via the
shared SecretPatterns library.

Registered across CLI/CommandDispatch/MCP (jadx_cordova_analysis).
Tests: whitelist wildcard/cleartext classifiers, CSP extract + unsafe detection
(strict CSP and scoped wildcard host not flagged), end-to-end wiring. Suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… layout

Enumerating any .dll already surfaced Assembly-CSharp.dll, but the flavor logic
misread a Unity Mono game as classic Xamarin (both ship mscorlib.dll). Now the
bin/Data/Managed layout + Assembly-CSharp.dll pin runtimeFlavor='Unity (Mono)'
(checked before the generic Mono branch), with unityMono/hasAssemblyCSharp
fields and a note that the game logic is plain, directly-decompilable IL — the
Mono counterpart to il2cpp-metadata-scan. Flavor precedence extracted to a
package-private runtimeFlavor() helper and unit-tested (Unity-over-Mono).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
EMPTY_CHECK_SERVER/CLIENT only matched whitespace-only "{ }" bodies. A
trust-all checkServerTrusted is frequently NOT literally empty — "{ return; }",
"{ Log.d(...); }", or any body that silently returns for every cert is just as
insecure (CWE-295, MITM). Add a balanced-brace body extractor + VALIDATION_SIGNAL
guard: flag a check*Trusted body only when it neither throws nor delegates to a
real validator (checkServerTrusted/checkValidity/verify). The delegation guard
prevents false positives on the common safe pattern of wrapping the platform
TrustManager. matchBrace honors string/char/comment context so a brace in a
literal can't skew the depth count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dingIntents

The class doc promised a mutable finding when "no immutability flag is set",
but the RULES only matched an explicit FLAG_MUTABLE — GET_ACTIVITY and
GET_FOREGROUND_SERVICE patterns were declared and never used, so the textbook
exploitable case PendingIntent.getActivity(ctx, 0, intent, 0) was silently
missed (CWE-927, Google lint UnspecifiedImmutableFlag — a hard error at
targetSdk 34).

Replace the line-based FLAG_MUTABLE rule with a creator-call-site scan: for each
PendingIntent.getActivity/getActivities/getBroadcast/getService/getForegroundService,
extract the balanced-paren argument list (matchParen honors string/char/comment
context) and classify by its actual flags arg — FLAG_IMMUTABLE = safe, explicit
FLAG_MUTABLE = high mutable_pending_intent, neither = new
implicitly_mutable_pending_intent (high, or medium when the class references
FLAG_IMMUTABLE elsewhere via an indirected flag var). Also fixes the prior
FLAG_UPDATE_CURRENT-is-mutable misclassification (it is orthogonal to mutability).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e + zero-IV detection

Three correctness gaps in the cipher classifier:

1. RSA/ECB false positive: "RSA/ECB/PKCS1Padding" was flagged as insecure ECB,
   but "ECB" in an RSA transformation is a JCA naming artifact (single-block),
   not the block-chaining weakness — it is the recommended spelling. Now RSA is
   classified by its real risk (padding): PKCS#1 v1.5 / bare-RSA default →
   rsa_weak_padding (Bleichenbacher/padding-oracle), OAEP → clean.

2. Static GCM nonce missed: IV_LITERAL only matched IvParameterSpec. A fixed
   GCMParameterSpec nonce is far worse than a static CBC IV — nonce reuse under
   one key breaks confidentiality AND authenticity. New static_gcm_nonce (high).

3. Zero-IV form missed: IV_LITERAL regex `new byte\[\]` only matched the
   initializer form `new byte[]{...}` and skipped the common zero-IV form
   `new byte[16]`. Broadened to `new byte\[` (inline construction = not
   randomized = static by construction).

classifyCipher made package-private for direct unit testing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oVerify/priority)

Both unsafe-export-scan and deeplink-scan tested for an intent-filter with a
literal "<intent-filter>" match, which only matches the bare tag. Real manifests
overwhelmingly carry attributes on it:
  <intent-filter android:autoVerify="true">  (every App Links / deep-link handler)
  <intent-filter android:priority="...">      (common on receivers)

Consequences before the fix:
- unsafe-export-scan: a component whose only intent-filter is attributed read as
  having NO filter, so its implicit-export (pre-API-31 default true) was missed —
  a real exposed-surface false negative.
- deeplink-scan: worse — its INTENT_FILTER body-extraction pattern
  "<intent-filter>(.*?)</intent-filter>" skipped every autoVerify filter, i.e.
  exactly the verified App Links the scanner most needs to enumerate.

Fix: unsafe-export INTENT_FILTER -> "<intent-filter[\s>]"; deeplink INTENT_FILTER
-> "<intent-filter\b[^>]*>(.*?)</intent-filter>". Both hasIntentFilter checks now
use the pattern (dead field removed) via a package-private hasIntentFilter(body)
helper, unit-tested against autoVerify/priority/bare/none + a hyphenated lookalike.
Sibling ManifestAuditCommand/ManifestSecurityAuditCommand already used the correct
"<intent-filter" (no ">") form — left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…override

PreferenceActivity + EXTRA_SHOW_FRAGMENT injection (Android < 4.4, CVE-2013-2090
class) was flagged high unconditionally — even when the app had already applied
the documented mitigation by overriding isValidFragment(String). That override IS
the fix (it whitelists allowed Fragment classes), so flagging hardened code as a
high-severity injection was a false positive.

Now: when a class overrides isValidFragment(String), downgrade the finding from
preference_fragment_injection/high to preference_fragment_protected/info. The
override is detected as a method declaration `isValidFragment(<at-least-one-arg>) {`
so a bare comment/string mentioning the name does not count, and a zero-arg
overload does not match.

protectedByIsValidFragment(String) + IS_VALID_FRAGMENT_OVERRIDE are package-private
for a synthetic-input unit test (FragmentIsValidFragmentGuardTest, 5 cases).
…ld.FINGERPRINT

The emulator rule listed a bare `Build.FINGERPRINT` as a marker, so any legit code
reading the device fingerprint (crash reporting, device fingerprinting, risk control)
was misreported as emulator detection. Real emulator detection compares the fingerprint/
model/hardware/brand against concrete emulator values — only those count.

Fix:
- Drop the bare `Build.FINGERPRINT` token from the emulator regex.
- Add the concrete comparison values an emulator check actually uses: google_sdk,
  vbox86p, sdk_gphone64, generic_x86_64, "Android SDK built for x86".
- Add a `Build.<field>.<compare>("generic")` form so Build.FINGERPRINT.startsWith
  ("generic") / Build.BRAND.equals("generic") still fire, while the bare field read
  `Build.FINGERPRINT;` (no method-call + generic arg) does NOT — exactly the FP fix.
- Extract EMULATOR_PATTERN as a package-private constant (declared before RULES for
  static-init order) so a synthetic-input test can assert the guard.

TamperEmulatorFingerprintGuardTest: 6 cases — bare read no-fire, comparison fires,
concrete values fire, isEmulator helper fires, non-Build "generic" no-fire, qemu/
Genymotion still fire.
…ra sources

INTENT_SOURCE only matched getParcelableExtra( / getParcelable(, missing
getParcelableArrayExtra( and getParcelableArrayListExtra(. An attacker can deliver
a nested Intent as an element of a Parcelable[] or ArrayList<Parcelable> just as
well as a single value, and the victim iterates and launches it — a real
CWE-927 redirection variant. The bare `getParcelable\s*\(` term does NOT match these
(the char after `getParcelable` is `A`, not `(`), so the class-level source gate
silently skipped affected classes and dropped the redirect.

Add explicit terms for getParcelableArrayExtra / getParcelableArrayListExtra.
INTENT_SOURCE is package-private for a synthetic-input test
(IntentRedirectionSourceTest, 6 cases): single/array/array-list/parseUri sources
match; getStringExtra/getIntExtra do not; array-list forms match on their own term.
…E↔FILE_SINK

provider_path_traversal required FILE_SINK and URI_SOURCE on the SAME line. A real
openFile reads uri.getLastPathSegment() on one line and builds `new File(root, name)`
on another, so the finding almost never fired on the common form.

Fix: match URI_SOURCE at CLASS scope (classHasUriSource), keep FILE_SINK as the
per-line anchor for lineNumber, keep the class-level canonical-guard suppression
(getCanonicalPath/startsWith/normalize). Only one path-traversal finding per class
(reportedPathTraversal flag) so multiple File sinks don't spam.

providerPathTraversalSignal(classHasOpenFile, classHasUriSource, classGuardsPath,
line) is package-private for a synthetic-input test (ExportedProviderPathTraversalTest,
5 cases): cross-line form now fires; canonical guard suppresses; no-openFile context
does not; same-line form still fires (no regression); URI source without a File sink
does not.
…+ activity-alias mistype

Two real parsing bugs:

1. findComponentEnd on a self-closing component (<activity .../>) `continue`d
   without returning, so compBlock scanned past the self-closing tag and absorbed
   the FOLLOWING component's attributes — cross-component exported/intent-filter
   misattribution (a self-closing non-exported activity followed by an exported
   service would read the activity as exported). Fix: at depth 0 a self-closing
   tag ends the component at its own `>`+1.

2. ACTIVITY_PATTERN was `activity[^>]+android:name` — `activity` is a prefix of
   `activity-alias`, so every <activity-alias> was mis-typed as an activity (and
   `\b` alone still matches at the y/- boundary, so a `(?!-alias)` lookahead is
   required). activity-alias now has its own pattern and is reported under its own
   type. service/receiver/provider patterns get `<tag\b` word boundaries.

findComponentEnd, ACTIVITY_PATTERN, ACTIVITY_ALIAS_PATTERN are package-private for
ManifestSecurityAuditParseTest (6 cases): self-closing does not overreach; block
with nested children ends at its matching close; activity pattern rejects
activity-alias; activity-alias pattern matches it; self-closing alias does not
overreach.
…r dynamic-arg forms

CONCAT only matched `" +` / `+ "` (literal-variable concatenation). A command built with
String.format / StringBuilder.append / .concat / MessageFormat and fed straight into
Runtime.exec(...) or new ProcessBuilder(...) is the same injection class but never set
concat=true, so the line fell through to process_exec/info instead of command_injection/high.

Add EXEC_DYNAMIC_ARG: matched only inside an exec()/ProcessBuilder() argument (up to the
next `;`), so a String.format used for logging elsewhere on the line is not a FP.
isCommandConcatenation(line) is package-private (CONCAT || EXEC_DYNAMIC_ARG) for a
synthetic-input test (CommandInjectionDynamicArgTest, 6 cases): literal+ still fires;
String.format/StringBuilder.append/.concat feeding the sink fire; static exec does not;
String.format outside an exec sink does not.
…namic-arg forms

CONCAT only matched `" +` / `+ "`. A query built with String.format / StringBuilder.append
/ .concat / MessageFormat and fed into rawQuery/execSQL/compileStatement/query (e.g.
db.rawQuery(String.format("SELECT ... WHERE name='%s'", name), null)) is the same injection
class but CONCAT does not match, so the line hit `if (!concat) continue;` and the finding was
dropped entirely instead of flagged.

Add SQL_DYNAMIC_ARG: matched only inside a SQL-sink argument (up to the next `;`), mirroring
the command-injection fix. isSqlConcatenation(line) is package-private (CONCAT ||
SQL_DYNAMIC_ARG) for a synthetic-input test (SqlInjectionDynamicArgTest, 6 cases): literal+
still fires; String.format/StringBuilder.append/.concat feeding the sink fire; parameterized
queries do not; String.format outside a SQL sink does not.
CC11001100 and others added 30 commits July 2, 2026 07:09
… broad (asymmetric vs exported-provider-scan)

The sql_injection_provider rule used class-scope SQL_CONCAT: + of operands literally named
selection/sortOrder OR any rawQuery()/execSQL() call. That was asymmetric with
ExportedProviderScanCommand's already-precise provider_sql_injection (SQL_SINK ∧ dynamic-construction
on the same line) in two ways:

- Narrow: missed String.format / StringBuilder.append / .concat forms feeding a SQL sink, and +
  concatenation of any operand NOT named selection/sortOrder (e.g. "..." + name). All are the same
  IPC-reachable CWE-89 class jadx produces.
- Broad: flagged parameterized rawQuery("...?", selectionArgs) as a false positive — any rawQuery call
  fired regardless of whether its argument was dynamically built.

Replaced with line-scope SQL_SINK ∧ (CONCAT ∨ PROVIDER_SQL_DYNAMIC_ARG), mirroring the unified
sink+dynamic-arg form already applied to sql-injection-scan / command-injection-scan /
exported-provider-scan. Parameterized queries no longer false-positive; String.format/StringBuilder/
.concat/+ of any operand now all fire. The three Patterns are package-private and covered by a
synthetic ContentProviderSqlDynamicArgTest (8 cases: every dynamic form fires, parameterized and
static rawQuery do not).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… jadx's final/synchronized/static echo

The five CRUD/openFile method-signature anchors (QUERY/INSERT/UPDATE/DELETE/OPEN_FILE_METHOD) used
`public\s+ReturnType\s+methodName\s*\(`. jadx echoes method modifiers via AccessInfo.makeString,
placing `final`/`synchronized`/`static` BETWEEN `public` and the return type — so a provider that
seals or locks its CRUD methods (`public final Uri insert(...)`, `public synchronized Cursor
query(...)`, common for thread-safe or subclass-sealed providers) did NOT match. The method went
unrecognised, silently dropping unvalidated_insert / unvalidated_query / sql_injection_provider /
etc.

Unlike DataResidueScan and ExportedProviderScan (whose provider gates have an `extends
ContentProvider` fallback — confirmed-and-declined here), this command has NO inheritance fallback
for method recognition: the signature regex IS the only signal, so the miss is fatal.

Widened `public\s+` → `public\s+(?:(?:final|synchronized|static)\s+)*` on all five anchors;
package-private + ContentProviderMethodSignatureModifierTest (plain/final/synchronized/static/combo
all match; non-provider methods of the same return type don't).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ree-form RPC entry point (CWE-862)

ContentProvider.call(String method, String arg, Bundle extras) is a free-form RPC entry point NOT
tied to the URI/selection permission model the CRUD methods use — an exported provider's call() is
an arbitrary method-dispatch channel any caller reaches with resolver.call(uri, "method", ...).
The scanner previously covered only query/insert/update/delete, so a provider that exposed
privileged operations ONLY via call() (no CRUD override, no getCallingPackage check) passed every
check — a real IPC privilege surface (CWE-862, multiple CVE precedents of settings/state mutation
exposed via call()).

Added CALL_METHOD signature anchor (same final/synchronized/static modifier gap + inlined
@param-annotation gap as the batch-23 CRUD anchors) and an unvalidated_call/high finding wired into
the existing !classHasCallerCheck branch. Package-private + ContentProviderCallMethodTest (plain/
final/synchronized/@NonNull-param/combo all match; non-call Bundle methods and callBack don't).
MCP description + @command description + class javadoc updated.

This is the first of the batch-24 coverage-gap sweep (real-world vuln patterns no detector covered).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vider path XML (CWE-22/CWE-732)

The file_provider_misconfig rule only flagged FileProvider *usage* in code with an info "verify path
config" note — it never read res/xml/file_paths.xml, so a real <root-path path=""/> or
<external-path ... path="/"/> exposure passed silently. With FLAG_GRANT_READ_URI_PERMISSION any holder
of the content URI reads the entire filesystem root or external storage (CWE-22/CWE-732, MASVS
MSTG-STORAGE-10).

Added BROAD_FILE_PATH pattern (<root-path>, external-path/external-cache-path/external-files-path/
files-path/cache-path with path="/" or "", grant-all-permissions) and a resource-scan pass over
decoded XML resources (mirroring firebase-scan's resource walk). A broad-path XML now emits
file_provider_misconfig/HIGH (vs the code-level info note which stays for plain FileProvider usage,
deduped by resource name). Package-private + InsecureFileIoBroadFilePathTest (root/external-root/
empty/grant-all fire; scoped paths and empty <paths> don't). MCP description + class javadoc updated.

Second of the batch-24 coverage-gap sweep (real-world vuln patterns no detector covered).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-1004) + modern WebMessageListener JS bridge

Two real WebView attack surfaces no detector covered:
- setAcceptThirdPartyCookies(true) / setAcceptCookie(true) + CookieManager.getInstance().getCookie —
  web content (or MITM-injected JS) exfiltrates the host app's session cookies incl. httpOnly http
  cookies → account takeover (CWE-1004). Added THIRD_PARTY_COOKIE (high) + COOKIE_READ (medium).
  THIRD_PARTY_COOKIE widens to [^;]*?true because the true arg follows the receiver
  (setAcceptThirdPartyCookies(webView, true)) — same sink+arg widening as the SQL/exec rules; the
  [^;] boundary keeps setAcceptThirdPartyCookies(webView, false) from firing.
- addWebMessageListener / WebMessageListener / onPostMessage(WebMessage) — the post-API-23 two-way
  JS bridge that bypasses the addJavascriptInterface rule, so a migrated bridge looked clean.
  Added WEB_MESSAGE_LISTENER (medium).

The three Patterns are package-private (Rule gained a Pattern-arg ctor overload) and covered by
WebviewCookieAndMessageListenerTest. MCP description + @command description + class javadoc updated.

Third of the batch-24 coverage-gap sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…en leak (CWE-200, MSTG-STORAGE-7)

A Notification/NotificationChannel carrying sensitive content (OTP/auth/payment/message preview) but
without setVisibility(VISIBILITY_PRIVATE) / setLockscreenVisibility shows its full body on the
lockscreen — anyone holding the device reads the OTP/preview (CWE-200, MASVS MSTG-STORAGE-7). No
detector covered this; OtpInterceptionScan only used NotificationChannel as an interception heuristic,
never checked lockscreen visibility.

Added NOTIFICATION_BUILD + LOCKSCREEN_PRIVATE patterns and a class-level signal
(NOTIFICATION_BUILD ∧ SENSITIVE_CONTENT ∧ ¬LOCKSCREEN_PRIVATE) emitting
notification_lockscreen_leak/medium. Gate ACTIVITY_MARKER widened to admit notification-building
classes (NotificationCompat/Notification.Builder/NotificationChannel). SENSITIVE_CONTENT made
package-private. ScreenshotLeakNotificationLockscreenTest covers OTP/auth firing, VISIBILITY_PRIVATE/
VISIBILITY_SECRET non-firing, non-sensitive and no-build non-firing. MCP + @command description +
class javadoc updated.

Fourth of the batch-24/25 coverage-gap sweep.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two same-line AND detectors silently missed the common jadx-decompiled form,
where the untrusted source extraction and the security-sensitive sink land on
separate lines. Both now pair a class-scope source with a line-scope sink,
mirroring the existing ExportedProviderScan / PathTraversal cross-line model.

trust-boundary-scan external_data_sql (high):
  Old: getIntent.*rawQuery|getStringExtra.*query|...  (same-line only)
  Missed: String n = getIntent().getStringExtra("n");
          db.rawQuery("SELECT * FROM t WHERE n='" + n + "'", null);
  Now: class-scope IPC_SOURCE (Intent/Bundle extras) ∧ first EXTERNAL_DATA_SQL_SINK
       line (rawQuery/execSQL/query/insert/update/delete/compileStatement).
  Dropped the dead same-line EXTERNAL_DATA_SQL Pattern; RULES no longer carries it.

insecure-deeplink-handler-scan deeplink_auth_decision (high):
  Old: getQueryParameter.*(?:isAdmin|role|auth|token|session)  (same-line only)
  Missed: String role = uri.getQueryParameter("role");
          if (role.equals("admin")) { grantAdmin(); }
  The keyword lives on the decision line, not the extraction line.
  Now: class-scope DEEPLINK_SOURCE ∧ first AUTH_DECISION_SINK line
       (grantAdmin/verifyToken/isAuthenticated/role.equals("admin")/...).
  Moved out of RULES into a dedicated class-scoped branch alongside the
  path-traversal / sql / webview / class-load cross-line fixes.

Each fix is backed by a package-private Pattern + signal-method unit test that
asserts cross-line fire and source-absent / sink-absent suppression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…able-indirect IV/key

Three real-world jadx-decompiled shapes that hardcoded-crypto-scan silently missed.
All are per-line fixes (the scanner is per-line, ONE/class per kind) — no cross-line
rework needed; closing the source-line gap is enough to flag the class.

hardcoded_key_bytes — pure-decimal byte array (HIGH value):
  jadx emits byte values <128 as bare decimals with no 0x prefix and no (byte) cast:
    byte[] bArr = new byte[]{65, 66, 67, 68};
  This is the most common dex2c/packed-layout key-array shape. The hex/cast arms alone
  missed it. Added a decimal arm (\d{1,3}\s*[,}]) that requires the digit to be followed
  by , or } so a sized new byte[16] (no body), an empty {}, and a method signature
  void f(byte[] p) do not fire.
  Reverses a prior false negative: HardcodedCryptoJadxFormTest asserted decimal arrays
  must NOT fire ("avoid noise") — that premise was wrong; decimal arrays ARE real key
  material. Test flipped to assert fire.

hardcoded_iv — variable-indirect source line:
  byte[] iv = "0102030405060708".getBytes();
  ...
  cipher.init(ENCRYPT_MODE, key, new IvParameterSpec(iv));
  The sink line new IvParameterSpec(iv) has no literal; the inline arm missed it.
  Added a source-line arm (byte[]\s*\w*\s*=\s*"[^"]{8,}"\.getBytes) with an {8,} floor.

hardcoded_symmetric_key — variable-indirect source line (name-gated):
  byte[] keyBytes = "MySecretKey123".getBytes();
  ...
  new SecretKeySpec(keyBytes, "AES");
  Added a name-gated source-line arm (\w*(?:key|Key|aes|Aes|secret|Secret)\w*|KEY[A-Z_]*)
  so a generic non-key buffer (byte[] data = "...".getBytes()) does not fire, while an
  iv-named buffer is left to the IV rule.

Each arm is package-private + covered by a synthetic-input unit test asserting fire and
the relevant FP suppressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…wallow trust-all

Two real-world TLS bypass shapes that ssl-scan silently missed. Both verified
against actual javac→d8→jadx decompiled output, not推测.

hostname_verifier — d8-desugared lambda (HIGH):
  `new OkHttpClient.Builder().hostnameVerifier((h, s) -> true)` is one of the
  most common OkHttp bypasses. d8 desugars the lambda into an anonymous
  HostnameVerifier whose verify() body is a synthetic BRIDGE call
  (return X.lambda$m$N(...)), and the real `return true;` lives in the synthetic
  lambda$m$N method — whose name is never `verify`. VERIFY_TRUE matched neither.
  Added LAMBDA_TRUE (lambda$name$index(...)\{ return true; }) paired with a
  class-scope hostnameVerifier( installer call (HOSTNAME_VERIFIER_CALL), gated by
  tlsClass. The lambda$m$N naming is a javac/d8 strong convention, so this does
  not fire on a plain verify(){return true;} (handled by VERIFY_TRUE) or on a
  lambda returning false.

trust_all_manager — try/catch swallow (HIGH):
  A checkServerTrusted body that APPEARS to validate — it calls checkValidity()
  (a VALIDATION_SIGNAL) — but wraps the call in `try { ... } catch (e) {}` that
  swallows the CertificateException is a real bypass: the validation never
  rejects. VALIDATION_SIGNAL alone misread it as safe. nonThrowingTrustBody now
  also flags a body that has a `catch` but no bare `throw` (jadx optimises a
  re-throwing catch{throw e} away, so catch + no-throw is specific to the swallow
  form and does not trip a genuine re-throwing handler).

Each fix is package-private + covered by a synthetic-input unit test asserting
fire and the relevant FP suppressions (re-throwing catch, plain verify, lambda
returning false, no installer call).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dingIntent wrapping

Two real-world CWE-927 redirection sinks that were silently missed.

intent_redirection/high — *AsUser broadcast variants:
  sendBroadcastAsUser / sendOrderedBroadcastAsUser / sendStickyBroadcastAsUser take a
  UserHandle and propagate the extracted nested Intent (even cross-user). The bare
  sendBroadcast\s*\( term did NOT match them — the char after `sendBroadcast` is `A`,
  not `(` — exactly the gap the sticky forms had. Added to LAUNCH_SINK as full high
  sinks (zero FP: the method names are exclusive).

pending_intent_redirection/medium — PendingIntent/TaskStackBuilder wrapping (NEW kind):
  Extracting a nested Intent and building a PendingIntent.get* (or
  TaskStackBuilder.makePendingIntent) is the Google Play-flagged CWE-927 variant: the
  caller triggers the PendingIntent with this app's identity, reaching non-exported
  components. These are NOT folded into LAUNCH_SINK — a class-level source ∧ sink
  heuristic cannot tell a PendingIntent built from the EXTRACTED Intent (redirection)
  from one built from a self-made `new Intent(...)` (the overwhelmingly common
  Notification case), so folding them into intent_redirection/high would flag every
  notification-building Activity that also reads an extra. New distinct kind at medium
  so the high pool stays clean; detail tells the human to confirm the data flow.
  makePendingIntent matched bare (chain tail like builder...makePendingIntent(, the
  method name is TaskStackBuilder-exclusive).

Verified against the existing source/sticky regression tests; new sink set covered by
a synthetic-input unit test asserting every form fires and that PendingIntent.get* is
NOT also a high launch sink (no double-fire).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…StackTrace

Two real-world log-leak shapes that log-info-leak-scan silently missed.

case-insensitive keyword matching (P1, zero new FP):
  Java/Android naming is camelCase (privateKey, accessToken, userToken,
  sessionKey, SecretKey) and log tags are often UPPER (Log.d("PASSWORD",...)).
  The keyword rules were case-sensitive — only underscore-lowercase forms
  (private_key, access_token) — so they missed the common Java identifier
  form. All 6 keyword rules now carry (?i). Mirrors LoggingScanCommand.SENSITIVE
  which was already (?i) with no FP; this only widens same-semantics word
  matching, no new false-positive surface.

printStackTrace sink + log_stacktrace rule (P2):
  e.printStackTrace() writes the full stack trace (and any exception message,
  which may carry SQL/path/secret-laden error text) to stderr/Logcat. The
  sister LoggingScanCommand.LOG_SINK already listed .printStackTrace(, but
  LogInfoLeakScanCommand.LOG_SINK_PREFIX omitted it — so a printStackTrace line
  bypassed the sink gate entirely and every rule was skipped. Added
  .printStackTrace( to LOG_SINK_PREFIX + LOG_MARKER gate, plus a dedicated
  log_stacktrace/info rule (a printStackTrace line usually has no
  password/token keyword, so the keyword rules would still miss it).

Verified: full suite green (the (?i) change does not break existing lowercase
assertions). New shapes covered by a synthetic-input unit test asserting
camelCase/UPPER fire, lowercase still fires, and printStackTrace fires the
dedicated rule where keyword rules miss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ceptor

HttpLoggingInterceptor at Level.BODY / Level.HEADERS writes the full HTTP
request/response payload — Authorization headers, tokens, PII — to Logcat. It is
as much a release-build data-leak surface as Chucker/Chuck (the in-app HTTP
inspectors already in the debug-lib list), but the list missed it entirely. A
class referencing only HttpLoggingInterceptor also failed the DEBUG_MARKER gate,
so it was never scanned.

Added as a debug_lib entry (http_logging_interceptor) alongside Chucker/Chuck,
and added to the DEBUG_MARKER gate so a class whose only debug artifact is
HttpLoggingInterceptor is still admitted. The pattern is a package-private
constant (HTTP_LOGGING_INTERCEPTOR) so a test can assert the canonical OkHttp
setup (new HttpLoggingInterceptor(); setLevel(Level.BODY/HEADERS)) fires and a
generic java.util.logging Logger / android.util.Log does not.

Surfaced by the log-info-leak audit: OkHttp body logging is configuration, not a
Log.* call, so it belongs here (debug-artifact) rather than in log-info-leak.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… bare-http + CLEARTEXT FNs

Three real-world false negatives found by auditing the scanner against jadx-decompiled forms:

- no_hostname_verify: the old ALLOW_ALL_VERIFIER third arm
  `HostnameVerifier\s*\{.*return true` was dead code — it required `{` to follow
  HostnameVerifier immediately, but Java/jadx syntax is `HostnameVerifier()` with a
  parameter list, so it never matched. Split out as a new package-private
  ANON_HOSTNAME_VERIFIER_TRUE pattern (DOTALL, class-scope) requiring the parameter
  list, so a real verifier (`return host.equals(h)`) does not fire. ALLOW_ALL_VERIFIER
  keeps the constant/install-call arms (per-line) and is now package-private.

- cleartext_http: HTTP_URL only matched complete `"http://..."` literals, so
  dynamically-assembled schemes (`"http://" + host`, `"http://".concat`, a `"http://"`
  constant) were missed. New HTTP_URL_BARE pattern catches the concatenated/constant
  form as its own rule (same kind/severity as the full-URL rule).

- insecure_okhttp_cleartext: ConnectionSpec.CLEARTEXT (an explicit cleartext-only
  OkHttp spec) was suppressed — the `connectionSpecs` token was listed in OKHTTP_SSL
  as a safe signal, so a builder calling `.connectionSpecs(CLEARTEXT)` was treated as
  TLS-configured. New CLEARTEXT_SPEC rule reports it independently, with a
  `(?!_AND_TLS)` lookahead so the safe CLEARTEXT_AND_TLS fallback does not fire.

Added NetworkTrafficAnonVerifierAndCleartextTest (12 cases) asserting the three
package-private patterns directly; updated javadoc / @command / MCP tool description.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ontent fold, js: URL, SR setSeed, FQN robustness)

Two detectors deepened against real javac→d8→jadx decompiled output (subagent did
actual compilation, not speculation).

webview-scan (2 fixes):
- mixed_content: the rule matched the MIXED_CONTENT_ALWAYS_ALLOW identifier, but
  WebSettings interface constants are compile-time-folded to integer literals in the
  bytecode; jadx decompiles the call as setMixedContentMode(0) (value 0 ==
  ALWAYS_ALLOW) with NO identifier, so the old pattern never matched any real input.
  New package-private MIXED_CONTENT_ALWAYS_ALLOW pattern matches the literal 0.
  (1 == NEVER_ALLOW and 2 == COMPATIBILITY_MODE do not fire.)
- js_loadurl: the cleartext-load rules only covered the http:// scheme; the most
  common WebView JS-injection / UXSS surface — loadUrl("javascript:...") — was missed.
  New package-private JS_LOAD_URL rule.

crypto-scan (2 fixes):
- static_secure_random_seed (high): SecureRandom.setSeed(<literal>) seeds the CSPRNG
  with a constant, making its output fully predictable. The class javadoc claimed
  coverage of insecure RNG but the implementation only matched new Random()/
  Math.random() and had ZERO detection for a static SecureRandom seed. New
  package-private SR_STATIC_SEED (gated by class-scope SECURE_RANDOM_USAGE) catches
  setSeed(0L)/setSeed(123)/setSeed(new byte[]{...})/setSeed(new byte[N]) and the
  chained/FQN forms; variable seeds (setSeed(someVar),
  setSeed(System.currentTimeMillis()), setSeed(generateSeed(n))) do NOT fire.
- FQN robustness: four new\s+<ShortName> patterns (SecretKeySpec, IvParameterSpec,
  GCMParameterSpec, Random) failed when jadx emitted a fully-qualified prefix (imports-
  off or an un-importable type — new javax.crypto.spec.SecretKeySpec(, new
  java.util.Random()). Each now carries an optional (?:[\w.]*\.)? prefix; verified
  against both imports-on and imports-off output. The Random arm now requires
  Random\s*\(\s*\) so Randomizer/etc. do not over-match.

Added WebviewMixedContentAndJsLoadUrlTest (10 cases) and
CryptoSecureRandomSeedAndFqnTest (16 cases), both asserting the package-private
patterns directly; updated javadoc / @command / MCP tool descriptions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e regex (false-positive amplifier)

PendingIntent.FLAG_IMMUTABLE / FLAG_MUTABLE are `static final int` constants, so javac/d8
fold them to their integer literals in the bytecode; jadx decompiles a creator call as
`PendingIntent.getActivity(..., 67108864)` — the identifier `FLAG_IMMUTABLE` NEVER appears.

The old FLAG_IMMUTABLE pattern matched only the identifier, so `hasImmutable` was ALWAYS
false. This is worse than a false negative: it flagged safe code as high-severity. Every
PendingIntent — even one explicitly created with FLAG_IMMUTABLE — was reported as
`implicitly_mutable_pending_intent` high, because the literal 67108864 was not recognized
as the immutable flag.

Fix: FLAG_IMMUTABLE / FLAG_MUTABLE now match the identifier OR the decimal literal OR the
hex literal (FLAG_IMMUTABLE = 0x04000000 = 67108864, FLAG_MUTABLE = 0x02000000 = 33554432),
mirroring the already-correct IntentScanCommand. Both patterns are package-private; the
balanced-paren args check now uses FLAG_IMMUTABLE.matcher(args).find() /
FLAG_MUTABLE.matcher(args).find() instead of the dead String.contains("FLAG_IMMUTABLE").

Added PendingIntentImmutableLiteralTest (10 cases) asserting the literal forms are
recognized and that an explicit-IMMUTABLE args span is no longer misclassified as mutable.

This is the same interface-constant compile-time-fold failure class documented in batch 30
(webview MIXED_CONTENT_ALWAYS_ALLOW); PendingIntentScan was missed because it produced a
false positive (amplifier) rather than a silent false negative.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e regex (false-positive amplifier)

WindowManager.LayoutParams.FLAG_SECURE is a `static final int` (= 8192 = 0x2000), so javac/d8
fold it to the integer literal; jadx decompiles the call as `setFlags(8192, 8192)` /
`addFlags(8192)` and the identifier `FLAG_SECURE` usually does NOT appear. Verified against
actual javac→d8→jadx output.

The old identifier-only patterns made hasFlagSecure / classHasFlagSecure ALWAYS false on real
decompiled output. This is the same false-positive-amplifier failure class as
PendingIntentScan's FLAG_IMMUTABLE (commit 1065da3): every Activity — even one that DID set
FLAG_SECURE — was flagged flag_secure_absent (screen-capture-scan) / flag_secure_missing
(screenshot-leak-scan) high, and the flag_secure_set / flag_secure_set defence-inventory
findings never fired.

Fix:
- ScreenCaptureScanCommand.FLAG_SECURE_ON_WINDOW: literal-8192 arms (within a setFlags/addFlags
  call, so a bare 8192 elsewhere does not false-positive) come FIRST; the identifier arms remain
  for source-form code. SCREEN_MARKER gate also admits the literal.
- ScreenshotLeakScanCommand.FLAG_SECURE_SET: same literal-8192 arms + identifier/flag_secure arms.
- ScreenshotLeakScanCommand.FLAG_SECURE_CLEARED: the dead `clearFlags(…FLAG_SECURE)` arm replaced
  with `clearFlags(8192)`; the setFlags(0, …) mask-out and boolean-flag arms stay.
- FLAG_SECURE_CONDITIONAL: documented as a best-effort limitation (the literal-8192 form cannot
  be reliably distinguished from a plain setFlags(8192, 8192) by a per-line regex without
  data-flow) — not dead code, just bounded.

Added FlagSecureLiteralTest (10 cases) asserting the literal-8192 forms fire, bare 8192 does
not false-positive, a defended Activity is no longer misclassified as absent, and
clearFlags(8192) fires the cleared rule. Existing ScreenCaptureFlagSecureAbsentTest and
ScreenshotLeakNotificationLockscreenTest stay green (identifier arms retained).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e regex

The overlay window-type constants (TYPE_APPLICATION_OVERLAY=2038, TYPE_SYSTEM_ALERT=2003,
TYPE_SYSTEM_OVERLAY=2006, TYPE_SYSTEM_ERROR=2010, TYPE_PHONE=2002) and FLAG_SECURE=8192 are
all `static final int` constants, folded to integer literals by javac/d8. jadx decompiles
`params.type = TYPE_APPLICATION_OVERLAY` as `params.type = 2038` and `setFlags(FLAG_SECURE,
FLAG_SECURE)` as `setFlags(8192, 8192)` — the identifiers NEVER appear. Verified against
actual javac→d8→jadx output (`layoutParams.type = 2038;`).

The old identifier-only arms were dead code on real decompiled output:
- overlay_window: fired only on the rare canDrawOverlays() runtime check; the common
  `.type = <overlayInt>` assignment form was missed. Also fixed a wrong constant name
  (TYPE_SYSTEM_ALERT_WINDOW does not exist; it is TYPE_SYSTEM_ALERT).
- flag_secure: the anti-screenshot defence-inventory rule never fired at all.

Fix: both rules now lead with a literal arm (`.type = (2038|2003|...)` for overlay types,
`setFlags/addFlags(…8192…)` for FLAG_SECURE) so the decompiled forms fire; the identifier
arms remain for source-form code. canDrawOverlays() and the SYSTEM_ALERT_WINDOW permission-
name string stay (the latter appears as a literal arg in code-level permission checks).

Added TapjackingOverlayLiteralTest (11 cases) asserting the literal forms fire, bare literals
do not false-positive, and the identifier forms still fire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…TYPE_VIEW_TEXT_CHANGED)

AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED is a `static final int` (= 16), folded to the integer
literal by javac/d8, so jadx decompiles `event.getEventType() == TYPE_VIEW_TEXT_CHANGED` as
`event.getEventType() == 16` and a `switch` on the event type as `case 16:` — the identifier
NEVER appears. Verified against actual javac→d8→jadx output.

The old keylogger rule matched ONLY the identifier `TYPE_VIEW_TEXT_CHANGED`, so the
high-severity keylogger finding — the signature banking-trojan primitive that captures
keystrokes/field edits across apps via the accessibility event stream — fired on ZERO real
decompiled output. This is a silent false negative (the rule looked comprehensive but never
matched).

Fix: the rule now matches `getEventType() == 16` (the comparison form) and `case 16` (the
switch form) — both scoped to accessibility classes by the existing ACCESSIBILITY_MARKER gate
— plus the identifier arm for source-form code. TYPE_VIEW_CLICKED (=1) etc. do not fire.

Added AccessibilityKeyloggerLiteralTest (7 cases) asserting the literal forms fire and that a
non-16 event type / a bare getEventType() call do not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… + dead motion/orientation/proximity rules)

Sensor.TYPE_* (TYPE_ACCELEROMETER=1, TYPE_GYROSCOPE=4, TYPE_MAGNETIC_FIELD=2, TYPE_PROXIMITY=8,
TYPE_LIGHT=5, TYPE_PRESSURE=6, TYPE_ROTATION_VECTOR=11, TYPE_LINEAR_ACCELERATION=10,
TYPE_GRAVITY=9, TYPE_ORIENTATION=3) are `static final int` constants, folded to their integer
literals by javac/d8, so jadx decompiles `getDefaultSensor(Sensor.TYPE_ACCELEROMETER)` as
`getDefaultSensor(1)` — the identifier NEVER appears. Verified against actual
javac→d8→jadx output (`sensorManager.getDefaultSensor(1);`).

The old code had TWO consequences of this dead-code-identifier failure:
1. The `sensorTypes` inventory was built with `code.contains("TYPE_ACCELEROMETER")` — always
   false on decompiled output, so the inventory was ALWAYS empty (the output field that
   promises which sensors an app touches showed nothing).
2. The `sensor_motion` / `sensor_orientation` / `sensor_proximity_light` rules matched ONLY
   the identifiers — dead code, zero hits — so the motion/orientation/proximity triage
   findings never fired (only the coarse `registerListener` rule did).

Fix:
- SENSOR_TYPES first column is now the integer literal value; the inventory loop captures the
  int arg of every `getDefaultSensor(<int>)` call (new package-private SENSOR_TYPE_CALL
  pattern) and maps it back to the human name. A bare 1/8 not in a getDefaultSensor() call
  does not false-positive.
- The motion/orientation/proximity rules now lead with `getDefaultSensor(<int>)` literal arms
  (1|4|10|9 for motion, 2|3|11 for orientation, 8|5 for proximity/light); the identifier arms
  remain for source-form code. TYPE_PRESSURE(6) does not fire any of the three.
- sensor_rate: documented that SENSOR_DELAY_* (FASTEST=0/GAME=1/UI=2/NORMAL=3) literals are
  too common to match reliably (false-positive heavy), so the rule keeps its setDelay/
  setReportingMode method-name arms; the identifier arms are dead but the rule stays alive.

Added SensorTypeLiteralTest (11 cases) asserting the literal forms fire, the inventory maps
int→name correctly, and bare/non-sensor ints do not false-positive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rld-readable mode, secure-processing)

Two more interface-constant compile-time-fold fixes (batch 31 cluster), both verified against
real javac→d8→jadx output.

insecure-file-io-scan — MODE_WORLD_READABLE / MODE_WORLD_WRITEABLE:
Context.MODE_WORLD_READABLE (=1) and MODE_WORLD_WRITEABLE (=2) are `static final int`
constants, folded to literals by javac/d8, so jadx emits `openFileOutput("f", 1)` and the
identifiers NEVER appear. The old identifier-only WORLD_READABLE / WORLD_WRITABLE arms were
dead code — the world_readable_file / world_writable_file findings fired only on the rarer
setReadable(true,false) / chmod 644 forms. Fix: literal arms match the mode arg of
openFileOutput / getSharedPreferences / getDir (mode 1/3 for readable, 2/3 for writable); the
setReadable/chmod arms stay. MODE_PRIVATE (0) does not fire. (WORLD_READABLE/WORLD_WRITABLE
now package-private.)

xxe-scan — FEATURE_SECURE_PROCESSING / ACCESS_EXTERNAL_DTD / ACCESS_EXTERNAL_SCHEMA:
These are `static final String` constants, folded to their string-literal URL values by
javac/d8, so jadx emits `setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true)`
and the identifiers NEVER appear. The old identifier-only HARDENED arms were dead code — a
parser hardened ONLY via FEATURE_SECURE_PROCESSING was misclassified as unhardened
(FALSE-POSITIVE AMPLIFIER: xxe_unhardened_parser high on safe code), because classHardened was
false. Fix: the literal-value substrings (secure-processing, accessExternalDTD,
accessExternalSchema) are matched; disallow-doctype-decl was already a literal value and stays;
the identifier arms remain for source-form code. (HARDENED now package-private.)

Added InsecureFileIoWorldReadableLiteralTest (11 cases) and XxeHardenedLiteralTest (8 cases),
both asserting the literal forms fire, the false-positive-amplifier cases (a hardened parser
recognized as hardened) are fixed, and the safe/identifier forms behave correctly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (flags & 2 literal)

ApplicationInfo.FLAG_DEBUGGABLE (= 2) is a `static final int` constant, folded to the integer
literal by javac/d8, so jadx decompiles `(flags & FLAG_DEBUGGABLE) != 0` as `(flags & 2) != 0`
— the identifier NEVER appears. Verified against actual javac→d8→jadx output.

The old debugger rule's identifier-only arms (ApplicationInfo.FLAG_DEBUGGABLE / FLAG_DEBUGGABLE)
were dead for this form. A pure FLAG_DEBUGGABLE check (with no isDebuggerConnected() call) was
missed — a silent false negative for one debugger-detection idiom.

Fix: the rule now leads with a `flags & 2` literal arm (the bitwise-AND form jadx emits) so the
decompiled form fires; the isDebuggerConnected/waitingForDebugger/Debug.* arms stay (they were
already live); the identifier arms remain for source-form code. `(flags & 4)` (a different bit)
and a bare 2 do not false-positive.

Added TamperDebuggerFlagDebuggableLiteralTest (6 cases).

Confirmed-and-declined the remaining batch-31 edge arms (method-name arm keeps the rule alive,
so NOT silent FN): RuntimeIntegrityScan GET_SIGNATURES (getSignatures()/getSigningInfo() arms
cover the runtime-verify idiom), PermissionRequestScan PERMISSION_GRANTED/DENIED
(checkSelfPermission() arm covers), IntentScan/ExportedProvider FLAG_GRANT_* (grantUriPermission()
arm covers). Their constant arms are redundant, not dead-code-amplifiers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d literal (TYPE_*_PASSWORD fold)

InputType.TYPE_TEXT_VARIATION_PASSWORD (=128) and TYPE_CLASS_TEXT (=1) are `static final int`
constants, folded by javac/d8, so jadx decompiles
`setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD)` as `setInputType(129)` and
`setInputType(TYPE_CLASS_NUMBER | TYPE_NUMBER_VARIATION_PASSWORD)` as `setInputType(18)` — the
identifiers NEVER appear. Verified against actual javac→d8→jadx output.

The old SENSITIVE_VIEW arms `setInputType.*TYPE_TEXT_VARIATION_PASSWORD` /
`setInputType.*TYPE_NUMBER_VARIATION_PASSWORD` were dead code on decompiled output — a password
field set up the standard way (the most common form) was not recognized as a sensitive view,
weakening the `sensitive_view_not_cleared` finding.

Fix: SENSITIVE_VIEW now leads with `setInputType(129|18)` literal arms (the decompiled password
/ number-password input-type combinations); the identifier arms remain for source-form; the
EditText.*password / passwordInput / android:password arms stay. Plain text (1) / number (2)
input types do not fire. (SENSITIVE_VIEW now package-private.)

This is the last interface-constant-fold edge arm from the batch-31 sweep; the cross-line
DOTALL audit dimension was separately confirmed CLOSED (0 real misses — prior commits
1b0069e / 655e090 covered every anonymous-class/method-body cross-line pattern).

Added ScreenshotLeakSensitiveViewLiteralTest (7 cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…safe code flagged high)

A new audit dimension: regex patterns whose `.*` or bare substring keywords matched
innocent code, flagging SAFE code as HIGH-severity vulnerabilities — the inverse of the
earlier constant-fold/DOTALL FN dimensions. Each was verified against real javac→d8→jadx
decompiled output (not source-form reasoning), then fixed and guarded by a unit test that
references the command's package-private Pattern.

1. unsafe-encryption-scan INSECURE_TLS: `TLSv1\b` word boundary sat between `1` and `.`,
   so "TLSv1.2" (modern secure) matched the insecure-TLS-1.0 arm — every TLS-1.2 app flagged
   high. Fix: negative-lookahead `TLSv1(?![.\d])` (same for PROTOCOL_TLSV1). Verified.
2. insecure-api-scan keyguard_dismiss: `isKeyguardLocked()` is a read-only query, not a
   bypass — removed; kept dismissKeyguard/requestDismissKeyguard. Extracted KEYGUARD_DISMISS
   field + added Pattern Rule overload for testing.
3. insecure-file-io-scan SENSITIVE_UNENCRYPTED: bare substring `key` matched
   `pref_key_cache.bin`, `auth` matched `author` — removed generic key/auth/session, kept
   concrete credential tokens (password/passwd/secret/credential/apiKey/accessToken/...).
4. broadcast-scan BROADCAST_SENSITIVE: same `key`/`auth`/`session` amplifier on
   `sendBroadcast.*putExtra("pref_key")` — now requires the credential token inside a
   putExtra("...") extra-NAME literal.
5. hardcoded-crypto-scan HARDCODED_IV source-line arm: `byte[] \w* = "...".getBytes` accepted
   any name, so a non-IV buffer (digest magic "SIGMAGIC9") in a crypto class was flagged
   hardcoded_iv high — now name-gated (iv|nonce|initVector|...), mirroring the symmetric-key
   arm; inline `IvParameterSpec("...")` still catches the un-named case.
6. cryptographic-misuse-scan MD5_SECURITY/SHA1_SECURITY: `MD5.*password`/`MD5.*verify` arms
   paired the token with arbitrary same-line substrings — a comment
   `// MD5 is broken - never hash passwords` or `Log.w("MD5 verify failed")` flagged high.
   Now restricted to MessageDigest/Signature/SecretKeyFactory.getInstance call context.
7. trust-boundary-scan INTENT_AUTH: `getIntent.*role` paired the getter with any same-line
   substring — a log line `Log.d("intent="+getIntent()+" role="+getRole())` flagged
   intent_auth_decision high. Now requires the keyword inside a getXxxExtra("...") extra-NAME
   literal.
8. trust-boundary-scan EXTERNAL_DATA_SQL_SINK: sink matched `.query/.insert/.update/.delete`
   (the parameterized/ContentValues APIs, SAFE by construction) — every DB class reading an
   Intent extra and writing via db.insert flagged external_data_sql high. Now restricted to
   rawQuery/execSQL/compileStatement AND requires a literal SQL + `+` concatenation (the
   injection shape); parameterized rawQuery("...=?", args) does not fire. Updated the legacy
   TrustBoundaryExternalDataSqlTest assertions that encoded the old over-broad semantics.

Recurring root cause: pairing `.*` with a bare dictionary substring (key/auth/role/login/
session/MD5+verify) without requiring the keyword inside an actual API call argument or
extra-NAME literal. All 8 fixes preserve true positives; MCP tool descriptions updated for
TLS and keyguard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eep (high-severity)

A new audit dimension, orthogonal to the prior three: Android SDK compile-time constructs that
jadx decompiles into a form source-form reasoning MISSES — causing silent high-severity FNs on
real-world obfuscation/externalization idioms. Each verified against real javac→d8→jadx output.

1. hardcoded-crypto-scan — StringBuilder-folded key/IV/nonce (the most common key obfuscation
   idiom). When a key is assembled via sb.append("part1").append("part2"), javac/d8 constant-
   folds the chain and jadx emits it with an EXTRA paren around the folded literal:
     SecretKeySpec(("MySuperSecretKey123456").getBytes(), "AES")
     IvParameterSpec(("0102030405060708").getBytes())
     GCMParameterSpec(128, ("01020304050607").getBytes())
   The inline arms required the literal immediately after \s*; the extra ( defeated them — the
   high-severity hardcoded_symmetric_key/hardcoded_iv rules missed the obfuscated form. Fix:
   \(? after \s* in HARDCODED_SYMMETRIC_KEY, HARDCODED_IV, HARDCODED_NONCE (now package-private).

2. hardcoded-crypto-scan — field-indirect key (static final String KEY = "..."). jadx does NOT
   inline a static final String field into its use sites (unlike static final int), so the
   declaration survives and the sink line `new SecretKeySpec(KEY.getBytes(), "AES")` has no
   literal — the old arms missed it. Fix: a name-gated String-field-declaration arm with an
   EXACT key-ish token list (KEY|SECRET_KEY|AES_KEY|ENCRYPTION_KEY|API_KEY|PRIVATE_KEY|aesKey|
   secretKey|encryptionKey|apiKey|privateKey) + \b, NOT a substring — so KEYWORDS/keyDesc/
   KEYBOARD do not fire (avoids reintroducing the batch-32 .*-substring FP pattern).

3. api-endpoint-extract — URLs assembled via String.format("%s/api", "https://host") and URLs
   stored in strings.xml/ARSC (loaded via getString(R.string.x)) never appear as a direct
   .url("...") argument, so the structured extractors missed them. Fix: enable the (previously
   dead) URL_LITERAL fallback to harvest any http(s):// literal in code (catches the format-arg
   URL), AND add a strings.xml/ARSC resource scan (mirrors FirebaseScanCommand/SecretsScanCommand)
   with a new --no-resources flag.

4. network-traffic-scan — a cleartext http:// URL stored in strings.xml/ARSC and loaded via
   getString(R.string.x) never appears in code, so the NETWORK_MARKER class gate (requiring an
   in-code http:// token) skipped the whole class — a silent FN that also masked
   hasCleartextTraffic. Fix: a resource scan pass (strings.xml/ARSC) with --no-resources flag.

MCP tool descriptions + optArgs updated for both new --no-resources flags. 2 new test files;
full suite EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…move 6 stale fields

A new audit dimension: `static final Pattern` fields DECLARED but NEVER REFERENCED — a dead
field means the detection it was meant to power never fires (silent FN). A subagent scanned all
131 command files / 464 Pattern declarations, counting name occurrences per file; 7 dead fields
confirmed by full-file read.

HIGH-impact (genuine FN — the field's comment described a real detection no live rule covered):
- ApiEndpointExtractCommand.RETROFIT_URL (`@Url\b`) — a Retrofit @Url-annotated method takes the
  full URL/path as a runtime argument, so NO literal path appears in code; the @get("...")-style
  extractors all miss it, and URL_LITERAL (literal-only) can't help. Now wired into execute as a
  `retrofit-@Url` dynamic-URL endpoint (method=DYNAMIC, path=<runtime @url argument>); made
  package-private for testing.

LOW-impact (redundant — a live rule or .contains() already covers the same signal; removed as
refactor cleanup):
- ManifestSecurityAuditCommand.EXPORTED_TRUE / INTENT_FILTER — the detection is implemented
  inline as compBlock.contains("android:exported=\"true\"") / .contains("<intent-filter"); the
  Pattern fields were stale leftovers from a refactor to .contains().
- ApiEndpointExtractCommand.OKHTTP_BUILDER (`Request.Builder()`) — weak existence signal, never
  wired; OkHttp endpoints are fully covered by OKHTTP_URL (.url("...")).
- ObfuscationReportCommand.BASE64_DECODE — string-decrypt candidate detection is covered by
  isDecryptShape() signature heuristics + REFLECTION count.
- FlutterAnalysisCommand.FLUTTER_JAVA — Flutter-Java-shell detection is covered by the
  `fullName.startsWith("io.flutter.")` class-name-prefix check.
- KeystoreScanCommand.USER_AUTH_LINE — setUserAuthenticationRequired(false) is already caught
  by the key_no_user_auth high rule (classRequiresAuth only matches =true, so a (false) call
  leaves classRequiresAuth=false → fires key_no_user_auth); the loose field was redundant.

1 new test (ApiEndpointRetrofitUrlTest). Full suite EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… rule anchors (silent high FN)

A new audit dimension: a scanner's class-level skip-gate (MARKER) must list every anchor its
per-line rules look for, or a class exercising ONLY a missed anchor is skipped at the gate and the
rule never fires — a silent FN. A subagent scanned all 67 scanners' gates against their rule
anchors; 4 verified asymmetries (each confirmed via real javac→d8→jadx that the missed anchor
appears alone in a realistic class).

1. local-auth-bypass-scan AUTH_MARKER was missing isUnlocked/isUserAuthenticated/isSessionValid/
   checkAuth/validateAuth/verifyAuth/isAuth (present in AUTH_METHOD). A backdoor method
   `public boolean isUserAuthenticated() { return true; }` (no biometric/fingerprint reference)
   matched no gate token → class skipped → auth_returns_true high never fired. THE most dangerous
   asymmetry — it hides literal backdoor auth-bypass methods.

2. webview-scan WEBVIEW_MARKER was missing CookieManager. A session/cookie-manager class reading
   the WebView cookie jar via CookieManager.getInstance().getCookie(url) has no
   WebView/WebSettings/WebViewClient/WebChromeClient token → skipped → cookie_read medium never
   fired.

3. insecure-deeplink-handler-scan DEEPLINK_MARKER was missing getDataString. jadx emits the
   variable form `intent.getDataString()` (Intent in a local), which does NOT match `getData\s*\(`
   (followed by S, not () nor `getIntent().getData` (it's `intent.`, not `getIntent().`). A handler
   reading the raw URI via a local Intent was skipped → deeplink_path_traversal /
   deeplink_sql_injection / deeplink_webview_load / deeplink_auth_decision high never fired.

4. hardcoded-crypto-scan CRYPTO_MARKER was missing the field-name anchors KEY_BYTES/keyBytes/
   SALT/salt/SEED/seed (the HARDCODED_KEY_BYTES / HARDCODED_SALT rule arms). A constant-holder
   class `static final byte[] KEY_BYTES = "...".getBytes()` with no other crypto token → skipped →
   hardcoded_key_bytes / hardcoded_salt never fired. (Gate looseness is safe — it only decides
   whether to scan; findings come from the rule Patterns. Deliberately did NOT add bare KEY/IV/
   nonce — too broad.)

All 4 gates made package-private for testing. 4 new test files (one per gate). Full suite EXIT=0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tant-fold

A 7th audit dimension: per-line scanner arms that weld two semantically-distinct
tokens with .* on ONE physical line. jadx frequently splits a logical statement
across multiple lines (build-then-send idiom, reused-local extraction, arg-list
wrapping) — when the two tokens land on different lines, the .*-welded arm never
matches → silent FN. Distinct from DOTALL-cross-line (batch 31: a single arm
intended to span lines but missing DOTALL) — here each arm is correctly single-line
but the REAL jadx form puts its tokens on separate lines.

A subagent scanned all 60 per-line scanners' .*-welded arms; candidates were then
verified via real javac→d8→jadx (NOT inference — several predicted splits turned
out to be inlined back by jadx, and ProcessBuilder/TLS were false alarms because
bare-literal fallback arms already caught them). 3 verified silent FNs fixed:

1. broadcast-scan BROADCAST_SENSITIVE_SPLIT (DOTALL): jadx emits the canonical
   build-then-send as 3 statements on 3 lines — `Intent i = new Intent(...);`
   `i.putExtra("password", pwd);` `sendBroadcast(i);`. All 3 per-line arms weld
   putExtra+sendBroadcast or credential+sendBroadcast on one line → none matches
   line 2 (putExtra, no send) or line 3 (send, no credential) → credential
   broadcast high FN. Added a class-level DOTALL fallback (putExtra("cred"...
   [\s\S]*?sendBroadcast() gated by reportedKinds dedup.

2. fragment-injection-scan FRAGMENT_FROM_INTENT_SPLIT (DOTALL): when the intent
   extra is read into a REUSED local, jadx keeps the local and splits across
   lines, renaming it (e.g. `stringExtra`) — `String stringExtra = getIntent()
   .getStringExtra("fragment_class");` / `Fragment.instantiate(this, stringExtra);`.
   Line 2 has instantiate but no getStringExtra; line 1 has getStringExtra but no
   instantiate → every per-line arm (instantiate.*getIntent/getStringExtra) misses
   → attacker-controlled Fragment class high FN. (When the extra is used ONCE
   jadx inlines it back to one line — caught by the per-line arms; the split only
   fires for the reused-local form.) Added DOTALL fallback gated by reportedKinds.

3. alarm-wakelock-scan WAKELOCK_PARTIAL folded-literal arm: PARTIAL_WAKE_LOCK is
   a static final int (=0x1) that javac/d8 folds to the integer literal 1 — jadx
   emits `newWakeLock(1, "tag")` and the identifier never appears. Both the
   `newWakeLock(.*PARTIAL_WAKE_LOCK` and bare `PARTIAL_WAKE_LOCK` arms miss →
   every real decompiled partial wakelock FN. Same class as the FLAG_SECURE/
   MODE_WORLD_READABLE folding (batch 31). Added `newWakeLock\s*\(\s*1\b` arm;
   extracted WAKELOCK_PARTIAL to a package-private Pattern field (+ Pattern-arg
   Rule ctor overload) so the test can assert it.

3 new test files (18 tests). Full suite EXIT=0.

Meta-lesson reinforced: ALWAYS compile a real snippet and read the actual jadx
output before trusting a predicted split — jadx inlines single-use locals (so
"variable-indirect" splits don't happen for single use) and bare-literal fallback
arms often already cover the split case (ProcessBuilder/TLS were false alarms).
Only splits forced by multi-statement structure (build-then-send) or reused
locals survive jadx's inlining.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gs without a signal

An 8th audit dimension: commands that cap a findings/list output at --limit but
do NOT emit a top-level `truncated` boolean. An AI consumer sees a capped list
and treats it as exhaustive — the tail findings (often the most dangerous, sorted
later or in later classes) are invisible. An FN amplifier distinct from the
detection-regex layers (batches 31-36): this is the OUTPUT layer, not the match
layer — even a perfectly-written rule's findings vanish past the limit with no
signal that they were dropped.

A subagent audited all 16 commands with limit-truncation logic but no `truncated`
field, classifying each by truncation shape (FLAT_LIST / MULTI_LIST / PER_ENTRY_LIST
/ NOT_TRUNCATING). Verified the 2 most insidious (SourceQualityReport: full
counters vs capped issues list; PackerDetect: isPacked computed inside the capped
loop) against actual source, then batch-fixed.

15 commands fixed (each adds `data.put("truncated", <bool>)`):
- FLAT_LIST: source-quality-report (truncated = errorMethods+stubMethods+obfuscatedClasses
  > issues.size() — full counters vs capped list), dead-code-report, google-services-config,
  packer-detect (low-limit edge: loop breaks before isPacked can be set), custom-permission-audit
  (downstream guard cross-ref operates on the truncated list), manifest-security-audit.
- MULTI_LIST: il2cpp-metadata-scan (4 name lists), ioc-extract (4 lists + endpoints subList
  double-truncation), obfuscation-report (3 capped + 2 bounded-excluded), cordova-analysis
  (secrets/urls + hardcoded-200 plugins cap flagged), dotnet-analysis (dllCount full vs
  assemblies capped), flutter-analysis (dartIdentifiers).
- PER_ENTRY_LIST: dangerous-api-map (per-permission hits cap, tracked via boolean),
  permission-risk-map (per-permission callSites cap, detected via size>=limit).
- limit*4 cap: entrypoint-scan (flagged the unusual multiplier for the AI consumer).

SearchCommand deliberately NOT changed: it emits a bare JSON array via
JsonOutput.list (not a wrapped object), so adding `truncated` is a consumer-visible
envelope change. Search returns top-N by design (paging primitive, not an exhaustive
scan), so the truncation semantics differ from scan commands. Documented as a design
decision; existing search tests use substring assertions unaffected by envelope shape.

1 contract test (TruncatedFlagContractTest, 12 tests) drives 11 fixed commands
against hello.dex/small.apk asserting the `truncated` field is present and boolean-
typed, plus a low-limit case asserting false serializes correctly. Full suite EXIT=0.

META-LESSON: the output layer is its own FN surface — a correct rule whose finding
lands past the limit is as good as no rule. The `truncated` flag is the minimal
contract that lets an AI re-run with a higher limit instead of silently trusting a
capped list. Eight audit dimensions now span both the match layer (31-36) and the
output layer (37).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on folded output

A 9th audit dimension (boolean-flag folding): top-level boolean output flags computed via
`code.contains("FOLDED_INT_IDENTIFIER")` are ALWAYS FALSE on real decompiled output, because
Android `static final int` constants are folded by javac/d8 to literals and jadx emits the
literal, not the identifier name. The AI consumer is told "no X problem" for EVERY app, even
ones that have it — a silent always-false signal. Same folding class as batches 31/36 fixed
for the per-line RULES, but the BOOLEAN FLAGS were missed.

A subagent systematically audited ALL ~67 scanners' boolean outputs. Only ONE remaining
instance: `partialWakeLockWithoutRelease` in AlarmWakelockScanCommand, computed via
`code.contains("PARTIAL_WAKE_LOCK")`. PARTIAL_WAKE_LOCK (=0x1) folds to `1`, so jadx emits
`newWakeLock(1, "MyTag").acquire()` and the identifier never appears — the flag was always
false. Every other boolean was verified sound (class/method names and `static final String`
constants like GPS_PROVIDER/BOOT_COMPLETED survive folding; per-line-Pattern-based booleans
already carry folded-literal arms).

Fix: reuse the WAKELOCK_PARTIAL Pattern (from batch 36, matches both the identifier and the
folded `newWakeLock(1,` form) instead of the bare `code.contains`, plus the existing
`!code.contains("release()")` check.

Verified end-to-end: built a real wakelock dex from Java source
(`newWakeLock(PARTIAL_WAKE_LOCK, ...).acquire()` with NO release) via javac→d8→jadx →
decompiles to `newWakeLock(1, "MyTag").acquire()`. The new wakelock-folded.dex fixture
asserts partialWakeLockWithoutRelease=true on the folded form (was always-false before).
Full suite EXIT=0.

The subagent's negative result (only 1 instance across 67 scanners) is itself the value:
the boolean-flag folding class is now closed, and the `static final int` (folds) vs
`static final String` (survives) distinction held up consistently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… transport asymmetries

A 10th audit dimension: three transports feed each command's options (picocli @option /
McpToolDefinitions optArg / applyArgs-or-dispatch args.get), hand-matched with NO automatic
kebab↔camel conversion. A drift between the MCP-declared optArg name and the args.get key is a
SILENT DROP: the MCP client sends the option, the command ignores it, the client thinks it took
effect. Distinct from the match (batches 31-36) and output (37-38) layers — this is the WIRING
layer between transports.

A subagent audited all ~120 tools' three naming sites. Verified findings:

SILENT DROPS (option sent but ignored):
1. jadx_dangerous_api_map: McpToolDefinitions declared `app_only` (snake_case) but
   DangerousApiMapCommand.applyArgs read `appOnly` (camelCase) — the lone snake_case optArg in
   the file, colliding with the camelCase reader. An MCP client setting app_only=false to include
   framework callers got appOnly=true (default) — framework callers silently excluded. Fixed:
   MCP def → `appOnly` (matches applyArgs + the camelCase convention used everywhere else).
2. jadx_list: McpToolDefinitions declared `limit` (default 100) but CommandDispatch.list() never
   read it — an MCP client asking for 500 classes got the internal default. Fixed by adding
   ListCommand `--limit` + a cap() helper applied to all 5 list return points (packages/classes/
   methods/fields + the index fast-path), and wiring `limit` through in list(). CLI default 0
   (no limit, backward-compat); MCP default 100 (protection for unknowing callers).

TRANSPORT ASYMMETRIES (CLI richer than MCP — capability gaps, now closed):
3. jadx_comment: dispatch read commentText/method/field/style/insnOffset (add/update/delete
   support) but MCP only declared class/type/query (read-only). The MCP tool was effectively
   read-only while the CLI supports full comment mutation. Fixed: added 5 optArgs + expanded the
   type enum (list/search/add/update/delete) and description.
4. jadx_search: dispatch read `searchParent` (--search-parent) but MCP didn't declare it. Fixed:
   added searchParent optArg.
5. jadx_ioc_extract: applyArgs read `decode-defang` (--decode-defang) but MCP only declared
   `defang`. Fixed: added decode-defang optArg.

Style note (NOT a drop, left alone): react_native_analysis/cordova_analysis use `min_entropy`
and smali/find_classes/string_xref use kebab (`max-chars`/`ignore-case`) — internally consistent
so no value is dropped, but break the camelCase convention. Future maintainers may "fix" one side
and introduce a real drop; flagged in memory, not changed (changing risks introducing a drop if
any client already sends the current spelling).

1 end-to-end test (ParameterWiringDriftTest): `list -t classes --limit 1` on small.apk caps the
class list to 1 (proves the cap fires; unlimited returns more). Full suite EXIT=0.

META-LESSON: the wiring layer between transports is its own silent-failure surface. A correctly-
named @option and a correctly-reading applyArgs still drop the MCP value if the optArg spelling
differs. The three-site hand-matching has no compiler check (Map<String,Object> keys are strings).
TEN audit dimensions now closed across three layers: match (31-36), output (37-38), wiring (39).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant