@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,433 @@
1
+ // Continuous CVE-watch daemon.
2
+ //
3
+ // Polls OSV for the project's dependency tree on a schedule (or one-shot),
4
+ // diffs new advisories against state, fires the configured webhook on each
5
+ // new ID. Multi-ecosystem: reads npm / pip / cargo / gem / pub / composer /
6
+ // go.mod / maven manifests via the same DEP_FILE_NAMES list runScan uses.
7
+ //
8
+ // Why this exists separately from `/scan --sca`:
9
+ // - `/scan` is interactive; this runs unattended, daily, in a cron / GitHub Action.
10
+ // - `/scan` reports the FULL set of advisories for the tree; this reports
11
+ // only the DELTA since the last run.
12
+ // - This produces a webhook fire, not a console report.
13
+ //
14
+ // Configuration is read from `.agentic-security/cve-alerts.json`:
15
+ // {
16
+ // "alertUrl": "https://hooks.slack.com/services/...",
17
+ // "alertType": "slack" | "discord" | "generic",
18
+ // "minSeverity": "low" | "medium" | "high" | "critical" (optional)
19
+ // }
20
+ //
21
+ // State is persisted to `.agentic-security/cve-alerts-state.json`:
22
+ // { "known": ["CVE-2024-1234", "GHSA-...", ...], "lastRun": "2026-…" }
23
+ //
24
+ // The daemon NEVER fires a webhook for an advisory ID it has already seen;
25
+ // the state file is the deduplication primitive.
26
+
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+
30
+ const OSV_API = 'https://api.osv.dev/v1/query';
31
+ const CFG_PATH = '.agentic-security/cve-alerts.json';
32
+ const STATE_PATH = '.agentic-security/cve-alerts-state.json';
33
+
34
+ // Multi-ecosystem dep extraction. Each entry: { manifest filename → ecosystem }
35
+ const ECOSYSTEM_BY_MANIFEST = {
36
+ 'package.json': 'npm',
37
+ 'requirements.txt':'PyPI',
38
+ 'pyproject.toml': 'PyPI',
39
+ 'Pipfile': 'PyPI',
40
+ 'Pipfile.lock': 'PyPI',
41
+ 'poetry.lock': 'PyPI',
42
+ 'Gemfile': 'RubyGems',
43
+ 'Gemfile.lock': 'RubyGems',
44
+ 'go.mod': 'Go',
45
+ 'go.sum': 'Go',
46
+ 'Cargo.toml': 'crates.io',
47
+ 'Cargo.lock': 'crates.io',
48
+ 'composer.json': 'Packagist',
49
+ 'composer.lock': 'Packagist',
50
+ 'pom.xml': 'Maven',
51
+ 'build.gradle': 'Maven',
52
+ 'build.gradle.kts':'Maven',
53
+ 'pubspec.yaml': 'Pub',
54
+ 'pubspec.lock': 'Pub',
55
+ };
56
+
57
+ // ─── Manifest readers ─────────────────────────────────────────────────────
58
+
59
+ function _readPackageJson(content) {
60
+ try {
61
+ const j = JSON.parse(content);
62
+ return [
63
+ ...Object.keys(j.dependencies || {}),
64
+ ...Object.keys(j.devDependencies || {}),
65
+ ...Object.keys(j.peerDependencies || {}),
66
+ ...Object.keys(j.optionalDependencies || {}),
67
+ ];
68
+ } catch { return []; }
69
+ }
70
+
71
+ function _readRequirementsTxt(content) {
72
+ // Each non-blank, non-comment line is "pkg" or "pkg==1.2.3" or "pkg>=1.2".
73
+ const out = [];
74
+ for (const raw of content.split('\n')) {
75
+ const line = raw.trim();
76
+ if (!line || line.startsWith('#') || line.startsWith('-')) continue;
77
+ const m = /^([A-Za-z0-9_.\-]+)/.exec(line);
78
+ if (m) out.push(m[1]);
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function _readPyprojectToml(content) {
84
+ // We don't ship a TOML parser; do a best-effort line scan for the common
85
+ // dep-table shapes. Caller is OK if this misses some — the full SCA scan
86
+ // does the heavy lifting.
87
+ const out = new Set();
88
+ // PEP 621 [project] dependencies = ["foo", "bar>=1.0"]
89
+ // Poetry [tool.poetry.dependencies] foo = "^1.0"
90
+ let inDeps = false;
91
+ for (const raw of content.split('\n')) {
92
+ const line = raw.trim();
93
+ if (!line || line.startsWith('#')) continue;
94
+ if (/^\[(?:tool\.poetry\.)?(?:dev-)?dependencies\]/i.test(line) || /^\[project\.optional-dependencies\.[^\]]+\]/.test(line)) {
95
+ inDeps = true; continue;
96
+ }
97
+ if (/^\[[^\]]+\]/.test(line)) { inDeps = false; continue; }
98
+ // Inline "dependencies = [...]" form within [project]
99
+ const inline = /dependencies\s*=\s*\[([^\]]+)\]/i.exec(line);
100
+ if (inline) {
101
+ for (const item of inline[1].split(',')) {
102
+ const m = /^[\s"']*([A-Za-z0-9_.\-]+)/.exec(item);
103
+ if (m) out.add(m[1]);
104
+ }
105
+ }
106
+ if (inDeps) {
107
+ const m = /^([A-Za-z0-9_.\-]+)\s*=/.exec(line);
108
+ if (m && m[1] !== 'python') out.add(m[1]);
109
+ }
110
+ }
111
+ return [...out];
112
+ }
113
+
114
+ function _readGemfile(content) {
115
+ const out = [];
116
+ for (const raw of content.split('\n')) {
117
+ const m = /^\s*gem\s+['"]([^'"]+)['"]/.exec(raw);
118
+ if (m) out.push(m[1]);
119
+ }
120
+ return out;
121
+ }
122
+
123
+ function _readGoMod(content) {
124
+ const out = [];
125
+ let inRequire = false;
126
+ for (const raw of content.split('\n')) {
127
+ const line = raw.trim();
128
+ if (line.startsWith('require (')) { inRequire = true; continue; }
129
+ if (inRequire && line === ')') { inRequire = false; continue; }
130
+ if (inRequire || line.startsWith('require ')) {
131
+ // "require module/path v1.2.3" or just "module/path v1.2.3" inside block
132
+ const m = /(?:require\s+)?([A-Za-z0-9_./\-]+)\s+v/.exec(line);
133
+ if (m) out.push(m[1]);
134
+ }
135
+ }
136
+ return out;
137
+ }
138
+
139
+ function _readCargoToml(content) {
140
+ // Lines under [dependencies] / [dev-dependencies] / [build-dependencies]
141
+ // shaped as `pkg = "1.0"` or `pkg = { … }`.
142
+ const out = new Set();
143
+ let inDeps = false;
144
+ for (const raw of content.split('\n')) {
145
+ const line = raw.trim();
146
+ if (/^\[(?:dev-|build-)?dependencies\]/.test(line)) { inDeps = true; continue; }
147
+ if (/^\[[^\]]+\]/.test(line)) { inDeps = false; continue; }
148
+ if (inDeps) {
149
+ const m = /^([A-Za-z0-9_\-]+)\s*=/.exec(line);
150
+ if (m) out.add(m[1]);
151
+ }
152
+ }
153
+ return [...out];
154
+ }
155
+
156
+ function _readComposerJson(content) {
157
+ try {
158
+ const j = JSON.parse(content);
159
+ return [
160
+ ...Object.keys(j.require || {}).filter(k => k !== 'php'),
161
+ ...Object.keys(j['require-dev'] || {}).filter(k => k !== 'php'),
162
+ ];
163
+ } catch { return []; }
164
+ }
165
+
166
+ function _readMavenLikeXml(content) {
167
+ // crude <artifactId>x</artifactId> extraction. Misses parent POM inheritance;
168
+ // for full coverage the /scan --sca path is the right tool.
169
+ const out = [];
170
+ const re = /<artifactId>\s*([^<\s]+)\s*<\/artifactId>/g;
171
+ let m;
172
+ while ((m = re.exec(content)) !== null) out.push(m[1]);
173
+ return out;
174
+ }
175
+
176
+ function _readPubspecYaml(content) {
177
+ const out = new Set();
178
+ let inDeps = false;
179
+ for (const raw of content.split('\n')) {
180
+ const line = raw.trimEnd();
181
+ if (/^(dev_|)dependencies\s*:/i.test(line)) { inDeps = true; continue; }
182
+ if (line && !line.startsWith(' ') && !line.startsWith('\t')) {
183
+ // top-level key other than dependencies / dev_dependencies
184
+ if (!/^(dev_|)dependencies\s*:/i.test(line)) inDeps = false;
185
+ }
186
+ if (inDeps) {
187
+ const m = /^\s+([A-Za-z0-9_]+)\s*:/.exec(line);
188
+ if (m && m[1] !== 'sdk' && m[1] !== 'flutter') out.add(m[1]);
189
+ }
190
+ }
191
+ return [...out];
192
+ }
193
+
194
+ const MANIFEST_READERS = {
195
+ 'package.json': _readPackageJson,
196
+ 'requirements.txt': _readRequirementsTxt,
197
+ 'pyproject.toml': _readPyprojectToml,
198
+ 'Pipfile': _readPyprojectToml, // close-enough shape
199
+ 'Gemfile': _readGemfile,
200
+ 'go.mod': _readGoMod,
201
+ 'Cargo.toml': _readCargoToml,
202
+ 'composer.json': _readComposerJson,
203
+ 'pom.xml': _readMavenLikeXml,
204
+ 'build.gradle': _readMavenLikeXml,
205
+ 'build.gradle.kts': _readMavenLikeXml,
206
+ 'pubspec.yaml': _readPubspecYaml,
207
+ };
208
+
209
+ export function readDeps(scanRoot) {
210
+ // Walks the scan root looking for known manifest files. Returns a flat
211
+ // [{name, ecosystem}, ...] list; deduplicated across manifests so a
212
+ // package in both package.json and yarn.lock isn't queried twice.
213
+ const seen = new Set();
214
+ const out = [];
215
+ function walk(dir, depth) {
216
+ if (depth > 4) return; // shallow walk; manifests live near the root
217
+ let entries;
218
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
219
+ for (const e of entries) {
220
+ if (e.name.startsWith('.') && e.name !== '.gradle') continue;
221
+ if (e.name === 'node_modules' || e.name === 'venv' || e.name === '__pycache__'
222
+ || e.name === 'vendor' || e.name === 'dist' || e.name === 'build'
223
+ || e.name === 'target') continue;
224
+ const fp = path.join(dir, e.name);
225
+ if (e.isDirectory()) { walk(fp, depth + 1); continue; }
226
+ if (!ECOSYSTEM_BY_MANIFEST[e.name]) continue;
227
+ let content;
228
+ try { content = fs.readFileSync(fp, 'utf8'); } catch { continue; }
229
+ const reader = MANIFEST_READERS[e.name];
230
+ if (!reader) continue;
231
+ const ecosystem = ECOSYSTEM_BY_MANIFEST[e.name];
232
+ for (const name of reader(content)) {
233
+ const key = `${ecosystem}::${name}`;
234
+ if (seen.has(key)) continue;
235
+ seen.add(key);
236
+ out.push({ name, ecosystem });
237
+ }
238
+ }
239
+ }
240
+ walk(scanRoot, 0);
241
+ return out;
242
+ }
243
+
244
+ // ─── OSV query ────────────────────────────────────────────────────────────
245
+
246
+ export async function _queryOsvForDep({ name, ecosystem }, { fetchImpl = globalThis.fetch, timeoutMs = 4000 } = {}) {
247
+ // POST { package: { name, ecosystem } } — returns the full vulnerability
248
+ // list for the package (any version). The daemon doesn't filter by version
249
+ // because we treat "new advisory ID" as the signal, not "your installed
250
+ // version is affected" — that's what /scan --sca produces.
251
+ try {
252
+ const controller = new AbortController();
253
+ const t = setTimeout(() => controller.abort(), timeoutMs);
254
+ const res = await fetchImpl(OSV_API, {
255
+ method: 'POST',
256
+ headers: { 'Content-Type': 'application/json' },
257
+ body: JSON.stringify({ package: { name, ecosystem } }),
258
+ signal: controller.signal,
259
+ });
260
+ clearTimeout(t);
261
+ if (!res.ok) return [];
262
+ const j = await res.json();
263
+ return Array.isArray(j.vulns) ? j.vulns : [];
264
+ } catch { return []; }
265
+ }
266
+
267
+ // ─── State persistence ───────────────────────────────────────────────────
268
+
269
+ export function loadState(scanRoot) {
270
+ const fp = path.join(scanRoot, STATE_PATH);
271
+ if (!fs.existsSync(fp)) return { known: new Set(), lastRun: null };
272
+ try {
273
+ const j = JSON.parse(fs.readFileSync(fp, 'utf8'));
274
+ return { known: new Set(j.known || []), lastRun: j.lastRun || null };
275
+ } catch { return { known: new Set(), lastRun: null }; }
276
+ }
277
+
278
+ export function persistState(scanRoot, state) {
279
+ const fp = path.join(scanRoot, STATE_PATH);
280
+ fs.mkdirSync(path.dirname(fp), { recursive: true });
281
+ fs.writeFileSync(fp, JSON.stringify({
282
+ known: [...state.known].sort(),
283
+ lastRun: state.lastRun,
284
+ }, null, 2));
285
+ }
286
+
287
+ export function loadConfig(scanRoot) {
288
+ const fp = path.join(scanRoot, CFG_PATH);
289
+ if (!fs.existsSync(fp)) return null;
290
+ try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
291
+ }
292
+
293
+ // ─── Webhook formatting ──────────────────────────────────────────────────
294
+
295
+ function _truncate(s, n) { s = String(s || ''); return s.length > n ? s.slice(0, n) + '…' : s; }
296
+
297
+ export function formatPayload(alertType, newAdvisories) {
298
+ // alertType: 'slack' | 'discord' | 'generic'
299
+ // Slack and Discord both accept `{ text }` for simple messages.
300
+ const lines = newAdvisories.slice(0, 10).map(a =>
301
+ `• \`${a.ecosystem}:${a.pkg}\` — ${a.id} (${a.severity || 'unknown'})${a.summary ? ' — ' + _truncate(a.summary, 100) : ''}`
302
+ );
303
+ const overflow = newAdvisories.length > 10 ? `\n+${newAdvisories.length - 10} more` : '';
304
+ const text = `🚨 *New CVEs in your dependencies* (${newAdvisories.length})\n` + lines.join('\n') + overflow + '\n\nRun `/scan --all` to assess impact.';
305
+ if (alertType === 'slack' || alertType === 'discord') return { text };
306
+ // Generic webhook: full structured payload.
307
+ return { event: 'new_cve', count: newAdvisories.length, advisories: newAdvisories };
308
+ }
309
+
310
+ async function _post(url, payload, { fetchImpl = globalThis.fetch, timeoutMs = 4000 } = {}) {
311
+ try {
312
+ const controller = new AbortController();
313
+ const t = setTimeout(() => controller.abort(), timeoutMs);
314
+ const res = await fetchImpl(url, {
315
+ method: 'POST',
316
+ headers: { 'Content-Type': 'application/json' },
317
+ body: JSON.stringify(payload),
318
+ signal: controller.signal,
319
+ });
320
+ clearTimeout(t);
321
+ return { ok: res.ok, status: res.status };
322
+ } catch (e) { return { ok: false, error: String(e?.message || e).slice(0, 200) }; }
323
+ }
324
+
325
+ // ─── Severity filter ─────────────────────────────────────────────────────
326
+
327
+ const SEV_RANK = { critical: 4, high: 3, medium: 2, low: 1, unknown: 0 };
328
+
329
+ function _normalizeSeverity(vuln) {
330
+ // OSV severity has a few possible shapes. Best-effort.
331
+ const dbSpec = vuln.database_specific?.severity || '';
332
+ if (/critical/i.test(dbSpec)) return 'critical';
333
+ if (/high/i.test(dbSpec)) return 'high';
334
+ if (/medium|moderate/i.test(dbSpec)) return 'medium';
335
+ if (/low/i.test(dbSpec)) return 'low';
336
+ // CVSS score
337
+ const score = vuln.severity?.[0]?.score;
338
+ if (typeof score === 'number') {
339
+ if (score >= 9) return 'critical';
340
+ if (score >= 7) return 'high';
341
+ if (score >= 4) return 'medium';
342
+ return 'low';
343
+ }
344
+ return 'unknown';
345
+ }
346
+
347
+ // ─── Main entry — runOnce ───────────────────────────────────────────────
348
+
349
+ export async function runOnce(scanRoot, opts = {}) {
350
+ // opts: { alertUrl, alertType, minSeverity, dryRun, offline, fetchImpl,
351
+ // logger, throttleMs }
352
+ const log = opts.logger || ((msg) => process.stderr.write(msg + '\n'));
353
+ const cfg = loadConfig(scanRoot);
354
+ const alertUrl = opts.alertUrl || cfg?.alertUrl || process.env.CVE_ALERT_URL;
355
+ const alertType = opts.alertType || cfg?.alertType || 'slack';
356
+ const minSeverity = opts.minSeverity || cfg?.minSeverity || 'low';
357
+ const dryRun = !!opts.dryRun;
358
+ const offline = !!opts.offline || process.env.AGENTIC_SECURITY_OFFLINE === '1';
359
+ const fetchImpl = opts.fetchImpl || globalThis.fetch;
360
+ const throttleMs = typeof opts.throttleMs === 'number' ? opts.throttleMs : 50;
361
+
362
+ if (offline && !dryRun) {
363
+ return { ok: false, reason: 'offline-mode-set' };
364
+ }
365
+ if (typeof fetchImpl !== 'function') {
366
+ return { ok: false, reason: 'fetch-unavailable' };
367
+ }
368
+
369
+ const deps = readDeps(scanRoot);
370
+ if (deps.length === 0) {
371
+ return { ok: true, depsChecked: 0, newAdvisories: [], alertSent: false, reason: 'no-deps-found' };
372
+ }
373
+
374
+ const state = loadState(scanRoot);
375
+ const newAdvisories = [];
376
+ const sevFloor = SEV_RANK[minSeverity] ?? 0;
377
+
378
+ for (const dep of deps) {
379
+ const vulns = await _queryOsvForDep(dep, { fetchImpl, timeoutMs: opts.timeoutMs });
380
+ for (const v of vulns) {
381
+ if (!v.id || state.known.has(v.id)) continue;
382
+ const sev = _normalizeSeverity(v);
383
+ if ((SEV_RANK[sev] ?? 0) < sevFloor) {
384
+ // Below user's floor; remember the ID so we don't re-fire next run,
385
+ // but don't surface in the alert.
386
+ state.known.add(v.id);
387
+ continue;
388
+ }
389
+ newAdvisories.push({
390
+ id: v.id,
391
+ pkg: dep.name,
392
+ ecosystem: dep.ecosystem,
393
+ severity: sev,
394
+ summary: v.summary || '',
395
+ published: v.published || null,
396
+ });
397
+ state.known.add(v.id);
398
+ }
399
+ if (throttleMs > 0) await new Promise(r => setTimeout(r, throttleMs));
400
+ }
401
+
402
+ state.lastRun = new Date().toISOString();
403
+ if (!dryRun) persistState(scanRoot, state);
404
+
405
+ let alertSent = false;
406
+ let alertError = null;
407
+ if (newAdvisories.length > 0 && alertUrl && !dryRun) {
408
+ const payload = formatPayload(alertType, newAdvisories);
409
+ const r = await _post(alertUrl, payload, { fetchImpl, timeoutMs: opts.timeoutMs });
410
+ alertSent = r.ok;
411
+ if (!r.ok) alertError = r.error || `HTTP ${r.status}`;
412
+ }
413
+
414
+ log(`agentic-security cve-watch: deps=${deps.length} new=${newAdvisories.length} sent=${alertSent}${alertError ? ' err=' + alertError : ''}${dryRun ? ' (dry-run)' : ''}`);
415
+
416
+ return {
417
+ ok: true,
418
+ depsChecked: deps.length,
419
+ newAdvisories,
420
+ alertSent,
421
+ alertError,
422
+ minSeverity,
423
+ dryRun,
424
+ };
425
+ }
426
+
427
+ export const _internals = {
428
+ ECOSYSTEM_BY_MANIFEST,
429
+ MANIFEST_READERS,
430
+ _readPackageJson, _readRequirementsTxt, _readPyprojectToml, _readGemfile,
431
+ _readGoMod, _readCargoToml, _readComposerJson, _readMavenLikeXml, _readPubspecYaml,
432
+ _normalizeSeverity,
433
+ };
@@ -0,0 +1,129 @@
1
+ // CVE lookup — read-only against the per-install OSV / KEV / EPSS caches.
2
+ //
3
+ // LangChain harness-anatomy post:
4
+ // "Knowledge cutoffs mean that models can't directly access new data like
5
+ // updated library versions without the user providing them directly."
6
+ //
7
+ // The validator and any subagent reasoning about an SCA finding can call
8
+ // `lookup_cve(cve_id)` to get the most recently-cached OSV advisory, the
9
+ // CISA KEV entry if listed, and the EPSS exploit-prediction percentile, all
10
+ // with `staleness` metadata so the caller can decide whether to trust the
11
+ // cached value.
12
+ //
13
+ // This module deliberately NEVER triggers a network fetch — the scan
14
+ // pipeline is the only thing that populates the cache. If a CVE isn't
15
+ // cached, we return `present: false` for that source rather than blocking
16
+ // on a fetch and risking a multi-second MCP timeout.
17
+
18
+ import * as fs from 'node:fs';
19
+ import * as os from 'node:os';
20
+ import * as path from 'node:path';
21
+ import * as crypto from 'node:crypto';
22
+
23
+ const CACHE_DIR = path.join(os.homedir(), '.claude', 'agentic-security', 'osv-cache');
24
+
25
+ function _keyToPath(key) {
26
+ const safe = crypto.createHash('sha256').update(key).digest('hex');
27
+ return path.join(CACHE_DIR, safe + '.json');
28
+ }
29
+
30
+ function _readCache(key) {
31
+ const fp = _keyToPath(key);
32
+ if (!fs.existsSync(fp)) return { present: false };
33
+ let body;
34
+ try { body = fs.readFileSync(fp, 'utf8'); }
35
+ catch { return { present: false, error: 'unreadable' }; }
36
+ let parsed;
37
+ try { parsed = JSON.parse(body); }
38
+ catch { return { present: false, error: 'unparseable' }; }
39
+ let mtime = null;
40
+ try { mtime = fs.statSync(fp).mtimeMs; } catch {}
41
+ return { present: true, data: parsed, cachedAt: mtime, ageMs: mtime ? Date.now() - mtime : null };
42
+ }
43
+
44
+ function _stalenessTier(ageMs) {
45
+ if (ageMs === null || ageMs === undefined) return 'unknown';
46
+ if (ageMs < 24 * 3600 * 1000) return 'fresh'; // <1d
47
+ if (ageMs < 7 * 24 * 3600 * 1000) return 'recent'; // <1w
48
+ if (ageMs < 30 * 24 * 3600 * 1000) return 'stale'; // <1mo
49
+ return 'very-stale';
50
+ }
51
+
52
+ const CVE_RE = /^CVE-\d{4}-\d{1,7}$/i;
53
+
54
+ export function lookupCve(rawId) {
55
+ if (typeof rawId !== 'string' || !CVE_RE.test(rawId)) {
56
+ return { ok: false, reason: 'invalid-cve-id', expected: 'CVE-YYYY-NNNN' };
57
+ }
58
+ const cve = rawId.toUpperCase();
59
+
60
+ // KEV catalog — single cached blob keyed at 'kev:catalog'.
61
+ const kevCacheRaw = _readCache('kev:catalog');
62
+ let kev = { present: false };
63
+ if (kevCacheRaw.present) {
64
+ // The blob shape from engine.js: { ts, byCve: { 'CVE-XXX': { ... } } }
65
+ // sessionStorage shim stores the value as the JSON-stringified inner
66
+ // object directly (no extra wrapper).
67
+ const blob = kevCacheRaw.data;
68
+ const byCve = blob?.byCve || null;
69
+ if (byCve && byCve[cve]) {
70
+ kev = {
71
+ present: true,
72
+ ...byCve[cve],
73
+ cachedAt: kevCacheRaw.cachedAt,
74
+ ageMs: kevCacheRaw.ageMs,
75
+ staleness: _stalenessTier(kevCacheRaw.ageMs),
76
+ };
77
+ } else if (byCve) {
78
+ // Catalog is cached but doesn't list this CVE — meaningful negative.
79
+ kev = {
80
+ present: false, listedInCatalog: false,
81
+ cachedAt: kevCacheRaw.cachedAt, ageMs: kevCacheRaw.ageMs,
82
+ staleness: _stalenessTier(kevCacheRaw.ageMs),
83
+ };
84
+ }
85
+ }
86
+
87
+ // EPSS — per-CVE cache at 'epss:CVE-XXX'.
88
+ const epssRaw = _readCache('epss:' + cve);
89
+ let epss = { present: false };
90
+ if (epssRaw.present) {
91
+ epss = {
92
+ present: epssRaw.data !== false, // engine stores `false` for "looked up, no record"
93
+ score: epssRaw.data?.score ?? null,
94
+ percentile: epssRaw.data?.percentile ?? null,
95
+ cachedAt: epssRaw.cachedAt,
96
+ ageMs: epssRaw.ageMs,
97
+ staleness: _stalenessTier(epssRaw.ageMs),
98
+ };
99
+ }
100
+
101
+ // OSV — entries are keyed by vuln id (GHSA-... or CVE-...). The engine
102
+ // caches them at 'vuln:<id>'. We do a direct CVE lookup AND a soft probe
103
+ // for any known alias the caller provided implicitly through the KEV
104
+ // hit's vendor/product (no — we keep this simple: direct lookup only).
105
+ const osvRaw = _readCache('vuln:' + cve);
106
+ let osv = { present: false };
107
+ if (osvRaw.present) {
108
+ osv = {
109
+ present: true,
110
+ data: osvRaw.data,
111
+ cachedAt: osvRaw.cachedAt, ageMs: osvRaw.ageMs,
112
+ staleness: _stalenessTier(osvRaw.ageMs),
113
+ };
114
+ }
115
+
116
+ return {
117
+ ok: true,
118
+ cve,
119
+ kev,
120
+ epss,
121
+ osv,
122
+ sourcesFound: [kev.present, epss.present, osv.present].filter(Boolean).length,
123
+ note: (kev.present || epss.present || osv.present)
124
+ ? 'cached values only; staleness tier per source. The MCP tool does NOT trigger a network fetch.'
125
+ : 'no cached data for this CVE on the current install. Run a scan against a project that depends on the affected package, or set $AGENTIC_SECURITY_OFFLINE=0 and run a scan to populate the cache.',
126
+ };
127
+ }
128
+
129
+ export const _internals = { CACHE_DIR, CVE_RE, _stalenessTier };