@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,173 @@
1
+ // Dart / Flutter security audit.
2
+ //
3
+ // Coverage:
4
+ // 1. SharedPreferences used for token / secret storage
5
+ // 2. rawQuery / rawRawUpdate with $-interpolated user input
6
+ // 3. Uri.parse without tryParse + allow-list (deep link unsafe)
7
+ // 4. WebView with JavaScriptMode.unrestricted and no navigationDelegate
8
+ // 5. Cleartext HTTP URL in Dart source
9
+ // 6. Hardcoded API key
10
+ // 7. http.get / Dio without timeout (resource exhaustion + slow loris)
11
+ // 8. print(token) / debugPrint(password) (PII leak)
12
+
13
+ const _DART_RE = /\.dart$/i;
14
+
15
+ function _line(raw, idx) {
16
+ return raw.slice(0, idx).split('\n').length;
17
+ }
18
+
19
+ const _CRED_RE = [
20
+ { re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/, label: 'Anthropic API key' },
21
+ { re: /\bsk-[A-Za-z0-9]{32,}\b/, label: 'OpenAI-style key' },
22
+ { re: /\bghp_[A-Za-z0-9]{36}\b/, label: 'GitHub PAT' },
23
+ { re: /\bAKIA[0-9A-Z]{16}\b/, label: 'AWS access key' },
24
+ ];
25
+
26
+ export function scanDartFlutter(file, raw) {
27
+ if (!file || !raw || typeof raw !== 'string') return [];
28
+ if (!_DART_RE.test(file)) return [];
29
+ if (raw.length > 200_000) return [];
30
+
31
+ const findings = [];
32
+
33
+ // 1. SharedPreferences for secrets — the string literal contains a secret-name substring.
34
+ const spSecretRe = /\bSharedPreferences\b[\s\S]{0,500}?\.\s*(?:setString|setInt|setBool)\s*\(\s*['"][\w-]*(?:[tT]oken|[pP]assword|[Aa]pi[Kk]ey|api_key|jwt|bearer|secret|sessionKey)/g;
35
+ for (const m of raw.matchAll(spSecretRe)) {
36
+ findings.push({
37
+ id: `dart:sharedprefs-secret:${file}:${_line(raw, m.index)}`,
38
+ file, line: _line(raw, m.index),
39
+ vuln: 'SharedPreferences used to store a secret (token / password / key)',
40
+ severity: 'high',
41
+ family: 'dart-insecure-storage',
42
+ cwe: 'CWE-922',
43
+ confidence: 0.85,
44
+ description: 'SharedPreferences stores values plaintext on Android and in NSUserDefaults-equivalent on iOS — neither is encrypted at rest. Rooted Android devices and iOS backups extract the value trivially.',
45
+ remediation: 'Use flutter_secure_storage (Keychain on iOS, EncryptedSharedPreferences on Android): final storage = FlutterSecureStorage(); await storage.write(key: "token", value: token).',
46
+ });
47
+ }
48
+
49
+ // 2. rawQuery / rawUpdate with $-interpolation. Outer quote can be ' or ";
50
+ // inner content may include the opposite quote (for SQL string literals).
51
+ const sqlInjectRe = /\b(?:rawQuery|rawInsert|rawUpdate|rawDelete|execute)\s*\(\s*(?:"[^"]*\$\{?[A-Za-z_]|'[^']*\$\{?[A-Za-z_])/g;
52
+ for (const m of raw.matchAll(sqlInjectRe)) {
53
+ findings.push({
54
+ id: `dart:sql-injection:${file}:${_line(raw, m.index)}`,
55
+ file, line: _line(raw, m.index),
56
+ vuln: 'sqflite/drift raw query with interpolated user input — SQL injection',
57
+ severity: 'critical',
58
+ family: 'dart-sql-injection',
59
+ cwe: 'CWE-89',
60
+ confidence: 0.9,
61
+ description: 'String interpolation directly into the SQL string lets the attacker rewrite the query.',
62
+ remediation: 'Use parameterized queries: db.query("users", where: "email = ?", whereArgs: [userInput]).',
63
+ });
64
+ }
65
+
66
+ // 3. Uri.parse without tryParse + allow-list (heuristic: Uri.parse followed by direct navigation without scheme/host check)
67
+ for (const m of raw.matchAll(/\bUri\.parse\s*\([^)]+\)(?![\s\S]{0,200}?\.(?:scheme|host)\s*==)/g)) {
68
+ // skip if next 200 chars contain allow-list pattern
69
+ const after = raw.slice(m.index + m[0].length, m.index + m[0].length + 400);
70
+ if (/\b(?:allowedHosts|allowedPaths|allowedSchemes|\.host\s*==\s*['"][a-zA-Z]|\.scheme\s*==\s*['"][a-z])/.test(after)) continue;
71
+ if (/context\.(?:go|push|pushNamed|pushReplacement)|Navigator\.(?:push|of\([^)]*\)\.push)/.test(after)) {
72
+ findings.push({
73
+ id: `dart:uri-parse-deeplink:${file}:${_line(raw, m.index)}`,
74
+ file, line: _line(raw, m.index),
75
+ vuln: 'Uri.parse() output passed to navigator without scheme/host allow-list',
76
+ severity: 'high',
77
+ family: 'dart-deeplink-unsafe',
78
+ cwe: 'CWE-20',
79
+ confidence: 0.65,
80
+ description: 'A deep link is parsed and navigated to without validating scheme or host. Attackers can craft links that route the app to internal-only screens or pivot through a WebView.',
81
+ remediation: 'Use Uri.tryParse(), then check uri.scheme == "https" && allowedHosts.contains(uri.host) && allowedPaths.contains(uri.path) before navigating.',
82
+ });
83
+ }
84
+ }
85
+
86
+ // 4. WebView with JavaScriptMode.unrestricted and no NavigationDelegate
87
+ if (/\bWebViewController\b/.test(raw) || /\bWebView\b/.test(raw)) {
88
+ if (/\bJavaScriptMode\.unrestricted\b/.test(raw) && !/\bNavigationDelegate\s*\(/.test(raw)) {
89
+ const m = /\bJavaScriptMode\.unrestricted\b/.exec(raw);
90
+ findings.push({
91
+ id: `dart:webview-js-no-delegate:${file}:${_line(raw, m.index)}`,
92
+ file, line: _line(raw, m.index),
93
+ vuln: 'WebView with JavaScriptMode.unrestricted and no NavigationDelegate',
94
+ severity: 'high',
95
+ family: 'dart-webview-unsafe',
96
+ cwe: 'CWE-829',
97
+ confidence: 0.7,
98
+ description: 'WebView with JS enabled and no NavigationDelegate accepts any URL. Combined with addJavaScriptChannel (or any JS-Dart bridge), this is a direct path to native code from attacker-controlled JS.',
99
+ remediation: 'Assign a NavigationDelegate with onNavigationRequest that returns NavigationDecision.prevent for any URL outside your allow-list.',
100
+ });
101
+ }
102
+ }
103
+
104
+ // 5. Cleartext HTTP URL literal
105
+ for (const m of raw.matchAll(/['"]http:\/\/(?!localhost|127\.0\.0\.1)[A-Za-z0-9.-]+/g)) {
106
+ findings.push({
107
+ id: `dart:cleartext-http:${file}:${_line(raw, m.index)}`,
108
+ file, line: _line(raw, m.index),
109
+ vuln: 'Cleartext HTTP URL literal in Dart source',
110
+ severity: 'medium',
111
+ family: 'dart-cleartext-http',
112
+ cwe: 'CWE-319',
113
+ confidence: 0.8,
114
+ description: 'A hard-coded http:// URL ships cleartext on the wire.',
115
+ remediation: 'Use https://. Set up network_security_config.xml on Android to block cleartext globally.',
116
+ snippet: m[0].slice(0, 60),
117
+ });
118
+ break; // one finding per file is enough — common case
119
+ }
120
+
121
+ // 6. Hardcoded credentials
122
+ for (const { re, label } of _CRED_RE) {
123
+ const m = re.exec(raw);
124
+ if (!m) continue;
125
+ findings.push({
126
+ id: `dart:hardcoded-${label.toLowerCase().replace(/\s+/g, '-')}:${file}:${_line(raw, m.index)}`,
127
+ file, line: _line(raw, m.index),
128
+ vuln: `Hardcoded ${label} in Dart source`,
129
+ severity: 'critical',
130
+ family: 'dart-hardcoded-credential',
131
+ cwe: 'CWE-798',
132
+ confidence: 0.95,
133
+ description: 'Flutter apps decompile easily (`flutter_extract` / `blutter`). Hardcoded credentials in source are extracted in seconds.',
134
+ remediation: 'Use String.fromEnvironment("API_KEY") for non-secret config, flutter_secure_storage for runtime secrets fetched from a backend.',
135
+ });
136
+ }
137
+
138
+ // 7. http.get / Dio without timeout
139
+ for (const m of raw.matchAll(/\b(?:http\.(?:get|post|put|delete)|Dio\s*\(\s*\))/g)) {
140
+ const after = raw.slice(m.index, m.index + 400);
141
+ if (/\b(?:connectTimeout|receiveTimeout|sendTimeout|timeout)\b/.test(after)) continue;
142
+ findings.push({
143
+ id: `dart:no-http-timeout:${file}:${_line(raw, m.index)}`,
144
+ file, line: _line(raw, m.index),
145
+ vuln: 'HTTP client / request without timeout',
146
+ severity: 'low',
147
+ family: 'dart-no-timeout',
148
+ cwe: 'CWE-400',
149
+ confidence: 0.6,
150
+ description: 'No connectTimeout / receiveTimeout set. A slow or unresponsive server will hang the request indefinitely, exhausting connection pool / blocking the UI thread.',
151
+ remediation: 'Set BaseOptions(connectTimeout: Duration(seconds:10), receiveTimeout: Duration(seconds:30)) on Dio. For http: wrap in Future.timeout(...).',
152
+ });
153
+ break; // one per file
154
+ }
155
+
156
+ // 8. print() / debugPrint() of secret values
157
+ const debugLeakRe = /\b(?:print|debugPrint)\s*\([^)]*\b(?:token|password|api[kK]ey|jwt|bearer|secret)\b/g;
158
+ for (const m of raw.matchAll(debugLeakRe)) {
159
+ findings.push({
160
+ id: `dart:debug-print-secret:${file}:${_line(raw, m.index)}`,
161
+ file, line: _line(raw, m.index),
162
+ vuln: 'print() / debugPrint() of a variable named token/password/apiKey/jwt',
163
+ severity: 'medium',
164
+ family: 'dart-secret-in-log',
165
+ cwe: 'CWE-532',
166
+ confidence: 0.75,
167
+ description: 'Secrets sent to print/debugPrint end up in logcat (Android) / Console.app (iOS) and any crash-reporting integration. Even in release builds, debugPrint emits.',
168
+ remediation: 'Remove the log statement, or redact the value: debugPrint("token=${token.substring(0, 6)}...").',
169
+ });
170
+ }
171
+
172
+ return findings;
173
+ }
@@ -0,0 +1,147 @@
1
+ // Database / Supabase RLS security audit.
2
+ //
3
+ // Vibecoders using Supabase routinely ship with Row-Level Security disabled,
4
+ // service-role keys exposed client-side, or admin APIs called from public
5
+ // endpoints. This module catches the static signals for those patterns.
6
+ //
7
+ // Findings:
8
+ // SUPABASE_SERVICE_KEY_CLIENT — service_role key in NEXT_PUBLIC_* var or client-side file
9
+ // SUPABASE_ADMIN_CLIENT_SIDE — supabase.auth.admin.* in browser/client code
10
+ // SUPABASE_BYPASS_RLS — explicit bypassRowLevelSecurity or serviceRole in query
11
+ // RLS_DISABLED_SQL — SQL CREATE TABLE without ALTER TABLE … ENABLE ROW LEVEL SECURITY
12
+ // SUPABASE_ANON_KEY_SERVER — anon key used server-side with service-role semantics
13
+ // POSTGRES_DIRECT_NO_RLS — raw pg/postgres connection in request handler (bypasses RLS)
14
+
15
+ const _SCAN_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|sql)$/i;
16
+ const _NONPROD_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|node_modules)\//i;
17
+ const _CLIENT_PATH_RE = /(?:^|\/)(?:app|pages|components|src\/app|src\/pages|src\/components|public|client|frontend|ui)\//i;
18
+
19
+ // Service role key referenced from a NEXT_PUBLIC_ env var or hardcoded in a client-side path.
20
+ const NEXT_PUBLIC_SERVICE_RE = /NEXT_PUBLIC_\w*(?:SERVICE|SUPABASE_SERVICE|ADMIN)\w*\s*[=:]/i;
21
+ const SERVICE_KEY_LITERAL_RE = /(?:serviceRoleKey|service_role_key|SUPABASE_SERVICE_ROLE_KEY)\s*[:=]\s*['"`][a-zA-Z0-9._-]{20,}/;
22
+ const SUPABASE_CLIENT_IMPORT_RE = /(?:from|require)\s*\(?\s*['"`]@supabase\/supabase-js['"`]/;
23
+
24
+ // Auth admin API called in code that might be client-accessible.
25
+ const ADMIN_API_RE = /supabase\s*\.\s*auth\s*\.\s*admin\s*\./;
26
+
27
+ // Explicit RLS bypass in a Supabase query builder chain.
28
+ const BYPASS_RLS_RE = /\.\s*(?:bypassRowLevelSecurity|serviceRole)\s*\(\s*\)/;
29
+
30
+ // SQL: table created without RLS enabled in the same statement block.
31
+ const SQL_CREATE_TABLE_RE = /CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+["'`]?(\w+)["'`]?\s*\(/gi;
32
+ const SQL_ENABLE_RLS_RE = /ALTER\s+TABLE\s+["'`]?\w+["'`]?\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY/i;
33
+
34
+ // Raw postgres client in a request handler.
35
+ const PG_CLIENT_RE = /new\s+(?:Pool|Client)\s*\(\s*\{/;
36
+ const REQUEST_HANDLER_RE = /(?:req|request|ctx|context)\s*[,)]/;
37
+
38
+ function scanDatabaseRLS(file, content) {
39
+ if (!_SCAN_EXT_RE.test(file)) return [];
40
+ if (_NONPROD_RE.test(file)) return [];
41
+ const findings = [];
42
+ const lines = content.split('\n');
43
+ const isClientPath = _CLIENT_PATH_RE.test(file);
44
+ const isSql = /\.sql$/i.test(file);
45
+
46
+ // --- NEXT_PUBLIC_ service key ---
47
+ for (let i = 0; i < lines.length; i++) {
48
+ const line = lines[i];
49
+ if (NEXT_PUBLIC_SERVICE_RE.test(line)) {
50
+ findings.push({
51
+ id: `db-rls:SUPABASE_SERVICE_KEY_CLIENT:${file}:${i + 1}`,
52
+ title: 'Supabase service-role key exposed via NEXT_PUBLIC_ variable',
53
+ severity: 'critical',
54
+ file, line: i + 1,
55
+ description: 'A NEXT_PUBLIC_ environment variable referencing a service-role key is visible to every browser client. Any visitor can extract it from the page source and bypass Row-Level Security entirely.',
56
+ remediation: 'Never prefix service-role keys with NEXT_PUBLIC_. Use them only in server-side code (API routes, Server Actions, edge functions). Rotate the key immediately if it was already deployed.',
57
+ cwe: 'CWE-522',
58
+ });
59
+ }
60
+
61
+ // Hardcoded service role key literal
62
+ if (SERVICE_KEY_LITERAL_RE.test(line)) {
63
+ findings.push({
64
+ id: `db-rls:SERVICE_KEY_LITERAL:${file}:${i + 1}`,
65
+ title: 'Supabase service-role key hardcoded in source',
66
+ severity: 'critical',
67
+ file, line: i + 1,
68
+ description: 'A Supabase service-role key is embedded directly in source code. This key bypasses Row-Level Security on every table and is now permanently stored in git history.',
69
+ remediation: 'Move to SUPABASE_SERVICE_ROLE_KEY env var, add it to .gitignore, and rotate via the Supabase dashboard → Settings → API.',
70
+ cwe: 'CWE-798',
71
+ });
72
+ }
73
+
74
+ // Auth admin API in client-side path
75
+ if (isClientPath && ADMIN_API_RE.test(line)) {
76
+ findings.push({
77
+ id: `db-rls:SUPABASE_ADMIN_CLIENT_SIDE:${file}:${i + 1}`,
78
+ title: 'Supabase auth.admin API called in client-side code',
79
+ severity: 'critical',
80
+ file, line: i + 1,
81
+ description: 'supabase.auth.admin.* requires the service-role key and must only be called server-side. Calling it in client code means the service-role key is bundled into the browser JavaScript.',
82
+ remediation: 'Move all auth.admin calls to a Server Action, API route, or edge function. Never import the service-role supabase client in client components.',
83
+ cwe: 'CWE-285',
84
+ });
85
+ }
86
+
87
+ // Explicit RLS bypass
88
+ if (BYPASS_RLS_RE.test(line)) {
89
+ findings.push({
90
+ id: `db-rls:SUPABASE_BYPASS_RLS:${file}:${i + 1}`,
91
+ title: 'Explicit Supabase RLS bypass in query',
92
+ severity: 'high',
93
+ file, line: i + 1,
94
+ description: 'bypassRowLevelSecurity() or serviceRole() is used in a query, disabling all RLS policies for that request. If this code is reachable from a user-controlled path, any user can read or modify other users\' data.',
95
+ remediation: 'Remove bypassRowLevelSecurity() from user-facing query paths. If admin operations are required, gate them behind a server-side role check and audit log.',
96
+ cwe: 'CWE-284',
97
+ });
98
+ }
99
+
100
+ // Raw pg client in request handler
101
+ if (PG_CLIENT_RE.test(line)) {
102
+ const ctx = lines.slice(Math.max(0, i - 5), i + 10).join('\n');
103
+ if (REQUEST_HANDLER_RE.test(ctx)) {
104
+ findings.push({
105
+ id: `db-rls:POSTGRES_DIRECT_NO_RLS:${file}:${i + 1}`,
106
+ title: 'Direct PostgreSQL connection in request handler bypasses RLS',
107
+ severity: 'high',
108
+ file, line: i + 1,
109
+ description: 'A raw pg Pool/Client connection used inside a request handler connects as a privileged database role, bypassing all Supabase Row-Level Security policies. Any data returned is not filtered by the authenticated user\'s context.',
110
+ remediation: 'Use the Supabase client with the user\'s JWT for RLS-filtered queries. If raw SQL is required, set SET LOCAL role to the authenticated user\'s role and pass the JWT claims via set_config.',
111
+ cwe: 'CWE-284',
112
+ });
113
+ }
114
+ }
115
+ }
116
+
117
+ // --- SQL file: CREATE TABLE without RLS ---
118
+ if (isSql) {
119
+ const tablesWithRLS = new Set();
120
+ let m;
121
+ if (SQL_ENABLE_RLS_RE.test(content)) {
122
+ // collect which tables have RLS
123
+ const rlsRe = /ALTER\s+TABLE\s+["'`]?(\w+)["'`]?\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY/gi;
124
+ while ((m = rlsRe.exec(content)) !== null) tablesWithRLS.add(m[1].toLowerCase());
125
+ }
126
+ SQL_CREATE_TABLE_RE.lastIndex = 0;
127
+ while ((m = SQL_CREATE_TABLE_RE.exec(content)) !== null) {
128
+ const tableName = m[1].toLowerCase();
129
+ if (!tablesWithRLS.has(tableName)) {
130
+ const lineNum = content.slice(0, m.index).split('\n').length;
131
+ findings.push({
132
+ id: `db-rls:RLS_DISABLED_SQL:${file}:${lineNum}`,
133
+ title: `Table "${m[1]}" created without Row-Level Security`,
134
+ severity: 'high',
135
+ file, line: lineNum,
136
+ description: `The table "${m[1]}" is created but no ALTER TABLE … ENABLE ROW LEVEL SECURITY statement is present in this file. Without RLS, any authenticated user can read and write every row regardless of ownership.`,
137
+ remediation: `Add after the CREATE TABLE:\n ALTER TABLE "${m[1]}" ENABLE ROW LEVEL SECURITY;\n CREATE POLICY "owner_only" ON "${m[1]}" USING (user_id = auth.uid());`,
138
+ cwe: 'CWE-284',
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ return findings;
145
+ }
146
+
147
+ export { scanDatabaseRLS };
@@ -0,0 +1,215 @@
1
+ // Database-aware taint (v0.70 #10).
2
+ //
3
+ // When `user.bio = req.body.bio` writes tainted text to a DB column, then
4
+ // later `display(getUser(id).bio)` reads it and feeds it to an HTML
5
+ // renderer, that's stored-XSS — but our forward-taint analysis loses the
6
+ // flow at the DB boundary because the engine doesn't model storage as a
7
+ // memory channel.
8
+ //
9
+ // This module fills the gap as a SAST detector. It walks file content
10
+ // looking for two patterns in the same file (v1 scope; cross-file in v2):
11
+ // 1. WRITE: ORM write/create/update of a model field FROM a user source
12
+ // 2. READ: ORM read of the SAME model field, used in a render/HTML sink
13
+ //
14
+ // When both patterns match, emit a stored-XSS-class finding with the
15
+ // trace pointing to both sites.
16
+ //
17
+ // ORM frameworks recognized (v1):
18
+ // - Sequelize: Model.create({ field: x }) / .save()
19
+ // - Prisma: prisma.<model>.create({ data: { field: x } })
20
+ // - TypeORM: repo.save({ field: x }) / repo.update(...)
21
+ // - Mongoose: new Model({ field: x }).save()
22
+ // - SQLAlchemy: model.field = x; session.add; session.commit
23
+ // - Django ORM: Model.objects.create(field=x) / Model(field=x).save()
24
+ //
25
+ // Render sinks recognized: res.send / res.render / res.write / res.json,
26
+ // ctx.body, template helpers (template literals, dangerouslySetInnerHTML).
27
+
28
+ import { blankComments } from './_comment-strip.js';
29
+
30
+ const TAINT_HINT_RE =
31
+ /\b(?:req\.|request\.|params\.|query\.|body\.|ctx\.query|ctx\.request|c\.Query|r\.URL\.Query|_GET|_POST|_REQUEST|getParameter)\b/;
32
+
33
+ // Patterns that capture (model, field, value) where value is a user source.
34
+ const WRITE_PATTERNS_JS = [
35
+ // Sequelize Model.create({ field: req.body.x })
36
+ // We capture: model, then any field initializer with a user source.
37
+ /\b([A-Z][\w]*)\s*\.\s*create\s*\(\s*\{[^}]*?\b([a-z_][\w]*)\s*:\s*([^,}]+)/g,
38
+ // Prisma prisma.<model>.create({ data: { field: req.body.x }})
39
+ /\bprisma\s*\.\s*([a-z][\w]*)\s*\.\s*(?:create|update)\s*\(\s*\{[^}]*?data\s*:\s*\{[^}]*?\b([a-z_][\w]*)\s*:\s*([^,}]+)/g,
40
+ // TypeORM repo.save({ field: req.body.x })
41
+ /\b([a-zA-Z_][\w]*)\s*\.\s*save\s*\(\s*\{[^}]*?\b([a-z_][\w]*)\s*:\s*([^,}]+)/g,
42
+ ];
43
+
44
+ const WRITE_PATTERNS_PY = [
45
+ // Django Model.objects.create(field=req.POST['x'])
46
+ /\b([A-Z][\w]*)\s*\.\s*objects\s*\.\s*create\s*\(([^)]*?)\b([a-z_]\w*)\s*=\s*([^,)]+)/g,
47
+ // SQLAlchemy: instance.field = request.x; followed by session.commit()
48
+ /\b([a-z_]\w*)\s*\.\s*([a-z_]\w*)\s*=\s*([^\n;]*?(?:request|req|params|query|body|_GET|_POST)[^\n;]*)/g,
49
+ ];
50
+
51
+ // Read sites: any `.find / findOne / findByPk / findById / query / get`
52
+ // call. We collect those + the captured-variable name (when `const x = ...`)
53
+ // + the model name. Then in a separate pass we look for `<var>.<field>`
54
+ // access AND for direct `.find(...).<field>` chains.
55
+ const READ_CALL_RE_JS =
56
+ /(?:(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\s*=\s*(?:await\s+)?)?\b([A-Z][\w]*|[a-z_][\w]*|prisma\s*\.\s*[a-z][\w]*)\s*\.\s*(?:find|findOne|findByPk|findById|findUnique|findFirst|get|query)\s*\(/g;
57
+
58
+ const READ_CALL_RE_PY =
59
+ /(?:([a-z_]\w*)\s*=\s*)?\b([A-Z][\w]*)\s*\.\s*objects\s*\.\s*(?:get|filter|first|all)\s*\(/g;
60
+
61
+ const RENDER_SINK_RE_JS =
62
+ /\b(?:res\.send|res\.write|res\.render|res\.json|ctx\.body\s*=|dangerouslySetInnerHTML|innerHTML\s*=)\b/;
63
+
64
+ const RENDER_SINK_RE_PY =
65
+ /\b(?:render_template_string|HttpResponse\s*\(|response\.write|HttpResponseHtml)\b/;
66
+
67
+ function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
68
+
69
+ function _lang(fp) {
70
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return 'js';
71
+ if (/\.py$/i.test(fp)) return 'py';
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Walk the file once collecting WRITE pairs (model, field, line) where the
77
+ * written value is a user source.
78
+ */
79
+ function _findTaintedWrites(code, lang) {
80
+ const writes = [];
81
+ const patterns = lang === 'js' ? WRITE_PATTERNS_JS : WRITE_PATTERNS_PY;
82
+ for (const pat of patterns) {
83
+ const re = new RegExp(pat.source, pat.flags);
84
+ let m;
85
+ while ((m = re.exec(code))) {
86
+ // The capture indices vary by pattern. Inspect groups 1..4.
87
+ // For JS Sequelize: m[1]=model, m[2]=field, m[3]=value
88
+ // For Prisma: m[1]=model, m[2]=field, m[3]=value
89
+ // For Python: m[1]=model, m[3]=field, m[4]=value (Django) or
90
+ // m[1]=instance, m[2]=field, m[3]=value (SQLA assign)
91
+ let model, field, value;
92
+ if (lang === 'js') {
93
+ model = m[1]; field = m[2]; value = m[3];
94
+ } else {
95
+ // Django: 4 groups; SQLA-style assign: 3 groups
96
+ if (m.length >= 5 && m[4] !== undefined) {
97
+ model = m[1]; field = m[3]; value = m[4];
98
+ } else {
99
+ model = m[1]; field = m[2]; value = m[3];
100
+ }
101
+ }
102
+ if (!value || !TAINT_HINT_RE.test(value)) continue;
103
+ writes.push({ model, field, line: _lineOf(code, m.index) });
104
+ }
105
+ }
106
+ return writes;
107
+ }
108
+
109
+ /**
110
+ * Walk the file collecting READ sites: any ORM read call, optionally
111
+ * captured into a local variable, where THAT variable's `.field` is
112
+ * accessed within a render-sink window in the following ~20 lines.
113
+ *
114
+ * Handles two indirection levels:
115
+ * (1) `const x = await Model.findOne({...}); res.send('<p>' + x.bio + '</p>')`
116
+ * (2) `res.send(Model.findOne({...}).bio)` (direct chain)
117
+ */
118
+ function _findRendersOfReads(code, lang) {
119
+ const reads = [];
120
+ const callRe = lang === 'js' ? new RegExp(READ_CALL_RE_JS.source, READ_CALL_RE_JS.flags)
121
+ : new RegExp(READ_CALL_RE_PY.source, READ_CALL_RE_PY.flags);
122
+ const sinkRe = lang === 'js' ? RENDER_SINK_RE_JS : RENDER_SINK_RE_PY;
123
+ const lines = code.split('\n');
124
+ let m;
125
+ while ((m = callRe.exec(code))) {
126
+ // JS groups: 1=varName (maybe undefined), 2=model. PY: 1=varName, 2=model.
127
+ const varName = m[1] || null;
128
+ const modelToken = (m[2] || '').trim();
129
+ // Pull a clean model name out of `prisma.user` → `user` style.
130
+ const model = modelToken.includes('.') ? modelToken.split('.').pop() : modelToken;
131
+ const line = _lineOf(code, m.index);
132
+ // Look ahead up to 25 lines for any field access + a render sink in the
133
+ // same window.
134
+ const lo = line - 1;
135
+ const hi = Math.min(lines.length, line + 25);
136
+ const window = lines.slice(lo, hi).join('\n');
137
+ if (!sinkRe.test(window)) continue;
138
+ // Collect every `.<field>` access in the window — they're the read
139
+ // candidates. We try BOTH shapes:
140
+ // (a) <varName>.<field> when the call was assigned
141
+ // (b) .find(...).<field> inline-chain
142
+ const FRAMEWORK_NOISE = new Set(['then', 'catch', 'finally', 'where',
143
+ 'select', 'data', 'create', 'update', 'delete', 'save', 'find',
144
+ 'findOne', 'findFirst', 'findUnique', 'findByPk', 'findById', 'all',
145
+ 'first', 'get', 'query', 'count', 'objects']);
146
+ const fieldRegexes = [];
147
+ if (varName) {
148
+ fieldRegexes.push(new RegExp(`\\b${_escapeRegex(varName)}\\.([a-zA-Z_]\\w*)`, 'g'));
149
+ }
150
+ fieldRegexes.push(new RegExp(
151
+ `\\.\\s*(?:find|findOne|findByPk|findById|findUnique|findFirst|get|query|first|all)\\s*\\([^)]*\\)\\s*\\.\\s*([a-zA-Z_]\\w*)`, 'g'));
152
+ for (const fieldRe of fieldRegexes) {
153
+ let fm;
154
+ while ((fm = fieldRe.exec(window))) {
155
+ const field = fm[1];
156
+ if (FRAMEWORK_NOISE.has(field)) continue;
157
+ const fieldLineLocal = window.slice(0, fm.index).split('\n').length;
158
+ reads.push({ model, field, line: line + fieldLineLocal - 1 });
159
+ }
160
+ }
161
+ }
162
+ return reads;
163
+ }
164
+
165
+ function _escapeRegex(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
166
+
167
+ export function scanDbTaint(fp, raw) {
168
+ if (!raw || raw.length > 500_000) return [];
169
+ const lang = _lang(fp);
170
+ if (!lang) return [];
171
+ const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
172
+ // Pre-filter: file must mention both an ORM-write and a render sink.
173
+ if (!/\bcreate\s*\(|\.\s*save\s*\(|\.\s*objects\s*\.|prisma\s*\./.test(code)) return [];
174
+ const writes = _findTaintedWrites(code, lang);
175
+ if (writes.length === 0) return [];
176
+ const reads = _findRendersOfReads(code, lang);
177
+ if (reads.length === 0) return [];
178
+
179
+ const findings = [];
180
+ const seen = new Set();
181
+ for (const w of writes) {
182
+ for (const r of reads) {
183
+ // Match on field name (column). Model-name matching is best-effort —
184
+ // many codebases pass model classes around through variables.
185
+ if (w.field !== r.field) continue;
186
+ const id = `db-taint:${fp}:${w.line}->${r.line}:${w.model}.${w.field}`;
187
+ if (seen.has(id)) continue;
188
+ seen.add(id);
189
+ findings.push({
190
+ id,
191
+ file: fp, line: r.line,
192
+ vuln: `Stored XSS via DB round-trip (${w.model}.${w.field})`,
193
+ severity: 'high',
194
+ cwe: 'CWE-79',
195
+ family: 'stored-xss',
196
+ stride: 'Tampering',
197
+ snippet: (raw.split('\n')[r.line - 1] || '').trim().slice(0, 200),
198
+ remediation:
199
+ `User content written to ${w.model}.${w.field} at line ${w.line} is later read at line ${r.line} and fed to a render sink. ` +
200
+ 'Mitigations: ' +
201
+ '(1) sanitize at WRITE time (HTML-escape before storage) and tag the column as "html-safe stored", OR ' +
202
+ '(2) escape at READ time inside the render (use the framework\'s auto-escape), OR ' +
203
+ '(3) use a content-security-policy that blocks inline scripts on rendered pages. ' +
204
+ 'Defense in depth: do BOTH (1) and (2).',
205
+ parser: 'DB-TAINT',
206
+ confidence: 0.7,
207
+ trace: [
208
+ { line: w.line, kind: 'db-write', sourceLabel: `${w.model}.${w.field} ← user source` },
209
+ { line: r.line, kind: 'db-read', sourceLabel: `${w.model}.${w.field} → render` },
210
+ ],
211
+ });
212
+ }
213
+ }
214
+ return findings;
215
+ }