@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,26 @@
1
+ {
2
+ "firstScanDate": "2026-05-13T12:55:24.025Z",
3
+ "lastScanDate": "2026-05-20T21:26:48.636Z",
4
+ "totalScans": 281,
5
+ "daysCleanCritical": 0,
6
+ "lastCleanDate": "2026-05-19",
7
+ "lastCriticalDate": "2026-05-20",
8
+ "hasEverHadCritical": true,
9
+ "bestDaysCleanCritical": 2,
10
+ "totalFindingsAtFirstScan": 135,
11
+ "totalFindingsAtLastScan": 408,
12
+ "totalFixesInferred": 114,
13
+ "lastGrade": "C",
14
+ "bestGrade": "B-",
15
+ "launchCheckPassedAt": null,
16
+ "achievements": [
17
+ "clean-sweep",
18
+ "first-fix",
19
+ "first-scan",
20
+ "scan-veteran-100",
21
+ "scan-veteran-25",
22
+ "triage-master",
23
+ "triage-silver"
24
+ ],
25
+ "previousGrade": "C"
26
+ }
package/src/badge.js ADDED
@@ -0,0 +1,188 @@
1
+ // Live SVG badge generator (v0.72).
2
+ //
3
+ // Every repo can drop a badge in its README pulling from the latest scan:
4
+ //
5
+ // ![agentic-security](https://agentic-security.dev/badge?repo=<slug>)
6
+ //
7
+ // or self-hosted via the CLI subcommand emitting an inline <img> URL or
8
+ // a static SVG. The badge format borrows from shields.io for visual
9
+ // consistency. Reads from .agentic-security/last-scan.json or accepts a
10
+ // scan object directly.
11
+ //
12
+ // Output formats:
13
+ // - 'svg' — inline SVG string (default; the bytes you'd serve)
14
+ // - 'json' — { label, count, color, severity } for a frontend renderer
15
+ //
16
+ // Style variants:
17
+ // - 'flat' — shields.io flat
18
+ // - 'for-the-badge' — caps + thicker
19
+ //
20
+ // Color is driven by the highest non-zero severity:
21
+ // critical → red
22
+ // high → orange
23
+ // medium → yellow
24
+ // low → blue
25
+ // info → lightgrey
26
+ // none → brightgreen
27
+
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+
31
+ const COLORS = {
32
+ critical: '#e05d44', // red
33
+ high: '#fe7d37', // orange
34
+ medium: '#dfb317', // yellow
35
+ low: '#007ec6', // blue
36
+ info: '#9f9f9f', // grey
37
+ none: '#4c1', // brightgreen
38
+ label: '#555',
39
+ };
40
+
41
+ const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
42
+
43
+ function _readLastScan(scanRoot) {
44
+ if (!scanRoot) return null;
45
+ const fp = path.join(scanRoot, '.agentic-security', 'last-scan.json');
46
+ if (!fs.existsSync(fp)) return null;
47
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); }
48
+ catch { return null; }
49
+ }
50
+
51
+ function _ageString(ts) {
52
+ if (!ts) return null;
53
+ const ageMs = Date.now() - new Date(ts).getTime();
54
+ if (isNaN(ageMs) || ageMs < 0) return null;
55
+ const min = Math.floor(ageMs / 60_000);
56
+ if (min < 60) return `${min}m ago`;
57
+ const hr = Math.floor(min / 60);
58
+ if (hr < 24) return `${hr}h ago`;
59
+ const day = Math.floor(hr / 24);
60
+ return `${day}d ago`;
61
+ }
62
+
63
+ /**
64
+ * Compute the badge value from a scan object.
65
+ *
66
+ * Returns:
67
+ * {
68
+ * label: 'agentic-security',
69
+ * summary: 'critical 0 · high 2 · medium 5' | 'passing' | 'no scan',
70
+ * color: '#fe7d37',
71
+ * highest: 'high' | 'none' | 'unknown',
72
+ * ageStr: '4h ago' | null,
73
+ * counts: { critical, high, medium, low, info },
74
+ * total: 7,
75
+ * }
76
+ */
77
+ export function summarizeForBadge(scan) {
78
+ if (!scan || !Array.isArray(scan.findings)) {
79
+ return {
80
+ label: 'agentic-security',
81
+ summary: 'no scan',
82
+ color: COLORS.info,
83
+ highest: 'unknown',
84
+ ageStr: null,
85
+ counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
86
+ total: 0,
87
+ };
88
+ }
89
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
90
+ for (const f of scan.findings) {
91
+ const s = f.severity || 'info';
92
+ if (counts[s] !== undefined) counts[s]++;
93
+ }
94
+ let highest = 'none';
95
+ for (const s of SEVERITIES) { if (counts[s] > 0) { highest = s; break; } }
96
+ const color = COLORS[highest] || COLORS.info;
97
+ const summary = highest === 'none'
98
+ ? 'passing'
99
+ : `crit ${counts.critical} · high ${counts.high} · med ${counts.medium}`;
100
+ const total = SEVERITIES.reduce((a, s) => a + counts[s], 0);
101
+ return {
102
+ label: 'agentic-security',
103
+ summary,
104
+ color,
105
+ highest,
106
+ ageStr: _ageString(scan.timestamp || scan.when || scan.lastScan),
107
+ counts,
108
+ total,
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Compute the badge from .agentic-security/last-scan.json under `scanRoot`.
114
+ */
115
+ export function badgeFromScanRoot(scanRoot) {
116
+ return summarizeForBadge(_readLastScan(scanRoot));
117
+ }
118
+
119
+ function _xmlEscape(s) {
120
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
121
+ }
122
+
123
+ function _textWidth(s) {
124
+ // Rough character width — works fine for the small badge label range.
125
+ return s.length * 7 + 10;
126
+ }
127
+
128
+ /**
129
+ * Render an inline SVG matching shields.io's flat style. Self-contained
130
+ * (no external font references) so the badge works in any README.
131
+ */
132
+ export function renderSvg(b, opts = {}) {
133
+ if (!b) b = summarizeForBadge(null);
134
+ const style = opts.style || 'flat';
135
+ const labelText = b.label;
136
+ const valueText = b.ageStr ? `${b.summary} · ${b.ageStr}` : b.summary;
137
+ const lblW = _textWidth(labelText);
138
+ const valW = _textWidth(valueText);
139
+ const totalW = lblW + valW;
140
+ const h = style === 'for-the-badge' ? 28 : 20;
141
+ const fontSize = style === 'for-the-badge' ? 12 : 11;
142
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${h}" role="img" aria-label="${_xmlEscape(labelText)}: ${_xmlEscape(valueText)}">
143
+ <title>${_xmlEscape(labelText)}: ${_xmlEscape(valueText)}</title>
144
+ <linearGradient id="s" x2="0" y2="100%">
145
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
146
+ <stop offset="1" stop-opacity=".1"/>
147
+ </linearGradient>
148
+ <clipPath id="r"><rect width="${totalW}" height="${h}" rx="3" fill="#fff"/></clipPath>
149
+ <g clip-path="url(#r)">
150
+ <rect width="${lblW}" height="${h}" fill="${COLORS.label}"/>
151
+ <rect x="${lblW}" width="${valW}" height="${h}" fill="${b.color}"/>
152
+ <rect width="${totalW}" height="${h}" fill="url(#s)"/>
153
+ </g>
154
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="${fontSize}">
155
+ <text aria-hidden="true" x="${lblW / 2}" y="${h - 6}" fill="#010101" fill-opacity=".3">${_xmlEscape(labelText)}</text>
156
+ <text x="${lblW / 2}" y="${h - 7}">${_xmlEscape(labelText)}</text>
157
+ <text aria-hidden="true" x="${lblW + valW / 2}" y="${h - 6}" fill="#010101" fill-opacity=".3">${_xmlEscape(valueText)}</text>
158
+ <text x="${lblW + valW / 2}" y="${h - 7}">${_xmlEscape(valueText)}</text>
159
+ </g>
160
+ </svg>`;
161
+ }
162
+
163
+ /**
164
+ * Public entry: produce the badge in the requested format.
165
+ *
166
+ * format: 'svg' (default) | 'json'
167
+ * style: 'flat' (default) | 'for-the-badge'
168
+ * scanRoot: directory containing .agentic-security/last-scan.json
169
+ * scan: pre-loaded scan object (skips disk read)
170
+ */
171
+ export function renderBadge({ format = 'svg', style = 'flat', scanRoot, scan } = {}) {
172
+ const summary = summarizeForBadge(scan || _readLastScan(scanRoot));
173
+ if (format === 'json') {
174
+ return JSON.stringify({
175
+ schemaVersion: 1,
176
+ label: summary.label,
177
+ message: summary.summary,
178
+ color: summary.color,
179
+ highest: summary.highest,
180
+ ageStr: summary.ageStr,
181
+ counts: summary.counts,
182
+ total: summary.total,
183
+ });
184
+ }
185
+ return renderSvg(summary, { style });
186
+ }
187
+
188
+ export const _internal = { COLORS, _ageString, _readLastScan };
package/src/compare.js ADDED
@@ -0,0 +1,203 @@
1
+ // Side-by-side compare runner framework (v0.74).
2
+ //
3
+ // Generic framework for running a user-supplied scanner alongside
4
+ // agentic-security on the same codebase and producing a comparison
5
+ // card. We DO NOT ship configs for specific competitors — the
6
+ // framework is bring-your-own-tool. The user provides:
7
+ //
8
+ // - the other tool's invocation (an argv array)
9
+ // - a JSON path / regex that pulls findings out of its output
10
+ // - the field names that map to {file, line, severity, vuln, cwe}
11
+ //
12
+ // The framework runs both, normalizes both to a common shape, and
13
+ // renders a Markdown comparison: overlap, agentic-security-only,
14
+ // other-only, severity disagreement.
15
+ //
16
+ // Usage:
17
+ // agentic-security compare \\
18
+ // --with "other-cli scan . --json" \\
19
+ // --field-file "path" --field-line "lineNumber" \\
20
+ // --field-severity "level" --field-vuln "ruleId" \\
21
+ // --out compare.md
22
+ //
23
+ // The framework has zero knowledge of which "other tool" is running —
24
+ // users supply config that maps the other tool's output shape to ours.
25
+
26
+ import { spawnSync } from 'node:child_process';
27
+ import * as fs from 'node:fs';
28
+ import { runFullScan } from './engine.js';
29
+
30
+ const SEVERITY_NORMALIZE = {
31
+ critical: 'critical', crit: 'critical', error: 'critical', '5': 'critical',
32
+ high: 'high', warning: 'high', warn: 'high', '4': 'high',
33
+ medium: 'medium', mid: 'medium', '3': 'medium',
34
+ low: 'low', info: 'low', '2': 'low',
35
+ note: 'info', none: 'info', '1': 'info',
36
+ };
37
+
38
+ function _normalizeSeverity(s) {
39
+ if (!s) return 'info';
40
+ return SEVERITY_NORMALIZE[String(s).toLowerCase()] || 'info';
41
+ }
42
+
43
+ /**
44
+ * Run the other tool with the given argv, parse its output as JSON,
45
+ * extract findings via the field map.
46
+ *
47
+ * argv: ['other-cli', 'scan', '.', '--json']
48
+ * fieldMap: { file: 'path', line: 'lineNumber', severity: 'level', vuln: 'ruleId', cwe: 'cwe' }
49
+ * rootArrayPath: 'results' (optional — JSONPath-lite into the response)
50
+ */
51
+ export function runOtherTool(argv, { fieldMap, rootArrayPath, timeoutMs = 120_000 } = {}) {
52
+ if (!Array.isArray(argv) || argv.length === 0) {
53
+ return { ok: false, reason: 'no-argv', findings: [] };
54
+ }
55
+ const r = spawnSync(argv[0], argv.slice(1), { encoding: 'utf8', timeout: timeoutMs });
56
+ if (r.error && r.error.code === 'ENOENT') {
57
+ return { ok: false, reason: 'binary-missing', findings: [] };
58
+ }
59
+ if (r.status === null) {
60
+ return { ok: false, reason: 'timed-out', findings: [] };
61
+ }
62
+ let parsed;
63
+ try { parsed = JSON.parse(r.stdout || '{}'); }
64
+ catch { return { ok: false, reason: 'json-parse-failed', findings: [] }; }
65
+ let arr = parsed;
66
+ if (rootArrayPath) {
67
+ for (const k of rootArrayPath.split('.')) arr = arr ? arr[k] : null;
68
+ }
69
+ if (!Array.isArray(arr)) arr = [];
70
+ const findings = arr.map(item => ({
71
+ file: _get(item, fieldMap?.file || 'file'),
72
+ line: Number(_get(item, fieldMap?.line || 'line')) || 0,
73
+ severity: _normalizeSeverity(_get(item, fieldMap?.severity || 'severity')),
74
+ vuln: String(_get(item, fieldMap?.vuln || 'vuln') || ''),
75
+ cwe: _get(item, fieldMap?.cwe || 'cwe') || null,
76
+ _other: true,
77
+ })).filter(f => f.file);
78
+ return { ok: true, findings, exitCode: r.status };
79
+ }
80
+
81
+ function _get(obj, path) {
82
+ if (!obj || !path) return null;
83
+ let cur = obj;
84
+ for (const k of String(path).split('.')) {
85
+ cur = cur ? cur[k] : null;
86
+ if (cur == null) return null;
87
+ }
88
+ return cur;
89
+ }
90
+
91
+ /**
92
+ * Compare two finding lists. Overlap is detected by (file, line, ±2)
93
+ * regardless of CWE — different tools sometimes assign different CWEs
94
+ * to the same shape.
95
+ *
96
+ * Returns:
97
+ * {
98
+ * overlap: [{ ours, theirs }], # both flagged same site
99
+ * oursOnly: Finding[], # we found, they missed
100
+ * theirsOnly: Finding[], # they found, we missed
101
+ * severityShift: [{ ours, theirs, oursSev, theirsSev }],
102
+ * }
103
+ */
104
+ export function compareFindings(ours, theirs) {
105
+ const oursByLoc = new Map();
106
+ for (const f of (ours || [])) {
107
+ if (!f.file) continue;
108
+ const key = `${f.file}:${f.line}`;
109
+ if (!oursByLoc.has(key)) oursByLoc.set(key, []);
110
+ oursByLoc.get(key).push(f);
111
+ }
112
+ const overlap = [];
113
+ const severityShift = [];
114
+ const theirsOnly = [];
115
+ const matchedOurs = new Set();
116
+ for (const t of (theirs || [])) {
117
+ let matched = null;
118
+ for (let d = -2; d <= 2; d++) {
119
+ const key = `${t.file}:${t.line + d}`;
120
+ const candidates = oursByLoc.get(key);
121
+ if (candidates && candidates.length) { matched = candidates[0]; break; }
122
+ }
123
+ if (matched) {
124
+ overlap.push({ ours: matched, theirs: t });
125
+ matchedOurs.add(matched);
126
+ if (matched.severity !== t.severity) {
127
+ severityShift.push({ ours: matched, theirs: t, oursSev: matched.severity, theirsSev: t.severity });
128
+ }
129
+ } else {
130
+ theirsOnly.push(t);
131
+ }
132
+ }
133
+ const oursOnly = (ours || []).filter(f => !matchedOurs.has(f));
134
+ return { overlap, oursOnly, theirsOnly, severityShift };
135
+ }
136
+
137
+ /**
138
+ * Render the comparison as a Markdown card. Tool names are user-supplied
139
+ * (per the no-competitor-names policy — the framework doesn't ship any
140
+ * built-in adapter for specific competitors).
141
+ */
142
+ export function renderComparison(comparison, { ourName = 'agentic-security', otherName = 'other-tool' } = {}) {
143
+ const c = comparison || { overlap: [], oursOnly: [], theirsOnly: [], severityShift: [] };
144
+ const lines = [];
145
+ lines.push(`# Comparison: ${ourName} vs. ${otherName}`);
146
+ lines.push('');
147
+ lines.push(`| Metric | ${ourName} | ${otherName} |`);
148
+ lines.push('|--------|------:|------:|');
149
+ lines.push(`| Findings | ${c.overlap.length + c.oursOnly.length} | ${c.overlap.length + c.theirsOnly.length} |`);
150
+ lines.push(`| Overlap | ${c.overlap.length} | ${c.overlap.length} |`);
151
+ lines.push(`| Unique | ${c.oursOnly.length} | ${c.theirsOnly.length} |`);
152
+ lines.push(`| Severity disagreement | ${c.severityShift.length} | — |`);
153
+ lines.push('');
154
+ if (c.oursOnly.length) {
155
+ lines.push(`### ${c.oursOnly.length} findings only ${ourName} caught`);
156
+ lines.push('');
157
+ for (const f of c.oursOnly.slice(0, 10)) {
158
+ lines.push(`- **${f.severity}** \`${f.file}:${f.line}\` — ${f.vuln} ${f.cwe ? `(${f.cwe})` : ''}`);
159
+ }
160
+ if (c.oursOnly.length > 10) lines.push(`- … +${c.oursOnly.length - 10} more`);
161
+ lines.push('');
162
+ }
163
+ if (c.theirsOnly.length) {
164
+ lines.push(`### ${c.theirsOnly.length} findings only ${otherName} caught`);
165
+ lines.push('');
166
+ for (const f of c.theirsOnly.slice(0, 10)) {
167
+ lines.push(`- **${f.severity}** \`${f.file}:${f.line}\` — ${f.vuln} ${f.cwe ? `(${f.cwe})` : ''}`);
168
+ }
169
+ if (c.theirsOnly.length > 10) lines.push(`- … +${c.theirsOnly.length - 10} more`);
170
+ lines.push('');
171
+ }
172
+ if (c.severityShift.length) {
173
+ lines.push(`### ${c.severityShift.length} severity disagreements`);
174
+ lines.push('');
175
+ for (const s of c.severityShift.slice(0, 10)) {
176
+ lines.push(`- \`${s.ours.file}:${s.ours.line}\` — ${ourName}: **${s.oursSev}**, ${otherName}: **${s.theirsSev}**`);
177
+ }
178
+ lines.push('');
179
+ }
180
+ if (c.overlap.length && !c.oursOnly.length && !c.theirsOnly.length) {
181
+ lines.push('Perfect overlap — both tools agree on every finding.');
182
+ }
183
+ return lines.join('\n');
184
+ }
185
+
186
+ /**
187
+ * Full pipeline: run agentic-security in-memory + run the other tool +
188
+ * compare + render.
189
+ */
190
+ export async function runComparison({ scanRoot, fileContents, otherArgv, otherFieldMap, otherRootArrayPath, ourName, otherName }) {
191
+ const ourScan = await runFullScan({ fileContents, scanRoot }, () => {});
192
+ const ours = (ourScan.findings || []).map(f => ({
193
+ file: f.file, line: f.line || 0,
194
+ severity: _normalizeSeverity(f.severity),
195
+ vuln: f.vuln, cwe: f.cwe || null,
196
+ }));
197
+ const other = runOtherTool(otherArgv, { fieldMap: otherFieldMap, rootArrayPath: otherRootArrayPath });
198
+ const comparison = compareFindings(ours, other.findings);
199
+ const md = renderComparison(comparison, { ourName, otherName });
200
+ return { ours, theirs: other.findings, comparison, markdown: md, otherStatus: other };
201
+ }
202
+
203
+ export const _internal = { _normalizeSeverity, _get };