@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,242 @@
|
|
|
1
|
+
// Deep DeFi / AMM Solidity audit.
|
|
2
|
+
//
|
|
3
|
+
// Extends scanner/src/sast/solidity.js with the canonical AMM / vault /
|
|
4
|
+
// swap-flow bugs. Targets vulnerabilities that don't show up in generic
|
|
5
|
+
// reentrancy or tx.origin checks:
|
|
6
|
+
//
|
|
7
|
+
// 1. Donation / inflation attack — share math using token.balanceOf(address(this))
|
|
8
|
+
// 2. Missing slippage / deadline on swap — caller-supplied minOut absent
|
|
9
|
+
// 3. Spot-price oracle — pool.slot0 / getReserves used directly, no TWAP
|
|
10
|
+
// 4. CEI violation — token.transfer before state update
|
|
11
|
+
// 5. Hand-rolled reentrancy guard — bool locked instead of OZ ReentrancyGuard
|
|
12
|
+
// 6. Ownable vs Ownable2Step — single-step ownership transfer
|
|
13
|
+
// 7. Missing safeTransfer — token.transfer instead of SafeERC20.safeTransfer
|
|
14
|
+
// 8. Naive mulDiv — a * b / c with reserve-sized numbers
|
|
15
|
+
// 9. Missing nonReentrant on payable withdraw / claim
|
|
16
|
+
// 10. Unchecked external call — (bool ok, ) = addr.call(...) without ok check
|
|
17
|
+
//
|
|
18
|
+
// Only fires on .sol files.
|
|
19
|
+
|
|
20
|
+
const _SOL_RE = /\.sol$/i;
|
|
21
|
+
|
|
22
|
+
function _line(raw, idx) {
|
|
23
|
+
return raw.slice(0, idx).split('\n').length;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _findFunctions(raw) {
|
|
27
|
+
// Match: function fnName(args) ... { body }
|
|
28
|
+
// Naive — single brace-counted body up to 2000 chars per fn.
|
|
29
|
+
const fns = [];
|
|
30
|
+
const re = /\bfunction\s+(\w+)\s*\(([^)]*)\)\s*([^{]*)\{/g;
|
|
31
|
+
let m;
|
|
32
|
+
while ((m = re.exec(raw))) {
|
|
33
|
+
const head = m[0];
|
|
34
|
+
const name = m[1];
|
|
35
|
+
const params = m[2];
|
|
36
|
+
const modifiers = m[3] || '';
|
|
37
|
+
const braceIdx = m.index + head.length - 1;
|
|
38
|
+
let depth = 1, end = braceIdx + 1;
|
|
39
|
+
for (let i = braceIdx + 1; i < Math.min(braceIdx + 4000, raw.length); i++) {
|
|
40
|
+
if (raw[i] === '{') depth++;
|
|
41
|
+
else if (raw[i] === '}') {
|
|
42
|
+
depth--;
|
|
43
|
+
if (depth === 0) { end = i; break; }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
fns.push({ name, params, modifiers, body: raw.slice(braceIdx, end + 1), startLine: _line(raw, m.index) });
|
|
47
|
+
}
|
|
48
|
+
return fns;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function scanDefiDeep(file, raw) {
|
|
52
|
+
if (!file || !raw || typeof raw !== 'string') return [];
|
|
53
|
+
if (!_SOL_RE.test(file)) return [];
|
|
54
|
+
if (raw.length > 300_000) return [];
|
|
55
|
+
|
|
56
|
+
const findings = [];
|
|
57
|
+
const fns = _findFunctions(raw);
|
|
58
|
+
|
|
59
|
+
for (const fn of fns) {
|
|
60
|
+
// ── 1. Donation / inflation attack ────────────────────────────────────
|
|
61
|
+
// Share math using raw balanceOf(address(this)) without internal accounting.
|
|
62
|
+
if (/balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)/.test(fn.body)) {
|
|
63
|
+
// Only flag when used as a divisor (share math) and there's no
|
|
64
|
+
// _totalAssets / _reserves / internal accounting var nearby.
|
|
65
|
+
if (/\(.*?balanceOf\s*\(\s*address\s*\(\s*this\s*\)\s*\)\s*\)/.test(fn.body) ||
|
|
66
|
+
/\*\s*totalShares\s*\)\s*\/\s*[A-Za-z_]/.test(fn.body)) {
|
|
67
|
+
const idx = fn.body.search(/balanceOf\s*\(\s*address\s*\(\s*this\s*\)/);
|
|
68
|
+
findings.push({
|
|
69
|
+
id: `defi:donation-inflation:${file}:${fn.startLine}`,
|
|
70
|
+
file,
|
|
71
|
+
line: fn.startLine + (idx >= 0 ? fn.body.slice(0, idx).split('\n').length - 1 : 0),
|
|
72
|
+
vuln: `${fn.name}() uses balanceOf(address(this)) directly in share / reserve math — donation/inflation attack vector`,
|
|
73
|
+
severity: 'high',
|
|
74
|
+
family: 'defi-donation-inflation',
|
|
75
|
+
cwe: 'CWE-682',
|
|
76
|
+
confidence: 0.7,
|
|
77
|
+
description: 'Anyone can send tokens directly to the contract (outside the deposit path) to manipulate the share-math denominator. First-depositor inflation attack and donation attacks on vaults all flow from this pattern.',
|
|
78
|
+
remediation: 'Track total assets in a state variable updated by deposit/withdraw. Measure tokens received via balBefore/balAfter pair around transferFrom rather than reading balanceOf at the end.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 2. Missing slippage / deadline on swap ────────────────────────────
|
|
84
|
+
if (/\bswap\b/i.test(fn.name) || /\bswap[A-Z]/.test(fn.name)) {
|
|
85
|
+
const hasMin = /\b(?:amountOutMin|minAmountOut|minOut|sqrtPriceLimitX96)\b/.test(fn.params);
|
|
86
|
+
const hasDeadline = /\b(?:deadline|expiry|until)\b/i.test(fn.params);
|
|
87
|
+
if (!hasMin) {
|
|
88
|
+
findings.push({
|
|
89
|
+
id: `defi:no-slippage-min:${file}:${fn.startLine}`,
|
|
90
|
+
file, line: fn.startLine,
|
|
91
|
+
vuln: `swap function ${fn.name}() does not accept amountOutMin parameter — no slippage protection`,
|
|
92
|
+
severity: 'high',
|
|
93
|
+
family: 'defi-no-slippage',
|
|
94
|
+
cwe: 'CWE-682',
|
|
95
|
+
confidence: 0.8,
|
|
96
|
+
description: 'Without a caller-supplied minimum output, sandwich-attackers can manipulate pool reserves to drain the swap. The router pattern requires the caller to compute and supply a slippage tolerance.',
|
|
97
|
+
remediation: 'Add `uint256 amountOutMin` to the signature and `require(amountOut >= amountOutMin, "Slippage exceeded")` before the external transfer.',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
if (!hasDeadline) {
|
|
101
|
+
findings.push({
|
|
102
|
+
id: `defi:no-deadline:${file}:${fn.startLine}`,
|
|
103
|
+
file, line: fn.startLine,
|
|
104
|
+
vuln: `swap function ${fn.name}() does not accept deadline parameter — stale transactions accepted`,
|
|
105
|
+
severity: 'medium',
|
|
106
|
+
family: 'defi-no-deadline',
|
|
107
|
+
cwe: 'CWE-672',
|
|
108
|
+
confidence: 0.8,
|
|
109
|
+
description: 'Without a deadline, a tx sitting in the mempool can execute hours later at a much worse price.',
|
|
110
|
+
remediation: 'Add `uint256 deadline` parameter and `require(block.timestamp <= deadline, "Expired")` at the top of the function.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 9. Missing nonReentrant on payable / withdraw functions ───────────
|
|
116
|
+
if (/payable\b/.test(fn.modifiers) || /^(?:withdraw|claim|exit|harvest|redeem)/i.test(fn.name)) {
|
|
117
|
+
if (!/\bnonReentrant\b/.test(fn.modifiers)) {
|
|
118
|
+
if (/\b(?:\.call\s*\{|\.transfer\s*\(|\.send\s*\(|safeTransfer)/.test(fn.body)) {
|
|
119
|
+
findings.push({
|
|
120
|
+
id: `defi:no-reentrancy-guard:${file}:${fn.startLine}`,
|
|
121
|
+
file, line: fn.startLine,
|
|
122
|
+
vuln: `${fn.name}() performs external transfer but is not nonReentrant`,
|
|
123
|
+
severity: 'high',
|
|
124
|
+
family: 'defi-missing-reentrancy-guard',
|
|
125
|
+
cwe: 'CWE-841',
|
|
126
|
+
confidence: 0.7,
|
|
127
|
+
description: 'Function moves funds via external call/transfer but does not carry the nonReentrant modifier. Cross-function reentrancy still drains the contract.',
|
|
128
|
+
remediation: 'Inherit OpenZeppelin\'s ReentrancyGuard and add the nonReentrant modifier. Apply CEI ordering: state changes BEFORE external calls.',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── 3. Spot-price oracle ──────────────────────────────────────────────────
|
|
136
|
+
// Reads getReserves() or slot0() as price without an observe() (TWAP) call.
|
|
137
|
+
if (/\b(?:getReserves|slot0)\s*\(/.test(raw) && !/\bobserve\s*\(/.test(raw)) {
|
|
138
|
+
const m = /\b(?:getReserves|slot0)\s*\(/.exec(raw);
|
|
139
|
+
findings.push({
|
|
140
|
+
id: `defi:spot-price-oracle:${file}:${_line(raw, m.index)}`,
|
|
141
|
+
file, line: _line(raw, m.index),
|
|
142
|
+
vuln: 'Spot-price oracle — reads pool reserves / slot0 without TWAP',
|
|
143
|
+
severity: 'high',
|
|
144
|
+
family: 'defi-spot-price-oracle',
|
|
145
|
+
cwe: 'CWE-1023',
|
|
146
|
+
confidence: 0.75,
|
|
147
|
+
description: 'Spot prices from getReserves() / slot0() are flash-loan manipulable in a single block. Any pricing logic that derives from these without a time-weighted average is exploitable.',
|
|
148
|
+
remediation: 'Use Uniswap V3 pool.observe(secondsAgos) to compute a TWAP over ≥30 minutes, or pull from a hardened oracle like Chainlink.',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 5. Hand-rolled reentrancy guard ───────────────────────────────────────
|
|
153
|
+
if (/\bbool\s+(?:locked|entered|_status|reentrancy)\s*[;=]/.test(raw) &&
|
|
154
|
+
!/\bReentrancyGuard\b/.test(raw)) {
|
|
155
|
+
const m = /\bbool\s+(?:locked|entered|_status|reentrancy)\s*[;=]/.exec(raw);
|
|
156
|
+
findings.push({
|
|
157
|
+
id: `defi:hand-rolled-reentrancy:${file}:${_line(raw, m.index)}`,
|
|
158
|
+
file, line: _line(raw, m.index),
|
|
159
|
+
vuln: 'Hand-rolled reentrancy guard instead of OpenZeppelin ReentrancyGuard',
|
|
160
|
+
severity: 'medium',
|
|
161
|
+
family: 'defi-hand-rolled-guard',
|
|
162
|
+
cwe: 'CWE-682',
|
|
163
|
+
confidence: 0.8,
|
|
164
|
+
description: 'A custom `bool locked` reentrancy guard is easy to write incorrectly (missing reset on revert, wrong scope, missing modifier on a function). The OpenZeppelin ReentrancyGuard is audited and idiomatic.',
|
|
165
|
+
remediation: 'Replace with: `import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";` and inherit + use the `nonReentrant` modifier.',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── 6. Ownable vs Ownable2Step ────────────────────────────────────────────
|
|
170
|
+
if (/\bis\s+Ownable\b(?!\s*2Step)/.test(raw)) {
|
|
171
|
+
const m = /\bis\s+Ownable\b/.exec(raw);
|
|
172
|
+
findings.push({
|
|
173
|
+
id: `defi:ownable-single-step:${file}:${_line(raw, m.index)}`,
|
|
174
|
+
file, line: _line(raw, m.index),
|
|
175
|
+
vuln: 'Contract uses single-step Ownable — typo-to-zero ownership transfer risk',
|
|
176
|
+
severity: 'medium',
|
|
177
|
+
family: 'defi-ownable-single-step',
|
|
178
|
+
cwe: 'CWE-269',
|
|
179
|
+
confidence: 0.85,
|
|
180
|
+
description: 'Ownable.transferOwnership(newOwner) takes effect immediately. Typing the wrong address (or signing a malicious tx) permanently bricks the contract.',
|
|
181
|
+
remediation: 'Use Ownable2Step (also from OpenZeppelin) — the new owner must call acceptOwnership() to take effect.',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── 7. Missing safeTransfer on ERC-20 transfer ────────────────────────────
|
|
186
|
+
// token.transfer / token.transferFrom without SafeERC20.
|
|
187
|
+
if (/\bIERC20\b/.test(raw) &&
|
|
188
|
+
/\b\w+\.(?:transfer|transferFrom)\s*\(/.test(raw) &&
|
|
189
|
+
!/\bSafeERC20\b/.test(raw)) {
|
|
190
|
+
const m = /\b\w+\.(?:transfer|transferFrom)\s*\(/.exec(raw);
|
|
191
|
+
findings.push({
|
|
192
|
+
id: `defi:no-safe-transfer:${file}:${_line(raw, m.index)}`,
|
|
193
|
+
file, line: _line(raw, m.index),
|
|
194
|
+
vuln: 'ERC-20 token.transfer / transferFrom used without SafeERC20',
|
|
195
|
+
severity: 'medium',
|
|
196
|
+
family: 'defi-no-safe-transfer',
|
|
197
|
+
cwe: 'CWE-252',
|
|
198
|
+
confidence: 0.7,
|
|
199
|
+
description: 'Some tokens (USDT, BNB) do not return a bool; some return false instead of reverting. Direct .transfer / .transferFrom either reverts unexpectedly or silently succeeds on failure.',
|
|
200
|
+
remediation: 'Use OpenZeppelin SafeERC20: `using SafeERC20 for IERC20;` then `token.safeTransfer(...)` and `token.safeTransferFrom(...)`.',
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── 10. Unchecked external call ───────────────────────────────────────────
|
|
205
|
+
// (bool ok, ) = addr.call{value: ...}(...); without an `ok` check.
|
|
206
|
+
for (const m of raw.matchAll(/\(\s*bool\s+(\w+)\s*,?[^)]*\)\s*=\s*[^;]*?\.call\s*(?:\{[^}]*\})?\s*\([^)]*\)\s*;/g)) {
|
|
207
|
+
const okName = m[1];
|
|
208
|
+
const restAfter = raw.slice(m.index + m[0].length, m.index + m[0].length + 200);
|
|
209
|
+
if (new RegExp(`\\brequire\\s*\\(\\s*${okName}\\b|\\bif\\s*\\(\\s*!?${okName}\\b`).test(restAfter)) continue;
|
|
210
|
+
findings.push({
|
|
211
|
+
id: `defi:unchecked-call:${file}:${_line(raw, m.index)}`,
|
|
212
|
+
file, line: _line(raw, m.index),
|
|
213
|
+
vuln: `External low-level call captures success flag (${okName}) but does not check it`,
|
|
214
|
+
severity: 'medium',
|
|
215
|
+
family: 'defi-unchecked-call',
|
|
216
|
+
cwe: 'CWE-252',
|
|
217
|
+
confidence: 0.85,
|
|
218
|
+
description: 'A low-level .call returns (bool, bytes). If you ignore the bool, the function appears to succeed even when the callee reverts — leading to silent state corruption.',
|
|
219
|
+
remediation: `Add require(${okName}, "call failed") immediately after the call, or revert with a meaningful error.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── 4. CEI violation — transfer before state update ───────────────────────
|
|
224
|
+
// Heuristic: find `external_call(...)` then `state_var -= ` or `state_var = `
|
|
225
|
+
// on the same path. Conservative: only flag when the lines are obviously
|
|
226
|
+
// in sequence and the state var is a mapping access.
|
|
227
|
+
for (const m of raw.matchAll(/(\w+)\.transfer\s*\([^)]*\)\s*;\s*\n[^\n}]*\b(\w+)\s*\[\s*(?:msg\.sender|tx\.origin)\s*\]\s*-=/g)) {
|
|
228
|
+
findings.push({
|
|
229
|
+
id: `defi:cei-violation:${file}:${_line(raw, m.index)}`,
|
|
230
|
+
file, line: _line(raw, m.index),
|
|
231
|
+
vuln: 'CEI violation — token.transfer() before state update on caller balance',
|
|
232
|
+
severity: 'high',
|
|
233
|
+
family: 'defi-cei-violation',
|
|
234
|
+
cwe: 'CWE-841',
|
|
235
|
+
confidence: 0.85,
|
|
236
|
+
description: 'External transfer happens before the caller\'s balance is decremented. A reentrant caller can re-enter through a token-callback (ERC-777, ERC-1363) and drain the balance.',
|
|
237
|
+
remediation: 'Apply CEI ordering: update internal accounting (state) BEFORE any external call (interaction). Use nonReentrant as defense-in-depth.',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return findings;
|
|
242
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { blankComments } from './_comment-strip.js';
|
|
2
|
+
// Deserialization gadget-chain awareness.
|
|
3
|
+
//
|
|
4
|
+
// Pure dataflow already flags unsafe deserialization sinks (Java
|
|
5
|
+
// ObjectInputStream.readObject, Python pickle.loads, Ruby Marshal.load, PHP
|
|
6
|
+
// unserialize, .NET BinaryFormatter). This module sits ON TOP of those:
|
|
7
|
+
// when one of those sinks is found *and* the classpath / dependency tree
|
|
8
|
+
// shows known gadget libraries (CommonsCollections, Spring AOP, Snakeyaml,
|
|
9
|
+
// json-io, etc.), we emit a separate `Deserialization-Gadget-Chain-Present`
|
|
10
|
+
// finding with severity bumped to critical.
|
|
11
|
+
//
|
|
12
|
+
// We do NOT emit on its own — we look for the dep names; the original
|
|
13
|
+
// deserialization finding still fires from its module. This module catches
|
|
14
|
+
// "yes the library is present, so this is exploitable, not theoretical."
|
|
15
|
+
|
|
16
|
+
const KNOWN_GADGETS_JAVA = [
|
|
17
|
+
'commons-collections', 'commons-beanutils', 'commons-fileupload',
|
|
18
|
+
'spring-aop', 'spring-core', 'spring-beans',
|
|
19
|
+
'snakeyaml', 'jackson-databind', 'json-io',
|
|
20
|
+
'xstream', 'castor', 'hibernate-core',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const KNOWN_GADGETS_PY = [
|
|
24
|
+
'pyyaml', 'jsonpickle', 'dill', 'shelve',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const KNOWN_GADGETS_RB = [
|
|
28
|
+
'oj', 'activesupport', // both have known Marshal gadgets
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const UNSAFE_SINK_RE = {
|
|
32
|
+
java: /\b(?:ObjectInputStream|XMLDecoder|SerializationUtils\s*\.\s*deserialize|new\s+Yaml\s*\(\s*\))/,
|
|
33
|
+
py: /\b(?:pickle|cPickle|dill|marshal)\s*\.\s*loads?|\byaml\s*\.\s*load\s*\(/,
|
|
34
|
+
rb: /\bMarshal\s*\.\s*load|\bYAML\s*\.\s*load\s*\(|\bOj\s*\.\s*load/,
|
|
35
|
+
php: /\bunserialize\s*\(/,
|
|
36
|
+
cs: /\bBinaryFormatter|NetDataContractSerializer|SoapFormatter/,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
40
|
+
|
|
41
|
+
function detectGadgets(allFileContents) {
|
|
42
|
+
const present = new Set();
|
|
43
|
+
if (!allFileContents) return present;
|
|
44
|
+
// Look in manifests
|
|
45
|
+
const mans = ['package.json', 'pom.xml', 'build.gradle', 'build.gradle.kts',
|
|
46
|
+
'requirements.txt', 'pyproject.toml', 'Pipfile.lock',
|
|
47
|
+
'Gemfile', 'Gemfile.lock', 'composer.json',
|
|
48
|
+
'packages.config'];
|
|
49
|
+
for (const [fp, c] of Object.entries(allFileContents)) {
|
|
50
|
+
const base = fp.split('/').pop();
|
|
51
|
+
if (!mans.includes(base)) continue;
|
|
52
|
+
if (!c || typeof c !== 'string') continue;
|
|
53
|
+
for (const g of KNOWN_GADGETS_JAVA) if (c.includes(g)) present.add(g);
|
|
54
|
+
for (const g of KNOWN_GADGETS_PY) if (c.toLowerCase().includes(g)) present.add(g);
|
|
55
|
+
for (const g of KNOWN_GADGETS_RB) if (c.toLowerCase().includes(g)) present.add(g);
|
|
56
|
+
}
|
|
57
|
+
return present;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function scanDeserializationGadgets(fp, raw, ctx = {}) {
|
|
61
|
+
if (!raw || raw.length > 500_000) return [];
|
|
62
|
+
let lang;
|
|
63
|
+
if (/\.java$/i.test(fp)) lang = 'java';
|
|
64
|
+
else if (/\.py$/i.test(fp)) lang = 'py';
|
|
65
|
+
else if (/\.rb$/i.test(fp)) lang = 'rb';
|
|
66
|
+
else if (/\.php$/i.test(fp)) lang = 'php';
|
|
67
|
+
else if (/\.cs$/i.test(fp)) lang = 'cs';
|
|
68
|
+
else return [];
|
|
69
|
+
|
|
70
|
+
const code = blankComments(raw, lang === 'py' ? 'py' : undefined);
|
|
71
|
+
if (!UNSAFE_SINK_RE[lang].test(code)) return [];
|
|
72
|
+
|
|
73
|
+
const gadgets = ctx.gadgets instanceof Set ? ctx.gadgets : detectGadgets(ctx.allFileContents || {});
|
|
74
|
+
if (!gadgets.size) return [];
|
|
75
|
+
|
|
76
|
+
// Gate: a known gadget library + an unsafe sink is necessary but not
|
|
77
|
+
// sufficient. We require an EXPLICIT request-source pattern IN THE SAME
|
|
78
|
+
// FILE — request.getParameter, request.getInputStream, etc. — so the
|
|
79
|
+
// exploitable chain is locally evidenced. Files that take byte[] / Stream
|
|
80
|
+
// method parameters (e.g. Juliet's cross-class flow variants 71-84) are
|
|
81
|
+
// NOT enough; many real Java apps pass deser arguments around without
|
|
82
|
+
// those args originating from an attacker.
|
|
83
|
+
const taintSource = /\b(?:request|req)\s*\.\s*(?:getParameter|getHeader|getCookies|getInputStream|getReader|getRequestURI|getQueryString)\b|\bnew\s+(?:Server)?Socket\s*\(/;
|
|
84
|
+
if (!taintSource.test(code)) return [];
|
|
85
|
+
|
|
86
|
+
// Emit one informational finding per unsafe-sink-per-file when gadgets are present.
|
|
87
|
+
const findings = [];
|
|
88
|
+
const re = new RegExp(UNSAFE_SINK_RE[lang].source, (UNSAFE_SINK_RE[lang].flags || '') + 'g');
|
|
89
|
+
let m;
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
while ((m = re.exec(code))) {
|
|
92
|
+
const line = lineOf(raw, m.index);
|
|
93
|
+
const id = `deserialize-gadgets:${fp}:${line}`;
|
|
94
|
+
if (seen.has(id)) continue;
|
|
95
|
+
seen.add(id);
|
|
96
|
+
findings.push({
|
|
97
|
+
id,
|
|
98
|
+
file: fp, line,
|
|
99
|
+
vuln: 'Deserialization Gadget-Chain Library Present',
|
|
100
|
+
severity: 'critical',
|
|
101
|
+
cwe: 'CWE-502',
|
|
102
|
+
stride: 'Tampering',
|
|
103
|
+
snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
|
|
104
|
+
remediation: `An unsafe deserialization sink is present alongside known gadget libraries in the dependency tree (${[...gadgets].slice(0, 4).join(', ')}). This converts the deserialization issue from "theoretical" to "exploitable today" — known exploit payloads exist for these libraries on Maven Central / PyPI. Either drop the deserialization (use a safe format like JSON Schema, protobuf, or msgpack with a known schema) or upgrade past the patched version of the gadget library.`,
|
|
105
|
+
parser: 'DESERIALIZE-GADGETS',
|
|
106
|
+
confidence: 0.90,
|
|
107
|
+
gadgets: [...gadgets],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { detectGadgets as _detectGadgets };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Django framework hardening audit.
|
|
2
|
+
//
|
|
3
|
+
// Targets Django settings.py production bombs the generic python-sinks
|
|
4
|
+
// rule doesn't catch. Each rule fires only on files that look like Django
|
|
5
|
+
// settings — settings/production.py, settings/base.py, settings.py at repo
|
|
6
|
+
// root, conf.py with DJANGO_SETTINGS — to keep precision high.
|
|
7
|
+
//
|
|
8
|
+
// Coverage:
|
|
9
|
+
// 1. DEBUG = True in production-ish settings (leaks stack traces + /debug/)
|
|
10
|
+
// 2. ALLOWED_HOSTS = ['*'] (host-header attacks)
|
|
11
|
+
// 3. SECRET_KEY literal in source (rotate-required)
|
|
12
|
+
// 4. Missing security cookies/headers when SECURITY_MIDDLEWARE present
|
|
13
|
+
// 5. SECURE_SSL_REDIRECT = False / not set
|
|
14
|
+
// 6. SECURE_HSTS_SECONDS not set or 0
|
|
15
|
+
// 7. SESSION_COOKIE_SECURE = False, CSRF_COOKIE_SECURE = False
|
|
16
|
+
// 8. AUTH_PASSWORD_VALIDATORS missing / empty
|
|
17
|
+
// 9. @csrf_exempt on a non-test, non-webhook view
|
|
18
|
+
// 10. X_FRAME_OPTIONS = 'ALLOW' / missing
|
|
19
|
+
// 11. SESSION_COOKIE_HTTPONLY = False
|
|
20
|
+
// 12. CORS_ALLOW_ALL_ORIGINS = True (django-cors-headers)
|
|
21
|
+
|
|
22
|
+
const _DJANGO_SETTINGS_FILE_RE = /(?:^|[\\/])(?:settings(?:\.py|[\\/](?:base|production|prod|dev|local|common|staging)\.py)|conf\.py)$/i;
|
|
23
|
+
const _PYTHON_VIEW_FILE_RE = /(?:^|[\\/])(?:views|api|urls|admin)\.py$/i;
|
|
24
|
+
const _MANAGE_PY_RE = /(?:^|[\\/])manage\.py$/i;
|
|
25
|
+
|
|
26
|
+
function _line(raw, idx) {
|
|
27
|
+
return raw.slice(0, idx).split('\n').length;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _isDjangoSettings(file, raw) {
|
|
31
|
+
if (_MANAGE_PY_RE.test(file)) return false;
|
|
32
|
+
if (!_DJANGO_SETTINGS_FILE_RE.test(file)) {
|
|
33
|
+
// Fall-back: any python file that imports django-style ROOT_URLCONF + INSTALLED_APPS together.
|
|
34
|
+
if (!/\.py$/i.test(file)) return false;
|
|
35
|
+
if (!/\bINSTALLED_APPS\b/.test(raw) || !/\bROOT_URLCONF\b/.test(raw)) return false;
|
|
36
|
+
}
|
|
37
|
+
// Final sanity — must contain django markers.
|
|
38
|
+
return /\b(?:INSTALLED_APPS|MIDDLEWARE|ROOT_URLCONF|DATABASES|WSGI_APPLICATION|django\.contrib)\b/.test(raw);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _isProductionish(file) {
|
|
42
|
+
return /(?:prod(?:uction)?|live|staging|common|base)/i.test(file);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function scanDjangoHardening(file, raw) {
|
|
46
|
+
if (!file || !raw || typeof raw !== 'string') return [];
|
|
47
|
+
if (raw.length > 200_000) return [];
|
|
48
|
+
|
|
49
|
+
// Quick check on path/content; expensive checks below only run if this is a settings file.
|
|
50
|
+
const isSettings = _isDjangoSettings(file, raw);
|
|
51
|
+
const isView = _PYTHON_VIEW_FILE_RE.test(file) && /\bfrom django\b|\bdjango\./.test(raw);
|
|
52
|
+
if (!isSettings && !isView) return [];
|
|
53
|
+
|
|
54
|
+
const findings = [];
|
|
55
|
+
const prod = _isProductionish(file);
|
|
56
|
+
|
|
57
|
+
// ── View-file checks ────────────────────────────────────────────────────
|
|
58
|
+
if (isView) {
|
|
59
|
+
// @csrf_exempt without a stripe/github webhook signature check nearby.
|
|
60
|
+
for (const m of raw.matchAll(/^\s*@csrf_exempt\b/gm)) {
|
|
61
|
+
const line = _line(raw, m.index);
|
|
62
|
+
// Look ±15 lines for a webhook-signature comment or HMAC verify call.
|
|
63
|
+
const lines = raw.split('\n');
|
|
64
|
+
const ctx = lines.slice(Math.max(0, line - 5), Math.min(lines.length, line + 15)).join('\n');
|
|
65
|
+
if (/(?:stripe|github|hmac|signature|svix|x-hub-signature|verify_webhook)/i.test(ctx)) continue;
|
|
66
|
+
findings.push({
|
|
67
|
+
id: `django:csrf-exempt:${file}:${line}`,
|
|
68
|
+
file, line,
|
|
69
|
+
vuln: 'Django @csrf_exempt on a non-webhook view',
|
|
70
|
+
severity: 'high',
|
|
71
|
+
family: 'django-csrf-exempt',
|
|
72
|
+
cwe: 'CWE-352',
|
|
73
|
+
confidence: 0.7,
|
|
74
|
+
description: 'View is decorated with @csrf_exempt but no webhook-signature verification is visible within 15 lines. State-changing endpoints without CSRF protection are exploitable from any cross-origin page that authenticates a Django user.',
|
|
75
|
+
remediation: 'Remove @csrf_exempt and let CsrfViewMiddleware run; OR if this truly is a webhook, add HMAC signature verification (e.g., stripe.webhooks.construct_event).',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return findings;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Settings-file checks ────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
// DEBUG = True in production-ish settings.
|
|
84
|
+
for (const m of raw.matchAll(/^\s*DEBUG\s*=\s*True\b/gm)) {
|
|
85
|
+
findings.push({
|
|
86
|
+
id: `django:debug-true:${file}:${_line(raw, m.index)}`,
|
|
87
|
+
file, line: _line(raw, m.index),
|
|
88
|
+
vuln: 'Django DEBUG=True in settings',
|
|
89
|
+
severity: prod ? 'critical' : 'high',
|
|
90
|
+
family: 'django-debug-enabled',
|
|
91
|
+
cwe: 'CWE-489',
|
|
92
|
+
confidence: 0.95,
|
|
93
|
+
description: 'DEBUG=True exposes the Django debug page on any exception — full stack trace, environment variables, request/response, source-code snippets. Catastrophic in production.',
|
|
94
|
+
remediation: 'Set DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true" and explicitly set DEBUG=false in your production env.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ALLOWED_HOSTS = ['*']
|
|
99
|
+
for (const m of raw.matchAll(/^\s*ALLOWED_HOSTS\s*=\s*\[\s*['"]\*['"]/gm)) {
|
|
100
|
+
findings.push({
|
|
101
|
+
id: `django:allowed-hosts-wildcard:${file}:${_line(raw, m.index)}`,
|
|
102
|
+
file, line: _line(raw, m.index),
|
|
103
|
+
vuln: 'Django ALLOWED_HOSTS = [\'*\']',
|
|
104
|
+
severity: prod ? 'high' : 'medium',
|
|
105
|
+
family: 'django-host-header',
|
|
106
|
+
cwe: 'CWE-20',
|
|
107
|
+
confidence: 0.9,
|
|
108
|
+
description: 'Wildcard ALLOWED_HOSTS disables Django\'s host-header validation. Attackers can poison password-reset emails, cache keys, and SSL termination by sending arbitrary Host headers.',
|
|
109
|
+
remediation: 'Set ALLOWED_HOSTS to an explicit list of your real hostnames: ["example.com", "api.example.com"].',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// SECRET_KEY literal in source.
|
|
114
|
+
for (const m of raw.matchAll(/^\s*SECRET_KEY\s*=\s*['"]([A-Za-z0-9!@#$%^&*()_+\-=\[\]{};:'",.<>?\/\\|`~]{20,})['"]/gm)) {
|
|
115
|
+
findings.push({
|
|
116
|
+
id: `django:hardcoded-secret-key:${file}:${_line(raw, m.index)}`,
|
|
117
|
+
file, line: _line(raw, m.index),
|
|
118
|
+
vuln: 'Django SECRET_KEY literal in source',
|
|
119
|
+
severity: 'critical',
|
|
120
|
+
family: 'django-hardcoded-secret',
|
|
121
|
+
cwe: 'CWE-798',
|
|
122
|
+
confidence: 0.95,
|
|
123
|
+
description: 'SECRET_KEY is the master signing key for session cookies, CSRF tokens, password reset URLs, and signed data. A literal in source is catastrophic — rotate immediately.',
|
|
124
|
+
remediation: 'SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY") with a raise ImproperlyConfigured if not set. Rotate the leaked value via `django-admin generate-secret-key`.',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// SECURE_SSL_REDIRECT not set or False in prod.
|
|
129
|
+
if (prod) {
|
|
130
|
+
if (!/\bSECURE_SSL_REDIRECT\s*=\s*True\b/.test(raw)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
id: `django:no-ssl-redirect:${file}:1`,
|
|
133
|
+
file, line: 1,
|
|
134
|
+
vuln: 'Django SECURE_SSL_REDIRECT not enabled in production settings',
|
|
135
|
+
severity: 'high',
|
|
136
|
+
family: 'django-no-https-enforce',
|
|
137
|
+
cwe: 'CWE-319',
|
|
138
|
+
confidence: 0.8,
|
|
139
|
+
description: 'Without SECURE_SSL_REDIRECT, plaintext HTTP requests succeed. Sessions / CSRF tokens / OAuth state ride over the wire unprotected on the first request.',
|
|
140
|
+
remediation: 'Add SECURE_SSL_REDIRECT = True at the top of the production settings module.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (!/\bSECURE_HSTS_SECONDS\s*=\s*\d{4,}/.test(raw)) {
|
|
144
|
+
findings.push({
|
|
145
|
+
id: `django:no-hsts:${file}:1`,
|
|
146
|
+
file, line: 1,
|
|
147
|
+
vuln: 'Django SECURE_HSTS_SECONDS not set (or set < 1000) in production',
|
|
148
|
+
severity: 'medium',
|
|
149
|
+
family: 'django-no-hsts',
|
|
150
|
+
cwe: 'CWE-319',
|
|
151
|
+
confidence: 0.75,
|
|
152
|
+
description: 'Without HSTS, an attacker on the network can downgrade to HTTP for the first visit. HSTS_SECONDS=31536000 + INCLUDE_SUBDOMAINS + PRELOAD locks the browser to HTTPS.',
|
|
153
|
+
remediation: 'Set SECURE_HSTS_SECONDS = 31536000, SECURE_HSTS_INCLUDE_SUBDOMAINS = True, SECURE_HSTS_PRELOAD = True.',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (!/\bSESSION_COOKIE_SECURE\s*=\s*True\b/.test(raw)) {
|
|
157
|
+
findings.push({
|
|
158
|
+
id: `django:session-cookie-not-secure:${file}:1`,
|
|
159
|
+
file, line: 1,
|
|
160
|
+
vuln: 'Django SESSION_COOKIE_SECURE = False (or missing)',
|
|
161
|
+
severity: 'high',
|
|
162
|
+
family: 'django-cookie-not-secure',
|
|
163
|
+
cwe: 'CWE-614',
|
|
164
|
+
confidence: 0.85,
|
|
165
|
+
description: 'Session cookie not marked Secure — sent over plain HTTP. Any network observer steals the session.',
|
|
166
|
+
remediation: 'Set SESSION_COOKIE_SECURE = True and SESSION_COOKIE_HTTPONLY = True and SESSION_COOKIE_SAMESITE = "Lax".',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (!/\bCSRF_COOKIE_SECURE\s*=\s*True\b/.test(raw)) {
|
|
170
|
+
findings.push({
|
|
171
|
+
id: `django:csrf-cookie-not-secure:${file}:1`,
|
|
172
|
+
file, line: 1,
|
|
173
|
+
vuln: 'Django CSRF_COOKIE_SECURE = False (or missing)',
|
|
174
|
+
severity: 'medium',
|
|
175
|
+
family: 'django-cookie-not-secure',
|
|
176
|
+
cwe: 'CWE-614',
|
|
177
|
+
confidence: 0.85,
|
|
178
|
+
description: 'CSRF cookie not marked Secure — sent over plain HTTP. Attackers on the network can read and reuse the CSRF token.',
|
|
179
|
+
remediation: 'Set CSRF_COOKIE_SECURE = True and CSRF_COOKIE_HTTPONLY = True.',
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// X_FRAME_OPTIONS = 'ALLOW'
|
|
185
|
+
for (const m of raw.matchAll(/^\s*X_FRAME_OPTIONS\s*=\s*['"]ALLOW['"]/gm)) {
|
|
186
|
+
findings.push({
|
|
187
|
+
id: `django:x-frame-allow:${file}:${_line(raw, m.index)}`,
|
|
188
|
+
file, line: _line(raw, m.index),
|
|
189
|
+
vuln: 'Django X_FRAME_OPTIONS = "ALLOW" — clickjacking permitted',
|
|
190
|
+
severity: 'medium',
|
|
191
|
+
family: 'django-clickjacking',
|
|
192
|
+
cwe: 'CWE-1021',
|
|
193
|
+
confidence: 0.95,
|
|
194
|
+
description: 'X_FRAME_OPTIONS = "ALLOW" lets any site embed your pages in iframes. UI redress / clickjacking attacks become trivial.',
|
|
195
|
+
remediation: 'Set X_FRAME_OPTIONS = "DENY" (or "SAMEORIGIN" if you genuinely embed yourself).',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// CORS_ALLOW_ALL_ORIGINS = True
|
|
200
|
+
for (const m of raw.matchAll(/^\s*CORS_ALLOW_ALL_ORIGINS\s*=\s*True\b/gm)) {
|
|
201
|
+
findings.push({
|
|
202
|
+
id: `django:cors-allow-all:${file}:${_line(raw, m.index)}`,
|
|
203
|
+
file, line: _line(raw, m.index),
|
|
204
|
+
vuln: 'Django CORS_ALLOW_ALL_ORIGINS = True',
|
|
205
|
+
severity: 'high',
|
|
206
|
+
family: 'django-cors-wildcard',
|
|
207
|
+
cwe: 'CWE-942',
|
|
208
|
+
confidence: 0.9,
|
|
209
|
+
description: 'Wildcard CORS combined with credentialed requests means any origin can read authenticated responses. Use an explicit allow-list.',
|
|
210
|
+
remediation: 'Replace with CORS_ALLOWED_ORIGINS = ["https://app.example.com", ...]. Set CORS_ALLOW_CREDENTIALS only when actually needed.',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// AUTH_PASSWORD_VALIDATORS missing or empty.
|
|
215
|
+
if (prod && !/\bAUTH_PASSWORD_VALIDATORS\s*=\s*\[\s*\{/.test(raw)) {
|
|
216
|
+
findings.push({
|
|
217
|
+
id: `django:no-password-validators:${file}:1`,
|
|
218
|
+
file, line: 1,
|
|
219
|
+
vuln: 'Django AUTH_PASSWORD_VALIDATORS missing or empty in production settings',
|
|
220
|
+
severity: 'medium',
|
|
221
|
+
family: 'django-weak-password-policy',
|
|
222
|
+
cwe: 'CWE-521',
|
|
223
|
+
confidence: 0.7,
|
|
224
|
+
description: 'No password validators configured — users can register with "password" / 12345 / etc.',
|
|
225
|
+
remediation: 'Configure UserAttributeSimilarity, MinimumLength (≥12), CommonPassword, NumericPassword validators at minimum.',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return findings;
|
|
230
|
+
}
|