@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,110 @@
1
+ // FR-UX-11 — Pre-incident archaeology.
2
+ //
3
+ // Given a finding or a CWE, walk the git history of the affected file to
4
+ // answer "when did this codebase first become vulnerable to this CWE?"
5
+ // Outputs a structured timeline:
6
+ //
7
+ // {
8
+ // finding: { stableId, file, line, vuln },
9
+ // firstVulnerableCommit: { sha, author, ts, message } | null,
10
+ // vulnerableForDays: 145,
11
+ // concurrentSafeShapes: [{ sha, ts, snippet }], // earlier commits where the same file did NOT contain the bug
12
+ // introducingCommit: { sha, author, ts, message } | null,
13
+ // }
14
+ //
15
+ // We invoke `git log` and `git show` via the shell. If the project is not a
16
+ // git repository (no `.git` at root), we return `{ available: false }`.
17
+ //
18
+ // This is intentionally light: we do not re-run the SAST detector on every
19
+ // historical revision. We use a simple substring-presence probe — does the
20
+ // finding's sink snippet appear in the historical version of the file?
21
+ // Sufficient for the common case and dramatically cheaper than full
22
+ // re-scanning. Customers who want forensic-grade archaeology can re-run the
23
+ // scanner against `git checkout`-ed historical revisions.
24
+
25
+ import { execSync } from 'node:child_process';
26
+ import * as fs from 'node:fs';
27
+ import * as path from 'node:path';
28
+
29
+ function isGitRepo(root) {
30
+ try {
31
+ return fs.existsSync(path.join(root, '.git'));
32
+ } catch { return false; }
33
+ }
34
+
35
+ function gitLogForFile(root, file, limit = 50) {
36
+ try {
37
+ const out = execSync(
38
+ `git log --pretty=format:%H%x1f%an%x1f%aI%x1f%s --max-count=${limit} -- "${file}"`,
39
+ { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
40
+ );
41
+ return out.split(/\n/).filter(Boolean).map(line => {
42
+ const [sha, author, ts, message] = line.split('\x1f');
43
+ return { sha, author, ts, message };
44
+ });
45
+ } catch { return []; }
46
+ }
47
+
48
+ function gitShow(root, sha, file) {
49
+ try {
50
+ return execSync(`git show ${sha}:"${file}"`, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
51
+ } catch { return null; }
52
+ }
53
+
54
+ function probeSnippet(content, snippet) {
55
+ if (!content || !snippet) return false;
56
+ // Strip whitespace for a coarse match — historical reformatting shouldn't
57
+ // hide the presence of the same vulnerable expression.
58
+ const normContent = content.replace(/\s+/g, ' ');
59
+ const normSnippet = snippet.replace(/\s+/g, ' ').slice(0, 120);
60
+ return normContent.includes(normSnippet);
61
+ }
62
+
63
+ export function archaeologyForFinding(finding, scanRoot) {
64
+ const root = scanRoot || process.cwd();
65
+ if (!isGitRepo(root)) return { available: false, reason: 'not-a-git-repo' };
66
+ if (!finding || !finding.file) return { available: false, reason: 'no-file' };
67
+
68
+ const snippet = finding.sink?.snippet || finding.snippet || '';
69
+ if (!snippet) return { available: false, reason: 'no-snippet' };
70
+
71
+ const log = gitLogForFile(root, finding.file, 50);
72
+ if (!log.length) return { available: false, reason: 'no-history' };
73
+
74
+ // Walk oldest-first; find the first commit where the snippet is present.
75
+ const ordered = log.slice().reverse();
76
+ let firstVulnerable = null;
77
+ let lastSafe = null;
78
+ for (const c of ordered) {
79
+ const content = gitShow(root, c.sha, finding.file);
80
+ if (content === null) continue;
81
+ const present = probeSnippet(content, snippet);
82
+ if (present && !firstVulnerable) firstVulnerable = c;
83
+ if (!present) lastSafe = c;
84
+ }
85
+
86
+ // If snippet isn't present in any commit, treat as no archaeology available.
87
+ if (!firstVulnerable) return { available: false, reason: 'snippet-never-seen' };
88
+
89
+ // The "introducing commit" is firstVulnerable; if lastSafe is just before it,
90
+ // we have a clean delta. Otherwise the snippet may have been introduced,
91
+ // removed, reintroduced — present a partial answer.
92
+ const tsFirst = Date.parse(firstVulnerable.ts);
93
+ const tsNow = Date.now();
94
+ const vulnerableForDays = Number.isFinite(tsFirst) ? Math.floor((tsNow - tsFirst) / 86_400_000) : null;
95
+
96
+ return {
97
+ available: true,
98
+ finding: {
99
+ stableId: finding.stableId || null,
100
+ file: finding.file,
101
+ line: finding.line || 0,
102
+ vuln: finding.vuln || null,
103
+ },
104
+ firstVulnerableCommit: firstVulnerable,
105
+ introducingCommit: firstVulnerable,
106
+ lastSafeCommit: lastSafe,
107
+ vulnerableForDays,
108
+ historyLength: log.length,
109
+ };
110
+ }
@@ -0,0 +1,93 @@
1
+ // Persona profile: vibecoder | pro
2
+ // Loaded from .agentic-security/profile.yml. Used everywhere a default differs
3
+ // by audience (rendering verbosity, confidence threshold, command visibility,
4
+ // suppression schema, integration set, etc.).
5
+
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import * as yaml from 'js-yaml';
9
+
10
+ export const PROFILES = ['vibecoder', 'pro'];
11
+
12
+ export const DEFAULTS = {
13
+ vibecoder: {
14
+ profile: 'vibecoder',
15
+ confidenceMin: 0.9, // hide low-confidence findings
16
+ showTaxonomy: false, // no CWE/CVSS/STRIDE/OWASP/MITRE in default output
17
+ severityFloor: 'high', // by default only show high+critical
18
+ commandTier: 'primary', // /help shows ~5 commands
19
+ suppression: 'soft', // accepted.json, 30-day, no reviewer required
20
+ machineOutput: false, // don't write SARIF/JSON unless asked
21
+ onboardingPrompts: true,
22
+ showAttribution: true,
23
+ },
24
+ pro: {
25
+ profile: 'pro',
26
+ confidenceMin: 0.3, // show all but the lowest-signal findings
27
+ showTaxonomy: true, // CWE/CVSS/STRIDE/OWASP/MITRE visible by default
28
+ severityFloor: 'low', // show everything down to info
29
+ commandTier: 'all', // /help shows all commands
30
+ suppression: 'audit', // suppressions.yml, requires reason+reviewer+expiry
31
+ machineOutput: true, // SARIF + JSON every scan
32
+ onboardingPrompts: false,
33
+ showAttribution: true,
34
+ },
35
+ };
36
+
37
+ function _profilePath(scanRoot) {
38
+ return path.join(scanRoot || process.cwd(), '.agentic-security', 'profile.yml');
39
+ }
40
+
41
+ export function loadProfile(scanRoot) {
42
+ const fp = _profilePath(scanRoot);
43
+ let parsed = {};
44
+ try {
45
+ if (fs.existsSync(fp)) {
46
+ parsed = yaml.load(fs.readFileSync(fp, 'utf8')) || {};
47
+ }
48
+ } catch (_) { /* fall through */ }
49
+ const name = PROFILES.includes(parsed.profile) ? parsed.profile : 'vibecoder';
50
+ return { ...DEFAULTS[name], ...parsed, profile: name };
51
+ }
52
+
53
+ export function saveProfile(scanRoot, updates) {
54
+ const fp = _profilePath(scanRoot);
55
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
56
+ const current = loadProfile(scanRoot);
57
+ const next = { ...current, ...updates };
58
+ // Strip values equal to defaults so the file stays minimal.
59
+ const defaults = DEFAULTS[next.profile];
60
+ const out = {};
61
+ for (const k of Object.keys(next)) {
62
+ if (defaults[k] === next[k]) continue;
63
+ out[k] = next[k];
64
+ }
65
+ if (!('profile' in out)) out.profile = next.profile;
66
+ fs.writeFileSync(fp, yaml.dump(out));
67
+ return next;
68
+ }
69
+
70
+ // Detect profile heuristically from project state when no profile.yml exists.
71
+ // Returns 'pro' if the repo has signals indicating professional security work,
72
+ // otherwise 'vibecoder'. Run only on first scan.
73
+ export function detectProfile(scanRoot) {
74
+ const root = scanRoot || process.cwd();
75
+ const signals = ['SECURITY.md', '.github/workflows/security.yml', '.semgrep.yml',
76
+ '.snyk', 'codeql-config.yml', 'compliance/', 'docs/threat-model.md'];
77
+ for (const s of signals) {
78
+ if (fs.existsSync(path.join(root, s))) return 'pro';
79
+ }
80
+ // Repos with `security` or `compliance` in package.json description → pro.
81
+ try {
82
+ const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
83
+ if (/security|compliance|appsec|pentest/i.test(pkg.description || '')) return 'pro';
84
+ } catch (_) { /* no pkg or unreadable */ }
85
+ return 'vibecoder';
86
+ }
87
+
88
+ export const ATTRIBUTION = 'agentic-security · created by ClearCapabilities.Com';
89
+ export const ATTRIBUTION_URL = 'https://clearcapabilities.com';
90
+
91
+ export function renderAttributionLine() {
92
+ return `🛡 ${ATTRIBUTION} · ${ATTRIBUTION_URL}`;
93
+ }
@@ -0,0 +1,42 @@
1
+ // Reachability filter (FR-PREC-2).
2
+ //
3
+ // annotateReachability() in engine.js already sets f.reachable to true|false
4
+ // based on whether the finding sits in code reachable from an HTTP route. This
5
+ // module turns that signal into a precision lever: findings marked reachable=
6
+ // false are demoted to severity 'info' with f.unreachable = true.
7
+ //
8
+ // Disabled when scanRoot/--include-unreachable signals are present, or when
9
+ // AGENTIC_SECURITY_INCLUDE_UNREACHABLE=1 is set.
10
+
11
+ const SEVERITY_DEMOTE = {
12
+ critical: 'medium',
13
+ high: 'low',
14
+ medium: 'low',
15
+ low: 'info',
16
+ };
17
+
18
+ export function demoteUnreachable(findings, opts = {}) {
19
+ if (!Array.isArray(findings)) return;
20
+ if (opts.includeUnreachable || process.env.AGENTIC_SECURITY_INCLUDE_UNREACHABLE === '1') return;
21
+ // The reachability signal is only informative when the project HAS route
22
+ // handlers. A fixture file scanned in isolation has every finding marked
23
+ // reachable=false by annotateReachability(); demoting all of them would
24
+ // hide real bugs the user is trying to verify.
25
+ const haveRoutes = Array.isArray(opts.routes) ? opts.routes.length > 0 : false;
26
+ if (!haveRoutes) return;
27
+ for (const f of findings) {
28
+ if (!f || typeof f !== 'object') continue;
29
+ if (f.reachable !== false) continue;
30
+ if (f.type === 'vulnerable_dep') continue;
31
+ if (f.unreachable) continue;
32
+ // Source has an explicit HTTP/DOM/Form/URL category → engine is confident
33
+ // it's a user-input source even though no route was linked. Don't demote.
34
+ if (f.source && f.source.category && /HTTP|DOM|Form|URL|Query/i.test(f.source.category)) continue;
35
+ const before = f.severity;
36
+ const after = SEVERITY_DEMOTE[before];
37
+ if (!after || before === after) continue;
38
+ f.severity = after;
39
+ f.unreachable = true;
40
+ f._reachabilityDemoted = before;
41
+ }
42
+ }
@@ -0,0 +1,200 @@
1
+ // Regression-test generator (FR-VER-3).
2
+ //
3
+ // For each finding that has a PoC (from P1.1 / poc-generator), emit a
4
+ // framework-idiomatic test file that:
5
+ // - Fails on the vulnerable code state (asserts the exploit succeeds)
6
+ // - Passes after the fix is applied (assert flips to "did not succeed")
7
+ //
8
+ // We piggy-back on the existing PoC template — the test wraps the same
9
+ // HTTP call but uses the framework's test runner (Jest / pytest / JUnit)
10
+ // for assertion + reporting.
11
+ //
12
+ // Output: `f.regression_test = { lang, framework, code, runHint, filename }`.
13
+ //
14
+ // Harness-engineering note (post-derived): generated code is parsed before
15
+ // emit. A test that doesn't even compile is worse than no test — it gives
16
+ // the agent the illusion of progress while shipping a broken artifact.
17
+ // JS/TS: @babel/parser. Python: heuristic indentation + paren-balance check
18
+ // (we can't run python3 from inside Node deterministically).
19
+
20
+ import { parse as babelParse } from '@babel/parser';
21
+
22
+ const FRAMEWORK_FOR_LANG = Object.freeze({
23
+ node: 'jest',
24
+ python: 'pytest',
25
+ java: 'junit',
26
+ });
27
+
28
+ function _validateJs(code) {
29
+ try {
30
+ babelParse(code, { sourceType: 'module', allowAwaitOutsideFunction: false, errorRecovery: false });
31
+ return { ok: true };
32
+ } catch (e) {
33
+ return { ok: false, reason: `parse-failed: ${e.message}` };
34
+ }
35
+ }
36
+
37
+ function _validatePython(code) {
38
+ // Best-effort balance check without a Python parser.
39
+ // Catches the common breakage: an unescaped quote in the PoC payload
40
+ // bleeding into the test source and unbalancing the assertion string.
41
+ let parens = 0, brackets = 0, braces = 0;
42
+ let inStr = false, q = '';
43
+ for (let i = 0; i < code.length; i++) {
44
+ const c = code[i];
45
+ if (inStr) {
46
+ if (c === '\\') { i++; continue; }
47
+ if (c === q) inStr = false;
48
+ continue;
49
+ }
50
+ if (c === '"' || c === "'") { inStr = true; q = c; continue; }
51
+ if (c === '(') parens++;
52
+ else if (c === ')') parens--;
53
+ else if (c === '[') brackets++;
54
+ else if (c === ']') brackets--;
55
+ else if (c === '{') braces++;
56
+ else if (c === '}') braces--;
57
+ }
58
+ if (parens || brackets || braces) {
59
+ return { ok: false, reason: `parse-failed: unbalanced delimiters (parens=${parens} brackets=${brackets} braces=${braces})` };
60
+ }
61
+ if (inStr) {
62
+ return { ok: false, reason: 'parse-failed: unterminated string literal' };
63
+ }
64
+ return { ok: true };
65
+ }
66
+
67
+ function _filenameFor(finding, lang) {
68
+ const slug = (finding.stableId || finding.id || 'finding')
69
+ .replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40);
70
+ if (lang === 'python') return `test_security_${slug}.py`;
71
+ if (lang === 'java') return `SecurityRegression_${slug}.java`;
72
+ return `security_${slug}.test.mjs`;
73
+ }
74
+
75
+ function _renderJest(finding, poc) {
76
+ // Wrap the PoC's HTTP call in a Jest test. The PoC's `process.exit(0)`
77
+ // becomes `expect(...).toBe(true)`; exit(1) becomes `expect(...).toBe(false)`.
78
+ const url = _extractUrl(poc.code) || 'http://localhost:3000';
79
+ const method = _extractMethod(poc.code) || 'POST';
80
+ const payloadLine = _extractPayloadLine(poc.code);
81
+ return `// Regression test for ${finding.vuln} (${finding.cwe || 'unknown CWE'}).
82
+ // Auto-generated from P1.1 PoC; fails on vulnerable code, passes after the fix.
83
+
84
+ import { test, expect } from '@jest/globals';
85
+
86
+ test('${finding.vuln.replace(/'/g, "\\'")}', async () => {
87
+ const URL_ = ${JSON.stringify(url)};
88
+ const METHOD = ${JSON.stringify(method)};
89
+ ${payloadLine}
90
+ const body = METHOD === 'GET' ? null : JSON.stringify({ input: PAYLOAD });
91
+ const reqUrl = METHOD === 'GET'
92
+ ? URL_ + (URL_.includes('?') ? '&' : '?') + 'input=' + encodeURIComponent(PAYLOAD)
93
+ : URL_;
94
+ const r = await fetch(reqUrl, { method: METHOD, headers: { 'Content-Type': 'application/json' }, body });
95
+ const text = await r.text();
96
+ // The assertion below is the "exploit demonstrated" signal — when this
97
+ // test PASSES, the vuln is still present. After the fix, this assertion
98
+ // SHOULD fail. Inverse the assertion in a green build.
99
+ const demonstrated = text.includes('PoC') || /syntax error/i.test(text) || text.includes('<script>');
100
+ expect(demonstrated).toBe(false);
101
+ });
102
+ `;
103
+ }
104
+
105
+ function _renderPytest(finding, poc) {
106
+ return `# Regression test for ${finding.vuln} (${finding.cwe || 'unknown CWE'}).
107
+ # Auto-generated from P1.1 PoC.
108
+
109
+ import requests
110
+
111
+
112
+ def test_${(finding.stableId || finding.id || 'finding').replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 40)}():
113
+ url = 'http://localhost:3000'
114
+ r = requests.post(url, json={'input': "' UNION SELECT username, password FROM users--"})
115
+ # After the fix, the response should not contain SQL-syntax-error evidence.
116
+ text = r.text
117
+ demonstrated = 'syntax error' in text.lower() or '<script>' in text
118
+ assert not demonstrated, f'Vulnerability still present: {text[:200]}'
119
+ `;
120
+ }
121
+
122
+ function _extractUrl(code) {
123
+ const m = String(code || '').match(/URL_ = (['"])([^'"]+)\1/);
124
+ return m ? m[2] : null;
125
+ }
126
+ function _extractMethod(code) {
127
+ const m = String(code || '').match(/METHOD = (['"])([A-Z]+)\1/);
128
+ return m ? m[2] : null;
129
+ }
130
+ function _extractPayloadLine(code) {
131
+ const m = String(code || '').match(/PAYLOAD = `([^`]+)`/);
132
+ if (m) return `const PAYLOAD = ${JSON.stringify(m[1])};`;
133
+ return `const PAYLOAD = 'PoC';`;
134
+ }
135
+
136
+ /**
137
+ * Public API. Annotates findings with f.regression_test = {...} when a PoC
138
+ * is available.
139
+ */
140
+ export function annotateRegressionTests(findings) {
141
+ if (!Array.isArray(findings)) return;
142
+ for (const f of findings) {
143
+ if (!f || typeof f !== 'object') continue;
144
+ if (!f.poc) { f.regression_test = null; continue; }
145
+ // Harness-engineering note (post-derived): refuse to emit a runnable
146
+ // regression test when the underlying PoC's parameter key was inferred
147
+ // with low confidence. A test that posts to the wrong handler key
148
+ // ALWAYS passes (because the exploit never demonstrates), giving the
149
+ // illusion of "fix verified" — exactly the silent-failure mode the
150
+ // post warns against. Surface the skip instead.
151
+ if (f.poc.paramKeyConfidence === 'low') {
152
+ f.regression_test = {
153
+ lang: f.poc.lang,
154
+ framework: null,
155
+ filename: null,
156
+ runHint: null,
157
+ code: null,
158
+ _skipped: 'poc-param-key-unverified',
159
+ _explain: 'PoC param key inference was low-confidence (no `req.body.X` / `req.query.X` / form-key match found in the handler window). A regression test against the wrong key would always pass and would falsely suggest the fix landed.',
160
+ };
161
+ continue;
162
+ }
163
+ const lang = f.poc.lang;
164
+ const framework = FRAMEWORK_FOR_LANG[lang];
165
+ if (!framework) { f.regression_test = null; continue; }
166
+ let code;
167
+ try {
168
+ code = framework === 'jest' ? _renderJest(f, f.poc)
169
+ : framework === 'pytest' ? _renderPytest(f, f.poc)
170
+ : null;
171
+ } catch { code = null; }
172
+ if (!code) { f.regression_test = null; continue; }
173
+ // Parse the generated source before claiming it's a runnable test.
174
+ const validation = framework === 'jest' ? _validateJs(code)
175
+ : framework === 'pytest' ? _validatePython(code)
176
+ : { ok: true };
177
+ if (!validation.ok) {
178
+ f.regression_test = {
179
+ lang,
180
+ framework,
181
+ filename: _filenameFor(f, lang),
182
+ runHint: null,
183
+ code: null,
184
+ _skipped: validation.reason,
185
+ _explain: 'Generated test source did not parse cleanly. The test would have shipped as a broken artifact; reporting unverified instead.',
186
+ _attemptedCode: code.length <= 4000 ? code : null,
187
+ };
188
+ continue;
189
+ }
190
+ f.regression_test = {
191
+ lang,
192
+ framework,
193
+ filename: _filenameFor(f, lang),
194
+ runHint: framework === 'jest' ? 'npx jest' : framework === 'pytest' ? 'pytest -q' : 'mvn test',
195
+ code,
196
+ };
197
+ }
198
+ }
199
+
200
+ export const _internals = { FRAMEWORK_FOR_LANG };
@@ -0,0 +1,110 @@
1
+ // FR-ADV-5 — Reverse blast-radius for dependencies.
2
+ //
3
+ // "If `lodash` is RCE'd tomorrow, what's our exposure?" — pre-compute a
4
+ // reverse-reachability map for every direct dependency. The map keys on
5
+ // package name and lists the source files that import the package, the
6
+ // API surfaces they expose (HTTP routes that ultimately call into the dep),
7
+ // and the data classes those routes touch.
8
+ //
9
+ // Composes with:
10
+ // - SCA pipeline: when a KEV-listed CVE matches a dep, the reverse map
11
+ // gives an instant impact summary without a re-scan.
12
+ // - crown-jewels.js: routes scored as crown-jewel that pull in the dep
13
+ // elevate the dep's risk score.
14
+ //
15
+ // Output shape:
16
+ // {
17
+ // [pkgName]: {
18
+ // directImporters: [{ file, count, exampleSymbols: [...] }],
19
+ // routeExposure: [{ route, file }],
20
+ // crownJewelTouch: 0..1,
21
+ // }
22
+ // }
23
+
24
+ import { mapCrownJewels } from './crown-jewels.js';
25
+
26
+ const IMPORT_RES = [
27
+ /(?:^|\n)\s*import\s+(?:[\w*${},\s]+\s+from\s+)?["']([^"'.][^"']+)["']/g,
28
+ /\brequire\(\s*["']([^"'.][^"']+)["']\s*\)/g,
29
+ ];
30
+
31
+ const ROUTE_RES = [
32
+ /app\.(?:get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g,
33
+ /router\.(?:get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/g,
34
+ ];
35
+
36
+ function packageNameFromSpec(spec) {
37
+ if (spec.startsWith('node:') || spec.startsWith('bun:')) return null;
38
+ return spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
39
+ }
40
+
41
+ function extractImports(text) {
42
+ const pkgs = new Map();
43
+ if (!text || typeof text !== 'string') return pkgs;
44
+ for (const re of IMPORT_RES) {
45
+ re.lastIndex = 0;
46
+ let m;
47
+ while ((m = re.exec(text))) {
48
+ const pkg = packageNameFromSpec(m[1]);
49
+ if (!pkg) continue;
50
+ pkgs.set(pkg, (pkgs.get(pkg) || 0) + 1);
51
+ }
52
+ }
53
+ return pkgs;
54
+ }
55
+
56
+ function extractRoutesInFile(text) {
57
+ const out = [];
58
+ if (!text || typeof text !== 'string') return out;
59
+ for (const re of ROUTE_RES) {
60
+ re.lastIndex = 0;
61
+ let m;
62
+ while ((m = re.exec(text))) out.push(m[1]);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function buildReverseBlastRadius(fileContents) {
68
+ const map = {};
69
+ if (!fileContents || typeof fileContents !== 'object') return map;
70
+ const crownMap = mapCrownJewels(fileContents);
71
+ for (const [fp, text] of Object.entries(fileContents)) {
72
+ const pkgs = extractImports(text);
73
+ if (!pkgs.size) continue;
74
+ const routes = extractRoutesInFile(text);
75
+ const crownScore = crownMap[fp]?.score || 0;
76
+ for (const [pkg, count] of pkgs) {
77
+ if (!map[pkg]) map[pkg] = { directImporters: [], routeExposure: [], crownJewelTouch: 0 };
78
+ map[pkg].directImporters.push({ file: fp, count });
79
+ for (const route of routes) map[pkg].routeExposure.push({ route, file: fp });
80
+ if (crownScore > map[pkg].crownJewelTouch) map[pkg].crownJewelTouch = crownScore;
81
+ }
82
+ }
83
+ // Compact: cap each list at 25 entries for SARIF embedding.
84
+ for (const pkg of Object.keys(map)) {
85
+ map[pkg].directImporters = map[pkg].directImporters.slice(0, 25);
86
+ map[pkg].routeExposure = map[pkg].routeExposure.slice(0, 25);
87
+ }
88
+ return map;
89
+ }
90
+
91
+ // Annotate SCA findings with reverse-blast context — when a CVE finding's
92
+ // `package` matches a known dep, the SCA finding gets `reverseExposure`.
93
+ export function annotateScaReverseBlast(findings, fileContents) {
94
+ if (!Array.isArray(findings)) return findings;
95
+ const map = buildReverseBlastRadius(fileContents || {});
96
+ if (!Object.keys(map).length) return findings;
97
+ for (const f of findings) {
98
+ if (!f || typeof f !== 'object') continue;
99
+ const pkg = f.package || f.dependency || f.pkg;
100
+ if (!pkg || !map[pkg]) continue;
101
+ f.reverseExposure = {
102
+ importerCount: map[pkg].directImporters.length,
103
+ routeCount: map[pkg].routeExposure.length,
104
+ crownJewelTouch: Number(map[pkg].crownJewelTouch.toFixed(2)),
105
+ sampleImporters: map[pkg].directImporters.slice(0, 5).map(i => i.file),
106
+ sampleRoutes: map[pkg].routeExposure.slice(0, 5).map(r => r.route),
107
+ };
108
+ }
109
+ return findings;
110
+ }
@@ -0,0 +1,109 @@
1
+ // Smart router — `agentic-security secure`.
2
+ //
3
+ // One entry point that inspects project state and tells the user the single
4
+ // best next action. The vibecoder doesn't have to choose between /scan,
5
+ // /fix, /launch-check, /report-card, /find-and-fix-everything, etc.
6
+ //
7
+ // Decision tree (cheap, no scan):
8
+ // - No prior scan? → run /scan first
9
+ // - Prior scan, criticals open? → run /fix --all --critical
10
+ // - Prior scan, highs open? → /fix --all --high OR /show-findings
11
+ // - Prior scan, only mediums? → /report-card
12
+ // - All clean? → /security-badge (celebrate + share)
13
+ // - Pre-deploy intent (--launch flag, or no scan in 7 days)? → /launch-check
14
+ //
15
+ // Returns { action, command, reason }.
16
+
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+
20
+ function readJson(fp) {
21
+ if (!fs.existsSync(fp)) return null;
22
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
23
+ }
24
+
25
+ function ageHours(fp) {
26
+ if (!fs.existsSync(fp)) return Infinity;
27
+ return (Date.now() - fs.statSync(fp).mtimeMs) / 3_600_000;
28
+ }
29
+
30
+ export function decide({ scanRoot, intent }) {
31
+ const stateDir = path.join(scanRoot, '.agentic-security');
32
+ const lastScan = readJson(path.join(stateDir, 'last-scan.json'));
33
+ const scanAge = ageHours(path.join(stateDir, 'last-scan.json'));
34
+
35
+ if (!lastScan) {
36
+ return {
37
+ action: 'first-scan',
38
+ command: 'agentic-security scan .',
39
+ reason: 'No prior scan found. Start with a full sweep.',
40
+ };
41
+ }
42
+
43
+ const findings = [
44
+ ...(lastScan.findings || []),
45
+ ...(lastScan.secrets || []),
46
+ ...(lastScan.supplyChain || []),
47
+ ];
48
+ const sev = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
49
+ for (const f of findings) sev[f.severity] = (sev[f.severity] || 0) + 1;
50
+
51
+ if (intent === 'launch' || intent === 'deploy') {
52
+ if (sev.critical > 0) return {
53
+ action: 'block-launch',
54
+ command: 'agentic-security fix --finding <id> --apply',
55
+ reason: `${sev.critical} critical finding(s) — do NOT deploy. Fix first.`,
56
+ };
57
+ return {
58
+ action: 'launch-check',
59
+ command: 'claude /launch-check',
60
+ reason: 'Pre-deploy checklist (HTTPS, headers, env hygiene, rate limits).',
61
+ };
62
+ }
63
+
64
+ if (sev.critical > 0) {
65
+ return {
66
+ action: 'fix-critical',
67
+ command: 'agentic-security fix --finding <id> --preview',
68
+ reason: `${sev.critical} critical finding(s) open. Preview each fix, then --apply.`,
69
+ };
70
+ }
71
+ if (sev.high > 0) {
72
+ return {
73
+ action: 'review-high',
74
+ command: 'claude /show-findings',
75
+ reason: `${sev.high} high finding(s). Review and triage before fixing.`,
76
+ };
77
+ }
78
+ if (scanAge > 24 * 7) {
79
+ return {
80
+ action: 'rescan',
81
+ command: 'agentic-security scan .',
82
+ reason: `Last scan was ${Math.round(scanAge / 24)} days ago. Re-scan for fresh CVEs.`,
83
+ };
84
+ }
85
+ if (sev.medium > 0) {
86
+ return {
87
+ action: 'report-card',
88
+ command: 'claude /report-card',
89
+ reason: `Only mediums remain. Get a letter-grade snapshot and pick what's worth fixing.`,
90
+ };
91
+ }
92
+ return {
93
+ action: 'celebrate',
94
+ command: 'claude /security-badge',
95
+ reason: 'Clean scan. Generate a badge for your README and share the win.',
96
+ };
97
+ }
98
+
99
+ export function explain(decision) {
100
+ const lines = [
101
+ `🛡 agentic-security · next step`,
102
+ ``,
103
+ ` Action: ${decision.action}`,
104
+ ` Why: ${decision.reason}`,
105
+ ` Run: ${decision.command}`,
106
+ ``,
107
+ ];
108
+ return lines.join('\n');
109
+ }