@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,198 @@
1
+ // Rule pack overrides (R9). Pro users edit .agentic-security/rules.yml to:
2
+ // - severityOverrides: per-rule severity remap
3
+ // - disable: list of rule vuln strings or rule IDs to skip entirely
4
+ // - custom: user-defined regex rules (with vuln/severity/cwe/fix metadata)
5
+ // - version: pin to a specific scanner version for reproducibility
6
+ //
7
+ // Engine integration: scanner consults this module after producing findings.
8
+ // `applyOverrides(findings, scanRoot)` returns a filtered/remapped list.
9
+
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as yaml from 'js-yaml';
13
+ import { verifyLastScan } from './integrity.js';
14
+
15
+ const OVERRIDES_PATH = '.agentic-security/rules.yml';
16
+
17
+ function _path(scanRoot) {
18
+ return path.join(scanRoot || process.cwd(), OVERRIDES_PATH);
19
+ }
20
+
21
+ export function loadOverrides(scanRoot) {
22
+ const fp = _path(scanRoot);
23
+ if (!fs.existsSync(fp)) return {};
24
+ try {
25
+ const raw = yaml.load(fs.readFileSync(fp, 'utf8')) || {};
26
+ return {
27
+ version: raw.version || null,
28
+ severityOverrides: raw.severityOverrides || {},
29
+ disable: Array.isArray(raw.disable) ? raw.disable : [],
30
+ custom: Array.isArray(raw.custom) ? raw.custom : [],
31
+ ignorePaths: Array.isArray(raw.ignorePaths) ? raw.ignorePaths : [],
32
+ };
33
+ } catch (_) { return {}; }
34
+ }
35
+
36
+ // Validate the user's rules.yml. Returns { ok, errors[] }.
37
+ export function validateOverrides(scanRoot) {
38
+ const errors = [];
39
+ const o = loadOverrides(scanRoot);
40
+ if (o.severityOverrides) {
41
+ for (const [vuln, sev] of Object.entries(o.severityOverrides)) {
42
+ if (!['critical', 'high', 'medium', 'low', 'info'].includes(sev)) {
43
+ errors.push(`severityOverrides["${vuln}"]: invalid severity "${sev}"`);
44
+ }
45
+ }
46
+ }
47
+ if (o.custom) {
48
+ for (let i = 0; i < o.custom.length; i++) {
49
+ const c = o.custom[i];
50
+ if (!c.id) errors.push(`custom[${i}]: missing id`);
51
+ if (!c.regex) errors.push(`custom[${i}]: missing regex`);
52
+ else { try { new RegExp(c.regex); } catch (e) { errors.push(`custom[${i}]: bad regex: ${e.message}`); } }
53
+ if (!c.vuln) errors.push(`custom[${i}]: missing vuln`);
54
+ if (!c.severity) errors.push(`custom[${i}]: missing severity`);
55
+ }
56
+ }
57
+ return { ok: errors.length === 0, errors };
58
+ }
59
+
60
+ // Premortem #4 — gate disable: entries on a sibling signature or explicit
61
+ // opt-out. `disable:` silently suppresses rules; without a guard, a PR that
62
+ // adds `disable: [cmd-injection-nodejs]` ships and the scanner stops firing
63
+ // on that family. By default we now REFUSE to honor `disable:` unless:
64
+ // 1. .agentic-security/rules.yml.sig exists and verifies the file body
65
+ // with the same HMAC key the rest of the engine uses, OR
66
+ // 2. AGENTIC_SECURITY_RULES_UNSIGNED=1 is set (developer escape hatch —
67
+ // this should NEVER be set in CI for an outside contribution).
68
+ // `severityOverrides`, `custom`, and `ignorePaths` remain in effect either
69
+ // way: they don't reduce coverage, they only add or remap.
70
+ function _disableAllowed(scanRoot) {
71
+ if (process.env.AGENTIC_SECURITY_RULES_UNSIGNED === '1') return { ok: true, reason: 'unsigned-opt-in' };
72
+ const fp = _path(scanRoot);
73
+ if (!fs.existsSync(fp)) return { ok: true, reason: 'no-rules-file' };
74
+ let body;
75
+ try { body = fs.readFileSync(fp, 'utf8'); }
76
+ catch { return { ok: false, reason: 'rules-unreadable' }; }
77
+ const sigPath = fp + '.sig';
78
+ if (!fs.existsSync(sigPath)) return { ok: false, reason: 'no-signature' };
79
+ const ok = verifyLastScan(body, sigPath);
80
+ return { ok: ok === true, reason: ok === true ? 'signed' : 'bad-signature' };
81
+ }
82
+
83
+ // Apply severity overrides + disable filter to a finding list.
84
+ // Harness-engineering note (post-derived): validate the overrides file BEFORE
85
+ // honoring any of it. If the YAML has a malformed severity, a broken custom
86
+ // regex, or any other syntax error, refuse the entire file and surface the
87
+ // reason on stderr. The previous shape "load, ignore broken entries, apply
88
+ // the rest" is exactly the silent-failure mode the post warns against.
89
+ export function applyOverrides(findings, scanRoot) {
90
+ const o = loadOverrides(scanRoot);
91
+ if (!o || (!o.severityOverrides && !o.disable?.length)) return findings;
92
+ const v = validateOverrides(scanRoot);
93
+ if (!v.ok) {
94
+ if (!globalThis.__as_overrides_warned) {
95
+ globalThis.__as_overrides_warned = true;
96
+ try {
97
+ process.stderr.write(`agentic-security: ignoring .agentic-security/rules.yml — validation failed:\n`);
98
+ for (const e of v.errors) process.stderr.write(` · ${e}\n`);
99
+ process.stderr.write(` Fix the errors above, or remove rules.yml. Findings will be returned unfiltered.\n`);
100
+ } catch {}
101
+ }
102
+ return findings;
103
+ }
104
+ let disable;
105
+ if (o.disable?.length) {
106
+ const gate = _disableAllowed(scanRoot);
107
+ if (gate.ok) {
108
+ disable = new Set(o.disable);
109
+ } else {
110
+ // Refused. Surface the reason on stderr once per process so operators
111
+ // who *meant* to disable a rule see why it didn't take.
112
+ if (!globalThis.__as_disable_warned) {
113
+ globalThis.__as_disable_warned = true;
114
+ try {
115
+ process.stderr.write(`agentic-security: ignoring 'disable:' in rules.yml — ${gate.reason}. Sign rules.yml with \`agentic-security rules sign\` or set AGENTIC_SECURITY_RULES_UNSIGNED=1 to opt in.\n`);
116
+ } catch {}
117
+ }
118
+ disable = new Set();
119
+ }
120
+ } else {
121
+ disable = new Set();
122
+ }
123
+ const sevMap = o.severityOverrides || {};
124
+ return findings
125
+ .filter(f => !disable.has(f.vuln) && !disable.has(f.id))
126
+ .map(f => sevMap[f.vuln] ? { ...f, severity: sevMap[f.vuln] } : f);
127
+ }
128
+
129
+ // Cache: compiled custom rules per scanRoot. Validated at first call;
130
+ // subsequent calls for the same scan reuse the compiled regexes.
131
+ const _compiledCustomRules = new Map(); // scanRoot → { compiled[], errors[] }
132
+
133
+ function _compileCustomRules(scanRoot) {
134
+ if (_compiledCustomRules.has(scanRoot)) return _compiledCustomRules.get(scanRoot);
135
+ const o = loadOverrides(scanRoot);
136
+ const result = { compiled: [], errors: [] };
137
+ if (!o.custom || !o.custom.length) {
138
+ _compiledCustomRules.set(scanRoot, result);
139
+ return result;
140
+ }
141
+ for (let i = 0; i < o.custom.length; i++) {
142
+ const rule = o.custom[i];
143
+ if (!rule.id) { result.errors.push(`custom[${i}]: missing id`); continue; }
144
+ if (!rule.regex) { result.errors.push(`custom[${i}] (${rule.id}): missing regex`); continue; }
145
+ if (!rule.vuln) { result.errors.push(`custom[${i}] (${rule.id}): missing vuln`); continue; }
146
+ if (!rule.severity) { result.errors.push(`custom[${i}] (${rule.id}): missing severity`); continue; }
147
+ try {
148
+ const re = new RegExp(rule.regex, rule.flags || 'g');
149
+ result.compiled.push({ rule, re });
150
+ } catch (e) {
151
+ result.errors.push(`custom[${i}] (${rule.id}): bad regex: ${e.message}`);
152
+ }
153
+ }
154
+ _compiledCustomRules.set(scanRoot, result);
155
+ return result;
156
+ }
157
+
158
+ // Run user-defined custom regex rules against a file. Returns custom findings.
159
+ //
160
+ // Harness-engineering note (post-derived): rules are compiled and validated
161
+ // at first invocation per scan; if ANY rule fails to compile, that rule is
162
+ // excluded AND surfaced via stderr — never silently skipped per-call.
163
+ export function runCustomRules(filePath, fileContent, scanRoot) {
164
+ const { compiled, errors } = _compileCustomRules(scanRoot);
165
+ if (errors.length && !globalThis.__as_custom_rules_warned) {
166
+ globalThis.__as_custom_rules_warned = true;
167
+ try {
168
+ process.stderr.write(`agentic-security: ${errors.length} custom rule(s) in .agentic-security/rules.yml failed to compile and were skipped:\n`);
169
+ for (const e of errors) process.stderr.write(` · ${e}\n`);
170
+ } catch {}
171
+ }
172
+ if (!compiled.length) return [];
173
+ const lines = fileContent.split('\n');
174
+ const out = [];
175
+ for (const { rule, re } of compiled) {
176
+ re.lastIndex = 0; // each file starts fresh for global regexes
177
+ let m;
178
+ while ((m = re.exec(fileContent)) !== null) {
179
+ const lineNum = fileContent.substring(0, m.index).split('\n').length;
180
+ out.push({
181
+ id: `custom:${rule.id}:${filePath}:${lineNum}`,
182
+ vuln: rule.vuln,
183
+ severity: rule.severity,
184
+ cwe: rule.cwe || '',
185
+ stride: rule.stride || '',
186
+ file: filePath,
187
+ line: lineNum,
188
+ snippet: lines[lineNum - 1]?.trim() || m[0],
189
+ fix: rule.fix || '',
190
+ description: rule.description || '',
191
+ custom: true,
192
+ parser: 'CUSTOM_RULE',
193
+ });
194
+ if (!re.global) break;
195
+ }
196
+ }
197
+ return out;
198
+ }
@@ -0,0 +1,209 @@
1
+ // Signed rule-pack verification (Sentinel-parity PRD FR-DSL-2; hardened in
2
+ // premortems R3.1, 2R3.1, 2R3.2).
3
+ //
4
+ // Threat model: a malicious PR drops a `.agentic-security/rules/foo.yml`
5
+ // into the repo. The next scanner run loads it and:
6
+ // - The rule's regex contains a ReDoS payload → hangs CI.
7
+ // - The rule fires custom-rule findings with attacker-controlled fix
8
+ // replacement strings → potential supply-chain attack via /fix.
9
+ // - The rule's llm_validate prompt exfiltrates context to an attacker
10
+ // endpoint.
11
+ //
12
+ // Defense (multi-layer):
13
+ //
14
+ // 1. Every rule file must be Ed25519-signed; the signature lives at
15
+ // `<rulefile>.sig` (raw 64 bytes).
16
+ //
17
+ // 2. The TRUST ROOT is bundled with the scanner code (BUNDLED_OFFICIAL_KEYS
18
+ // below), NOT read from the project tree by default. An attacker who
19
+ // can drop a `.agentic-security/trusted-keys.json` cannot bootstrap
20
+ // their own key into trust. Project-local keys are honored ONLY when
21
+ // AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1 (audit-logged).
22
+ //
23
+ // 3. Keys carry an optional `revokedAt` timestamp. A signature is rejected
24
+ // when the rule-file's mtime postdates the revocation, OR the signature's
25
+ // SHA-256 hash appears in the project's `crl[]` array.
26
+ //
27
+ // 4. Unsigned packs refused unless AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1
28
+ // (audit-logged + findings tagged `_unsigned: true`).
29
+
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import * as crypto from 'node:crypto';
33
+
34
+ const TRUSTED_KEYS_FILE = '.agentic-security/trusted-keys.json';
35
+
36
+ // Built-in trust root. These are the keys the maintainers of agentic-security
37
+ // use to sign official rule packs. Production deployment requires the
38
+ // maintainers to generate a real keypair, distribute the private key offline,
39
+ // and ship the corresponding public key here on a release. Until then the
40
+ // effective behavior is "no official keys, unsigned-only via the opt-in env."
41
+ export const BUNDLED_OFFICIAL_KEYS = [
42
+ // {
43
+ // id: 'agentic-security-official-2026-q1',
44
+ // alg: 'ed25519',
45
+ // publicKey: '<base64-32-bytes>',
46
+ // issuedAt: '2026-01-01T00:00:00Z',
47
+ // revokedAt: null,
48
+ // },
49
+ ];
50
+
51
+ function _trustedKeysPath(scanRoot) {
52
+ return path.join(scanRoot || process.cwd(), TRUSTED_KEYS_FILE);
53
+ }
54
+
55
+ // Load the EFFECTIVE trusted-key set. Composition:
56
+ // 1. Always: BUNDLED_OFFICIAL_KEYS (built into the scanner code).
57
+ // 2. When AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1: union with
58
+ // .agentic-security/trusted-keys.json from the project tree (logged).
59
+ //
60
+ // CRL: trusted-keys.json may also carry a top-level `crl` array of revoked
61
+ // signature hashes (sha256 of the signature bytes). These apply project-
62
+ // locally regardless of opt-in.
63
+ export function loadTrustedKeys(scanRoot) {
64
+ const keys = [...BUNDLED_OFFICIAL_KEYS];
65
+ let projectCrl = [];
66
+ const fp = _trustedKeysPath(scanRoot);
67
+ if (fs.existsSync(fp)) {
68
+ let data;
69
+ try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { data = null; }
70
+ if (data && Array.isArray(data.crl)) projectCrl = data.crl.filter(x => typeof x === 'string');
71
+ if (data && Array.isArray(data.keys)) {
72
+ if (process.env.AGENTIC_SECURITY_ALLOW_PROJECT_KEYS === '1') {
73
+ console.error('agentic-security: WARNING — project-local trusted-keys.json honored (AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1). An attacker who can write to .agentic-security/ can bypass signing — use only on trusted workstations.');
74
+ for (const k of data.keys) {
75
+ if (k && k.publicKey && k.alg === 'ed25519') keys.push(k);
76
+ }
77
+ } else if (data.keys.length > 0) {
78
+ console.error('agentic-security: ignoring project-local trusted-keys.json (set AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1 to honor; audit-logged).');
79
+ }
80
+ }
81
+ }
82
+ Object.defineProperty(keys, '_crl', { value: projectCrl, enumerable: false });
83
+ return keys;
84
+ }
85
+
86
+ // Pass-through warning issued at most once per process.
87
+ let _passThroughWarned = false;
88
+
89
+ // Verify a rule-pack file. Returns one of:
90
+ // { ok: true, keyId: '<id>' } // signature valid
91
+ // { ok: true, passThrough: true } // bundled trust root empty AND no project keys — pass-through (premortem 3R3.1)
92
+ // { ok: false, reason: 'unsigned', allowUnsigned: bool } // no sig file
93
+ // { ok: false, reason: 'bad-signature' } // sig present but invalid
94
+ // { ok: false, reason: 'no-trusted-keys' } // no keys configured (no bundled, no project)
95
+ // { ok: false, reason: 'revoked-key', keyId } // key revoked + rule mtime > revokedAt
96
+ // { ok: false, reason: 'revoked-signature' } // signature SHA in project CRL
97
+ // { ok: false, reason: 'read-error' }
98
+ //
99
+ // PREMORTEM 3R3.1 — pass-through mode. When BUNDLED_OFFICIAL_KEYS is empty
100
+ // (today's reality during product bring-up) AND the operator has not opted
101
+ // into project-local keys, REFUSING every rule pack trains operators to set
102
+ // AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1 permanently — which recreates the
103
+ // exact threat model signing was supposed to defend. Instead we accept
104
+ // rule packs in pass-through mode with a one-time warning + a _passThrough
105
+ // flag on each accepted rule. Operators get visibility AND the gate doesn't
106
+ // train them to bypass.
107
+ export function verifyRulePack(rulePackPath, trustedKeys) {
108
+ const sigPath = rulePackPath + '.sig';
109
+ if (!fs.existsSync(sigPath)) {
110
+ return { ok: false, reason: 'unsigned', allowUnsigned: process.env.AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS === '1' };
111
+ }
112
+ if (!Array.isArray(trustedKeys) || trustedKeys.length === 0) {
113
+ // Pass-through mode: empty bundled trust root + no project keys.
114
+ // Issue ONE warning per process, then accept with passThrough flag.
115
+ //
116
+ // Premortem 4R-1: CI mode is fail-closed. CI environments are the place
117
+ // where supply-chain compromise gets weaponized, and the per-session stderr
118
+ // warning is invisible there. So when CI=true (or any common CI env-var is
119
+ // set), refuse pass-through entirely. Operators can opt-in by setting
120
+ // AGENTIC_SECURITY_ALLOW_PASSTHROUGH_IN_CI=1 — making the bypass an
121
+ // intentional, auditable decision rather than the silent default.
122
+ const inCi = !!(
123
+ process.env.CI ||
124
+ process.env.GITHUB_ACTIONS ||
125
+ process.env.GITLAB_CI ||
126
+ process.env.BUILDKITE ||
127
+ process.env.CIRCLECI ||
128
+ process.env.JENKINS_URL ||
129
+ process.env.TF_BUILD
130
+ );
131
+ const allowPassThroughInCi = process.env.AGENTIC_SECURITY_ALLOW_PASSTHROUGH_IN_CI === '1';
132
+ if (BUNDLED_OFFICIAL_KEYS.length === 0 && process.env.AGENTIC_SECURITY_STRICT_SIGNING !== '1') {
133
+ if (inCi && !allowPassThroughInCi) {
134
+ return {
135
+ ok: false,
136
+ reason: 'pass-through-disabled-in-ci',
137
+ remediation: 'CI run detected with no signing keys configured. Either (a) set AGENTIC_SECURITY_ALLOW_PASSTHROUGH_IN_CI=1 to accept unsigned rule packs in this CI run, or (b) configure project keys in .agentic-security/trusted-keys.json and set AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1.',
138
+ };
139
+ }
140
+ if (!_passThroughWarned) {
141
+ _passThroughWarned = true;
142
+ console.error('agentic-security: signed-rule-pack defense in PASS-THROUGH mode.');
143
+ console.error(' · No bundled official keys are baked into this release.');
144
+ console.error(' · Rule packs will be ACCEPTED, tagged _passThroughSigning:true.');
145
+ console.error(' · To switch to refuse-mode set AGENTIC_SECURITY_STRICT_SIGNING=1.');
146
+ console.error(' · To honor project-local trusted-keys.json, set AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1.');
147
+ }
148
+ return { ok: true, passThrough: true };
149
+ }
150
+ return { ok: false, reason: 'no-trusted-keys' };
151
+ }
152
+ let body, sig, ruleMtime;
153
+ try {
154
+ body = fs.readFileSync(rulePackPath);
155
+ sig = fs.readFileSync(sigPath);
156
+ ruleMtime = fs.statSync(rulePackPath).mtime;
157
+ } catch { return { ok: false, reason: 'read-error' }; }
158
+ // CRL check first — independent of which key signed.
159
+ const sigHash = crypto.createHash('sha256').update(sig).digest('hex');
160
+ const crl = trustedKeys._crl || [];
161
+ if (crl.includes(sigHash)) return { ok: false, reason: 'revoked-signature' };
162
+ for (const k of trustedKeys) {
163
+ try {
164
+ const keyBytes = Buffer.from(k.publicKey, 'base64');
165
+ if (keyBytes.length !== 32) continue;
166
+ const keyObj = crypto.createPublicKey({
167
+ key: { kty: 'OKP', crv: 'Ed25519', x: keyBytes.toString('base64url') },
168
+ format: 'jwk',
169
+ });
170
+ const valid = crypto.verify(null, body, keyObj, sig);
171
+ if (!valid) continue;
172
+ // Signature is valid by this key. Check revocation.
173
+ if (k.revokedAt) {
174
+ const revokedAt = new Date(k.revokedAt);
175
+ if (Number.isFinite(revokedAt.getTime()) && ruleMtime > revokedAt) {
176
+ return { ok: false, reason: 'revoked-key', keyId: k.id || '(unnamed)' };
177
+ }
178
+ }
179
+ return { ok: true, keyId: k.id || '(unnamed)' };
180
+ } catch { /* try next key */ }
181
+ }
182
+ return { ok: false, reason: 'bad-signature' };
183
+ }
184
+
185
+ // CLI helper — generate an Ed25519 key pair. Returns { publicKey, privateKey }
186
+ // as base64 strings.
187
+ export function keygen() {
188
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
189
+ const pubRaw = publicKey.export({ format: 'jwk' }).x; // base64url
190
+ const privRaw = privateKey.export({ format: 'jwk' }).d; // base64url
191
+ return {
192
+ publicKey: Buffer.from(pubRaw, 'base64url').toString('base64'),
193
+ privateKey: Buffer.from(privRaw, 'base64url').toString('base64'),
194
+ };
195
+ }
196
+
197
+ // Sign a rule-pack file. Writes <path>.sig as raw 64 bytes.
198
+ export function signRulePack(rulePackPath, privateKeyB64) {
199
+ const body = fs.readFileSync(rulePackPath);
200
+ const privBytes = Buffer.from(privateKeyB64, 'base64');
201
+ if (privBytes.length !== 32) throw new Error('private key must be 32 bytes (raw ed25519)');
202
+ const keyObj = crypto.createPrivateKey({
203
+ key: { kty: 'OKP', crv: 'Ed25519', d: privBytes.toString('base64url') },
204
+ format: 'jwk',
205
+ });
206
+ const sig = crypto.sign(null, body, keyObj);
207
+ fs.writeFileSync(rulePackPath + '.sig', sig);
208
+ return sig;
209
+ }
@@ -0,0 +1,143 @@
1
+ // Curated rule packs. Each pack is a focused view of the engine's findings
2
+ // based on a CWE allowlist. Activated via `--pack <name>` on the CLI.
3
+ //
4
+ // Packs do not change *detection* — they filter the scan output to the rules
5
+ // the user wants to focus on, and optionally bump severity for matching rules.
6
+
7
+ export const PACKS = {
8
+ 'owasp-top-10': {
9
+ description: 'OWASP Top 10 (2021) — A01 through A10',
10
+ cwes: [
11
+ // A01 Broken Access Control
12
+ 'CWE-22', 'CWE-200', 'CWE-269', 'CWE-284', 'CWE-352', 'CWE-639', 'CWE-732', 'CWE-862', 'CWE-863',
13
+ // A02 Cryptographic Failures
14
+ 'CWE-261', 'CWE-296', 'CWE-310', 'CWE-319', 'CWE-321', 'CWE-322', 'CWE-323', 'CWE-324',
15
+ 'CWE-325', 'CWE-326', 'CWE-327', 'CWE-328', 'CWE-329', 'CWE-330', 'CWE-331', 'CWE-335',
16
+ 'CWE-336', 'CWE-337', 'CWE-338', 'CWE-340', 'CWE-347', 'CWE-523', 'CWE-720', 'CWE-757',
17
+ 'CWE-759', 'CWE-760', 'CWE-780', 'CWE-818', 'CWE-916',
18
+ // A03 Injection
19
+ 'CWE-20', 'CWE-74', 'CWE-77', 'CWE-78', 'CWE-79', 'CWE-83', 'CWE-87', 'CWE-88', 'CWE-89',
20
+ 'CWE-90', 'CWE-91', 'CWE-93', 'CWE-94', 'CWE-95', 'CWE-96', 'CWE-97', 'CWE-98', 'CWE-99',
21
+ 'CWE-100', 'CWE-113', 'CWE-116', 'CWE-138', 'CWE-184', 'CWE-470', 'CWE-471', 'CWE-564',
22
+ 'CWE-610', 'CWE-643', 'CWE-644', 'CWE-652', 'CWE-917', 'CWE-1336', 'CWE-1427',
23
+ // A04 Insecure Design
24
+ 'CWE-209', 'CWE-256', 'CWE-501', 'CWE-522',
25
+ // A05 Security Misconfiguration
26
+ 'CWE-2', 'CWE-11', 'CWE-13', 'CWE-15', 'CWE-16', 'CWE-260', 'CWE-315', 'CWE-520',
27
+ 'CWE-526', 'CWE-537', 'CWE-541', 'CWE-547', 'CWE-611', 'CWE-614', 'CWE-756', 'CWE-776',
28
+ 'CWE-942', 'CWE-1004', 'CWE-1032', 'CWE-1174',
29
+ // A06 Vulnerable and Outdated Components
30
+ 'CWE-937', 'CWE-1035', 'CWE-1104',
31
+ // A07 Identification and Authentication Failures
32
+ 'CWE-255', 'CWE-259', 'CWE-287', 'CWE-288', 'CWE-290', 'CWE-294', 'CWE-295', 'CWE-297',
33
+ 'CWE-300', 'CWE-302', 'CWE-304', 'CWE-306', 'CWE-307', 'CWE-346', 'CWE-384', 'CWE-521',
34
+ 'CWE-613', 'CWE-620', 'CWE-640', 'CWE-798',
35
+ // A08 Software and Data Integrity Failures
36
+ 'CWE-345', 'CWE-353', 'CWE-426', 'CWE-494', 'CWE-502', 'CWE-565', 'CWE-784', 'CWE-829',
37
+ 'CWE-830', 'CWE-915',
38
+ // A09 Security Logging and Monitoring Failures
39
+ 'CWE-117', 'CWE-223', 'CWE-532', 'CWE-778',
40
+ // A10 Server-Side Request Forgery
41
+ 'CWE-918',
42
+ ],
43
+ },
44
+
45
+ 'cwe-top-25': {
46
+ description: 'CWE Top 25 Most Dangerous Software Weaknesses (2023)',
47
+ cwes: [
48
+ 'CWE-787', 'CWE-79', 'CWE-89', 'CWE-416', 'CWE-78', 'CWE-20', 'CWE-125', 'CWE-22',
49
+ 'CWE-352', 'CWE-434', 'CWE-862', 'CWE-476', 'CWE-287', 'CWE-190', 'CWE-502', 'CWE-77',
50
+ 'CWE-119', 'CWE-798', 'CWE-918', 'CWE-306', 'CWE-362', 'CWE-269', 'CWE-94', 'CWE-863',
51
+ 'CWE-276',
52
+ ],
53
+ },
54
+
55
+ 'llm-security': {
56
+ description: 'LLM / agent / prompt-injection risks (OWASP LLM Top 10)',
57
+ cwes: [
58
+ 'CWE-20', // Improper Input Validation (prompts)
59
+ 'CWE-74', // Injection family
60
+ 'CWE-77', // Command Injection (via tools)
61
+ 'CWE-78', // OS Command Injection (via tools)
62
+ 'CWE-79', // XSS (LLM-generated output)
63
+ 'CWE-94', // Code Injection
64
+ 'CWE-200', // Information Disclosure (model leakage)
65
+ 'CWE-285', // Improper Authorization (over-privileged agents)
66
+ 'CWE-285',
67
+ 'CWE-352', // CSRF (agent state)
68
+ 'CWE-494', // Download of Code Without Integrity Check (model weights)
69
+ 'CWE-502', // Insecure Deserialization (pickle / torch.load)
70
+ 'CWE-732', // Incorrect Permission Assignment (MCP tools)
71
+ 'CWE-798', // Hardcoded credentials (in prompts / tools)
72
+ 'CWE-918', // SSRF (LLM-controlled requests)
73
+ 'CWE-1336', // Improper Neutralization of Special Elements (template injection)
74
+ 'CWE-1357', // Reliance on Insufficiently Trustworthy Component
75
+ 'CWE-1427', // LLM prompt injection (CWE assigned 2025)
76
+ ],
77
+ },
78
+
79
+ 'supply-chain': {
80
+ description: 'Dependency vulnerabilities, dep-confusion, pipeline & container risks',
81
+ cwes: [
82
+ 'CWE-494', // Download of Code Without Integrity Check
83
+ 'CWE-502', // Insecure Deserialization
84
+ 'CWE-532', // Insertion of Sensitive Information into Log File
85
+ 'CWE-693', // Protection Mechanism Failure
86
+ 'CWE-829', // Inclusion of Functionality from Untrusted Control Sphere
87
+ 'CWE-830', // Inclusion of Web Functionality from Untrusted Source
88
+ 'CWE-915', // Improperly Controlled Modification of Dynamically-Determined Object Attributes
89
+ 'CWE-937', // Using Components with Known Vulnerabilities
90
+ 'CWE-1035', // OWASP A06 — Vulnerable and Outdated Components
91
+ 'CWE-1104', // Use of Unmaintained Third-Party Components
92
+ 'CWE-1188', // Insecure Default Initialization of Resource
93
+ 'CWE-1336', // Template Injection
94
+ 'CWE-1357', // Reliance on Insufficiently Trustworthy Component
95
+ 'CWE-272', // Least Privilege Violation (CI permissions)
96
+ 'CWE-78', // OS Command Injection (CI script injection)
97
+ ],
98
+ },
99
+ };
100
+
101
+ export function listPacks() {
102
+ return Object.entries(PACKS).map(([name, p]) => ({
103
+ name,
104
+ description: p.description,
105
+ cweCount: p.cwes.length,
106
+ }));
107
+ }
108
+
109
+ export function loadPack(name) {
110
+ const p = PACKS[name];
111
+ if (!p) {
112
+ const known = Object.keys(PACKS).join(', ');
113
+ throw new Error(`Unknown pack "${name}". Known: ${known}`);
114
+ }
115
+ return { name, ...p };
116
+ }
117
+
118
+ // Return the set of CWE IDs covered by the given pack names.
119
+ // Multiple packs union their CWE sets.
120
+ export function packsCweSet(names) {
121
+ const set = new Set();
122
+ for (const n of names) {
123
+ const p = loadPack(n);
124
+ for (const c of p.cwes) set.add(c);
125
+ }
126
+ return set;
127
+ }
128
+
129
+ // Filter a scan object's findings/secrets/supplyChain arrays to those whose CWE
130
+ // is present in the union of `packNames`' CWE sets. Returns a new scan object.
131
+ // If `packNames` is empty/falsy, returns the scan unchanged.
132
+ export function applyPacks(scan, packNames) {
133
+ if (!packNames || !packNames.length) return scan;
134
+ const cwes = packsCweSet(packNames);
135
+ const keep = (f) => f && f.cwe && cwes.has(f.cwe);
136
+ return {
137
+ ...scan,
138
+ findings: (scan.findings || []).filter(keep),
139
+ secrets: (scan.secrets || []).filter(keep),
140
+ logicVulns: (scan.logicVulns || []).filter(keep),
141
+ supplyChain: (scan.supplyChain || []).filter(keep),
142
+ };
143
+ }
@@ -0,0 +1,108 @@
1
+ // Auto-rule synthesis from repeated FPs (FR-LEARN-6).
2
+ //
3
+ // Reads `.agentic-security/triage-feedback.json` (populated by the /triage
4
+ // slash command). When 5+ findings sharing a similar shape get marked FP,
5
+ // propose a YAML suppression rule and write it to
6
+ // `.agentic-security/rules-proposed/auto-<timestamp>.yml`. The operator
7
+ // reviews and either drops it into `rules/` (active) or deletes it.
8
+ //
9
+ // Honest scope:
10
+ // - We propose, we don't auto-activate. The customer decides.
11
+ // - Similar-shape = same (rule_id or vuln-family) AND same file glob root.
12
+ // - Threshold = 5 occurrences by default (env override).
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+
17
+ const TRIAGE_PATH = path.join('.agentic-security', 'triage-feedback.json');
18
+ const PROPOSED_DIR = path.join('.agentic-security', 'rules-proposed');
19
+
20
+ const DEFAULT_FP_THRESHOLD = 5;
21
+
22
+ function _readTriage(scanRoot) {
23
+ const fp = path.join(scanRoot || process.cwd(), TRIAGE_PATH);
24
+ if (!fs.existsSync(fp)) return null;
25
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
26
+ }
27
+
28
+ function _shapeKey(entry) {
29
+ // Group by rule/family + the dir prefix (first two path segments) so we
30
+ // suggest rules scoped to the same module — never project-wide.
31
+ const fam = entry.family || entry.cwe || entry.vuln || 'unknown';
32
+ const file = entry.file || '';
33
+ const dir = file.split('/').slice(0, 2).join('/') || '.';
34
+ return `${fam}::${dir}`;
35
+ }
36
+
37
+ function _summarizeGroup(entries) {
38
+ const e0 = entries[0];
39
+ return {
40
+ family: e0.family || null,
41
+ rule: e0.cwe || e0.vuln || 'unknown',
42
+ dirGlob: (e0.file || '').split('/').slice(0, 2).join('/') + '/**',
43
+ count: entries.length,
44
+ examples: entries.slice(0, 3).map(e => `${e.file}:${e.line}`),
45
+ };
46
+ }
47
+
48
+ function _yamlProposal(group) {
49
+ const ruleId = `auto-suppress-${group.family || group.rule}-${Date.now().toString(36)}`;
50
+ return `# Auto-synthesised suppression proposal (FR-LEARN-6).
51
+ # Generated from ${group.count} false-positive verdicts on ${group.family || group.rule}
52
+ # in ${group.dirGlob}.
53
+ #
54
+ # Examples:
55
+ ${group.examples.map(e => '# - ' + e).join('\n')}
56
+ #
57
+ # Review carefully BEFORE moving into rules/. This is a PROPOSAL.
58
+
59
+ - id: ${ruleId}
60
+ title: "Auto-suppress: ${group.family || group.rule}"
61
+ description: "Repeated FP verdicts in ${group.dirGlob}"
62
+ shadow: true # never blocks CI; safe by default
63
+ match:
64
+ family: ${group.family || group.rule}
65
+ paths:
66
+ - "${group.dirGlob}"
67
+ action: suppress
68
+ `;
69
+ }
70
+
71
+ /**
72
+ * Public entry: scan the triage history and emit a proposal YAML for any
73
+ * group with ≥ threshold FP verdicts. Returns the list of proposals written.
74
+ */
75
+ export function synthesizeRules(scanRoot, opts = {}) {
76
+ const threshold = parseInt(opts.threshold || process.env.AGENTIC_SECURITY_RULE_SYNTHESIS_THRESHOLD || String(DEFAULT_FP_THRESHOLD), 10);
77
+ const triage = _readTriage(scanRoot);
78
+ if (!triage) return [];
79
+ // triage format (per v0.46): { verdicts: [{file, line, vuln, family, verdict, ...}] }
80
+ const verdicts = triage.verdicts || [];
81
+ const fps = verdicts.filter(v => v.verdict === 'fp' || v.verdict === 'false-positive');
82
+ if (!fps.length) return [];
83
+ const groups = new Map();
84
+ for (const e of fps) {
85
+ const k = _shapeKey(e);
86
+ if (!groups.has(k)) groups.set(k, []);
87
+ groups.get(k).push(e);
88
+ }
89
+ const proposals = [];
90
+ const dir = path.join(scanRoot || process.cwd(), PROPOSED_DIR);
91
+ for (const [, group] of groups) {
92
+ if (group.length < threshold) continue;
93
+ const summary = _summarizeGroup(group);
94
+ const yaml = _yamlProposal(summary);
95
+ const name = `auto-${(summary.family || summary.rule).replace(/[^a-zA-Z0-9_-]/g, '-')}-${Date.now().toString(36)}.yml`;
96
+ const fp = path.join(dir, name);
97
+ if (!opts.dryRun) {
98
+ try {
99
+ fs.mkdirSync(dir, { recursive: true });
100
+ fs.writeFileSync(fp, yaml);
101
+ } catch { /* non-fatal */ }
102
+ }
103
+ proposals.push({ ...summary, file: fp, yaml });
104
+ }
105
+ return proposals;
106
+ }
107
+
108
+ export const _internals = { DEFAULT_FP_THRESHOLD, TRIAGE_PATH, PROPOSED_DIR };