@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,212 @@
1
+ // Probabilistic exploitability with Wilson 95% confidence intervals (v0.68).
2
+ //
3
+ // Replaces opaque severity strings with a calibrated number per finding:
4
+ // f.exploitProbability ∈ [0,1] — Bayesian point estimate
5
+ // f.exploitProbabilityCI95 [lo, hi] — Wilson interval on point
6
+ // f.exploitProbabilityWhy string[] — feature names that fired
7
+ //
8
+ // This is the COMPLEMENT to `exploitability.js`, which is intentionally an
9
+ // ordinal priority (sort key). This module is the calibrated probability
10
+ // you'd put on a slide or stake a budget on.
11
+ //
12
+ // Inputs (per finding + context):
13
+ // - finding.cwe CWE family
14
+ // - finding.parser, finding.family detector identity
15
+ // - taint signals (`trace`, `chain`, source provenance)
16
+ // - reachability tier from posture/reachability-filter.js (set externally)
17
+ // - project context: hasAuth / hasWAF / hasCSP / route_unauth
18
+ // - historical signal: .agentic-security/exploit-history.jsonl (operator-
19
+ // curated record of past confirmed exploits per CWE family).
20
+ //
21
+ // Method:
22
+ // 1. Start with a CWE-family prior (CISA-KEV-aggregated base rate).
23
+ // 2. Multiplicatively update with feature factors (reachability +,
24
+ // sanitizer-present −, auth/WAF/CSP −, source-from-network +).
25
+ // 3. Wilson CI computed from operator's historical hit rate at finer
26
+ // grain (CWE × language × framework) when enough samples exist; falls
27
+ // back to wider CI from the global CWE-family prior otherwise.
28
+ //
29
+ // Important: this is NOT yet a calibrated statistical model. It's a
30
+ // principled heuristic that EXPOSES its uncertainty via the CI rather than
31
+ // hiding it behind a severity label. The CI width is the honest signal —
32
+ // when n is small or factors conflict, the interval gets wide on purpose.
33
+
34
+ import * as fs from 'node:fs';
35
+ import * as path from 'node:path';
36
+ import { wilsonInterval } from './calibration.js';
37
+
38
+ // CISA KEV-derived base rate per CWE family. These are rough mid-2025
39
+ // observations of "actually exploited in the wild" rates among findings
40
+ // of the family, not academic numbers. Refresh annually.
41
+ const CWE_BASE_RATE = {
42
+ 'CWE-78': 0.50, // command injection
43
+ 'CWE-89': 0.55, // SQLi
44
+ 'CWE-79': 0.35, // XSS
45
+ 'CWE-22': 0.40, // path traversal
46
+ 'CWE-918': 0.40, // SSRF
47
+ 'CWE-502': 0.55, // deserialization
48
+ 'CWE-94': 0.55, // code injection / SSTI
49
+ 'CWE-611': 0.30, // XXE
50
+ 'CWE-1321': 0.45, // prototype pollution
51
+ 'CWE-352': 0.20, // CSRF
52
+ 'CWE-601': 0.20, // open redirect (low impact alone, often a chain link)
53
+ 'CWE-113': 0.30, // response splitting
54
+ 'CWE-798': 0.65, // hardcoded secrets — when the secret leaks, exploit is immediate
55
+ 'CWE-90': 0.40, // LDAP injection
56
+ 'CWE-643': 0.40, // XPath injection
57
+ 'CWE-1333': 0.25, // ReDoS — exploitation often DoS only
58
+ 'CWE-327': 0.25, // weak crypto — usually requires chained access
59
+ 'CWE-329': 0.30, // static IV
60
+ 'CWE-338': 0.30, // weak RNG
61
+ 'CWE-916': 0.40, // weak password hash
62
+ 'CWE-1336': 0.40, // prompt injection
63
+ 'CWE-269': 0.45, // privilege escalation
64
+ };
65
+
66
+ const DEFAULT_BASE_RATE = 0.30;
67
+
68
+ // Multiplicative factors. The point estimate is base * Π(factors), clamped
69
+ // to (0, 1). Factor > 1 raises the probability; factor < 1 lowers it. We
70
+ // also collect which factors fired into `why`.
71
+ const FACTORS = [
72
+ // POSITIVE (raise probability)
73
+ {
74
+ name: 'reachable-from-public-route',
75
+ factor: 1.5,
76
+ test: (f) => f.reachabilityTier === 'reachable-public' || f.reachabilityTier === 'public-unauthed',
77
+ },
78
+ {
79
+ name: 'source-from-network',
80
+ factor: 1.3,
81
+ test: (f) => (f.trace || f.chain || []).some(t =>
82
+ /http-body|url-param|header|cookie/i.test(t.provenance || '')),
83
+ },
84
+ {
85
+ name: 'critical-severity-detector',
86
+ factor: 1.15,
87
+ test: (f) => f.severity === 'critical',
88
+ },
89
+ {
90
+ name: 'cwe-on-cisa-kev',
91
+ factor: 1.25,
92
+ test: (f) => !!f.kevReferenced,
93
+ },
94
+ // NEGATIVE (lower probability)
95
+ {
96
+ name: 'sanitizer-in-path',
97
+ factor: 0.5,
98
+ test: (f) => Array.isArray(f.sanitizerCallsInPath) && f.sanitizerCallsInPath.length > 0,
99
+ },
100
+ {
101
+ name: 'auth-middleware-on-route',
102
+ factor: 0.6,
103
+ test: (f) => f.routeAuth === true,
104
+ },
105
+ {
106
+ name: 'project-has-waf',
107
+ factor: 0.75,
108
+ test: (f, ctx) => ctx && ctx.hasWAF === true,
109
+ },
110
+ {
111
+ name: 'project-has-csp',
112
+ factor: 0.85,
113
+ test: (f, ctx) => ctx && ctx.hasCSP === true && /CWE-79|xss/i.test(f.cwe || f.family || ''),
114
+ },
115
+ {
116
+ name: 'low-severity-detector',
117
+ factor: 0.6,
118
+ test: (f) => f.severity === 'low' || f.severity === 'info',
119
+ },
120
+ {
121
+ name: 'unreachable',
122
+ factor: 0.1,
123
+ test: (f) => f.unreachable === true,
124
+ },
125
+ ];
126
+
127
+ function _basePrior(cwe) {
128
+ if (!cwe) return DEFAULT_BASE_RATE;
129
+ return CWE_BASE_RATE[cwe] || DEFAULT_BASE_RATE;
130
+ }
131
+
132
+ function _clamp01(x) { return Math.max(0.001, Math.min(0.999, x)); }
133
+
134
+ // Operator-curated exploit history. JSONL, one row per past confirmed
135
+ // exploit: {"cwe": "CWE-89", "language": "javascript", "framework": "express",
136
+ // "verdict": "true_positive_exploited" | "false_positive"}
137
+ // Used to refine the Wilson CI when enough samples exist at the
138
+ // (cwe × language × framework) grain.
139
+ function _loadHistory(scanRoot) {
140
+ if (!scanRoot) return [];
141
+ const fp = path.join(scanRoot, '.agentic-security', 'exploit-history.jsonl');
142
+ if (!fs.existsSync(fp)) return [];
143
+ const out = [];
144
+ try {
145
+ for (const line of fs.readFileSync(fp, 'utf8').split('\n')) {
146
+ const t = line.trim();
147
+ if (!t) continue;
148
+ try {
149
+ const o = JSON.parse(t);
150
+ if (o && o.cwe && o.verdict) out.push(o);
151
+ } catch {}
152
+ }
153
+ } catch {}
154
+ return out;
155
+ }
156
+
157
+ function _historicalCi(history, cwe, language) {
158
+ if (!Array.isArray(history) || history.length === 0) return null;
159
+ // Tier the slice: prefer (cwe × language) when n ≥ 5, fall back to cwe
160
+ // alone when n ≥ 10, fall back to nothing when neither slice has volume.
161
+ const cl = history.filter(h => h.cwe === cwe && h.language === language);
162
+ const c = history.filter(h => h.cwe === cwe);
163
+ let pool, sliceName;
164
+ if (cl.length >= 5) { pool = cl; sliceName = `${cwe}×${language}`; }
165
+ else if (c.length >= 10) { pool = c; sliceName = cwe; }
166
+ else return null;
167
+ const tp = pool.filter(h => h.verdict === 'true_positive_exploited').length;
168
+ const n = pool.length;
169
+ return { ci: wilsonInterval(tp, n), p: tp / n, n, slice: sliceName };
170
+ }
171
+
172
+ // Public entry. Annotates each finding in `findings` with
173
+ // exploitProbability + exploitProbabilityCI95 + exploitProbabilityWhy.
174
+ //
175
+ // Pure function over findings — returns the same array.
176
+ export function annotateExploitProbability(findings, ctx = {}) {
177
+ if (!Array.isArray(findings) || findings.length === 0) return findings;
178
+ const history = _loadHistory(ctx.scanRoot);
179
+ for (const f of findings) {
180
+ const cwe = f.cwe || null;
181
+ const why = [];
182
+ let p = _basePrior(cwe);
183
+ for (const F of FACTORS) {
184
+ try {
185
+ if (F.test(f, ctx)) { p *= F.factor; why.push(F.name); }
186
+ } catch {}
187
+ }
188
+ p = _clamp01(p);
189
+ // Wilson CI: prefer historical CI when we have one, otherwise derive
190
+ // a wider CI from the prior (n=10 implied sample at base rate).
191
+ const hist = _historicalCi(history, cwe, f.language || (f.file || '').split('.').pop());
192
+ if (hist) {
193
+ f.exploitProbability = hist.p;
194
+ f.exploitProbabilityCI95 = hist.ci;
195
+ f.exploitProbabilitySlice = hist.slice;
196
+ f.exploitProbabilityN = hist.n;
197
+ } else {
198
+ // Wilson with a synthetic n=10 "prior pseudo-observations" at the
199
+ // updated probability — gives a wide CI when we have no data.
200
+ const tp = Math.round(p * 10);
201
+ f.exploitProbability = p;
202
+ f.exploitProbabilityCI95 = wilsonInterval(tp, 10);
203
+ f.exploitProbabilitySlice = 'prior-only';
204
+ f.exploitProbabilityN = 10;
205
+ }
206
+ f.exploitProbabilityWhy = why;
207
+ }
208
+ return findings;
209
+ }
210
+
211
+ // Convenience for tests / diagnostics.
212
+ export const _internal = { CWE_BASE_RATE, DEFAULT_BASE_RATE, FACTORS, _basePrior };
@@ -0,0 +1,121 @@
1
+ // Exploitability scoring (FR-PREC-3) — ORDINAL PRIORITY, NOT A PROBABILITY.
2
+ //
3
+ // The output is a 0–1 ordinal priority score used to rank findings, NOT a
4
+ // calibrated probability. The weights below are hand-picked heuristics and
5
+ // have not been calibrated against a labeled outcome set. Treat the number
6
+ // as "use this to sort, not to bet money on."
7
+ //
8
+ // In particular, do NOT:
9
+ // - render the score as a percentage in customer-facing UI ("95% likely
10
+ // to be exploited" implies a precision the score doesn't have)
11
+ // - feed the score into a downstream pricing / risk-acceptance decision
12
+ // - aggregate it across findings (average exploitability is meaningless)
13
+ //
14
+ // DO use it for:
15
+ // - ranking findings within a single scan ("show me the top 10")
16
+ // - tier labels (critical/high/medium/low) which intentionally collapse
17
+ // the score into coarse buckets
18
+ //
19
+ // To turn this into a calibrated probability, you need a labeled outcome
20
+ // dataset (PRs merged with no incident vs. PRs that became incidents) and
21
+ // isotonic regression. See PRD §10 / bench/README.md for the open work.
22
+ //
23
+ // Output:
24
+ // f.exploitability ∈ [0,1] — ordinal priority score
25
+ // f.priorityScore = f.exploitability — explicit alias for new callers
26
+ // f.exploitabilityFactors: string[] — which signals contributed
27
+ // f.exploitabilityTier: 'critical' | 'high' | 'medium' | 'low'
28
+
29
+ const SEVERITY_BASE = {
30
+ critical: 0.80,
31
+ high: 0.65,
32
+ medium: 0.40,
33
+ low: 0.20,
34
+ info: 0.10,
35
+ };
36
+
37
+ // Detected from project signals — passed in via ctx.
38
+ // ctx.hasCSP — Content-Security-Policy header configured
39
+ // ctx.hasHelmet — helmet middleware present
40
+ // ctx.hasWAF — WAF / Cloudflare rules ingested
41
+ // ctx.hasAuth — auth middleware/guards detected somewhere
42
+ // ctx.unauthRoutes — count of routes that lack auth on the project
43
+ export function detectProjectContext(fc, routes) {
44
+ const ctx = {
45
+ hasCSP: false,
46
+ hasHelmet: false,
47
+ hasWAF: false,
48
+ hasAuth: false,
49
+ unauthRoutes: 0,
50
+ };
51
+ if (!fc) return ctx;
52
+ for (const [fp, c] of Object.entries(fc)) {
53
+ if (!c || typeof c !== 'string') continue;
54
+ if (!ctx.hasCSP && /content[-_]security[-_]policy|Content-Security-Policy/i.test(c)) ctx.hasCSP = true;
55
+ if (!ctx.hasHelmet && /\bhelmet\s*\(/.test(c)) ctx.hasHelmet = true;
56
+ if (!ctx.hasWAF && /(cloudflare|aws[-_]?waf|akamai[-_]?waf|fastly[-_]?waf)/i.test(c)) ctx.hasWAF = true;
57
+ if (!ctx.hasAuth && /(passport\.|express-jwt|requireAuth|requires?_login|@login_required|isAuthenticated|verifyJWT)/i.test(c)) ctx.hasAuth = true;
58
+ void fp;
59
+ }
60
+ if (Array.isArray(routes)) ctx.unauthRoutes = routes.filter(r => r && r.unauthed).length;
61
+ return ctx;
62
+ }
63
+
64
+ function scoreOne(f, ctx) {
65
+ const factors = [];
66
+ let s = SEVERITY_BASE[f.severity] ?? 0.30;
67
+ factors.push(`sev:${f.severity}`);
68
+
69
+ // Reachability from entry points raises exploitability sharply.
70
+ if (f.routeRooted) { s += 0.20; factors.push('route-rooted'); }
71
+ else if (f.reachable === true) { s += 0.10; factors.push('reachable'); }
72
+ else if (f.reachable === false) { s -= 0.20; factors.push('unreachable'); }
73
+
74
+ // Auth gating on the path lowers it.
75
+ if (f.guards && f.guards.length) { s -= 0.15; factors.push(`guards:${f.guards.length}`); }
76
+
77
+ // Sanitized paths are very low.
78
+ if (f.isSanitized) { s = Math.min(s, 0.10); factors.push('sanitized'); }
79
+ if (f.sanitizerMismatch) { s += 0.10; factors.push('sanitizer-mismatch'); }
80
+
81
+ // SCA enrichment: KEV-listed vulns are by definition actively exploited;
82
+ // EPSS percentile ≥ 0.95 puts the CVE in the "exploited now" cohort.
83
+ if (f.kev || f.kevListed) { s = Math.max(s, 0.92); factors.push('kev'); }
84
+ if (typeof f.epssPercentile === 'number' && f.epssPercentile >= 0.95) {
85
+ s = Math.max(s, 0.85); factors.push('epss>=p95');
86
+ }
87
+ if (f.exploitedNow) { s = Math.max(s, 0.85); factors.push('exploited-now'); }
88
+
89
+ // Project-wide mitigations reduce exploitability.
90
+ if (ctx) {
91
+ if (ctx.hasCSP && /xss/i.test(f.family || f.vuln || '')) { s -= 0.10; factors.push('csp-present'); }
92
+ if (ctx.hasHelmet) { s -= 0.05; factors.push('helmet'); }
93
+ if (ctx.hasWAF) { s -= 0.05; factors.push('waf'); }
94
+ if (!ctx.hasAuth && f.routeRooted) { s += 0.10; factors.push('no-auth-middleware'); }
95
+ }
96
+
97
+ s = Math.max(0, Math.min(1, s));
98
+ // Round to 2 decimals — the score is ordinal, not calibrated. Three
99
+ // decimals of precision suggested precision the model doesn't have.
100
+ return { score: Math.round(s * 100) / 100, factors };
101
+ }
102
+
103
+ export function annotateExploitability(findings, ctx) {
104
+ if (!Array.isArray(findings)) return;
105
+ for (const f of findings) {
106
+ if (!f || typeof f !== 'object') continue;
107
+ try {
108
+ const { score, factors } = scoreOne(f, ctx);
109
+ f.exploitability = score;
110
+ f.priorityScore = score; // ordinal-priority alias; new code should use this name
111
+ f.exploitabilityFactors = factors;
112
+ f._scoreIsOrdinal = true; // marker so downstream code knows not to treat as probability
113
+ if (score >= 0.80) f.exploitabilityTier = 'critical';
114
+ else if (score >= 0.60) f.exploitabilityTier = 'high';
115
+ else if (score >= 0.35) f.exploitabilityTier = 'medium';
116
+ else f.exploitabilityTier = 'low';
117
+ } catch {
118
+ f.exploitability = null;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,110 @@
1
+ // FR-PROD-6 — Feature-flag awareness.
2
+ //
3
+ // When a security finding lives behind a feature-flag check, its real-world
4
+ // exposure depends on the flag's rollout state, not on the code itself.
5
+ // A flag at 0% rollout is `info`; the same code at 100% is `critical`.
6
+ //
7
+ // This module recognizes flag-gated code regions for the major providers:
8
+ // - LaunchDarkly (`ldClient.variation`, `useFlag`)
9
+ // - Statsig (`Statsig.checkGate`, `useGate`)
10
+ // - ConfigCat (`configCatClient.getValue`)
11
+ // - Unleash (`unleash.isEnabled`)
12
+ // - OpenFeature (`client.getBooleanValue`)
13
+ // - Vercel Flags (`@vercel/flags`)
14
+ // - Custom env-var flags (`process.env.FEATURE_X === 'true'`)
15
+ //
16
+ // We tag findings with the gating flag name when detected. Rollout percentage
17
+ // can be supplied via `.agentic-security/feature-flag-rollouts.json` (a map
18
+ // of flag name → percentage 0..100); if absent, default to 100% (assume the
19
+ // flag is on) — fail-open against the security side.
20
+
21
+ import * as fs from 'node:fs';
22
+ import * as path from 'node:path';
23
+
24
+ const FLAG_PATTERNS = [
25
+ // LaunchDarkly
26
+ [/\bldClient\.variation\s*\(\s*['"`]([^'"`]+)['"`]/g, 'launchdarkly'],
27
+ [/\buseFlag\s*\(\s*['"`]([^'"`]+)['"`]/g, 'launchdarkly'],
28
+ [/\bvariation\s*\(\s*['"`]([^'"`]+)['"`]/g, 'launchdarkly'],
29
+ // Statsig
30
+ [/\bStatsig\.(?:checkGate|getExperiment|getConfig)\s*\(\s*['"`]([^'"`]+)['"`]/g, 'statsig'],
31
+ [/\buseGate\s*\(\s*['"`]([^'"`]+)['"`]/g, 'statsig'],
32
+ // ConfigCat
33
+ [/\bconfigCatClient\.getValue\s*\(\s*['"`]([^'"`]+)['"`]/g, 'configcat'],
34
+ // Unleash
35
+ [/\bunleash\.isEnabled\s*\(\s*['"`]([^'"`]+)['"`]/g, 'unleash'],
36
+ // OpenFeature
37
+ [/\b(?:client|of)\.get(?:Boolean|String|Number)Value\s*\(\s*['"`]([^'"`]+)['"`]/g, 'openfeature'],
38
+ // Vercel
39
+ [/\b(?:flag|getFlag|flags)\s*\(\s*['"`]([^'"`]+)['"`]/g, 'vercel-or-generic'],
40
+ // env-var flags
41
+ [/process\.env\.(FEATURE_[A-Z0-9_]+)\s*===?\s*['"`]?(?:true|1)/g, 'env-var'],
42
+ [/process\.env\.(FF_[A-Z0-9_]+)\s*===?\s*['"`]?(?:true|1)/g, 'env-var'],
43
+ ];
44
+
45
+ function loadRollouts(scanRoot) {
46
+ const candidates = [
47
+ '.agentic-security/feature-flag-rollouts.json',
48
+ '.agentic-security/feature-flags.json',
49
+ ];
50
+ for (const rel of candidates) {
51
+ const fp = path.join(scanRoot || process.cwd(), rel);
52
+ try {
53
+ if (fs.existsSync(fp)) {
54
+ const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
55
+ if (data && typeof data === 'object') return data;
56
+ }
57
+ } catch {}
58
+ }
59
+ return null;
60
+ }
61
+
62
+ // Walk every file once and build a map: file → list of { flagName, line, vendor }.
63
+ // Cheap regex scan, suitable to call per-scan.
64
+ export function detectFlagSites(fileContents) {
65
+ const out = {};
66
+ if (!fileContents || typeof fileContents !== 'object') return out;
67
+ for (const [fp, text] of Object.entries(fileContents)) {
68
+ if (!text || typeof text !== 'string') continue;
69
+ const sample = text.length > 100_000 ? text.slice(0, 100_000) : text;
70
+ const hits = [];
71
+ for (const [re, vendor] of FLAG_PATTERNS) {
72
+ re.lastIndex = 0;
73
+ let m;
74
+ while ((m = re.exec(sample))) {
75
+ const flagName = m[1];
76
+ const line = sample.slice(0, m.index).split('\n').length;
77
+ hits.push({ flagName, vendor, line });
78
+ }
79
+ }
80
+ if (hits.length) out[fp] = hits;
81
+ }
82
+ return out;
83
+ }
84
+
85
+ // For each finding, search ±20 lines around the finding location in its file
86
+ // for a flag site. If found, tag with the controlling flag and rollout %.
87
+ export function annotateFeatureFlagGating(findings, fileContents, opts = {}) {
88
+ if (!Array.isArray(findings)) return findings;
89
+ const rollouts = opts.rollouts || loadRollouts(opts.scanRoot);
90
+ const sites = detectFlagSites(fileContents || {});
91
+ for (const f of findings) {
92
+ if (!f || typeof f !== 'object') continue;
93
+ const fp = f.file;
94
+ const ln = f.line || 0;
95
+ if (!fp || !sites[fp]) continue;
96
+ const window = 20;
97
+ const nearby = sites[fp].find(s => Math.abs(s.line - ln) <= window);
98
+ if (!nearby) continue;
99
+ f.featureFlag = nearby.flagName;
100
+ f.featureFlagVendor = nearby.vendor;
101
+ const rollout = rollouts && Object.prototype.hasOwnProperty.call(rollouts, nearby.flagName)
102
+ ? Number(rollouts[nearby.flagName])
103
+ : 100;
104
+ f.featureFlagRollout = Number.isFinite(rollout) ? Math.max(0, Math.min(100, rollout)) : 100;
105
+ if (f.featureFlagRollout === 0) f.featureFlagState = 'gated-off';
106
+ else if (f.featureFlagRollout < 100) f.featureFlagState = 'partial-rollout';
107
+ else f.featureFlagState = 'fully-rolled-out';
108
+ }
109
+ return findings;
110
+ }
@@ -0,0 +1,132 @@
1
+ // Premortem #8: backfill the `parser` and `family` fields on every finding.
2
+ //
3
+ // Symptom this fixes: a smoke run on test/fixtures/vulnerable-js reported 31
4
+ // findings, all with `parser: null` — which silenced the PARSER_PRIOR boost in
5
+ // confidence.js, AND 20/31 had `family: null` — which silenced the entire
6
+ // calibration table in calibration.js. The annotation pipeline downstream
7
+ // expected these fields to be set by every detector, but most regex-style
8
+ // detectors emit a plain Finding shape without them.
9
+ //
10
+ // We backfill, never overwrite. Detector-set values win.
11
+
12
+ // Lightweight family inference. CWE-based first, then title/vuln keyword.
13
+ const _CWE_FAMILY = {
14
+ 'CWE-78': 'command-injection',
15
+ 'CWE-79': 'xss',
16
+ 'CWE-80': 'xss',
17
+ 'CWE-87': 'xss',
18
+ 'CWE-89': 'sql-injection',
19
+ 'CWE-90': 'ldap-injection',
20
+ 'CWE-91': 'xpath-injection',
21
+ 'CWE-94': 'code-injection',
22
+ 'CWE-22': 'path-traversal',
23
+ 'CWE-23': 'path-traversal',
24
+ 'CWE-36': 'path-traversal',
25
+ 'CWE-200': 'info-disclosure',
26
+ 'CWE-209': 'info-disclosure',
27
+ 'CWE-256': 'hardcoded-secret',
28
+ 'CWE-259': 'hardcoded-secret',
29
+ 'CWE-287': 'broken-auth',
30
+ 'CWE-295': 'cert-validation',
31
+ 'CWE-306': 'broken-auth',
32
+ 'CWE-307': 'rate-limit',
33
+ 'CWE-326': 'weak-crypto',
34
+ 'CWE-327': 'weak-crypto',
35
+ 'CWE-328': 'weak-crypto',
36
+ 'CWE-329': 'weak-crypto',
37
+ 'CWE-330': 'weak-rng',
38
+ 'CWE-338': 'weak-rng',
39
+ 'CWE-345': 'integrity',
40
+ 'CWE-352': 'csrf',
41
+ 'CWE-384': 'session-fixation',
42
+ 'CWE-434': 'unrestricted-upload',
43
+ 'CWE-502': 'insecure-deserialization',
44
+ 'CWE-601': 'open-redirect',
45
+ 'CWE-611': 'xxe',
46
+ 'CWE-639': 'idor',
47
+ 'CWE-640': 'broken-auth',
48
+ 'CWE-732': 'permissions',
49
+ 'CWE-770': 'rate-limit',
50
+ 'CWE-776': 'xxe',
51
+ 'CWE-798': 'hardcoded-secret',
52
+ 'CWE-829': 'supply-chain',
53
+ 'CWE-862': 'broken-authz',
54
+ 'CWE-863': 'broken-authz',
55
+ 'CWE-915': 'mass-assignment',
56
+ 'CWE-918': 'ssrf',
57
+ 'CWE-1004': 'cookie-flag',
58
+ 'CWE-1021': 'clickjacking',
59
+ 'CWE-1287': 'idor',
60
+ 'CWE-1321': 'prototype-pollution',
61
+ 'CWE-1333': 'redos',
62
+ };
63
+
64
+ const _KEYWORD_FAMILY = [
65
+ [/sql\s*injection/i, 'sql-injection'],
66
+ [/command\s*injection/i, 'command-injection'],
67
+ [/code\s*injection|eval|insecure\s*eval/i, 'code-injection'],
68
+ [/cross[-\s]?site\s*scripting|\bxss\b/i, 'xss'],
69
+ [/path\s*traversal|directory\s*traversal/i, 'path-traversal'],
70
+ [/server[-\s]?side\s*request\s*forgery|\bssrf\b/i, 'ssrf'],
71
+ [/cross[-\s]?site\s*request\s*forgery|\bcsrf\b/i, 'csrf'],
72
+ [/open\s*redirect/i, 'open-redirect'],
73
+ [/\bxxe\b|external\s*entity/i, 'xxe'],
74
+ [/\bidor\b|insecure\s*direct\s*object/i, 'idor'],
75
+ [/mass[-\s]?assignment|over[-\s]?posting/i, 'mass-assignment'],
76
+ [/prototype\s*pollution/i, 'prototype-pollution'],
77
+ [/deserialization|deserialize/i, 'insecure-deserialization'],
78
+ [/weak\s*(?:crypto|hash|cipher)|\bmd5\b|\bsha1\b|\brc4\b/i, 'weak-crypto'],
79
+ [/weak\s*rng|insecure\s*random|math\.random/i, 'weak-rng'],
80
+ [/hardcoded\s*(?:secret|credential|key|password|token)|secret\s*in\s*code/i, 'hardcoded-secret'],
81
+ [/jwt|json\s*web\s*token/i, 'broken-auth'],
82
+ [/jndi/i, 'jndi-injection'],
83
+ [/log\s*injection|log4shell/i, 'log-injection'],
84
+ [/insecure\s*http|http\s*without\s*tls|missing\s*https/i, 'insecure-http'],
85
+ [/host\s*header/i, 'host-header'],
86
+ [/rate[-\s]?limit/i, 'rate-limit'],
87
+ [/vulnerable\s*dependency|cve-\d{4}-\d+|known\s*vulnerable\s*component/i, 'vulnerable-dep'],
88
+ [/prompt\s*injection|llm\s*injection/i, 'prompt-injection'],
89
+ [/clickjacking|x-frame-options/i, 'clickjacking'],
90
+ [/zip[-\s]?slip/i, 'path-traversal'],
91
+ ];
92
+
93
+ function _inferFamily(f) {
94
+ if (!f || typeof f !== 'object') return null;
95
+ if (typeof f.family === 'string' && f.family.length) return f.family;
96
+ if (typeof f.cwe === 'string' && _CWE_FAMILY[f.cwe]) return _CWE_FAMILY[f.cwe];
97
+ const hay = `${f.vuln || ''} ${f.title || ''} ${f.description || ''}`.slice(0, 600);
98
+ for (const [re, fam] of _KEYWORD_FAMILY) {
99
+ if (re.test(hay)) return fam;
100
+ }
101
+ return null;
102
+ }
103
+
104
+ function _inferParser(f) {
105
+ if (!f || typeof f !== 'object') return 'REGEX';
106
+ if (typeof f.parser === 'string' && f.parser.length) return f.parser;
107
+ // SCA findings carry pkg/component/purl; tag them so triage and validator
108
+ // can short-circuit (LLM validator already special-cases parser SCA).
109
+ if (f.parser === 'SCA' || f.kind === 'sca' ||
110
+ typeof f.pkg === 'string' || typeof f.component === 'string' ||
111
+ typeof f.purl === 'string') return 'SCA';
112
+ if (f.custom === true) return 'CUSTOM_RULE';
113
+ // Findings carrying a source/sink chain are layer-2 taint outputs.
114
+ if (Array.isArray(f.chain) && f.chain.length && f.source && f.sink) return 'IR-TAINT';
115
+ // AST-driven detectors mark themselves; fall back to REGEX otherwise.
116
+ return 'REGEX';
117
+ }
118
+
119
+ export function backfillFindingDefaults(findings) {
120
+ if (!Array.isArray(findings)) return;
121
+ for (const f of findings) {
122
+ if (!f || typeof f !== 'object') continue;
123
+ if (!f.parser) f.parser = _inferParser(f);
124
+ if (!f.family) {
125
+ const fam = _inferFamily(f);
126
+ if (fam) f.family = fam;
127
+ }
128
+ }
129
+ }
130
+
131
+ // Exported for tests.
132
+ export const _internals = { _CWE_FAMILY, _KEYWORD_FAMILY, _inferFamily, _inferParser };