@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,200 @@
1
+ // FR-PROD-1 — WAF rule-set ingest.
2
+ //
3
+ // Read a WAF rule set (Cloudflare custom rules JSON, AWS WAF JSON, ModSecurity
4
+ // `.conf`, or a normalized YAML) from one of the conventional locations:
5
+ //
6
+ // .agentic-security/waf-rules.{json,yml,yaml,conf}
7
+ // waf/rules.json
8
+ // cloudflare-rules.json
9
+ // aws-waf.json
10
+ //
11
+ // Build a list of `BlockRule { id, pattern, families }` where `families` is the
12
+ // set of attack families this rule unambiguously blocks. Conservative under-
13
+ // approximation: a finding is demoted to `mitigated-by-waf` only when its
14
+ // family appears in some rule's families AND the rule does not depend on
15
+ // runtime context the scanner cannot evaluate (e.g., per-customer rate
16
+ // thresholds).
17
+ //
18
+ // The format is intentionally narrow because vendor WAF rule semantics differ.
19
+ // We support these inputs:
20
+ //
21
+ // Normalized YAML / JSON (recommended):
22
+ // rules:
23
+ // - id: cf-1
24
+ // pattern: 'SQLi attempt'
25
+ // families: ['sql-injection']
26
+ // - id: cf-2
27
+ // pattern: 'XSS reflected'
28
+ // families: ['xss']
29
+ //
30
+ // Cloudflare custom-rule export (best-effort):
31
+ // [{ "id": "...", "expression": "...", "action": "block" }, ...]
32
+ // Family is inferred from the expression text via SIGNAL_PATTERNS.
33
+ //
34
+ // AWS WAF list-rules JSON: same expression-based heuristic.
35
+
36
+ import * as fs from 'node:fs';
37
+ import * as path from 'node:path';
38
+
39
+ const CANDIDATE_PATHS = [
40
+ '.agentic-security/waf-rules.json',
41
+ '.agentic-security/waf-rules.yml',
42
+ '.agentic-security/waf-rules.yaml',
43
+ '.agentic-security/waf-rules.conf',
44
+ 'waf/rules.json',
45
+ 'cloudflare-rules.json',
46
+ 'aws-waf.json',
47
+ ];
48
+
49
+ const SIGNAL_PATTERNS = [
50
+ [/sqli|sql.injection|union.+select/i, 'sql-injection'],
51
+ [/xss|cross.site|<script|javascript:/i, 'xss'],
52
+ [/ssrf|169\.254\.169\.254|metadata.google|localhost.+request/i, 'ssrf'],
53
+ [/path.traversal|\.\.\//, 'path-traversal'],
54
+ [/command.injection|shell.injection|\$\(|system\(|exec\(/i, 'command-injection'],
55
+ [/xxe|external.entity|!ENTITY/i, 'xxe'],
56
+ [/csrf|cross.site request/i, 'csrf'],
57
+ [/open.redirect|url.redirect/i, 'open-redirect'],
58
+ [/log4shell|log4j|jndi/i, 'jndi'],
59
+ [/deserialization|gadget chain/i, 'unsafe-deserialization'],
60
+ [/rate.?limit|rate.limit/i, 'unbounded-llm'],
61
+ [/prompt.injection|jailbreak/i, 'prompt-injection'],
62
+ ];
63
+
64
+ function inferFamilyFromExpression(expr) {
65
+ if (!expr || typeof expr !== 'string') return [];
66
+ const fams = new Set();
67
+ for (const [re, fam] of SIGNAL_PATTERNS) {
68
+ if (re.test(expr)) fams.add(fam);
69
+ }
70
+ return [...fams];
71
+ }
72
+
73
+ function parseNormalized(obj) {
74
+ const rules = Array.isArray(obj.rules) ? obj.rules : [];
75
+ return rules.map((r) => ({
76
+ id: r.id || r.name || 'unnamed',
77
+ pattern: r.pattern || r.description || '',
78
+ families: Array.isArray(r.families) ? r.families : inferFamilyFromExpression(r.pattern || ''),
79
+ vendor: r.vendor || 'normalized',
80
+ }));
81
+ }
82
+
83
+ function parseCloudflare(arr) {
84
+ return arr.filter(r => r && (r.action === 'block' || r.action === 'managed_challenge')).map((r) => ({
85
+ id: r.id || r.ref || 'cf-' + Math.random().toString(36).slice(2, 8),
86
+ pattern: r.description || r.expression || '',
87
+ families: inferFamilyFromExpression(r.expression || r.description || ''),
88
+ vendor: 'cloudflare',
89
+ }));
90
+ }
91
+
92
+ function parseAwsWaf(obj) {
93
+ const rules = obj.Rules || obj.rules || [];
94
+ return rules.map((r) => ({
95
+ id: r.Name || r.name || 'aws-' + Math.random().toString(36).slice(2, 8),
96
+ pattern: JSON.stringify(r.Statement || r.statement || {}).slice(0, 200),
97
+ families: inferFamilyFromExpression(JSON.stringify(r.Statement || r.statement || {})),
98
+ vendor: 'aws-waf',
99
+ })).filter(r => r.families.length > 0);
100
+ }
101
+
102
+ function parseYamlLike(text) {
103
+ // Tiny YAML subset parser — enough for our recommended shape. NOT a real
104
+ // YAML parser. Falls back to JSON.parse on failure.
105
+ try { return JSON.parse(text); } catch {}
106
+ const rules = [];
107
+ let current = null;
108
+ for (const raw of text.split(/\n/)) {
109
+ const ln = raw.replace(/#.*$/, '').replace(/\s+$/, '');
110
+ if (!ln.trim()) continue;
111
+ const dashMatch = /^\s*-\s+(\w+):\s*(.*)$/.exec(ln);
112
+ const kvMatch = /^\s+(\w+):\s*(.*)$/.exec(ln);
113
+ if (dashMatch) {
114
+ if (current) rules.push(current);
115
+ current = {};
116
+ current[dashMatch[1]] = parseScalar(dashMatch[2]);
117
+ } else if (kvMatch && current) {
118
+ current[kvMatch[1]] = parseScalar(kvMatch[2]);
119
+ }
120
+ }
121
+ if (current) rules.push(current);
122
+ return { rules };
123
+ }
124
+
125
+ function parseScalar(s) {
126
+ s = s.trim();
127
+ if (s === '' || s === '~' || s === 'null') return null;
128
+ if (/^\[.*\]$/.test(s)) {
129
+ return s.slice(1, -1).split(',').map(t => t.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean);
130
+ }
131
+ return s.replace(/^['"]|['"]$/g, '');
132
+ }
133
+
134
+ export function loadWafRules(scanRoot) {
135
+ const root = scanRoot || process.cwd();
136
+ for (const rel of CANDIDATE_PATHS) {
137
+ const fp = path.join(root, rel);
138
+ if (!fs.existsSync(fp)) continue;
139
+ let text;
140
+ try { text = fs.readFileSync(fp, 'utf8'); } catch { continue; }
141
+ try {
142
+ const trimmed = text.trim();
143
+ if (trimmed.startsWith('[')) {
144
+ return parseCloudflare(JSON.parse(trimmed));
145
+ }
146
+ if (trimmed.startsWith('{')) {
147
+ const obj = JSON.parse(trimmed);
148
+ if (obj.Rules || obj.rules?.[0]?.Statement) return parseAwsWaf(obj);
149
+ if (obj.rules) return parseNormalized(obj);
150
+ }
151
+ // YAML-like
152
+ const parsed = parseYamlLike(text);
153
+ if (parsed && parsed.rules) return parseNormalized(parsed);
154
+ } catch {
155
+ // Fall through to next candidate.
156
+ }
157
+ }
158
+ return [];
159
+ }
160
+
161
+ function familyOf(f) {
162
+ if (f.family) return String(f.family).toLowerCase();
163
+ const v = (f.vuln || '').toLowerCase();
164
+ if (/sql.*injection/.test(v)) return 'sql-injection';
165
+ if (/command.*injection/.test(v)) return 'command-injection';
166
+ if (/xss|cross.site/.test(v)) return 'xss';
167
+ if (/ssrf/.test(v)) return 'ssrf';
168
+ if (/path.travers/.test(v)) return 'path-traversal';
169
+ if (/xxe/.test(v)) return 'xxe';
170
+ if (/csrf/.test(v)) return 'csrf';
171
+ if (/jndi|log4shell/.test(v)) return 'jndi';
172
+ if (/deserial/.test(v)) return 'unsafe-deserialization';
173
+ if (/open.redirect/.test(v)) return 'open-redirect';
174
+ if (/prompt injection/.test(v)) return 'prompt-injection';
175
+ if (/max_tokens|unbounded/.test(v)) return 'unbounded-llm';
176
+ return 'unknown';
177
+ }
178
+
179
+ export function annotateWafMitigation(findings, scanRoot) {
180
+ if (!Array.isArray(findings)) return { findings, rules: [] };
181
+ const rules = loadWafRules(scanRoot);
182
+ if (!rules.length) return { findings, rules };
183
+ const byFamily = new Map();
184
+ for (const r of rules) {
185
+ for (const fam of r.families) {
186
+ if (!byFamily.has(fam)) byFamily.set(fam, []);
187
+ byFamily.get(fam).push(r);
188
+ }
189
+ }
190
+ for (const f of findings) {
191
+ if (!f || typeof f !== 'object') continue;
192
+ const fam = familyOf(f);
193
+ if (!byFamily.has(fam)) continue;
194
+ const matched = byFamily.get(fam);
195
+ f.mitigatedByWaf = true;
196
+ f.wafRuleId = matched.map(r => `${r.vendor}:${r.id}`).join(',');
197
+ f.wafMatchedFamilies = [fam];
198
+ }
199
+ return { findings, rules };
200
+ }
@@ -0,0 +1,141 @@
1
+ // FR-UX-9 — Why-fired / why-didn't-fire transparency.
2
+ //
3
+ // Every finding emitted by the engine carries an implicit "I fired because
4
+ // X." This module materializes that into an explicit provenance record so
5
+ // the user can see exactly what produced the finding without reading the
6
+ // scanner source. The provenance record is attached to `f.whyFired` and
7
+ // surfaces in SARIF properties + the HTML report.
8
+ //
9
+ // We intentionally do NOT try to capture LLM prompt hashes here — those are
10
+ // the llm-validator's responsibility and live on `f.validator_meta`. This
11
+ // module captures the pre-validator path: which detector ran, what rule
12
+ // matched, what evidence was present.
13
+ //
14
+ // The record shape:
15
+ // {
16
+ // detector: 'sast/sql-injection',
17
+ // ruleId: 'CWE-89',
18
+ // parser: 'JS' | 'IR-TAINT' | 'PYTHON' | ...,
19
+ // evidence: {
20
+ // sinkSnippet: '...',
21
+ // sourceSnippet:'...',
22
+ // pathSteps: [...],
23
+ // sanitizers: [], // if any were considered+rejected
24
+ // guards: [], // auth/RBAC checks observed
25
+ // },
26
+ // considered: {
27
+ // suppressionsApplied: [...],
28
+ // suppressionsSkipped: [...],
29
+ // reachabilityFilter: 'kept'|'demoted'|'unaffected',
30
+ // clusterCollapsed: boolean,
31
+ // typeNarrowed: boolean,
32
+ // },
33
+ // scanner: { rulesetVersion, packHash, modelId? }
34
+ // }
35
+ //
36
+ // `whyNotFired` is a separate API call — see `explainWhyNotFired`.
37
+
38
+ function detectorOf(f) {
39
+ if (f.detector) return f.detector;
40
+ const fam = f.family || '';
41
+ const parser = f.parser || '';
42
+ if (parser === 'IR-TAINT') return 'dataflow/ir-taint';
43
+ if (parser === 'PYTHON' || /python/i.test(parser)) return 'sast/python';
44
+ if (parser === 'SCA') return 'sca/cve';
45
+ if (parser === 'SECRET') return 'secrets/entropy';
46
+ if (fam) return `sast/${fam.toLowerCase().replace(/\s+/g, '-')}`;
47
+ return 'sast/unknown';
48
+ }
49
+
50
+ function ruleOf(f) {
51
+ return f.ruleId || f.cwe || f.family || (f.vuln || '').slice(0, 40) || 'unknown-rule';
52
+ }
53
+
54
+ export function buildWhyFired(f, ctx = {}) {
55
+ if (!f || typeof f !== 'object') return null;
56
+ return {
57
+ detector: detectorOf(f),
58
+ ruleId: ruleOf(f),
59
+ parser: f.parser || 'pattern',
60
+ evidence: {
61
+ sinkSnippet: f.sink?.snippet || f.snippet || null,
62
+ sourceSnippet: f.source?.snippet || null,
63
+ pathSteps: Array.isArray(f.pathSteps) ? f.pathSteps.map(s => ({ type: s.type, label: s.label })) : [],
64
+ sanitizers: f.sanitizers || [],
65
+ guards: f.guards || [],
66
+ },
67
+ considered: {
68
+ suppressionsApplied: f._suppressionsApplied || [],
69
+ suppressionsSkipped: f._suppressionsSkipped || [],
70
+ reachabilityFilter: f.unreachable ? 'demoted' : (f.reachable === true ? 'kept' : 'unaffected'),
71
+ clusterCollapsed: !!f.clusterSize && f.clusterSize > 1,
72
+ typeNarrowed: !!f.typeNarrowed,
73
+ crownJewelTier: f.crownJewelTier || null,
74
+ mitigationVerdict: f.mitigationVerdict || null,
75
+ },
76
+ scanner: {
77
+ rulesetVersion: ctx.rulesetVersion || null,
78
+ packHash: ctx.packHash || null,
79
+ modelId: ctx.modelId || null,
80
+ },
81
+ };
82
+ }
83
+
84
+ export function annotateWhyFired(findings, ctx = {}) {
85
+ if (!Array.isArray(findings)) return findings;
86
+ for (const f of findings) {
87
+ if (!f || typeof f !== 'object') continue;
88
+ f.whyFired = buildWhyFired(f, ctx);
89
+ }
90
+ return findings;
91
+ }
92
+
93
+ // "Why didn't you fire CWE-X on this file?" — used by the /why-not slash
94
+ // command. Given a target CWE and a project, surface what the engine considered
95
+ // and why it did not emit. This is intentionally simple: it walks the file
96
+ // contents, runs the family's regex set, and reports which patterns matched +
97
+ // which suppressions / mitigations dropped them.
98
+ export function explainWhyNotFired(targetCwe, fileContents, suppressions = []) {
99
+ const out = { cwe: targetCwe, considered: [], dropped: [] };
100
+ if (!targetCwe || !fileContents) return out;
101
+ const probes = WHY_NOT_PROBES[targetCwe] || [];
102
+ if (!probes.length) {
103
+ out.note = `No registered probe set for ${targetCwe}; cannot explain why-not from data.`;
104
+ return out;
105
+ }
106
+ for (const [fp, text] of Object.entries(fileContents)) {
107
+ if (!text || typeof text !== 'string') continue;
108
+ for (const probe of probes) {
109
+ probe.re.lastIndex = 0;
110
+ let m;
111
+ while ((m = probe.re.exec(text))) {
112
+ const line = text.slice(0, m.index).split('\n').length;
113
+ const entry = { file: fp, line, probe: probe.label, snippet: m[0].slice(0, 80) };
114
+ if (probe.suppress && probe.suppress(m[0], text)) {
115
+ out.dropped.push({ ...entry, reason: probe.suppressReason });
116
+ } else {
117
+ out.considered.push(entry);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ return out;
123
+ }
124
+
125
+ const WHY_NOT_PROBES = {
126
+ 'CWE-89': [
127
+ { label: 'sql-concat', re: /(?:SELECT|INSERT|UPDATE|DELETE)[^;]{0,200}\+\s*\w+/gi,
128
+ suppress: (m) => /\$\{[^}]+\}/.test(m) === false && /\?|\$\d+/.test(m),
129
+ suppressReason: 'parameterized-placeholder-present' },
130
+ ],
131
+ 'CWE-78': [
132
+ { label: 'shell-exec', re: /\b(?:exec|spawn|spawnSync)\s*\([^)]*\+/g,
133
+ suppress: (m) => /\bsanitize\b|\bshellEscape\b/.test(m),
134
+ suppressReason: 'sanitizer-call-detected' },
135
+ ],
136
+ 'CWE-79': [
137
+ { label: 'innerHTML-write', re: /\.innerHTML\s*=\s*[^;]+/g,
138
+ suppress: (m) => /\bDOMPurify\b|\bsanitize\b/.test(m),
139
+ suppressReason: 'sanitizer-present' },
140
+ ],
141
+ };
@@ -0,0 +1,172 @@
1
+ // Advisor-tone PR comment renderer (v0.72).
2
+ //
3
+ // Replaces the typical "12 findings detected, see SARIF" wall of text
4
+ // with a single security-advisor's note:
5
+ //
6
+ // "I noticed you added /api/admin/users in this PR. I checked 4 things:
7
+ // ✓ auth (route is behind requireAdmin)
8
+ // ⚠ rate-limit (no rateLimit() middleware found)
9
+ // ✓ input validation (express-validator chain present)
10
+ // ✓ audit log (logger.security() called on success path)
11
+ //
12
+ // The rate-limit gap matters because /admin endpoints are common
13
+ // credential-stuffing targets. A 5-line fix:
14
+ //
15
+ // ```js
16
+ // import rateLimit from 'express-rate-limit';
17
+ // app.use('/api/admin', rateLimit({ windowMs: 15*60_000, max: 30 }));
18
+ // ```"
19
+ //
20
+ // The pure-narrative format optimizes for SCREENSHOTABILITY — engineers
21
+ // share security-tool comments that read like a person, not a table.
22
+ //
23
+ // Generation is deterministic (no LLM) for the v1: we render from the
24
+ // delta + a per-CWE narrative template. A future v2 could optionally
25
+ // route through an LLM for richer prose when AGENTIC_SECURITY_LLM_ENDPOINT
26
+ // is configured.
27
+
28
+ const SEVERITY_GLYPH = {
29
+ critical: '🟥',
30
+ high: '🟧',
31
+ medium: '🟨',
32
+ low: '🟦',
33
+ info: '⬜',
34
+ };
35
+
36
+ const CWE_NARRATIVE = {
37
+ 'CWE-89': { name: 'SQL injection', why: 'A malicious payload like `1\' OR 1=1--` would dump every row.' },
38
+ 'CWE-79': { name: 'XSS', why: 'An attacker who controls this string can execute JavaScript in another user\'s browser.' },
39
+ 'CWE-78': { name: 'Command injection', why: 'A `;rm -rf /` style payload would run with the privileges of your service account.' },
40
+ 'CWE-22': { name: 'path traversal', why: 'A `../../etc/passwd` style payload would read files outside the intended directory.' },
41
+ 'CWE-918': { name: 'SSRF', why: 'An attacker can pivot to the cloud metadata endpoint (`169.254.169.254`) or internal services.' },
42
+ 'CWE-502': { name: 'insecure deserialization', why: 'A crafted payload triggers gadget chains — typically remote code execution.' },
43
+ 'CWE-611': { name: 'XXE', why: 'A malicious DOCTYPE can exfiltrate local files or trigger SSRF via entity expansion.' },
44
+ 'CWE-94': { name: 'template injection', why: 'A `{{7*7}}` style payload escapes to the template engine\'s expression evaluator (often RCE).' },
45
+ 'CWE-1321': { name: 'prototype pollution', why: 'A `__proto__` injection can alter the behavior of every downstream object check.' },
46
+ 'CWE-352': { name: 'CSRF', why: 'A cross-origin form can trigger authenticated state changes without the user\'s knowledge.' },
47
+ 'CWE-601': { name: 'open redirect', why: 'Trusted-domain bouncer for phishing — credentials get harvested on the second hop.' },
48
+ 'CWE-113': { name: 'HTTP response splitting', why: 'CR/LF injection lets an attacker forge an entire response, including cache poisoning.' },
49
+ 'CWE-798': { name: 'hardcoded secret', why: 'Committed credentials are scraped by automated scanners minutes after a push.' },
50
+ 'CWE-327': { name: 'weak crypto', why: 'Modern adversaries can crack MD5/SHA-1/RC4/3DES at practical cost.' },
51
+ 'CWE-1333': { name: 'regex DoS', why: 'A pathological input freezes the worker for seconds — DoS without much effort.' },
52
+ 'CWE-90': { name: 'LDAP injection', why: 'An attacker can extend the filter to enumerate users or bypass auth.' },
53
+ 'CWE-643': { name: 'XPath injection', why: 'A `\' or \'1\'=\'1` payload can return data the query never intended to expose.' },
54
+ 'CWE-1336': { name: 'prompt injection', why: 'Untrusted text in the LLM context can override your system prompt.' },
55
+ 'CWE-269': { name: 'privilege escalation', why: 'A low-privilege actor can reach a higher-privilege capability through this surface.' },
56
+ };
57
+
58
+ function _topCwes(findings, n = 3) {
59
+ const counts = new Map();
60
+ for (const f of findings) {
61
+ const k = f.cwe || 'unknown';
62
+ counts.set(k, (counts.get(k) || 0) + 1);
63
+ }
64
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, n);
65
+ }
66
+
67
+ function _route(f) {
68
+ // Best-effort: pull a route-ish string from the file path / vuln text.
69
+ // Many SAST findings carry the route via `f.route` or in the vuln name.
70
+ if (f.route) return f.route;
71
+ const m = (f.vuln || '').match(/\b(GET|POST|PUT|DELETE|PATCH)\s+([\/\w:-]+)/i);
72
+ if (m) return `${m[1].toUpperCase()} ${m[2]}`;
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Top-level renderer. Takes a delta produced by `computePrDelta` and
78
+ * emits a Markdown string suitable for posting as a single PR comment.
79
+ *
80
+ * Mode is chosen automatically:
81
+ * - "clean": no introduced + (resolved > 0 || persistent > 0)
82
+ * - "trivial": no introduced + nothing resolved
83
+ * - "needs-work": one or more introduced findings
84
+ */
85
+ export function renderPrComment(delta, { repoName, prNumber, prTitle } = {}) {
86
+ if (!delta) return '_no security delta available_';
87
+ const intro = delta.introduced || [];
88
+ const resolved = delta.resolved || [];
89
+ const shifted = delta.shifted || [];
90
+ const heading = repoName && prNumber
91
+ ? `### 🛡 agentic-security on ${repoName}#${prNumber}`
92
+ : `### 🛡 agentic-security`;
93
+ if (intro.length === 0 && resolved.length === 0 && shifted.length === 0) {
94
+ return [
95
+ heading,
96
+ ``,
97
+ `No security delta from \`${delta.baseRef}\` → \`${delta.headRef}\`. ` +
98
+ `${delta.changedFiles.length} file${delta.changedFiles.length === 1 ? '' : 's'} touched; ` +
99
+ `nothing in those changes introduced or resolved a finding. **Safe to merge.**`,
100
+ ``,
101
+ `<sub>Scanned ${delta.head?.summary?.total ?? 0} pre-existing findings on the head ref ` +
102
+ `(${delta.base?.summary?.total ?? 0} on base) — none of them moved in this PR.</sub>`,
103
+ ].join('\n');
104
+ }
105
+ if (intro.length === 0 && (resolved.length || shifted.length)) {
106
+ const r = delta.summary?.resolved || {};
107
+ return [
108
+ heading,
109
+ ``,
110
+ `This PR **resolves** ${resolved.length} finding${resolved.length === 1 ? '' : 's'}` +
111
+ (r.critical || r.high ? ` (including ${r.critical} critical + ${r.high} high)` : '') +
112
+ ` and introduces **none**. Nice cleanup work — safe to merge. ✨`,
113
+ ``,
114
+ `<sub>${delta.changedFiles.length} file${delta.changedFiles.length === 1 ? '' : 's'} ` +
115
+ `touched between \`${delta.baseRef}\` and \`${delta.headRef}\`.</sub>`,
116
+ ].join('\n');
117
+ }
118
+ // needs-work mode: narrative + per-finding paragraphs.
119
+ const lines = [];
120
+ lines.push(heading);
121
+ lines.push('');
122
+ const topCwes = _topCwes(intro);
123
+ const cweSummary = topCwes.map(([cwe, n]) => {
124
+ const meta = CWE_NARRATIVE[cwe];
125
+ return meta ? `${n} ${meta.name}` : `${n} ${cwe}`;
126
+ }).join(', ');
127
+ const hint = prTitle ? ` in "${prTitle}"` : '';
128
+ lines.push(`I looked at the ${delta.changedFiles.length} file${delta.changedFiles.length === 1 ? '' : 's'} ` +
129
+ `you changed${hint} and noticed **${intro.length} new finding${intro.length === 1 ? '' : 's'}** ` +
130
+ `that wasn't on \`${delta.baseRef}\`. Top concerns: ${cweSummary}.`);
131
+ lines.push('');
132
+ // Per-introduced-finding paragraph (cap at 5 for readability).
133
+ const SHOW = intro.slice(0, 5);
134
+ for (const f of SHOW) {
135
+ const meta = CWE_NARRATIVE[f.cwe];
136
+ const sev = SEVERITY_GLYPH[f.severity] || '⬜';
137
+ const route = _route(f);
138
+ const where = route ? `\`${route}\` (\`${f.file}:${f.line}\`)` : `\`${f.file}:${f.line}\``;
139
+ lines.push(`${sev} **${meta?.name || f.vuln}** — ${where}`);
140
+ if (meta) lines.push(` > ${meta.why}`);
141
+ if (f.remediation) {
142
+ const onelineFix = String(f.remediation).split('\n')[0].slice(0, 240);
143
+ lines.push(` Suggested fix: ${onelineFix}`);
144
+ }
145
+ if (f.confidence != null && f.confidence < 0.7) {
146
+ lines.push(` <sub>Lower confidence (${(f.confidence * 100).toFixed(0)}%) — may be a false positive worth a quick look.</sub>`);
147
+ }
148
+ lines.push('');
149
+ }
150
+ if (intro.length > SHOW.length) {
151
+ lines.push(`<sub>+${intro.length - SHOW.length} more new finding${intro.length - SHOW.length === 1 ? '' : 's'} — see the scan output for the full list.</sub>`);
152
+ lines.push('');
153
+ }
154
+ if (resolved.length) {
155
+ lines.push(`On the bright side: this PR **resolved ${resolved.length} pre-existing finding${resolved.length === 1 ? '' : 's'}**. 👏`);
156
+ lines.push('');
157
+ }
158
+ const i = delta.summary?.introduced || {};
159
+ const blockMerge = (i.critical || 0) + (i.high || 0) > 0;
160
+ if (blockMerge) {
161
+ lines.push(`---`);
162
+ lines.push(`**Blocking merge:** ${i.critical || 0} critical + ${i.high || 0} high severity ` +
163
+ `finding${(i.critical || 0) + (i.high || 0) === 1 ? '' : 's'} introduced. ` +
164
+ `Fix or suppress with \`// agentic-security-ignore: <rule-id>\` before merging.`);
165
+ } else {
166
+ lines.push(`---`);
167
+ lines.push(`Non-blocking: no critical/high severity findings introduced — but consider addressing the items above before this becomes harder to revisit.`);
168
+ }
169
+ return lines.join('\n');
170
+ }
171
+
172
+ export const _internal = { CWE_NARRATIVE, SEVERITY_GLYPH, _topCwes };