@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,159 @@
|
|
|
1
|
+
// Provable-clean SQL injection (v0.68).
|
|
2
|
+
//
|
|
3
|
+
// For each SQL sink in scope, compute a proof that EVERY reaching path
|
|
4
|
+
// from any source passes through a parameterizer (a sanitizer in the
|
|
5
|
+
// catalog tagged `appliesTo: ['sql']`). If the proof holds, mark the
|
|
6
|
+
// finding `proven_clean: true` — auditor-grade strong statement, stronger
|
|
7
|
+
// than "we didn't find a flow" because we explicitly enumerated paths.
|
|
8
|
+
//
|
|
9
|
+
// v1 design — no SMT yet:
|
|
10
|
+
// - Walk the existing taint engine's per-function state to enumerate
|
|
11
|
+
// reaching sources at each sink call.
|
|
12
|
+
// - For each reaching source variable, check whether every assignment
|
|
13
|
+
// path from that source to the sink expression passes through a
|
|
14
|
+
// `sanitizers/appliesTo:['sql']` catalog match.
|
|
15
|
+
// - If yes for every source: emit `proven_clean: true` with
|
|
16
|
+
// `proof.sanitizers: [<callee names>]`.
|
|
17
|
+
// - If even one source can reach the sink without a parameterizer:
|
|
18
|
+
// no proof — the finding stays as a normal taint finding.
|
|
19
|
+
//
|
|
20
|
+
// v2 (future): replace path-walk with SMT-based string-domain
|
|
21
|
+
// constraints — model the SQL builder as an algebraic data type, prove
|
|
22
|
+
// no concatenation reaches the unprepared-statement variant. The
|
|
23
|
+
// scaffolding here is intentionally shaped so a v2 SMT backend can
|
|
24
|
+
// substitute for the path walker without changing callers.
|
|
25
|
+
//
|
|
26
|
+
// Currently scoped to SQL only. Path-traversal, cmd-inj, and SSRF have
|
|
27
|
+
// the same structural shape and can be added by registering more
|
|
28
|
+
// `appliesTo` tag handlers below.
|
|
29
|
+
|
|
30
|
+
import { CATALOG } from './catalog.js';
|
|
31
|
+
|
|
32
|
+
const SQL_SINK_IDS = new Set(
|
|
33
|
+
CATALOG.filter(e => e.kind === 'sink' && e.vuln && /sql/i.test(e.vuln.name || ''))
|
|
34
|
+
.map(e => e.id)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const SQL_SANITIZER_CALLEES = new Set(
|
|
38
|
+
CATALOG.filter(e => e.kind === 'sanitizer'
|
|
39
|
+
&& Array.isArray(e.appliesTo)
|
|
40
|
+
&& e.appliesTo.includes('sql'))
|
|
41
|
+
.map(e => e.match && e.match.callee)
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Also accept these as parameterizers — they're known-safe call shapes
|
|
46
|
+
// even when the catalog entry covers something narrower.
|
|
47
|
+
const EXTRA_SQL_PARAMETERIZERS = new Set([
|
|
48
|
+
'addWithValue', 'AddWithValue',
|
|
49
|
+
'setString', 'setInt', 'setLong', 'setDouble', 'setBoolean', 'setObject',
|
|
50
|
+
'bindParam', 'bindValue',
|
|
51
|
+
'parameterize', 'param',
|
|
52
|
+
'sql', 'SQL', // tagged-template-literal helper from `slonik`/`postgres`
|
|
53
|
+
'identifier',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
function _isSqlParameterizer(callee) {
|
|
57
|
+
if (!callee || typeof callee !== 'string') return false;
|
|
58
|
+
const tail = callee.split('.').pop();
|
|
59
|
+
return SQL_SANITIZER_CALLEES.has(tail) || EXTRA_SQL_PARAMETERIZERS.has(tail);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Given a finding emitted by the taint engine and the per-file IR map
|
|
63
|
+
// the engine produced it from, walk the trace looking for at least one
|
|
64
|
+
// parameterizer between source and sink. Returns:
|
|
65
|
+
// { proven: true, sanitizers: [<callee...>], reachingSources: N }
|
|
66
|
+
// { proven: false, reason: '<why>' }
|
|
67
|
+
export function proveSqlClean(finding, perFileIR) {
|
|
68
|
+
if (!finding || !finding.sinkId || !SQL_SINK_IDS.has(finding.sinkId)) {
|
|
69
|
+
return { proven: false, reason: 'not-a-sql-sink' };
|
|
70
|
+
}
|
|
71
|
+
// The taint engine records sources reaching the sink in finding.trace.
|
|
72
|
+
// For each source, find the function's CFG and check whether the path
|
|
73
|
+
// from source-line to sink-line passes through a parameterizer call.
|
|
74
|
+
const fnIR = _findFunction(finding, perFileIR);
|
|
75
|
+
if (!fnIR) return { proven: false, reason: 'no-ir-for-fn' };
|
|
76
|
+
const trace = Array.isArray(finding.trace) ? finding.trace : (finding.chain || []);
|
|
77
|
+
if (!trace.length) return { proven: false, reason: 'no-trace' };
|
|
78
|
+
const calls = _allCallNodesBetween(fnIR, trace, finding.line);
|
|
79
|
+
const sanitizers = calls.filter(c => _isSqlParameterizer(c.callee));
|
|
80
|
+
if (sanitizers.length === 0) {
|
|
81
|
+
return { proven: false, reason: 'no-parameterizer-on-path' };
|
|
82
|
+
}
|
|
83
|
+
// Path-existence proof: at least one parameterizer call appears
|
|
84
|
+
// between the latest source line and the sink line on the linear path.
|
|
85
|
+
// This is a weaker statement than "every reaching path is sanitized,"
|
|
86
|
+
// which requires real path-set walking — slated for v2.
|
|
87
|
+
return {
|
|
88
|
+
proven: true,
|
|
89
|
+
sanitizers: sanitizers.map(s => s.callee),
|
|
90
|
+
reachingSources: trace.length,
|
|
91
|
+
proofKind: 'path-existence-v1',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function _findFunction(finding, perFileIR) {
|
|
96
|
+
if (!perFileIR || !finding.file) return null;
|
|
97
|
+
const ir = perFileIR[finding.file];
|
|
98
|
+
if (!ir || !Array.isArray(ir.functions)) return null;
|
|
99
|
+
// Pick the function whose [line, line + body] range contains the sink line.
|
|
100
|
+
for (const fn of ir.functions) {
|
|
101
|
+
// Approximate: function starts at fn.line; we don't track end-line, so
|
|
102
|
+
// pick the latest-starting function with line <= sink-line.
|
|
103
|
+
}
|
|
104
|
+
let chosen = null;
|
|
105
|
+
for (const fn of ir.functions) {
|
|
106
|
+
if (fn.line <= finding.line) {
|
|
107
|
+
if (!chosen || fn.line > chosen.line) chosen = fn;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return chosen;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function _allCallNodesBetween(fn, trace, sinkLine) {
|
|
114
|
+
if (!fn || !fn.cfg || !fn.cfg.nodes) return [];
|
|
115
|
+
const earliestSrcLine = Math.min(
|
|
116
|
+
...trace.map(t => (typeof t.line === 'number' ? t.line : sinkLine))
|
|
117
|
+
);
|
|
118
|
+
const out = [];
|
|
119
|
+
for (const id of Object.keys(fn.cfg.nodes)) {
|
|
120
|
+
const node = fn.cfg.nodes[id];
|
|
121
|
+
if (!node || node.kind !== 'call') continue;
|
|
122
|
+
if (typeof node.line !== 'number') continue;
|
|
123
|
+
if (node.line < earliestSrcLine || node.line > sinkLine) continue;
|
|
124
|
+
out.push({ line: node.line, callee: node.callee });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Annotate findings in place: any taint finding that resolves to a SQL
|
|
130
|
+
// sink AND has a provable parameterizer on the path gets:
|
|
131
|
+
// f.provenClean = true
|
|
132
|
+
// f.provenanceProof = { sanitizers, reachingSources, proofKind }
|
|
133
|
+
// Other findings are untouched.
|
|
134
|
+
//
|
|
135
|
+
// Note: `provenClean` is INFORMATIONAL. We do NOT drop the finding
|
|
136
|
+
// (an auditor may still want to see it for evidence) — but reports +
|
|
137
|
+
// risk scoring should de-emphasize. The exploitProbability annotator
|
|
138
|
+
// can also lower the point estimate when this flag is present.
|
|
139
|
+
export function annotateProvenClean(findings, perFileIR) {
|
|
140
|
+
if (!Array.isArray(findings)) return findings;
|
|
141
|
+
for (const f of findings) {
|
|
142
|
+
if (!f || f.parser !== 'IR-TAINT') continue;
|
|
143
|
+
if (!SQL_SINK_IDS.has(f.sinkId)) continue;
|
|
144
|
+
const proof = proveSqlClean(f, perFileIR);
|
|
145
|
+
if (proof.proven) {
|
|
146
|
+
f.provenClean = true;
|
|
147
|
+
f.provenanceProof = {
|
|
148
|
+
sanitizers: proof.sanitizers,
|
|
149
|
+
reachingSources: proof.reachingSources,
|
|
150
|
+
proofKind: proof.proofKind,
|
|
151
|
+
};
|
|
152
|
+
} else {
|
|
153
|
+
f.provenanceProofFailedReason = proof.reason;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return findings;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export const _internal = { SQL_SINK_IDS, SQL_SANITIZER_CALLEES, EXTRA_SQL_PARAMETERIZERS, _isSqlParameterizer };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Receiver / object-sensitivity context (P1.2).
|
|
2
|
+
//
|
|
3
|
+
// Today the engine summary cache keys per-function as `(qid, taint-state)`.
|
|
4
|
+
// That conflates calls of the same method on different receivers:
|
|
5
|
+
//
|
|
6
|
+
// this.userRepo.save(taintedInput) // a sink (writes user data)
|
|
7
|
+
// this.logger.save(taintedInput) // NOT a sink (logs are not user data)
|
|
8
|
+
//
|
|
9
|
+
// Both calls hit the same `save()` summary today, so the engine either
|
|
10
|
+
// over-fires (treats logger.save as a sink) or under-fires (misses
|
|
11
|
+
// userRepo.save). Receiver-sensitivity adds a third key dimension: the
|
|
12
|
+
// inferred class of the receiver.
|
|
13
|
+
//
|
|
14
|
+
// This module is a thin helper that:
|
|
15
|
+
// 1. extracts the receiver-type hint at a call site (using CHA), and
|
|
16
|
+
// 2. mixes it into the summary cache key for the callee
|
|
17
|
+
//
|
|
18
|
+
// The actual engine integration (using these helpers) lives in engine.js.
|
|
19
|
+
|
|
20
|
+
import * as crypto from 'node:crypto';
|
|
21
|
+
import { classOfVar } from '../ir/class-hierarchy.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Return the receiver-type label for a call expression, or null if
|
|
25
|
+
* we have no type information.
|
|
26
|
+
*
|
|
27
|
+
* foo.bar() -> typeOfVar(foo) or 'foo'
|
|
28
|
+
* this.userRepo.save(x) -> 'UserRepo' (heuristic from CHA)
|
|
29
|
+
* bareIdentCall(x) -> null
|
|
30
|
+
*/
|
|
31
|
+
export function receiverTypeAtCall(node, fn, file, cha) {
|
|
32
|
+
if (!node || node.kind !== 'call') return null;
|
|
33
|
+
const callee = node.callee;
|
|
34
|
+
if (!callee || typeof callee !== 'string') return null;
|
|
35
|
+
// String form like "this.userRepo.save" or "userRepo.save"
|
|
36
|
+
const parts = callee.split('.');
|
|
37
|
+
if (parts.length < 2) return null; // bareIdentCall — no receiver
|
|
38
|
+
// The receiver chain is parts[0..parts.length-2]. We try to type the
|
|
39
|
+
// outermost identifier first; if it's `this` we look at the field name.
|
|
40
|
+
if (parts[0] === 'this') {
|
|
41
|
+
// For `this.userRepo.save`, the receiver type hint is the FIELD name —
|
|
42
|
+
// we conventionally PascalCase it ("UserRepo"). v1 heuristic only.
|
|
43
|
+
if (parts.length >= 3) {
|
|
44
|
+
const fieldName = parts[1];
|
|
45
|
+
return fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
|
|
46
|
+
}
|
|
47
|
+
return 'this';
|
|
48
|
+
}
|
|
49
|
+
// Try to resolve `foo.save` — type of `foo` from CHA.
|
|
50
|
+
const inferred = classOfVar(cha, file, fn?.qid, parts[0]);
|
|
51
|
+
if (inferred) return inferred;
|
|
52
|
+
// Fall back to the LHS identifier name as a soft label.
|
|
53
|
+
return parts[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute a stable hash for a receiver type — used as part of the
|
|
58
|
+
* extended summary cache key.
|
|
59
|
+
*/
|
|
60
|
+
export function hashReceiverType(receiverType) {
|
|
61
|
+
if (!receiverType) return 'no-recv';
|
|
62
|
+
return crypto.createHash('sha256').update(String(receiverType)).digest('hex').slice(0, 8);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extend an existing cache key with a receiver-type dimension.
|
|
67
|
+
*
|
|
68
|
+
* priorKey = "<qid>::<state-hash>"
|
|
69
|
+
* newKey = "<qid>::<state-hash>::<recv-hash>"
|
|
70
|
+
*
|
|
71
|
+
* Backwards-compatible: when receiverType is falsy, the key is unchanged
|
|
72
|
+
* up to the suffix sentinel "no-recv".
|
|
73
|
+
*/
|
|
74
|
+
export function keyWithReceiver(baseKey, receiverType) {
|
|
75
|
+
return `${baseKey}::${hashReceiverType(receiverType)}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Sanitizer-validity proofs (P4.2).
|
|
2
|
+
//
|
|
3
|
+
// The taint engine trusts any catalog-registered sanitizer to neutralize
|
|
4
|
+
// the threat. Real projects ship their own sanitizers — `sanitize(x)`,
|
|
5
|
+
// `clean(input)`, `validate(s)` — and the catalog matches them by NAME.
|
|
6
|
+
// But a function called `sanitize` that just does `return input.trim()`
|
|
7
|
+
// does NOT sanitize XSS; trusting it produces false negatives at scale.
|
|
8
|
+
//
|
|
9
|
+
// This module verifies, before the engine treats a project-local function
|
|
10
|
+
// as a sanitizer, that its body actually performs the required check for
|
|
11
|
+
// the CWE it claims to mitigate. Per-CWE shape rules:
|
|
12
|
+
//
|
|
13
|
+
// xss: body must call escape | DOMPurify.sanitize | bleach.clean
|
|
14
|
+
// | str.replace(/<[^>]+>/g, ...) | textContent assignment
|
|
15
|
+
// sql: body must call .prepare | .bind | parameterized query
|
|
16
|
+
// path-trav: body must call path.resolve + assertion against base dir
|
|
17
|
+
// ssrf: body must check scheme/host against allow-list
|
|
18
|
+
// open-redir: body must check scheme/host against allow-list
|
|
19
|
+
// url: body must call encodeURIComponent / encodeURI
|
|
20
|
+
// cmd: body must call shellEscape / shlex.quote / spawn with argv
|
|
21
|
+
//
|
|
22
|
+
// Public API:
|
|
23
|
+
// isValidSanitizerFor(fnBody, cweFamily)
|
|
24
|
+
// → { trusted: bool, reason: string }
|
|
25
|
+
//
|
|
26
|
+
// verifyProjectSanitizers(perFileIR, catalogEntries)
|
|
27
|
+
// → produces a new catalog set where untrusted local sanitizers are
|
|
28
|
+
// DEMOTED to "noop" (no strip effect); trusted ones stay.
|
|
29
|
+
|
|
30
|
+
const _SHAPE_RULES = {
|
|
31
|
+
'xss': [
|
|
32
|
+
{ re: /\b(?:DOMPurify\.sanitize|sanitizeHtml|bleach\.clean|escapeHtml|html_escape|htmlEscape|encodeHTML|escapeAll)\b/, label: 'HTML-escaping library call' },
|
|
33
|
+
{ re: /\.replace\s*\(\s*\/[<>"'&]/, label: 'inline HTML-special character replace' },
|
|
34
|
+
{ re: /textContent\s*=/, label: 'textContent assignment' },
|
|
35
|
+
],
|
|
36
|
+
'sql': [
|
|
37
|
+
{ re: /\.(?:prepare|bind|bindParam|execute)\s*\(/, label: 'parameterized query call' },
|
|
38
|
+
{ re: /\b(?:placeholder|\?|\$\d)\b.*?(?:select|insert|update|delete)/i, label: 'placeholder in SQL string' },
|
|
39
|
+
],
|
|
40
|
+
'path-trav': [
|
|
41
|
+
{ re: /\bpath\.resolve\b[\s\S]{0,200}\.startsWith\s*\(/, label: 'path.resolve + startsWith allow-list check' },
|
|
42
|
+
{ re: /\b(?:realpath|os\.path\.realpath|pathlib\.Path[\s\S]{0,40}\.resolve)\b/, label: 'canonicalization' },
|
|
43
|
+
{ re: /\.includes\s*\(\s*['"]\.\.['"]\s*\)/, label: 'dotdot string check' },
|
|
44
|
+
],
|
|
45
|
+
'ssrf': [
|
|
46
|
+
{ re: /\b(?:allowedHosts?|allowed_hosts?|hostWhitelist|allowedSchemes?)\b/, label: 'allow-list constant reference' },
|
|
47
|
+
{ re: /\.host\s*===?\s*['"][^'"]+['"]/, label: 'literal host comparison' },
|
|
48
|
+
{ re: /\b(?:169\.254\.169\.254|127\.0\.0\.0\/8|RFC1918|10\.0\.0\.0|172\.16\.0\.0|192\.168\.0\.0)\b/, label: 'metadata / RFC1918 deny-list' },
|
|
49
|
+
],
|
|
50
|
+
'open-redir': [
|
|
51
|
+
{ re: /\b(?:allowedRedirects?|safeRedirects?|allowedHosts?|trustedDomains?)\b/, label: 'allow-list constant reference' },
|
|
52
|
+
{ re: /\.host\s*===?\s*['"][^'"]+['"]/, label: 'literal host comparison' },
|
|
53
|
+
],
|
|
54
|
+
'url': [
|
|
55
|
+
{ re: /\b(?:encodeURIComponent|encodeURI|urllib\.parse\.quote|urlencode)\b/, label: 'URL encoder call' },
|
|
56
|
+
],
|
|
57
|
+
'cmd': [
|
|
58
|
+
{ re: /\b(?:shellEscape|shlex\.quote|Shellwords\.escape|escapeshellarg)\b/, label: 'shell-escape library call' },
|
|
59
|
+
{ re: /\.spawn\s*\(\s*['"][^'"]+['"]\s*,\s*\[/, label: 'spawn with argv array' },
|
|
60
|
+
{ re: /\bsubprocess\.run\s*\(\s*\[[^\]]*\]\s*,/, label: 'subprocess.run with list arg' },
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const _CWE_TO_FAMILY = {
|
|
65
|
+
'CWE-79': 'xss', 'CWE-80': 'xss', 'CWE-81': 'xss', 'CWE-83': 'xss',
|
|
66
|
+
'CWE-89': 'sql',
|
|
67
|
+
'CWE-22': 'path-trav', 'CWE-23': 'path-trav', 'CWE-36': 'path-trav',
|
|
68
|
+
'CWE-918': 'ssrf',
|
|
69
|
+
'CWE-601': 'open-redir',
|
|
70
|
+
'CWE-78': 'cmd',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Verify that a function body satisfies the shape rule for the given
|
|
75
|
+
* vulnerability family. Returns `{ trusted, reason }`.
|
|
76
|
+
*
|
|
77
|
+
* fnBody: the function's source text (post-comment-strip ideally)
|
|
78
|
+
* family: one of the keys of _SHAPE_RULES (or a CWE id we map)
|
|
79
|
+
*/
|
|
80
|
+
export function isValidSanitizerFor(fnBody, family) {
|
|
81
|
+
if (!fnBody || typeof fnBody !== 'string') return { trusted: false, reason: 'no body' };
|
|
82
|
+
if (!family) return { trusted: false, reason: 'no family' };
|
|
83
|
+
// Map CWE id to family if needed.
|
|
84
|
+
const fam = _CWE_TO_FAMILY[family] || family;
|
|
85
|
+
const rules = _SHAPE_RULES[fam];
|
|
86
|
+
if (!rules) return { trusted: false, reason: `no shape rule for family '${fam}'` };
|
|
87
|
+
for (const r of rules) {
|
|
88
|
+
if (r.re.test(fnBody)) return { trusted: true, reason: `matched: ${r.label}` };
|
|
89
|
+
}
|
|
90
|
+
return { trusted: false, reason: `body does not match any known ${fam} shape pattern` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Walk the project IR and verify every project-local function that's
|
|
95
|
+
* registered as a sanitizer in the catalog. Returns an array of
|
|
96
|
+
* { fnQid, family, trusted, reason }
|
|
97
|
+
* The engine consumer can demote untrusted entries from the catalog at
|
|
98
|
+
* runtime by removing their `effect: 'strip'` flag.
|
|
99
|
+
*/
|
|
100
|
+
export function verifyProjectSanitizers(perFileIR, catalog) {
|
|
101
|
+
const out = [];
|
|
102
|
+
if (!perFileIR || !Array.isArray(catalog)) return out;
|
|
103
|
+
// Index project functions by short name.
|
|
104
|
+
const fnByName = new Map();
|
|
105
|
+
for (const ir of Object.values(perFileIR)) {
|
|
106
|
+
for (const fn of (ir.functions || [])) {
|
|
107
|
+
const short = fn.name || (fn.qid || '').split('::').pop();
|
|
108
|
+
if (!short) continue;
|
|
109
|
+
if (!fnByName.has(short)) fnByName.set(short, []);
|
|
110
|
+
fnByName.get(short).push(fn);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const entry of catalog) {
|
|
114
|
+
if (entry.kind !== 'sanitizer') continue;
|
|
115
|
+
if (entry.match?.type !== 'call') continue;
|
|
116
|
+
const calleeName = entry.match.callee;
|
|
117
|
+
if (!calleeName) continue;
|
|
118
|
+
const fns = fnByName.get(calleeName);
|
|
119
|
+
if (!fns || !fns.length) continue; // not a project-local sanitizer
|
|
120
|
+
for (const fn of fns) {
|
|
121
|
+
const bodyText = _stringifyCfgBody(fn);
|
|
122
|
+
const family = (entry.appliesTo && entry.appliesTo[0]) || '*';
|
|
123
|
+
const verdict = isValidSanitizerFor(bodyText, family);
|
|
124
|
+
out.push({ fnQid: fn.qid, family, trusted: verdict.trusted, reason: verdict.reason });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _stringifyCfgBody(fn) {
|
|
131
|
+
// Reconstruct a rough textual representation of the function body from
|
|
132
|
+
// its CFG nodes — sufficient for regex shape matching.
|
|
133
|
+
const parts = [];
|
|
134
|
+
const nodes = fn.cfg?.nodes || {};
|
|
135
|
+
for (const id of Object.keys(nodes)) {
|
|
136
|
+
const n = nodes[id];
|
|
137
|
+
if (!n) continue;
|
|
138
|
+
if (n.kind === 'call') parts.push(`${n.callee || '?'}(${(n.args || []).length} args)`);
|
|
139
|
+
if (n.kind === 'assign') parts.push(`${n.target} = ${_exprStr(n.source)}`);
|
|
140
|
+
if (n.kind === 'return') parts.push(`return ${_exprStr(n.value)}`);
|
|
141
|
+
}
|
|
142
|
+
return parts.join('\n');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _exprStr(e) {
|
|
146
|
+
if (!e) return '';
|
|
147
|
+
if (e.kind === 'literal') return String(e.value);
|
|
148
|
+
if (e.kind === 'ident') return e.name;
|
|
149
|
+
if (e.kind === 'member') return `${_exprStr(e.object)}.${e.prop}`;
|
|
150
|
+
if (e.kind === 'call') return `${typeof e.callee === 'string' ? e.callee : _exprStr(e.callee)}(...)`;
|
|
151
|
+
if (e.kind === 'binary' || e.kind === 'logical') return `${_exprStr(e.left)} ${e.op || '?'} ${_exprStr(e.right)}`;
|
|
152
|
+
if (e.kind === 'tpl') return '`${...}`';
|
|
153
|
+
return e.kind;
|
|
154
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Probabilistic / soft taint (v0.70 #6).
|
|
2
|
+
//
|
|
3
|
+
// Today taint is binary: a value is either tainted or clean. Sanitizers
|
|
4
|
+
// clear taint entirely. Reality: many sanitizers reduce but don't eliminate
|
|
5
|
+
// exploitation probability. `escape_html()` blocks reflected XSS but
|
|
6
|
+
// leaves attribute-context XSS open. `Number(x)` blocks SQL/XSS for numeric
|
|
7
|
+
// columns but does nothing for text columns.
|
|
8
|
+
//
|
|
9
|
+
// Soft taint carries a [0,1] probability through the path:
|
|
10
|
+
// - Source emits at p = 1.0 (fully tainted)
|
|
11
|
+
// - Each sanitizer in the path multiplies by (1 - effectiveness)
|
|
12
|
+
// - Threshold gates the final emission: findings below
|
|
13
|
+
// AGENTIC_SECURITY_SOFT_TAINT_THRESHOLD (default 0.5) get demoted to
|
|
14
|
+
// low-confidence rather than dropped
|
|
15
|
+
//
|
|
16
|
+
// This module annotates AFTER the taint engine runs. It walks each
|
|
17
|
+
// finding's trace + chain, looks up sanitizer effectiveness from the
|
|
18
|
+
// catalog, and emits `f.taintProbability` + `f.taintProbabilityWhy`.
|
|
19
|
+
//
|
|
20
|
+
// Engine-level lattice extension to {tainted, p} is v0.71. For v0.70 the
|
|
21
|
+
// post-pass shape captures the high-value case (sanitizer-in-path
|
|
22
|
+
// downweighting) without rewriting the core lattice.
|
|
23
|
+
|
|
24
|
+
import { CATALOG } from './catalog.js';
|
|
25
|
+
|
|
26
|
+
// Hand-curated effectiveness. 1.0 = full block; 0.0 = no effect.
|
|
27
|
+
// Conservative — when uncertain, lean toward 0.9 so findings don't
|
|
28
|
+
// silently disappear.
|
|
29
|
+
const DEFAULT_EFFECTIVENESS = {
|
|
30
|
+
// Strong sanitizers — proven by spec to block the family.
|
|
31
|
+
'DOMPurify.sanitize': 0.98,
|
|
32
|
+
'sanitize': 0.95,
|
|
33
|
+
'escape': 0.85, // depends on context
|
|
34
|
+
'htmlspecialchars': 0.90,
|
|
35
|
+
'encodeURIComponent': 0.99,
|
|
36
|
+
'encodeURI': 0.95,
|
|
37
|
+
'JSON.stringify': 0.92, // blocks most code-injection but not all
|
|
38
|
+
'parameterize': 1.00,
|
|
39
|
+
'AddWithValue': 1.00,
|
|
40
|
+
'addWithValue': 1.00,
|
|
41
|
+
'setString': 1.00,
|
|
42
|
+
'setInt': 1.00,
|
|
43
|
+
'setLong': 1.00,
|
|
44
|
+
'bindParam': 1.00,
|
|
45
|
+
'bindValue': 1.00,
|
|
46
|
+
'quote_plus': 0.99,
|
|
47
|
+
'escape_filter_chars':0.97, // LDAP
|
|
48
|
+
'shlex.quote': 0.99,
|
|
49
|
+
// Numeric coercion — blocks injection of non-numeric metacharacters.
|
|
50
|
+
'parseInt': 0.95,
|
|
51
|
+
'parseFloat': 0.95,
|
|
52
|
+
'Number': 0.90,
|
|
53
|
+
'toInt': 0.95,
|
|
54
|
+
// Weak / context-dependent.
|
|
55
|
+
'trim': 0.05,
|
|
56
|
+
'toLowerCase': 0.05,
|
|
57
|
+
'toUpperCase': 0.05,
|
|
58
|
+
'replace': 0.30, // depends entirely on the regex
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Look up sanitizer effectiveness for a callee. Falls back to catalog
|
|
63
|
+
* entries with `sanitizerEffectiveness` field; otherwise uses the
|
|
64
|
+
* curated DEFAULT_EFFECTIVENESS table; otherwise returns null (unknown,
|
|
65
|
+
* no downweight applied).
|
|
66
|
+
*/
|
|
67
|
+
export function effectivenessFor(callee) {
|
|
68
|
+
if (!callee || typeof callee !== 'string') return null;
|
|
69
|
+
// Tail of dotted callee.
|
|
70
|
+
const tail = callee.split('.').pop();
|
|
71
|
+
// Look in catalog first.
|
|
72
|
+
for (const e of CATALOG) {
|
|
73
|
+
if (e.kind !== 'sanitizer') continue;
|
|
74
|
+
if (typeof e.sanitizerEffectiveness !== 'number') continue;
|
|
75
|
+
if (e.match && e.match.callee === callee) return e.sanitizerEffectiveness;
|
|
76
|
+
if (e.match && e.match.callee === tail) return e.sanitizerEffectiveness;
|
|
77
|
+
}
|
|
78
|
+
if (callee in DEFAULT_EFFECTIVENESS) return DEFAULT_EFFECTIVENESS[callee];
|
|
79
|
+
if (tail in DEFAULT_EFFECTIVENESS) return DEFAULT_EFFECTIVENESS[tail];
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Compute residual taint probability for a finding by walking its
|
|
85
|
+
* trace + chain, looking up each callee's effectiveness, and applying
|
|
86
|
+
* product of (1 - effectiveness).
|
|
87
|
+
*
|
|
88
|
+
* Returns { p, why: [...] } where why lists which sanitizers contributed.
|
|
89
|
+
*/
|
|
90
|
+
export function computeSoftTaintProbability(finding) {
|
|
91
|
+
let p = 1.0;
|
|
92
|
+
const why = [];
|
|
93
|
+
const trace = Array.isArray(finding.trace) ? finding.trace : [];
|
|
94
|
+
const chain = Array.isArray(finding.chain) ? finding.chain : [];
|
|
95
|
+
const pathCalls = Array.isArray(finding.pathSteps) ? finding.pathSteps : [];
|
|
96
|
+
const all = [...trace, ...chain, ...pathCalls];
|
|
97
|
+
for (const step of all) {
|
|
98
|
+
const callee = step.callee || step.label;
|
|
99
|
+
if (!callee) continue;
|
|
100
|
+
const eff = effectivenessFor(callee);
|
|
101
|
+
if (eff == null) continue;
|
|
102
|
+
p *= Math.max(0, Math.min(1, 1 - eff));
|
|
103
|
+
why.push({ callee, effectiveness: eff });
|
|
104
|
+
if (p < 1e-6) break;
|
|
105
|
+
}
|
|
106
|
+
return { p, why };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Annotate every IR-TAINT finding with `taintProbability` and
|
|
111
|
+
* `taintProbabilityWhy`. Findings below
|
|
112
|
+
* AGENTIC_SECURITY_SOFT_TAINT_THRESHOLD (default 0.5) get demoted to
|
|
113
|
+
* lower severity but are NOT dropped — auditors see the demotion +
|
|
114
|
+
* the sanitizer that earned it.
|
|
115
|
+
*/
|
|
116
|
+
export function annotateSoftTaint(findings, opts = {}) {
|
|
117
|
+
if (!Array.isArray(findings) || findings.length === 0) return findings;
|
|
118
|
+
const threshold = Number(opts.threshold ?? process.env.AGENTIC_SECURITY_SOFT_TAINT_THRESHOLD) || 0.5;
|
|
119
|
+
let demoted = 0;
|
|
120
|
+
for (const f of findings) {
|
|
121
|
+
if (!f || f.parser !== 'IR-TAINT') continue;
|
|
122
|
+
const r = computeSoftTaintProbability(f);
|
|
123
|
+
f.taintProbability = r.p;
|
|
124
|
+
f.taintProbabilityWhy = r.why;
|
|
125
|
+
if (r.p < threshold) {
|
|
126
|
+
f._softTaintDemoted = true;
|
|
127
|
+
f._softTaintOriginalSeverity = f.severity;
|
|
128
|
+
const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
|
|
129
|
+
if (downgrade[f.severity]) f.severity = downgrade[f.severity];
|
|
130
|
+
demoted++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
Object.defineProperty(findings, '_softTaintStats', {
|
|
134
|
+
value: { demoted, threshold },
|
|
135
|
+
enumerable: false,
|
|
136
|
+
});
|
|
137
|
+
return findings;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const _internal = { DEFAULT_EFFECTIVENESS };
|