@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,101 @@
1
+ // Webhook signature verification audit.
2
+ //
3
+ // Every major webhook provider (Stripe, GitHub, Clerk, Svix, Resend, Twilio)
4
+ // requires callers to verify the request signature before processing the
5
+ // payload. Skipping verification means anyone who discovers your webhook URL
6
+ // can trigger real business logic (fake payments, fake user events, fake
7
+ // deploys) with zero authentication.
8
+ //
9
+ // F1 safety: rules fire only when ALL of:
10
+ // 1. The file path or a route string contains "webhook" or provider name
11
+ // 2. The file reads req.body or payload from request
12
+ // 3. NO recognised verification call is present in the file
13
+ //
14
+ // Benchmark apps (NodeGoat, Juice Shop) predate webhook patterns; this rule
15
+ // produces no findings on them.
16
+
17
+ const _SCAN_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i;
18
+ const _NONPROD_RE = /(?:^|\/)(?:tests?|__tests__|spec|fixtures?|examples?|node_modules)\//i;
19
+
20
+ // File-path signals: the file is a webhook handler
21
+ const WEBHOOK_FILE_RE = /(?:^|\/)(?:webhook|webhooks|wh|hook|hooks)[\w.-]*\.[cm]?[jt]sx?$/i;
22
+ // Route string signals within file content
23
+ const WEBHOOK_ROUTE_RE = /(?:router|app|server)\s*\.\s*(?:post|all)\s*\(\s*['"`][^'"`]*webhook[^'"`]*['"`]/i;
24
+ // Next.js route file in a webhook directory/segment
25
+ const NEXT_WEBHOOK_RE = /(?:^|\/)(?:app|pages)\/(?:api\/)?[^/]*webhook[^/]*\/(?:route|index)\.[cm]?[jt]sx?$/i;
26
+
27
+ // Provider-specific verification calls
28
+ const STRIPE_VERIFY_RE = /(?:stripe|Stripe)\s*\.\s*webhooks?\s*\.\s*constructEvent/;
29
+ const GITHUB_VERIFY_RE = /(?:X-Hub-Signature|x-hub-signature|createHmac|timingSafeEqual)[^;]{0,200}(?:sha256|sha1)/i;
30
+ const SVIX_VERIFY_RE = /(?:new\s+Webhook|wh\.verify|Svix|svix)/;
31
+ const CLERK_VERIFY_RE = /(?:verifyWebhook|clerkClient\.verifyToken|Webhook\s*\()/;
32
+ const RESEND_VERIFY_RE = /(?:Resend\.verifyWebhookSignature|resend\.webhooks\.verify)/i;
33
+ const TWILIO_VERIFY_RE = /(?:twilio\.validateRequest|validateExpressRequest|validateWebhook)/i;
34
+ const GENERIC_SIG_VERIFY_RE = /(?:signature|sig)\s*[!=]{2,3}|timingSafeEqual|hmac\.digest|verifySignature|validateSignature|webhookSecret|WEBHOOK_SECRET/i;
35
+
36
+ // Request body consumed (confirms it's a handler, not a type def)
37
+ const BODY_READ_RE = /(?:req|request)\s*\.\s*(?:body|rawBody|text\(\)|json\(\))|await\s+(?:req|request)\.(?:text|json)\s*\(/;
38
+
39
+ function _isVerified(content) {
40
+ return STRIPE_VERIFY_RE.test(content) ||
41
+ GITHUB_VERIFY_RE.test(content) ||
42
+ SVIX_VERIFY_RE.test(content) ||
43
+ CLERK_VERIFY_RE.test(content) ||
44
+ RESEND_VERIFY_RE.test(content) ||
45
+ TWILIO_VERIFY_RE.test(content) ||
46
+ GENERIC_SIG_VERIFY_RE.test(content);
47
+ }
48
+
49
+ function scanWebhook(file, content) {
50
+ if (!_SCAN_EXT_RE.test(file)) return [];
51
+ if (_NONPROD_RE.test(file)) return [];
52
+
53
+ // Gate 1: is this actually a webhook handler file?
54
+ const isWebhookFile = WEBHOOK_FILE_RE.test(file) || NEXT_WEBHOOK_RE.test(file);
55
+ const hasWebhookRoute = WEBHOOK_ROUTE_RE.test(content);
56
+ if (!isWebhookFile && !hasWebhookRoute) return [];
57
+
58
+ // Gate 2: does it read the request body (confirms it's a handler, not a util)?
59
+ if (!BODY_READ_RE.test(content)) return [];
60
+
61
+ // Gate 3: no verification present → finding
62
+ if (_isVerified(content)) return [];
63
+
64
+ // Detect which provider(s) are referenced to give a precise title
65
+ const providers = [];
66
+ if (/stripe/i.test(content)) providers.push('Stripe');
67
+ if (/github/i.test(content)) providers.push('GitHub');
68
+ if (/svix/i.test(content)) providers.push('Svix');
69
+ if (/clerk/i.test(content)) providers.push('Clerk');
70
+ if (/resend/i.test(content)) providers.push('Resend');
71
+ if (/twilio/i.test(content)) providers.push('Twilio');
72
+ const providerStr = providers.length ? providers.join('/') + ' ' : '';
73
+
74
+ // Find the line of the first body read or route definition
75
+ const lines = content.split('\n');
76
+ const triggerLine = lines.findIndex(l => BODY_READ_RE.test(l) || WEBHOOK_ROUTE_RE.test(l));
77
+ const lineNum = triggerLine >= 0 ? triggerLine + 1 : 1;
78
+
79
+ const providerRemediations = {
80
+ 'Stripe': 'const event = stripe.webhooks.constructEvent(rawBody, req.headers[\'stripe-signature\'], process.env.STRIPE_WEBHOOK_SECRET);',
81
+ 'GitHub': 'Use crypto.timingSafeEqual to compare HMAC-SHA256 of the raw body against the X-Hub-Signature-256 header.',
82
+ 'Svix': 'const wh = new Webhook(process.env.WEBHOOK_SECRET); wh.verify(payload, headers);',
83
+ 'Clerk': 'const evt = await clerkClient.verifyWebhook(req);',
84
+ };
85
+ const fixSnippet = providers.length
86
+ ? providerRemediations[providers[0]] || 'Verify the provider-specific HMAC signature before processing the payload.'
87
+ : 'Verify the HMAC signature from the webhook provider before processing any payload data.';
88
+
89
+ return [{
90
+ id: `webhook:MISSING_SIGNATURE_VERIFY:${file}:${lineNum}`,
91
+ title: `${providerStr}Webhook handler missing signature verification`,
92
+ severity: 'high',
93
+ file, line: lineNum,
94
+ vuln: 'Webhook — Missing Signature Verification',
95
+ description: `This webhook handler reads the request body without verifying the ${providerStr}signature header. Anyone who discovers the endpoint URL can POST arbitrary payloads and trigger real business logic — fake Stripe payments marked as successful, fake GitHub events triggering deploys, fake user creation events.`,
96
+ remediation: fixSnippet + '\n\nIMPORTANT: you must pass the raw (un-parsed) request body to the signature verifier, not the parsed JSON object.',
97
+ cwe: 'CWE-345',
98
+ }];
99
+ }
100
+
101
+ export { scanWebhook };
@@ -0,0 +1,51 @@
1
+ import { blankComments } from './_comment-strip.js';
2
+ // XPath injection.
3
+ //
4
+ // Same shape as LDAP injection — string concatenation into a query language
5
+ // that has its own operators. We catch concatenation patterns into:
6
+ // - javax.xml.xpath / org.jaxen / org.dom4j (Java)
7
+ // - lxml.etree / xml.etree (Python)
8
+ // - xpath npm pkg (Node)
9
+
10
+ const PATTERNS = {
11
+ java: /\.\s*(?:compile|evaluate)\s*\(\s*"[^"]*"\s*\+\s*\w+/g,
12
+ py: /\.\s*(?:xpath|find|findall)\s*\(\s*["'][^"']*["']\s*[%+]\s*\w+|\.\s*xpath\s*\(\s*f["']/g,
13
+ js: /\b(?:xpath|select)\s*\(\s*[`"][^`"]*[`"]\s*\+\s*\w+|\bxpath\.select\s*\(\s*`[^`]*\$\{/g,
14
+ };
15
+
16
+ function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
17
+
18
+ export function scanXPathInjection(fp, raw) {
19
+ if (!raw || raw.length > 500_000) return [];
20
+ let lang;
21
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) lang = 'js';
22
+ else if (/\.java$/i.test(fp)) lang = 'java';
23
+ else if (/\.py$/i.test(fp)) lang = 'py';
24
+ else return [];
25
+
26
+ const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
27
+ if (!/\bxpath|XPath|\.xpath\(/i.test(code)) return [];
28
+ const re = new RegExp(PATTERNS[lang].source, PATTERNS[lang].flags);
29
+ const findings = [];
30
+ const seen = new Set();
31
+ let m;
32
+ while ((m = re.exec(code))) {
33
+ const line = lineOf(raw, m.index);
34
+ const id = `xpath-injection:${fp}:${line}`;
35
+ if (seen.has(id)) continue;
36
+ seen.add(id);
37
+ findings.push({
38
+ id,
39
+ file: fp, line,
40
+ vuln: 'XPath Injection: query built via string concatenation',
41
+ severity: 'high',
42
+ cwe: 'CWE-643',
43
+ stride: 'Tampering',
44
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
45
+ remediation: 'Use a parameterized XPath API. Java: `XPathExpression.evaluate(doc, XPathConstants.NODESET)` with `xpath.setXPathVariableResolver(...)`. Python lxml: `tree.xpath("//user[name=$n]", n=name)`. JavaScript: pass values as variables to an evaluator that supports binding, never via concatenation.',
46
+ parser: 'XPATH-INJECTION',
47
+ confidence: 0.85,
48
+ });
49
+ }
50
+ return findings;
51
+ }
@@ -0,0 +1,140 @@
1
+ // XML External Entity (XXE) detection for Java and Python.
2
+ // Node.js xml2js/libxmljs/sax is already covered in engine.js SINK_PATTERNS.
3
+ //
4
+ // Java vulnerable APIs:
5
+ // - DocumentBuilderFactory.newInstance() (CWE-611)
6
+ // - SAXParserFactory.newInstance()
7
+ // - XMLInputFactory.newInstance() (StAX)
8
+ // - SAXBuilder() (JDOM)
9
+ // - SchemaFactory.newInstance()
10
+ // - TransformerFactory.newInstance()
11
+ // - XMLReaderFactory.createXMLReader()
12
+ //
13
+ // Java-safe configurations (any one suppresses the finding for the file):
14
+ // - setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
15
+ // - setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
16
+ // - setExpandEntityReferences(false)
17
+ // - setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false)
18
+ // - setXIncludeAware(false) + setExpandEntityReferences(false)
19
+ //
20
+ // Python vulnerable APIs:
21
+ // - lxml.etree.parse / fromstring (CVE class — XXE possible)
22
+ // - xml.etree.ElementTree.parse / fromstring (older Python; modern is safer
23
+ // but defusedxml is the canonical fix)
24
+ // - xml.sax.parse / parseString / make_parser
25
+ // - xml.dom.minidom.parse / parseString
26
+ // - xml.dom.pulldom.parse / parseString
27
+ //
28
+ // Python-safe configurations:
29
+ // - `from defusedxml` import anywhere in the file
30
+ // - `import defusedxml`
31
+ // - For lxml: parser with `resolve_entities=False, no_network=True`
32
+
33
+ const JAVA_VULN_PATTERNS = [
34
+ { name: 'DocumentBuilderFactory', re: /\bDocumentBuilderFactory\s*\.\s*newInstance\s*\(\s*\)/g },
35
+ { name: 'SAXParserFactory', re: /\bSAXParserFactory\s*\.\s*newInstance\s*\(\s*\)/g },
36
+ { name: 'XMLInputFactory', re: /\bXMLInputFactory\s*\.\s*newInstance\s*\(\s*\)/g },
37
+ { name: 'SAXBuilder', re: /\bnew\s+SAXBuilder\s*\(\s*\)/g },
38
+ { name: 'SchemaFactory', re: /\bSchemaFactory\s*\.\s*newInstance\s*\(/g },
39
+ { name: 'TransformerFactory', re: /\bTransformerFactory\s*\.\s*newInstance\s*\(\s*\)/g },
40
+ { name: 'XMLReaderFactory', re: /\bXMLReaderFactory\s*\.\s*createXMLReader\s*\(/g },
41
+ ];
42
+
43
+ const JAVA_SAFE_RES = [
44
+ /setFeature\s*\(\s*["']http:\/\/apache\.org\/xml\/features\/disallow-doctype-decl["']\s*,\s*true\s*\)/,
45
+ /setFeature\s*\(\s*XMLConstants\.FEATURE_SECURE_PROCESSING\s*,\s*true\s*\)/,
46
+ /setExpandEntityReferences\s*\(\s*false\s*\)/,
47
+ /XMLInputFactory\.IS_SUPPORTING_EXTERNAL_ENTITIES\s*,\s*false/,
48
+ /setFeature\s*\(\s*["']http:\/\/xml\.org\/sax\/features\/external-general-entities["']\s*,\s*false\s*\)/,
49
+ /setFeature\s*\(\s*["']http:\/\/xml\.org\/sax\/features\/external-parameter-entities["']\s*,\s*false\s*\)/,
50
+ ];
51
+
52
+ const PYTHON_VULN_PATTERNS = [
53
+ { name: 'lxml.etree.parse', re: /\blxml\.etree\.(?:parse|fromstring|XMLParser)\s*\(/g },
54
+ { name: 'lxml.etree (aliased)', re: /\b(?:from\s+lxml\s+import\s+etree\b[\s\S]{0,200}?\b)?etree\s*\.\s*(?:parse|fromstring|XMLParser)\s*\(/g },
55
+ { name: 'xml.etree.ElementTree', re: /\b(?:xml\.etree\.ElementTree|ET)\s*\.\s*(?:parse|fromstring|XMLParser)\s*\(/g },
56
+ { name: 'xml.sax', re: /\bxml\.sax\s*\.\s*(?:parse|parseString|make_parser)\s*\(/g },
57
+ { name: 'xml.dom.minidom', re: /\bxml\.dom\.minidom\s*\.\s*(?:parse|parseString)\s*\(/g },
58
+ { name: 'xml.dom.pulldom', re: /\bxml\.dom\.pulldom\s*\.\s*(?:parse|parseString)\s*\(/g },
59
+ { name: 'minidom (aliased)', re: /\bminidom\s*\.\s*(?:parse|parseString)\s*\(/g },
60
+ ];
61
+
62
+ const PYTHON_DEFUSED_RE = /(?:^|\n)\s*(?:from\s+defusedxml\b|import\s+defusedxml\b)/;
63
+ // lxml-specific: XMLParser(resolve_entities=False, no_network=True) is the
64
+ // upstream-recommended safe shape.
65
+ const PYTHON_LXML_SAFE_RE = /XMLParser\s*\([^)]*\bresolve_entities\s*=\s*False\b[^)]*\)/;
66
+
67
+ import { blankComments } from './_comment-strip.js';
68
+
69
+ function _stripLineComment(s, lang) {
70
+ if (lang === 'java') return blankComments(s);
71
+ if (lang === 'py') return blankComments(s, 'py');
72
+ return s;
73
+ }
74
+
75
+ function _lineOf(raw, idx) {
76
+ return raw.substring(0, idx).split('\n').length;
77
+ }
78
+
79
+ export function scanXXE(fp, raw) {
80
+ if (!raw || raw.length > 500_000) return [];
81
+ const findings = [];
82
+
83
+ if (/\.java$/i.test(fp)) {
84
+ const code = _stripLineComment(raw, 'java');
85
+ // If ANY known-safe configuration appears in the file, suppress all Java XXE
86
+ // findings in that file. This is intentionally generous — false negatives
87
+ // here are preferable to flagging code that's already hardened.
88
+ const fileSafe = JAVA_SAFE_RES.some(r => r.test(code));
89
+ if (fileSafe) return [];
90
+ for (const p of JAVA_VULN_PATTERNS) {
91
+ const re = new RegExp(p.re.source, p.re.flags);
92
+ let m;
93
+ while ((m = re.exec(code))) {
94
+ const line = _lineOf(raw, m.index);
95
+ findings.push({
96
+ id: `xxe:${fp}:${line}:${p.name}`,
97
+ file: fp, line,
98
+ vuln: `XXE: ${p.name} created without external-entity protections`,
99
+ severity: 'high',
100
+ cwe: 'CWE-611',
101
+ stride: 'Information Disclosure',
102
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
103
+ remediation: `Disable external entities before using the parser. For ${p.name} call setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) and setExpandEntityReferences(false), or use XMLConstants.FEATURE_SECURE_PROCESSING. Prefer DTDs to be rejected at parse time.`,
104
+ confidence: 0.85,
105
+ parser: 'XXE',
106
+ });
107
+ }
108
+ }
109
+ return findings;
110
+ }
111
+
112
+ if (/\.py$/i.test(fp)) {
113
+ const code = _stripLineComment(raw, 'py');
114
+ if (PYTHON_DEFUSED_RE.test(code)) return [];
115
+ for (const p of PYTHON_VULN_PATTERNS) {
116
+ const re = new RegExp(p.re.source, p.re.flags);
117
+ let m;
118
+ while ((m = re.exec(code))) {
119
+ // lxml-only safe shape: caller passed an XMLParser with resolve_entities=False
120
+ if (/lxml/i.test(p.name) && PYTHON_LXML_SAFE_RE.test(code)) continue;
121
+ const line = _lineOf(raw, m.index);
122
+ findings.push({
123
+ id: `xxe:${fp}:${line}:${p.name}`,
124
+ file: fp, line,
125
+ vuln: `XXE: ${p.name} parses XML without external-entity protections`,
126
+ severity: 'high',
127
+ cwe: 'CWE-611',
128
+ stride: 'Information Disclosure',
129
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
130
+ remediation: 'Use defusedxml instead: `from defusedxml import ElementTree as ET` (drop-in replacement). For lxml, pass an XMLParser with resolve_entities=False, no_network=True.',
131
+ confidence: 0.85,
132
+ parser: 'XXE',
133
+ });
134
+ }
135
+ }
136
+ return findings;
137
+ }
138
+
139
+ return [];
140
+ }
@@ -0,0 +1,200 @@
1
+ // Zip slip / archive path traversal detection. CWE-22 via archive extraction.
2
+ //
3
+ // Java vulnerable patterns:
4
+ // - ZipEntry.getName() concatenated into a File / Files.write path
5
+ // - new File(outDir, entry.getName()) without subsequent canonical-prefix check
6
+ //
7
+ // Python vulnerable patterns:
8
+ // - tarfile.open(...).extractall() pre-3.12 default behaviour (CVE-2007-4559)
9
+ // - tarfile member.name joined to output path
10
+ // - zipfile.extract(...) / extractall() without path normalization
11
+ //
12
+ // Node.js vulnerable patterns:
13
+ // - unzipper / yauzl entry.path written to disk without sanitization
14
+ // - tar package: tar.extract({cwd, ...}) with cwd inside writable area
15
+ //
16
+ // Safe shapes (suppress the finding for the file):
17
+ // Java: canonicalPath check, .normalize() then startsWith(outDir.toPath())
18
+ // Python: shutil._extract_member with explicit filter; tarfile filter='data'
19
+ // Node: sanitize-filename, path.resolve + startsWith check
20
+
21
+ const JAVA_ZIP_ENTRY_NAME_RE = /\b(?:ZipEntry|TarArchiveEntry|ArchiveEntry|entry)\s*\.\s*getName\s*\(\s*\)/g;
22
+ const JAVA_NEW_FILE_WITH_ENTRY_RE = /\bnew\s+File\s*\([^)]*\b(?:entry|zipEntry|tarEntry|archiveEntry)\s*\.\s*getName\s*\(\s*\)/g;
23
+ const JAVA_SAFE_CANONICAL_RE = /\b(?:getCanonicalPath|toRealPath|toAbsolutePath|normalize)\s*\(/;
24
+ const JAVA_SAFE_STARTSWITH_RE = /\.\s*startsWith\s*\(\s*[a-zA-Z_$][\w$.]*(?:\.\s*(?:getCanonicalPath|toPath|toAbsolutePath))?\s*\(?/;
25
+
26
+ const PY_TARFILE_EXTRACTALL_RE = /\btarfile\.[\w_]+\([^)]*\)\s*\.\s*extractall\s*\(/g;
27
+ const PY_TARFILE_EXTRACTALL_SHORT_RE = /\b(?:tf|tar|archive|t)\s*\.\s*extractall\s*\(/g;
28
+ const PY_TARFILE_FILTER_RE = /\bextractall\s*\([^)]*\bfilter\s*=\s*(?:["']data["']|tarfile\.data_filter)/;
29
+ const PY_TARFILE_IMPORT_RE = /\bimport\s+tarfile\b|\bfrom\s+tarfile\b/;
30
+ const PY_TARFILE_NAME_JOIN_RE = /\b(?:os\.path\.join|Path|os\.path\.normpath)\s*\([^)]*\b(?:member|m|entry|info)\s*\.\s*name\b/g;
31
+ const PY_ZIPFILE_EXTRACT_RE = /\b(?:zipfile\.[\w_]+\([^)]*\)|zf|zip_file|archive)\s*\.\s*extract(?:all)?\s*\(/g;
32
+ const PY_ZIPFILE_IMPORT_RE = /\bimport\s+zipfile\b|\bfrom\s+zipfile\b/;
33
+
34
+ const NODE_UNZIPPER_ENTRY_RE = /\bentry\s*\.\s*path\b[\s\S]{0,80}?\b(?:fs\.|path\.|createWriteStream|writeFile|pipe\s*\(\s*fs\.)/g;
35
+ const NODE_TAR_EXTRACT_RE = /\b(?:tar)\s*\.\s*(?:extract|x)\s*\(\s*\{[^}]*\bcwd\b/g;
36
+
37
+ import { blankComments } from './_comment-strip.js';
38
+
39
+ function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
40
+
41
+ export function scanZipSlip(fp, raw) {
42
+ if (!raw || raw.length > 500_000) return [];
43
+ const findings = [];
44
+ const seen = new Set();
45
+ const push = (f) => { if (!seen.has(f.id)) { seen.add(f.id); findings.push(f); } };
46
+
47
+ if (/\.(?:java|kt|kts|scala|groovy)$/i.test(fp)) {
48
+ const code = blankComments(raw);
49
+ // File-wide suppression: canonical path + startsWith pair present
50
+ const hasCanonical = JAVA_SAFE_CANONICAL_RE.test(code) && JAVA_SAFE_STARTSWITH_RE.test(code);
51
+ if (!hasCanonical) {
52
+ const re = new RegExp(JAVA_NEW_FILE_WITH_ENTRY_RE.source, JAVA_NEW_FILE_WITH_ENTRY_RE.flags);
53
+ let m;
54
+ while ((m = re.exec(code))) {
55
+ const line = _lineOf(raw, m.index);
56
+ push({
57
+ id: `zip-slip:${fp}:${line}:java`,
58
+ file: fp, line,
59
+ vuln: 'Zip Slip: ZipEntry.getName() joined into output path without normalization',
60
+ severity: 'high',
61
+ cwe: 'CWE-22',
62
+ stride: 'Tampering',
63
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
64
+ remediation: 'A zip entry name like `../../etc/passwd` lets an attacker write outside the extraction directory. Before any FileOutputStream / Files.write, canonicalize the joined path with `outFile.getCanonicalPath()` and verify `canonicalPath.startsWith(outDir.getCanonicalPath() + File.separator)`. Reject the entry on mismatch.',
65
+ confidence: 0.85,
66
+ parser: 'ZIP-SLIP',
67
+ });
68
+ }
69
+ }
70
+ }
71
+
72
+ if (/\.py$/i.test(fp)) {
73
+ const code = blankComments(raw, 'py');
74
+ const importsTarfile = PY_TARFILE_IMPORT_RE.test(code);
75
+ const importsZipfile = PY_ZIPFILE_IMPORT_RE.test(code);
76
+ // Per-call safe-shape check: extract the call's argument list and look for
77
+ // filter="data" / filter=tarfile.data_filter in the same call. File-level
78
+ // suppression was too aggressive — a safe function later in the file would
79
+ // hide an unsafe one earlier.
80
+ const _isFilteredExtract = (afterIdx) => {
81
+ let depth = 0;
82
+ let inS = null;
83
+ for (let i = afterIdx; i < code.length && i < afterIdx + 500; i++) {
84
+ const c = code[i];
85
+ if (inS) {
86
+ if (c === '\\') { i++; continue; }
87
+ if (c === inS) inS = null;
88
+ continue;
89
+ }
90
+ if (c === "'" || c === '"') { inS = c; continue; }
91
+ if (c === '(') depth++;
92
+ else if (c === ')') { depth--; if (depth === 0) {
93
+ const args = code.substring(afterIdx, i);
94
+ return /\bfilter\s*=\s*(?:["']data["']|tarfile\.data_filter)/.test(args);
95
+ } }
96
+ }
97
+ return false;
98
+ };
99
+ if (importsTarfile) {
100
+ const reA = new RegExp(PY_TARFILE_EXTRACTALL_RE.source, PY_TARFILE_EXTRACTALL_RE.flags);
101
+ let m;
102
+ while ((m = reA.exec(code))) {
103
+ const openParen = m.index + m[0].length - 1; // position of '('
104
+ if (_isFilteredExtract(openParen)) continue;
105
+ const line = _lineOf(raw, m.index);
106
+ push({
107
+ id: `zip-slip:${fp}:${line}:py-tarfile`,
108
+ file: fp, line,
109
+ vuln: 'Zip Slip: tarfile.extractall() without filter="data" (CVE-2007-4559)',
110
+ severity: 'high',
111
+ cwe: 'CWE-22',
112
+ stride: 'Tampering',
113
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
114
+ remediation: 'Python 3.12+: pass `filter="data"` to extractall (or set TarFile.extraction_filter). For older Python: validate every member.name before extraction — reject paths containing `..`, absolute paths, or device files. The official guidance is in PEP 706.',
115
+ confidence: 0.9,
116
+ parser: 'ZIP-SLIP',
117
+ });
118
+ }
119
+ const reB = new RegExp(PY_TARFILE_EXTRACTALL_SHORT_RE.source, PY_TARFILE_EXTRACTALL_SHORT_RE.flags);
120
+ while ((m = reB.exec(code))) {
121
+ const openParen = m.index + m[0].length - 1;
122
+ if (_isFilteredExtract(openParen)) continue;
123
+ const line = _lineOf(raw, m.index);
124
+ push({
125
+ id: `zip-slip:${fp}:${line}:py-tarfile-bare`,
126
+ file: fp, line,
127
+ vuln: 'Zip Slip: tar.extractall() without filter="data" (CVE-2007-4559)',
128
+ severity: 'high',
129
+ cwe: 'CWE-22',
130
+ stride: 'Tampering',
131
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
132
+ remediation: 'Python 3.12+: pass `filter="data"` to extractall. For older Python: validate every member.name (reject `..`, absolute paths, device files).',
133
+ confidence: 0.85,
134
+ parser: 'ZIP-SLIP',
135
+ });
136
+ }
137
+ }
138
+ if (importsTarfile) {
139
+ const reC = new RegExp(PY_TARFILE_NAME_JOIN_RE.source, PY_TARFILE_NAME_JOIN_RE.flags);
140
+ let m;
141
+ while ((m = reC.exec(code))) {
142
+ const line = _lineOf(raw, m.index);
143
+ push({
144
+ id: `zip-slip:${fp}:${line}:py-tarfile-join`,
145
+ file: fp, line,
146
+ vuln: 'Zip Slip: tar member.name joined into output path without validation',
147
+ severity: 'high',
148
+ cwe: 'CWE-22',
149
+ stride: 'Tampering',
150
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
151
+ remediation: 'Reject `member.name` if it contains `..`, starts with `/`, or is a device/symlink. Or migrate to extractall(filter="data").',
152
+ confidence: 0.85,
153
+ parser: 'ZIP-SLIP',
154
+ });
155
+ }
156
+ }
157
+ if (importsZipfile) {
158
+ const re = new RegExp(PY_ZIPFILE_EXTRACT_RE.source, PY_ZIPFILE_EXTRACT_RE.flags);
159
+ let m;
160
+ while ((m = re.exec(code))) {
161
+ const line = _lineOf(raw, m.index);
162
+ push({
163
+ id: `zip-slip:${fp}:${line}:py-zipfile`,
164
+ file: fp, line,
165
+ vuln: 'Zip Slip: zipfile.extract / extractall without path validation',
166
+ severity: 'medium',
167
+ cwe: 'CWE-22',
168
+ stride: 'Tampering',
169
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
170
+ remediation: 'Python\'s ZipFile.extract sanitizes some absolute paths but still resolves `..` segments in many CPython versions. Validate every name explicitly, or restrict the writable directory and verify the final path stays inside it.',
171
+ confidence: 0.7,
172
+ parser: 'ZIP-SLIP',
173
+ });
174
+ }
175
+ }
176
+ }
177
+
178
+ if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) {
179
+ const code = blankComments(raw);
180
+ const re = new RegExp(NODE_UNZIPPER_ENTRY_RE.source, NODE_UNZIPPER_ENTRY_RE.flags);
181
+ let m;
182
+ while ((m = re.exec(code))) {
183
+ const line = _lineOf(raw, m.index);
184
+ push({
185
+ id: `zip-slip:${fp}:${line}:node-entry`,
186
+ file: fp, line,
187
+ vuln: 'Zip Slip: archive entry.path written to filesystem without sanitization',
188
+ severity: 'high',
189
+ cwe: 'CWE-22',
190
+ stride: 'Tampering',
191
+ snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
192
+ remediation: 'Validate entry.path with `path.resolve(outDir, entry.path)` then assert `resolved.startsWith(outDir + path.sep)`. Reject entries where this is false.',
193
+ confidence: 0.7,
194
+ parser: 'ZIP-SLIP',
195
+ });
196
+ }
197
+ }
198
+
199
+ return findings;
200
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "_doc": "Known-vulnerable / EOL base images. Used by container.js to flag Dockerfile FROM lines using EOL distros.",
3
+ "_format": "image-name -> { tag-pattern: { sev, eol, message } }",
4
+
5
+ "alpine": {
6
+ "3.10": { "sev": "high", "eol": "2021-05-01", "message": "Alpine 3.10 reached end of life on 2021-05-01. No security patches since." },
7
+ "3.11": { "sev": "high", "eol": "2021-11-01", "message": "Alpine 3.11 reached EOL on 2021-11-01." },
8
+ "3.12": { "sev": "high", "eol": "2022-05-01", "message": "Alpine 3.12 reached EOL on 2022-05-01." },
9
+ "3.13": { "sev": "high", "eol": "2022-11-01", "message": "Alpine 3.13 reached EOL on 2022-11-01." },
10
+ "3.14": { "sev": "high", "eol": "2023-05-01", "message": "Alpine 3.14 reached EOL on 2023-05-01." },
11
+ "3.15": { "sev": "medium", "eol": "2023-11-01", "message": "Alpine 3.15 reached EOL on 2023-11-01." },
12
+ "3.16": { "sev": "medium", "eol": "2024-05-23", "message": "Alpine 3.16 reached EOL on 2024-05-23." },
13
+ "latest": { "sev": "low", "eol": null, "message": "alpine:latest is a floating tag — pin to a specific minor version (e.g. alpine:3.21) for reproducible builds." }
14
+ },
15
+ "debian": {
16
+ "9": { "sev": "critical", "eol": "2022-06-30", "message": "Debian 9 (Stretch) reached EOL on 2022-06-30. No security updates." },
17
+ "stretch": { "sev": "critical", "eol": "2022-06-30", "message": "Debian Stretch reached EOL on 2022-06-30. No security updates." },
18
+ "10": { "sev": "high", "eol": "2024-06-30", "message": "Debian 10 (Buster) reached EOL on 2024-06-30. No security updates." },
19
+ "buster": { "sev": "high", "eol": "2024-06-30", "message": "Debian Buster reached EOL on 2024-06-30. No security updates." },
20
+ "11": { "sev": "low", "eol": "2026-06-30", "message": "Debian 11 (Bullseye) reaches EOL on 2026-06-30. Plan migration to Bookworm." },
21
+ "bullseye": { "sev": "low", "eol": "2026-06-30", "message": "Debian Bullseye reaches EOL on 2026-06-30." },
22
+ "latest": { "sev": "low", "eol": null, "message": "debian:latest is a floating tag — pin to a release codename (e.g. debian:bookworm-slim)." }
23
+ },
24
+ "ubuntu": {
25
+ "16.04": { "sev": "critical", "eol": "2021-04-30", "message": "Ubuntu 16.04 LTS reached EOL on 2021-04-30. No security updates." },
26
+ "18.04": { "sev": "critical", "eol": "2023-05-31", "message": "Ubuntu 18.04 LTS reached EOL on 2023-05-31." },
27
+ "20.04": { "sev": "low", "eol": "2025-04-30", "message": "Ubuntu 20.04 LTS reaches standard support EOL on 2025-04-30." },
28
+ "latest": { "sev": "low", "eol": null, "message": "ubuntu:latest is a floating tag — pin to an LTS (e.g. ubuntu:22.04 or ubuntu:24.04)." }
29
+ },
30
+ "node": {
31
+ "12": { "sev": "critical", "eol": "2022-04-30", "message": "Node.js 12 reached EOL on 2022-04-30. No security patches." },
32
+ "14": { "sev": "critical", "eol": "2023-04-30", "message": "Node.js 14 reached EOL on 2023-04-30." },
33
+ "16": { "sev": "high", "eol": "2023-09-11", "message": "Node.js 16 reached EOL on 2023-09-11." },
34
+ "18": { "sev": "low", "eol": "2025-04-30", "message": "Node.js 18 enters EOL on 2025-04-30." },
35
+ "latest": { "sev": "low", "eol": null, "message": "node:latest is a floating tag — pin to an LTS (e.g. node:22-alpine)." }
36
+ },
37
+ "python": {
38
+ "2": { "sev": "critical", "eol": "2020-01-01", "message": "Python 2 reached EOL on 2020-01-01. Migrate to Python 3." },
39
+ "2.7": { "sev": "critical", "eol": "2020-01-01", "message": "Python 2.7 reached EOL on 2020-01-01." },
40
+ "3.6": { "sev": "critical", "eol": "2021-12-23", "message": "Python 3.6 reached EOL on 2021-12-23." },
41
+ "3.7": { "sev": "critical", "eol": "2023-06-27", "message": "Python 3.7 reached EOL on 2023-06-27." },
42
+ "3.8": { "sev": "high", "eol": "2024-10-07", "message": "Python 3.8 reached EOL on 2024-10-07." },
43
+ "latest": { "sev": "low", "eol": null, "message": "python:latest is a floating tag — pin to a specific minor (e.g. python:3.12-slim)." }
44
+ }
45
+ }
@@ -0,0 +1,107 @@
1
+ // 0.9.0 Feat-14: Container base image EOL detection — maps FROM lines to known-vulnerable distro versions.
2
+ //
3
+ // Two passes:
4
+ // 1. Parse `FROM <image>:<tag>` lines and check the tag against a vendored
5
+ // base-images map (alpine/debian/ubuntu/node/python). Emit a finding for
6
+ // EOL or floating tags.
7
+ // 2. Parse `RUN apt-get install` / `apk add` package lists and synthesize
8
+ // lightweight components[] entries that the SCA OSV pipeline can query.
9
+ //
10
+ // All-local: no Docker registry pulls, no shell-out to docker. Just regex.
11
+
12
+ import { createRequire } from 'node:module';
13
+ const _require = createRequire(import.meta.url);
14
+ const _BASE_IMAGES = (() => {
15
+ try {
16
+ const raw = _require('./base-images.json');
17
+ const out = {};
18
+ for (const [k, v] of Object.entries(raw)) {
19
+ if (k.startsWith('_')) continue;
20
+ out[k] = v;
21
+ }
22
+ return out;
23
+ } catch (_) {
24
+ return null;
25
+ }
26
+ })();
27
+
28
+ const _DOCKERFILE_RE = /(?:^|\/)(?:[Dd]ockerfile|[^/]+\.dockerfile)$/i;
29
+
30
+ // FROM <image>[:<tag>] [AS <stage>]
31
+ const _FROM_RE = /^\s*FROM\s+(?:--platform=\S+\s+)?([\w./-]+?)(?::([\w.\-]+))?(?:@sha256:[a-f0-9]{64})?(?:\s+AS\s+\S+)?\s*$/im;
32
+
33
+ // FROM <image>:<tag> covering all FROM lines in the file
34
+ const _ALL_FROM_RE = /^\s*FROM\s+(?:--platform=\S+\s+)?([\w./-]+?)(?::([\w.\-]+))?(?:@sha256:[a-f0-9]{64})?(?:\s+AS\s+\S+)?\s*$/img;
35
+
36
+ // `apt-get install -y pkg pkg pkg` / `apk add pkg pkg`
37
+ const _APT_INSTALL_RE = /\bapt(?:-get)?\s+install\b[^\n]*?(?:--?[\w-]+\s+)*((?:[a-z0-9][\w.+-]*(?:=[\w.+:-]+)?\s*)+)/gi;
38
+ const _APK_ADD_RE = /\bapk\s+(?:--no-cache\s+)?(?:--update\s+)?add\b[^\n]*?(?:--?[\w-]+\s+)*((?:[a-z0-9][\w.+-]*(?:=[\w.+:-]+)?\s*)+)/gi;
39
+
40
+ function _scoreTag(image, tag) {
41
+ if (!_BASE_IMAGES) return null;
42
+ const m = _BASE_IMAGES[image];
43
+ if (!m) return null;
44
+ // Direct tag match
45
+ if (m[tag]) return { ...m[tag], image, tag };
46
+ // Major-only match: tag '20.04-slim' falls back to '20.04'
47
+ for (const k of Object.keys(m)) {
48
+ if (tag && tag.startsWith(k + '.')) return { ...m[k], image, tag };
49
+ if (tag && tag.startsWith(k + '-')) return { ...m[k], image, tag };
50
+ if (tag === k) return { ...m[k], image, tag };
51
+ }
52
+ // Tag missing entirely (e.g. "FROM alpine") → treat as 'latest'
53
+ if (!tag && m.latest) return { ...m.latest, image, tag: 'latest' };
54
+ return null;
55
+ }
56
+
57
+ export function scanContainer(fp, raw) {
58
+ if (!_DOCKERFILE_RE.test(fp.replace(/\\/g, '/'))) return [];
59
+ if (!raw || raw.length > 200_000) return [];
60
+ const findings = [];
61
+ const lines = raw.split('\n');
62
+ let m;
63
+
64
+ // Pass 1: FROM lines
65
+ _ALL_FROM_RE.lastIndex = 0;
66
+ while ((m = _ALL_FROM_RE.exec(raw))) {
67
+ const image = m[1].split('/').pop(); // strip registry / namespace prefixes
68
+ const tag = m[2] || '';
69
+ const line = raw.substring(0, m.index).split('\n').length;
70
+ const score = _scoreTag(image, tag);
71
+ if (!score) continue;
72
+ findings.push({
73
+ id: `container-base:${fp}:${line}:${image}:${tag || 'latest'}`,
74
+ kind: 'container', severity: score.sev,
75
+ vuln: `Container base image: ${image}:${tag || 'latest'} ${score.eol ? '(EOL)' : '(floating tag)'}`,
76
+ cwe: score.eol ? 'CWE-1104' : 'CWE-1357',
77
+ stride: 'Tampering',
78
+ file: fp, line, snippet: (lines[line - 1] || '').trim(),
79
+ fix: score.message,
80
+ });
81
+ }
82
+
83
+ // Pass 2: apt/apk packages — surface as components hint for the SCA pipeline.
84
+ // We do NOT query OSV here (the engine's SCA pass owns that). Just collect names.
85
+ const packages = [];
86
+ _APT_INSTALL_RE.lastIndex = 0;
87
+ while ((m = _APT_INSTALL_RE.exec(raw))) {
88
+ for (const tok of m[1].split(/\s+/)) {
89
+ const t = tok.trim();
90
+ if (!t || t.startsWith('-')) continue;
91
+ const [name, ver] = t.split('=', 2);
92
+ if (/^[a-z0-9][\w.+-]*$/.test(name)) packages.push({ ecosystem: 'debian', name, version: ver || '' });
93
+ }
94
+ }
95
+ _APK_ADD_RE.lastIndex = 0;
96
+ while ((m = _APK_ADD_RE.exec(raw))) {
97
+ for (const tok of m[1].split(/\s+/)) {
98
+ const t = tok.trim();
99
+ if (!t || t.startsWith('-')) continue;
100
+ const [name, ver] = t.split('=', 2);
101
+ if (/^[a-z0-9][\w.+-]*$/.test(name)) packages.push({ ecosystem: 'alpine', name, version: ver || '' });
102
+ }
103
+ }
104
+ // Stash packages on the first finding so the engine can consume them downstream
105
+ if (packages.length && findings.length) findings[0]._containerPackages = packages;
106
+ return findings;
107
+ }