@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,347 @@
1
+ // IFDS — Interprocedural Finite Distributive Subset (v0.71 #3).
2
+ //
3
+ // Reps, Horwitz, and Sagiv. "Precise interprocedural dataflow analysis
4
+ // via graph reachability." POPL 1995.
5
+ //
6
+ // The classical formal framework for context-sensitive, flow-sensitive
7
+ // interprocedural dataflow analysis. Polynomial in the size of the
8
+ // exploded supergraph (|nodes| × |facts|). Each fact is a single domain
9
+ // element (here: one access path or the special bottom fact `0̂`). The
10
+ // solver maintains path edges (`(d1, d2)` pairs at each statement) and
11
+ // summary edges (`(d1, d2)` pairs at each callee).
12
+ //
13
+ // What this v1 supports:
14
+ // - Intraprocedural fact propagation via per-node flow functions
15
+ // - Interprocedural call → start → return → after-call edges
16
+ // - Summary cache (per-function `Map<entryFact, Set<exitFact>>`)
17
+ // - Sink detection driven by the same catalog as the worklist engine
18
+ //
19
+ // What we DON'T do yet (left for v2):
20
+ // - The "may-must" extension (IDE)
21
+ // - Demand-driven querying (we still solve from every start node)
22
+ // - Field-sensitive facts (facts are full access paths today; for
23
+ // field-sensitivity we'd partition the fact set by path-prefix)
24
+ //
25
+ // Out-of-scope: we don't replace the existing k=2 worklist engine here.
26
+ // `runIfdsTaintEngine` is an ALTERNATIVE analyzer the operator opts
27
+ // into via AGENTIC_SECURITY_IFDS=1; the equivalence vs. the worklist
28
+ // engine is gated on the CVE-replay regression corpus.
29
+
30
+ import { matchSource, matchSinkOrSanitizer } from './catalog.js';
31
+
32
+ // Special bottom fact: "no taint yet, but reachable." Every node propagates
33
+ // 0̂ → 0̂ to mark reachability, then layers domain facts on top.
34
+ export const ZERO = '0';
35
+
36
+ // ─── Flow functions ──────────────────────────────────────────────────────
37
+ //
38
+ // Each function takes (node, fact) → Set<fact'>. The semantics are
39
+ // distributive: f(d1 ⊔ d2) = f(d1) ⊔ f(d2). For taint analysis where the
40
+ // domain is sets of access paths and the transfer is "propagate or
41
+ // generate," the distributivity holds.
42
+
43
+ function _flowAssign(node, fact) {
44
+ const out = new Set([fact]);
45
+ if (!node || node.kind !== 'assign') return out;
46
+ const target = typeof node.target === 'string' ? node.target : null;
47
+ if (!target) return out;
48
+ // Source generation: an assign whose source matches a catalog source
49
+ // generates a new fact (target is tainted from now on).
50
+ const src = node.source ? matchSource(node.source) : null;
51
+ if (src && fact === ZERO) {
52
+ out.add(target);
53
+ return out;
54
+ }
55
+ // Kill: assign to `target` from a clean expression kills `target`.
56
+ if (fact === target || (typeof fact === 'string' && fact.startsWith(target + '.'))) {
57
+ // Check if RHS reads `fact` (in which case we propagate, don't kill).
58
+ const sourcePath = _exprAccessPath(node.source);
59
+ if (sourcePath === fact) return out; // copy, fact survives
60
+ // Otherwise the LHS clobbers; remove the fact.
61
+ out.delete(fact);
62
+ }
63
+ // Propagation via copy: `x = y` and `fact == y` → out adds `x`.
64
+ if (typeof fact === 'string' && fact !== ZERO) {
65
+ const sourcePath = _exprAccessPath(node.source);
66
+ if (sourcePath === fact || (sourcePath && fact.startsWith(sourcePath + '.'))) {
67
+ out.add(target + (fact === sourcePath ? '' : fact.slice(sourcePath.length)));
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /**
74
+ * v0.73 — derive the callee's entry fact from a call site + caller fact.
75
+ *
76
+ * - If the caller's currentFact is ZERO (no taint), the callee's entry
77
+ * is ZERO too (reachability propagation).
78
+ * - If the caller has a tainted access path that matches one of the
79
+ * call's arguments, the corresponding callee parameter becomes the
80
+ * callee's entry fact.
81
+ * - Otherwise the callee starts clean (ZERO) — the caller's taint
82
+ * doesn't flow into this call.
83
+ */
84
+ function _entryFactForCall(callNode, currentFact, callee) {
85
+ if (currentFact === ZERO) return ZERO;
86
+ const args = callNode.args || [];
87
+ const params = callee.params || [];
88
+ for (let i = 0; i < args.length && i < params.length; i++) {
89
+ const argPath = _exprAccessPath(args[i]);
90
+ if (argPath === currentFact || (argPath && currentFact.startsWith(argPath + '.'))) {
91
+ // The caller's tainted path arrives via this argument; bind it to
92
+ // the parameter name in the callee's scope.
93
+ const tail = currentFact === argPath ? '' : currentFact.slice(argPath.length);
94
+ return params[i] + tail;
95
+ }
96
+ }
97
+ return ZERO;
98
+ }
99
+
100
+ /**
101
+ * v0.73 — when a callee's summary says it returns with exit fact F,
102
+ * translate that back into the caller's variable namespace. For now:
103
+ * - ZERO stays ZERO (reachability)
104
+ * - If the exit fact is a callee-local var (assign target inside the
105
+ * callee body) it doesn't escape the call — return ZERO.
106
+ * - If the exit fact corresponds to a parameter the caller bound to a
107
+ * specific arg, map it back to that arg's access path. This is the
108
+ * interprocedural mutation-through-param case.
109
+ * v1 is conservative: returns ZERO unless we can map back precisely.
110
+ */
111
+ function _mapReturnFact(callNode, exitFact, callerCurrent) {
112
+ if (exitFact === ZERO) return ZERO;
113
+ // For v1, we don't try to map callee param mutations back to caller
114
+ // args without alias info — that's covered by the worklist engine's
115
+ // _addPathAliasAware path. Here we conservatively propagate the
116
+ // caller's current fact (which is already correct if the callee was
117
+ // pure w.r.t. the caller's tainted vars).
118
+ return callerCurrent;
119
+ }
120
+
121
+ function _exprAccessPath(expr) {
122
+ if (!expr) return null;
123
+ if (expr.kind === 'ident') return expr.name;
124
+ if (expr.kind === 'member' && expr.object && expr.object.kind === 'ident' && typeof expr.prop === 'string') {
125
+ return `${expr.object.name}.${expr.prop}`;
126
+ }
127
+ return null;
128
+ }
129
+
130
+ // Sink detection at call nodes. Returns a list of sink-finding records
131
+ // {sinkId, vuln, severity, cwe, line} when `fact` reaches a sink arg.
132
+ function _detectSinkAtCall(node, fact) {
133
+ if (!node || node.kind !== 'call') return [];
134
+ if (fact === ZERO) return [];
135
+ const cat = matchSinkOrSanitizer(node.callee);
136
+ if (!cat) return [];
137
+ const findings = [];
138
+ const args = node.args || [];
139
+ for (const e of cat) {
140
+ if (e.kind !== 'sink') continue;
141
+ const argTaintedIdx = args.findIndex(a => {
142
+ const p = _exprAccessPath(a);
143
+ return p === fact || (p && fact.startsWith(p + '.'));
144
+ });
145
+ if (e.argIndex === 'all' || (typeof e.argIndex === 'number' && argTaintedIdx === e.argIndex)) {
146
+ findings.push({
147
+ sinkId: e.id,
148
+ vuln: e.vuln?.name || 'Tainted Sink',
149
+ severity: e.vuln?.severity || 'high',
150
+ cwe: e.vuln?.cwe || null,
151
+ remediation: e.vuln?.remediation || null,
152
+ line: node.line,
153
+ argIndex: argTaintedIdx,
154
+ callee: node.callee,
155
+ });
156
+ }
157
+ }
158
+ return findings;
159
+ }
160
+
161
+ // ─── Solver ──────────────────────────────────────────────────────────────
162
+
163
+ export class IFDSSolver {
164
+ constructor(perFileIR, callGraph, opts = {}) {
165
+ this.perFileIR = perFileIR;
166
+ this.callGraph = callGraph;
167
+ this.opts = opts;
168
+ // Path edges: for each node id, Set<"entryFact|currentFact">
169
+ this.pathEdges = new Map();
170
+ // Summary edges: per qid, Map<entryFact, Set<exitFact>>
171
+ // This is the v0.73 lift: real summaries replace the v0.71 bottom
172
+ // placeholder. When a callee has been solved under `entryFact` and
173
+ // we already know the resulting `exitFacts`, the caller jumps over
174
+ // the call site by applying those facts directly.
175
+ this.summaries = new Map();
176
+ // Pending-callsite registry: per (callee qid + entry fact), the list
177
+ // of return sites waiting on more summaries to materialize. When a
178
+ // new exit fact is added, we re-propagate at each return site.
179
+ this.pendingReturns = new Map();
180
+ // Findings: emitted whenever a sink call fires.
181
+ this.findings = [];
182
+ // Worklist: array of { fn, nodeId, entryFact, currentFact }
183
+ this.work = [];
184
+ // Budget — IFDS can blow up; cap path-edge count.
185
+ this.maxEdges = Number(opts.budgetFacts) || Number(process.env.AGENTIC_SECURITY_IFDS_BUDGET_FACTS) || 10_000;
186
+ this.edgeCount = 0;
187
+ }
188
+
189
+ _summaryKey(qid, entryFact) { return `${qid}|${entryFact}`; }
190
+
191
+ _addSummary(qid, entryFact, exitFact) {
192
+ const key = this._summaryKey(qid, entryFact);
193
+ if (!this.summaries.has(key)) this.summaries.set(key, new Set());
194
+ const set = this.summaries.get(key);
195
+ if (set.has(exitFact)) return false;
196
+ set.add(exitFact);
197
+ // Replay pending return sites: any caller that asked for this
198
+ // (callee, entryFact) gets the new exit fact propagated.
199
+ const pending = this.pendingReturns.get(key);
200
+ if (pending) {
201
+ for (const { fn, returnNodeId, callerEntry } of pending) {
202
+ this._propagate(fn, returnNodeId, callerEntry, exitFact);
203
+ }
204
+ }
205
+ return true;
206
+ }
207
+
208
+ _getSummaries(qid, entryFact) {
209
+ const set = this.summaries.get(this._summaryKey(qid, entryFact));
210
+ return set ? [...set] : [];
211
+ }
212
+
213
+ _registerPendingReturn(qid, entryFact, fn, returnNodeId, callerEntry) {
214
+ const key = this._summaryKey(qid, entryFact);
215
+ if (!this.pendingReturns.has(key)) this.pendingReturns.set(key, []);
216
+ this.pendingReturns.get(key).push({ fn, returnNodeId, callerEntry });
217
+ }
218
+
219
+ run() {
220
+ if (!this.callGraph || !this.callGraph.functions) return [];
221
+ // Seed: for every function, add ZERO → ZERO at its entry. This is
222
+ // the main-procedure seed in IFDS; with no main, every function is
223
+ // potentially the entry point for the analysis.
224
+ for (const fn of this.callGraph.functions.values()) {
225
+ if (!fn.cfg) continue;
226
+ this._propagate(fn, fn.cfg.entry, ZERO, ZERO);
227
+ }
228
+ // Drain the worklist.
229
+ while (this.work.length) {
230
+ if (this.edgeCount >= this.maxEdges) break;
231
+ const item = this.work.shift();
232
+ this._processNode(item);
233
+ }
234
+ return this.findings;
235
+ }
236
+
237
+ _propagate(fn, nodeId, entryFact, currentFact) {
238
+ if (this.edgeCount >= this.maxEdges) return;
239
+ const key = `${fn.qid}|${nodeId}`;
240
+ if (!this.pathEdges.has(key)) this.pathEdges.set(key, new Set());
241
+ const edge = `${entryFact}|${currentFact}`;
242
+ const set = this.pathEdges.get(key);
243
+ if (set.has(edge)) return;
244
+ set.add(edge);
245
+ this.edgeCount++;
246
+ this.work.push({ fn, nodeId, entryFact, currentFact });
247
+ }
248
+
249
+ _processNode({ fn, nodeId, entryFact, currentFact }) {
250
+ const node = fn.cfg.nodes[nodeId];
251
+ if (!node) return;
252
+ // Emit findings if this is a sink.
253
+ if (node.kind === 'call') {
254
+ for (const f of _detectSinkAtCall(node, currentFact)) {
255
+ this.findings.push({ ...f, _fnQid: fn.qid, _entryFact: entryFact });
256
+ }
257
+ // v0.73 — interprocedural call: if the callee is resolved AND we
258
+ // can derive an entry fact for it, look up (or queue) its summary
259
+ // and apply the resulting exit facts at the return site.
260
+ const resolved = this.callGraph.resolve ? this.callGraph.resolve(node.callee) : null;
261
+ const callee = resolved && resolved.qid ? resolved : null;
262
+ if (callee && callee.cfg) {
263
+ const calleeEntryFact = _entryFactForCall(node, currentFact, callee);
264
+ // Seed the callee at its entry with the derived fact.
265
+ this._propagate(callee, callee.cfg.entry, calleeEntryFact, calleeEntryFact);
266
+ // Apply any already-known summaries to OUR return site.
267
+ const existing = this._getSummaries(callee.qid, calleeEntryFact);
268
+ for (const succ of (node.succ || [])) {
269
+ for (const exitFact of existing) {
270
+ this._propagate(fn, succ, entryFact, _mapReturnFact(node, exitFact, currentFact));
271
+ }
272
+ // Register the return site so when more summaries arrive, we
273
+ // re-propagate at it.
274
+ this._registerPendingReturn(callee.qid, calleeEntryFact, fn, succ, entryFact);
275
+ }
276
+ }
277
+ }
278
+ // v0.73 — at the function exit, record a summary edge
279
+ // (entryFact, currentFact) for this function. Callers use it.
280
+ if (node.kind === 'exit' || (node.succ || []).length === 0) {
281
+ this._addSummary(fn.qid, entryFact, currentFact);
282
+ }
283
+ // Compute next facts.
284
+ let nextFacts;
285
+ if (node.kind === 'assign') {
286
+ nextFacts = _flowAssign(node, currentFact);
287
+ } else {
288
+ // Identity transfer for non-modeled kinds.
289
+ nextFacts = new Set([currentFact]);
290
+ }
291
+ // Propagate to successors.
292
+ for (const succ of (node.succ || [])) {
293
+ for (const nf of nextFacts) {
294
+ this._propagate(fn, succ, entryFact, nf);
295
+ }
296
+ }
297
+ }
298
+
299
+ // Diagnostics
300
+ stats() {
301
+ return {
302
+ pathEdges: this.edgeCount,
303
+ summaries: this.summaries.size,
304
+ findings: this.findings.length,
305
+ capped: this.edgeCount >= this.maxEdges,
306
+ };
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Top-level entry mirroring runTaintEngine's shape. Returns a flat array
312
+ * of findings. The IFDS solver runs only when AGENTIC_SECURITY_IFDS=1
313
+ * (gated by runDeepAnalysis).
314
+ */
315
+ export function runIfdsTaintEngine(perFileIR, callGraph, opts = {}) {
316
+ const solver = new IFDSSolver(perFileIR, callGraph, opts);
317
+ const raw = solver.run();
318
+ // Shape to match runTaintEngine's output.
319
+ const out = [];
320
+ const seen = new Set();
321
+ for (const f of raw) {
322
+ const fn = callGraph.functions.get(f._fnQid);
323
+ if (!fn) continue;
324
+ const key = `${f.sinkId}:${fn.file}:${f.line}`;
325
+ if (seen.has(key)) continue;
326
+ seen.add(key);
327
+ out.push({
328
+ id: `ir-taint-ifds:${fn.file}:${f.line}:${f.sinkId}`,
329
+ file: fn.file,
330
+ line: f.line,
331
+ vuln: f.vuln,
332
+ severity: f.severity,
333
+ cwe: f.cwe,
334
+ remediation: f.remediation,
335
+ parser: 'IR-TAINT-IFDS',
336
+ confidence: 0.8,
337
+ sink: { file: fn.file, line: f.line, label: f.sinkId },
338
+ });
339
+ }
340
+ Object.defineProperty(out, '_ifdsStats', {
341
+ value: solver.stats(),
342
+ enumerable: false,
343
+ });
344
+ return out;
345
+ }
346
+
347
+ export const _internal = { _flowAssign, _exprAccessPath, _detectSinkAtCall };
@@ -0,0 +1,129 @@
1
+ // Implicit-flow detection (P1.5).
2
+ //
3
+ // Today the engine tracks EXPLICIT taint — values flow into sinks. Implicit
4
+ // flow tracks CONTROL-DEPENDENCE: when a branch's condition is tainted, any
5
+ // variable assigned in that branch carries a SECONDARY taint at lower
6
+ // confidence (0.5 by default).
7
+ //
8
+ // Canonical motivating pattern:
9
+ //
10
+ // if (req.body.role === 'admin') {
11
+ // isAdmin = true;
12
+ // }
13
+ // if (isAdmin) callDangerous();
14
+ //
15
+ // The explicit-flow engine never sees taint flow into `isAdmin` — the
16
+ // dependence is control-only. Implicit-flow analysis adds `isAdmin` to a
17
+ // SHADOW state with confidence 0.5 whenever it's mutated inside a
18
+ // tainted-conditional branch.
19
+ //
20
+ // Over-approximation: implicit flow is famously noisy (sneaks into pure
21
+ // branches that don't actually leak info). So this analysis is OPT-IN
22
+ // only — gated by AGENTIC_SECURITY_IMPLICIT_FLOW=1 — and findings emitted
23
+ // carry an explicit `implicit:true` flag + capped confidence at 0.55.
24
+ //
25
+ // Public API:
26
+ // isImplicitFlowEnabled() → bool from env
27
+ // buildImplicitContext(cfg, taintState)
28
+ // → Map<nodeId, { tainted: bool, conditionLabel: string }>
29
+ // mutationsInTaintedBranch(node, ctx)
30
+ // → array of variable names that get assigned in a tainted branch
31
+ // applyImplicitFlow(state, mutatedVars, conditionLabel)
32
+ // → new state with the implicit-tainted vars added at confidence 0.5
33
+
34
+ import { addPath } from './access-paths.js';
35
+
36
+ export function isImplicitFlowEnabled() {
37
+ return process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '1';
38
+ }
39
+
40
+ /**
41
+ * Compute, for each CFG node, whether it's "inside" a tainted-condition
42
+ * branch and what condition tainted it. Returns Map<nodeId, ctx>.
43
+ *
44
+ * cfg: the function CFG
45
+ * exprTaint: callback (expr) -> bool, using the current taint state
46
+ *
47
+ * Heuristic: walk forward from entry, and when we hit an `if` whose
48
+ * condition is tainted, mark all nodes reachable from the consequent (and,
49
+ * if present, the alternate) until we exit those branches.
50
+ */
51
+ export function buildImplicitContext(cfg, exprTaint) {
52
+ const ctxByNid = new Map();
53
+ if (!cfg || !cfg.nodes) return ctxByNid;
54
+ // Walk forward; track "depth" of how many tainted-branches we're nested in.
55
+ const visited = new Set();
56
+ const stack = [{ nid: cfg.entry, depth: 0, label: null }];
57
+ while (stack.length) {
58
+ const { nid, depth, label } = stack.pop();
59
+ if (visited.has(nid)) continue;
60
+ visited.add(nid);
61
+ if (depth > 0) ctxByNid.set(nid, { tainted: true, conditionLabel: label });
62
+ const n = cfg.nodes[nid];
63
+ if (!n) continue;
64
+ if (n.kind === 'if' && n.cond && exprTaint(n.cond)) {
65
+ // Push the consequent at depth+1. We don't have a separate alternate
66
+ // edge in this v1 IR — `succ` carries both. v2 should add `then`/`else`
67
+ // distinguishing edges.
68
+ for (const s of (n.succ || [])) {
69
+ stack.push({ nid: s, depth: depth + 1, label: _formatCondLabel(n.cond) });
70
+ }
71
+ } else {
72
+ for (const s of (n.succ || [])) {
73
+ stack.push({ nid: s, depth, label });
74
+ }
75
+ }
76
+ }
77
+ return ctxByNid;
78
+ }
79
+
80
+ function _formatCondLabel(cond) {
81
+ if (!cond) return '?';
82
+ if (cond.kind === 'ident') return cond.name;
83
+ if (cond.kind === 'member') return `${_formatCondLabel(cond.object)}.${cond.prop}`;
84
+ if (cond.kind === 'binary') return `${_formatCondLabel(cond.left)} ${cond.op} ${_formatCondLabel(cond.right)}`;
85
+ if (cond.kind === 'literal') return JSON.stringify(cond.value);
86
+ return cond.kind || '?';
87
+ }
88
+
89
+ /**
90
+ * Given an `assign` node, return the target name if the assignment happens
91
+ * inside a tainted branch.
92
+ */
93
+ export function implicitAssignTarget(node, ctx) {
94
+ if (!node || node.kind !== 'assign') return null;
95
+ if (!ctx || !ctx.tainted) return null;
96
+ if (typeof node.target !== 'string') return null;
97
+ return node.target;
98
+ }
99
+
100
+ /**
101
+ * Add `varName` to `state` with an `implicit-` marker prefix so consumers
102
+ * can distinguish primary taint from implicit (lower-confidence) taint.
103
+ *
104
+ * Convention: implicit taint markers look like `implicit:<varName>` in the
105
+ * state set. Engine sink-checks consult both the primary path AND any
106
+ * `implicit:<...>` paths when emitting findings.
107
+ */
108
+ export function markImplicitTaint(state, varName) {
109
+ if (!varName || typeof varName !== 'string') return state;
110
+ return addPath(state, `implicit:${varName}`);
111
+ }
112
+
113
+ /**
114
+ * Helper: produce an implicit-flow finding from an assign-in-tainted-branch
115
+ * event. Used by the engine when a sink later consumes an implicit-tainted
116
+ * variable.
117
+ */
118
+ export function createImplicitFinding(node, conditionLabel) {
119
+ return {
120
+ kind: 'taint',
121
+ implicit: true,
122
+ confidence: 0.5,
123
+ vuln: `Implicit flow — variable mutated inside tainted-conditional branch (condition: ${conditionLabel || '?'})`,
124
+ severity: 'medium',
125
+ cwe: 'CWE-200',
126
+ line: node?.line || 0,
127
+ remediation: 'Verify that the conditional branch does not let user-controlled state escape into privileged paths. Implicit-flow findings are noisier than explicit-flow; review with elevated scrutiny.',
128
+ };
129
+ }