@clear-capabilities/agentic-security-scanner 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/CHANGELOG.md +1580 -0
  2. package/bin/.agentic-security/findings.json +1577 -0
  3. package/bin/.agentic-security/last-scan.json +1577 -0
  4. package/bin/.agentic-security/last-scan.json.sig +1 -0
  5. package/bin/.agentic-security/scan-history.json +465 -0
  6. package/bin/.agentic-security/streak.json +25 -0
  7. package/bin/agentic-security-audit.js +198 -0
  8. package/bin/agentic-security-consistency.js +80 -0
  9. package/bin/agentic-security-diff.js +136 -0
  10. package/bin/agentic-security-lsp.js +12 -0
  11. package/bin/agentic-security-mcp.js +40 -0
  12. package/bin/agentic-security-rule.js +153 -0
  13. package/bin/agentic-security.js +1683 -0
  14. package/dist/117.index.js +207 -0
  15. package/dist/178.index.js +250 -0
  16. package/dist/218.index.js +793 -0
  17. package/dist/227.index.js +192 -0
  18. package/dist/301.index.js +167 -0
  19. package/dist/384.index.js +18 -0
  20. package/dist/476.index.js +126 -0
  21. package/dist/513.index.js +373 -0
  22. package/dist/520.index.js +13 -0
  23. package/dist/601.index.js +1038 -0
  24. package/dist/634.index.js +1892 -0
  25. package/dist/637.index.js +216 -0
  26. package/dist/660.index.js +131 -0
  27. package/dist/675.index.js +451 -0
  28. package/dist/826.index.js +188 -0
  29. package/dist/830.index.js +133 -0
  30. package/dist/agentic-security.mjs +272 -0
  31. package/dist/agentic-security.mjs.sha256 +1 -0
  32. package/dist/calibration-seed.json +27 -0
  33. package/package.json +77 -0
  34. package/src/.agentic-security/findings.json +80844 -0
  35. package/src/.agentic-security/last-scan.json +80844 -0
  36. package/src/.agentic-security/last-scan.json.sig +1 -0
  37. package/src/.agentic-security/scan-history.json +8408 -0
  38. package/src/.agentic-security/streak.json +26 -0
  39. package/src/badge.js +188 -0
  40. package/src/compare.js +203 -0
  41. package/src/dataflow/.agentic-security/findings.json +3487 -0
  42. package/src/dataflow/.agentic-security/last-scan.json +3487 -0
  43. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  44. package/src/dataflow/.agentic-security/scan-history.json +735 -0
  45. package/src/dataflow/.agentic-security/streak.json +24 -0
  46. package/src/dataflow/CLAUDE.md +38 -0
  47. package/src/dataflow/access-paths.js +172 -0
  48. package/src/dataflow/async-sequencing.js +177 -0
  49. package/src/dataflow/backward.js +201 -0
  50. package/src/dataflow/catalog-expanded.js +485 -0
  51. package/src/dataflow/catalog.js +659 -0
  52. package/src/dataflow/cross-repo.js +219 -0
  53. package/src/dataflow/engine.js +588 -0
  54. package/src/dataflow/exception-flow.js +116 -0
  55. package/src/dataflow/exploit-prover.js +187 -0
  56. package/src/dataflow/higher-order.js +221 -0
  57. package/src/dataflow/ifds.js +347 -0
  58. package/src/dataflow/implicit-flow.js +129 -0
  59. package/src/dataflow/incremental.js +229 -0
  60. package/src/dataflow/index.js +181 -0
  61. package/src/dataflow/numeric-domain.js +192 -0
  62. package/src/dataflow/path-feasibility.js +114 -0
  63. package/src/dataflow/points-to.js +337 -0
  64. package/src/dataflow/polyglot.js +190 -0
  65. package/src/dataflow/proven-clean.js +159 -0
  66. package/src/dataflow/receiver-context.js +76 -0
  67. package/src/dataflow/sanitizer-proof.js +154 -0
  68. package/src/dataflow/soft-taint.js +140 -0
  69. package/src/dataflow/string-domain.js +234 -0
  70. package/src/dataflow/stub-aware-filter.js +100 -0
  71. package/src/dataflow/summaries.js +132 -0
  72. package/src/dataflow/symbolic-exec.js +238 -0
  73. package/src/dataflow/tabulation.js +135 -0
  74. package/src/engine.js +7763 -0
  75. package/src/history-scan.js +229 -0
  76. package/src/index.js +3 -0
  77. package/src/integrations/.agentic-security/findings.json +1504 -0
  78. package/src/integrations/.agentic-security/last-scan.json +1504 -0
  79. package/src/integrations/.agentic-security/scan-history.json +40 -0
  80. package/src/integrations/.agentic-security/streak.json +21 -0
  81. package/src/integrations/index.js +321 -0
  82. package/src/integrations/tickets.js +200 -0
  83. package/src/ir/.agentic-security/findings.json +3036 -0
  84. package/src/ir/.agentic-security/last-scan.json +3036 -0
  85. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  86. package/src/ir/.agentic-security/scan-history.json +364 -0
  87. package/src/ir/.agentic-security/streak.json +23 -0
  88. package/src/ir/CLAUDE.md +172 -0
  89. package/src/ir/callgraph.js +73 -0
  90. package/src/ir/class-hierarchy.js +195 -0
  91. package/src/ir/index.js +152 -0
  92. package/src/ir/parser-cs.js +260 -0
  93. package/src/ir/parser-java.js +286 -0
  94. package/src/ir/parser-js.js +413 -0
  95. package/src/ir/parser-kt.js +258 -0
  96. package/src/ir/parser-py-cst.js +136 -0
  97. package/src/ir/parser-py.helper.py +501 -0
  98. package/src/ir/parser-py.js +312 -0
  99. package/src/ir/ssa.js +315 -0
  100. package/src/ir/type-stubs.js +288 -0
  101. package/src/leaderboard.js +152 -0
  102. package/src/llm-validator/.agentic-security/findings.json +1891 -0
  103. package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
  104. package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
  105. package/src/llm-validator/.agentic-security/scan-history.json +168 -0
  106. package/src/llm-validator/.agentic-security/streak.json +20 -0
  107. package/src/llm-validator/consistency.js +141 -0
  108. package/src/llm-validator/index.js +437 -0
  109. package/src/lsp/.agentic-security/findings.json +28 -0
  110. package/src/lsp/.agentic-security/last-scan.json +28 -0
  111. package/src/lsp/.agentic-security/scan-history.json +79 -0
  112. package/src/lsp/.agentic-security/streak.json +22 -0
  113. package/src/lsp/server.js +275 -0
  114. package/src/mcp/.agentic-security/findings.json +8358 -0
  115. package/src/mcp/.agentic-security/last-scan.json +8358 -0
  116. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  117. package/src/mcp/.agentic-security/scan-history.json +1125 -0
  118. package/src/mcp/.agentic-security/streak.json +22 -0
  119. package/src/mcp/CLAUDE.md +54 -0
  120. package/src/mcp/audit.js +136 -0
  121. package/src/mcp/redact.js +75 -0
  122. package/src/mcp/server.js +158 -0
  123. package/src/mcp/stdio.js +83 -0
  124. package/src/mcp/tools.js +940 -0
  125. package/src/mcp/validate.js +49 -0
  126. package/src/personality.js +164 -0
  127. package/src/poc-video.js +239 -0
  128. package/src/posture/.agentic-security/findings.json +51239 -0
  129. package/src/posture/.agentic-security/last-scan.json +51239 -0
  130. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  131. package/src/posture/.agentic-security/scan-history.json +5557 -0
  132. package/src/posture/.agentic-security/streak.json +24 -0
  133. package/src/posture/CLAUDE.md +42 -0
  134. package/src/posture/adversarial-self-test.js +114 -0
  135. package/src/posture/adversary-agent.js +204 -0
  136. package/src/posture/agents-memory.js +135 -0
  137. package/src/posture/ai-code-fingerprint.js +171 -0
  138. package/src/posture/aibom.js +284 -0
  139. package/src/posture/api-inventory.js +96 -0
  140. package/src/posture/attack-playbooks.js +305 -0
  141. package/src/posture/auditor-agent.js +115 -0
  142. package/src/posture/auth-posture-import.js +135 -0
  143. package/src/posture/baseline-compare.js +114 -0
  144. package/src/posture/blast-radius.js +836 -0
  145. package/src/posture/bounty-prediction.js +141 -0
  146. package/src/posture/business-logic.js +239 -0
  147. package/src/posture/calibration-drift.js +93 -0
  148. package/src/posture/calibration-seed.json +27 -0
  149. package/src/posture/calibration.js +204 -0
  150. package/src/posture/clustering.js +75 -0
  151. package/src/posture/concurrency-checker.js +265 -0
  152. package/src/posture/confidence.js +65 -0
  153. package/src/posture/container-runtime.js +149 -0
  154. package/src/posture/counterfactual.js +109 -0
  155. package/src/posture/cross-lang-graphql.js +165 -0
  156. package/src/posture/cross-lang-grpc.js +166 -0
  157. package/src/posture/cross-lang-meta.js +101 -0
  158. package/src/posture/cross-lang-openapi.js +187 -0
  159. package/src/posture/cross-lang-orm.js +153 -0
  160. package/src/posture/cross-lang-queues.js +210 -0
  161. package/src/posture/crown-jewels.js +110 -0
  162. package/src/posture/custom-rules.js +361 -0
  163. package/src/posture/cve-alert-daemon.js +433 -0
  164. package/src/posture/cve-lookup.js +129 -0
  165. package/src/posture/dead-code.js +430 -0
  166. package/src/posture/defender-agent.js +158 -0
  167. package/src/posture/deploy-platform.js +204 -0
  168. package/src/posture/detector-fuzz.js +61 -0
  169. package/src/posture/deterministic.js +99 -0
  170. package/src/posture/drift.js +165 -0
  171. package/src/posture/epss.js +156 -0
  172. package/src/posture/exploitability-probability.js +212 -0
  173. package/src/posture/exploitability.js +121 -0
  174. package/src/posture/feature-flags.js +110 -0
  175. package/src/posture/finding-defaults.js +132 -0
  176. package/src/posture/fix-history.js +411 -0
  177. package/src/posture/fix-plan.js +121 -0
  178. package/src/posture/fix-verify-loop.js +157 -0
  179. package/src/posture/fix-verify.js +130 -0
  180. package/src/posture/flow-narration.js +105 -0
  181. package/src/posture/grader-calibration.js +156 -0
  182. package/src/posture/harness-discovery.js +113 -0
  183. package/src/posture/holdout-eval.js +144 -0
  184. package/src/posture/iac-reachability.js +163 -0
  185. package/src/posture/iam-policy.js +128 -0
  186. package/src/posture/integrity.js +97 -0
  187. package/src/posture/learning.js +166 -0
  188. package/src/posture/license-policy.js +109 -0
  189. package/src/posture/llm-redteam-prompts.js +418 -0
  190. package/src/posture/llm-redteam.js +303 -0
  191. package/src/posture/material-change.js +163 -0
  192. package/src/posture/mitigation-composite.js +55 -0
  193. package/src/posture/mttr.js +91 -0
  194. package/src/posture/network-policy-import.js +126 -0
  195. package/src/posture/path-predicates.js +99 -0
  196. package/src/posture/persona-prioritization.js +153 -0
  197. package/src/posture/poc-cwe-map.js +51 -0
  198. package/src/posture/poc-generator.js +500 -0
  199. package/src/posture/policy-gate.js +174 -0
  200. package/src/posture/pre-incident-archaeology.js +110 -0
  201. package/src/posture/profile.js +93 -0
  202. package/src/posture/reachability-filter.js +42 -0
  203. package/src/posture/regression-test-gen.js +200 -0
  204. package/src/posture/reverse-blast-radius.js +110 -0
  205. package/src/posture/router.js +109 -0
  206. package/src/posture/rule-overrides.js +198 -0
  207. package/src/posture/rule-pack-signing.js +209 -0
  208. package/src/posture/rule-packs.js +143 -0
  209. package/src/posture/rule-synthesis.js +108 -0
  210. package/src/posture/ruleset-version.js +71 -0
  211. package/src/posture/sbom.js +129 -0
  212. package/src/posture/schema-aware-bridge.js +207 -0
  213. package/src/posture/security-trend.js +87 -0
  214. package/src/posture/semantic-clone.js +114 -0
  215. package/src/posture/specification-mining.js +170 -0
  216. package/src/posture/stable-id.js +75 -0
  217. package/src/posture/stack-playbook.js +229 -0
  218. package/src/posture/streak.js +249 -0
  219. package/src/posture/suppressions.js +135 -0
  220. package/src/posture/telemetry-ingest.js +112 -0
  221. package/src/posture/threat-model.js +145 -0
  222. package/src/posture/three-agent-pipeline.js +74 -0
  223. package/src/posture/triage.js +146 -0
  224. package/src/posture/trust-boundary-diagram.js +115 -0
  225. package/src/posture/type-narrowing.js +129 -0
  226. package/src/posture/validator-metrics.js +179 -0
  227. package/src/posture/verifier-ephemeral.js +118 -0
  228. package/src/posture/verifier-target.js +147 -0
  229. package/src/posture/verifier.js +257 -0
  230. package/src/posture/version.js +75 -0
  231. package/src/posture/waf-ingest.js +200 -0
  232. package/src/posture/why-fired.js +141 -0
  233. package/src/pr-comment.js +172 -0
  234. package/src/pr-delta.js +198 -0
  235. package/src/report/.agentic-security/findings.json +79 -0
  236. package/src/report/.agentic-security/last-scan.json +79 -0
  237. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  238. package/src/report/.agentic-security/scan-history.json +332 -0
  239. package/src/report/.agentic-security/streak.json +23 -0
  240. package/src/report/index.js +1136 -0
  241. package/src/report/mascot.js +42 -0
  242. package/src/runScan.js +141 -0
  243. package/src/sast/.agentic-security/findings.json +5051 -0
  244. package/src/sast/.agentic-security/last-scan.json +5051 -0
  245. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  246. package/src/sast/.agentic-security/scan-history.json +788 -0
  247. package/src/sast/.agentic-security/streak.json +23 -0
  248. package/src/sast/CLAUDE.md +39 -0
  249. package/src/sast/_comment-strip.js +46 -0
  250. package/src/sast/agent-tool-escalation.js +131 -0
  251. package/src/sast/auth-provider.js +171 -0
  252. package/src/sast/authz.js +236 -0
  253. package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
  254. package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
  255. package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
  256. package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
  257. package/src/sast/bench-shape/index.js +62 -0
  258. package/src/sast/claude-hook-injection.js +199 -0
  259. package/src/sast/claude-md-prompt-injection.js +170 -0
  260. package/src/sast/claude-settings.js +165 -0
  261. package/src/sast/client-side.js +149 -0
  262. package/src/sast/cpp-bench-extras.js +122 -0
  263. package/src/sast/cpp-dataflow.js +430 -0
  264. package/src/sast/cpp.js +248 -0
  265. package/src/sast/csharp.js +152 -0
  266. package/src/sast/csrf.js +82 -0
  267. package/src/sast/dart-flutter.js +173 -0
  268. package/src/sast/db-rls.js +147 -0
  269. package/src/sast/db-taint.js +215 -0
  270. package/src/sast/defi-deep.js +242 -0
  271. package/src/sast/deserialization-gadgets.js +113 -0
  272. package/src/sast/django-hardening.js +230 -0
  273. package/src/sast/env-hygiene.js +125 -0
  274. package/src/sast/fastapi-hardening.js +145 -0
  275. package/src/sast/go-extended.js +84 -0
  276. package/src/sast/host-header.js +106 -0
  277. package/src/sast/index.js +17 -0
  278. package/src/sast/java-ast-folding.js +561 -0
  279. package/src/sast/java-bench-extras.js +708 -0
  280. package/src/sast/java-collection-passthrough.js +178 -0
  281. package/src/sast/java-constant-fold.js +244 -0
  282. package/src/sast/java-deserialization.js +125 -0
  283. package/src/sast/jndi.js +104 -0
  284. package/src/sast/juliet-shape.js +324 -0
  285. package/src/sast/jwt-exp.js +104 -0
  286. package/src/sast/kotlin.js +82 -0
  287. package/src/sast/laravel-hardening.js +198 -0
  288. package/src/sast/ldap-injection.js +100 -0
  289. package/src/sast/llm-owasp.js +465 -0
  290. package/src/sast/llm-stored-prompt.js +103 -0
  291. package/src/sast/llm-trading-agent.js +161 -0
  292. package/src/sast/llm.js +308 -0
  293. package/src/sast/logic.js +140 -0
  294. package/src/sast/mass-assignment.js +101 -0
  295. package/src/sast/mcp-audit.js +242 -0
  296. package/src/sast/mobile-manifest.js +195 -0
  297. package/src/sast/model-load.js +164 -0
  298. package/src/sast/mutation-xss.js +87 -0
  299. package/src/sast/nosql-injection.js +82 -0
  300. package/src/sast/open-redirect.js +119 -0
  301. package/src/sast/php.js +91 -0
  302. package/src/sast/pipeline.js +122 -0
  303. package/src/sast/primary-cwe-java.js +155 -0
  304. package/src/sast/prompt-firewall.js +151 -0
  305. package/src/sast/prompt-template.js +157 -0
  306. package/src/sast/prototype-pollution.js +112 -0
  307. package/src/sast/python-sinks.js +195 -0
  308. package/src/sast/quarkus-hardening.js +102 -0
  309. package/src/sast/rag-poisoning.js +118 -0
  310. package/src/sast/rate-limit.js +128 -0
  311. package/src/sast/response-splitting.js +138 -0
  312. package/src/sast/ruby.js +108 -0
  313. package/src/sast/rust.js +105 -0
  314. package/src/sast/solidity.js +167 -0
  315. package/src/sast/springboot-hardening.js +186 -0
  316. package/src/sast/ssrf-cloud-metadata.js +80 -0
  317. package/src/sast/ssti.js +116 -0
  318. package/src/sast/swift.js +162 -0
  319. package/src/sast/toctou.js +95 -0
  320. package/src/sast/webhook.js +101 -0
  321. package/src/sast/xpath-injection.js +51 -0
  322. package/src/sast/xxe.js +140 -0
  323. package/src/sast/zip-slip.js +200 -0
  324. package/src/sca/base-images.json +45 -0
  325. package/src/sca/container.js +107 -0
  326. package/src/sca/dep-confusion.js +134 -0
  327. package/src/sca/index.js +6 -0
  328. package/src/sca/popular-packages.json +41 -0
  329. package/src/sca/sarif-ingest.js +187 -0
  330. package/src/sca/vuln-function-hints.json +89 -0
  331. package/src/secrets/index.js +4 -0
