@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.
- package/CHANGELOG.md +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- 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
|
+
}
|
package/src/sast/xxe.js
ADDED
|
@@ -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
|
+
}
|