@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,1683 @@
1
+ #!/usr/bin/env node
2
+ // agentic-security CLI — scan, fix, setup, version.
3
+ // Created by ClearCapabilities.Com — https://clearcapabilities.com
4
+ import * as fs from 'node:fs';
5
+ import * as fsp from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import { signLastScan as _signLastScan, verifyLastScan as _verifyLastScanShared } from '../src/posture/integrity.js';
8
+ import { runScan } from '../src/runScan.js';
9
+ import { toJSON, toMarkdown, toSARIF, toSTIX, toCSV, toJUnit, toCLI, toCLIByProfile, toShipVerdict, toProTable, toHTML, toSummary, exitCodeFor, normalizeFindings } from '../src/report/index.js';
10
+ import { toCycloneDX, toSPDX } from '../src/posture/sbom.js';
11
+ import { toPBOM } from '../src/sast/pipeline.js';
12
+ import { buildAIBOM, aibomToMarkdown } from '../src/posture/aibom.js';
13
+ import { recordScan, formatStreakLine, formatGradeDelta } from '../src/posture/streak.js';
14
+ import { ingestAndMerge } from '../src/sca/sarif-ingest.js';
15
+ import { loadProfile, saveProfile, detectProfile, renderAttributionLine, ATTRIBUTION, ATTRIBUTION_URL } from '../src/posture/profile.js';
16
+ import { applySuppressions, addSoftAcceptance, expiredSoftAcceptances } from '../src/posture/suppressions.js';
17
+ import { applyOverrides, validateOverrides } from '../src/posture/rule-overrides.js';
18
+ import { listPacks, loadPack, applyPacks } from '../src/posture/rule-packs.js';
19
+ import { writeLockfile, verifyLockfile, makeDeterministic, isDeterministic } from '../src/posture/deterministic.js';
20
+ import { enrichWithEPSS } from '../src/posture/epss.js';
21
+ import { enrichWithBlastRadius } from '../src/posture/blast-radius.js';
22
+ import { applyCustomRules, runRuleTests, loadCustomRules } from '../src/posture/custom-rules.js';
23
+ import { applyFix, undoLast, undoAll, listHistory, preview as previewDiff, compactLog } from '../src/posture/fix-history.js';
24
+ import { syncTickets } from '../src/integrations/tickets.js';
25
+ import { decide as decideNextAction, explain as explainDecision } from '../src/posture/router.js';
26
+ import * as triage from '../src/posture/triage.js';
27
+ import { buildSlackDigest, buildDiscordDigest, postWebhook, buildJiraIssue, buildPrComment, buildSiemEvent, loadIntegrationConfig } from '../src/integrations/index.js';
28
+ import fg from 'fast-glob';
29
+
30
+ // last-scan.json integrity helpers — implementation in posture/integrity.js
31
+ // so the MCP server tools can share verification.
32
+ function _verifyLastScan(body, sigFile) {
33
+ const v = _verifyLastScanShared(body, sigFile);
34
+ return v;
35
+ }
36
+
37
+ const USAGE = `agentic-security <command> [options]
38
+
39
+ 🛡 Created by ClearCapabilities.Com · https://clearcapabilities.com
40
+
41
+ Commands:
42
+ secure [path] [--launch] Smart router: tells you the single best next action
43
+ scan [path] Full SAST + SCA + Secrets sweep (default: cwd)
44
+ ship (internal) Vibecoder verdict — invoked by /scan-all
45
+ ci [path] Baseline-aware CI scan: auto-detects PR base ref,
46
+ writes SARIF + JUnit + JSON, applies --fail-on policy
47
+ fix --finding <id> [--preview|--apply] Show diff or apply fix for a single finding
48
+ undo [--all|--list|--compact] Revert the most recent applied fix; --compact archives terminal entries (--retain-days N --prune-backups)
49
+ accept --finding <id> Soft-suppress a finding for 30 days (vibecoder)
50
+ setup [project-dir] Install /security-* shortcut commands into a project
51
+ profile set <vibecoder|pro> Set or change the persona profile
52
+ profile show Print current profile
53
+ org-scan --repos <list> Pro: scan multiple repos and produce roll-up
54
+ triage list|assign|trend Pro: per-finding state, MTTR, assignment
55
+ rules validate Pro: lint .agentic-security/rules.yml
56
+ packs list List available curated rule packs
57
+ rule list | test <glob> List/test custom YAML rules in .agentic-security/rules/
58
+ tickets sync --provider <p> Two-way sync findings ↔ GitHub Issues / Linear / Jira
59
+ digest --slack <webhook> Vibecoder: send daily digest to Slack
60
+ mcp Start the MCP stdio server (scan_diff, query_taint, explain_finding, apply_fix)
61
+ validator-cache stats|gc Inspect / prune .agentic-security/llm-cache/ (use --older-than <days> --dry-run)
62
+ verify [--finding <id>] Re-run the verifier loop on last-scan findings (use --live --target <url> to execute PoCs)
63
+ reset [--yes] [--keep ...] Right-to-delete: wipe accumulated learned state under .agentic-security/ (preserves operator-authored config)
64
+ rule-synth [--dry-run] Auto-synthesise suppression rules from repeated FP verdicts (proposes — does not activate)
65
+ version Print version
66
+ banner [--full] Print the Patch-the-frog mascot + brand lockup
67
+ harness [path] [--include-home] Multi-harness config audit: scans .claude/,
68
+ .cursor/, .codex/, .gemini/, .kiro/, .opencode/,
69
+ .trae/, .qwen/, .zed/, .continue/, .aider/ at the
70
+ project root. --include-home also sweeps ~/.
71
+ scan-baseline --current <f> --previous <f>
72
+ Finding-level diff between two scan JSON outputs.
73
+ Reports added / removed / changed findings.
74
+
75
+ Options:
76
+ --profile vibecoder|pro Override profile for this run
77
+ --only sast|sca|secrets Limit scan to one pillar
78
+ --format <fmt> cli | json | md | sarif | stix | junit | csv | html | cyclonedx | spdx | pbom | aibom | aibom-md
79
+ --pack <name> Focus on a curated rule pack (repeatable): owasp-top-10 | cwe-top-25 | llm-security | supply-chain
80
+ --baseline <ref> Diff against a git ref; only findings new vs. that ref count (ci subcommand)
81
+ --fail-on critical|high|medium|low|none ci-mode exit policy (default: critical)
82
+ --policy <file.rego> ci-mode policy-as-code gate; deny[] rules fail the build (FR-SDLC-9)
83
+ --columns standard|mitre|capec|owasp Pro-mode column set (default: standard)
84
+ --confidence <0..1> Override per-profile confidence threshold
85
+ --firehose Show ALL findings (ignore confidence threshold)
86
+ --honest Show only high-confidence (≥0.9) findings
87
+ --exposed-only Filter to findings the production stack does NOT mitigate
88
+ --mitigated-only Filter to findings already mitigated by WAF/auth/network/flag
89
+ --unreachable-only Filter to findings on unreachable code paths
90
+ --persona <name> Filter to findings whose top-2 personas include <name>
91
+ (script-kiddie|opportunistic-criminal|apt-nation-state|
92
+ supply-chain-attacker|malicious-insider)
93
+ --show-personas Append per-persona top-picks block
94
+ --show-bounty Append predicted bug-bounty payout block
95
+ --show-playbook Append attack-playbook block for high+ findings
96
+ --show-spof Append single-point-of-failure-controls block
97
+ --show-trust-boundary Append the auto-generated trust-boundary Mermaid diagram
98
+ --show-threat-model Append the auto-derived STRIDE threat model summary
99
+ --show-drift Append calibration-drift alarms (overconfidence detection)
100
+ --sca-reachable-only Only SCA findings where the vulnerable function is reachable
101
+ --ingest-sarif <glob> Merge external SARIF into this scan
102
+ --scorecard Enrich components with OSSF Scorecard scores
103
+ --no-network Skip OSV/registry queries (offline mode)
104
+ --pr [ref] Diff-aware: scan only files changed since ref (auto-detects PR base)
105
+ --deterministic Reproducible scan: stable sort, no-network, lockfile-checked
106
+ --no-epss Skip EPSS exploit-prediction enrichment (default: enabled)
107
+ --no-blast-radius Skip blast-radius / cost framing (default: enabled)
108
+ --verbose Include fix bodies + taxonomy in CLI output
109
+ --output <file> Write report to file instead of stdout
110
+ --machine-output Always write .agentic-security/findings.{sarif,json,csv}
111
+
112
+ Exit codes:
113
+ 0 = clean 1 = low/medium 2 = high 3 = critical 4 = error`;
114
+
115
+ // Load profile, allowing CLI flags to override. CLI flag takes precedence.
116
+ function loadPersonaProfile(scanRoot, args) {
117
+ const flagProfile = args.flags.profile;
118
+ const base = loadProfile(scanRoot);
119
+ if (flagProfile === 'pro' || flagProfile === 'vibecoder') {
120
+ return { ...base, profile: flagProfile };
121
+ }
122
+ return base;
123
+ }
124
+
125
+ // Compute confidence threshold from profile + flags.
126
+ // `agentic-security banner [--full|--compact]` — Patch the frog mascot +
127
+ // brand line. `--compact` (default) prints a single coloured frog face beside
128
+ // the wordmark. `--full` prints the seven-line lockup mirroring the SVG.
129
+ // Colour is suppressed under NO_COLOR or non-TTY stderr.
130
+ function printBanner(args) {
131
+ const useColor = !!process.stderr.isTTY && !process.env.NO_COLOR;
132
+ const C = useColor ? {
133
+ FROG: '\x1b[38;2;255;107;44m',
134
+ DEEP: '\x1b[38;2;201;52;20m',
135
+ CREAM: '\x1b[38;2;244;239;230m',
136
+ DIM: '\x1b[2m',
137
+ BOLD: '\x1b[1m',
138
+ RESET: '\x1b[0m',
139
+ } : { FROG:'', DEEP:'', CREAM:'', DIM:'', BOLD:'', RESET:'' };
140
+ const v = '0.74.0';
141
+ const compact = !args.flags.full;
142
+ if (compact) {
143
+ const lines = [
144
+ '',
145
+ ` ${C.FROG}╭─╮╭─╮${C.RESET} ${C.BOLD}agentic-security${C.RESET} ${C.DIM}·${C.RESET} ${C.CREAM}by Clear Capabilities${C.RESET} ${C.DIM}· v${v}${C.RESET}`,
146
+ ` ${C.FROG}│${C.BOLD}◉${C.RESET}${C.FROG}││${C.BOLD}◉${C.RESET}${C.FROG}│${C.RESET} ${C.DIM}Tiny.${C.RESET} ${C.FROG}${C.BOLD}Bright.${C.RESET} ${C.DIM}Watching.${C.RESET}`,
147
+ ` ${C.FROG}╰─╯╰─╯${C.RESET}`,
148
+ '',
149
+ ];
150
+ process.stdout.write(lines.join('\n'));
151
+ return;
152
+ }
153
+ // Full lockup — mirrors hooks/mascot.js lockup() for first-run / banner output.
154
+ const lines = [
155
+ '',
156
+ ` ${C.FROG}╭───╮ ╭───╮${C.RESET}`,
157
+ ` ${C.FROG}│ ${C.BOLD}◉${C.RESET}${C.FROG} │ │ ${C.BOLD}◉${C.RESET}${C.FROG} │${C.RESET} ${C.BOLD}agentic-security${C.RESET}`,
158
+ ` ${C.FROG}╰─┬─╯ ╰─┬─╯${C.RESET} ${C.DIM}─────────────────${C.RESET}`,
159
+ ` ${C.FROG}╭──┴─────┴──╮${C.RESET} ${C.CREAM}Tiny. ${C.FROG}${C.BOLD}Bright.${C.RESET}${C.CREAM} Watching.${C.RESET}`,
160
+ ` ${C.FROG}│ ${C.DEEP}·${C.FROG} ${C.BOLD}⌣${C.RESET}${C.FROG} ${C.DEEP}·${C.FROG} │${C.RESET} ${C.CREAM}by Clear Capabilities Inc.${C.RESET} ${C.DIM}· v${v}${C.RESET}`,
161
+ ` ${C.FROG}╰───────────╯${C.RESET} ${C.DIM}https://clearcapabilities.com${C.RESET}`,
162
+ '',
163
+ ];
164
+ process.stdout.write(lines.join('\n'));
165
+ }
166
+
167
+ function effectiveConfidence(profile, args) {
168
+ if (args.flags['firehose']) return 0.0;
169
+ if (args.flags['honest']) return 0.9;
170
+ if (args.flags['confidence'] != null) return parseFloat(args.flags['confidence']);
171
+ return profile.confidenceMin ?? (profile.profile === 'pro' ? 0.3 : 0.9);
172
+ }
173
+
174
+ // v3 next-gen — render supplementary blocks on top of the normal CLI body.
175
+ // Each block is opt-in via a flag; renderV3Blocks returns '' when no flags
176
+ // are set, so the default output is unchanged.
177
+ function renderV3Blocks(scan, flags) {
178
+ const out = [];
179
+ const findings = scan.findings || [];
180
+ if (flags['show-personas']) {
181
+ out.push('\n── Per-attacker-persona top picks ───────────────────────────────');
182
+ const byPersona = new Map();
183
+ for (const f of findings) {
184
+ if (!Array.isArray(f.personaTopTwo)) continue;
185
+ for (const p of f.personaTopTwo) {
186
+ if (!byPersona.has(p)) byPersona.set(p, []);
187
+ byPersona.get(p).push(f);
188
+ }
189
+ }
190
+ if (!byPersona.size) out.push(' (no findings carry persona scores yet — rerun /scan)');
191
+ for (const [persona, items] of byPersona) {
192
+ items.sort((a, b) => (b.personaMaxScore || 0) - (a.personaMaxScore || 0));
193
+ out.push(`\n ${persona} (${items.length} relevant)`);
194
+ for (const f of items.slice(0, 3)) {
195
+ const sev = (f.severity || '').toUpperCase();
196
+ out.push(` [${sev}] ${(f.vuln || '').slice(0, 60)} — ${f.file}:${f.line}`);
197
+ }
198
+ }
199
+ }
200
+ if (flags['show-bounty']) {
201
+ out.push('\n── Predicted bug-bounty payouts ─────────────────────────────────');
202
+ const withBounty = findings.filter(f => f.predictedBountyUsd);
203
+ if (!withBounty.length) out.push(' (no findings carry bounty predictions — rerun /scan)');
204
+ const sorted = withBounty.slice().sort((a, b) => (b.predictedBountyUsd.likely || 0) - (a.predictedBountyUsd.likely || 0));
205
+ for (const f of sorted.slice(0, 15)) {
206
+ const b = f.predictedBountyUsd;
207
+ out.push(` $${b.low}-$${b.high} (likely $${b.likely}, ${b.program}) — ${(f.vuln || '').slice(0, 50)} ${f.file}:${f.line}`);
208
+ }
209
+ }
210
+ if (flags['show-playbook']) {
211
+ out.push('\n── Attack playbooks (high+ findings only) ───────────────────────');
212
+ const withPb = findings.filter(f => f.attackPlaybook);
213
+ if (!withPb.length) out.push(' (no high+/critical findings to show playbooks for)');
214
+ for (const f of withPb.slice(0, 5)) {
215
+ const pb = f.attackPlaybook;
216
+ out.push(`\n ${pb.cwe} — ${pb.title} (${f.file}:${f.line})`);
217
+ out.push(' ────────────────────────────────────');
218
+ out.push(pb.script.split('\n').map(l => ' ' + l).join('\n'));
219
+ }
220
+ }
221
+ if (flags['show-spof']) {
222
+ out.push('\n── Single-point-of-failure controls (counterfactual) ────────────');
223
+ const spof = scan._v3?.counterfactual?.spofControls || [];
224
+ if (!spof.length) out.push(' (no SPOF controls detected — either no controls or no clusters of high+ findings depend on one)');
225
+ for (const c of spof.slice(0, 10)) {
226
+ out.push(` ${c.control} @ ${c.location} — would expose ${c.wouldExpose} high+ findings if removed`);
227
+ }
228
+ }
229
+ if (flags['show-trust-boundary']) {
230
+ out.push('\n── Trust-boundary diagram (Mermaid) ─────────────────────────────');
231
+ const d = scan._v3?.trustBoundaryDiagram;
232
+ if (!d) out.push(' (no diagram — rerun /scan)');
233
+ else {
234
+ out.push(' ```mermaid');
235
+ out.push(d.mermaid.split('\n').map(l => ' ' + l).join('\n'));
236
+ out.push(' ```');
237
+ }
238
+ }
239
+ if (flags['show-threat-model']) {
240
+ out.push('\n── Auto-generated STRIDE threat model ───────────────────────────');
241
+ const tm = scan._v3?.threatModel;
242
+ if (!tm) out.push(' (no threat model — rerun /scan)');
243
+ else {
244
+ out.push(` Assets: ${tm.summary.assetCount} Trust boundaries: ${tm.summary.boundaryCount}`);
245
+ for (const [cat, count] of Object.entries(tm.summary.strideCounts)) {
246
+ out.push(` ${cat.padEnd(22)} ${count}`);
247
+ }
248
+ }
249
+ }
250
+ if (flags['show-drift']) {
251
+ out.push('\n── Calibration-drift alarms ─────────────────────────────────────');
252
+ const dr = scan._v3?.calibrationDrift;
253
+ const alarms = dr?.alarms || [];
254
+ if (!alarms.length) out.push(' (no drift detected — confidence matches realized accuracy within threshold)');
255
+ for (const a of alarms) {
256
+ out.push(` ${a.family}: reported ${(a.reportedAccuracy * 100).toFixed(0)}% vs realized ${(a.realizedAccuracy * 100).toFixed(0)}% (N=${a.sampleSize})`);
257
+ out.push(` ${a.recommendation}`);
258
+ }
259
+ }
260
+ return out.join('\n');
261
+ }
262
+
263
+ // Always-on machine output (R2). Vibecoder gets JSON only; pro gets JSON+SARIF+CSV.
264
+ async function writeMachineOutput(targetAbs, scan, meta, profile) {
265
+ const stateDir = path.join(targetAbs, '.agentic-security');
266
+ await fsp.mkdir(stateDir, { recursive: true });
267
+ // Always JSON (used by /security-fix and /security-report).
268
+ await fsp.writeFile(path.join(stateDir, 'findings.json'),
269
+ JSON.stringify(toJSON(scan, meta), null, 2));
270
+ if (profile.profile === 'pro' || profile.machineOutput) {
271
+ await fsp.writeFile(path.join(stateDir, 'findings.sarif'),
272
+ JSON.stringify(toSARIF(scan, meta), null, 2));
273
+ await fsp.writeFile(path.join(stateDir, 'findings.csv'), toCSV(scan));
274
+ }
275
+ }
276
+
277
+ function parseArgs(argv) {
278
+ const args = { _: [], flags: {} };
279
+ for (let i = 0; i < argv.length; i++) {
280
+ const a = argv[i];
281
+ if (a.startsWith('--')) {
282
+ const [k, v] = a.slice(2).split('=', 2);
283
+ if (v !== undefined) { args.flags[k] = v; continue; }
284
+ const next = argv[i + 1];
285
+ if (next && !next.startsWith('--')) { args.flags[k] = next; i++; }
286
+ else args.flags[k] = true;
287
+ } else {
288
+ args._.push(a);
289
+ }
290
+ }
291
+ return args;
292
+ }
293
+
294
+ async function cmdScan(args) {
295
+ const target = args._[1] || '.';
296
+ const targetAbs = path.resolve(target);
297
+ // Load persona profile (R1). Persona-aware defaults flow from here.
298
+ const profile = loadPersonaProfile(targetAbs, args);
299
+ const format = args.flags.format || (profile.profile === 'pro' ? 'cli' : 'ship');
300
+ const verbose = !!args.flags.verbose;
301
+ const output = args.flags.output;
302
+ const noNet = !!args.flags['no-network'];
303
+ if (noNet) process.env.AGENTIC_SECURITY_OFFLINE = '1';
304
+
305
+ // Deterministic mode: stable output, no-network, lockfile verification.
306
+ if (args.flags['deterministic']) {
307
+ process.env.AGENTIC_SECURITY_DETERMINISTIC = '1';
308
+ process.env.AGENTIC_SECURITY_OFFLINE = '1';
309
+ const v = verifyLockfile(targetAbs);
310
+ if (!v.ok) {
311
+ process.stderr.write(`[deterministic] lockfile mismatch:\n - ${v.mismatches.join('\n - ')}\n`);
312
+ process.stderr.write(`[deterministic] run \`agentic-security rules lock\` to refresh.\n`);
313
+ return 4;
314
+ }
315
+ }
316
+
317
+ // --pr [ref] : friendlier alias for --changed-since that auto-detects the PR
318
+ // base ref (GitHub/GitLab/Buildkite/Bitbucket env vars) when no value is given.
319
+ let changedSince = args.flags['changed-since'] || null;
320
+ if (args.flags['pr']) {
321
+ const pr = args.flags['pr'];
322
+ changedSince = (typeof pr === 'string' && pr !== 'true') ? pr : (detectBaseline() || 'origin/main');
323
+ process.stderr.write(`[pr-mode] scanning files changed since: ${changedSince}\n`);
324
+ }
325
+
326
+ const { scan, meta } = await runScan(target, {
327
+ changedSince,
328
+ onProgress: (p) => {
329
+ if (process.stderr.isTTY) process.stderr.write(`\r[${p.phase}] ${p.current}/${p.total} ${p.file} `);
330
+ },
331
+ });
332
+ if (process.stderr.isTTY) process.stderr.write('\r' + ' '.repeat(80) + '\r');
333
+
334
+ const only = args.flags.only;
335
+ if (only) {
336
+ if (only === 'sast') { scan.secrets = []; scan.supplyChain = []; }
337
+ if (only === 'sca') { scan.findings = []; scan.secrets = []; }
338
+ if (only === 'secrets') { scan.findings = []; scan.supplyChain = []; }
339
+ }
340
+
341
+ // 0.9.0 Feat-18: --scorecard flag enables OSSF Scorecard enrichment
342
+ if (args.flags['scorecard']) process.env.AGENTIC_SECURITY_SCORECARD = '1';
343
+
344
+ // 0.7.0 Feat-7: --ingest-sarif <path-or-glob> merges SARIF from external tools (Semgrep,
345
+ // gitleaks, Bandit, Trivy, Checkov, etc.) into this scan's findings, deduping by
346
+ // fingerprint and tracking provenance via sources[].
347
+ if (args.flags['ingest-sarif']) {
348
+ const glob = args.flags['ingest-sarif'];
349
+ const paths = await fg(glob, { dot: false, onlyFiles: true });
350
+ if (paths.length) {
351
+ const r = ingestAndMerge(scan, paths);
352
+ if (process.stderr.isTTY) process.stderr.write(`[ingest] merged ${r.merged} / added ${r.added} findings from ${paths.length} SARIF file(s)\n`);
353
+ }
354
+ }
355
+
356
+ // 0.6.0 Feat-1: --sca-reachable-only filters to only SCA findings where the vulnerable
357
+ // function was confirmed reachable from a route handler.
358
+ if (args.flags['sca-reachable-only']) {
359
+ scan.supplyChain = (scan.supplyChain || []).filter(sc =>
360
+ sc.functionReachable === 'reachable' || sc.functionReachable !== 'unreachable'
361
+ );
362
+ }
363
+
364
+ // R4: Apply persona-appropriate suppressions BEFORE rendering.
365
+ // R9: Apply rule overrides (severity remap, disable list).
366
+ // R3: Compute effective confidence threshold for renderers.
367
+ const confidenceMin = effectiveConfidence(profile, args);
368
+ const effProfile = { ...profile, confidenceMin };
369
+ // Apply suppressions to each findings bucket (findings/secrets/logicVulns/supplyChain).
370
+ scan.findings = applySuppressions(scan.findings || [], targetAbs, profile);
371
+ scan.secrets = applySuppressions(scan.secrets || [], targetAbs, profile);
372
+ scan.logicVulns = applySuppressions(scan.logicVulns || [], targetAbs, profile);
373
+ scan.supplyChain = applySuppressions(scan.supplyChain || [], targetAbs, profile);
374
+ // Apply rule overrides (severity remaps + disable list).
375
+ scan.findings = applyOverrides(scan.findings || [], targetAbs);
376
+ scan.secrets = applyOverrides(scan.secrets || [], targetAbs);
377
+ scan.logicVulns = applyOverrides(scan.logicVulns || [], targetAbs);
378
+
379
+ // Curated rule packs: --pack <name> (repeatable). Narrows findings to the
380
+ // CWEs covered by the requested pack(s).
381
+ const packArg = args.flags.pack;
382
+ const packNames = packArg ? (Array.isArray(packArg) ? packArg : String(packArg).split(',')) : [];
383
+ if (packNames.length) Object.assign(scan, applyPacks(scan, packNames));
384
+
385
+ // Custom pattern-rule DSL — load .agentic-security/rules/*.yml and append findings.
386
+ try {
387
+ const { fileContents } = await import('../src/runScan.js').then(m => m.readTree(targetAbs));
388
+ const customFindings = applyCustomRules(targetAbs, fileContents);
389
+ if (customFindings.length) {
390
+ scan.findings = [...(scan.findings || []), ...customFindings];
391
+ if (process.stderr.isTTY) {
392
+ process.stderr.write(`[custom-rules] +${customFindings.length} finding(s) from ${loadCustomRules(targetAbs).length} rule(s)\n`);
393
+ }
394
+ }
395
+ } catch {}
396
+
397
+ // EPSS exploit-prediction enrichment (skipped under --no-network / --deterministic).
398
+ // Bumps severity on actively-exploited CVEs so they sort to the top.
399
+ if (!args.flags['no-epss'] && !isDeterministic() && !noNet) {
400
+ try { await enrichWithEPSS(scan); } catch {}
401
+ }
402
+
403
+ // Blast-radius narrative — purely local, always safe to run.
404
+ if (!args.flags['no-blast-radius']) {
405
+ try { enrichWithBlastRadius(scan, targetAbs); } catch {}
406
+ }
407
+
408
+ // v3 next-gen filter flags — operate on the production-aware composite
409
+ // verdict. These run after every annotator so the verdict is final.
410
+ if (args.flags['exposed-only']) {
411
+ scan.findings = (scan.findings || []).filter(f => f.mitigationVerdict === 'exposed-in-prod' || !f.mitigationVerdict);
412
+ scan.supplyChain = (scan.supplyChain || []).filter(f => f.mitigationVerdict === 'exposed-in-prod' || !f.mitigationVerdict);
413
+ }
414
+ if (args.flags['mitigated-only']) {
415
+ scan.findings = (scan.findings || []).filter(f => f.mitigationVerdict === 'mitigated-in-prod');
416
+ scan.supplyChain = (scan.supplyChain || []).filter(f => f.mitigationVerdict === 'mitigated-in-prod');
417
+ }
418
+ if (args.flags['unreachable-only']) {
419
+ scan.findings = (scan.findings || []).filter(f => f.mitigationVerdict === 'unreachable-in-prod');
420
+ scan.supplyChain = (scan.supplyChain || []).filter(f => f.mitigationVerdict === 'unreachable-in-prod');
421
+ }
422
+ // --persona <name> filter — keep only findings where the named persona
423
+ // appears in the top-2 ranked personas for the finding.
424
+ if (args.flags['persona']) {
425
+ const want = String(args.flags['persona']);
426
+ scan.findings = (scan.findings || []).filter(f =>
427
+ Array.isArray(f.personaTopTwo) && f.personaTopTwo.includes(want)
428
+ );
429
+ }
430
+
431
+ // Deterministic post-process: stable-sort findings + zero out timing.
432
+ if (isDeterministic()) makeDeterministic(scan, meta);
433
+
434
+ // R2: Always emit machine-readable artifacts to .agentic-security/.
435
+ await writeMachineOutput(targetAbs, scan, meta, profile);
436
+
437
+ const includeSuppressed = !!args.flags['include-suppressed'];
438
+ let body;
439
+ if (format === 'json') body = JSON.stringify(toJSON(scan, meta, { includeSuppressed }), null, 2);
440
+ else if (format === 'md' || format === 'markdown') body = toMarkdown(scan, meta);
441
+ else if (format === 'sarif') body = JSON.stringify(toSARIF(scan, meta), null, 2);
442
+ else if (format === 'stix') body = JSON.stringify(toSTIX(scan, meta), null, 2);
443
+ else if (format === 'junit') body = toJUnit(scan, meta);
444
+ else if (format === 'csv') body = toCSV(scan);
445
+ else if (format === 'html') body = toHTML(scan, meta);
446
+ else if (format === 'cyclonedx' || format === 'sbom') body = JSON.stringify(toCycloneDX(scan, meta), null, 2);
447
+ else if (format === 'spdx') body = JSON.stringify(toSPDX(scan, meta), null, 2);
448
+ else if (format === 'pbom') body = JSON.stringify(toPBOM(scan.fc || {}, meta), null, 2);
449
+ else if (format === 'aibom') body = JSON.stringify(buildAIBOM(scan, scan.fc || {}, meta), null, 2);
450
+ else if (format === 'aibom-md') body = aibomToMarkdown(buildAIBOM(scan, scan.fc || {}, meta));
451
+ else if (format === 'ship') body = toShipVerdict(scan, { profile: effProfile });
452
+ else if (format === 'pro') body = toProTable(scan, { profile: effProfile, columns: args.flags.columns });
453
+ else if (format === 'cli') body = toCLIByProfile(scan, { profile: effProfile, columns: args.flags.columns, verbose });
454
+ else body = toSummary(scan);
455
+
456
+ // v3 next-gen — supplementary blocks for human-readable formats. These
457
+ // are append-only and do not change the verdict / exit code. The blocks
458
+ // are only meaningful when v3 annotators have run (default scan path).
459
+ if (format === 'cli' || format === 'ship' || format === 'pro' || format === 'md' || format === 'markdown') {
460
+ body += renderV3Blocks(scan, args.flags);
461
+ }
462
+
463
+ if (output) await fsp.writeFile(output, body);
464
+ else process.stdout.write(body + '\n');
465
+
466
+ // Persist last scan for /security-fix and /security-report
467
+ const stateDir = path.join(path.resolve(target), '.agentic-security');
468
+ await fsp.mkdir(stateDir, { recursive: true });
469
+ const persistedScan = toJSON(scan, meta);
470
+ const lastScanBody = JSON.stringify(persistedScan, null, 2);
471
+ await fsp.writeFile(path.join(stateDir, 'last-scan.json'), lastScanBody);
472
+ try {
473
+ await fsp.writeFile(path.join(stateDir, 'last-scan.json.sig'), _signLastScan(lastScanBody));
474
+ } catch { /* non-fatal — sig file is best-effort */ }
475
+
476
+ // 0.14.0 — update streak / achievements after every full scan. Suppress
477
+ // streak side effects when the user only wants raw JSON output (CI piping).
478
+ try {
479
+ const streak = recordScan(stateDir, persistedScan);
480
+ // Print celebration / streak line to stderr so it doesn't pollute --format json
481
+ if (process.stderr.isTTY && format !== 'json' && format !== 'sarif') {
482
+ const delta = formatGradeDelta(streak);
483
+ const line = formatStreakLine(streak);
484
+ if (delta) process.stderr.write('\n' + delta + '\n');
485
+ if (line) process.stderr.write('🛡️ ' + line + '\n');
486
+ }
487
+ } catch {}
488
+
489
+ return exitCodeFor(scan);
490
+ }
491
+
492
+ // /scan-all — vibecoder one-screen verdict (internal CLI subcommand: `ship`).
493
+ //
494
+ // Always returns shell exit 0 for a valid verdict (clean, low, high, or
495
+ // critical findings). Only a real engine error (exit 4) propagates. The
496
+ // slash-command UX surfaces "Not safe to deploy" as the answer the user
497
+ // asked for — it's information, not a process failure. CI consumers
498
+ // needing severity-based gating should use the `ci` subcommand which has
499
+ // explicit `--fail-on` policy control.
500
+ async function cmdShip(args) {
501
+ const target = args._[1] || '.';
502
+ args.flags.format = 'ship';
503
+ const code = await cmdScan(args);
504
+ return code >= 4 ? code : 0;
505
+ }
506
+
507
+ // Detect the PR base ref from common CI environment variables. Returns null
508
+ // if no CI baseline ref is in scope. The CLI --baseline flag takes precedence.
509
+ function detectBaseline() {
510
+ return process.env.GITHUB_BASE_REF
511
+ || process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME // GitLab
512
+ || process.env.BUILDKITE_PULL_REQUEST_BASE_BRANCH // Buildkite
513
+ || process.env.BITBUCKET_PR_DESTINATION_BRANCH // Bitbucket
514
+ || null;
515
+ }
516
+
517
+ // Translate a scan exit code (0..3) and a --fail-on threshold into a CI exit code.
518
+ // Returns 0 (pass) or 1 (fail).
519
+ function ciExitCode(scanExitCode, failOn) {
520
+ switch (failOn) {
521
+ case 'none': return 0;
522
+ case 'critical': default: return scanExitCode >= 3 ? 1 : 0;
523
+ case 'high': return scanExitCode >= 2 ? 1 : 0;
524
+ case 'medium':
525
+ case 'low': return scanExitCode >= 1 ? 1 : 0;
526
+ }
527
+ }
528
+
529
+ // `agentic-security ci [path] [--baseline <ref>] [--fail-on <sev>]`
530
+ // Single-shot CI command: auto-detects PR base ref, runs a baseline-aware scan,
531
+ // writes findings.{sarif,junit.xml,json} to .agentic-security/, and exits per
532
+ // the --fail-on policy.
533
+ async function cmdCi(args) {
534
+ const target = args._[1] || '.';
535
+ const targetAbs = path.resolve(target);
536
+ const failOn = args.flags['fail-on'] || 'critical';
537
+ const baseline = args.flags.baseline || detectBaseline();
538
+
539
+ if (baseline) process.stderr.write(`[ci] baseline: ${baseline}\n`);
540
+ else process.stderr.write(`[ci] full scan (no baseline ref detected)\n`);
541
+
542
+ const profile = loadPersonaProfile(targetAbs, args);
543
+ const { scan, meta } = await runScan(target, { changedSince: baseline || null });
544
+
545
+ // Apply suppressions + overrides + packs, mirroring cmdScan's pipeline.
546
+ scan.findings = applySuppressions(scan.findings || [], targetAbs, profile);
547
+ scan.secrets = applySuppressions(scan.secrets || [], targetAbs, profile);
548
+ scan.logicVulns = applySuppressions(scan.logicVulns || [], targetAbs, profile);
549
+ scan.supplyChain = applySuppressions(scan.supplyChain || [], targetAbs, profile);
550
+ scan.findings = applyOverrides(scan.findings || [], targetAbs);
551
+ scan.secrets = applyOverrides(scan.secrets || [], targetAbs);
552
+ scan.logicVulns = applyOverrides(scan.logicVulns || [], targetAbs);
553
+ const packArg = args.flags.pack;
554
+ const packNames = packArg ? (Array.isArray(packArg) ? packArg : String(packArg).split(',')) : [];
555
+ if (packNames.length) Object.assign(scan, applyPacks(scan, packNames));
556
+
557
+ // Persist the three CI artifacts.
558
+ const stateDir = path.join(targetAbs, '.agentic-security');
559
+ await fsp.mkdir(stateDir, { recursive: true });
560
+ await fsp.writeFile(path.join(stateDir, 'findings.json'),
561
+ JSON.stringify(toJSON(scan, meta), null, 2));
562
+ await fsp.writeFile(path.join(stateDir, 'findings.sarif'),
563
+ JSON.stringify(toSARIF(scan, meta), null, 2));
564
+ await fsp.writeFile(path.join(stateDir, 'findings.junit.xml'),
565
+ toJUnit(scan, meta));
566
+
567
+ const scanCode = exitCodeFor(scan);
568
+ const findings = normalizeFindings(scan);
569
+ const sev = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
570
+ for (const f of findings) sev[f.severity] = (sev[f.severity] || 0) + 1;
571
+ process.stderr.write(
572
+ `[ci] ${findings.length} findings — ${sev.critical} critical · ${sev.high} high · ${sev.medium} medium · ${sev.low} low\n` +
573
+ `[ci] artifacts: .agentic-security/findings.{json,sarif,junit.xml}\n` +
574
+ `[ci] fail-on=${failOn} scan-exit=${scanCode}\n`
575
+ );
576
+ // FR-SDLC-9: when --policy <file.rego> is supplied, evaluate against the
577
+ // findings and fail the gate if the policy denies anything. Policy runs
578
+ // ALONGSIDE the --fail-on threshold; either gate can fail the build.
579
+ const policyFile = args.flags.policy;
580
+ if (policyFile) {
581
+ const { evaluatePolicy } = await import('../src/posture/policy-gate.js');
582
+ const r = evaluatePolicy(path.resolve(policyFile), findings);
583
+ if (!r.ok) {
584
+ console.error(`[ci] policy gate error: ${r.reason || 'unknown'}`);
585
+ return 1;
586
+ }
587
+ if (r.denials.length) {
588
+ console.error(`[ci] policy gate FAILED (${r.runner}, ${r.denials.length} denial(s)):`);
589
+ for (const d of r.denials.slice(0, 20)) console.error(` - ${d}`);
590
+ return 1;
591
+ }
592
+ process.stderr.write(`[ci] policy gate PASSED (${r.runner}, 0 denials)\n`);
593
+ }
594
+ return ciExitCode(scanCode, failOn);
595
+ }
596
+
597
+ // /accept --finding <id> --reason "..." (vibecoder soft 30-day suppression)
598
+ async function cmdAccept(args) {
599
+ const target = path.resolve(args._[1] || '.');
600
+ const id = args.flags.finding;
601
+ if (!id) { console.error('--finding <id> required'); return 4; }
602
+ const reason = args.flags.reason || 'vibecoded for now';
603
+ const lastScanPath = path.join(target, '.agentic-security', 'findings.json');
604
+ if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found. Run `agentic-security scan` first.'); return 4; }
605
+ const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
606
+ const f = (last.findings || []).find(x => x.id === id);
607
+ if (!f) { console.error(`Finding ${id} not found.`); return 4; }
608
+ // Disallow accepting criticals without explicit flag.
609
+ if (f.severity === 'critical' && !args.flags['accept-critical']) {
610
+ console.error('Cannot soft-accept a CRITICAL finding without --accept-critical.');
611
+ return 4;
612
+ }
613
+ const expires = addSoftAcceptance(target, f, reason);
614
+ console.log(`✓ Accepted finding ${id} until ${expires}.`);
615
+ console.log(` ${ATTRIBUTION}`);
616
+ return 0;
617
+ }
618
+
619
+ // /profile set <name> | /profile show
620
+ async function cmdProfile(args) {
621
+ const target = path.resolve(args._[2] || '.');
622
+ const sub = args._[1];
623
+ if (sub === 'show') {
624
+ const p = loadProfile(target);
625
+ console.log(`Profile: ${p.profile}`);
626
+ console.log(` confidence threshold: ${p.confidenceMin}`);
627
+ console.log(` taxonomy visible: ${p.showTaxonomy}`);
628
+ console.log(` suppression schema: ${p.suppression}`);
629
+ console.log(` machine output: ${p.machineOutput ? 'always' : 'on-request'}`);
630
+ console.log(` ${ATTRIBUTION}`);
631
+ return 0;
632
+ }
633
+ if (sub === 'set') {
634
+ const name = args._[2];
635
+ if (name !== 'vibecoder' && name !== 'pro') {
636
+ console.error('profile set <vibecoder|pro>'); return 4;
637
+ }
638
+ const next = saveProfile(target, { profile: name });
639
+ console.log(`✓ Profile set to: ${next.profile}`);
640
+ return 0;
641
+ }
642
+ if (sub === 'detect') {
643
+ const detected = detectProfile(target);
644
+ console.log(`Detected profile: ${detected}`);
645
+ return 0;
646
+ }
647
+ console.error('profile show | profile set <vibecoder|pro> | profile detect');
648
+ return 4;
649
+ }
650
+
651
+ // /triage list | assign | transition | trend
652
+ async function cmdTriage(args) {
653
+ const target = path.resolve(args._[args._.length - 1] && !args._[args._.length - 1].startsWith('--') ? args._[args._.length - 1] : '.');
654
+ const profile = loadProfile(target);
655
+ if (profile.profile !== 'pro') {
656
+ console.error('Triage is a pro-mode feature. Run `agentic-security profile set pro` to enable.');
657
+ return 4;
658
+ }
659
+ const sub = args._[1];
660
+ // Sync first so list reflects the latest scan.
661
+ const lastScanPath = path.join(target, '.agentic-security', 'findings.json');
662
+ if (fs.existsSync(lastScanPath)) {
663
+ const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
664
+ triage.syncWithScan(target, last.findings || []);
665
+ }
666
+ if (sub === 'list') {
667
+ const filter = {};
668
+ if (args.flags.status) filter.state = args.flags.status;
669
+ if (args.flags.severity) filter.severity = args.flags.severity;
670
+ if (args.flags.assignee) filter.assignee = args.flags.assignee;
671
+ if (args.flags.unassigned) filter.unassigned = true;
672
+ const items = triage.list(target, filter);
673
+ const hdr = ['ID', 'Severity', 'State', 'Assignee', 'File:Line', 'Vuln'].join(' ');
674
+ console.log(hdr);
675
+ console.log('─'.repeat(80));
676
+ for (const t of items.slice(0, 50)) {
677
+ console.log([
678
+ t.id.slice(0, 16),
679
+ (t.severity || '').padEnd(8),
680
+ t.state.padEnd(13),
681
+ (t.assignee || '—').padEnd(20),
682
+ `${t.file}:${t.line}`.padEnd(40),
683
+ t.vuln,
684
+ ].join(' '));
685
+ }
686
+ return 0;
687
+ }
688
+ if (sub === 'assign') {
689
+ const id = args._[2];
690
+ const assignee = args._[3] || args.flags.assignee;
691
+ if (!id || !assignee) { console.error('triage assign <id> <assignee>'); return 4; }
692
+ const r = triage.assign(target, id, assignee);
693
+ if (!r.ok) { console.error(r.error); return 4; }
694
+ console.log(`✓ Assigned ${id} to ${assignee}`); return 0;
695
+ }
696
+ if (sub === 'transition') {
697
+ const id = args._[2];
698
+ const state = args._[3];
699
+ const r = triage.transition(target, id, state, args.flags.comment);
700
+ if (!r.ok) { console.error(r.error); return 4; }
701
+ console.log(`✓ ${id} → ${state}`); return 0;
702
+ }
703
+ if (sub === 'trend') {
704
+ const days = parseInt(args.flags.since || '30', 10);
705
+ const t = triage.trend(target, days);
706
+ console.log(`Trend over ${t.sinceDays} days:`);
707
+ console.log(` Opened: ${t.opened}`);
708
+ console.log(` Closed: ${t.closed}`);
709
+ console.log(` Net: ${t.net} (${t.net <= 0 ? 'improving' : 'regressing'})`);
710
+ console.log(` Open: critical=${t.openBySev.critical} high=${t.openBySev.high} medium=${t.openBySev.medium} low=${t.openBySev.low}`);
711
+ if (t.medianMttrDays != null) console.log(` MTTR median: ${t.medianMttrDays.toFixed(1)} days`);
712
+ console.log(` Total open: ${t.totalOpen}`);
713
+ return 0;
714
+ }
715
+ console.error('triage list | assign <id> <assignee> | transition <id> <state> | trend [--since N]');
716
+ return 4;
717
+ }
718
+
719
+ // /org-scan — clone or visit multiple repos, run scan, produce roll-up.
720
+ async function cmdOrgScan(args) {
721
+ const reposCsv = args.flags.repos;
722
+ if (!reposCsv) { console.error('--repos <path1,path2,...> required'); return 4; }
723
+ const repos = reposCsv.split(',').map(s => s.trim()).filter(Boolean);
724
+ const workers = parseInt(args.flags.workers || '4', 10);
725
+ const rollup = { scannedAt: new Date().toISOString(), repos: [] };
726
+
727
+ console.log(`🛡 agentic-security org-scan — ${repos.length} repo(s), ${workers} worker(s)`);
728
+ console.log(` created by ClearCapabilities.Com`);
729
+ console.log('');
730
+
731
+ // Simple bounded concurrency.
732
+ const queue = repos.slice();
733
+ const active = [];
734
+ while (queue.length || active.length) {
735
+ while (active.length < workers && queue.length) {
736
+ const repo = queue.shift();
737
+ const p = (async () => {
738
+ const t0 = Date.now();
739
+ try {
740
+ const { scan, meta } = await runScan(repo);
741
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
742
+ for (const f of scan.findings || []) counts[f.severity || 'medium']++;
743
+ for (const f of scan.secrets || []) counts[f.severity || 'high']++;
744
+ rollup.repos.push({
745
+ repo,
746
+ scanned: scan.filesScanned || 0,
747
+ critical: counts.critical, high: counts.high, medium: counts.medium, low: counts.low,
748
+ elapsed_ms: Date.now() - t0,
749
+ });
750
+ console.log(` ✓ ${repo.padEnd(60)} crit=${counts.critical} high=${counts.high} (${((Date.now() - t0) / 1000).toFixed(1)}s)`);
751
+ } catch (e) {
752
+ rollup.repos.push({ repo, error: e.message });
753
+ console.log(` ✗ ${repo.padEnd(60)} ERROR: ${e.message}`);
754
+ }
755
+ })();
756
+ active.push(p);
757
+ p.finally(() => { const i = active.indexOf(p); if (i >= 0) active.splice(i, 1); });
758
+ }
759
+ if (active.length) await Promise.race(active);
760
+ }
761
+
762
+ const total = rollup.repos.reduce((acc, r) => ({
763
+ critical: acc.critical + (r.critical || 0), high: acc.high + (r.high || 0),
764
+ medium: acc.medium + (r.medium || 0), low: acc.low + (r.low || 0),
765
+ }), { critical: 0, high: 0, medium: 0, low: 0 });
766
+ console.log('');
767
+ console.log('Org-wide summary:');
768
+ console.log(` Critical: ${total.critical} High: ${total.high} Medium: ${total.medium} Low: ${total.low}`);
769
+ const sorted = rollup.repos.filter(r => !r.error).sort((a, b) => (b.critical + b.high) - (a.critical + a.high)).slice(0, 5);
770
+ if (sorted.length) {
771
+ console.log('');
772
+ console.log('Top 5 repos by critical+high:');
773
+ for (const r of sorted) console.log(` ${r.repo.padEnd(60)} crit=${r.critical} high=${r.high}`);
774
+ }
775
+ // Write rollup JSON.
776
+ const out = args.flags.output || 'org-scan-' + new Date().toISOString().slice(0, 10) + '.json';
777
+ await fsp.writeFile(out, JSON.stringify(rollup, null, 2));
778
+ console.log(`\nFull rollup: ${out}`);
779
+ return 0;
780
+ }
781
+
782
+ // /rules validate | rules lock
783
+ async function cmdRules(args) {
784
+ const target = path.resolve(args._[2] || '.');
785
+ const sub = args._[1];
786
+ if (sub === 'validate') {
787
+ const r = validateOverrides(target);
788
+ if (r.ok) { console.log('✓ rules.yml is valid'); return 0; }
789
+ console.error('rules.yml has errors:');
790
+ for (const e of r.errors) console.error(' - ' + e);
791
+ return 4;
792
+ }
793
+ if (sub === 'lock') {
794
+ const { path: fp, lock } = writeLockfile(target);
795
+ console.log(`✓ wrote ${fp}`);
796
+ console.log(` scanner: ${lock.scannerVersion} rulePackHash: ${lock.rulePackHash}`);
797
+ return 0;
798
+ }
799
+ console.error('rules validate | rules lock'); return 4;
800
+ }
801
+
802
+ // `agentic-security secure [--launch]` — smart router. One command picks the
803
+ // right next step based on project state.
804
+ // `agentic-security harness [path] [--include-home] [--format ...]`
805
+ // Multi-harness sweep — discovers .claude/ .cursor/ .codex/ .gemini/ .kiro/
806
+ // .opencode/ .trae/ .qwen/ etc. at the project root (and optionally under ~/)
807
+ // and runs the harness-config detectors directly on each file. Bypasses
808
+ // runScan's shouldScan filter (which excludes .json / .md by default) so
809
+ // the harness-config files actually get inspected.
810
+ async function cmdHarness(args) {
811
+ const root = path.resolve(args._[1] || '.');
812
+ const includeHome = !!args.flags['include-home'];
813
+ const { discoverHarnessConfigs, summarizeHarnessPresence } = await import('../src/posture/harness-discovery.js');
814
+ const { scanClaudeSettings } = await import('../src/sast/claude-settings.js');
815
+ const { scanClaudeMdPromptInjection } = await import('../src/sast/claude-md-prompt-injection.js');
816
+ const { scanClaudeHookInjection } = await import('../src/sast/claude-hook-injection.js');
817
+ const { scanMCP } = await import('../src/sast/mcp-audit.js');
818
+ const { scanCredentials } = await import('../src/secrets/index.js');
819
+
820
+ const fileContents = await discoverHarnessConfigs(root, { includeHome });
821
+ const present = summarizeHarnessPresence(fileContents);
822
+ const fileCount = Object.keys(fileContents).length;
823
+ process.stderr.write(`[harness] discovered harnesses: ${present.length ? present.join(', ') : '(none found)'}\n`);
824
+ process.stderr.write(`[harness] scanning ${fileCount} config file(s)${includeHome ? ' (incl. ~/)' : ''}\n`);
825
+ if (fileCount === 0) {
826
+ process.stdout.write('No harness configuration files found.\n');
827
+ return 0;
828
+ }
829
+
830
+ const findings = [];
831
+ const secrets = [];
832
+ for (const [fp, content] of Object.entries(fileContents)) {
833
+ try { findings.push(...scanClaudeSettings(fp, content)); } catch {}
834
+ try { findings.push(...scanClaudeMdPromptInjection(fp, content)); } catch {}
835
+ try { findings.push(...scanClaudeHookInjection(fp, content)); } catch {}
836
+ try { findings.push(...scanMCP(fp, content)); } catch {}
837
+ try { secrets.push(...scanCredentials(fp, content)); } catch {}
838
+ }
839
+
840
+ // Annotate each finding with a stable id and confidence default so the
841
+ // ship verdict has something to render.
842
+ for (const f of findings) {
843
+ if (!f.confidence) f.confidence = 0.9;
844
+ }
845
+
846
+ const scan = {
847
+ findings,
848
+ secrets,
849
+ logicVulns: [],
850
+ supplyChain: [],
851
+ routes: [],
852
+ components: [],
853
+ suppressions: [],
854
+ filesScanned: fileCount,
855
+ fc: fileContents,
856
+ };
857
+ const meta = { startedAt: new Date().toISOString(), durationMs: 0, mode: 'harness' };
858
+
859
+ const format = args.flags.format || 'cli';
860
+ let body;
861
+ if (format === 'json') body = JSON.stringify(toJSON(scan, meta), null, 2);
862
+ else if (format === 'sarif') body = JSON.stringify(toSARIF(scan, meta), null, 2);
863
+ else if (format === 'md' || format === 'markdown') body = toMarkdown(scan, meta);
864
+ else if (format === 'ship') body = toShipVerdict(scan, { profile: { profile: 'vibecoder', confidenceMin: 0 } });
865
+ else body = toCLIByProfile(scan, { profile: { profile: 'pro', confidenceMin: 0 } });
866
+ // Append a one-line harness-presence footer to CLI output.
867
+ if ((format === 'cli' || format === 'ship') && present.length) {
868
+ body += `\n\nHarnesses discovered: ${present.join(', ')}${includeHome ? ' (project + ~/)' : ' (project only)'}\n`;
869
+ }
870
+ if (args.flags.output) await fsp.writeFile(args.flags.output, body);
871
+ else process.stdout.write(body + '\n');
872
+ return exitCodeFor(scan);
873
+ }
874
+
875
+ // `agentic-security scan-baseline --previous a.json --current b.json [--format cli|json]`
876
+ // Finding-level diff between two scan JSON outputs. Independent of scanner
877
+ // version (use the dedicated `agentic-security-diff` bin for that).
878
+ async function cmdScanBaseline(args) {
879
+ const prevPath = args.flags.previous;
880
+ const currPath = args.flags.current;
881
+ if (!prevPath || !currPath) {
882
+ console.error('Usage: agentic-security scan-baseline --previous <a.json> --current <b.json> [--format cli|json]');
883
+ return 2;
884
+ }
885
+ let prev, curr;
886
+ try { prev = JSON.parse(fs.readFileSync(prevPath, 'utf8')); }
887
+ catch (e) { console.error(`Cannot read previous scan: ${e.message}`); return 2; }
888
+ try { curr = JSON.parse(fs.readFileSync(currPath, 'utf8')); }
889
+ catch (e) { console.error(`Cannot read current scan: ${e.message}`); return 2; }
890
+ const { diffScans, renderDiff } = await import('../src/posture/baseline-compare.js');
891
+ const diff = diffScans(prev, curr);
892
+ if (args.flags.format === 'json') {
893
+ process.stdout.write(JSON.stringify({ summary: { added: diff.added.length, removed: diff.removed.length, changed: diff.changed.length, unchanged: diff.unchanged }, diff }, null, 2));
894
+ } else {
895
+ process.stdout.write(renderDiff(diff));
896
+ }
897
+ // Exit 0 if no delta, 1 if delta — useful for CI gating.
898
+ const hasDelta = diff.added.length || diff.removed.length || diff.changed.length;
899
+ return hasDelta ? 1 : 0;
900
+ }
901
+
902
+ async function cmdSecure(args) {
903
+ const scanRoot = path.resolve(args._[1] || '.');
904
+ const intent = args.flags.launch ? 'launch' : (args.flags.deploy ? 'deploy' : null);
905
+ const decision = decideNextAction({ scanRoot, intent });
906
+ process.stdout.write(explainDecision(decision));
907
+ if (args.flags.json) process.stdout.write(JSON.stringify(decision, null, 2) + '\n');
908
+ if (args.flags.run && /^agentic-security /.test(decision.command)) {
909
+ process.stderr.write(`\n[secure] running: ${decision.command}\n`);
910
+ const sub = decision.command.replace(/^agentic-security /, '').split(' ');
911
+ process.argv = [process.argv[0], process.argv[1], ...sub];
912
+ return main();
913
+ }
914
+ return 0;
915
+ }
916
+
917
+ // `agentic-security tickets sync --provider github|linear|jira [--severity high]`
918
+ async function cmdTickets(args) {
919
+ const sub = args._[1];
920
+ const scanRoot = path.resolve(args.flags.root || '.');
921
+ if (sub === 'sync') {
922
+ const provider = args.flags.provider;
923
+ if (!provider) { console.error('--provider github|linear|jira required'); return 4; }
924
+ const r = await syncTickets({
925
+ scanRoot,
926
+ provider,
927
+ severity: args.flags.severity || 'high',
928
+ repo: args.flags.repo,
929
+ teamId: args.flags['team-id'],
930
+ dryRun: !!args.flags['dry-run'],
931
+ });
932
+ if (!r.ok) { console.error(r.error); return 4; }
933
+ console.log(`✓ tickets sync (${provider}${args.flags['dry-run'] ? ', dry-run' : ''})`);
934
+ console.log(` created: ${r.created.length} closed: ${r.closed.length} failed: ${r.failed.length} tracked: ${r.totalTracked}`);
935
+ for (const c of r.created.slice(0, 10)) console.log(` + ${c.externalId || '(dry-run)'} ${c.id}`);
936
+ for (const c of r.closed.slice(0, 10)) console.log(` ↩ ${c.externalId || '(dry-run)'} ${c.id}`);
937
+ for (const f of r.failed.slice(0, 10)) console.log(` ✗ ${f.id} ${f.error}`);
938
+ return r.failed.length ? 1 : 0;
939
+ }
940
+ if (sub === 'list') {
941
+ const { readState } = await import('../src/integrations/tickets.js');
942
+ const state = readState(scanRoot);
943
+ const entries = Object.entries(state);
944
+ if (!entries.length) { console.log('No tracked tickets.'); return 0; }
945
+ for (const [id, e] of entries) {
946
+ console.log(` ${e.state.padEnd(7)} ${e.provider.padEnd(7)} ${e.externalUrl || e.externalId} ${id}`);
947
+ }
948
+ return 0;
949
+ }
950
+ console.error('Usage: agentic-security tickets sync --provider <github|linear|jira> [--repo OWNER/REPO] [--team-id ID] [--severity high|critical] [--dry-run]');
951
+ return 4;
952
+ }
953
+
954
+ // `agentic-security rule test <fixture-glob>` — test custom rules against fixtures.
955
+ async function cmdRule(args) {
956
+ const sub = args._[1];
957
+ if (sub === 'test') {
958
+ const glob = args._[2];
959
+ if (!glob) { console.error('Usage: agentic-security rule test <fixture-glob>'); return 4; }
960
+ const target = path.resolve(args.flags.root || '.');
961
+ const r = await runRuleTests(target, glob);
962
+ return r.ok ? 0 : 4;
963
+ }
964
+ if (sub === 'list') {
965
+ const target = path.resolve(args.flags.root || '.');
966
+ const rules = loadCustomRules(target);
967
+ if (!rules.length) {
968
+ console.log(`No custom rules in ${path.join(target, '.agentic-security/rules/')}.`);
969
+ return 0;
970
+ }
971
+ for (const r of rules) console.log(` ${r.id} [${r.severity}] ${r.title}`);
972
+ return 0;
973
+ }
974
+ console.error('Usage: agentic-security rule test <glob> | rule list');
975
+ return 4;
976
+ }
977
+
978
+ // packs list — enumerate the curated rule packs available to --pack.
979
+ // Premortem 3R-14: validator-cache GC. .agentic-security/llm-cache/ grows
980
+ // without bound — every cache miss writes a small JSON. After months of CI
981
+ // runs, a project carries hundreds of MB of stale verdicts whose prompt or
982
+ // model versions no longer match. This subcommand prunes entries by age and
983
+ // by prompt-version mismatch.
984
+ async function cmdValidatorCache(args) {
985
+ const sub = args._[1] || 'help';
986
+ const root = path.resolve(args._[2] || '.');
987
+ const cacheDir = path.join(root, '.agentic-security', 'llm-cache');
988
+ if (!fs.existsSync(cacheDir)) {
989
+ console.log(`No validator cache at ${cacheDir}`);
990
+ return 0;
991
+ }
992
+ if (sub === 'list' || sub === 'stats') {
993
+ const entries = await fsp.readdir(cacheDir);
994
+ let total = 0, bytes = 0;
995
+ for (const f of entries) {
996
+ if (!f.endsWith('.json')) continue;
997
+ try {
998
+ const st = await fsp.stat(path.join(cacheDir, f));
999
+ total++; bytes += st.size;
1000
+ } catch {}
1001
+ }
1002
+ console.log(`validator cache: ${total} entries, ${(bytes / 1024).toFixed(1)} KB at ${cacheDir}`);
1003
+ return 0;
1004
+ }
1005
+ if (sub === 'gc' || sub === 'prune') {
1006
+ const olderThanDays = parseInt(args.flags['older-than'] || '30', 10);
1007
+ const dryRun = !!args.flags['dry-run'];
1008
+ const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
1009
+ // Premortem 4R-15: use the public PROMPT_VERSION export rather than
1010
+ // reaching through the underscore-prefixed _internal API.
1011
+ const { PROMPT_VERSION } = await import('../src/llm-validator/index.js');
1012
+ if (!PROMPT_VERSION) {
1013
+ console.error('agentic-security: validator module did not export PROMPT_VERSION — refusing to GC (would prune everything).');
1014
+ return 4;
1015
+ }
1016
+ const wantedPromptVersion = PROMPT_VERSION;
1017
+ const entries = await fsp.readdir(cacheDir);
1018
+ let removed = 0, kept = 0, bytesFreed = 0;
1019
+ for (const f of entries) {
1020
+ if (!f.endsWith('.json')) continue;
1021
+ const fp = path.join(cacheDir, f);
1022
+ let st, body;
1023
+ try { st = await fsp.stat(fp); } catch { continue; }
1024
+ try { body = JSON.parse(await fsp.readFile(fp, 'utf8')); } catch { body = null; }
1025
+ const tooOld = st.mtimeMs < cutoff;
1026
+ const wrongVersion = body && wantedPromptVersion && body.prompt_version && body.prompt_version !== wantedPromptVersion;
1027
+ if (tooOld || wrongVersion) {
1028
+ if (!dryRun) { try { await fsp.unlink(fp); } catch {} }
1029
+ removed++; bytesFreed += st.size;
1030
+ } else { kept++; }
1031
+ }
1032
+ console.log(`${dryRun ? '[dry-run] would remove' : 'removed'} ${removed} entries (${(bytesFreed / 1024).toFixed(1)} KB), kept ${kept}.`);
1033
+ return 0;
1034
+ }
1035
+ console.error('Usage: agentic-security validator-cache <stats|gc> [path] [--older-than <days>] [--dry-run]');
1036
+ return 4;
1037
+ }
1038
+
1039
+ // `agentic-security verify [--finding <id>] [--target <url>] [--live]`
1040
+ //
1041
+ // Re-runs the verifier loop over the most-recent scan. Without --live, it
1042
+ // validates each finding's PoC (refuses destructive payloads, hardcoded
1043
+ // metadata IPs, runaway lengths) and assigns a static verdict. With --live
1044
+ // AND --target, it actually executes each PoC in a Docker sandbox (or
1045
+ // subprocess fallback) against the supplied URL.
1046
+ //
1047
+ // FR-VER-7 fail-closed: any error → cannot-verify, never silent drop.
1048
+ async function cmdVerify(args) {
1049
+ const scanRoot = path.resolve(args.flags.root || '.');
1050
+ const lastScanPath = path.join(scanRoot, '.agentic-security', 'last-scan.json');
1051
+ if (!fs.existsSync(lastScanPath)) {
1052
+ console.error(`No prior scan found at ${lastScanPath}. Run \`agentic-security scan\` first.`);
1053
+ return 4;
1054
+ }
1055
+ const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
1056
+ const findings = last.findings || [];
1057
+ let targetFlag = args.flags.target || process.env.AGENTIC_SECURITY_VERIFY_TARGET || null;
1058
+ const liveFlag = !!args.flags.live || process.env.AGENTIC_SECURITY_VERIFY_LIVE === '1';
1059
+ // FR-LIVE-HARNESS: if no --target was supplied, check the
1060
+ // .agentic-security/verifier-target.yaml manifest. We don't bring up the
1061
+ // target here (that's the operator's call); we surface the URL it declares.
1062
+ if (liveFlag && !targetFlag) {
1063
+ const { loadTargetManifest, describeTarget, validateTarget } =
1064
+ await import('../src/posture/verifier-target.js');
1065
+ const m = loadTargetManifest(scanRoot);
1066
+ if (m.ok) {
1067
+ const v = validateTarget(m.target);
1068
+ if (!v.ok) {
1069
+ console.error(`Verifier target manifest rejected: ${v.reason}`);
1070
+ return 4;
1071
+ }
1072
+ targetFlag = m.target.url;
1073
+ console.error(`Verifier target: ${describeTarget(m.target)}`);
1074
+ console.error(`(Read from .agentic-security/verifier-target.yaml; bring it up yourself before re-running --live.)`);
1075
+ } else {
1076
+ console.error('--live requires --target <url>, AGENTIC_SECURITY_VERIFY_TARGET, or a .agentic-security/verifier-target.yaml manifest.');
1077
+ console.error(` Manifest check: ${m.reason}`);
1078
+ return 4;
1079
+ }
1080
+ }
1081
+ if (liveFlag) {
1082
+ // Set the env so verifier.js picks it up. We don't permanently mutate
1083
+ // process.env beyond this run.
1084
+ process.env.AGENTIC_SECURITY_VERIFY_LIVE = '1';
1085
+ process.env.AGENTIC_SECURITY_VERIFY_TARGET = targetFlag;
1086
+ }
1087
+ const { annotateVerifierVerdicts, verifierCoverageSummary } = await import('../src/posture/verifier.js');
1088
+ const filter = args.flags.finding ? findings.filter(f => f.id === args.flags.finding || f.stableId === args.flags.finding) : findings;
1089
+ if (!filter.length) {
1090
+ console.error(`No matching findings (use --finding <id>).`);
1091
+ return 4;
1092
+ }
1093
+ // Load file contents so sanitizer-absence proofs can run. Only load the
1094
+ // files referenced by the findings being verified, to keep this fast even
1095
+ // on large projects.
1096
+ const fileContents = {};
1097
+ const fileSet = new Set();
1098
+ for (const f of filter) {
1099
+ const fp = f.file || f.sink?.file;
1100
+ if (fp) fileSet.add(fp);
1101
+ }
1102
+ for (const rel of fileSet) {
1103
+ try {
1104
+ const abs = path.resolve(scanRoot, rel);
1105
+ const st = fs.statSync(abs);
1106
+ if (st.size <= 500_000) fileContents[rel] = fs.readFileSync(abs, 'utf8');
1107
+ } catch { /* file missing or unreadable; skip */ }
1108
+ }
1109
+ annotateVerifierVerdicts(filter, { target: targetFlag, fileContents });
1110
+ const sum = verifierCoverageSummary(filter);
1111
+ console.log(`Verified ${filter.length} finding(s):`);
1112
+ for (const [k, v] of Object.entries(sum)) console.log(` ${k}: ${v}`);
1113
+ if (args.flags.verbose || args.flags.finding) {
1114
+ for (const f of filter) {
1115
+ console.log(` ${f.file}:${f.line} ${f.vuln}`);
1116
+ console.log(` → ${f.verifier_verdict || 'none'} (${f.verifier_reason || 'no-reason'})${f.verifier_runner ? ' [' + f.verifier_runner + ']' : ''}`);
1117
+ }
1118
+ }
1119
+ // Persist back to last-scan.json so downstream tools see the verdicts.
1120
+ last.findings = findings;
1121
+ await fsp.writeFile(lastScanPath, JSON.stringify(last, null, 2));
1122
+ return 0;
1123
+ }
1124
+
1125
+ // `agentic-security reset [--yes] [--keep <rules|streak|...>]`
1126
+ //
1127
+ // FR-LEARN-7 right-to-delete: wipes the learned-state files under
1128
+ // .agentic-security/ that the engine accumulates across runs:
1129
+ //
1130
+ // - validator-metrics.json (per-CWE TP/FP scorecard)
1131
+ // - triage-feedback.json (active-learning verdicts)
1132
+ // - llm-cache/* (LLM validator responses)
1133
+ // - scan-history.json (security-trend snapshots)
1134
+ // - fix-history/{log,backups} (auto-fix history)
1135
+ // - last-scan.json[.sig]
1136
+ // - shadow-findings.json
1137
+ // - mcp-audit.log
1138
+ // - hook-throttle.json
1139
+ // - tickets.json (two-way ticket sync state)
1140
+ //
1141
+ // Preserves by default:
1142
+ // - rules.yml (operator-authored, not learned)
1143
+ // - rules/ (custom rule files)
1144
+ // - license-policy.yml (operator-authored)
1145
+ // - trusted-keys.json (signing trust root)
1146
+ // - ruleset-version.json (pinning intent)
1147
+ //
1148
+ // Use --keep <names> (comma-separated) to preserve specific items;
1149
+ // --yes to skip the confirmation prompt (for scripted use).
1150
+ async function cmdReset(args) {
1151
+ const scanRoot = path.resolve(args.flags.root || '.');
1152
+ const stateDir = path.join(scanRoot, '.agentic-security');
1153
+ if (!fs.existsSync(stateDir)) {
1154
+ console.log(`No state to reset at ${stateDir}`);
1155
+ return 0;
1156
+ }
1157
+ const WIPE = new Set([
1158
+ 'validator-metrics.json',
1159
+ 'triage-feedback.json',
1160
+ 'scan-history.json',
1161
+ 'last-scan.json',
1162
+ 'last-scan.json.sig',
1163
+ 'shadow-findings.json',
1164
+ 'mcp-audit.log',
1165
+ 'hook-throttle.json',
1166
+ 'tickets.json',
1167
+ 'streak.json',
1168
+ 'findings.json',
1169
+ 'findings.sarif',
1170
+ 'findings.csv',
1171
+ ]);
1172
+ const WIPE_DIRS = new Set([
1173
+ 'llm-cache',
1174
+ 'fix-history',
1175
+ 'fix-plans',
1176
+ ]);
1177
+ const keep = new Set((args.flags.keep || '').split(',').filter(Boolean));
1178
+ const targets = [];
1179
+ for (const entry of await fsp.readdir(stateDir, { withFileTypes: true })) {
1180
+ if (keep.has(entry.name)) continue;
1181
+ if (WIPE.has(entry.name) || WIPE_DIRS.has(entry.name)) {
1182
+ targets.push({ name: entry.name, dir: entry.isDirectory() });
1183
+ }
1184
+ }
1185
+ if (!targets.length) {
1186
+ console.log(`Nothing to reset under ${stateDir}.`);
1187
+ return 0;
1188
+ }
1189
+ console.log(`agentic-security reset — will remove from ${stateDir}:`);
1190
+ for (const t of targets) console.log(` ${t.name}${t.dir ? '/' : ''}`);
1191
+ console.log('');
1192
+ console.log('Preserving operator-authored config: rules.yml, rules/, license-policy.yml, trusted-keys.json, ruleset-version.json');
1193
+ if (!args.flags.yes) {
1194
+ console.log('');
1195
+ console.log('Pass --yes to proceed (or --keep <name,name> to spare specific items).');
1196
+ return 0;
1197
+ }
1198
+ for (const t of targets) {
1199
+ const p = path.join(stateDir, t.name);
1200
+ try {
1201
+ if (t.dir) await fsp.rm(p, { recursive: true, force: true });
1202
+ else await fsp.rm(p, { force: true });
1203
+ } catch (e) {
1204
+ console.error(`reset: failed to remove ${p}: ${e.message}`);
1205
+ }
1206
+ }
1207
+ console.log(`Reset ${targets.length} item(s). Operator-authored config preserved.`);
1208
+ return 0;
1209
+ }
1210
+
1211
+ // `agentic-security rule-synth [--dry-run] [--threshold N]`
1212
+ //
1213
+ // FR-LEARN-6: read triage-feedback.json, group repeated FP verdicts by
1214
+ // (family, dir prefix), and propose a suppression YAML when ≥ threshold
1215
+ // (default 5) verdicts cluster. Writes to .agentic-security/rules-proposed/.
1216
+ async function cmdRuleSynth(args) {
1217
+ const scanRoot = path.resolve(args.flags.root || '.');
1218
+ const { synthesizeRules } = await import('../src/posture/rule-synthesis.js');
1219
+ const proposals = synthesizeRules(scanRoot, {
1220
+ threshold: args.flags.threshold,
1221
+ dryRun: !!args.flags['dry-run'],
1222
+ });
1223
+ if (!proposals.length) {
1224
+ console.log('No proposals — either no triage feedback, or no shape clustered above threshold.');
1225
+ return 0;
1226
+ }
1227
+ console.log(`Synthesised ${proposals.length} proposal(s) in .agentic-security/rules-proposed/:`);
1228
+ for (const p of proposals) {
1229
+ console.log(` ${p.file} (${p.count} FPs, family=${p.family || p.rule}, glob=${p.dirGlob})`);
1230
+ }
1231
+ console.log('');
1232
+ console.log('Review each YAML before moving it to .agentic-security/rules/ to make it active.');
1233
+ return 0;
1234
+ }
1235
+
1236
+ async function cmdPacks(args) {
1237
+ const sub = args._[1] || 'list';
1238
+ if (sub !== 'list') { console.error('Usage: agentic-security packs list'); return 4; }
1239
+ const rows = listPacks();
1240
+ const namePad = Math.max(...rows.map(r => r.name.length));
1241
+ console.log('Available rule packs (use --pack <name>):\n');
1242
+ for (const r of rows) {
1243
+ console.log(` ${r.name.padEnd(namePad)} ${r.description} [${r.cweCount} CWEs]`);
1244
+ }
1245
+ return 0;
1246
+ }
1247
+
1248
+ // /digest --slack <webhook> | --discord <webhook>
1249
+ async function cmdDigest(args) {
1250
+ const target = path.resolve(args._[1] || '.');
1251
+ const profile = loadProfile(target);
1252
+ const lastScanPath = path.join(target, '.agentic-security', 'findings.json');
1253
+ if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found.'); return 4; }
1254
+ const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
1255
+ const findings = (last.findings || []).filter(f => f.severity === 'critical' || f.severity === 'high');
1256
+ const summary = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
1257
+ for (const f of (last.findings || [])) summary[f.severity || 'medium']++;
1258
+ const project = args.flags.project || path.basename(target);
1259
+ if (args.flags.slack) {
1260
+ const payload = buildSlackDigest(findings, summary, { project });
1261
+ const r = await postWebhook(args.flags.slack, payload);
1262
+ console.log(r.ok ? `✓ Slack digest sent` : `✗ Slack failed: ${r.reason || r.status}`);
1263
+ return r.ok ? 0 : 4;
1264
+ }
1265
+ if (args.flags.discord) {
1266
+ const payload = buildDiscordDigest(findings, summary, { project });
1267
+ const r = await postWebhook(args.flags.discord, payload);
1268
+ console.log(r.ok ? `✓ Discord digest sent` : `✗ Discord failed: ${r.reason || r.status}`);
1269
+ return r.ok ? 0 : 4;
1270
+ }
1271
+ console.error('digest --slack <url> OR digest --discord <url>'); return 4;
1272
+ }
1273
+
1274
+ async function cmdFix(args) {
1275
+ const id = args.flags.finding;
1276
+ const isPreview = !!args.flags.preview;
1277
+ const isApply = !!args.flags.apply;
1278
+ const scanRoot = path.resolve(args.flags.root || '.');
1279
+ if (!id) { console.error('--finding <id> required'); return 4; }
1280
+ const lastScanPath = path.join(scanRoot, '.agentic-security', 'last-scan.json');
1281
+ if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found. Run `agentic-security scan` first.'); return 4; }
1282
+ const lastScanBody = await fsp.readFile(lastScanPath, 'utf8');
1283
+ const sigVerified = _verifyLastScan(lastScanBody, lastScanPath + '.sig');
1284
+ if (sigVerified === false) {
1285
+ console.error('Warning: last-scan.json integrity check failed — file may have been modified outside the scanner. Re-run `agentic-security scan` to refresh.');
1286
+ }
1287
+ const last = JSON.parse(lastScanBody);
1288
+ const f = (last.findings || []).find(x => x.id === id) || (last.secrets || []).find(x => x.id === id);
1289
+ if (!f) { console.error(`Finding ${id} not found in last scan.`); return 4; }
1290
+
1291
+ // Default mode: print the canonical template (back-compat — security-fixer subagent applies it).
1292
+ if (!isPreview && !isApply) {
1293
+ console.log(JSON.stringify(f, null, 2));
1294
+ if (f.fix?.code) { console.log('\n--- suggested patch ---\n'); console.log(f.fix.code); }
1295
+ console.log('\nUse --preview to see a diff, or --apply to apply directly.');
1296
+ return 0;
1297
+ }
1298
+
1299
+ // Both --preview and --apply require an actual replacement to operate on.
1300
+ // For now we accept either f.fix.replacement (full new file content) or
1301
+ // f.fix.replaceLine (single-line replacement). Anything else falls back
1302
+ // to the template output and tells the user to run the security-fixer subagent.
1303
+ const absFile = path.resolve(scanRoot, f.file);
1304
+ if (!fs.existsSync(absFile)) { console.error(`File not found: ${absFile}`); return 4; }
1305
+ const originalContent = await fsp.readFile(absFile, 'utf8');
1306
+ let newContent = null;
1307
+ if (typeof f.fix?.replacement === 'string') newContent = f.fix.replacement;
1308
+ else if (typeof f.fix?.replaceLine === 'string' && f.line) {
1309
+ const lines = originalContent.split('\n');
1310
+ if (lines[f.line - 1] !== undefined) {
1311
+ lines[f.line - 1] = f.fix.replaceLine;
1312
+ newContent = lines.join('\n');
1313
+ }
1314
+ }
1315
+
1316
+ if (newContent === null) {
1317
+ console.error('No mechanical fix is available for this finding. Use the security-fixer subagent (default `fix` mode) and apply with `--apply` after it produces a replacement.');
1318
+ return 4;
1319
+ }
1320
+
1321
+ if (isPreview) {
1322
+ console.log(previewDiff(originalContent, newContent, f.file));
1323
+ console.log('\nRun with --apply to write this change. Use `agentic-security undo` to revert.');
1324
+ return 0;
1325
+ }
1326
+
1327
+ // --apply. Premortem 4R-8: pass stableId from the engine directly so the
1328
+ // recover() cross-check is robust against line-number drift (f.id is
1329
+ // `${file}:${line}:${rule}` and rotates when the user edits the file).
1330
+ const entry = await applyFix({
1331
+ scanRoot, file: f.file, originalContent, newContent,
1332
+ findingId: f.id, stableId: f.stableId || null,
1333
+ ruleId: f.cwe || f.title, vuln: f.vuln || f.title,
1334
+ });
1335
+ console.log(`✓ applied fix ${entry.id} (file: ${entry.file})`);
1336
+ console.log(` backup: ${entry.backupPath}`);
1337
+ console.log(` revert with: agentic-security undo`);
1338
+ return 0;
1339
+ }
1340
+
1341
+ // `agentic-security undo` — revert the most recent fix (or --all).
1342
+ async function cmdUndo(args) {
1343
+ const scanRoot = path.resolve(args.flags.root || '.');
1344
+ if (args.flags.list) {
1345
+ const log = listHistory(scanRoot);
1346
+ if (!log.length) { console.log('No fix history.'); return 0; }
1347
+ for (const e of log) {
1348
+ const status = e.reverted ? '↩ reverted' : '✓ applied ';
1349
+ console.log(` ${status} ${e.id} ${e.file} (${e.vuln || e.findingId})`);
1350
+ }
1351
+ return 0;
1352
+ }
1353
+ if (args.flags.compact) {
1354
+ // Premortem 3R-17: surface log compaction so operators can keep the
1355
+ // fix-history dir bounded on long-lived projects.
1356
+ const retainDays = parseInt(args.flags['retain-days'] || '90', 10);
1357
+ const r = await compactLog(scanRoot, { retainDays, pruneBackups: !!args.flags['prune-backups'] });
1358
+ console.log(`Compacted: archived ${r.archived} entries, retained ${r.kept} in active log.`);
1359
+ return 0;
1360
+ }
1361
+ if (args.flags.all) {
1362
+ const reverted = await undoAll(scanRoot);
1363
+ if (!reverted.length) { console.log('Nothing to revert.'); return 0; }
1364
+ for (const e of reverted) console.log(`↩ reverted ${e.id} ${e.file}`);
1365
+ console.log(`Reverted ${reverted.length} fix(es).`);
1366
+ return 0;
1367
+ }
1368
+ const r = await undoLast(scanRoot);
1369
+ if (!r) { console.log('Nothing to revert.'); return 0; }
1370
+ if (r.error) { console.error(r.error); return 4; }
1371
+ console.log(`↩ reverted ${r.id} ${r.file}`);
1372
+ console.log(` finding: ${r.vuln || r.findingId}`);
1373
+ return 0;
1374
+ }
1375
+
1376
+ async function cmdSetup(args) {
1377
+ const projectDir = path.resolve(args._[1] || '.');
1378
+ const commandsDir = path.join(projectDir, '.claude', 'commands');
1379
+ await fsp.mkdir(commandsDir, { recursive: true });
1380
+ const bundle = path.resolve(process.argv[1]);
1381
+
1382
+ const commands = {
1383
+ 'security-scan-all.md': `---
1384
+ description: Run a full security scan (SAST + SCA + Secrets) on this project or a given path.
1385
+ argument-hint: "[path]"
1386
+ ---
1387
+ \`\`\`bash
1388
+ node ${bundle} scan \${1:-.}; ec=$?; [ $ec -le 3 ] && exit 0 || exit $ec
1389
+ \`\`\`
1390
+ Output is a grouped summary: severity counts, finding types by frequency, top affected files.
1391
+ Use \`--format cli\` for the full per-finding list. Findings are always saved to \`.agentic-security/last-scan.json\`.
1392
+ If you see critical findings, run \`/fix-all --severity critical\` to remediate.
1393
+ `,
1394
+ 'security-fix.md': `---
1395
+ description: Apply a remediation patch for a single finding from the last scan.
1396
+ argument-hint: "<finding-id>"
1397
+ ---
1398
+ \`\`\`bash
1399
+ node ${bundle} fix --finding \${1}
1400
+ \`\`\`
1401
+ Hand the finding to the security-fixer subagent: read the file, apply the fix template adapted to the surrounding code, and run the project's test command. Do not declare done until the finding no longer reproduces on re-scan.
1402
+ `,
1403
+ 'fix-all.md': `---
1404
+ description: Remediate every finding at or above a severity threshold (default: critical).
1405
+ argument-hint: "[--severity critical|high|medium]"
1406
+ ---
1407
+
1408
+ Read \`.agentic-security/last-scan.json\`. For every finding at or above \`\${1:-critical}\` severity, dispatch the security-fixer subagent in sequence — not in parallel, as each fix may change subsequent findings. After each batch, re-run \`/security-scan-all\` to confirm. Stop and report if any test fails.
1409
+ `,
1410
+ 'security-report.md': `---
1411
+ description: Generate an HTML security report (or JSON / Markdown / SARIF).
1412
+ argument-hint: "[--format html|json|md|sarif] [--output <file>]"
1413
+ ---
1414
+ \`\`\`bash
1415
+ node ${bundle} scan . --format \${1:-html} --output \${2:-security-report.html}
1416
+ \`\`\`
1417
+ Default produces \`security-report.html\` — a self-contained interactive page with severity charts and filterable findings. Open with \`open security-report.html\`.
1418
+ `,
1419
+ 'security-sca.md': `---
1420
+ description: Run a dependency vulnerability scan (SCA only) against this project.
1421
+ argument-hint: "[path]"
1422
+ ---
1423
+ \`\`\`bash
1424
+ node ${bundle} scan \${1:-.} --only sca --format cli
1425
+ \`\`\`
1426
+ `,
1427
+ 'security-secrets.md': `---
1428
+ description: Scan for leaked credentials and hardcoded secrets.
1429
+ argument-hint: "[path]"
1430
+ ---
1431
+ \`\`\`bash
1432
+ node ${bundle} scan \${1:-.} --only secrets --format cli
1433
+ \`\`\`
1434
+ `,
1435
+ 'security-triage.md': `---
1436
+ description: Validate scan findings for false positives and suppress confirmed FPs before reporting.
1437
+ argument-hint: "[--severity critical|high|all]"
1438
+ ---
1439
+
1440
+ Read \`.agentic-security/last-scan.json\` and validate each finding at or above \`\${1:-critical}\` severity for false positives.
1441
+
1442
+ For each finding:
1443
+ 1. Read the file at the reported path and extract ±20 lines around the flagged line
1444
+ 2. Evaluate whether it is a **true positive** using these criteria:
1445
+ - **True positive**: user-controlled input demonstrably reaches the sink without validation — flag it
1446
+ - **False positive**: the value is validated against an allowlist / switch / explicit enum before the sink, the sink is a safe API overload (e.g. \`execFile\` with an array, parameterized query), the finding is in a test fixture or mock, or the "source" is an internal constant rather than external input
1447
+ 3. For each confirmed false positive, add a suppression entry to \`.agentic-security/rules.yml\`:
1448
+
1449
+ \`\`\`yaml
1450
+ suppressions:
1451
+ - rule: "<vuln name from finding>"
1452
+ files: ["<file path>"]
1453
+ reason: "<one sentence: why this is a FP>"
1454
+ \`\`\`
1455
+
1456
+ If \`.agentic-security/rules.yml\` does not exist, create it with the suppressions block.
1457
+
1458
+ After processing all findings, print a summary table:
1459
+
1460
+ | File:Line | Vulnerability | Verdict | Reason |
1461
+ |---|---|---|---|
1462
+ | ... | ... | TP / FP | ... |
1463
+
1464
+ Then re-run the scan so suppressions take effect:
1465
+
1466
+ \`\`\`bash
1467
+ node ${bundle} scan .; ec=$?; [ $ec -le 3 ] && exit 0 || exit $ec
1468
+ \`\`\`
1469
+
1470
+ Do not suppress anything you are not certain is a false positive. When in doubt, mark it TP and leave remediation to \`/security-fix\`.
1471
+ `,
1472
+ };
1473
+
1474
+ for (const [name, content] of Object.entries(commands)) {
1475
+ await fsp.writeFile(path.join(commandsDir, name), content);
1476
+ }
1477
+
1478
+ const names = Object.keys(commands).map(f => '/' + f.replace('.md', '')).join(', ');
1479
+ console.log(`✓ Installed ${Object.keys(commands).length} command shortcuts in ${commandsDir}`);
1480
+ console.log(` ${names}`);
1481
+ console.log('');
1482
+ console.log('These work in this project only. Re-run in other projects as needed.');
1483
+ return 0;
1484
+ }
1485
+
1486
+ async function main() {
1487
+ const args = parseArgs(process.argv.slice(2));
1488
+ const cmd = args._[0];
1489
+ try {
1490
+ switch (cmd) {
1491
+ case 'scan': process.exit(await cmdScan(args));
1492
+ case 'ship': process.exit(await cmdShip(args));
1493
+ case 'ci': process.exit(await cmdCi(args));
1494
+ case 'fix': process.exit(await cmdFix(args));
1495
+ case 'undo': process.exit(await cmdUndo(args));
1496
+ case 'accept': process.exit(await cmdAccept(args));
1497
+ case 'profile': process.exit(await cmdProfile(args));
1498
+ case 'triage': process.exit(await cmdTriage(args));
1499
+ case 'org-scan': process.exit(await cmdOrgScan(args));
1500
+ case 'rules': process.exit(await cmdRules(args));
1501
+ case 'rule': process.exit(await cmdRule(args));
1502
+ case 'tickets': process.exit(await cmdTickets(args));
1503
+ case 'secure': process.exit(await cmdSecure(args));
1504
+ case 'packs': process.exit(await cmdPacks(args));
1505
+ case 'validator-cache': process.exit(await cmdValidatorCache(args));
1506
+ case 'verify': process.exit(await cmdVerify(args));
1507
+ case 'reset': process.exit(await cmdReset(args));
1508
+ case 'rule-synth': process.exit(await cmdRuleSynth(args));
1509
+ case 'digest': process.exit(await cmdDigest(args));
1510
+ case 'setup': process.exit(await cmdSetup(args));
1511
+ case 'mcp': {
1512
+ const { runStdio } = await import('../src/mcp/stdio.js');
1513
+ const root = args.flags.root || process.env.AGENTIC_SECURITY_MCP_ROOT || process.cwd();
1514
+ runStdio({ sessionRoot: path.resolve(root) });
1515
+ return;
1516
+ }
1517
+ case 'cve-watch': {
1518
+ // Continuous CVE-watch daemon (one-shot). Polls OSV for the project's
1519
+ // dependency tree, fires the configured webhook on each new advisory.
1520
+ // Designed to be invoked from cron or a GitHub Action; the state file
1521
+ // (.agentic-security/cve-alerts-state.json) deduplicates across runs.
1522
+ const { runOnce } = await import('../src/posture/cve-alert-daemon.js');
1523
+ const root = args.flags.root || process.cwd();
1524
+ const r = await runOnce(path.resolve(root), {
1525
+ alertUrl: args.flags['alert-url'],
1526
+ alertType: args.flags['alert-type'],
1527
+ minSeverity: args.flags['min-severity'],
1528
+ dryRun: args.flags['dry-run'] === true,
1529
+ });
1530
+ if (args.flags.json) {
1531
+ // Stringify Set/etc. safely.
1532
+ console.log(JSON.stringify(r, null, 2));
1533
+ } else if (!r.ok) {
1534
+ console.error(`cve-watch: ${r.reason || 'failed'}`);
1535
+ }
1536
+ process.exit(r.ok ? 0 : 1);
1537
+ }
1538
+ case 'pr-delta': {
1539
+ // Shadowscan: compute the security delta between two git refs.
1540
+ // Useful in PR CI to show ONLY what changed, not the absolute
1541
+ // finding count. Pairs with `pr-comment` to render the result.
1542
+ const { computePrDelta, renderPrDeltaText } = await import('../src/pr-delta.js');
1543
+ const root = args.flags.root || process.cwd();
1544
+ const baseRef = args.flags.base || args.flags.b;
1545
+ const headRef = args.flags.head || args.flags.h || 'HEAD';
1546
+ if (!baseRef) { console.error('pr-delta: --base <ref> is required'); process.exit(2); }
1547
+ const delta = await computePrDelta(path.resolve(root), { baseRef, headRef });
1548
+ if (args.flags.json) console.log(JSON.stringify(delta, null, 2));
1549
+ else console.log(renderPrDeltaText(delta));
1550
+ // Exit non-zero if any critical/high introduced (useful as CI gate).
1551
+ const i = delta.summary?.introduced || {};
1552
+ const blocking = (i.critical || 0) + (i.high || 0);
1553
+ process.exit(args.flags['fail-on-introduced'] && blocking > 0 ? 1 : 0);
1554
+ }
1555
+ case 'pr-comment': {
1556
+ // Render the advisor-tone PR comment from a delta (stdin or
1557
+ // pr-delta --json output). Reads JSON from --in <path> or stdin.
1558
+ const { renderPrComment } = await import('../src/pr-comment.js');
1559
+ const { computePrDelta } = await import('../src/pr-delta.js');
1560
+ const fs2 = await import('node:fs');
1561
+ let delta;
1562
+ if (args.flags.base) {
1563
+ const root = args.flags.root || process.cwd();
1564
+ delta = await computePrDelta(path.resolve(root), {
1565
+ baseRef: args.flags.base, headRef: args.flags.head || 'HEAD',
1566
+ });
1567
+ } else if (args.flags.in) {
1568
+ delta = JSON.parse(fs2.readFileSync(args.flags.in, 'utf8'));
1569
+ } else {
1570
+ const data = await new Promise(r => {
1571
+ const chunks = []; process.stdin.on('data', c => chunks.push(c));
1572
+ process.stdin.on('end', () => r(Buffer.concat(chunks).toString('utf8')));
1573
+ });
1574
+ delta = JSON.parse(data);
1575
+ }
1576
+ const comment = renderPrComment(delta, {
1577
+ repoName: args.flags.repo, prNumber: args.flags.pr, prTitle: args.flags.title,
1578
+ });
1579
+ console.log(comment);
1580
+ process.exit(0);
1581
+ }
1582
+ case 'badge': {
1583
+ // Emit a live SVG badge from the most recent scan. Drop the URL
1584
+ // (or inline SVG) into README for pull-marketing.
1585
+ const { renderBadge } = await import('../src/badge.js');
1586
+ const root = args.flags.root || process.cwd();
1587
+ const format = args.flags.format || 'svg';
1588
+ const style = args.flags.style || 'flat';
1589
+ console.log(renderBadge({ format, style, scanRoot: path.resolve(root) }));
1590
+ process.exit(0);
1591
+ }
1592
+ case 'leaderboard-row': {
1593
+ // Generate one repo's leaderboard row (JSON). The future public
1594
+ // leaderboard at agentic-security.dev/leaderboard aggregates rows.
1595
+ const { leaderboardRowFor } = await import('../src/leaderboard.js');
1596
+ const root = args.flags.root || process.cwd();
1597
+ const repo = args.flags.repo;
1598
+ if (!repo) { console.error('leaderboard-row: --repo <owner/name> is required'); process.exit(2); }
1599
+ const row = leaderboardRowFor({ scanRoot: path.resolve(root), repo });
1600
+ console.log(JSON.stringify(row, null, 2));
1601
+ process.exit(0);
1602
+ }
1603
+ case 'history': {
1604
+ // Time-travel scan. Walk N historical git refs within --since,
1605
+ // scan each, emit a per-ref timeline + introduced/resolved deltas
1606
+ // between consecutive snapshots.
1607
+ const { runHistory } = await import('../src/history-scan.js');
1608
+ const root = args.flags.root || process.cwd();
1609
+ const r = await runHistory(path.resolve(root), {
1610
+ since: args.flags.since || '6.months',
1611
+ interval: args.flags.interval || '1.month',
1612
+ });
1613
+ if (args.flags.json) console.log(JSON.stringify(r, null, 2));
1614
+ else if (r.error) console.error(`history: ${r.error}`);
1615
+ else {
1616
+ console.log(`Scanned ${r.refs.length} refs.`);
1617
+ for (const ev of r.timeline) {
1618
+ console.log(` ${ev.fromWhen} → ${ev.toWhen}: +${ev.introducedN} introduced, -${ev.resolvedN} resolved`);
1619
+ }
1620
+ }
1621
+ process.exit(r.error ? 1 : 0);
1622
+ }
1623
+ case 'what-if': {
1624
+ // Counterfactual scan. Apply file overlays + virtual deletes to
1625
+ // the working tree, scan, return delta vs. baseline.
1626
+ const { runWhatIf } = await import('../src/history-scan.js');
1627
+ const root = args.flags.root || process.cwd();
1628
+ const overlays = [];
1629
+ const overlayArg = args.flags.overlay;
1630
+ if (overlayArg) {
1631
+ // overlay format: <relpath>:<source-file>
1632
+ for (const spec of Array.isArray(overlayArg) ? overlayArg : [overlayArg]) {
1633
+ const idx = spec.indexOf(':');
1634
+ if (idx < 0) continue;
1635
+ const file = spec.slice(0, idx);
1636
+ const src = spec.slice(idx + 1);
1637
+ try {
1638
+ overlays.push({ file, content: (await import('node:fs')).readFileSync(src, 'utf8') });
1639
+ } catch (e) {
1640
+ console.error(`what-if: cannot read overlay source ${src}: ${e.message}`);
1641
+ process.exit(1);
1642
+ }
1643
+ }
1644
+ }
1645
+ const remove = args.flags.remove
1646
+ ? (Array.isArray(args.flags.remove) ? args.flags.remove : [args.flags.remove])
1647
+ : [];
1648
+ const r = await runWhatIf(path.resolve(root), { overlays, remove });
1649
+ if (args.flags.json) console.log(JSON.stringify(r, null, 2));
1650
+ else {
1651
+ console.log(`baseline: ${r.baselineFindings} findings`);
1652
+ console.log(`what-if: ${r.whatIfFindings} findings (delta ${r.delta >= 0 ? '+' : ''}${r.delta})`);
1653
+ if (r.introduced.length) {
1654
+ console.log(`Introduced by this counterfactual:`);
1655
+ for (const f of r.introduced.slice(0, 20)) {
1656
+ console.log(` + ${f.severity} ${f.vuln} (${f.file}:${f.line})`);
1657
+ }
1658
+ }
1659
+ if (r.removed.length) {
1660
+ console.log(`Removed by this counterfactual:`);
1661
+ for (const f of r.removed.slice(0, 20)) {
1662
+ console.log(` - ${f.severity} ${f.vuln} (${f.file}:${f.line})`);
1663
+ }
1664
+ }
1665
+ }
1666
+ process.exit(0);
1667
+ }
1668
+ case 'version': console.log('agentic-security 0.74.0 · created by ClearCapabilities.Com'); process.exit(0);
1669
+ case 'banner': { printBanner(args); process.exit(0); }
1670
+ case 'harness': process.exit(await cmdHarness(args));
1671
+ case 'scan-baseline': process.exit(await cmdScanBaseline(args));
1672
+ case 'help': case '--help': case '-h': case undefined:
1673
+ console.log(USAGE); process.exit(cmd ? 0 : 1);
1674
+ default:
1675
+ console.error(`Unknown command: ${cmd}\n\n${USAGE}`); process.exit(4);
1676
+ }
1677
+ } catch (e) {
1678
+ console.error('agentic-security: error:', e?.stack || e?.message || e);
1679
+ process.exit(4);
1680
+ }
1681
+ }
1682
+
1683
+ main();