@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,234 @@
|
|
|
1
|
+
// String-value abstract domain (P4.4).
|
|
2
|
+
//
|
|
3
|
+
// The taint engine treats every string as opaque — "tainted" or "clean."
|
|
4
|
+
// Real codebases have lots of strings that are KNOWN at compile time:
|
|
5
|
+
//
|
|
6
|
+
// const url = "https://internal.example.com/health";
|
|
7
|
+
// await fetch(url);
|
|
8
|
+
//
|
|
9
|
+
// `url` is constant. SSRF is *impossible*. The current engine doesn't fire
|
|
10
|
+
// (because the literal isn't tainted), but it ALSO doesn't actively prove
|
|
11
|
+
// safety — and when a project mixes constants with user-influenced fragments,
|
|
12
|
+
// the engine over-approximates conservatively.
|
|
13
|
+
//
|
|
14
|
+
// This module models strings with a three-element lattice:
|
|
15
|
+
//
|
|
16
|
+
// Const(literal) "https://internal.example.com"
|
|
17
|
+
// Concat(parts) "https://" + host + "/" + path
|
|
18
|
+
// Unknown anything we can't statically analyze
|
|
19
|
+
//
|
|
20
|
+
// And provides:
|
|
21
|
+
// - abstract(expr) → returns the abstract value for an IR expression
|
|
22
|
+
// - isSafeUrl(absVal, allowedHosts) → bool, prove the URL is safe
|
|
23
|
+
// - join(a, b) → lattice meet (used at branch joins)
|
|
24
|
+
//
|
|
25
|
+
// v1: enough to handle the common SSRF / open-redirect "constant URL" case.
|
|
26
|
+
// v2 would add prefix/suffix analysis, regex membership, etc.
|
|
27
|
+
|
|
28
|
+
export const TOP = { kind: 'Unknown' };
|
|
29
|
+
export const BOTTOM = { kind: 'Const', value: '' }; // empty string = bottom of useful domain
|
|
30
|
+
|
|
31
|
+
export function makeConst(value) {
|
|
32
|
+
if (typeof value !== 'string') return TOP;
|
|
33
|
+
return { kind: 'Const', value };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* v0.69 #4a — regex-constrained string value.
|
|
38
|
+
*
|
|
39
|
+
* Represents a string whose concrete value is unknown but whose CHARSET +
|
|
40
|
+
* SHAPE are bounded to a regex. Sanitizers produce these:
|
|
41
|
+
* encodeURIComponent(x) → Regex(/^[A-Za-z0-9-_.!~*'()%]*$/)
|
|
42
|
+
* parseInt(x).toString() → Regex(/^-?\d+$/)
|
|
43
|
+
* bcrypt.hash(x) → Regex(/^\$2[aby]?\$\d{1,2}\$[./A-Za-z0-9]{53}$/)
|
|
44
|
+
*
|
|
45
|
+
* The pattern MUST be anchored with ^ and $ to be sound.
|
|
46
|
+
*/
|
|
47
|
+
export function makeRegex(pattern) {
|
|
48
|
+
if (!(pattern instanceof RegExp)) return TOP;
|
|
49
|
+
const src = pattern.source;
|
|
50
|
+
if (!src.startsWith('^') || !src.endsWith('$')) return TOP;
|
|
51
|
+
return { kind: 'Regex', pattern };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function makeConcat(parts) {
|
|
55
|
+
// Optimize: if every part is Const, collapse to a single Const.
|
|
56
|
+
if (parts.every(p => p && p.kind === 'Const')) {
|
|
57
|
+
return makeConst(parts.map(p => p.value).join(''));
|
|
58
|
+
}
|
|
59
|
+
// If any part is Unknown, the whole concat is Unknown.
|
|
60
|
+
if (parts.some(p => !p || p.kind === 'Unknown')) return TOP;
|
|
61
|
+
return { kind: 'Concat', parts };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lattice join: a ⊔ b. Returns the least-upper-bound.
|
|
66
|
+
*
|
|
67
|
+
* Const(s) ⊔ Const(s) = Const(s)
|
|
68
|
+
* Const(s) ⊔ Const(t) = Regex(escape(s)|escape(t)) if both anchor-friendly
|
|
69
|
+
* Regex(p) ⊔ Const(s) where s matches p = Regex(p)
|
|
70
|
+
* Regex(p1) ⊔ Regex(p2) = Regex(p1) if patterns identical, else TOP
|
|
71
|
+
* anything ⊔ Unknown = Unknown
|
|
72
|
+
*/
|
|
73
|
+
export function join(a, b) {
|
|
74
|
+
if (!a) return b;
|
|
75
|
+
if (!b) return a;
|
|
76
|
+
if (a.kind === 'Unknown' || b.kind === 'Unknown') return TOP;
|
|
77
|
+
if (a.kind === 'Const' && b.kind === 'Const') {
|
|
78
|
+
if (a.value === b.value) return a;
|
|
79
|
+
return TOP; // distinct constants from two branches — be conservative
|
|
80
|
+
}
|
|
81
|
+
if (a.kind === 'Regex' && b.kind === 'Regex') {
|
|
82
|
+
return a.pattern.source === b.pattern.source ? a : TOP;
|
|
83
|
+
}
|
|
84
|
+
if (a.kind === 'Regex' && b.kind === 'Const') {
|
|
85
|
+
return a.pattern.test(b.value) ? a : TOP;
|
|
86
|
+
}
|
|
87
|
+
if (b.kind === 'Regex' && a.kind === 'Const') {
|
|
88
|
+
return b.pattern.test(a.value) ? b : TOP;
|
|
89
|
+
}
|
|
90
|
+
return TOP;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Abstract an IR expression into a string-value abstract value. The engine
|
|
95
|
+
* walks expressions during evaluation; this helper gives us the
|
|
96
|
+
* `Const | Concat | Unknown` summary alongside the boolean taint check.
|
|
97
|
+
*/
|
|
98
|
+
export function abstract(expr) {
|
|
99
|
+
if (!expr) return TOP;
|
|
100
|
+
switch (expr.kind) {
|
|
101
|
+
case 'literal':
|
|
102
|
+
if (typeof expr.value === 'string') return makeConst(expr.value);
|
|
103
|
+
return TOP;
|
|
104
|
+
case 'tpl':
|
|
105
|
+
if (Array.isArray(expr.parts)) return makeConcat(expr.parts.map(abstract));
|
|
106
|
+
return TOP;
|
|
107
|
+
case 'binary': {
|
|
108
|
+
if (expr.op === '+' || expr.op === '+=') {
|
|
109
|
+
return makeConcat([abstract(expr.left), abstract(expr.right)]);
|
|
110
|
+
}
|
|
111
|
+
return TOP;
|
|
112
|
+
}
|
|
113
|
+
case 'call': {
|
|
114
|
+
// v0.69 #4a — sanitizer-call output is regex-constrained.
|
|
115
|
+
const tail = String(expr.callee || '').split('.').pop();
|
|
116
|
+
const r = SANITIZER_OUTPUT_REGEX[tail];
|
|
117
|
+
if (r) return makeRegex(r);
|
|
118
|
+
return TOP;
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
return TOP;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Catalog of known sanitizer output regexes. The output of these calls is
|
|
127
|
+
* provably bounded to the listed charset. This is what powers the
|
|
128
|
+
* `provenClean` flag for non-SQL injection classes.
|
|
129
|
+
*
|
|
130
|
+
* Patterns are conservative — only listed when the spec REQUIRES the
|
|
131
|
+
* output to fit the regex. Empty / null returns are part of the domain.
|
|
132
|
+
*/
|
|
133
|
+
const SANITIZER_OUTPUT_REGEX = {
|
|
134
|
+
// URL-safe encoding (RFC 3986 reserved + unreserved with %xx escapes).
|
|
135
|
+
encodeURIComponent: /^[A-Za-z0-9\-_.!~*'()%]*$/,
|
|
136
|
+
encodeURI: /^[A-Za-z0-9\-_.!~*'();/?:@&=+$,#%]*$/,
|
|
137
|
+
// Numeric-coerced.
|
|
138
|
+
parseInt: /^-?\d+$/,
|
|
139
|
+
parseFloat: /^-?\d+(?:\.\d+)?$/,
|
|
140
|
+
// bcrypt / scrypt output format.
|
|
141
|
+
hashSync: /^\$2[aby]?\$\d{1,2}\$[.\/A-Za-z0-9]{53}$/,
|
|
142
|
+
// Hex digest from crypto.
|
|
143
|
+
digest: /^[0-9a-f]+$/,
|
|
144
|
+
toString: /^[A-Za-z0-9+/=]*$/, // when called on a Buffer with 'base64' — over-approximate, narrowed by argIndex in v2
|
|
145
|
+
// Java URLEncoder.encode — RFC 3986 + spaces as '+'.
|
|
146
|
+
// (We can't distinguish overloads from regex name alone; conservative listing.)
|
|
147
|
+
// PHP htmlspecialchars / htmlentities — HTML-entity escape.
|
|
148
|
+
htmlspecialchars: /^[^<>&"']*(?:&(?:lt|gt|amp|quot|#039);)*[^<>&"']*$/,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* SAFE-CHARSET PROOF — does the abstract value provably fit the given regex?
|
|
153
|
+
*
|
|
154
|
+
* Used by sanitizer-proof.js to verify that a sanitizer's output cannot
|
|
155
|
+
* contain the metacharacters of a target injection family (no `'` for SQL,
|
|
156
|
+
* no `<` for XSS, no `\r\n` for response-splitting, etc.).
|
|
157
|
+
*
|
|
158
|
+
* Returns true iff EVERY concrete string in the abstract value's denotation
|
|
159
|
+
* matches `safe`.
|
|
160
|
+
*/
|
|
161
|
+
export function provablyMatches(absVal, safe) {
|
|
162
|
+
if (!absVal || !(safe instanceof RegExp)) return false;
|
|
163
|
+
if (absVal.kind === 'Const') return safe.test(absVal.value);
|
|
164
|
+
if (absVal.kind === 'Regex') {
|
|
165
|
+
// Sound approximation: same source string → provable. Otherwise we'd
|
|
166
|
+
// need regex-subset, which is undecidable in general; v2 could do
|
|
167
|
+
// structural checks for common cases.
|
|
168
|
+
return absVal.pattern.source === safe.source;
|
|
169
|
+
}
|
|
170
|
+
if (absVal.kind === 'Concat') {
|
|
171
|
+
// A concat is provably safe iff every part is provably safe AND the
|
|
172
|
+
// safe regex permits arbitrary repetition (i.e. is of the form ^X*$).
|
|
173
|
+
if (!/^\^.+\*\$$/.test(safe.source)) return false;
|
|
174
|
+
return absVal.parts.every(p => provablyMatches(p, safe));
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Render an abstract string back to a textual form for diagnostics.
|
|
181
|
+
*
|
|
182
|
+
* Const("hello") → "hello"
|
|
183
|
+
* Concat([Const("a"), Unknown, Const("b")]) → "a${...}b"
|
|
184
|
+
* Unknown → "${...}"
|
|
185
|
+
*/
|
|
186
|
+
export function render(absVal) {
|
|
187
|
+
if (!absVal || absVal.kind === 'Unknown') return '${...}';
|
|
188
|
+
if (absVal.kind === 'Const') return absVal.value;
|
|
189
|
+
if (absVal.kind === 'Concat') {
|
|
190
|
+
return absVal.parts.map(p => render(p)).join('');
|
|
191
|
+
}
|
|
192
|
+
if (absVal.kind === 'Regex') return `${absVal.pattern.source}`;
|
|
193
|
+
return '${...}';
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* SSRF guard: given an abstract URL value and a list of trusted hosts,
|
|
198
|
+
* return true iff the URL is provably to one of the trusted hosts.
|
|
199
|
+
*
|
|
200
|
+
* isProvablyToHost(makeConst("https://internal.example.com/x"), ["internal.example.com"]) → true
|
|
201
|
+
* isProvablyToHost(TOP, [...]) → false (can't prove anything about Unknown)
|
|
202
|
+
* isProvablyToHost(Concat(["https://" + Unknown]), [...]) → false
|
|
203
|
+
*/
|
|
204
|
+
export function isProvablyToHost(absVal, allowedHosts) {
|
|
205
|
+
if (!absVal || absVal.kind !== 'Const') return false;
|
|
206
|
+
if (!Array.isArray(allowedHosts) || !allowedHosts.length) return false;
|
|
207
|
+
let url;
|
|
208
|
+
try {
|
|
209
|
+
url = new URL(absVal.value);
|
|
210
|
+
} catch { return false; }
|
|
211
|
+
return allowedHosts.includes(url.host);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Open-redirect safe? An abstract URL value passed to res.redirect is safe
|
|
216
|
+
* when it's either provably to an allowed host OR a relative path with no
|
|
217
|
+
* scheme/host parts.
|
|
218
|
+
*/
|
|
219
|
+
export function isSafeRedirectTarget(absVal, allowedHosts) {
|
|
220
|
+
if (!absVal) return false;
|
|
221
|
+
if (absVal.kind === 'Const') {
|
|
222
|
+
// Relative path starting with / and not //
|
|
223
|
+
if (/^\/(?!\/)/.test(absVal.value)) return true;
|
|
224
|
+
return isProvablyToHost(absVal, allowedHosts);
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Hash an abstract value for cache-key purposes.
|
|
231
|
+
*/
|
|
232
|
+
export function hashAbstract(absVal) {
|
|
233
|
+
return render(absVal);
|
|
234
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Type-stub-aware taint filter (v0.73).
|
|
2
|
+
//
|
|
3
|
+
// v0.70 added scanner/src/ir/type-stubs.js but only wired it into the
|
|
4
|
+
// receiver-context lookup. This post-pass closes the loop: after the
|
|
5
|
+
// taint engine emits findings, we consult the stubs map to demote
|
|
6
|
+
// findings whose source/sink type pair is provably incompatible with
|
|
7
|
+
// the vulnerability class.
|
|
8
|
+
//
|
|
9
|
+
// Example: a source that returns `number` (per stub signature) flowing
|
|
10
|
+
// to an XSS sink is suppressable — a number coerced to string can only
|
|
11
|
+
// produce digits/decimal/sign, which can't form the metacharacters
|
|
12
|
+
// (<, >, ', ") required to break out of an HTML context.
|
|
13
|
+
//
|
|
14
|
+
// Rules per vuln family:
|
|
15
|
+
// XSS (CWE-79): source type ∈ {number, boolean, Date, RegExp} → demote
|
|
16
|
+
// SQL inj (CWE-89): source type ∈ {number, boolean, Date} → demote
|
|
17
|
+
// Cmd inj (CWE-78): source type ∈ {number, boolean} → demote
|
|
18
|
+
// Path trav (CWE-22): source type ∈ {number, boolean} → demote
|
|
19
|
+
//
|
|
20
|
+
// Demotion lowers severity by one tier and sets `_stubTypeDemoted: true`
|
|
21
|
+
// with a `_stubTypeReason`. We never DROP findings — the stub-aware
|
|
22
|
+
// reason is shown to the operator so they can override if the stub is
|
|
23
|
+
// wrong or out of date.
|
|
24
|
+
|
|
25
|
+
const FAMILY_SAFE_TYPES = {
|
|
26
|
+
'CWE-79': new Set(['number', 'boolean', 'Date', 'RegExp', 'bigint']),
|
|
27
|
+
'CWE-89': new Set(['number', 'boolean', 'Date', 'bigint']),
|
|
28
|
+
'CWE-78': new Set(['number', 'boolean', 'bigint']),
|
|
29
|
+
'CWE-22': new Set(['number', 'boolean']),
|
|
30
|
+
'CWE-918': new Set(['number', 'boolean']),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Try to resolve the type of the finding's source from the type-stubs map.
|
|
35
|
+
* The lookup chain: (1) the finding's source.label or trace[0].sourceLabel
|
|
36
|
+
* is the catalog source id; (2) the stub signature for the source's
|
|
37
|
+
* underlying function. Returns the type string ('string', 'number', …)
|
|
38
|
+
* or null if unknown.
|
|
39
|
+
*/
|
|
40
|
+
function _sourceTypeFromStubs(finding, stubs) {
|
|
41
|
+
if (!stubs || !stubs.signatures) return null;
|
|
42
|
+
const trace = Array.isArray(finding.trace) ? finding.trace : [];
|
|
43
|
+
const src = trace[0] || finding.source;
|
|
44
|
+
const label = src?.sourceLabel || src?.label || '';
|
|
45
|
+
// The label is shaped like 'req.body' / 'request.GET' / etc. The
|
|
46
|
+
// underlying function lookup uses the LAST identifier as a callable name.
|
|
47
|
+
const tail = String(label).split('.').pop();
|
|
48
|
+
if (!tail) return null;
|
|
49
|
+
const sig = stubs.signatures.get(tail);
|
|
50
|
+
if (!sig) return null;
|
|
51
|
+
return _normalizeType(sig.returnType);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _normalizeType(t) {
|
|
55
|
+
if (!t) return null;
|
|
56
|
+
const trimmed = String(t).trim().toLowerCase();
|
|
57
|
+
if (trimmed === 'number' || trimmed === 'numeric' || /^int(8|16|32|64)?$/.test(trimmed)) return 'number';
|
|
58
|
+
if (trimmed === 'bigint') return 'bigint';
|
|
59
|
+
if (trimmed === 'boolean' || trimmed === 'bool') return 'boolean';
|
|
60
|
+
if (trimmed === 'date') return 'Date';
|
|
61
|
+
if (trimmed === 'regexp') return 'RegExp';
|
|
62
|
+
if (trimmed === 'string' || trimmed === 'str') return 'string';
|
|
63
|
+
if (trimmed.endsWith('[]') || trimmed.startsWith('array<')) return 'array';
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Post-pass entry. Mutates findings in place: adds `_stubTypeDemoted`,
|
|
69
|
+
* `_stubTypeReason`, downgrades `severity` by one tier when the source
|
|
70
|
+
* type is in the family-safe set for the finding's CWE.
|
|
71
|
+
*
|
|
72
|
+
* Returns the (mutated) findings array with `_stubFilterStats` non-
|
|
73
|
+
* enumerable sidecar.
|
|
74
|
+
*/
|
|
75
|
+
export function applyStubAwareFilter(findings, stubs) {
|
|
76
|
+
if (!Array.isArray(findings) || findings.length === 0) return findings;
|
|
77
|
+
if (!stubs || !stubs.signatures) return findings;
|
|
78
|
+
let demoted = 0;
|
|
79
|
+
for (const f of findings) {
|
|
80
|
+
if (!f || f.parser !== 'IR-TAINT') continue;
|
|
81
|
+
const safeSet = FAMILY_SAFE_TYPES[f.cwe];
|
|
82
|
+
if (!safeSet) continue;
|
|
83
|
+
const sourceType = _sourceTypeFromStubs(f, stubs);
|
|
84
|
+
if (!sourceType) continue;
|
|
85
|
+
if (!safeSet.has(sourceType)) continue;
|
|
86
|
+
f._stubTypeDemoted = true;
|
|
87
|
+
f._stubTypeReason = `source type ${sourceType} cannot carry ${f.cwe} metacharacters`;
|
|
88
|
+
f._stubTypeOriginalSeverity = f.severity;
|
|
89
|
+
const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
|
|
90
|
+
if (downgrade[f.severity]) f.severity = downgrade[f.severity];
|
|
91
|
+
demoted++;
|
|
92
|
+
}
|
|
93
|
+
Object.defineProperty(findings, '_stubFilterStats', {
|
|
94
|
+
value: { demoted, totalConsidered: findings.length },
|
|
95
|
+
enumerable: false,
|
|
96
|
+
});
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const _internal = { FAMILY_SAFE_TYPES, _sourceTypeFromStubs, _normalizeType };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Function-summary cache for context-sensitive interprocedural taint.
|
|
2
|
+
//
|
|
3
|
+
// PRD §6.2: "k-CFA configurable per analysis." This module implements the
|
|
4
|
+
// k=1 monovariant version — each function gets ONE summary per distinct
|
|
5
|
+
// entry-taint-state, cached by hash. Higher k = exponential blowup we don't
|
|
6
|
+
// pay yet.
|
|
7
|
+
//
|
|
8
|
+
// A summary captures, given a set of tainted parameter names at function
|
|
9
|
+
// entry, what the function does:
|
|
10
|
+
// - which return value(s) are tainted
|
|
11
|
+
// - which call-site arguments get mutated to tainted (by-reference)
|
|
12
|
+
// - which global / module variables get tainted
|
|
13
|
+
// - which findings emit
|
|
14
|
+
//
|
|
15
|
+
// The taint engine (engine.js) consults the summary cache before re-analyzing
|
|
16
|
+
// a callee. Cache key = `${qid}::${sorted-taint-state}`. Cache hits are O(1).
|
|
17
|
+
//
|
|
18
|
+
// Limitations:
|
|
19
|
+
// - Field sensitivity is at the parameter granularity only (not arbitrary
|
|
20
|
+
// access paths). `f(obj)` with obj.foo tainted is treated the same as
|
|
21
|
+
// obj.bar tainted.
|
|
22
|
+
// - No higher-order tracking — callbacks passed as args aren't analyzed.
|
|
23
|
+
// - Recursion: when we'd recurse into a function already on the analysis
|
|
24
|
+
// stack, we return the bottom summary (no-taint) and rely on fixed-point
|
|
25
|
+
// iteration. With k=1 this converges in ≤2 iterations for typical code.
|
|
26
|
+
|
|
27
|
+
import * as crypto from 'node:crypto';
|
|
28
|
+
import { canonicalize as canonicalizeAccessSet } from './access-paths.js';
|
|
29
|
+
import { hashReceiverType } from './receiver-context.js';
|
|
30
|
+
|
|
31
|
+
function _hashState(taintedParams) {
|
|
32
|
+
if (!taintedParams || taintedParams.size === 0) return 'empty';
|
|
33
|
+
// P1.1: canonicalize the access-path lattice before hashing so equivalent
|
|
34
|
+
// states (e.g. {"x", "x.y"} and {"x"}) produce the same cache key.
|
|
35
|
+
const canon = canonicalizeAccessSet(taintedParams);
|
|
36
|
+
const sorted = [...canon].sort().join('|');
|
|
37
|
+
return crypto.createHash('sha256').update(sorted).digest('hex').slice(0, 12);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SummaryCache {
|
|
41
|
+
constructor() {
|
|
42
|
+
this._cache = new Map(); // qid::hash → summary
|
|
43
|
+
this._stack = new Set(); // qids currently being analyzed (recursion guard)
|
|
44
|
+
this._iter = 0;
|
|
45
|
+
this._maxIter = 5000;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_key(qid, taintedParams, receiverType) {
|
|
49
|
+
// P1.2: when a receiver type is provided, extend the cache key with
|
|
50
|
+
// its hash. Backward-compatible: no receiverType → same key as before.
|
|
51
|
+
const base = `${qid}::${_hashState(taintedParams)}`;
|
|
52
|
+
if (!receiverType) return base;
|
|
53
|
+
return `${base}::${hashReceiverType(receiverType)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get(qid, taintedParams, receiverType) {
|
|
57
|
+
return this._cache.get(this._key(qid, taintedParams, receiverType));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
set(qid, taintedParams, summary, receiverType) {
|
|
61
|
+
this._cache.set(this._key(qid, taintedParams, receiverType), summary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
has(qid, taintedParams, receiverType) {
|
|
65
|
+
return this._cache.has(this._key(qid, taintedParams, receiverType));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Compute the summary for a function (or return cached). The `analyze`
|
|
69
|
+
// callback is the per-function walker that returns
|
|
70
|
+
// { returnTainted, mutatedParams: Set, taintedGlobals: Set, findings: [] }
|
|
71
|
+
compute(qid, taintedParams, analyze) {
|
|
72
|
+
const k = this._key(qid, taintedParams);
|
|
73
|
+
if (this._cache.has(k)) return this._cache.get(k);
|
|
74
|
+
if (this._stack.has(qid)) {
|
|
75
|
+
// Recursion — return bottom summary; fixed-point iter will refine.
|
|
76
|
+
return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _recursive: true };
|
|
77
|
+
}
|
|
78
|
+
if (++this._iter > this._maxIter) {
|
|
79
|
+
return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _budgetExceeded: true };
|
|
80
|
+
}
|
|
81
|
+
this._stack.add(qid);
|
|
82
|
+
try {
|
|
83
|
+
const summary = analyze(qid, taintedParams);
|
|
84
|
+
this._cache.set(k, summary);
|
|
85
|
+
return summary;
|
|
86
|
+
} finally {
|
|
87
|
+
this._stack.delete(qid);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Helper: apply a summary to a caller's taint state given the call site's
|
|
92
|
+
// argument bindings. Returns { calleeReturnTainted, mutated: Set of caller-side
|
|
93
|
+
// var names that should become tainted because the callee mutated them }.
|
|
94
|
+
applyAtCallSite(summary, paramNames, callArgs, callerTaintedVars) {
|
|
95
|
+
if (!summary) return { returnTainted: false, mutated: new Set() };
|
|
96
|
+
const mutated = new Set();
|
|
97
|
+
if (summary.mutatedParams && summary.mutatedParams.size) {
|
|
98
|
+
// Map each mutated parameter position back to the caller-side argument name.
|
|
99
|
+
for (const paramName of summary.mutatedParams) {
|
|
100
|
+
const idx = paramNames.indexOf(paramName);
|
|
101
|
+
if (idx < 0) continue;
|
|
102
|
+
const arg = callArgs[idx];
|
|
103
|
+
if (arg && arg.kind === 'ident') mutated.add(arg.name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { returnTainted: !!summary.returnTainted, mutated };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
size() { return this._cache.size; }
|
|
110
|
+
clear() { this._cache.clear(); this._iter = 0; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build the entry-taint-state for a callee from a call site:
|
|
114
|
+
// given the callee's param names + the caller's tainted-var set + the
|
|
115
|
+
// call args, return a Set of param names that are tainted at entry.
|
|
116
|
+
export function entryStateFromCall(paramNames, callArgs, callerTaintedVars) {
|
|
117
|
+
const out = new Set();
|
|
118
|
+
if (!Array.isArray(paramNames) || !Array.isArray(callArgs)) return out;
|
|
119
|
+
for (let i = 0; i < paramNames.length && i < callArgs.length; i++) {
|
|
120
|
+
const arg = callArgs[i];
|
|
121
|
+
if (!arg) continue;
|
|
122
|
+
if (arg.kind === 'ident' && callerTaintedVars.has(arg.name)) {
|
|
123
|
+
out.add(paramNames[i]);
|
|
124
|
+
} else if (arg.kind === 'member' && arg.object?.kind === 'ident') {
|
|
125
|
+
const base = arg.object.name;
|
|
126
|
+
if (callerTaintedVars.has(base) || callerTaintedVars.has(`${base}.${arg.prop}`)) {
|
|
127
|
+
out.add(paramNames[i]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|