@@ -0,0 +1,101 @@
1
+ import { blankComments } from './_comment-strip.js';
2
+ // Mass assignment / over-posting detector.
3
+ //
4
+ // The classic shape: developer wires the entire request body into an ORM
5
+ // create/update call, allowing the client to set fields the route handler
6
+ // never intended (is_admin, role, balance, etc.).
7
+ //
8
+ // Languages covered:
9
+ // - JS/TS Express + Mongoose/Sequelize/Prisma/TypeORM
10
+ // - Ruby on Rails ActiveRecord
11
+ // - Python Django ORM / Flask-SQLAlchemy
12
+ // - Java Spring Data
13
+ // - Go GORM
14
+ //
15
+ // Heuristic per-language: spread/splat of a request-body shape directly into
16
+ // a constructor, create(), update(), .save(), .set(), or Object.assign on a
17
+ // model. Whitelisting (allowed-fields pick) is the canonical fix.
18
+
19
+ const ALLOW_LIST_HINTS = /(?:pick|allowedFields|permit|allowed_params|whitelist|strong_params|FILTER_FIELDS|select_for|only:)/;
20
+
21
+ const JS_PATTERNS = [
22
+ // Object.assign(user, req.body)
23
+ /Object\.assign\s*\(\s*(\w+)\s*,\s*(req|request)\s*\.\s*(body|params|query)\b/g,
24
+ // new User({ ...req.body }) or User.create({ ...req.body })
25
+ /(?:new\s+([A-Z]\w+)\s*\(\s*\{[^}]*\.\.\.\s*(?:req|request)\s*\.\s*(?:body|params|query))/g,
26
+ // User.create(req.body) · Model.update(req.body, …)
27
+ /\b([A-Z]\w+)\s*\.\s*(?:create|update|build|save)\s*\(\s*(?:req|request)\s*\.\s*(?:body|params|query)\s*[,)]/g,
28
+ // prisma.user.update({ data: req.body })
29
+ /\.\s*(?:create|update|upsert)\s*\(\s*\{[^}]*data\s*:\s*(?:req|request)\s*\.\s*(?:body|params|query)/g,
30
+ ];
31
+
32
+ const PY_PATTERNS = [
33
+ // Model.objects.create(**request.POST) / **request.data / **request.json
34
+ /\b([A-Z]\w+)\s*(?:\.objects)?\s*\.\s*(?:create|update|filter\([^)]*\)\.update)\s*\(\s*\*\*\s*request\s*\.\s*(?:POST|data|json|form)/g,
35
+ // serializer.save(**request.data)
36
+ /\.save\s*\(\s*\*\*\s*request\s*\.\s*(?:data|json|POST|form)/g,
37
+ // setattr(obj, k, v) loop over request.data without allow-list
38
+ /for\s+\w+\s*,\s*\w+\s+in\s+request\s*\.\s*(?:data|json|POST|form)\s*\.\s*items\s*\([^)]*\)\s*:\s*\n\s*setattr/g,
39
+ ];
40
+
41
+ const RB_PATTERNS = [
42
+ // User.create(params) · user.update(params)
43
+ /\b([A-Z]\w+)\s*\.\s*(?:create|update|new)\s*\(\s*params\s*[,)]/g,
44
+ // user.assign_attributes(params) · user.attributes = params
45
+ /\.(?:assign_attributes|attributes\s*=)\s*\(?\s*params\s*[,)]?/g,
46
+ ];
47
+
48
+ const GO_PATTERNS = [
49
+ // db.Model(&user).Updates(input) where input is fully user-controlled
50
+ /\bdb\s*\.\s*(?:Model|Updates|Create)\s*\([^)]*&?\s*(\w+)\s*\)/g,
51
+ ];
52
+
53
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
54
+
55
+ function pickPatterns(fp) {
56
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return { lang: 'js', patterns: JS_PATTERNS };
57
+ if (/\.py$/i.test(fp)) return { lang: 'py', patterns: PY_PATTERNS };
58
+ if (/\.rb$/i.test(fp)) return { lang: 'rb', patterns: RB_PATTERNS };
59
+ if (/\.go$/i.test(fp)) return { lang: 'go', patterns: GO_PATTERNS };
60
+ return null;
61
+ }
62
+
63
+ export function scanMassAssignment(fp, raw) {
64
+ if (!raw || raw.length > 500_000) return [];
65
+ const sel = pickPatterns(fp);
66
+ if (!sel) return [];
67
+ const findings = [];
68
+ const seen = new Set();
69
+ const code = blankComments(raw, sel.lang === 'py' ? 'py' : undefined);
70
+ // Skip files that look like they use an allow-list — strong signal of safety.
71
+ if (ALLOW_LIST_HINTS.test(code.slice(0, 4000))) {
72
+ // not a hard skip: still scan but downgrade
73
+ }
74
+ for (const re of sel.patterns) {
75
+ const r = new RegExp(re.source, re.flags);
76
+ let m;
77
+ while ((m = r.exec(code))) {
78
+ const line = lineOf(raw, m.index);
79
+ const id = `mass-assignment:${fp}:${line}`;
80
+ if (seen.has(id)) continue;
81
+ seen.add(id);
82
+ // Look ±5 lines for an allow-list signal — if present, downgrade.
83
+ const lines = raw.split('\n');
84
+ const window = lines.slice(Math.max(0, line - 6), line + 5).join(' ');
85
+ const hasAllowList = ALLOW_LIST_HINTS.test(window);
86
+ findings.push({
87
+ id,
88
+ file: fp, line,
89
+ vuln: 'Mass Assignment: Unfiltered request body into model write',
90
+ severity: hasAllowList ? 'low' : 'high',
91
+ cwe: 'CWE-915',
92
+ stride: 'Tampering',
93
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
94
+ remediation: 'Explicitly allow-list the fields a client may set, instead of spreading the whole request body into the model write. Express: `pick(req.body, ["name", "email"])` (lodash) before `.create()`. Rails: `params.require(:user).permit(:name, :email)`. Django: a `ModelForm` / `Serializer` with explicit fields. Mass-assigning the whole body lets a client elevate privileges by adding `is_admin: true` to the JSON.',
95
+ parser: 'MASS-ASSIGN',
96
+ confidence: hasAllowList ? 0.40 : 0.85,
97
+ });
98
+ }
99
+ }
100
+ return findings;
101
+ }
@@ -0,0 +1,242 @@
1
+ // MCP / Agent tool security audit.
2
+ //
3
+ // Scans MCP server config files (claude_desktop_config.json, *.mcp.json, mcp.json,
4
+ // .claude/mcp.json) and tool-definition source for the canonical agent-host
5
+ // risks:
6
+ // - Untrusted server install vector (curl|sh, http://, unpinned npx)
7
+ // - Over-scoped filesystem grant (root, $HOME, /, *)
8
+ // - Hardcoded credential in env (sk-…, github_pat_…, AKIA…, ghp_…)
9
+ // - Prompt injection in description (instruction overrides in tool descriptions)
10
+ // - Dangerous tool capability (shell/exec/eval/sql exposed to model)
11
+ // - Unrestricted network passthrough (proxy tools without allow-list)
12
+ //
13
+ // F1 strategy:
14
+ // Recall — broad config-file matching across config name variants.
15
+ // Precision — fire only when one of the patterns above is concretely present
16
+ // in a JSON config or in a tool definition with description+name.
17
+
18
+ const _MCP_FILE_RE = /(?:^|[\\/])(?:claude_desktop_config\.json|\.?mcp\.json|[^/\\]+\.mcp\.json|mcp_servers\.json)$/i;
19
+ const _NONPROD_RE = /(?:^|[\\/])(?:tests?|examples?|fixtures?|node_modules|docs?)[\\/]/i;
20
+
21
+ // Hardcoded credential shapes commonly leaked in MCP env: blocks
22
+ const _HARDCODED_CRED_RE = [
23
+ /\b(?:sk-[A-Za-z0-9]{20,})\b/, // OpenAI / Anthropic
24
+ /\b(?:sk-ant-[A-Za-z0-9_-]{20,})\b/, // Anthropic
25
+ /\b(?:ghp_[A-Za-z0-9]{36})\b/, // GitHub PAT
26
+ /\b(?:github_pat_[A-Za-z0-9_]{20,})\b/, // GitHub fine-grained PAT
27
+ /\b(?:gho_[A-Za-z0-9]{36})\b/, // GitHub OAuth
28
+ /\bAKIA[0-9A-Z]{16}\b/, // AWS access key id
29
+ /\b(?:xox[abprs]-[A-Za-z0-9-]{10,})\b/, // Slack
30
+ /\b(?:gsk_[A-Za-z0-9]{30,})\b/, // Groq
31
+ ];
32
+
33
+ // Description fields that try to inject instructions into the agent
34
+ const _PROMPT_INJECTION_RE = [
35
+ /\b(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above|preceding)\s+(?:instructions|directives|prompts|rules)/i,
36
+ /\b(?:you\s+are\s+now|new\s+system\s+prompt|act\s+as|pretend\s+to\s+be)\b/i,
37
+ /\b(?:before\s+(?:running|invoking|using)\s+this\s+tool[^.]*?(?:read|exfiltrate|send|leak|copy|reveal|exec))/i,
38
+ /<\s*\|?\s*(?:system|im_start|im_end|assistant|user)\s*\|?\s*>/i,
39
+ /\b(?:print|reveal|output|show|expose|reveal)\s+(?:your|the)?\s*(?:system\s+prompt|instructions|api\s+key|credentials|secrets?)\b/i,
40
+ ];
41
+
42
+ // Tool/server names that imply dangerous capabilities
43
+ const _DANGEROUS_CAPABILITY_NAMES = /\b(?:shell|bash|exec|run_command|run_shell|execute_shell|eval|eval_python|sandbox_exec|run_code|sudo|root|kubectl|docker_exec|admin|drop_table|raw_query|ssh|fetch_url_unrestricted)\b/i;
44
+
45
+ // Filesystem args / env paths that grant excessive scope
46
+ const _FS_OVERSCOPE_RE = [
47
+ /^(?:\/|~\/?|\$HOME\/?|\$\{?HOME\}?\/?)$/,
48
+ /^(?:\/|~|\$HOME|\$\{HOME\})\s*\*$/,
49
+ /^(?:\/|~|\$HOME|\$\{HOME\})\/\*\*$/,
50
+ ];
51
+ const _FS_LIKE_PATH_RE = /^(?:[A-Za-z]:|\/|~\/?|\$HOME|\$\{HOME\}|\.\.?\/)/;
52
+
53
+ // Untrusted install / command vectors
54
+ const _UNTRUSTED_INSTALL_RE = [
55
+ /\bcurl\s+[^|]*\|\s*(?:sh|bash|zsh)\b/,
56
+ /\bwget\s+[^|]*\|\s*(?:sh|bash|zsh)\b/,
57
+ /^http:\/\//i,
58
+ ];
59
+
60
+ // Floating npx pins / unpinned versions in command:/args:
61
+ const _FLOATING_PIN_RE = /@(?:latest|next|main|master|beta|canary)\b/;
62
+
63
+ function _stringsFromValue(v, out=[]) {
64
+ if (v === null || v === undefined) return out;
65
+ if (typeof v === 'string') { out.push(v); return out; }
66
+ if (typeof v === 'number' || typeof v === 'boolean') { out.push(String(v)); return out; }
67
+ if (Array.isArray(v)) { for (const x of v) _stringsFromValue(x, out); return out; }
68
+ if (typeof v === 'object') { for (const k of Object.keys(v)) _stringsFromValue(v[k], out); return out; }
69
+ return out;
70
+ }
71
+
72
+ function _findLineOf(raw, needle) {
73
+ if (!raw || !needle) return 1;
74
+ const idx = raw.indexOf(needle);
75
+ if (idx === -1) return 1;
76
+ return raw.substring(0, idx).split('\n').length;
77
+ }
78
+
79
+ function _findKeyLine(raw, key) {
80
+ if (!raw || !key) return 1;
81
+ const re = new RegExp('"' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '"\\s*:');
82
+ const m = raw.match(re);
83
+ if (!m) return 1;
84
+ return raw.substring(0, m.index).split('\n').length;
85
+ }
86
+
87
+ function _isMcpConfigFile(fp) {
88
+ const norm = fp.replace(/\\/g, '/');
89
+ if (_NONPROD_RE.test(norm)) return false;
90
+ return _MCP_FILE_RE.test(norm);
91
+ }
92
+
93
+ // Public: scan a single file. Mirrors scanLLM/scanPipeline shape so the engine
94
+ // can call it inline for each file.
95
+ export function scanMCP(fp, raw) {
96
+ if (!_isMcpConfigFile(fp)) return [];
97
+ if (!raw || raw.length > 200_000) return [];
98
+ let parsed;
99
+ try { parsed = JSON.parse(raw); } catch { return []; }
100
+ if (!parsed || typeof parsed !== 'object') return [];
101
+
102
+ const findings = [];
103
+ const seen = new Set();
104
+ const push = (f) => {
105
+ const key = `${f.file}:${f.line}:${f.vuln}`;
106
+ if (seen.has(key)) return;
107
+ seen.add(key);
108
+ findings.push(f);
109
+ };
110
+
111
+ // Both `mcpServers` (Claude desktop config) and top-level server maps
112
+ const servers = (parsed.mcpServers && typeof parsed.mcpServers === 'object')
113
+ ? parsed.mcpServers
114
+ : (parsed.servers && typeof parsed.servers === 'object')
115
+ ? parsed.servers
116
+ : parsed;
117
+
118
+ for (const [name, srv] of Object.entries(servers || {})) {
119
+ if (!srv || typeof srv !== 'object') continue;
120
+ const line = _findKeyLine(raw, name);
121
+
122
+ // 1. Untrusted install vector in command/args
123
+ const cmd = typeof srv.command === 'string' ? srv.command : '';
124
+ const args = Array.isArray(srv.args) ? srv.args.filter(a => typeof a === 'string') : [];
125
+ const fullCmd = [cmd, ...args].join(' ');
126
+ if (fullCmd && _UNTRUSTED_INSTALL_RE.some(re => re.test(fullCmd))) {
127
+ push({
128
+ id: `mcp-audit:${fp}:${line}:untrusted-install`,
129
+ kind: 'mcp', severity: 'critical',
130
+ vuln: 'MCP: untrusted install vector (curl|sh / http://) in server command',
131
+ cwe: 'CWE-494', stride: 'Tampering',
132
+ file: fp, line, snippet: `${name}: ${fullCmd}`.slice(0, 200),
133
+ fix: `MCP server "${name}" runs through an untrusted bootstrap (curl|sh or http://). Pin to a published, signed package and use https. Example: replace \`curl http://… | sh\` with an explicit \`npx -y package@<sha>\` that you have audited.`,
134
+ });
135
+ }
136
+
137
+ // 2. Floating tag pin in npx/uvx invocations
138
+ if (args.some(a => _FLOATING_PIN_RE.test(a))) {
139
+ const offending = args.find(a => _FLOATING_PIN_RE.test(a));
140
+ push({
141
+ id: `mcp-audit:${fp}:${line}:floating-pin`,
142
+ kind: 'mcp', severity: 'high',
143
+ vuln: 'MCP: server pinned to a floating tag (@latest/@main)',
144
+ cwe: 'CWE-1357', stride: 'Tampering',
145
+ file: fp, line, snippet: `${name}: ${offending}`,
146
+ fix: `Floating tag means the publisher (or an attacker who compromises them) can ship new code into your agent any time. Pin to a specific semver: \`pkg@1.2.3\` — or, even better, a published SHA.`,
147
+ });
148
+ }
149
+
150
+ // 3. Hardcoded credentials in env or args
151
+ const envObj = (srv.env && typeof srv.env === 'object') ? srv.env : {};
152
+ const allEnvStrings = _stringsFromValue(envObj);
153
+ const allArgStrings = _stringsFromValue(args);
154
+ const allHaystacks = [...allEnvStrings, ...allArgStrings];
155
+ for (const s of allHaystacks) {
156
+ if (_HARDCODED_CRED_RE.some(re => re.test(s))) {
157
+ push({
158
+ id: `mcp-audit:${fp}:${line}:hardcoded-cred`,
159
+ kind: 'mcp', severity: 'critical',
160
+ vuln: 'MCP: hardcoded credential in server env/args',
161
+ cwe: 'CWE-798', stride: 'Information Disclosure',
162
+ file: fp, line, snippet: `${name}: <credential redacted>`,
163
+ fix: `MCP server "${name}" carries a hardcoded API key. Move it to a secret store and reference via \`env: { API_KEY: "\${{ secrets.API_KEY }}" }\` or read from the user's keychain at startup.`,
164
+ });
165
+ break;
166
+ }
167
+ }
168
+
169
+ // 4. Filesystem over-scope. Common when @modelcontextprotocol/server-filesystem
170
+ // is invoked with a root/home arg.
171
+ const isFsServer = /filesystem|files?|fs/i.test(name) || /server-filesystem/.test(fullCmd);
172
+ if (isFsServer || args.some(a => _FS_LIKE_PATH_RE.test(a))) {
173
+ const overscoped = args.find(a =>
174
+ typeof a === 'string' && _FS_OVERSCOPE_RE.some(re => re.test(a.trim()))
175
+ );
176
+ if (overscoped) {
177
+ push({
178
+ id: `mcp-audit:${fp}:${line}:fs-overscope`,
179
+ kind: 'mcp', severity: 'high',
180
+ vuln: 'MCP: filesystem server granted root or $HOME scope',
181
+ cwe: 'CWE-732', stride: 'Elevation of Privilege',
182
+ file: fp, line, snippet: `${name}: ${overscoped}`,
183
+ fix: `Filesystem MCP server "${name}" can read every file in ${overscoped}. Scope to the specific project directory the agent needs, e.g. \`/Users/me/code/this-project\`. Never grant \`/\`, \`$HOME\`, or \`~\`.`,
184
+ });
185
+ }
186
+ }
187
+
188
+ // 5. Dangerous capability name (shell, exec, eval, etc.) exposed unscoped
189
+ if (_DANGEROUS_CAPABILITY_NAMES.test(name) || _DANGEROUS_CAPABILITY_NAMES.test(fullCmd)) {
190
+ push({
191
+ id: `mcp-audit:${fp}:${line}:dangerous-capability`,
192
+ kind: 'mcp', severity: 'high',
193
+ vuln: 'MCP: server exposes a dangerous capability (shell/exec/eval) to the model',
194
+ cwe: 'CWE-77', stride: 'Elevation of Privilege',
195
+ file: fp, line, snippet: `${name}`,
196
+ fix: `Server "${name}" lets the model run arbitrary commands. If you keep it, restrict the working directory and the allowed binary list, and require user confirmation per call (most clients support an \`approval: ask\` flag).`,
197
+ });
198
+ }
199
+
200
+ // 6. Description-field prompt injection
201
+ const desc = typeof srv.description === 'string' ? srv.description : '';
202
+ if (desc && _PROMPT_INJECTION_RE.some(re => re.test(desc))) {
203
+ const dline = _findLineOf(raw, desc.slice(0, 40));
204
+ push({
205
+ id: `mcp-audit:${fp}:${dline}:prompt-injection-description`,
206
+ kind: 'mcp', severity: 'critical',
207
+ vuln: 'MCP: prompt-injection text inside server description',
208
+ cwe: 'CWE-1336', stride: 'Spoofing',
209
+ file: fp, line: dline, snippet: desc.slice(0, 200),
210
+ fix: `The description for "${name}" contains an instruction that overrides agent behavior. Treat MCP server metadata as untrusted input — the agent reads it. Strip the override and only describe what the server actually does.`,
211
+ });
212
+ }
213
+
214
+ // Also: prompt injection inside any tool definitions embedded in this config
215
+ const toolsArr = Array.isArray(srv.tools) ? srv.tools : [];
216
+ for (const t of toolsArr) {
217
+ if (!t || typeof t !== 'object') continue;
218
+ const tDesc = typeof t.description === 'string' ? t.description : '';
219
+ if (tDesc && _PROMPT_INJECTION_RE.some(re => re.test(tDesc))) {
220
+ const tline = _findLineOf(raw, tDesc.slice(0, 40));
221
+ push({
222
+ id: `mcp-audit:${fp}:${tline}:prompt-injection-tool-desc`,
223
+ kind: 'mcp', severity: 'critical',
224
+ vuln: 'MCP: prompt-injection text inside tool description',
225
+ cwe: 'CWE-1336', stride: 'Spoofing',
226
+ file: fp, line: tline, snippet: tDesc.slice(0, 200),
227
+ fix: `Tool "${t.name || '(unnamed)'}" carries an instruction inside its description. The agent reads tool descriptions as part of its system context. Remove the injection.`,
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ return findings;
234
+ }
235
+
236
+ // Public for tests + the engine
237
+ export const _internal = {
238
+ _isMcpConfigFile,
239
+ _HARDCODED_CRED_RE,
240
+ _PROMPT_INJECTION_RE,
241
+ _DANGEROUS_CAPABILITY_NAMES,
242
+ };
@@ -0,0 +1,195 @@
1
+ // Mobile manifest audit — AndroidManifest.xml + Info.plist + module.json5.
2
+ //
3
+ // Cross-language: catches mobile security misconfig regardless of whether
4
+ // the codebase is Java / Kotlin / Swift / Dart / ArkTS.
5
+ //
6
+ // Coverage:
7
+ // AndroidManifest.xml
8
+ // - android:exported="true" on non-launcher activities
9
+ // - <application android:debuggable="true">
10
+ // - <application android:allowBackup="true"> with sensitive data hints
11
+ // - <application android:usesCleartextTraffic="true">
12
+ // - dangerous permissions without rationale
13
+ // Info.plist
14
+ // - NSAllowsArbitraryLoads = true (ATS bypass)
15
+ // - Missing NS*UsageDescription for declared permissions
16
+ // module.json5 (HarmonyOS)
17
+ // - Permission without usedScene / reason
18
+
19
+ const _ANDROID_MANIFEST_RE = /(?:^|[\\/])AndroidManifest\.xml$/i;
20
+ const _INFO_PLIST_RE = /(?:^|[\\/])Info\.plist$/i;
21
+ const _MODULE_JSON5_RE = /(?:^|[\\/])module\.json5$/i;
22
+
23
+ function _line(raw, idx) {
24
+ return raw.slice(0, idx).split('\n').length;
25
+ }
26
+
27
+ export function scanMobileManifest(file, raw) {
28
+ if (!file || !raw || typeof raw !== 'string') return [];
29
+ if (raw.length > 200_000) return [];
30
+
31
+ if (_ANDROID_MANIFEST_RE.test(file)) return _scanAndroidManifest(file, raw);
32
+ if (_INFO_PLIST_RE.test(file)) return _scanInfoPlist(file, raw);
33
+ if (_MODULE_JSON5_RE.test(file)) return _scanModuleJson5(file, raw);
34
+ return [];
35
+ }
36
+
37
+ function _scanAndroidManifest(file, raw) {
38
+ const findings = [];
39
+
40
+ // android:debuggable="true"
41
+ for (const m of raw.matchAll(/android:debuggable\s*=\s*["']true["']/g)) {
42
+ findings.push({
43
+ id: `mobile-android:debuggable-true:${file}:${_line(raw, m.index)}`,
44
+ file, line: _line(raw, m.index),
45
+ vuln: 'AndroidManifest <application android:debuggable="true">',
46
+ severity: 'critical',
47
+ family: 'mobile-android-debuggable',
48
+ cwe: 'CWE-489',
49
+ confidence: 0.95,
50
+ description: 'A debuggable APK shipped to production allows any user with adb to attach jdb, inspect process memory, and arbitrarily call methods. JADX + Frida pair this into a one-step bypass for any client-side check.',
51
+ remediation: 'Remove android:debuggable from <application>, or set false in release builds. Most build systems strip this from release variants automatically — verify your release manifest.',
52
+ });
53
+ }
54
+
55
+ // usesCleartextTraffic="true"
56
+ for (const m of raw.matchAll(/android:usesCleartextTraffic\s*=\s*["']true["']/g)) {
57
+ findings.push({
58
+ id: `mobile-android:cleartext-traffic:${file}:${_line(raw, m.index)}`,
59
+ file, line: _line(raw, m.index),
60
+ vuln: 'AndroidManifest android:usesCleartextTraffic="true"',
61
+ severity: 'high',
62
+ family: 'mobile-android-cleartext',
63
+ cwe: 'CWE-319',
64
+ confidence: 0.95,
65
+ description: 'Allows the app to make plaintext HTTP requests. Network-level attackers on the same Wi-Fi can intercept and tamper with traffic.',
66
+ remediation: 'Set usesCleartextTraffic="false" and rely on TLS. If specific dev/internal hosts genuinely need HTTP, scope them via network_security_config.xml domain-config — never the whole app.',
67
+ });
68
+ }
69
+
70
+ // android:exported="true" on non-launcher activities (no LAUNCHER intent filter)
71
+ // Walk each <activity ... android:exported="true" ...> ... </activity>
72
+ const activityRe = /<activity\b([^>]*)>([\s\S]*?)<\/activity>|<activity\b([^/]*)\/>/g;
73
+ let am;
74
+ while ((am = activityRe.exec(raw))) {
75
+ const head = am[1] || am[3] || '';
76
+ const body = am[2] || '';
77
+ if (!/android:exported\s*=\s*["']true["']/.test(head)) continue;
78
+ // Skip the launcher activity (which MUST be exported).
79
+ if (/android\.intent\.category\.LAUNCHER/.test(body) ||
80
+ /android\.intent\.category\.LAUNCHER/.test(head)) continue;
81
+ findings.push({
82
+ id: `mobile-android:exported-true:${file}:${_line(raw, am.index)}`,
83
+ file, line: _line(raw, am.index),
84
+ vuln: 'AndroidManifest <activity android:exported="true"> on a non-launcher activity',
85
+ severity: 'high',
86
+ family: 'mobile-android-exported',
87
+ cwe: 'CWE-926',
88
+ confidence: 0.85,
89
+ description: 'Any other app on the device can start this Activity via Intent. Combined with a vulnerable intent extras handler, this is a one-app RCE.',
90
+ remediation: 'Set android:exported="false" unless another app genuinely needs to start this Activity. If it does, verify intent extras and require android:permission to gate access.',
91
+ });
92
+ }
93
+
94
+ // allowBackup="true" with no fullBackupContent restrictions
95
+ if (/android:allowBackup\s*=\s*["']true["']/.test(raw) && !/android:fullBackupContent\s*=/.test(raw)) {
96
+ const m = /android:allowBackup\s*=\s*["']true["']/.exec(raw);
97
+ findings.push({
98
+ id: `mobile-android:allow-backup:${file}:${_line(raw, m.index)}`,
99
+ file, line: _line(raw, m.index),
100
+ vuln: 'AndroidManifest android:allowBackup="true" without fullBackupContent restriction',
101
+ severity: 'medium',
102
+ family: 'mobile-android-backup',
103
+ cwe: 'CWE-552',
104
+ confidence: 0.75,
105
+ description: 'On debug-enabled devices, adb can pull the app\'s entire data dir (preferences, databases, files) via adb backup. Without a fullBackupContent restriction, sensitive data is included.',
106
+ remediation: 'Set allowBackup="false", or provide fullBackupContent="@xml/backup_rules" with an explicit include/exclude list.',
107
+ });
108
+ }
109
+
110
+ return findings;
111
+ }
112
+
113
+ function _scanInfoPlist(file, raw) {
114
+ const findings = [];
115
+
116
+ // NSAllowsArbitraryLoads = true
117
+ const re = /<key>\s*NSAllowsArbitraryLoads\s*<\/key>\s*<true\s*\/>/i;
118
+ if (re.test(raw)) {
119
+ const m = re.exec(raw);
120
+ findings.push({
121
+ id: `mobile-ios:ats-disabled:${file}:${_line(raw, m.index)}`,
122
+ file, line: _line(raw, m.index),
123
+ vuln: 'Info.plist NSAllowsArbitraryLoads = true — App Transport Security disabled',
124
+ severity: 'high',
125
+ family: 'mobile-ios-ats-disabled',
126
+ cwe: 'CWE-319',
127
+ confidence: 0.95,
128
+ description: 'ATS-disabled apps make plaintext HTTP calls to arbitrary hosts. Network-level attackers can intercept tokens / PII / session cookies trivially.',
129
+ remediation: 'Remove NSAllowsArbitraryLoads or set to false. If a specific domain genuinely needs HTTP (legacy backend), scope via NSExceptionDomains with NSExceptionAllowsInsecureHTTPLoads on that single host.',
130
+ });
131
+ }
132
+
133
+ // Permissions declared without usage description — known iOS keys.
134
+ const IOS_PERMS = [
135
+ ['NSCameraUsageDescription', 'camera'],
136
+ ['NSMicrophoneUsageDescription', 'microphone'],
137
+ ['NSLocationWhenInUseUsageDescription', 'location-when-in-use'],
138
+ ['NSLocationAlwaysAndWhenInUseUsageDescription', 'location-always'],
139
+ ['NSPhotoLibraryUsageDescription', 'photo library'],
140
+ ['NSContactsUsageDescription', 'contacts'],
141
+ ['NSCalendarsUsageDescription', 'calendar'],
142
+ ['NSBluetoothAlwaysUsageDescription', 'bluetooth'],
143
+ ['NSAppleMusicUsageDescription', 'media library'],
144
+ ['NSMotionUsageDescription', 'motion'],
145
+ ['NSFaceIDUsageDescription', 'face id'],
146
+ ];
147
+ for (const [key, label] of IOS_PERMS) {
148
+ // Check: <key>KEY</key> present AND followed by empty <string></string> (within 200 chars)
149
+ const re2 = new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>\\s*</string>`, 'i');
150
+ if (re2.test(raw)) {
151
+ const m = re2.exec(raw);
152
+ findings.push({
153
+ id: `mobile-ios:empty-usage-desc:${file}:${_line(raw, m.index)}:${label}`,
154
+ file, line: _line(raw, m.index),
155
+ vuln: `Info.plist ${key} declared but description is empty`,
156
+ severity: 'low',
157
+ family: 'mobile-ios-empty-permission-rationale',
158
+ cwe: 'CWE-1059',
159
+ confidence: 0.9,
160
+ description: `iOS displays the usage description to the user when the app first requests ${label} access. An empty string leads to App Store rejection and a worse user-trust signal.`,
161
+ remediation: `Provide a clear, specific reason: <string>Camera access lets you scan QR codes for fast pairing.</string>`,
162
+ });
163
+ }
164
+ }
165
+
166
+ return findings;
167
+ }
168
+
169
+ function _scanModuleJson5(file, raw) {
170
+ const findings = [];
171
+ // HarmonyOS: declared permission without usedScene or reason.
172
+ // Cheap regex check on the JSON5.
173
+ const permRe = /"requestPermissions"\s*:\s*\[([\s\S]*?)\]/;
174
+ const m = permRe.exec(raw);
175
+ if (!m) return findings;
176
+ const body = m[1];
177
+ // For each block { "name": "...", ... } in the array, check usedScene / reason.
178
+ for (const pm of body.matchAll(/\{[^{}]*"name"\s*:\s*"([^"]+)"[^{}]*\}/g)) {
179
+ const block = pm[0];
180
+ const permName = pm[1];
181
+ if (/"usedScene"\s*:/.test(block) && /"reason"\s*:/.test(block)) continue;
182
+ findings.push({
183
+ id: `mobile-harmony:missing-permission-rationale:${file}:${_line(raw, pm.index + m.index)}:${permName}`,
184
+ file, line: _line(raw, pm.index + m.index),
185
+ vuln: `HarmonyOS module.json5 permission "${permName}" missing usedScene or reason`,
186
+ severity: 'low',
187
+ family: 'mobile-harmony-permission-rationale',
188
+ cwe: 'CWE-862',
189
+ confidence: 0.85,
190
+ description: 'HarmonyOS requires every requested permission to declare usedScene and a user-facing reason. Missing the rationale leads to runtime denial.',
191
+ remediation: 'Add "usedScene": { "abilities": ["EntryAbility"], "when": "always" } and "reason": "$string:permission_reason".',
192
+ });
193
+ }
194
+ return findings;
195
+ }