@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,659 @@
1
+ // Sources / sinks / sanitizers catalog.
2
+ //
3
+ // Each entry describes a callable or member-access pattern. The taint engine
4
+ // consults this catalog when it sees a call site or a property read.
5
+ //
6
+ // Shape:
7
+ // { kind: 'source' | 'sink' | 'sanitizer',
8
+ // id: '<short-id>',
9
+ // language: 'js' | 'java' | 'py' | '*',
10
+ // framework: '<name>' | null,
11
+ // match: { type: 'call', callee: 'name' // match by callee name (last segment)
12
+ // | 'name.foo' // match by full path 'name.foo'
13
+ // | '*' } // any call
14
+ // | { type: 'member', object, prop } // match member read 'object.prop'
15
+ // | { type: 'global', name }, // free-var reference, e.g. `process.env`
16
+ // // For sources/sinks: which arguments matter?
17
+ // argIndex: number | 'all' | null,
18
+ // // For sinks: the vuln to emit when reached.
19
+ // vuln: { name, severity, cwe, remediation } | null,
20
+ // // For sanitizers: how the sanitizer behaves.
21
+ // effect: 'strip' | 'taintNever' | 'taintIf-not-pinned',
22
+ // }
23
+ //
24
+ // The catalog is intentionally narrow — it's a curated starter set. Adding
25
+ // entries here directly raises recall. Custom rules in .agentic-security/rules
26
+ // can extend it per-project.
27
+
28
+ export const CATALOG = [
29
+ // ─── SOURCES (JS/TS) ───────────────────────────────────────────────────────
30
+ // P4.6 — every source carries a `provenance` label so findings can be
31
+ // severity-scaled by where the input actually came from.
32
+ // Express / common Node HTTP shapes.
33
+ { kind: 'source', id: 'js-req-body', language: 'js', framework: 'express', match: { type: 'member', object: 'req', prop: 'body' }, label: 'req.body', provenance: 'http-body' },
34
+ { kind: 'source', id: 'js-req-query', language: 'js', framework: 'express', match: { type: 'member', object: 'req', prop: 'query' }, label: 'req.query', provenance: 'url-param' },
35
+ { kind: 'source', id: 'js-req-params', language: 'js', framework: 'express', match: { type: 'member', object: 'req', prop: 'params' }, label: 'req.params', provenance: 'path-param' },
36
+ { kind: 'source', id: 'js-req-headers', language: 'js', framework: 'express', match: { type: 'member', object: 'req', prop: 'headers' }, label: 'req.headers', provenance: 'header' },
37
+ { kind: 'source', id: 'js-req-cookies', language: 'js', framework: 'express', match: { type: 'member', object: 'req', prop: 'cookies' }, label: 'req.cookies', provenance: 'cookie' },
38
+ { kind: 'source', id: 'js-request-body', language: 'js', framework: 'express', match: { type: 'member', object: 'request', prop: 'body' }, label: 'request.body', provenance: 'http-body' },
39
+ { kind: 'source', id: 'js-ctx-request', language: 'js', framework: 'koa', match: { type: 'member', object: 'ctx', prop: 'request' }, label: 'ctx.request', provenance: 'http-body' },
40
+ // Browser DOM-derived (XSS sources).
41
+ { kind: 'source', id: 'js-location', language: 'js', framework: 'dom', match: { type: 'global', name: 'location' }, label: 'window.location', provenance: 'url-fragment' },
42
+ { kind: 'source', id: 'js-doc-cookie', language: 'js', framework: 'dom', match: { type: 'member', object: 'document', prop: 'cookie' }, label: 'document.cookie', provenance: 'cookie' },
43
+ { kind: 'source', id: 'js-loc-search', language: 'js', framework: 'dom', match: { type: 'member', object: 'location', prop: 'search' }, label: 'location.search', provenance: 'url-param' },
44
+ { kind: 'source', id: 'js-loc-hash', language: 'js', framework: 'dom', match: { type: 'member', object: 'location', prop: 'hash' }, label: 'location.hash', provenance: 'url-fragment' },
45
+ // process.env is a fixed but partially attacker-controllable surface for some apps.
46
+ { kind: 'source', id: 'js-process-env', language: 'js', framework: 'node', match: { type: 'member', object: 'process', prop: 'env' }, label: 'process.env', provenance: 'env' },
47
+
48
+ // ─── SINKS (JS/TS) ─────────────────────────────────────────────────────────
49
+ // SQL.
50
+ { kind: 'sink', id: 'js-sql-query', language: 'js', framework: 'sql', match: { type: 'call', callee: 'query' }, argIndex: 0,
51
+ vuln: { name: 'SQL Injection (db.query)', severity: 'critical', cwe: 'CWE-89',
52
+ remediation: 'Use parameterized queries: db.query("SELECT * FROM t WHERE id = ?", [id]). Never interpolate untrusted strings into SQL.' } },
53
+ { kind: 'sink', id: 'js-sql-execute', language: 'js', framework: 'sql', match: { type: 'call', callee: 'execute' }, argIndex: 0,
54
+ vuln: { name: 'SQL Injection (db.execute)', severity: 'critical', cwe: 'CWE-89',
55
+ remediation: 'Use parameterized queries: db.execute("SELECT * FROM t WHERE id = ?", [id]).' } },
56
+ // OS command.
57
+ { kind: 'sink', id: 'js-exec', language: 'js', framework: 'node', match: { type: 'call', callee: 'exec' }, argIndex: 0,
58
+ vuln: { name: 'Command Injection (child_process.exec)', severity: 'critical', cwe: 'CWE-78',
59
+ remediation: 'Use execFile or spawn with an argv array instead of exec — exec invokes the shell. If shell features are required, escape with shell-escape, never string-concat user input.' } },
60
+ { kind: 'sink', id: 'js-execSync', language: 'js', framework: 'node', match: { type: 'call', callee: 'execSync' }, argIndex: 0,
61
+ vuln: { name: 'Command Injection (execSync)', severity: 'critical', cwe: 'CWE-78',
62
+ remediation: 'Use spawnSync with an argv array.' } },
63
+ // Code evaluation.
64
+ { kind: 'sink', id: 'js-eval', language: 'js', framework: 'node', match: { type: 'call', callee: 'eval' }, argIndex: 0,
65
+ vuln: { name: 'Code Injection (eval)', severity: 'critical', cwe: 'CWE-95',
66
+ remediation: 'Never eval user input. Use JSON.parse for structured data; for dispatch, use an explicit map.' } },
67
+ { kind: 'sink', id: 'js-Function', language: 'js', framework: 'node', match: { type: 'call', callee: 'Function' }, argIndex: 'all',
68
+ vuln: { name: 'Code Injection (Function constructor)', severity: 'critical', cwe: 'CWE-95',
69
+ remediation: 'The Function constructor is equivalent to eval — never feed user input into it.' } },
70
+ // XSS / DOM sinks (assignment-form, not match).
71
+ // innerHTML and outerHTML are handled in the engine via assignment LHS matching.
72
+ // DOM sinks.
73
+ { kind: 'sink', id: 'js-document-write', language: 'js', framework: 'dom', match: { type: 'call', callee: 'write' }, argIndex: 0,
74
+ vuln: { name: 'XSS (document.write)', severity: 'high', cwe: 'CWE-79',
75
+ remediation: 'document.write is universally unsafe — use textContent or a typed templating engine.' } },
76
+ // SSRF / HTTP-client sinks: matched by callee; rich-CWE classification in engine.
77
+ { kind: 'sink', id: 'js-fetch', language: 'js', framework: 'browser', match: { type: 'call', callee: 'fetch' }, argIndex: 0,
78
+ vuln: { name: 'SSRF (fetch)', severity: 'high', cwe: 'CWE-918',
79
+ remediation: 'Resolve the target host first and reject RFC1918 / metadata-endpoint addresses before fetching.' } },
80
+ // File system sinks.
81
+ { kind: 'sink', id: 'js-fs-readFile', language: 'js', framework: 'node', match: { type: 'call', callee: 'readFile' }, argIndex: 0,
82
+ vuln: { name: 'Path Traversal (fs.readFile)', severity: 'high', cwe: 'CWE-22',
83
+ remediation: 'Canonicalize the path and assert it stays within an allow-listed base directory before reading.' } },
84
+ { kind: 'sink', id: 'js-fs-writeFile', language: 'js', framework: 'node', match: { type: 'call', callee: 'writeFile' }, argIndex: 0,
85
+ vuln: { name: 'Arbitrary File Write (fs.writeFile)', severity: 'critical', cwe: 'CWE-73',
86
+ remediation: 'Never write to a path derived from untrusted input. Generate filenames server-side from content hashes.' } },
87
+ // Redirects.
88
+ { kind: 'sink', id: 'js-res-redirect', language: 'js', framework: 'express', match: { type: 'call', callee: 'redirect' }, argIndex: 0,
89
+ vuln: { name: 'Open Redirect', severity: 'medium', cwe: 'CWE-601',
90
+ remediation: 'Whitelist destination URLs; never pass req-derived strings straight into res.redirect.' } },
91
+
92
+ // ─── SANITIZERS (JS/TS) ────────────────────────────────────────────────────
93
+ { kind: 'sanitizer', id: 'js-encodeURIComponent', language: 'js', match: { type: 'call', callee: 'encodeURIComponent' }, effect: 'strip', appliesTo: ['url'] },
94
+ { kind: 'sanitizer', id: 'js-html-escape', language: 'js', match: { type: 'call', callee: 'escapeHtml' }, effect: 'strip', appliesTo: ['xss'] },
95
+ { kind: 'sanitizer', id: 'js-dompurify', language: 'js', match: { type: 'call', callee: 'sanitize' }, effect: 'strip', appliesTo: ['xss'] },
96
+ { kind: 'sanitizer', id: 'js-shell-escape', language: 'js', match: { type: 'call', callee: 'shellEscape' }, effect: 'strip', appliesTo: ['cmd'] },
97
+ { kind: 'sanitizer', id: 'js-parseInt', language: 'js', match: { type: 'call', callee: 'parseInt' }, effect: 'strip', appliesTo: ['*'] },
98
+ { kind: 'sanitizer', id: 'js-Number', language: 'js', match: { type: 'call', callee: 'Number' }, effect: 'strip', appliesTo: ['*'] },
99
+ { kind: 'sanitizer', id: 'js-String-coerce', language: 'js', match: { type: 'call', callee: 'String' }, effect: 'strip', appliesTo: ['mongo-operator'] },
100
+ { kind: 'sanitizer', id: 'js-validator-escape', language: 'js', match: { type: 'call', callee: 'escape' }, effect: 'strip', appliesTo: ['xss'] },
101
+ { kind: 'sanitizer', id: 'js-strip_tags', language: 'js', match: { type: 'call', callee: 'stripTags' }, effect: 'strip', appliesTo: ['xss'] },
102
+
103
+ // ─── SOURCES (Python — Flask / FastAPI / Django) ──────────────────────────
104
+ { kind: 'source', id: 'py-flask-request-args', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'args' }, label: 'request.args' },
105
+ { kind: 'source', id: 'py-flask-request-form', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'form' }, label: 'request.form' },
106
+ { kind: 'source', id: 'py-flask-request-json', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'json' }, label: 'request.json' },
107
+ { kind: 'source', id: 'py-flask-request-values', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'values' }, label: 'request.values' },
108
+ { kind: 'source', id: 'py-flask-request-cookies',language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'cookies' }, label: 'request.cookies' },
109
+ { kind: 'source', id: 'py-flask-request-headers',language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'headers' }, label: 'request.headers' },
110
+ { kind: 'source', id: 'py-flask-request-data', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'data' }, label: 'request.data' },
111
+ { kind: 'source', id: 'py-fastapi-request-query',language: 'py', framework: 'fastapi', match: { type: 'call', callee: 'Query' }, label: 'fastapi.Query()' },
112
+ { kind: 'source', id: 'py-fastapi-request-body', language: 'py', framework: 'fastapi', match: { type: 'call', callee: 'Body' }, label: 'fastapi.Body()' },
113
+ { kind: 'source', id: 'py-fastapi-form', language: 'py', framework: 'fastapi', match: { type: 'call', callee: 'Form' }, label: 'fastapi.Form()' },
114
+ { kind: 'source', id: 'py-django-request-GET', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'GET' }, label: 'request.GET' },
115
+ { kind: 'source', id: 'py-django-request-POST', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'POST' }, label: 'request.POST' },
116
+ { kind: 'source', id: 'py-django-request-FILES', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'FILES' }, label: 'request.FILES' },
117
+ { kind: 'source', id: 'py-django-request-META', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'META' }, label: 'request.META' },
118
+ { kind: 'source', id: 'py-os-getenv', language: 'py', framework: 'stdlib', match: { type: 'call', callee: 'getenv' }, label: 'os.getenv' },
119
+ { kind: 'source', id: 'py-os-environ', language: 'py', framework: 'stdlib', match: { type: 'member', object: 'os', prop: 'environ' }, label: 'os.environ' },
120
+ { kind: 'source', id: 'py-input', language: 'py', framework: 'stdlib', match: { type: 'call', callee: 'input' }, label: 'input()' },
121
+
122
+ // ─── SOURCES (Java — Spring / Servlet) ────────────────────────────────────
123
+ { kind: 'source', id: 'java-request-getParameter', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getParameter' }, label: 'request.getParameter' },
124
+ { kind: 'source', id: 'java-request-getHeader', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getHeader' }, label: 'request.getHeader' },
125
+ { kind: 'source', id: 'java-request-getCookies', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getCookies' }, label: 'request.getCookies' },
126
+ { kind: 'source', id: 'java-request-getInputStream', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getInputStream' }, label: 'request.getInputStream' },
127
+ { kind: 'source', id: 'java-request-getReader', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getReader' }, label: 'request.getReader' },
128
+ { kind: 'source', id: 'java-system-getenv', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'getenv' }, label: 'System.getenv' },
129
+ { kind: 'source', id: 'java-system-getProperty', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'getProperty' }, label: 'System.getProperty' },
130
+ // Spring annotation-style sources are detected per-rule, not as catalog
131
+ // members (they're parameter decorators rather than callable shapes).
132
+
133
+ // ─── SOURCES (Go) ─────────────────────────────────────────────────────────
134
+ { kind: 'source', id: 'go-r-form', language: 'go', framework: 'net/http', match: { type: 'member', object: 'r', prop: 'Form' }, label: 'r.Form' },
135
+ { kind: 'source', id: 'go-r-postform', language: 'go', framework: 'net/http', match: { type: 'member', object: 'r', prop: 'PostForm' }, label: 'r.PostForm' },
136
+ { kind: 'source', id: 'go-r-body', language: 'go', framework: 'net/http', match: { type: 'member', object: 'r', prop: 'Body' }, label: 'r.Body' },
137
+ { kind: 'source', id: 'go-r-formvalue',language: 'go', framework: 'net/http', match: { type: 'call', callee: 'FormValue' }, label: 'r.FormValue' },
138
+ { kind: 'source', id: 'go-r-uquery', language: 'go', framework: 'net/http', match: { type: 'call', callee: 'Query' }, label: 'r.URL.Query' },
139
+ { kind: 'source', id: 'go-gin-query', language: 'go', framework: 'gin', match: { type: 'call', callee: 'Query' }, label: 'c.Query (gin)' },
140
+ { kind: 'source', id: 'go-gin-bindjson',language:'go', framework: 'gin', match: { type: 'call', callee: 'BindJSON' }, label: 'c.BindJSON (gin)' },
141
+ { kind: 'source', id: 'go-echo-param', language: 'go', framework: 'echo', match: { type: 'call', callee: 'Param' }, label: 'c.Param (echo)' },
142
+
143
+ // ─── SOURCES (Ruby — Rails / Sinatra) ─────────────────────────────────────
144
+ { kind: 'source', id: 'rb-rails-params', language: 'rb', framework: 'rails', match: { type: 'global', name: 'params' }, label: 'params (Rails)' },
145
+ { kind: 'source', id: 'rb-rails-cookies', language: 'rb', framework: 'rails', match: { type: 'global', name: 'cookies' }, label: 'cookies (Rails)' },
146
+ { kind: 'source', id: 'rb-rails-session', language: 'rb', framework: 'rails', match: { type: 'global', name: 'session' }, label: 'session (Rails)' },
147
+ { kind: 'source', id: 'rb-env', language: 'rb', framework: 'stdlib',match: { type: 'global', name: 'ENV' }, label: 'ENV (Ruby)' },
148
+
149
+ // ─── SOURCES (PHP) ────────────────────────────────────────────────────────
150
+ { kind: 'source', id: 'php-request', language: 'php', framework: 'core', match: { type: 'global', name: '_REQUEST' }, label: '$_REQUEST' },
151
+ { kind: 'source', id: 'php-get', language: 'php', framework: 'core', match: { type: 'global', name: '_GET' }, label: '$_GET' },
152
+ { kind: 'source', id: 'php-post', language: 'php', framework: 'core', match: { type: 'global', name: '_POST' }, label: '$_POST' },
153
+ { kind: 'source', id: 'php-cookie', language: 'php', framework: 'core', match: { type: 'global', name: '_COOKIE' }, label: '$_COOKIE' },
154
+ { kind: 'source', id: 'php-server', language: 'php', framework: 'core', match: { type: 'global', name: '_SERVER' }, label: '$_SERVER' },
155
+
156
+ // ─── SINKS (SQL — Python) ─────────────────────────────────────────────────
157
+ { kind: 'sink', id: 'py-cursor-execute', language: 'py', framework: 'dbapi', match: { type: 'call', callee: 'execute' }, argIndex: 0,
158
+ vuln: { name: 'SQL Injection (cursor.execute)', severity: 'critical', cwe: 'CWE-89',
159
+ remediation: 'Use parameterised execute: `cur.execute("SELECT * FROM t WHERE id = %s", (id,))`.' } },
160
+ { kind: 'sink', id: 'py-cursor-executemany', language: 'py', framework: 'dbapi', match: { type: 'call', callee: 'executemany' }, argIndex: 0,
161
+ vuln: { name: 'SQL Injection (cursor.executemany)', severity: 'critical', cwe: 'CWE-89',
162
+ remediation: 'Use parameterised executemany with a list of tuples.' } },
163
+ { kind: 'sink', id: 'py-sa-text', language: 'py', framework: 'sqlalchemy', match: { type: 'call', callee: 'text' }, argIndex: 0,
164
+ vuln: { name: 'SQL Injection (sqlalchemy.text)', severity: 'critical', cwe: 'CWE-89',
165
+ remediation: 'Use sqlalchemy.text with bound parameters: `text("SELECT :x").bindparams(x=v)`.' } },
166
+
167
+ // ─── SINKS (SQL — Java) ───────────────────────────────────────────────────
168
+ { kind: 'sink', id: 'java-stmt-executeQuery', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'executeQuery' }, argIndex: 0,
169
+ vuln: { name: 'SQL Injection (Statement.executeQuery)', severity: 'critical', cwe: 'CWE-89',
170
+ remediation: 'Use PreparedStatement + setX(N, value). Never concatenate user input into the SQL string.' } },
171
+ { kind: 'sink', id: 'java-stmt-executeUpdate', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'executeUpdate' }, argIndex: 0,
172
+ vuln: { name: 'SQL Injection (Statement.executeUpdate)', severity: 'critical', cwe: 'CWE-89',
173
+ remediation: 'Use PreparedStatement + setX(N, value).' } },
174
+ { kind: 'sink', id: 'java-stmt-execute', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'execute' }, argIndex: 0,
175
+ vuln: { name: 'SQL Injection (Statement.execute)', severity: 'critical', cwe: 'CWE-89', remediation: 'Use PreparedStatement.' } },
176
+ { kind: 'sink', id: 'java-jdbc-prepareStatement', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'prepareStatement' }, argIndex: 0,
177
+ vuln: { name: 'SQL Injection (PreparedStatement built via concat)', severity: 'critical', cwe: 'CWE-89',
178
+ remediation: 'Use placeholders (?) in the SQL string; bind values via setX(N, value).' } },
179
+ { kind: 'sink', id: 'java-stmt-addBatch', language: 'java', framework: 'jdbc', match: { type: 'call', callee: 'addBatch' }, argIndex: 0,
180
+ vuln: { name: 'SQL Injection (Statement.addBatch)', severity: 'critical', cwe: 'CWE-89',
181
+ remediation: 'Use PreparedStatement.addBatch — bind parameters per-batch.' } },
182
+ { kind: 'sink', id: 'java-hibernate-createQuery', language: 'java', framework: 'hibernate', match: { type: 'call', callee: 'createQuery' }, argIndex: 0,
183
+ vuln: { name: 'HQL Injection (Hibernate.createQuery)', severity: 'critical', cwe: 'CWE-89',
184
+ remediation: 'Use setParameter / named parameters instead of HQL string concat.' } },
185
+ { kind: 'sink', id: 'java-hibernate-createSqlQuery', language: 'java', framework: 'hibernate', match: { type: 'call', callee: 'createSQLQuery' }, argIndex: 0,
186
+ vuln: { name: 'Native SQL Injection (Hibernate.createSQLQuery)', severity: 'critical', cwe: 'CWE-89',
187
+ remediation: 'Use setParameter on the resulting query.' } },
188
+ { kind: 'sink', id: 'java-jpa-createNativeQuery', language: 'java', framework: 'jpa', match: { type: 'call', callee: 'createNativeQuery' }, argIndex: 0,
189
+ vuln: { name: 'Native SQL Injection (EntityManager.createNativeQuery)', severity: 'critical', cwe: 'CWE-89',
190
+ remediation: 'Use setParameter on the resulting Query.' } },
191
+
192
+ // ─── SINKS (XSS / template — JS/TS / browser) ─────────────────────────────
193
+ { kind: 'sink', id: 'js-innerHTML-assign', language: 'js', framework: 'dom', match: { type: 'member', object: '_any_', prop: 'innerHTML' }, argIndex: 'rhs',
194
+ vuln: { name: 'DOM XSS (innerHTML)', severity: 'high', cwe: 'CWE-79',
195
+ remediation: 'Use textContent or a trusted-types sanitizer; never assign user-derived strings to innerHTML.' } },
196
+ { kind: 'sink', id: 'js-outerHTML-assign', language: 'js', framework: 'dom', match: { type: 'member', object: '_any_', prop: 'outerHTML' }, argIndex: 'rhs',
197
+ vuln: { name: 'DOM XSS (outerHTML)', severity: 'high', cwe: 'CWE-79',
198
+ remediation: 'Use textContent or a trusted-types sanitizer.' } },
199
+ { kind: 'sink', id: 'js-insertAdjacentHTML', language: 'js', framework: 'dom', match: { type: 'call', callee: 'insertAdjacentHTML' }, argIndex: 1,
200
+ vuln: { name: 'DOM XSS (insertAdjacentHTML)', severity: 'high', cwe: 'CWE-79',
201
+ remediation: 'Use insertAdjacentText, or sanitize the HTML with DOMPurify first.' } },
202
+ { kind: 'sink', id: 'react-dangerouslySetInnerHTML', language: 'js', framework: 'react', match: { type: 'member', object: '_any_', prop: 'dangerouslySetInnerHTML' }, argIndex: 'rhs',
203
+ vuln: { name: 'XSS via dangerouslySetInnerHTML', severity: 'high', cwe: 'CWE-79',
204
+ remediation: 'Sanitize the __html field via DOMPurify before passing it to dangerouslySetInnerHTML — better, render text via children.' } },
205
+
206
+ // ─── SINKS (HTTP outbound / SSRF) ─────────────────────────────────────────
207
+ { kind: 'sink', id: 'py-requests-get', language: 'py', framework: 'requests', match: { type: 'call', callee: 'get' }, argIndex: 0,
208
+ vuln: { name: 'SSRF (requests.get)', severity: 'high', cwe: 'CWE-918',
209
+ remediation: 'Resolve the URL host and reject RFC1918 + metadata endpoints before fetching. Use an allow-list.' } },
210
+ { kind: 'sink', id: 'py-requests-post', language: 'py', framework: 'requests', match: { type: 'call', callee: 'post' }, argIndex: 0,
211
+ vuln: { name: 'SSRF (requests.post)', severity: 'high', cwe: 'CWE-918', remediation: 'Validate the URL host before posting.' } },
212
+ { kind: 'sink', id: 'py-urlopen', language: 'py', framework: 'urllib', match: { type: 'call', callee: 'urlopen' }, argIndex: 0,
213
+ vuln: { name: 'SSRF (urllib.request.urlopen)', severity: 'high', cwe: 'CWE-918', remediation: 'Validate the URL host before opening.' } },
214
+ { kind: 'sink', id: 'go-http-get', language: 'go', framework: 'net/http', match: { type: 'call', callee: 'Get' }, argIndex: 0,
215
+ vuln: { name: 'SSRF (http.Get)', severity: 'high', cwe: 'CWE-918', remediation: 'Validate the URL host before fetching; reject RFC1918 + metadata endpoints.' } },
216
+
217
+ // ─── SINKS (command exec) ─────────────────────────────────────────────────
218
+ { kind: 'sink', id: 'py-subprocess-run', language: 'py', framework: 'subprocess', match: { type: 'call', callee: 'run' }, argIndex: 0,
219
+ vuln: { name: 'Command Injection (subprocess.run shell=True)', severity: 'critical', cwe: 'CWE-78',
220
+ remediation: 'Pass argv as a list; never pass a single string with shell=True.' } },
221
+ { kind: 'sink', id: 'py-os-system', language: 'py', framework: 'os', match: { type: 'call', callee: 'system' }, argIndex: 0,
222
+ vuln: { name: 'Command Injection (os.system)', severity: 'critical', cwe: 'CWE-78',
223
+ remediation: 'os.system invokes /bin/sh -c; use subprocess.run([...]) with an argv list.' } },
224
+ { kind: 'sink', id: 'java-runtime-exec', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'exec' }, argIndex: 0,
225
+ vuln: { name: 'Command Injection (Runtime.exec string-form)', severity: 'critical', cwe: 'CWE-78',
226
+ remediation: 'Use Runtime.exec(String[]) or ProcessBuilder(String[]).' } },
227
+ { kind: 'sink', id: 'go-os-exec-command', language: 'go', framework: 'os/exec', match: { type: 'call', callee: 'Command' }, argIndex: 0,
228
+ vuln: { name: 'Command Injection (exec.Command via /bin/sh -c)', severity: 'critical', cwe: 'CWE-78',
229
+ remediation: 'When the first arg is "/bin/sh" or "bash" with a -c string built from user input, the shell parses it. Pass argv array values directly to exec.Command.' } },
230
+
231
+ // ─── SINKS (deserialization) ──────────────────────────────────────────────
232
+ { kind: 'sink', id: 'py-pickle-loads', language: 'py', framework: 'pickle', match: { type: 'call', callee: 'loads' }, argIndex: 0,
233
+ vuln: { name: 'Insecure Deserialization (pickle.loads)', severity: 'critical', cwe: 'CWE-502',
234
+ remediation: 'Never pickle-load attacker-controlled data. Use JSON / msgpack with an explicit schema.' } },
235
+ { kind: 'sink', id: 'py-yaml-load', language: 'py', framework: 'pyyaml', match: { type: 'call', callee: 'load' }, argIndex: 0,
236
+ vuln: { name: 'Insecure Deserialization (yaml.load)', severity: 'critical', cwe: 'CWE-502',
237
+ remediation: 'Use yaml.safe_load.' } },
238
+ { kind: 'sink', id: 'java-ois-readObject', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'readObject' }, argIndex: 'all',
239
+ vuln: { name: 'Insecure Deserialization (ObjectInputStream.readObject)', severity: 'critical', cwe: 'CWE-502',
240
+ remediation: 'Use a typed format (Jackson with explicit class allow-list, protobuf).' } },
241
+ { kind: 'sink', id: 'rb-marshal-load', language: 'rb', framework: 'stdlib', match: { type: 'call', callee: 'load' }, argIndex: 0,
242
+ vuln: { name: 'Insecure Deserialization (Marshal.load)', severity: 'critical', cwe: 'CWE-502',
243
+ remediation: 'Marshal is unsafe by design — use JSON.' } },
244
+ { kind: 'sink', id: 'php-unserialize', language: 'php', framework: 'stdlib', match: { type: 'call', callee: 'unserialize' }, argIndex: 0,
245
+ vuln: { name: 'Insecure Deserialization (unserialize)', severity: 'critical', cwe: 'CWE-502',
246
+ remediation: 'Use json_decode instead — unserialize triggers __destruct on gadget classes.' } },
247
+
248
+ // ─── SINKS (template / SSTI) ──────────────────────────────────────────────
249
+ { kind: 'sink', id: 'py-jinja-from-string', language: 'py', framework: 'jinja2', match: { type: 'call', callee: 'from_string' }, argIndex: 0,
250
+ vuln: { name: 'SSTI (Jinja2.from_string)', severity: 'critical', cwe: 'CWE-94',
251
+ remediation: 'Never feed a user-supplied string into a template engine. Use pre-registered templates and pass values as variables.' } },
252
+ { kind: 'sink', id: 'rb-erb-new', language: 'rb', framework: 'erb', match: { type: 'call', callee: 'new' }, argIndex: 0,
253
+ vuln: { name: 'SSTI (ERB.new)', severity: 'critical', cwe: 'CWE-94',
254
+ remediation: 'Use pre-existing templates with binding/locals — never construct a template from user input.' } },
255
+ { kind: 'sink', id: 'js-handlebars-compile',language: 'js', framework: 'handlebars', match: { type: 'call', callee: 'compile' }, argIndex: 0,
256
+ vuln: { name: 'SSTI (Handlebars.compile)', severity: 'high', cwe: 'CWE-94', remediation: 'Compile only known templates; never compile a user-supplied string.' } },
257
+
258
+ // ─── SINKS (file paths / traversal) ───────────────────────────────────────
259
+ { kind: 'sink', id: 'py-open', language: 'py', framework: 'stdlib', match: { type: 'call', callee: 'open' }, argIndex: 0,
260
+ vuln: { name: 'Path Traversal (open)', severity: 'high', cwe: 'CWE-22',
261
+ remediation: 'Canonicalize the path with os.path.realpath + verify it stays within an allow-list of base directories.' } },
262
+ { kind: 'sink', id: 'java-new-File', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'File' }, argIndex: 0,
263
+ vuln: { name: 'Path Traversal (new File)', severity: 'high', cwe: 'CWE-22',
264
+ remediation: 'Canonicalize with Path.normalize + startsWith(base).' } },
265
+ { kind: 'sink', id: 'go-os-open', language: 'go', framework: 'os', match: { type: 'call', callee: 'Open' }, argIndex: 0,
266
+ vuln: { name: 'Path Traversal (os.Open)', severity: 'high', cwe: 'CWE-22',
267
+ remediation: 'Use filepath.Clean + verify the path is rooted in your allow-list dir.' } },
268
+
269
+ // ─── SINKS (LDAP / XPath) ─────────────────────────────────────────────────
270
+ { kind: 'sink', id: 'java-ldap-search', language: 'java', framework: 'jndi', match: { type: 'call', callee: 'search' }, argIndex: 1,
271
+ vuln: { name: 'LDAP Injection (DirContext.search)', severity: 'high', cwe: 'CWE-90',
272
+ remediation: 'Escape LDAP filter metacharacters with Rdn.escapeValue or use a parameterised filter.' } },
273
+ { kind: 'sink', id: 'java-xpath-compile', language: 'java', framework: 'xpath', match: { type: 'call', callee: 'compile' }, argIndex: 0,
274
+ vuln: { name: 'XPath Injection (XPath.compile)', severity: 'high', cwe: 'CWE-643',
275
+ remediation: 'Use XPathVariableResolver or setXPathVariableResolver; never concat user input into the expression.' } },
276
+
277
+ // ─── SINKS (regex DoS / ReDoS) ────────────────────────────────────────────
278
+ { kind: 'sink', id: 'js-RegExp-new', language: 'js', framework: 'core', match: { type: 'call', callee: 'RegExp' }, argIndex: 0,
279
+ vuln: { name: 'ReDoS via user-controlled RegExp', severity: 'medium', cwe: 'CWE-1333',
280
+ remediation: 'Treat user-supplied patterns as untrusted: limit length, reject nested quantifiers, time-bound the match with a watchdog. Better: don\'t accept regex from users at all.' } },
281
+
282
+ // ─── SINKS (redirect) ─────────────────────────────────────────────────────
283
+ { kind: 'sink', id: 'py-redirect', language: 'py', framework: 'flask', match: { type: 'call', callee: 'redirect' }, argIndex: 0,
284
+ vuln: { name: 'Open Redirect (flask.redirect)', severity: 'medium', cwe: 'CWE-601',
285
+ remediation: 'Validate the target URL against an allow-list of internal paths.' } },
286
+ { kind: 'sink', id: 'java-sendRedirect', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'sendRedirect' }, argIndex: 0,
287
+ vuln: { name: 'Open Redirect (response.sendRedirect)', severity: 'medium', cwe: 'CWE-601',
288
+ remediation: 'Validate the target URL against an allow-list.' } },
289
+
290
+ // ─── SINKS (XXE) ──────────────────────────────────────────────────────────
291
+ { kind: 'sink', id: 'java-DocumentBuilder-parse', language: 'java', framework: 'jaxp', match: { type: 'call', callee: 'parse' }, argIndex: 'all',
292
+ vuln: { name: 'XXE (DocumentBuilder.parse)', severity: 'high', cwe: 'CWE-611',
293
+ remediation: 'Disable DTDs: dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true).' } },
294
+ { kind: 'sink', id: 'py-etree-parse', language: 'py', framework: 'lxml', match: { type: 'call', callee: 'parse' }, argIndex: 0,
295
+ vuln: { name: 'XXE (lxml.etree.parse)', severity: 'high', cwe: 'CWE-611',
296
+ remediation: 'Use defusedxml.ElementTree or pass resolve_entities=False.' } },
297
+
298
+ // ─── SINKS (NoSQL) ────────────────────────────────────────────────────────
299
+ { kind: 'sink', id: 'js-mongo-where', language: 'js', framework: 'mongo', match: { type: 'call', callee: '$where' }, argIndex: 0,
300
+ vuln: { name: 'NoSQL Injection ($where)', severity: 'critical', cwe: 'CWE-943',
301
+ remediation: 'Never build a $where string from user input — it runs server-side JavaScript.' } },
302
+
303
+ // ─── SANITIZERS (Python) ──────────────────────────────────────────────────
304
+ { kind: 'sanitizer', id: 'py-bleach-clean', language: 'py', match: { type: 'call', callee: 'clean' }, effect: 'strip', appliesTo: ['xss'] },
305
+ { kind: 'sanitizer', id: 'py-html-escape', language: 'py', match: { type: 'call', callee: 'escape' }, effect: 'strip', appliesTo: ['xss'] },
306
+ { kind: 'sanitizer', id: 'py-markupsafe-escape',language: 'py', match: { type: 'call', callee: 'Markup' }, effect: 'strip', appliesTo: ['xss'] },
307
+ { kind: 'sanitizer', id: 'py-shlex-quote', language: 'py', match: { type: 'call', callee: 'quote' }, effect: 'strip', appliesTo: ['cmd'] },
308
+ { kind: 'sanitizer', id: 'py-int', language: 'py', match: { type: 'call', callee: 'int' }, effect: 'strip', appliesTo: ['*'] },
309
+ { kind: 'sanitizer', id: 'py-float', language: 'py', match: { type: 'call', callee: 'float' }, effect: 'strip', appliesTo: ['*'] },
310
+
311
+ // ─── SANITIZERS (Java) ────────────────────────────────────────────────────
312
+ { kind: 'sanitizer', id: 'java-esapi-encoder-htmlEncode', language: 'java', match: { type: 'call', callee: 'encodeForHTML' }, effect: 'strip', appliesTo: ['xss'] },
313
+ { kind: 'sanitizer', id: 'java-esapi-encoder-sqlEncode', language: 'java', match: { type: 'call', callee: 'encodeForSQL' }, effect: 'strip', appliesTo: ['sql'] },
314
+ { kind: 'sanitizer', id: 'java-esapi-encoder-ldapEncode', language: 'java', match: { type: 'call', callee: 'encodeForLDAP' }, effect: 'strip', appliesTo: ['ldap'] },
315
+ { kind: 'sanitizer', id: 'java-esapi-encoder-xpathEncode', language: 'java', match: { type: 'call', callee: 'encodeForXPath' }, effect: 'strip', appliesTo: ['xpath'] },
316
+ { kind: 'sanitizer', id: 'java-stringutils-escapeHtml', language: 'java', match: { type: 'call', callee: 'escapeHtml4' }, effect: 'strip', appliesTo: ['xss'] },
317
+ { kind: 'sanitizer', id: 'java-stringutils-escapeXml', language: 'java', match: { type: 'call', callee: 'escapeXml' }, effect: 'strip', appliesTo: ['xml','xss'] },
318
+ { kind: 'sanitizer', id: 'java-html-utils', language: 'java', match: { type: 'call', callee: 'htmlEscape' }, effect: 'strip', appliesTo: ['xss'] },
319
+ { kind: 'sanitizer', id: 'java-integer-parseInt', language: 'java', match: { type: 'call', callee: 'parseInt' }, effect: 'strip', appliesTo: ['*'] },
320
+ { kind: 'sanitizer', id: 'java-long-parseLong', language: 'java', match: { type: 'call', callee: 'parseLong' }, effect: 'strip', appliesTo: ['*'] },
321
+ { kind: 'sanitizer', id: 'java-uuid-fromString', language: 'java', match: { type: 'call', callee: 'fromString' }, effect: 'strip', appliesTo: ['*'] },
322
+
323
+ // ─── SANITIZERS (PHP) ─────────────────────────────────────────────────────
324
+ { kind: 'sanitizer', id: 'php-htmlspecialchars', language: 'php', match: { type: 'call', callee: 'htmlspecialchars' }, effect: 'strip', appliesTo: ['xss'] },
325
+ { kind: 'sanitizer', id: 'php-htmlentities', language: 'php', match: { type: 'call', callee: 'htmlentities' }, effect: 'strip', appliesTo: ['xss'] },
326
+ { kind: 'sanitizer', id: 'php-escapeshellarg', language: 'php', match: { type: 'call', callee: 'escapeshellarg' }, effect: 'strip', appliesTo: ['cmd'] },
327
+ { kind: 'sanitizer', id: 'php-escapeshellcmd', language: 'php', match: { type: 'call', callee: 'escapeshellcmd' }, effect: 'strip', appliesTo: ['cmd'] },
328
+ { kind: 'sanitizer', id: 'php-intval', language: 'php', match: { type: 'call', callee: 'intval' }, effect: 'strip', appliesTo: ['*'] },
329
+ { kind: 'sanitizer', id: 'php-filter-var', language: 'php', match: { type: 'call', callee: 'filter_var' }, effect: 'strip', appliesTo: ['*'] },
330
+
331
+ // ─── SANITIZERS (Ruby) ────────────────────────────────────────────────────
332
+ { kind: 'sanitizer', id: 'rb-rails-html-escape', language: 'rb', match: { type: 'call', callee: 'h' }, effect: 'strip', appliesTo: ['xss'] },
333
+ { kind: 'sanitizer', id: 'rb-erb-util-html', language: 'rb', match: { type: 'call', callee: 'html_escape' },effect: 'strip', appliesTo: ['xss'] },
334
+ { kind: 'sanitizer', id: 'rb-shellwords-escape', language: 'rb', match: { type: 'call', callee: 'shellescape' },effect: 'strip', appliesTo: ['cmd'] },
335
+ { kind: 'sanitizer', id: 'rb-cgi-escape', language: 'rb', match: { type: 'call', callee: 'escape' }, effect: 'strip', appliesTo: ['xss','url'] },
336
+
337
+ // ─── SANITIZERS (Go) ──────────────────────────────────────────────────────
338
+ { kind: 'sanitizer', id: 'go-html-escape', language: 'go', match: { type: 'call', callee: 'EscapeString' }, effect: 'strip', appliesTo: ['xss'] },
339
+ { kind: 'sanitizer', id: 'go-strconv-atoi', language: 'go', match: { type: 'call', callee: 'Atoi' }, effect: 'strip', appliesTo: ['*'] },
340
+
341
+ // ─── SOURCES (Python) ──────────────────────────────────────────────────────
342
+ // Flask request object — request is module-imported, properties are sources.
343
+ { kind: 'source', id: 'py-flask-form', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'form' }, label: 'flask.request.form', provenance: 'http-body' },
344
+ { kind: 'source', id: 'py-flask-args', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'args' }, label: 'flask.request.args', provenance: 'url-param' },
345
+ { kind: 'source', id: 'py-flask-json', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'json' }, label: 'flask.request.json', provenance: 'http-body' },
346
+ { kind: 'source', id: 'py-flask-values', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'values' }, label: 'flask.request.values', provenance: 'http-body' },
347
+ { kind: 'source', id: 'py-flask-cookies', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'cookies'}, label: 'flask.request.cookies',provenance: 'cookie' },
348
+ { kind: 'source', id: 'py-flask-headers', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'headers'}, label: 'flask.request.headers',provenance: 'header' },
349
+ { kind: 'source', id: 'py-flask-data', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'data' }, label: 'flask.request.data', provenance: 'http-body' },
350
+ { kind: 'source', id: 'py-flask-files', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'files' }, label: 'flask.request.files', provenance: 'http-body' },
351
+ { kind: 'source', id: 'py-flask-stream', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'stream' }, label: 'flask.request.stream', provenance: 'http-body' },
352
+ // Django request object.
353
+ { kind: 'source', id: 'py-django-post', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'POST' }, label: 'django.request.POST', provenance: 'http-body' },
354
+ { kind: 'source', id: 'py-django-get', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'GET' }, label: 'django.request.GET', provenance: 'url-param' },
355
+ { kind: 'source', id: 'py-django-body', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'body' }, label: 'django.request.body', provenance: 'http-body' },
356
+ { kind: 'source', id: 'py-django-meta', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'META' }, label: 'django.request.META', provenance: 'header' },
357
+ { kind: 'source', id: 'py-django-files', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'FILES' }, label: 'django.request.FILES', provenance: 'http-body' },
358
+ { kind: 'source', id: 'py-django-headers', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'headers' }, label: 'django.request.headers', provenance: 'header' },
359
+ { kind: 'source', id: 'py-django-cookies', language: 'py', framework: 'django', match: { type: 'member', object: 'request', prop: 'COOKIES' }, label: 'django.request.COOKIES', provenance: 'cookie' },
360
+ // FastAPI / Starlette — Request object.
361
+ { kind: 'source', id: 'py-fastapi-query', language: 'py', framework: 'fastapi', match: { type: 'member', object: 'request', prop: 'query_params' }, label: 'fastapi.request.query_params', provenance: 'url-param' },
362
+ { kind: 'source', id: 'py-fastapi-path', language: 'py', framework: 'fastapi', match: { type: 'member', object: 'request', prop: 'path_params' }, label: 'fastapi.request.path_params', provenance: 'path-param' },
363
+ { kind: 'source', id: 'py-fastapi-headers', language: 'py', framework: 'fastapi', match: { type: 'member', object: 'request', prop: 'headers' }, label: 'fastapi.request.headers', provenance: 'header' },
364
+ { kind: 'source', id: 'py-fastapi-cookies', language: 'py', framework: 'fastapi', match: { type: 'member', object: 'request', prop: 'cookies' }, label: 'fastapi.request.cookies', provenance: 'cookie' },
365
+ // Tornado RequestHandler.
366
+ { kind: 'source', id: 'py-tornado-get-arg', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_argument' }, argIndex: 0, label: 'tornado.get_argument', provenance: 'http-body' },
367
+ { kind: 'source', id: 'py-tornado-get-args', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_arguments' }, argIndex: 0, label: 'tornado.get_arguments', provenance: 'http-body' },
368
+ { kind: 'source', id: 'py-tornado-get-body', language: 'py', framework: 'tornado', match: { type: 'call', callee: 'get_body_argument' }, argIndex: 0, label: 'tornado.get_body_argument', provenance: 'http-body' },
369
+ // sys.argv — CLI input source. (os.environ already declared above.)
370
+ { kind: 'source', id: 'py-sys-argv', language: 'py', framework: 'std', match: { type: 'member', object: 'sys', prop: 'argv' }, label: 'sys.argv', provenance: 'cli' },
371
+ // File reads.
372
+ { kind: 'source', id: 'py-open-read', language: 'py', framework: 'std', match: { type: 'call', callee: 'open' }, argIndex: 0, label: 'open()', provenance: 'file-read' },
373
+ // input() already declared above as a stdlib source (line ~120).
374
+
375
+ // ─── SINKS (Python) ────────────────────────────────────────────────────────
376
+ // SQL.
377
+ { kind: 'sink', id: 'py-cursor-execute-v2', language: 'py', framework: 'db', match: { type: 'call', callee: 'execute' }, argIndex: 0,
378
+ vuln: { name: 'SQL Injection (cursor.execute)', severity: 'critical', cwe: 'CWE-89',
379
+ remediation: 'Use parameterized queries: cursor.execute("SELECT * FROM t WHERE id = %s", (id,)). Never %-format or f-string the SQL with untrusted input.' } },
380
+ { kind: 'sink', id: 'py-cursor-executemany-v2', language: 'py', framework: 'db', match: { type: 'call', callee: 'executemany' }, argIndex: 0,
381
+ vuln: { name: 'SQL Injection (executemany)', severity: 'critical', cwe: 'CWE-89',
382
+ remediation: 'Use parameterized queries with executemany; never concatenate user input.' } },
383
+ { kind: 'sink', id: 'py-sqlalchemy-text', language: 'py', framework: 'sqlalchemy', match: { type: 'call', callee: 'text' }, argIndex: 0,
384
+ vuln: { name: 'SQL Injection (sqlalchemy.text)', severity: 'critical', cwe: 'CWE-89',
385
+ remediation: 'sqlalchemy.text() does not parameterize. Use bindparam() or Core expressions for any untrusted input.' } },
386
+ { kind: 'sink', id: 'py-django-raw', language: 'py', framework: 'django', match: { type: 'call', callee: 'raw' }, argIndex: 0,
387
+ vuln: { name: 'SQL Injection (Model.objects.raw)', severity: 'critical', cwe: 'CWE-89',
388
+ remediation: 'Use Django ORM Q-objects or parameterized raw(): Model.objects.raw("SELECT ... %s", [val]).' } },
389
+ // Command execution.
390
+ { kind: 'sink', id: 'py-os-system-v2', language: 'py', framework: 'std', match: { type: 'call', callee: 'system' }, argIndex: 0,
391
+ vuln: { name: 'Command Injection (os.system)', severity: 'critical', cwe: 'CWE-78',
392
+ remediation: 'Replace os.system with subprocess.run([...]) using an argv array; never feed untrusted strings to a shell.' } },
393
+ { kind: 'sink', id: 'py-os-popen', language: 'py', framework: 'std', match: { type: 'call', callee: 'popen' }, argIndex: 0,
394
+ vuln: { name: 'Command Injection (os.popen)', severity: 'critical', cwe: 'CWE-78',
395
+ remediation: 'os.popen is a shell wrapper; use subprocess.run with argv array.' } },
396
+ { kind: 'sink', id: 'py-subprocess-call', language: 'py', framework: 'std', match: { type: 'call', callee: 'call' }, argIndex: 0,
397
+ vuln: { name: 'Command Injection (subprocess.call)', severity: 'critical', cwe: 'CWE-78',
398
+ remediation: 'Pass argv as a list and ensure shell=False (the default). If shell=True is required, escape with shlex.quote.' } },
399
+ { kind: 'sink', id: 'py-subprocess-run-v2', language: 'py', framework: 'std', match: { type: 'call', callee: 'run' }, argIndex: 0,
400
+ vuln: { name: 'Command Injection (subprocess.run with shell=True)', severity: 'critical', cwe: 'CWE-78',
401
+ remediation: 'Pass argv as a list and ensure shell=False.' } },
402
+ { kind: 'sink', id: 'py-subprocess-Popen', language: 'py', framework: 'std', match: { type: 'call', callee: 'Popen' }, argIndex: 0,
403
+ vuln: { name: 'Command Injection (subprocess.Popen)', severity: 'critical', cwe: 'CWE-78',
404
+ remediation: 'Pass argv as a list and shell=False.' } },
405
+ { kind: 'sink', id: 'py-commands-getoutput', language: 'py', framework: 'std', match: { type: 'call', callee: 'getoutput' }, argIndex: 0,
406
+ vuln: { name: 'Command Injection (commands.getoutput)', severity: 'critical', cwe: 'CWE-78',
407
+ remediation: 'commands module is deprecated and shell-based; switch to subprocess with argv.' } },
408
+ // Code evaluation.
409
+ { kind: 'sink', id: 'py-eval', language: 'py', framework: 'std', match: { type: 'call', callee: 'eval' }, argIndex: 0,
410
+ vuln: { name: 'Code Injection (eval)', severity: 'critical', cwe: 'CWE-95',
411
+ remediation: 'Never eval user input. Use ast.literal_eval for trusted literal forms; reject otherwise.' } },
412
+ { kind: 'sink', id: 'py-exec', language: 'py', framework: 'std', match: { type: 'call', callee: 'exec' }, argIndex: 0,
413
+ vuln: { name: 'Code Injection (exec)', severity: 'critical', cwe: 'CWE-95',
414
+ remediation: 'Never exec user-controlled code.' } },
415
+ { kind: 'sink', id: 'py-compile', language: 'py', framework: 'std', match: { type: 'call', callee: 'compile' }, argIndex: 0,
416
+ vuln: { name: 'Code Injection (compile)', severity: 'high', cwe: 'CWE-95',
417
+ remediation: 'compile() followed by exec is equivalent to eval. Avoid on untrusted input.' } },
418
+ // Deserialization.
419
+ { kind: 'sink', id: 'py-pickle-loads-v2', language: 'py', framework: 'std', match: { type: 'call', callee: 'loads' }, argIndex: 0,
420
+ vuln: { name: 'Unsafe Deserialization (pickle.loads)', severity: 'critical', cwe: 'CWE-502',
421
+ remediation: 'pickle.loads on untrusted data is RCE. Use JSON / msgpack with explicit schema.' } },
422
+ { kind: 'sink', id: 'py-pickle-load', language: 'py', framework: 'std', match: { type: 'call', callee: 'load' }, argIndex: 0,
423
+ vuln: { name: 'Unsafe Deserialization (pickle.load)', severity: 'critical', cwe: 'CWE-502',
424
+ remediation: 'pickle.load on untrusted streams is RCE.' } },
425
+ { kind: 'sink', id: 'py-yaml-load-v2', language: 'py', framework: 'yaml', match: { type: 'call', callee: 'load' }, argIndex: 0,
426
+ vuln: { name: 'Unsafe Deserialization (yaml.load)', severity: 'high', cwe: 'CWE-502',
427
+ remediation: 'Use yaml.safe_load instead of yaml.load on untrusted YAML.' } },
428
+ // SSRF / HTTP-out.
429
+ { kind: 'sink', id: 'py-requests-get-v2', language: 'py', framework: 'requests', match: { type: 'call', callee: 'get' }, argIndex: 0,
430
+ vuln: { name: 'SSRF (requests.get)', severity: 'high', cwe: 'CWE-918',
431
+ remediation: 'Resolve the host first, reject 169.254.169.254 / RFC1918 / localhost; or proxy through a server-side allow-list.' } },
432
+ { kind: 'sink', id: 'py-requests-post-v2', language: 'py', framework: 'requests', match: { type: 'call', callee: 'post' }, argIndex: 0,
433
+ vuln: { name: 'SSRF (requests.post)', severity: 'high', cwe: 'CWE-918',
434
+ remediation: 'Resolve the host first and reject metadata-endpoint addresses.' } },
435
+ { kind: 'sink', id: 'py-urllib-urlopen', language: 'py', framework: 'std', match: { type: 'call', callee: 'urlopen' }, argIndex: 0,
436
+ vuln: { name: 'SSRF (urllib.urlopen)', severity: 'high', cwe: 'CWE-918',
437
+ remediation: 'Resolve and validate the URL host before opening.' } },
438
+ // File system sinks.
439
+ { kind: 'sink', id: 'py-send-file', language: 'py', framework: 'flask', match: { type: 'call', callee: 'send_file' }, argIndex: 0,
440
+ vuln: { name: 'Path Traversal (send_file)', severity: 'high', cwe: 'CWE-22',
441
+ remediation: 'Use flask.send_from_directory with a strict base dir, or canonicalize the path and assert it stays within the allowed root.' } },
442
+ { kind: 'sink', id: 'py-send-from-directory', language: 'py', framework: 'flask', match: { type: 'call', callee: 'send_from_directory' }, argIndex: 1,
443
+ vuln: { name: 'Path Traversal (send_from_directory)', severity: 'medium', cwe: 'CWE-22',
444
+ remediation: 'send_from_directory protects against trivial traversal but verify the filename argument has no ".." or absolute prefix.' } },
445
+ // Template injection.
446
+ { kind: 'sink', id: 'py-jinja2-from-string', language: 'py', framework: 'jinja2', match: { type: 'call', callee: 'from_string' }, argIndex: 0,
447
+ vuln: { name: 'Server-Side Template Injection (jinja2.Environment.from_string)', severity: 'critical', cwe: 'CWE-1336',
448
+ remediation: 'Never compile a template from user input. If user-supplied substitution is required, use a strict allow-listed sandboxed environment.' } },
449
+ // Crypto / hash sinks (weak hash + plaintext compare are covered elsewhere).
450
+ // XML — XXE.
451
+ { kind: 'sink', id: 'py-etree-fromstring', language: 'py', framework: 'xml', match: { type: 'call', callee: 'fromstring' }, argIndex: 0,
452
+ vuln: { name: 'XXE (xml.etree.fromstring)', severity: 'high', cwe: 'CWE-611',
453
+ remediation: 'Use defusedxml.ElementTree.fromstring instead.' } },
454
+ // Redirects.
455
+ { kind: 'sink', id: 'py-flask-redirect', language: 'py', framework: 'flask', match: { type: 'call', callee: 'redirect' }, argIndex: 0,
456
+ vuln: { name: 'Open Redirect (flask.redirect)', severity: 'medium', cwe: 'CWE-601',
457
+ remediation: 'Validate redirect target against an allow-list; never pass req-derived strings straight to redirect.' } },
458
+
459
+ // ─── SANITIZERS (Python) ───────────────────────────────────────────────────
460
+ { kind: 'sanitizer', id: 'py-shlex-quote-v2', language: 'py', match: { type: 'call', callee: 'quote' }, effect: 'strip', appliesTo: ['cmd'] },
461
+ { kind: 'sanitizer', id: 'py-html-escape-v2', language: 'py', match: { type: 'call', callee: 'escape' }, effect: 'strip', appliesTo: ['xss','url'] },
462
+ { kind: 'sanitizer', id: 'py-markupsafe-escape-v2', language: 'py', match: { type: 'call', callee: 'Markup' }, effect: 'strip', appliesTo: ['xss'] },
463
+ { kind: 'sanitizer', id: 'py-bleach-clean-v2', language: 'py', match: { type: 'call', callee: 'clean' }, effect: 'strip', appliesTo: ['xss'] },
464
+ { kind: 'sanitizer', id: 'py-urllib-quote', language: 'py', match: { type: 'call', callee: 'quote_plus' }, effect: 'strip', appliesTo: ['url'] },
465
+ { kind: 'sanitizer', id: 'py-int-v2', language: 'py', match: { type: 'call', callee: 'int' }, effect: 'strip', appliesTo: ['*'] },
466
+ { kind: 'sanitizer', id: 'py-float-v2', language: 'py', match: { type: 'call', callee: 'float' }, effect: 'strip', appliesTo: ['*'] },
467
+ { kind: 'sanitizer', id: 'py-ast-literal-eval', language: 'py', match: { type: 'call', callee: 'literal_eval' }, effect: 'strip', appliesTo: ['*'] },
468
+ { kind: 'sanitizer', id: 'py-yaml-safe-load', language: 'py', match: { type: 'call', callee: 'safe_load' }, effect: 'strip', appliesTo: ['deserial'] },
469
+ { kind: 'sanitizer', id: 'py-pathlib-resolve', language: 'py', match: { type: 'call', callee: 'resolve' }, effect: 'taintIf-not-pinned', appliesTo: ['path'] },
470
+ { kind: 'sanitizer', id: 'py-defusedxml', language: 'py', match: { type: 'call', callee: 'fromstring' }, effect: 'strip', appliesTo: ['xxe'] }, // when called from defusedxml namespace
471
+
472
+ // ─── SOURCES (C# — ASP.NET MVC / Core) ───────────────────────────────────
473
+ { kind: 'source', id: 'cs-request-form', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'Form' }, label: 'Request.Form', provenance: 'http-body' },
474
+ { kind: 'source', id: 'cs-request-query', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'QueryString' }, label: 'Request.QueryString', provenance: 'url-param' },
475
+ { kind: 'source', id: 'cs-request-cookies', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'Cookies' }, label: 'Request.Cookies', provenance: 'cookie' },
476
+ { kind: 'source', id: 'cs-request-headers', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'Headers' }, label: 'Request.Headers', provenance: 'header' },
477
+ { kind: 'source', id: 'cs-request-params', language: 'cs', framework: 'aspnet', match: { type: 'member', object: 'Request', prop: 'Params' }, label: 'Request.Params' },
478
+ { kind: 'source', id: 'cs-request-body', language: 'cs', framework: 'aspnet-core', match: { type: 'member', object: 'Request', prop: 'Body' }, label: 'Request.Body', provenance: 'http-body' },
479
+ { kind: 'source', id: 'cs-env-var', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetEnvironmentVariable' }, label: 'Environment.GetEnvironmentVariable', provenance: 'env' },
480
+
481
+ // ─── SINKS (C#) ──────────────────────────────────────────────────────────
482
+ { kind: 'sink', id: 'cs-sqlcommand', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'SqlCommand' }, argIndex: 0,
483
+ vuln: { name: 'SQL Injection (new SqlCommand with concatenated user input)', severity: 'critical', cwe: 'CWE-89',
484
+ remediation: 'Use parameterized SqlCommand: `new SqlCommand("SELECT * FROM u WHERE id=@id"); cmd.Parameters.AddWithValue("@id", id);`' } },
485
+ { kind: 'sink', id: 'cs-executequery', language: 'cs', framework: 'ado', match: { type: 'call', callee: 'ExecuteQuery' }, argIndex: 0,
486
+ vuln: { name: 'SQL Injection (DataContext.ExecuteQuery string-form)', severity: 'critical', cwe: 'CWE-89',
487
+ remediation: 'Use parameterized form or LINQ Where clauses.' } },
488
+ { kind: 'sink', id: 'cs-dapper-query', language: 'cs', framework: 'dapper', match: { type: 'call', callee: 'Query' }, argIndex: 0,
489
+ vuln: { name: 'SQL Injection (Dapper Query with string concat)', severity: 'critical', cwe: 'CWE-89',
490
+ remediation: 'Pass parameters as the 2nd arg: `Query<T>("SELECT … WHERE id=@id", new { id })`.' } },
491
+ { kind: 'sink', id: 'cs-process-start', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Start' }, argIndex: 0,
492
+ vuln: { name: 'Command Injection (Process.Start string-form)', severity: 'critical', cwe: 'CWE-78',
493
+ remediation: 'Use ProcessStartInfo with separated FileName + Arguments; never pass /c with concat.' } },
494
+ { kind: 'sink', id: 'cs-file-readall', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'ReadAllText' }, argIndex: 0,
495
+ vuln: { name: 'Path Traversal (File.ReadAllText with user input)', severity: 'high', cwe: 'CWE-22',
496
+ remediation: 'Canonicalize the path with Path.GetFullPath and verify it starts with an allow-listed base directory.' } },
497
+ { kind: 'sink', id: 'cs-file-writeall', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'WriteAllText' }, argIndex: 0,
498
+ vuln: { name: 'Path Traversal (File.WriteAllText with user input)', severity: 'high', cwe: 'CWE-22',
499
+ remediation: 'Canonicalize the path and verify it stays within the allowed base.' } },
500
+ { kind: 'sink', id: 'cs-webclient', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'DownloadString' }, argIndex: 0,
501
+ vuln: { name: 'SSRF (WebClient.DownloadString)', severity: 'high', cwe: 'CWE-918',
502
+ remediation: 'Validate the URL host against an allow-list before fetching.' } },
503
+ { kind: 'sink', id: 'cs-httpclient-getstr', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'GetStringAsync' }, argIndex: 0,
504
+ vuln: { name: 'SSRF (HttpClient.GetStringAsync)', severity: 'high', cwe: 'CWE-918',
505
+ remediation: 'Validate the URL before fetching.' } },
506
+ { kind: 'sink', id: 'cs-binformatter', language: 'cs', framework: 'stdlib', match: { type: 'call', callee: 'Deserialize' }, argIndex: 0,
507
+ vuln: { name: 'Insecure Deserialization (BinaryFormatter.Deserialize)', severity: 'critical', cwe: 'CWE-502',
508
+ remediation: 'BinaryFormatter is deprecated and unsafe. Use System.Text.Json with explicit type constraints.' } },
509
+
510
+ // ─── SANITIZERS (C#) ─────────────────────────────────────────────────────
511
+ { kind: 'sanitizer', id: 'cs-html-encode', language: 'cs', match: { type: 'call', callee: 'HtmlEncode' }, effect: 'strip', appliesTo: ['xss'] },
512
+ { kind: 'sanitizer', id: 'cs-url-encode', language: 'cs', match: { type: 'call', callee: 'UrlEncode' }, effect: 'strip', appliesTo: ['url'] },
513
+ { kind: 'sanitizer', id: 'cs-path-getfullpath',language: 'cs', match: { type: 'call', callee: 'GetFullPath' }, effect: 'taintIf-not-pinned', appliesTo: ['path'] },
514
+ { kind: 'sanitizer', id: 'cs-int-parse', language: 'cs', match: { type: 'call', callee: 'Parse' }, effect: 'strip', appliesTo: ['*'] },
515
+ { kind: 'sanitizer', id: 'cs-int-tryparse', language: 'cs', match: { type: 'call', callee: 'TryParse' }, effect: 'strip', appliesTo: ['*'] },
516
+ { kind: 'sanitizer', id: 'cs-regex-escape', language: 'cs', match: { type: 'call', callee: 'Escape' }, effect: 'strip', appliesTo: ['regex'] },
517
+ { kind: 'sanitizer', id: 'cs-addwithvalue', language: 'cs', match: { type: 'call', callee: 'AddWithValue' }, effect: 'strip', appliesTo: ['sql'] },
518
+
519
+ // ─── SOURCES (Kotlin — Spring / Ktor) ────────────────────────────────────
520
+ { kind: 'source', id: 'kt-request-param', language: 'kt', framework: 'spring', match: { type: 'call', callee: 'getParameter' }, label: 'request.getParameter (Kotlin Spring)' },
521
+ { kind: 'source', id: 'kt-request-header', language: 'kt', framework: 'spring', match: { type: 'call', callee: 'getHeader' }, label: 'request.getHeader' },
522
+ { kind: 'source', id: 'kt-ktor-receive', language: 'kt', framework: 'ktor', match: { type: 'call', callee: 'receive' }, label: 'call.receive() (Ktor)', provenance: 'http-body' },
523
+ { kind: 'source', id: 'kt-ktor-parameters', language: 'kt', framework: 'ktor', match: { type: 'member', object: 'call', prop: 'parameters' }, label: 'call.parameters (Ktor)' },
524
+ { kind: 'source', id: 'kt-env-var', language: 'kt', framework: 'stdlib', match: { type: 'call', callee: 'getenv' }, label: 'System.getenv (Kotlin)', provenance: 'env' },
525
+
526
+ // ─── SINKS (Kotlin) ──────────────────────────────────────────────────────
527
+ { kind: 'sink', id: 'kt-jdbc-execute', language: 'kt', framework: 'jdbc', match: { type: 'call', callee: 'executeQuery' }, argIndex: 0,
528
+ vuln: { name: 'SQL Injection (JDBC executeQuery from Kotlin)', severity: 'critical', cwe: 'CWE-89',
529
+ remediation: 'Use PreparedStatement + setX(N, v) — Kotlin string templates concatenated into SQL are still injection.' } },
530
+ { kind: 'sink', id: 'kt-exposed-exec', language: 'kt', framework: 'exposed', match: { type: 'call', callee: 'exec' }, argIndex: 0,
531
+ vuln: { name: 'SQL Injection (Exposed.exec with raw string)', severity: 'critical', cwe: 'CWE-89',
532
+ remediation: 'Use Exposed DSL queries or named-parameter exec with a typed parameter list.' } },
533
+ { kind: 'sink', id: 'kt-runtime-exec', language: 'kt', framework: 'stdlib', match: { type: 'call', callee: 'exec' }, argIndex: 0,
534
+ vuln: { name: 'Command Injection (Runtime.exec / ProcessBuilder string-form, Kotlin)', severity: 'critical', cwe: 'CWE-78',
535
+ remediation: 'Use ProcessBuilder(listOf("cmd", "arg")) — never pass a single string to exec.' } },
536
+ { kind: 'sink', id: 'kt-file-readtext', language: 'kt', framework: 'stdlib', match: { type: 'call', callee: 'readText' }, argIndex: 0,
537
+ vuln: { name: 'Path Traversal (File(name).readText)', severity: 'high', cwe: 'CWE-22',
538
+ remediation: 'Canonicalize: `File(name).canonicalFile` and verify path stays inside an allow-listed base.' } },
539
+ { kind: 'sink', id: 'kt-url-readtext', language: 'kt', framework: 'stdlib', match: { type: 'call', callee: 'readText' }, argIndex: 'all',
540
+ vuln: { name: 'SSRF (URL(...).readText with user URL)', severity: 'high', cwe: 'CWE-918',
541
+ remediation: 'Validate the URL host against an allow-list before reading.' } },
542
+ { kind: 'sink', id: 'kt-objectinputstream', language: 'kt', framework: 'stdlib', match: { type: 'call', callee: 'readObject' }, argIndex: 'all',
543
+ vuln: { name: 'Insecure Deserialization (ObjectInputStream.readObject, Kotlin)', severity: 'critical', cwe: 'CWE-502',
544
+ remediation: 'Use kotlinx.serialization with explicit class allow-list.' } },
545
+
546
+ // ─── SANITIZERS (Kotlin) ─────────────────────────────────────────────────
547
+ { kind: 'sanitizer', id: 'kt-html-escape', language: 'kt', match: { type: 'call', callee: 'escapeHtml4' }, effect: 'strip', appliesTo: ['xss'] },
548
+ { kind: 'sanitizer', id: 'kt-url-encode', language: 'kt', match: { type: 'call', callee: 'URLEncoder' }, effect: 'strip', appliesTo: ['url'] },
549
+ { kind: 'sanitizer', id: 'kt-int-toint', language: 'kt', match: { type: 'call', callee: 'toInt' }, effect: 'strip', appliesTo: ['*'] },
550
+ { kind: 'sanitizer', id: 'kt-int-toIntOrNull',language: 'kt', match: { type: 'call', callee: 'toIntOrNull' }, effect: 'strip', appliesTo: ['*'] },
551
+ { kind: 'sanitizer', id: 'kt-path-canonical',language: 'kt', match: { type: 'call', callee: 'canonicalFile' },effect: 'taintIf-not-pinned', appliesTo: ['path'] },
552
+ { kind: 'sanitizer', id: 'kt-jdbc-setstring',language: 'kt', match: { type: 'call', callee: 'setString' }, effect: 'strip', appliesTo: ['sql'] },
553
+ ];
554
+
555
+ // ─── Expanded sanitizer catalog (v0.65.0) ────────────────────────────────
556
+ // ~450 additional entries across JS / Python / Java / Ruby / PHP / Go.
557
+ // Lives in catalog-expanded.js to keep the diff reviewable. Merged into
558
+ // the main CATALOG below so the indexer treats them identically.
559
+ import { EXPANDED_SANITIZERS } from './catalog-expanded.js';
560
+ // Merge the expanded sanitizer catalog. We dedupe on `id` (case-insensitive)
561
+ // so a base-catalog entry always wins over a same-id expanded one — the base
562
+ // catalog is the curated/blessed surface; the expansion is additive coverage.
563
+ {
564
+ const _ids = new Set();
565
+ for (const e of CATALOG) if (e && e.id) _ids.add(String(e.id).toLowerCase());
566
+ for (const e of EXPANDED_SANITIZERS) {
567
+ if (!e || !e.id) continue;
568
+ const k = String(e.id).toLowerCase();
569
+ if (_ids.has(k)) continue; // base catalog wins on id collision
570
+ _ids.add(k);
571
+ CATALOG.push(e);
572
+ }
573
+ }
574
+
575
+ // Provenance defaults (Sentinel-parity audit P1-10):
576
+ //
577
+ // Every catalog entry is implicitly `source: 'official'` (curated by this
578
+ // repo's maintainers, drawn from upstream framework docs). Future community
579
+ // contributions or LLM-inferred entries will carry `source: 'community'` or
580
+ // `source: 'inferred'`. Operators who want to opt OUT of non-official
581
+ // entries set `AGENTIC_SECURITY_CATALOG_OFFICIAL_ONLY=1`.
582
+ //
583
+ // We default-stamp `source: 'official'` on entries that don't have one so
584
+ // existing callers keep working.
585
+ for (const e of CATALOG) {
586
+ if (!e.source) e.source = 'official';
587
+ }
588
+
589
+ // Premortem 3R-10: OFFICIAL_ONLY was captured ONCE at module load, baked
590
+ // into the prebuilt indexes. A caller that sets the env var just before
591
+ // running a scan (e.g., a CI lane that wants strict-mode just for this one
592
+ // invocation) was silently ignored. Now the indexes hold ALL entries; the
593
+ // filter runs per-match by reading the env each call.
594
+ const CALLEE_INDEX = new Map();
595
+ const MEMBER_INDEX = new Map();
596
+ for (const e of CATALOG) {
597
+ if (!e.match) continue;
598
+ if (e.match.type === 'call' && e.match.callee && e.match.callee !== '*') {
599
+ const k = e.match.callee;
600
+ if (!CALLEE_INDEX.has(k)) CALLEE_INDEX.set(k, []);
601
+ CALLEE_INDEX.get(k).push(e);
602
+ } else if (e.match.type === 'member' && e.match.object && e.match.prop) {
603
+ const k = `${e.match.object}.${e.match.prop}`;
604
+ if (!MEMBER_INDEX.has(k)) MEMBER_INDEX.set(k, []);
605
+ MEMBER_INDEX.get(k).push(e);
606
+ }
607
+ }
608
+
609
+ function isOfficialOnlyMode() {
610
+ return process.env.AGENTIC_SECURITY_CATALOG_OFFICIAL_ONLY === '1';
611
+ }
612
+
613
+ // Premortem 4R-4: the per-match `filter()` allocated a fresh array on every
614
+ // taint-engine lookup. On a 100-file Java codebase this was millions of
615
+ // allocations. Memoize by entries-identity; bump a generation counter when
616
+ // the env mode changes so a mid-process toggle invalidates cleanly.
617
+ let _modeGeneration = 0;
618
+ let _lastMode = null;
619
+ const _filterCache = new WeakMap();
620
+ function filterByProvenance(entries) {
621
+ const mode = isOfficialOnlyMode();
622
+ if (!mode) return entries;
623
+ if (mode !== _lastMode) {
624
+ _modeGeneration++;
625
+ _lastMode = mode;
626
+ }
627
+ const cached = _filterCache.get(entries);
628
+ if (cached && cached.gen === _modeGeneration) return cached.list;
629
+ const list = entries.filter(e => e.source === 'official');
630
+ _filterCache.set(entries, { gen: _modeGeneration, list });
631
+ return list;
632
+ }
633
+
634
+ export function matchSource(memberExpr) {
635
+ // memberExpr is exprDesc: { kind: 'member', object: {kind:'ident',name}, prop }
636
+ if (!memberExpr || memberExpr.kind !== 'member') return null;
637
+ if (memberExpr.object?.kind !== 'ident') return null;
638
+ const k = `${memberExpr.object.name}.${memberExpr.prop}`;
639
+ const raw = MEMBER_INDEX.get(k);
640
+ if (!raw) return null;
641
+ const hits = filterByProvenance(raw);
642
+ if (!hits.length) return null;
643
+ return hits.find(h => h.kind === 'source') || null;
644
+ }
645
+
646
+ export function matchSinkOrSanitizer(calleeExpr) {
647
+ if (!calleeExpr) return null;
648
+ let calleeName = null;
649
+ if (calleeExpr.kind === 'ident') calleeName = calleeExpr.name;
650
+ else if (calleeExpr.kind === 'member') calleeName = calleeExpr.prop;
651
+ if (!calleeName) return null;
652
+ const raw = CALLEE_INDEX.get(calleeName);
653
+ if (!raw) return null;
654
+ const hits = filterByProvenance(raw);
655
+ return hits.length ? hits : null;
656
+ }
657
+
658
+ // For tests and reflection.
659
+ export function _catalogSize() { return CATALOG.length; }