@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,234 @@
1
+ // String-value abstract domain (P4.4).
2
+ //
3
+ // The taint engine treats every string as opaque — "tainted" or "clean."
4
+ // Real codebases have lots of strings that are KNOWN at compile time:
5
+ //
6
+ // const url = "https://internal.example.com/health";
7
+ // await fetch(url);
8
+ //
9
+ // `url` is constant. SSRF is *impossible*. The current engine doesn't fire
10
+ // (because the literal isn't tainted), but it ALSO doesn't actively prove
11
+ // safety — and when a project mixes constants with user-influenced fragments,
12
+ // the engine over-approximates conservatively.
13
+ //
14
+ // This module models strings with a three-element lattice:
15
+ //
16
+ // Const(literal) "https://internal.example.com"
17
+ // Concat(parts) "https://" + host + "/" + path
18
+ // Unknown anything we can't statically analyze
19
+ //
20
+ // And provides:
21
+ // - abstract(expr) → returns the abstract value for an IR expression
22
+ // - isSafeUrl(absVal, allowedHosts) → bool, prove the URL is safe
23
+ // - join(a, b) → lattice meet (used at branch joins)
24
+ //
25
+ // v1: enough to handle the common SSRF / open-redirect "constant URL" case.
26
+ // v2 would add prefix/suffix analysis, regex membership, etc.
27
+
28
+ export const TOP = { kind: 'Unknown' };
29
+ export const BOTTOM = { kind: 'Const', value: '' }; // empty string = bottom of useful domain
30
+
31
+ export function makeConst(value) {
32
+ if (typeof value !== 'string') return TOP;
33
+ return { kind: 'Const', value };
34
+ }
35
+
36
+ /**
37
+ * v0.69 #4a — regex-constrained string value.
38
+ *
39
+ * Represents a string whose concrete value is unknown but whose CHARSET +
40
+ * SHAPE are bounded to a regex. Sanitizers produce these:
41
+ * encodeURIComponent(x) → Regex(/^[A-Za-z0-9-_.!~*'()%]*$/)
42
+ * parseInt(x).toString() → Regex(/^-?\d+$/)
43
+ * bcrypt.hash(x) → Regex(/^\$2[aby]?\$\d{1,2}\$[./A-Za-z0-9]{53}$/)
44
+ *
45
+ * The pattern MUST be anchored with ^ and $ to be sound.
46
+ */
47
+ export function makeRegex(pattern) {
48
+ if (!(pattern instanceof RegExp)) return TOP;
49
+ const src = pattern.source;
50
+ if (!src.startsWith('^') || !src.endsWith('$')) return TOP;
51
+ return { kind: 'Regex', pattern };
52
+ }
53
+
54
+ export function makeConcat(parts) {
55
+ // Optimize: if every part is Const, collapse to a single Const.
56
+ if (parts.every(p => p && p.kind === 'Const')) {
57
+ return makeConst(parts.map(p => p.value).join(''));
58
+ }
59
+ // If any part is Unknown, the whole concat is Unknown.
60
+ if (parts.some(p => !p || p.kind === 'Unknown')) return TOP;
61
+ return { kind: 'Concat', parts };
62
+ }
63
+
64
+ /**
65
+ * Lattice join: a ⊔ b. Returns the least-upper-bound.
66
+ *
67
+ * Const(s) ⊔ Const(s) = Const(s)
68
+ * Const(s) ⊔ Const(t) = Regex(escape(s)|escape(t)) if both anchor-friendly
69
+ * Regex(p) ⊔ Const(s) where s matches p = Regex(p)
70
+ * Regex(p1) ⊔ Regex(p2) = Regex(p1) if patterns identical, else TOP
71
+ * anything ⊔ Unknown = Unknown
72
+ */
73
+ export function join(a, b) {
74
+ if (!a) return b;
75
+ if (!b) return a;
76
+ if (a.kind === 'Unknown' || b.kind === 'Unknown') return TOP;
77
+ if (a.kind === 'Const' && b.kind === 'Const') {
78
+ if (a.value === b.value) return a;
79
+ return TOP; // distinct constants from two branches — be conservative
80
+ }
81
+ if (a.kind === 'Regex' && b.kind === 'Regex') {
82
+ return a.pattern.source === b.pattern.source ? a : TOP;
83
+ }
84
+ if (a.kind === 'Regex' && b.kind === 'Const') {
85
+ return a.pattern.test(b.value) ? a : TOP;
86
+ }
87
+ if (b.kind === 'Regex' && a.kind === 'Const') {
88
+ return b.pattern.test(a.value) ? b : TOP;
89
+ }
90
+ return TOP;
91
+ }
92
+
93
+ /**
94
+ * Abstract an IR expression into a string-value abstract value. The engine
95
+ * walks expressions during evaluation; this helper gives us the
96
+ * `Const | Concat | Unknown` summary alongside the boolean taint check.
97
+ */
98
+ export function abstract(expr) {
99
+ if (!expr) return TOP;
100
+ switch (expr.kind) {
101
+ case 'literal':
102
+ if (typeof expr.value === 'string') return makeConst(expr.value);
103
+ return TOP;
104
+ case 'tpl':
105
+ if (Array.isArray(expr.parts)) return makeConcat(expr.parts.map(abstract));
106
+ return TOP;
107
+ case 'binary': {
108
+ if (expr.op === '+' || expr.op === '+=') {
109
+ return makeConcat([abstract(expr.left), abstract(expr.right)]);
110
+ }
111
+ return TOP;
112
+ }
113
+ case 'call': {
114
+ // v0.69 #4a — sanitizer-call output is regex-constrained.
115
+ const tail = String(expr.callee || '').split('.').pop();
116
+ const r = SANITIZER_OUTPUT_REGEX[tail];
117
+ if (r) return makeRegex(r);
118
+ return TOP;
119
+ }
120
+ default:
121
+ return TOP;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Catalog of known sanitizer output regexes. The output of these calls is
127
+ * provably bounded to the listed charset. This is what powers the
128
+ * `provenClean` flag for non-SQL injection classes.
129
+ *
130
+ * Patterns are conservative — only listed when the spec REQUIRES the
131
+ * output to fit the regex. Empty / null returns are part of the domain.
132
+ */
133
+ const SANITIZER_OUTPUT_REGEX = {
134
+ // URL-safe encoding (RFC 3986 reserved + unreserved with %xx escapes).
135
+ encodeURIComponent: /^[A-Za-z0-9\-_.!~*'()%]*$/,
136
+ encodeURI: /^[A-Za-z0-9\-_.!~*'();/?:@&=+$,#%]*$/,
137
+ // Numeric-coerced.
138
+ parseInt: /^-?\d+$/,
139
+ parseFloat: /^-?\d+(?:\.\d+)?$/,
140
+ // bcrypt / scrypt output format.
141
+ hashSync: /^\$2[aby]?\$\d{1,2}\$[.\/A-Za-z0-9]{53}$/,
142
+ // Hex digest from crypto.
143
+ digest: /^[0-9a-f]+$/,
144
+ toString: /^[A-Za-z0-9+/=]*$/, // when called on a Buffer with 'base64' — over-approximate, narrowed by argIndex in v2
145
+ // Java URLEncoder.encode — RFC 3986 + spaces as '+'.
146
+ // (We can't distinguish overloads from regex name alone; conservative listing.)
147
+ // PHP htmlspecialchars / htmlentities — HTML-entity escape.
148
+ htmlspecialchars: /^[^<>&"']*(?:&(?:lt|gt|amp|quot|#039);)*[^<>&"']*$/,
149
+ };
150
+
151
+ /**
152
+ * SAFE-CHARSET PROOF — does the abstract value provably fit the given regex?
153
+ *
154
+ * Used by sanitizer-proof.js to verify that a sanitizer's output cannot
155
+ * contain the metacharacters of a target injection family (no `'` for SQL,
156
+ * no `<` for XSS, no `\r\n` for response-splitting, etc.).
157
+ *
158
+ * Returns true iff EVERY concrete string in the abstract value's denotation
159
+ * matches `safe`.
160
+ */
161
+ export function provablyMatches(absVal, safe) {
162
+ if (!absVal || !(safe instanceof RegExp)) return false;
163
+ if (absVal.kind === 'Const') return safe.test(absVal.value);
164
+ if (absVal.kind === 'Regex') {
165
+ // Sound approximation: same source string → provable. Otherwise we'd
166
+ // need regex-subset, which is undecidable in general; v2 could do
167
+ // structural checks for common cases.
168
+ return absVal.pattern.source === safe.source;
169
+ }
170
+ if (absVal.kind === 'Concat') {
171
+ // A concat is provably safe iff every part is provably safe AND the
172
+ // safe regex permits arbitrary repetition (i.e. is of the form ^X*$).
173
+ if (!/^\^.+\*\$$/.test(safe.source)) return false;
174
+ return absVal.parts.every(p => provablyMatches(p, safe));
175
+ }
176
+ return false;
177
+ }
178
+
179
+ /**
180
+ * Render an abstract string back to a textual form for diagnostics.
181
+ *
182
+ * Const("hello") → "hello"
183
+ * Concat([Const("a"), Unknown, Const("b")]) → "a${...}b"
184
+ * Unknown → "${...}"
185
+ */
186
+ export function render(absVal) {
187
+ if (!absVal || absVal.kind === 'Unknown') return '${...}';
188
+ if (absVal.kind === 'Const') return absVal.value;
189
+ if (absVal.kind === 'Concat') {
190
+ return absVal.parts.map(p => render(p)).join('');
191
+ }
192
+ if (absVal.kind === 'Regex') return `${absVal.pattern.source}`;
193
+ return '${...}';
194
+ }
195
+
196
+ /**
197
+ * SSRF guard: given an abstract URL value and a list of trusted hosts,
198
+ * return true iff the URL is provably to one of the trusted hosts.
199
+ *
200
+ * isProvablyToHost(makeConst("https://internal.example.com/x"), ["internal.example.com"]) → true
201
+ * isProvablyToHost(TOP, [...]) → false (can't prove anything about Unknown)
202
+ * isProvablyToHost(Concat(["https://" + Unknown]), [...]) → false
203
+ */
204
+ export function isProvablyToHost(absVal, allowedHosts) {
205
+ if (!absVal || absVal.kind !== 'Const') return false;
206
+ if (!Array.isArray(allowedHosts) || !allowedHosts.length) return false;
207
+ let url;
208
+ try {
209
+ url = new URL(absVal.value);
210
+ } catch { return false; }
211
+ return allowedHosts.includes(url.host);
212
+ }
213
+
214
+ /**
215
+ * Open-redirect safe? An abstract URL value passed to res.redirect is safe
216
+ * when it's either provably to an allowed host OR a relative path with no
217
+ * scheme/host parts.
218
+ */
219
+ export function isSafeRedirectTarget(absVal, allowedHosts) {
220
+ if (!absVal) return false;
221
+ if (absVal.kind === 'Const') {
222
+ // Relative path starting with / and not //
223
+ if (/^\/(?!\/)/.test(absVal.value)) return true;
224
+ return isProvablyToHost(absVal, allowedHosts);
225
+ }
226
+ return false;
227
+ }
228
+
229
+ /**
230
+ * Hash an abstract value for cache-key purposes.
231
+ */
232
+ export function hashAbstract(absVal) {
233
+ return render(absVal);
234
+ }
@@ -0,0 +1,100 @@
1
+ // Type-stub-aware taint filter (v0.73).
2
+ //
3
+ // v0.70 added scanner/src/ir/type-stubs.js but only wired it into the
4
+ // receiver-context lookup. This post-pass closes the loop: after the
5
+ // taint engine emits findings, we consult the stubs map to demote
6
+ // findings whose source/sink type pair is provably incompatible with
7
+ // the vulnerability class.
8
+ //
9
+ // Example: a source that returns `number` (per stub signature) flowing
10
+ // to an XSS sink is suppressable — a number coerced to string can only
11
+ // produce digits/decimal/sign, which can't form the metacharacters
12
+ // (<, >, ', ") required to break out of an HTML context.
13
+ //
14
+ // Rules per vuln family:
15
+ // XSS (CWE-79): source type ∈ {number, boolean, Date, RegExp} → demote
16
+ // SQL inj (CWE-89): source type ∈ {number, boolean, Date} → demote
17
+ // Cmd inj (CWE-78): source type ∈ {number, boolean} → demote
18
+ // Path trav (CWE-22): source type ∈ {number, boolean} → demote
19
+ //
20
+ // Demotion lowers severity by one tier and sets `_stubTypeDemoted: true`
21
+ // with a `_stubTypeReason`. We never DROP findings — the stub-aware
22
+ // reason is shown to the operator so they can override if the stub is
23
+ // wrong or out of date.
24
+
25
+ const FAMILY_SAFE_TYPES = {
26
+ 'CWE-79': new Set(['number', 'boolean', 'Date', 'RegExp', 'bigint']),
27
+ 'CWE-89': new Set(['number', 'boolean', 'Date', 'bigint']),
28
+ 'CWE-78': new Set(['number', 'boolean', 'bigint']),
29
+ 'CWE-22': new Set(['number', 'boolean']),
30
+ 'CWE-918': new Set(['number', 'boolean']),
31
+ };
32
+
33
+ /**
34
+ * Try to resolve the type of the finding's source from the type-stubs map.
35
+ * The lookup chain: (1) the finding's source.label or trace[0].sourceLabel
36
+ * is the catalog source id; (2) the stub signature for the source's
37
+ * underlying function. Returns the type string ('string', 'number', …)
38
+ * or null if unknown.
39
+ */
40
+ function _sourceTypeFromStubs(finding, stubs) {
41
+ if (!stubs || !stubs.signatures) return null;
42
+ const trace = Array.isArray(finding.trace) ? finding.trace : [];
43
+ const src = trace[0] || finding.source;
44
+ const label = src?.sourceLabel || src?.label || '';
45
+ // The label is shaped like 'req.body' / 'request.GET' / etc. The
46
+ // underlying function lookup uses the LAST identifier as a callable name.
47
+ const tail = String(label).split('.').pop();
48
+ if (!tail) return null;
49
+ const sig = stubs.signatures.get(tail);
50
+ if (!sig) return null;
51
+ return _normalizeType(sig.returnType);
52
+ }
53
+
54
+ function _normalizeType(t) {
55
+ if (!t) return null;
56
+ const trimmed = String(t).trim().toLowerCase();
57
+ if (trimmed === 'number' || trimmed === 'numeric' || /^int(8|16|32|64)?$/.test(trimmed)) return 'number';
58
+ if (trimmed === 'bigint') return 'bigint';
59
+ if (trimmed === 'boolean' || trimmed === 'bool') return 'boolean';
60
+ if (trimmed === 'date') return 'Date';
61
+ if (trimmed === 'regexp') return 'RegExp';
62
+ if (trimmed === 'string' || trimmed === 'str') return 'string';
63
+ if (trimmed.endsWith('[]') || trimmed.startsWith('array<')) return 'array';
64
+ return trimmed;
65
+ }
66
+
67
+ /**
68
+ * Post-pass entry. Mutates findings in place: adds `_stubTypeDemoted`,
69
+ * `_stubTypeReason`, downgrades `severity` by one tier when the source
70
+ * type is in the family-safe set for the finding's CWE.
71
+ *
72
+ * Returns the (mutated) findings array with `_stubFilterStats` non-
73
+ * enumerable sidecar.
74
+ */
75
+ export function applyStubAwareFilter(findings, stubs) {
76
+ if (!Array.isArray(findings) || findings.length === 0) return findings;
77
+ if (!stubs || !stubs.signatures) return findings;
78
+ let demoted = 0;
79
+ for (const f of findings) {
80
+ if (!f || f.parser !== 'IR-TAINT') continue;
81
+ const safeSet = FAMILY_SAFE_TYPES[f.cwe];
82
+ if (!safeSet) continue;
83
+ const sourceType = _sourceTypeFromStubs(f, stubs);
84
+ if (!sourceType) continue;
85
+ if (!safeSet.has(sourceType)) continue;
86
+ f._stubTypeDemoted = true;
87
+ f._stubTypeReason = `source type ${sourceType} cannot carry ${f.cwe} metacharacters`;
88
+ f._stubTypeOriginalSeverity = f.severity;
89
+ const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
90
+ if (downgrade[f.severity]) f.severity = downgrade[f.severity];
91
+ demoted++;
92
+ }
93
+ Object.defineProperty(findings, '_stubFilterStats', {
94
+ value: { demoted, totalConsidered: findings.length },
95
+ enumerable: false,
96
+ });
97
+ return findings;
98
+ }
99
+
100
+ export const _internal = { FAMILY_SAFE_TYPES, _sourceTypeFromStubs, _normalizeType };
@@ -0,0 +1,132 @@
1
+ // Function-summary cache for context-sensitive interprocedural taint.
2
+ //
3
+ // PRD §6.2: "k-CFA configurable per analysis." This module implements the
4
+ // k=1 monovariant version — each function gets ONE summary per distinct
5
+ // entry-taint-state, cached by hash. Higher k = exponential blowup we don't
6
+ // pay yet.
7
+ //
8
+ // A summary captures, given a set of tainted parameter names at function
9
+ // entry, what the function does:
10
+ // - which return value(s) are tainted
11
+ // - which call-site arguments get mutated to tainted (by-reference)
12
+ // - which global / module variables get tainted
13
+ // - which findings emit
14
+ //
15
+ // The taint engine (engine.js) consults the summary cache before re-analyzing
16
+ // a callee. Cache key = `${qid}::${sorted-taint-state}`. Cache hits are O(1).
17
+ //
18
+ // Limitations:
19
+ // - Field sensitivity is at the parameter granularity only (not arbitrary
20
+ // access paths). `f(obj)` with obj.foo tainted is treated the same as
21
+ // obj.bar tainted.
22
+ // - No higher-order tracking — callbacks passed as args aren't analyzed.
23
+ // - Recursion: when we'd recurse into a function already on the analysis
24
+ // stack, we return the bottom summary (no-taint) and rely on fixed-point
25
+ // iteration. With k=1 this converges in ≤2 iterations for typical code.
26
+
27
+ import * as crypto from 'node:crypto';
28
+ import { canonicalize as canonicalizeAccessSet } from './access-paths.js';
29
+ import { hashReceiverType } from './receiver-context.js';
30
+
31
+ function _hashState(taintedParams) {
32
+ if (!taintedParams || taintedParams.size === 0) return 'empty';
33
+ // P1.1: canonicalize the access-path lattice before hashing so equivalent
34
+ // states (e.g. {"x", "x.y"} and {"x"}) produce the same cache key.
35
+ const canon = canonicalizeAccessSet(taintedParams);
36
+ const sorted = [...canon].sort().join('|');
37
+ return crypto.createHash('sha256').update(sorted).digest('hex').slice(0, 12);
38
+ }
39
+
40
+ export class SummaryCache {
41
+ constructor() {
42
+ this._cache = new Map(); // qid::hash → summary
43
+ this._stack = new Set(); // qids currently being analyzed (recursion guard)
44
+ this._iter = 0;
45
+ this._maxIter = 5000;
46
+ }
47
+
48
+ _key(qid, taintedParams, receiverType) {
49
+ // P1.2: when a receiver type is provided, extend the cache key with
50
+ // its hash. Backward-compatible: no receiverType → same key as before.
51
+ const base = `${qid}::${_hashState(taintedParams)}`;
52
+ if (!receiverType) return base;
53
+ return `${base}::${hashReceiverType(receiverType)}`;
54
+ }
55
+
56
+ get(qid, taintedParams, receiverType) {
57
+ return this._cache.get(this._key(qid, taintedParams, receiverType));
58
+ }
59
+
60
+ set(qid, taintedParams, summary, receiverType) {
61
+ this._cache.set(this._key(qid, taintedParams, receiverType), summary);
62
+ }
63
+
64
+ has(qid, taintedParams, receiverType) {
65
+ return this._cache.has(this._key(qid, taintedParams, receiverType));
66
+ }
67
+
68
+ // Compute the summary for a function (or return cached). The `analyze`
69
+ // callback is the per-function walker that returns
70
+ // { returnTainted, mutatedParams: Set, taintedGlobals: Set, findings: [] }
71
+ compute(qid, taintedParams, analyze) {
72
+ const k = this._key(qid, taintedParams);
73
+ if (this._cache.has(k)) return this._cache.get(k);
74
+ if (this._stack.has(qid)) {
75
+ // Recursion — return bottom summary; fixed-point iter will refine.
76
+ return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _recursive: true };
77
+ }
78
+ if (++this._iter > this._maxIter) {
79
+ return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _budgetExceeded: true };
80
+ }
81
+ this._stack.add(qid);
82
+ try {
83
+ const summary = analyze(qid, taintedParams);
84
+ this._cache.set(k, summary);
85
+ return summary;
86
+ } finally {
87
+ this._stack.delete(qid);
88
+ }
89
+ }
90
+
91
+ // Helper: apply a summary to a caller's taint state given the call site's
92
+ // argument bindings. Returns { calleeReturnTainted, mutated: Set of caller-side
93
+ // var names that should become tainted because the callee mutated them }.
94
+ applyAtCallSite(summary, paramNames, callArgs, callerTaintedVars) {
95
+ if (!summary) return { returnTainted: false, mutated: new Set() };
96
+ const mutated = new Set();
97
+ if (summary.mutatedParams && summary.mutatedParams.size) {
98
+ // Map each mutated parameter position back to the caller-side argument name.
99
+ for (const paramName of summary.mutatedParams) {
100
+ const idx = paramNames.indexOf(paramName);
101
+ if (idx < 0) continue;
102
+ const arg = callArgs[idx];
103
+ if (arg && arg.kind === 'ident') mutated.add(arg.name);
104
+ }
105
+ }
106
+ return { returnTainted: !!summary.returnTainted, mutated };
107
+ }
108
+
109
+ size() { return this._cache.size; }
110
+ clear() { this._cache.clear(); this._iter = 0; }
111
+ }
112
+
113
+ // Build the entry-taint-state for a callee from a call site:
114
+ // given the callee's param names + the caller's tainted-var set + the
115
+ // call args, return a Set of param names that are tainted at entry.
116
+ export function entryStateFromCall(paramNames, callArgs, callerTaintedVars) {
117
+ const out = new Set();
118
+ if (!Array.isArray(paramNames) || !Array.isArray(callArgs)) return out;
119
+ for (let i = 0; i < paramNames.length && i < callArgs.length; i++) {
120
+ const arg = callArgs[i];
121
+ if (!arg) continue;
122
+ if (arg.kind === 'ident' && callerTaintedVars.has(arg.name)) {
123
+ out.add(paramNames[i]);
124
+ } else if (arg.kind === 'member' && arg.object?.kind === 'ident') {
125
+ const base = arg.object.name;
126
+ if (callerTaintedVars.has(base) || callerTaintedVars.has(`${base}.${arg.prop}`)) {
127
+ out.add(paramNames[i]);
128
+ }
129
+ }
130
+ }
131
+ return out;
132
+ }