@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,40 @@
1
+ [
2
+ {
3
+ "timestamp": "2026-05-15T12:30:28.201Z",
4
+ "label": "scan",
5
+ "total": 7,
6
+ "critical": 0,
7
+ "high": 0,
8
+ "medium": 7,
9
+ "low": 0,
10
+ "kev": 0,
11
+ "ids": [
12
+ "struct:index.js:23:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
13
+ "struct:index.js:24:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
14
+ "struct:tickets.js:146:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
15
+ "struct:tickets.js:147:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
16
+ "struct:tickets.js:26:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
17
+ "struct:tickets.js:27:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
18
+ "struct:tickets.js:31:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)"
19
+ ]
20
+ },
21
+ {
22
+ "timestamp": "2026-05-18T17:50:17.394Z",
23
+ "label": "scan",
24
+ "total": 7,
25
+ "critical": 0,
26
+ "high": 0,
27
+ "medium": 7,
28
+ "low": 0,
29
+ "kev": 0,
30
+ "ids": [
31
+ "struct:index.js:23:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
32
+ "struct:index.js:24:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
33
+ "struct:tickets.js:146:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
34
+ "struct:tickets.js:147:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
35
+ "struct:tickets.js:26:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
36
+ "struct:tickets.js:27:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)",
37
+ "struct:tickets.js:31:Synchronous_Blocking_I/O_(DoS_Risk_in_Server_Context)"
38
+ ]
39
+ }
40
+ ]
@@ -0,0 +1,21 @@
1
+ {
2
+ "firstScanDate": "2026-05-15T12:30:28.207Z",
3
+ "lastScanDate": "2026-05-18T17:50:17.413Z",
4
+ "totalScans": 2,
5
+ "daysCleanCritical": 1,
6
+ "lastCleanDate": "2026-05-18",
7
+ "lastCriticalDate": null,
8
+ "hasEverHadCritical": false,
9
+ "bestDaysCleanCritical": 1,
10
+ "totalFindingsAtFirstScan": 14,
11
+ "totalFindingsAtLastScan": 17,
12
+ "totalFixesInferred": 0,
13
+ "lastGrade": "A",
14
+ "bestGrade": "A",
15
+ "launchCheckPassedAt": null,
16
+ "achievements": [
17
+ "first-scan",
18
+ "grade-a"
19
+ ],
20
+ "previousGrade": "A"
21
+ }
@@ -0,0 +1,321 @@
1
+ // Integration adapters (R7). Persona-split:
2
+ // - vibecoder: Slack/Discord daily digest, PR comment renderer
3
+ // - pro: Jira sync, ServiceNow incident, GH Security tab (SARIF), SIEM
4
+ //
5
+ // Each adapter takes (findings, config) and returns either:
6
+ // - a payload (for webhook-based integrations)
7
+ // - a side-effect summary (for sync-based integrations like Jira)
8
+ //
9
+ // Adapters are lazy-loaded — vibecoders don't pay for Jira when they never
10
+ // configure it. The config file lives at .agentic-security/integrations.yml
11
+ // and is gitignored by default (it carries webhook URLs and API tokens).
12
+
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import * as yaml from 'js-yaml';
16
+
17
+ function _configPath(scanRoot) {
18
+ return path.join(scanRoot || process.cwd(), '.agentic-security', 'integrations.yml');
19
+ }
20
+
21
+ export function loadIntegrationConfig(scanRoot) {
22
+ const fp = _configPath(scanRoot);
23
+ if (!fs.existsSync(fp)) return {};
24
+ try { return yaml.load(fs.readFileSync(fp, 'utf8')) || {}; }
25
+ catch (_) { return {}; }
26
+ }
27
+
28
+ export function configHas(scanRoot, key) {
29
+ const c = loadIntegrationConfig(scanRoot);
30
+ return !!c[key];
31
+ }
32
+
33
+ // ─── Slack webhook (vibecoder) ───────────────────────────────────────────────
34
+ export function buildSlackDigest(findings, summary, options = {}) {
35
+ const project = options.project || 'project';
36
+ const status = (summary.critical || 0) === 0 && (summary.high || 0) === 0
37
+ ? '✅ safe to deploy' : '❌ not safe to deploy';
38
+ const top = (findings || []).slice(0, 3);
39
+
40
+ const lines = [
41
+ `*🛡 agentic-security daily — ${new Date().toISOString().slice(0,10)}*`,
42
+ `Project: \`${project}\``,
43
+ `Status: ${status}`,
44
+ '',
45
+ `*Findings:* ${summary.critical || 0} critical · ${summary.high || 0} high · ${summary.medium || 0} medium`,
46
+ ];
47
+ if (top.length) {
48
+ lines.push('');
49
+ lines.push('*Top findings:*');
50
+ for (const f of top) {
51
+ lines.push(`• \`${f.file}:${f.line}\` — ${f.vuln}`);
52
+ }
53
+ }
54
+ if (summary.streak) lines.push(`\n_Streak: ${summary.streak} days clean 🔥_`);
55
+ lines.push(`\n_Powered by agentic-security · ClearCapabilities.Com_`);
56
+ return { text: lines.join('\n') };
57
+ }
58
+
59
+ // ─── Discord webhook (vibecoder) ─────────────────────────────────────────────
60
+ export function buildDiscordDigest(findings, summary, options = {}) {
61
+ const project = options.project || 'project';
62
+ const safe = (summary.critical || 0) === 0 && (summary.high || 0) === 0;
63
+ const top = (findings || []).slice(0, 3);
64
+ return {
65
+ embeds: [{
66
+ title: `🛡 agentic-security — ${project}`,
67
+ color: safe ? 0x2ecc71 : 0xe74c3c,
68
+ description: safe ? '✅ safe to deploy' : '❌ not safe to deploy',
69
+ fields: [
70
+ { name: 'Critical', value: String(summary.critical || 0), inline: true },
71
+ { name: 'High', value: String(summary.high || 0), inline: true },
72
+ { name: 'Medium', value: String(summary.medium || 0), inline: true },
73
+ ...top.map(f => ({ name: f.file.split('/').pop() + ':' + f.line, value: f.vuln })),
74
+ ],
75
+ footer: { text: 'Powered by ClearCapabilities.Com' },
76
+ }],
77
+ };
78
+ }
79
+
80
+ export async function postWebhook(url, payload) {
81
+ if (!url || process.env.AGENTIC_SECURITY_OFFLINE === '1') return { ok: false, reason: 'offline-or-no-url' };
82
+ try {
83
+ const res = await fetch(url, {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify(payload),
87
+ });
88
+ return { ok: res.ok, status: res.status };
89
+ } catch (e) {
90
+ return { ok: false, reason: e.message };
91
+ }
92
+ }
93
+
94
+ // ─── Jira sync (pro) ─────────────────────────────────────────────────────────
95
+ // Build the Jira issue body for one finding. Caller is responsible for actual
96
+ // API calls — we keep network code in one place (postWebhook) and let callers
97
+ // pipe the body through their own Jira client.
98
+ export function buildJiraIssue(finding, project) {
99
+ const summary = `[${(finding.severity || 'medium').toUpperCase()}] ${finding.vuln} at ${finding.file}:${finding.line}`;
100
+ const description = [
101
+ `**Vulnerability:** ${finding.vuln}`,
102
+ `**File:** \`${finding.file}\``,
103
+ `**Line:** ${finding.line}`,
104
+ `**Severity:** ${finding.severity}`,
105
+ finding.cwe ? `**CWE:** ${finding.cwe}` : null,
106
+ finding.cvss ? `**CVSS:** ${finding.cvss}` : null,
107
+ '',
108
+ '**Code:**',
109
+ '```',
110
+ finding.snippet || '',
111
+ '```',
112
+ '',
113
+ finding.fix ? `**Recommended fix:**\n${finding.fix}` : null,
114
+ '',
115
+ '---',
116
+ `_Surfaced by agentic-security · ClearCapabilities.Com · Finding ID: ${finding.id}_`,
117
+ ].filter(Boolean).join('\n');
118
+ return {
119
+ fields: {
120
+ project: { key: project || 'SEC' },
121
+ summary,
122
+ description,
123
+ issuetype: { name: 'Bug' },
124
+ priority: { name: _sevToJiraPriority(finding.severity) },
125
+ labels: [
126
+ 'agentic-security',
127
+ finding.cwe ? finding.cwe.toLowerCase().replace(/[^a-z0-9-]/g, '-') : null,
128
+ ].filter(Boolean),
129
+ },
130
+ };
131
+ }
132
+
133
+ function _sevToJiraPriority(sev) {
134
+ return { critical: 'Highest', high: 'High', medium: 'Medium', low: 'Low', info: 'Lowest' }[sev] || 'Medium';
135
+ }
136
+
137
+ // ─── ServiceNow incident (pro) ───────────────────────────────────────────────
138
+ export function buildServiceNowIncident(finding) {
139
+ return {
140
+ short_description: `${finding.vuln} at ${finding.file}:${finding.line}`,
141
+ description: finding.fix
142
+ ? `${finding.vuln}\n\n${finding.snippet || ''}\n\nFix:\n${finding.fix}`
143
+ : `${finding.vuln}\n\n${finding.snippet || ''}`,
144
+ urgency: _sevToServiceNowUrgency(finding.severity),
145
+ impact: _sevToServiceNowImpact(finding.severity),
146
+ caller_id: 'agentic-security',
147
+ work_notes: 'Created by agentic-security · ClearCapabilities.Com',
148
+ };
149
+ }
150
+
151
+ function _sevToServiceNowUrgency(sev) {
152
+ return { critical: '1', high: '2', medium: '3', low: '3', info: '4' }[sev] || '3';
153
+ }
154
+ function _sevToServiceNowImpact(sev) {
155
+ return { critical: '1', high: '2', medium: '3', low: '3', info: '3' }[sev] || '3';
156
+ }
157
+
158
+ // ─── SIEM log line (pro) ─────────────────────────────────────────────────────
159
+ // Structured event suitable for piping into Splunk/Datadog/Elastic.
160
+ export function buildSiemEvent(finding, options = {}) {
161
+ return {
162
+ timestamp: new Date().toISOString(),
163
+ event: 'security.finding',
164
+ source: 'agentic-security',
165
+ source_attribution: 'ClearCapabilities.Com',
166
+ severity: finding.severity,
167
+ vuln: finding.vuln,
168
+ file: finding.file,
169
+ line: finding.line,
170
+ cwe: finding.cwe || null,
171
+ cvss: finding.cvss || null,
172
+ confidence: finding.confidence ?? null,
173
+ rule_version: options.ruleVersion || null,
174
+ project: options.project || null,
175
+ };
176
+ }
177
+
178
+ // ─── ServiceNow REST API poster ──────────────────────────────────────────────
179
+ // Activated by AGENTIC_SECURITY_SERVICENOW_URL +
180
+ // AGENTIC_SECURITY_SERVICENOW_USER + AGENTIC_SECURITY_SERVICENOW_PASS.
181
+ // URL example: https://example.service-now.com/api/now/table/incident
182
+ export async function postServiceNowIncident(finding) {
183
+ const url = process.env.AGENTIC_SECURITY_SERVICENOW_URL;
184
+ const user = process.env.AGENTIC_SECURITY_SERVICENOW_USER;
185
+ const pass = process.env.AGENTIC_SECURITY_SERVICENOW_PASS;
186
+ if (!url || !user || !pass) {
187
+ return { ok: false, reason: 'servicenow-not-configured' };
188
+ }
189
+ const body = buildServiceNowIncident(finding);
190
+ const auth = Buffer.from(`${user}:${pass}`).toString('base64');
191
+ try {
192
+ const r = await fetch(url, {
193
+ method: 'POST',
194
+ headers: {
195
+ 'Authorization': `Basic ${auth}`,
196
+ 'Content-Type': 'application/json',
197
+ 'Accept': 'application/json',
198
+ },
199
+ body: JSON.stringify(body),
200
+ });
201
+ return { ok: r.ok, status: r.status, body: await r.text().catch(() => '') };
202
+ } catch (e) {
203
+ return { ok: false, error: e.message };
204
+ }
205
+ }
206
+
207
+ // ─── PagerDuty Events API v2 ────────────────────────────────────────────────
208
+ // Activated by AGENTIC_SECURITY_PAGERDUTY_KEY (a routing key, NOT a user API
209
+ // token). One event per critical finding; PagerDuty handles de-dup by
210
+ // dedup_key so the same finding doesn't spam.
211
+ export async function postPagerDutyEvent(finding) {
212
+ const routingKey = process.env.AGENTIC_SECURITY_PAGERDUTY_KEY;
213
+ if (!routingKey) return { ok: false, reason: 'pagerduty-not-configured' };
214
+ const dedupKey = finding.stableId || finding.id || `${finding.file}:${finding.line}:${finding.vuln}`;
215
+ const sevMap = { critical: 'critical', high: 'error', medium: 'warning', low: 'info', info: 'info' };
216
+ const payload = {
217
+ routing_key: routingKey,
218
+ event_action: 'trigger',
219
+ dedup_key: dedupKey,
220
+ payload: {
221
+ summary: `[${(finding.severity || '').toUpperCase()}] ${finding.vuln} — ${finding.file}:${finding.line}`,
222
+ source: 'agentic-security',
223
+ severity: sevMap[finding.severity] || 'warning',
224
+ class: finding.cwe || finding.family || 'security',
225
+ custom_details: {
226
+ file: finding.file,
227
+ line: finding.line,
228
+ cwe: finding.cwe || null,
229
+ confidence: finding.confidence ?? null,
230
+ exploitability: finding.exploitability ?? null,
231
+ snippet: finding.snippet,
232
+ remediation: typeof finding.remediation === 'string' ? finding.remediation.slice(0, 500) : null,
233
+ },
234
+ },
235
+ };
236
+ try {
237
+ const r = await fetch('https://events.pagerduty.com/v2/enqueue', {
238
+ method: 'POST',
239
+ headers: { 'Content-Type': 'application/json' },
240
+ body: JSON.stringify(payload),
241
+ });
242
+ return { ok: r.ok, status: r.status, body: await r.text().catch(() => '') };
243
+ } catch (e) {
244
+ return { ok: false, error: e.message };
245
+ }
246
+ }
247
+
248
+ // ─── Microsoft Teams (Adaptive Card via Incoming Webhook) ───────────────────
249
+ // Activated by AGENTIC_SECURITY_TEAMS_WEBHOOK (a webhook URL from a Teams
250
+ // channel's "Incoming Webhook" connector).
251
+ export function buildTeamsCard(findings, summary) {
252
+ const sev = summary || {};
253
+ const total = (sev.critical || 0) + (sev.high || 0) + (sev.medium || 0) + (sev.low || 0);
254
+ const top = (findings || []).filter(f => /critical|high/.test(f.severity || '')).slice(0, 5);
255
+ const card = {
256
+ type: 'message',
257
+ attachments: [{
258
+ contentType: 'application/vnd.microsoft.card.adaptive',
259
+ content: {
260
+ $schema: 'http://adaptivecards.io/schemas/adaptive-card.json',
261
+ type: 'AdaptiveCard',
262
+ version: '1.4',
263
+ body: [
264
+ { type: 'TextBlock', size: 'Medium', weight: 'Bolder', text: '🛡 agentic-security scan' },
265
+ { type: 'FactSet', facts: [
266
+ { title: 'Critical', value: String(sev.critical || 0) },
267
+ { title: 'High', value: String(sev.high || 0) },
268
+ { title: 'Medium', value: String(sev.medium || 0) },
269
+ { title: 'Low', value: String(sev.low || 0) },
270
+ { title: 'Total', value: String(total) },
271
+ ]},
272
+ ...(top.length ? [
273
+ { type: 'TextBlock', wrap: true, weight: 'Bolder', text: 'Top critical/high' },
274
+ ...top.map(f => ({
275
+ type: 'TextBlock', wrap: true, isSubtle: false,
276
+ text: `**[${(f.severity || '').toUpperCase()}]** ${f.vuln} — \`${f.file}:${f.line}\``,
277
+ })),
278
+ ] : []),
279
+ ],
280
+ },
281
+ }],
282
+ };
283
+ return card;
284
+ }
285
+
286
+ export async function postTeamsCard(findings, summary) {
287
+ const url = process.env.AGENTIC_SECURITY_TEAMS_WEBHOOK;
288
+ if (!url) return { ok: false, reason: 'teams-not-configured' };
289
+ const card = buildTeamsCard(findings, summary);
290
+ try {
291
+ const r = await fetch(url, {
292
+ method: 'POST',
293
+ headers: { 'Content-Type': 'application/json' },
294
+ body: JSON.stringify(card),
295
+ });
296
+ return { ok: r.ok, status: r.status, body: await r.text().catch(() => '') };
297
+ } catch (e) {
298
+ return { ok: false, error: e.message };
299
+ }
300
+ }
301
+
302
+ // ─── GH PR comment (vibecoder) ───────────────────────────────────────────────
303
+ export function buildPrComment(findings, summary, options = {}) {
304
+ const sev = summary;
305
+ const top = (findings || []).filter(f => /critical|high/.test(f.severity)).slice(0, 10);
306
+ const body = [
307
+ '## 🛡 agentic-security',
308
+ '',
309
+ '| Critical | High | Medium | Low | Info |',
310
+ '|---:|---:|---:|---:|---:|',
311
+ `| ${sev.critical||0} | ${sev.high||0} | ${sev.medium||0} | ${sev.low||0} | ${sev.info||0} |`,
312
+ '',
313
+ top.length ? '### Top critical/high findings' : '_No critical or high findings._',
314
+ '',
315
+ ...top.map(f => `- **[${(f.severity||'').toUpperCase()}]** \`${f.file}:${f.line}\` — ${f.vuln}${f.cwe ? ` (${f.cwe})` : ''}`),
316
+ '',
317
+ '---',
318
+ '_Powered by [agentic-security](https://clearcapabilities.com) · created by ClearCapabilities.Com_',
319
+ ].join('\n');
320
+ return { body };
321
+ }
@@ -0,0 +1,200 @@
1
+ // Two-way ticket sync — GitHub Issues / Linear / Jira.
2
+ //
3
+ // State file: .agentic-security/tickets.json
4
+ // { findingId → { provider, externalId, externalUrl, state, syncedAt } }
5
+ //
6
+ // Sync algorithm:
7
+ // - For every open critical/high finding without a ticket → create one.
8
+ // - For every existing ticket whose finding is no longer in last-scan → close it.
9
+ // - The state file is idempotent: re-running sync is a no-op once everything matches.
10
+ //
11
+ // Auth via environment variables:
12
+ // GitHub — `gh` CLI (uses existing auth)
13
+ // Linear — LINEAR_API_KEY
14
+ // Jira — JIRA_BASE_URL, JIRA_EMAIL, JIRA_TOKEN, JIRA_PROJECT_KEY
15
+
16
+ import * as fs from 'node:fs';
17
+ import * as path from 'node:path';
18
+ import * as cp from 'node:child_process';
19
+ import { buildJiraIssue } from './index.js';
20
+
21
+ function statePath(scanRoot) {
22
+ return path.join(scanRoot, '.agentic-security', 'tickets.json');
23
+ }
24
+ export function readState(scanRoot) {
25
+ const fp = statePath(scanRoot);
26
+ if (!fs.existsSync(fp)) return {};
27
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return {}; }
28
+ }
29
+ function writeState(scanRoot, state) {
30
+ fs.mkdirSync(path.dirname(statePath(scanRoot)), { recursive: true });
31
+ fs.writeFileSync(statePath(scanRoot), JSON.stringify(state, null, 2));
32
+ }
33
+
34
+ function findingTitle(f) {
35
+ return `[${(f.severity || 'medium').toUpperCase()}] ${f.vuln || f.title || 'security finding'} at ${f.file}:${f.line}`;
36
+ }
37
+ function findingBody(f) {
38
+ const br = f.blastRadius;
39
+ const exploited = f.exploitedNow ? `\n> ⚠️ **EPSS percentile ${(f.epssPercentile * 100).toFixed(1)}%** — actively exploited.\n` : '';
40
+ return [
41
+ `**File:** \`${f.file}:${f.line}\``,
42
+ `**Severity:** ${f.severity}`,
43
+ f.cwe ? `**CWE:** ${f.cwe}` : null,
44
+ f.epss != null ? `**EPSS:** ${f.epss.toFixed(4)} (percentile ${(f.epssPercentile * 100).toFixed(1)}%)` : null,
45
+ exploited,
46
+ f.description ? `\n${f.description}` : null,
47
+ br?.narrative ? `\n**Blast radius:** ${br.narrative}` : null,
48
+ f.snippet ? `\n\`\`\`\n${f.snippet}\n\`\`\`` : null,
49
+ f.remediation ? `\n**Remediation:** ${f.remediation}` : null,
50
+ `\n---\n_Surfaced by agentic-security · finding id: ${f.id}_`,
51
+ ].filter(Boolean).join('\n');
52
+ }
53
+
54
+ // ─── GitHub Issues (via gh CLI) ──────────────────────────────────────────────
55
+ function ghCreate(repo, title, body, labels) {
56
+ const args = ['issue', 'create', '--title', title, '--body', body];
57
+ if (repo) args.unshift('--repo', repo);
58
+ for (const l of labels) args.push('--label', l);
59
+ try {
60
+ const out = cp.execFileSync('gh', args, { encoding: 'utf8' });
61
+ return { ok: true, url: out.trim() };
62
+ } catch (e) { return { ok: false, error: e.message }; }
63
+ }
64
+ function ghClose(repo, url, comment) {
65
+ const args = ['issue', 'close', url, '--comment', comment];
66
+ if (repo) args.unshift('--repo', repo);
67
+ try { cp.execFileSync('gh', args, { encoding: 'utf8' }); return { ok: true }; }
68
+ catch (e) { return { ok: false, error: e.message }; }
69
+ }
70
+
71
+ // ─── Linear (REST GraphQL) ───────────────────────────────────────────────────
72
+ async function linearGraphQL(query, variables) {
73
+ const key = process.env.LINEAR_API_KEY;
74
+ if (!key) return { ok: false, error: 'LINEAR_API_KEY not set' };
75
+ try {
76
+ const res = await fetch('https://api.linear.app/graphql', {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json', 'Authorization': key },
79
+ body: JSON.stringify({ query, variables }),
80
+ });
81
+ const body = await res.json();
82
+ if (body.errors) return { ok: false, error: JSON.stringify(body.errors) };
83
+ return { ok: true, data: body.data };
84
+ } catch (e) { return { ok: false, error: e.message }; }
85
+ }
86
+ async function linearCreate(teamId, title, description) {
87
+ const m = `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { issue { id url } } }`;
88
+ const r = await linearGraphQL(m, { input: { teamId, title, description } });
89
+ if (!r.ok) return r;
90
+ const issue = r.data?.issueCreate?.issue;
91
+ return issue ? { ok: true, externalId: issue.id, url: issue.url } : { ok: false, error: 'no issue returned' };
92
+ }
93
+ async function linearClose(issueId, stateName) {
94
+ const sm = `query($id: String!) { issue(id: $id) { team { states { nodes { id name } } } } }`;
95
+ const sr = await linearGraphQL(sm, { id: issueId });
96
+ if (!sr.ok) return sr;
97
+ const states = sr.data?.issue?.team?.states?.nodes || [];
98
+ const target = states.find(s => s.name.toLowerCase() === (stateName || 'done').toLowerCase()) || states[states.length - 1];
99
+ if (!target) return { ok: false, error: 'no target state' };
100
+ const m = `mutation($id: String!, $stateId: String!) { issueUpdate(id: $id, input: {stateId: $stateId}) { success } }`;
101
+ return linearGraphQL(m, { id: issueId, stateId: target.id });
102
+ }
103
+
104
+ // ─── Jira (REST) ─────────────────────────────────────────────────────────────
105
+ async function jiraRequest(method, urlPath, body) {
106
+ const base = process.env.JIRA_BASE_URL;
107
+ const email = process.env.JIRA_EMAIL;
108
+ const token = process.env.JIRA_TOKEN;
109
+ if (!base || !email || !token) return { ok: false, error: 'JIRA_BASE_URL, JIRA_EMAIL, JIRA_TOKEN required' };
110
+ const auth = Buffer.from(`${email}:${token}`).toString('base64');
111
+ try {
112
+ const res = await fetch(`${base.replace(/\/$/, '')}${urlPath}`, {
113
+ method,
114
+ headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/json', 'Accept': 'application/json' },
115
+ body: body ? JSON.stringify(body) : undefined,
116
+ });
117
+ const text = await res.text();
118
+ let data; try { data = JSON.parse(text); } catch { data = text; }
119
+ return res.ok ? { ok: true, data } : { ok: false, error: `HTTP ${res.status}: ${text.slice(0, 200)}` };
120
+ } catch (e) { return { ok: false, error: e.message }; }
121
+ }
122
+ async function jiraCreate(finding) {
123
+ const project = process.env.JIRA_PROJECT_KEY || 'SEC';
124
+ const issue = buildJiraIssue(finding, project);
125
+ const r = await jiraRequest('POST', '/rest/api/2/issue', issue);
126
+ if (!r.ok) return r;
127
+ const key = r.data?.key;
128
+ const base = process.env.JIRA_BASE_URL.replace(/\/$/, '');
129
+ return { ok: true, externalId: key, url: `${base}/browse/${key}` };
130
+ }
131
+ async function jiraClose(externalId) {
132
+ const t = await jiraRequest('GET', `/rest/api/2/issue/${externalId}/transitions`);
133
+ if (!t.ok) return t;
134
+ const transitions = t.data?.transitions || [];
135
+ const done = transitions.find(x => /done|closed|resolved/i.test(x.name)) || transitions[transitions.length - 1];
136
+ if (!done) return { ok: false, error: 'no transition available' };
137
+ return jiraRequest('POST', `/rest/api/2/issue/${externalId}/transitions`, { transition: { id: done.id } });
138
+ }
139
+
140
+ // ─── orchestrator ────────────────────────────────────────────────────────────
141
+ const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
142
+
143
+ export async function syncTickets({ scanRoot, provider, severity = 'high', repo, teamId, dryRun = false }) {
144
+ const minRank = SEV_RANK[severity] ?? 3;
145
+ const lastScanPath = path.join(scanRoot, '.agentic-security', 'last-scan.json');
146
+ if (!fs.existsSync(lastScanPath)) return { ok: false, error: 'no last-scan.json — run a scan first' };
147
+ const last = JSON.parse(fs.readFileSync(lastScanPath, 'utf8'));
148
+ const allFindings = [...(last.findings || []), ...(last.secrets || []), ...(last.supplyChain || [])];
149
+ const eligible = allFindings.filter(f => (SEV_RANK[f.severity] ?? 0) >= minRank);
150
+ const stillOpen = new Set(eligible.map(f => f.id));
151
+ const state = readState(scanRoot);
152
+ const created = [], closed = [], failed = [];
153
+
154
+ // Create tickets for new findings.
155
+ for (const f of eligible) {
156
+ if (state[f.id] && !state[f.id].closedAt) continue; // already tracked + open
157
+ const title = findingTitle(f);
158
+ const body = findingBody(f);
159
+ if (dryRun) { created.push({ id: f.id, title, dryRun: true }); continue; }
160
+ let r;
161
+ if (provider === 'github') {
162
+ const labels = ['security', `severity:${f.severity}`];
163
+ if (f.cwe) labels.push(f.cwe.toLowerCase());
164
+ r = ghCreate(repo, title, body, labels);
165
+ if (r.ok) state[f.id] = { provider, externalUrl: r.url, externalId: r.url, state: 'open', syncedAt: new Date().toISOString() };
166
+ } else if (provider === 'linear') {
167
+ if (!teamId) { failed.push({ id: f.id, error: 'linear: --team-id required' }); continue; }
168
+ r = await linearCreate(teamId, title, body);
169
+ if (r.ok) state[f.id] = { provider, externalId: r.externalId, externalUrl: r.url, state: 'open', syncedAt: new Date().toISOString() };
170
+ } else if (provider === 'jira') {
171
+ r = await jiraCreate(f);
172
+ if (r.ok) state[f.id] = { provider, externalId: r.externalId, externalUrl: r.url, state: 'open', syncedAt: new Date().toISOString() };
173
+ } else {
174
+ return { ok: false, error: `unknown provider: ${provider}` };
175
+ }
176
+ if (r.ok) created.push({ id: f.id, externalId: r.externalId || r.url });
177
+ else failed.push({ id: f.id, error: r.error });
178
+ }
179
+
180
+ // Close tickets for findings no longer present.
181
+ for (const [findingId, entry] of Object.entries(state)) {
182
+ if (entry.closedAt || stillOpen.has(findingId)) continue;
183
+ if (entry.provider !== provider) continue;
184
+ if (dryRun) { closed.push({ id: findingId, dryRun: true }); continue; }
185
+ let r;
186
+ if (provider === 'github') r = ghClose(repo, entry.externalUrl, 'Auto-closed by agentic-security: finding no longer present.');
187
+ else if (provider === 'linear') r = await linearClose(entry.externalId);
188
+ else if (provider === 'jira') r = await jiraClose(entry.externalId);
189
+ if (r?.ok) {
190
+ entry.closedAt = new Date().toISOString();
191
+ entry.state = 'closed';
192
+ closed.push({ id: findingId, externalId: entry.externalId });
193
+ } else {
194
+ failed.push({ id: findingId, error: r?.error || 'close failed' });
195
+ }
196
+ }
197
+
198
+ if (!dryRun) writeState(scanRoot, state);
199
+ return { ok: true, created, closed, failed, totalTracked: Object.keys(state).length };
200
+ }