@clear-capabilities/agentic-security-scanner 0.86.0 → 0.119.1
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 +660 -0
- package/bin/agentic-security.js +78 -1
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/415.index.js +90 -0
- package/dist/444.index.js +89 -0
- package/dist/503.index.js +288 -0
- package/dist/637.index.js +15 -1
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +5 -1
- package/dist/agentic-security.mjs +84 -84
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +21 -10
- package/src/dataflow/CLAUDE.md +18 -6
- package/src/dataflow/catalog.js +59 -11
- package/src/dataflow/engine.js +106 -5
- package/src/dataflow/implicit-flow.js +4 -2
- package/src/dataflow/index.js +17 -4
- package/src/dataflow/proof-gate.js +100 -0
- package/src/dataflow/summaries.js +63 -6
- package/src/engine.js +292 -10
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/callgraph.js +12 -1
- package/src/ir/parser-go.js +8 -0
- package/src/ir/tree-sitter-loader.js +108 -0
- package/src/mcp/tools.js +6 -2
- package/src/posture/corpus-status.js +74 -0
- package/src/posture/coverage-report.js +113 -0
- package/src/posture/fix-history.js +24 -0
- package/src/posture/holdout-eval.js +57 -0
- package/src/posture/provenance.js +39 -0
- package/src/posture/router.js +33 -3
- package/src/posture/sca-verdict.js +130 -0
- package/src/posture/secret-history.js +72 -0
- package/src/pr-delta.js +13 -0
- package/src/report/index.js +67 -0
- package/src/sast/CLAUDE.md +9 -3
- package/src/sast/agent-untrusted-flow.js +107 -0
- package/src/sast/api-authz.js +71 -0
- package/src/sast/code-injection-multilang.js +117 -0
- package/src/sast/cross-service.js +98 -0
- package/src/sast/crypto-protocol.js +43 -1
- package/src/sast/csharp-structural.js +65 -0
- package/src/sast/csharp.js +3 -0
- package/src/sast/csrf.js +24 -3
- package/src/sast/csv-injection.js +84 -0
- package/src/sast/event-entrypoint.js +90 -0
- package/src/sast/frontend-hygiene.js +106 -0
- package/src/sast/go-structural.js +60 -0
- package/src/sast/iac-terraform.js +107 -0
- package/src/sast/java-structural.js +81 -0
- package/src/sast/js-framework-structural.js +115 -0
- package/src/sast/kotlin.js +72 -0
- package/src/sast/ldap-injection.js +73 -17
- package/src/sast/open-redirect.js +47 -5
- package/src/sast/php.js +35 -0
- package/src/sast/python-structural.js +83 -0
- package/src/sast/rbac-consistency.js +113 -0
- package/src/sast/redos-nfa.js +75 -0
- package/src/sast/response-splitting.js +48 -19
- package/src/sast/ruby.js +38 -0
- package/src/sast/secret-concat.js +60 -0
- package/src/sast/ssrf-cloud-metadata.js +13 -0
- package/src/sast/stored-taint.js +88 -0
- package/src/sast/tree-sitter-sinks.js +62 -0
- package/src/sast/weak-password-hash.js +28 -0
- package/src/sast/weak-randomness.js +46 -5
- package/src/sast/wrong-context-sanitizer.js +149 -0
- package/src/sast/xpath-injection.js +30 -6
- package/src/sast/xss-reflected-multilang.js +116 -0
- package/src/sast/xxe.js +67 -0
- package/src/sca/image-packages.js +74 -0
- package/src/sca/import-reachability.js +271 -0
- package/src/sca/install-script-analysis.js +82 -0
- package/bin/.agentic-security/findings.json +0 -1907
- package/bin/.agentic-security/last-scan.json +0 -1907
- package/bin/.agentic-security/last-scan.json.sig +0 -1
- package/bin/.agentic-security/scan-history.json +0 -166
- package/bin/.agentic-security/streak.json +0 -20
- package/src/.agentic-security/findings.json +0 -117732
- package/src/.agentic-security/last-scan.json +0 -117732
- package/src/.agentic-security/last-scan.json.sig +0 -1
- package/src/.agentic-security/scan-history.json +0 -12946
- package/src/.agentic-security/streak.json +0 -21
- package/src/dataflow/.agentic-security/findings.json +0 -6086
- package/src/dataflow/.agentic-security/last-scan.json +0 -6086
- package/src/dataflow/.agentic-security/last-scan.json.sig +0 -1
- package/src/dataflow/.agentic-security/scan-history.json +0 -250
- package/src/dataflow/.agentic-security/streak.json +0 -21
- package/src/ir/.agentic-security/findings.json +0 -4011
- package/src/ir/.agentic-security/last-scan.json +0 -4011
- package/src/ir/.agentic-security/last-scan.json.sig +0 -1
- package/src/ir/.agentic-security/scan-history.json +0 -193
- package/src/ir/.agentic-security/streak.json +0 -20
- package/src/mcp/.agentic-security/findings.json +0 -8632
- package/src/mcp/.agentic-security/last-scan.json +0 -8632
- package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
- package/src/mcp/.agentic-security/scan-history.json +0 -331
- package/src/mcp/.agentic-security/streak.json +0 -20
- package/src/posture/.agentic-security/dpia.md +0 -26
- package/src/posture/.agentic-security/findings.json +0 -77181
- package/src/posture/.agentic-security/last-scan.json +0 -77181
- package/src/posture/.agentic-security/last-scan.json.sig +0 -1
- package/src/posture/.agentic-security/pqc-migration-plan.json +0 -65
- package/src/posture/.agentic-security/pqc-migration-plan.md +0 -30
- package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +0 -6
- package/src/posture/.agentic-security/scan-history.json +0 -8904
- package/src/posture/.agentic-security/streak.json +0 -21
- package/src/posture/.agentic-security/threat-model.json +0 -2038
- package/src/posture/.agentic-security/threat-model.md +0 -73
- package/src/posture/compliance-frameworks/.agentic-security/findings.json +0 -80
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json +0 -80
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json.sig +0 -1
- package/src/posture/compliance-frameworks/.agentic-security/scan-history.json +0 -90
- package/src/posture/compliance-frameworks/.agentic-security/streak.json +0 -22
- package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +0 -6
- package/src/report/.agentic-security/threat-model.json +0 -7
- package/src/report/.agentic-security/threat-model.md +0 -22
- package/src/sast/.agentic-security/findings.json +0 -6154
- package/src/sast/.agentic-security/last-scan.json +0 -6154
- package/src/sast/.agentic-security/last-scan.json.sig +0 -1
- package/src/sast/.agentic-security/scan-history.json +0 -941
- package/src/sast/.agentic-security/streak.json +0 -22
- package/src/sca/.agentic-security/findings.json +0 -7460
- package/src/sca/.agentic-security/last-scan.json +0 -7460
- package/src/sca/.agentic-security/last-scan.json.sig +0 -1
- package/src/sca/.agentic-security/scan-history.json +0 -113
- package/src/sca/.agentic-security/streak.json +0 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
40d6882dc7b5dbd62ac41f10cb01ec4e43fa42081dbcc0427553e0282748fedb agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Scanner engine for the agentic-security Claude Code plugin
|
|
3
|
+
"version": "0.119.1",
|
|
4
|
+
"description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
"src/",
|
|
28
28
|
"bin/",
|
|
29
29
|
"dist/",
|
|
30
|
-
"CHANGELOG.md"
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"!**/.agentic-security",
|
|
32
|
+
"!**/.agentic-security/**"
|
|
31
33
|
],
|
|
32
34
|
"engines": {
|
|
33
35
|
"node": ">=24.0.0"
|
|
@@ -41,6 +43,10 @@
|
|
|
41
43
|
"js-yaml": "^4.1.1",
|
|
42
44
|
"safe-regex": "^2.1.1"
|
|
43
45
|
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"web-tree-sitter": "0.20.8",
|
|
48
|
+
"tree-sitter-wasms": "0.1.13"
|
|
49
|
+
},
|
|
44
50
|
"devDependencies": {
|
|
45
51
|
"@types/node": "^24.0.0",
|
|
46
52
|
"@vercel/ncc": "^0.38.3"
|
|
@@ -50,17 +56,20 @@
|
|
|
50
56
|
"lodash-es": "^4.18.1"
|
|
51
57
|
},
|
|
52
58
|
"scripts": {
|
|
53
|
-
"build": "ncc build bin/agentic-security.js -o dist --minify && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
54
|
-
"prepublishOnly": "npm run build && node
|
|
55
|
-
"test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
59
|
+
"build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
60
|
+
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs",
|
|
61
|
+
"test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && npm run test:eval && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
56
62
|
"test:smoke": "node --test test/smoke.test.js",
|
|
57
|
-
"test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js",
|
|
58
|
-
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js",
|
|
59
|
-
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js",
|
|
63
|
+
"test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js",
|
|
64
|
+
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js",
|
|
65
|
+
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js",
|
|
60
66
|
"test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js",
|
|
61
|
-
"test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js",
|
|
67
|
+
"test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js",
|
|
62
68
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js",
|
|
63
69
|
"test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js && node ../scripts/lint-command-descriptions.mjs",
|
|
70
|
+
"test:eval": "node --test test/independent-eval.test.js",
|
|
71
|
+
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
72
|
+
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
64
73
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
|
65
74
|
"prebench": "npm run build",
|
|
66
75
|
"bench": "node test/benchmark/bench.js",
|
|
@@ -70,6 +79,8 @@
|
|
|
70
79
|
"bench:llm-goats": "node test/benchmark/llm-goats/bench-llm-goats.js",
|
|
71
80
|
"bench:cve-replay": "node ../bench/cve-replay/runner.mjs",
|
|
72
81
|
"bench:cve-replay:ci": "node ../bench/cve-replay/runner.mjs --ci",
|
|
82
|
+
"bench:cve-replay:check": "node ../bench/cve-replay/runner.mjs --check-baseline",
|
|
83
|
+
"bench:cve-replay:update-baseline": "node ../bench/cve-replay/runner.mjs --update-baseline",
|
|
73
84
|
"bench:bigquery:smoke": "node ../bench/bigquery-github/bin/run-cycle.mjs --smoke",
|
|
74
85
|
"bench:bigquery": "node ../bench/bigquery-github/bin/run-cycle.mjs",
|
|
75
86
|
"bench:agent-tasks": "node ../bench/agent-tasks/security-fixer/runner.mjs",
|
package/src/dataflow/CLAUDE.md
CHANGED
|
@@ -5,23 +5,34 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
|
|
|
5
5
|
## Scope — what we actually model
|
|
6
6
|
|
|
7
7
|
- **Intra-procedural field-sensitive taint** with access-path lattice (`access-paths.js`). `user.email` is distinguishable from `user.password`.
|
|
8
|
-
- **
|
|
8
|
+
- **Value-context-sensitive interprocedural taint (FR-SEM-2).** `SummaryCache` (`summaries.js`) holds a distinct summary per distinct entry-taint-state. A pre-pass computes the empty-entry base for every function; call sites then lazily compute the summary under their actual tainted-arg context (at both assign-from-call and plain-call sites) so a helper that is clean with clean args but tainted with user input is detected per call site. Bounded by a per-function context cap (`AGENTIC_SECURITY_KCFA_MAX_CONTEXTS`, default 16; 0 = monovariant). Over the cap → reuse the empty-entry summary.
|
|
9
9
|
- **Catalog-driven source/sink/sanitizer matching.** Add entries in `catalog.js`. Each entry: `kind` ∈ {source, sink, sanitizer}, plus language + framework + match shape. 200+ entries spanning Express/Flask/FastAPI/Django/Rails/PHP/Go-net-http/Gin/Echo.
|
|
10
10
|
- **Path feasibility.** Constant-folds `if` conditions to prune unreachable branches.
|
|
11
11
|
- **Per-flow source attribution.** Sources reported on a finding are the ones actually reaching the sink argument (via free-var matching in the sink expression), NOT the first source the worklist happened to see. Premortem-derived.
|
|
12
12
|
|
|
13
|
-
## Scope —
|
|
13
|
+
## Scope — now modelled (was previously listed as gaps; closed in v0.66)
|
|
14
14
|
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
15
|
+
- **Mutated-parameter taint at call sites.** `engine.js` consults `SummaryCache.applyAtCallSite` at both assign-from-call and plain-call sites: a callee that mutates a param (e.g. `Object.assign(target, tainted)`, `_.merge`) taints the caller's argument variable. Covered by `test/interproc-k2.test.js`.
|
|
16
|
+
- **Higher-order taint flow.** `_higherOrderInvocations` recorded during `analyzeFunction` are consumed in `runTaintEngine`: the callback is resolved, analyzed with a tainted parameter, and its findings merged back into the caller (`_via: 'higher-order'`, capped at `HO_CAP`). Covered by `test/closure-capture.test.js` and `test/phase6-taint.test.js`.
|
|
17
|
+
- **Recursion via fixed point.** `runTaintEngine` runs a multi-pass fixed-point loop (`MAX_FP_ITERS=3`) until the summary cache stabilizes, so recursive cycles and call chains converge instead of under-approximating on a single pass.
|
|
18
|
+
|
|
19
|
+
## Scope — what we still do NOT model (today)
|
|
20
|
+
|
|
21
|
+
- **Call-string (k>1) context-sensitivity.** Context is the *value* abstraction — which params are tainted at entry — not the call stack. Two call paths that reach a helper with the same tainted-arg shape share a summary. Entry-state granularity is also param-level, not arbitrary access paths (`f(obj)` with `obj.a` tainted ≡ `obj.b` tainted).
|
|
22
|
+
- **Contexts beyond the per-function cap.** Once a function has been computed under `AGENTIC_SECURITY_KCFA_MAX_CONTEXTS` distinct tainted-arg shapes, further shapes fall back to the empty-entry summary (an under-approximation, bounded on purpose).
|
|
18
23
|
- **Implicit flow.** `implicit-flow.js` exists for `if (tainted) { x = "yes" }` propagation but is conservative-by-default.
|
|
19
24
|
|
|
25
|
+
## Precision: centralized SSRF/path guard recognition
|
|
26
|
+
|
|
27
|
+
`engine.js` `dropGuardedFindings(findings, fc)` runs after all detectors and drops a CWE-918 (SSRF) finding when the sink window has a host allow/deny check (deny/allow-list, `getHost`/`hostname` comparison, RFC1918/`169.254.169.254` prefix check, `ipaddress`/`getaddrinfo`/`ssrf-req-filter`), or a CWE-22 (path) finding when the window has a containment guard (`basename`/`GetFileName`/`secure_filename`/`send_from_directory`, or canonicalize+`startsWith`). It's the single source of truth so every emitter (regex, structural, per-language flow, PY-SAST, CSHARP, GO) is treated uniformly. The window is **comment-stripped** (a "no allow-list / 169.254…" vuln comment must not read as a guard). Opt out: `AGENTIC_SECURITY_NO_GUARD_RECOGNITION=1`.
|
|
28
|
+
|
|
20
29
|
## Entry points
|
|
21
30
|
|
|
22
|
-
- `runTaintEngine(perFileIR, callGraph, opts)` — the public entry.
|
|
31
|
+
- `runTaintEngine(perFileIR, callGraph, opts)` — the public entry. Runs from `engine.js` when `AGENTIC_SECURITY_DEEP=1`. **R1 (PRD §5):** the CLI entry (`bin/agentic-security.js#cmdScan`) sets that env var by default for local/interactive scans (not in CI, and not when `--no-deep`/`AGENTIC_SECURITY_DEEP=0`), so the default `/scan --all` runs deep. In-process callers (tests, the cve-replay corpus) invoke `runScan()` directly without the CLI default and therefore stay deep-off unless they set the env var themselves (e.g. `test/deep-taint.test.js`).
|
|
23
32
|
- `applyPathFeasibility` — constant-fold pass that runs before the worklist.
|
|
24
33
|
- `annotateBackwardSlices` — backward-slice annotation for already-emitted findings.
|
|
34
|
+
- `annotateProvenClean(findings, perFileIR)` (`proven-clean.js`) — proves a SQL sink is reached only through a parameterizer; sets `provenClean`. Wired by default in `runDeepAnalysis` (opt out: `AGENTIC_SECURITY_NO_PROOF_GATE=1`).
|
|
35
|
+
- `annotateProofGate(findings)` (`proof-gate.js`) — the precision gate. Consolidates `provenClean` + `_provenUnreachable` into one `finding.proof = { verdict, reasons }` and applies a **recall-preserving demotion** (lowers `confidence` + `confidenceTier` + `exploitabilityTier`, never `severity`). Runs in `engine.js` after confidence/exploitability, before mitigation/composite-risk. Default on.
|
|
25
36
|
|
|
26
37
|
## Configuration / opt-in
|
|
27
38
|
|
|
@@ -29,6 +40,7 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
|
|
|
29
40
|
- `AGENTIC_SECURITY_DEEP_TIMEOUT_MS` — global walltime budget (default 300_000).
|
|
30
41
|
- `AGENTIC_SECURITY_DEEP_FN_LIMIT` — function-count budget (default 5000).
|
|
31
42
|
- `AGENTIC_SECURITY_DEEP_IN_CI=1` — also enable in CI (off by default; CI runs are time-bounded).
|
|
43
|
+
- `AGENTIC_SECURITY_KCFA_MAX_CONTEXTS` — distinct non-empty entry contexts kept per function (default 16; 0 = monovariant).
|
|
32
44
|
|
|
33
45
|
## Gotchas
|
|
34
46
|
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -99,6 +99,15 @@ export const CATALOG = [
|
|
|
99
99
|
{ kind: 'sanitizer', id: 'js-String-coerce', language: 'js', match: { type: 'call', callee: 'String' }, effect: 'strip', appliesTo: ['mongo-operator'] },
|
|
100
100
|
{ kind: 'sanitizer', id: 'js-validator-escape', language: 'js', match: { type: 'call', callee: 'escape' }, effect: 'strip', appliesTo: ['xss'] },
|
|
101
101
|
{ kind: 'sanitizer', id: 'js-strip_tags', language: 'js', match: { type: 'call', callee: 'stripTags' }, effect: 'strip', appliesTo: ['xss'] },
|
|
102
|
+
// Schema-validation libraries (#7). Scoped to NoSQL/operator injection ONLY:
|
|
103
|
+
// validating that input matches a typed shape defeats operator injection
|
|
104
|
+
// (a `{$gt:''}` object can't satisfy `z.string()`). It does NOT sanitize the
|
|
105
|
+
// value for XSS/SQL/cmd — a validated string is still a payload — so these
|
|
106
|
+
// are deliberately NOT tagged for those families (doing so would cause false
|
|
107
|
+
// negatives). Only distinctive callees, to avoid colliding with JSON.parse.
|
|
108
|
+
{ kind: 'sanitizer', id: 'js-zod-safeParse', language: 'js', match: { type: 'call', callee: 'safeParse' }, effect: 'strip', appliesTo: ['mongo-operator'] },
|
|
109
|
+
{ kind: 'sanitizer', id: 'js-zod-parseAsync', language: 'js', match: { type: 'call', callee: 'parseAsync' }, effect: 'strip', appliesTo: ['mongo-operator'] },
|
|
110
|
+
{ kind: 'sanitizer', id: 'js-class-validator', language: 'js', match: { type: 'call', callee: 'validateOrReject' }, effect: 'strip', appliesTo: ['mongo-operator'] },
|
|
102
111
|
|
|
103
112
|
// ─── SOURCES (Python — Flask / FastAPI / Django) ──────────────────────────
|
|
104
113
|
{ kind: 'source', id: 'py-flask-request-args', language: 'py', framework: 'flask', match: { type: 'member', object: 'request', prop: 'args' }, label: 'request.args' },
|
|
@@ -157,6 +166,21 @@ export const CATALOG = [
|
|
|
157
166
|
{ kind: 'source', id: 'go-buffalo-request',language:'go', framework:'buffalo',match: { type: 'member', object: 'c', prop: 'Request' }, label: 'c.Request (buffalo)' },
|
|
158
167
|
{ kind: 'source', id: 'go-gorilla-vars', language: 'go', framework: 'gorilla',match: { type: 'call', callee: 'Vars' }, label: 'mux.Vars (gorilla)' },
|
|
159
168
|
|
|
169
|
+
// ─── SINKS (Go — database/sql) — R3 (PRD §5) ──────────────────────────────
|
|
170
|
+
// callee 'Query' is also a net/http SOURCE (r.URL.Query). The engine
|
|
171
|
+
// disambiguates by position — source at the assignment RHS, sink at a call
|
|
172
|
+
// with a tainted query argument — so coexistence is benign (a source call
|
|
173
|
+
// like r.URL.Query() has no tainted arg, so it never fires the sink).
|
|
174
|
+
{ kind: 'sink', id: 'go-sql-query', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Query' }, argIndex: 0,
|
|
175
|
+
vuln: { name: 'SQL Injection (db.Query — Go)', severity: 'critical', cwe: 'CWE-89',
|
|
176
|
+
remediation: 'Use parameterized queries: db.Query("SELECT … WHERE id = $1", id). Never concatenate untrusted input into the SQL string.' } },
|
|
177
|
+
{ kind: 'sink', id: 'go-sql-queryrow', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'QueryRow' }, argIndex: 0,
|
|
178
|
+
vuln: { name: 'SQL Injection (db.QueryRow — Go)', severity: 'critical', cwe: 'CWE-89',
|
|
179
|
+
remediation: 'Use placeholders ($1 / ?) and pass args separately.' } },
|
|
180
|
+
{ kind: 'sink', id: 'go-sql-exec', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
|
|
181
|
+
vuln: { name: 'SQL Injection (db.Exec — Go)', severity: 'critical', cwe: 'CWE-89',
|
|
182
|
+
remediation: 'Use parameterized statements: db.Exec("UPDATE t SET x=$1 WHERE id=$2", x, id).' } },
|
|
183
|
+
|
|
160
184
|
// ─── SOURCES (Ruby — Rails / Sinatra) ─────────────────────────────────────
|
|
161
185
|
{ kind: 'source', id: 'rb-rails-params', language: 'rb', framework: 'rails', match: { type: 'global', name: 'params' }, label: 'params (Rails)' },
|
|
162
186
|
{ kind: 'source', id: 'rb-rails-cookies', language: 'rb', framework: 'rails', match: { type: 'global', name: 'cookies' }, label: 'cookies (Rails)' },
|
|
@@ -738,22 +762,46 @@ function filterByProvenance(entries) {
|
|
|
738
762
|
return list;
|
|
739
763
|
}
|
|
740
764
|
|
|
741
|
-
export function matchSource(
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
if (
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
765
|
+
export function matchSource(expr) {
|
|
766
|
+
if (!expr) return null;
|
|
767
|
+
// Member sources (req.query): the original path — unchanged.
|
|
768
|
+
if (expr.kind === 'member' && expr.object?.kind === 'ident') {
|
|
769
|
+
const raw = MEMBER_INDEX.get(`${expr.object.name}.${expr.prop}`);
|
|
770
|
+
if (raw) {
|
|
771
|
+
const hits = filterByProvenance(raw);
|
|
772
|
+
const s = hits.find(h => h.kind === 'source');
|
|
773
|
+
if (s) return s;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
// R3 (PRD §5): CALL sources (r.FormValue(), r.URL.Query(), c.QueryParam()).
|
|
777
|
+
// matchSource previously handled only member reads, so Go's call-shaped
|
|
778
|
+
// sources were never recognized at the assignment RHS. Match by callee last
|
|
779
|
+
// segment (Go gives a dotted string; JS/Py a member/ident expr).
|
|
780
|
+
if (expr.kind === 'call') {
|
|
781
|
+
let cn = null;
|
|
782
|
+
if (typeof expr.callee === 'string') cn = expr.callee.includes('.') ? expr.callee.slice(expr.callee.lastIndexOf('.') + 1) : expr.callee;
|
|
783
|
+
else if (expr.callee && expr.callee.kind === 'member') cn = expr.callee.prop;
|
|
784
|
+
else if (expr.callee && expr.callee.kind === 'ident') cn = expr.callee.name;
|
|
785
|
+
if (cn) {
|
|
786
|
+
const raw = CALLEE_INDEX.get(cn);
|
|
787
|
+
if (raw) {
|
|
788
|
+
const hits = filterByProvenance(raw);
|
|
789
|
+
const s = hits.find(h => h.kind === 'source');
|
|
790
|
+
if (s) return s;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return null;
|
|
751
795
|
}
|
|
752
796
|
|
|
753
797
|
export function matchSinkOrSanitizer(calleeExpr) {
|
|
754
798
|
if (!calleeExpr) return null;
|
|
755
799
|
let calleeName = null;
|
|
756
|
-
|
|
800
|
+
// R3 (PRD §5): the Go IR (and other string-callee parsers) represent a call
|
|
801
|
+
// target as a dotted STRING ("db.Query") rather than a member expr. Match on
|
|
802
|
+
// the last segment so those languages' sinks/sanitizers are recognized too.
|
|
803
|
+
if (typeof calleeExpr === 'string') calleeName = calleeExpr.includes('.') ? calleeExpr.slice(calleeExpr.lastIndexOf('.') + 1) : calleeExpr;
|
|
804
|
+
else if (calleeExpr.kind === 'ident') calleeName = calleeExpr.name;
|
|
757
805
|
else if (calleeExpr.kind === 'member') calleeName = calleeExpr.prop;
|
|
758
806
|
if (!calleeName) return null;
|
|
759
807
|
const raw = CALLEE_INDEX.get(calleeName);
|
package/src/dataflow/engine.js
CHANGED
|
@@ -39,6 +39,7 @@ import { aliasesForVar } from './points-to.js';
|
|
|
39
39
|
import { higherOrderTaintFlow } from './higher-order.js';
|
|
40
40
|
import { SummaryCache, entryStateFromCall } from './summaries.js';
|
|
41
41
|
import { lookupBuiltinSummary } from './builtin-summaries.js';
|
|
42
|
+
import { isImplicitFlowEnabled, buildImplicitContext, implicitAssignTarget, markImplicitTaint, createImplicitFinding } from './implicit-flow.js';
|
|
42
43
|
|
|
43
44
|
// v0.70 #2 — addPath that also taints every alias of the variable.
|
|
44
45
|
// When `target` is a dotted path like "a.x" and the root `a` has aliases
|
|
@@ -157,6 +158,13 @@ function exprIsSource(expr) {
|
|
|
157
158
|
const hit = matchSource(expr);
|
|
158
159
|
if (hit) return hit;
|
|
159
160
|
}
|
|
161
|
+
// R3 (PRD §5): call-shaped sources (r.FormValue(), r.URL.Query(), c.Query()).
|
|
162
|
+
// Previously only member reads were recognized, so Go's call-style sources
|
|
163
|
+
// never tainted the assignment target. matchSource now resolves call sources.
|
|
164
|
+
if (expr.kind === 'call') {
|
|
165
|
+
const hit = matchSource(expr);
|
|
166
|
+
if (hit) return hit;
|
|
167
|
+
}
|
|
160
168
|
if (expr.kind === 'member' && expr.object) {
|
|
161
169
|
return exprIsSource(expr.object);
|
|
162
170
|
}
|
|
@@ -215,7 +223,8 @@ function step(node, stateIn, callContext) {
|
|
|
215
223
|
const calleeName = node.source && node.source.kind === 'call' && typeof node.source.callee === 'string'
|
|
216
224
|
? node.source.callee : null;
|
|
217
225
|
if (target && calleeName && callContext._summaryCache && callContext._callGraph) {
|
|
218
|
-
const
|
|
226
|
+
const _callerFile = (callContext._currentFnQid || '').split('::')[0] || undefined;
|
|
227
|
+
const resolved = callContext._callGraph.resolve ? callContext._callGraph.resolve(calleeName, _callerFile) : null;
|
|
219
228
|
const fn = resolved && resolved.qid ? resolved : null;
|
|
220
229
|
const qid = resolved && (resolved.qid || resolved);
|
|
221
230
|
if (typeof qid === 'string') {
|
|
@@ -322,8 +331,9 @@ function step(node, stateIn, callContext) {
|
|
|
322
331
|
// Object.assign(target, tainted) → target becomes tainted in caller.
|
|
323
332
|
if (callContext._summaryCache && callContext._callGraph
|
|
324
333
|
&& typeof node.callee === 'string') {
|
|
334
|
+
const _callerFile = (callContext._currentFnQid || '').split('::')[0] || undefined;
|
|
325
335
|
const resolved = callContext._callGraph.resolve
|
|
326
|
-
? callContext._callGraph.resolve(node.callee) : null;
|
|
336
|
+
? callContext._callGraph.resolve(node.callee, _callerFile) : null;
|
|
327
337
|
const fn = resolved && resolved.qid ? resolved : null;
|
|
328
338
|
const qid = resolved && (resolved.qid || resolved);
|
|
329
339
|
if (typeof qid === 'string' && fn && Array.isArray(fn.params)) {
|
|
@@ -331,7 +341,31 @@ function step(node, stateIn, callContext) {
|
|
|
331
341
|
const entry = paramNames.length
|
|
332
342
|
? entryStateFromCall(paramNames, node.args || [], state)
|
|
333
343
|
: new Set();
|
|
334
|
-
|
|
344
|
+
let sum = callContext._summaryCache.get(qid, entry);
|
|
345
|
+
// FR-SEM-2: context-sensitive lazy compute at the plain-call site,
|
|
346
|
+
// mirroring the assign-call site. On a miss for a NON-empty entry,
|
|
347
|
+
// compute the callee's summary UNDER that tainted-arg context so a
|
|
348
|
+
// param mutated only when called with user input is detected here
|
|
349
|
+
// too (not just when the call's result is assigned). Bounded by the
|
|
350
|
+
// SummaryCache context cap.
|
|
351
|
+
if (!sum && entry.size && fn && fn.cfg) {
|
|
352
|
+
sum = callContext._summaryCache.compute(qid, entry, () => {
|
|
353
|
+
const inner = {
|
|
354
|
+
_findings: [], _taintSources: [], _returnTainted: false,
|
|
355
|
+
_stack: new Set(), deadlineMs: callContext.deadlineMs,
|
|
356
|
+
_summaryCache: callContext._summaryCache,
|
|
357
|
+
_callGraph: callContext._callGraph,
|
|
358
|
+
_mutatedParamsOut: new Set(),
|
|
359
|
+
};
|
|
360
|
+
try { analyzeFunction(fn, entry, inner); } catch {}
|
|
361
|
+
return {
|
|
362
|
+
returnTainted: !!inner._returnTainted,
|
|
363
|
+
mutatedParams: inner._mutatedParamsOut || new Set(),
|
|
364
|
+
taintedGlobals: new Set(),
|
|
365
|
+
findings: [],
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
}
|
|
335
369
|
if (sum && sum.mutatedParams && sum.mutatedParams.size) {
|
|
336
370
|
const mutated = callContext._summaryCache.applyAtCallSite(
|
|
337
371
|
sum, paramNames, node.args || [], state);
|
|
@@ -357,6 +391,20 @@ function step(node, stateIn, callContext) {
|
|
|
357
391
|
});
|
|
358
392
|
}
|
|
359
393
|
}
|
|
394
|
+
// R4 (PRD §5): array-element taint. A mutating array method (push/unshift/
|
|
395
|
+
// splice/fill/copyWithin) called with a tainted argument taints the
|
|
396
|
+
// receiver array; an index read (a[0] → access path "a.0") is then covered
|
|
397
|
+
// by the receiver prefix. Object-property taint already flows via the
|
|
398
|
+
// access-path lattice — this closes the array case.
|
|
399
|
+
if (node.callee && node.callee.kind === 'member' && typeof node.callee.prop === 'string'
|
|
400
|
+
&& /^(?:push|unshift|splice|fill|copyWithin)$/.test(node.callee.prop)
|
|
401
|
+
&& Array.isArray(argTaints) && argTaints.some(Boolean)) {
|
|
402
|
+
// Mutate the state Set IN PLACE (the binding is const; the call case
|
|
403
|
+
// returns this same Set ref). Avoids touching the unrelated mutated-param
|
|
404
|
+
// paths in this case, keeping the blast radius to array-element taint only.
|
|
405
|
+
const _arrRecv = accessPathOf(node.callee.object);
|
|
406
|
+
if (_arrRecv) state.add(_arrRecv);
|
|
407
|
+
}
|
|
360
408
|
if (cat) {
|
|
361
409
|
for (const e of cat) {
|
|
362
410
|
if (e.kind === 'sink' && (
|
|
@@ -467,6 +515,11 @@ function step(node, stateIn, callContext) {
|
|
|
467
515
|
// control flow) can otherwise hold past the global timeout.
|
|
468
516
|
function analyzeFunction(fn, entryState, callContext) {
|
|
469
517
|
const nodes = fn.cfg.nodes;
|
|
518
|
+
// R2 (PRD §5): set the call-string caller context to THIS function while its
|
|
519
|
+
// worklist computes callee summaries (so a callee is keyed by its caller).
|
|
520
|
+
// No-op for the key unless AGENTIC_SECURITY_KCFA_CALLSTRING=1. Restored below.
|
|
521
|
+
const _prevCallerCtx = (callContext && callContext._summaryCache && callContext._summaryCache.setCallerContext)
|
|
522
|
+
? callContext._summaryCache.setCallerContext(fn.qid) : undefined;
|
|
470
523
|
const work = [];
|
|
471
524
|
const inStates = new Map();
|
|
472
525
|
const outStates = new Map();
|
|
@@ -509,6 +562,47 @@ function analyzeFunction(fn, entryState, callContext) {
|
|
|
509
562
|
}
|
|
510
563
|
}
|
|
511
564
|
|
|
565
|
+
// R4 (PRD §5) — implicit / control-dependence flow. OPT-IN (default OFF, see
|
|
566
|
+
// isImplicitFlowEnabled). Post-pass over the converged CFG: a sink reached
|
|
567
|
+
// INSIDE a tainted-condition branch can leak information even when its
|
|
568
|
+
// argument is constant or only implicitly tainted (a var assigned in that
|
|
569
|
+
// branch). Findings carry implicit:true + capped confidence.
|
|
570
|
+
if (isImplicitFlowEnabled() && fn.cfg) {
|
|
571
|
+
try {
|
|
572
|
+
const union = new Set();
|
|
573
|
+
for (const s of inStates.values()) for (const p of s) union.add(p);
|
|
574
|
+
const ictx = buildImplicitContext(fn.cfg, (expr) => exprTaint(expr, union));
|
|
575
|
+
// Mark vars assigned inside a tainted branch as implicit-tainted.
|
|
576
|
+
let implicitState = new Set();
|
|
577
|
+
for (const [nid, ctx] of ictx) {
|
|
578
|
+
const t = implicitAssignTarget(nodes[nid], ctx);
|
|
579
|
+
if (t) implicitState = markImplicitTaint(implicitState, t);
|
|
580
|
+
}
|
|
581
|
+
// A sink in a tainted branch whose arg is implicit-tainted (or constant)
|
|
582
|
+
// — and NOT already explicitly tainted (the normal pass covers that).
|
|
583
|
+
for (const [nid, ctx] of ictx) {
|
|
584
|
+
const node = nodes[nid];
|
|
585
|
+
if (!node || node.kind !== 'call') continue;
|
|
586
|
+
const cat = matchSinkOrSanitizer(node.callee);
|
|
587
|
+
const sink = cat && cat.find((e) => e.kind === 'sink');
|
|
588
|
+
if (!sink) continue;
|
|
589
|
+
const inS = inStates.get(nid) || new Set();
|
|
590
|
+
if ((node.args || []).some((a) => exprTaint(a, inS))) continue;
|
|
591
|
+
const argRefsImplicit = (node.args || []).some((a) => {
|
|
592
|
+
const ap = accessPathOf(a); return ap && isCoveredBy(implicitState, `implicit:${ap}`);
|
|
593
|
+
});
|
|
594
|
+
const allConst = (node.args || []).length > 0 && (node.args || []).every((a) => a && a.kind === 'literal');
|
|
595
|
+
if (argRefsImplicit || allConst) {
|
|
596
|
+
callContext._findings.push({
|
|
597
|
+
...createImplicitFinding(node, ctx.conditionLabel),
|
|
598
|
+
_funcQid: fn.qid, sinkId: sink.id,
|
|
599
|
+
cwe: (sink.vuln && sink.vuln.cwe) || 'CWE-200',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
} catch { /* implicit flow is best-effort + opt-in */ }
|
|
604
|
+
}
|
|
605
|
+
|
|
512
606
|
const exit = outStates.get(fn.cfg.exit) || new Set();
|
|
513
607
|
// v0.66 — record which params are tainted at function exit so the
|
|
514
608
|
// caller's applyAtCallSite can propagate that mutated taint back. We
|
|
@@ -520,6 +614,10 @@ function analyzeFunction(fn, entryState, callContext) {
|
|
|
520
614
|
if (isCoveredBy(exit, p)) callContext._mutatedParamsOut.add(p);
|
|
521
615
|
}
|
|
522
616
|
}
|
|
617
|
+
// R2: restore the caller context for the enclosing function's analysis.
|
|
618
|
+
if (_prevCallerCtx !== undefined && callContext && callContext._summaryCache && callContext._summaryCache.setCallerContext) {
|
|
619
|
+
callContext._summaryCache.setCallerContext(_prevCallerCtx);
|
|
620
|
+
}
|
|
523
621
|
return exit;
|
|
524
622
|
}
|
|
525
623
|
|
|
@@ -685,7 +783,7 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
685
783
|
if (Date.now() > deadlineMs) break;
|
|
686
784
|
const inv = hoInvocations[hi];
|
|
687
785
|
if (!inv.callee || !inv.taintedParam) continue;
|
|
688
|
-
const resolved = callGraph.resolve ? callGraph.resolve(inv.callee) : null;
|
|
786
|
+
const resolved = callGraph.resolve ? callGraph.resolve(inv.callee, fn && fn.file) : null;
|
|
689
787
|
const cbFn = resolved && resolved.qid ? resolved : null;
|
|
690
788
|
if (!cbFn || !cbFn.params || !cbFn.params.length) continue;
|
|
691
789
|
const cbEntry = new Set([cbFn.params[inv.paramIndex || 0]]);
|
|
@@ -723,7 +821,10 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
723
821
|
cwe: f.cwe,
|
|
724
822
|
remediation: f.remediation,
|
|
725
823
|
parser: 'IR-TAINT',
|
|
726
|
-
|
|
824
|
+
// R4 implicit-flow: preserve the implicit flag + its capped confidence
|
|
825
|
+
// (a control-dependence finding, not an explicit data-flow one).
|
|
826
|
+
confidence: (f.implicit && typeof f.confidence === 'number') ? f.confidence : 0.75,
|
|
827
|
+
...(f.implicit === true ? { implicit: true } : {}),
|
|
727
828
|
source: f.trace && f.trace.length ? {
|
|
728
829
|
file: fn.file,
|
|
729
830
|
line: f.trace[0].line,
|
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
import { addPath } from './access-paths.js';
|
|
35
35
|
|
|
36
36
|
export function isImplicitFlowEnabled() {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// OPT-IN (default OFF): implicit/control-dependence flow is famously noisy, so
|
|
38
|
+
// it must be explicitly requested. Findings carry implicit:true + capped
|
|
39
|
+
// confidence so the standard pipeline ranks them below explicit flows.
|
|
40
|
+
return process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '1';
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/**
|
package/src/dataflow/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Layer 2 entry point.
|
|
2
2
|
import { runTaintEngine } from './engine.js';
|
|
3
|
+
import { annotateProvenClean } from './proven-clean.js';
|
|
3
4
|
import { CATALOG, matchSource, matchSinkOrSanitizer, _catalogSize } from './catalog.js';
|
|
4
5
|
import { applyPathFeasibility } from './path-feasibility.js';
|
|
5
6
|
import { SummaryCache, entryStateFromCall } from './summaries.js';
|
|
@@ -47,12 +48,17 @@ export function runDeepAnalysis(perFileIR, callGraph, opts = {}) {
|
|
|
47
48
|
scanner: opts.scannerVersion || 'unknown',
|
|
48
49
|
rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
|
|
49
50
|
};
|
|
51
|
+
// Compute current file hashes UNCONDITIONALLY — the first scan (and any
|
|
52
|
+
// scan after a version/rule invalidation) must persist a baseline so the
|
|
53
|
+
// NEXT scan has something to reuse. This previously lived inside the
|
|
54
|
+
// valid-state branch, so a cold cache never committed and incremental was
|
|
55
|
+
// a permanent no-op (the commit guard at the bottom keys on currentFileHashes).
|
|
56
|
+
currentFileHashes = {};
|
|
57
|
+
for (const [fp, content] of Object.entries(opts.fileContents)) {
|
|
58
|
+
currentFileHashes[fp] = hashFileContent(content);
|
|
59
|
+
}
|
|
50
60
|
const valid = validateIncrementalState(priorState, currentVersion);
|
|
51
61
|
if (valid.valid) {
|
|
52
|
-
currentFileHashes = {};
|
|
53
|
-
for (const [fp, content] of Object.entries(opts.fileContents)) {
|
|
54
|
-
currentFileHashes[fp] = hashFileContent(content);
|
|
55
|
-
}
|
|
56
62
|
const diff = diffFileHashes(priorState.files || {}, currentFileHashes);
|
|
57
63
|
const changedQids = new Set();
|
|
58
64
|
// Map a changed file to the qids it owns. perFileIR exposes file→fns.
|
|
@@ -114,6 +120,13 @@ export function runDeepAnalysis(perFileIR, callGraph, opts = {}) {
|
|
|
114
120
|
if (process.env.AGENTIC_SECURITY_BACKWARD_SLICE === '1') {
|
|
115
121
|
findings = annotateBackwardSlices(findings, perFileIR, callGraph);
|
|
116
122
|
}
|
|
123
|
+
// Roadmap #6 — flow-proof: prove SQL sinks reached only through a
|
|
124
|
+
// parameterizer (`provenClean`). Runs by default in the deep pass; it only
|
|
125
|
+
// touches IR-TAINT SQL findings and never drops anything. The proof-gate
|
|
126
|
+
// annotator (engine.js) consolidates this into the demotion verdict.
|
|
127
|
+
if (process.env.AGENTIC_SECURITY_NO_PROOF_GATE !== '1') {
|
|
128
|
+
try { findings = annotateProvenClean(findings, perFileIR); } catch { /* proof failure must not fail the scan */ }
|
|
129
|
+
}
|
|
117
130
|
// v0.70 #6 — probabilistic / soft taint. Walks each finding's trace +
|
|
118
131
|
// chain, multiplies (1 - effectiveness) across sanitizers, demotes
|
|
119
132
|
// below-threshold findings to lower severity (never drops).
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Proof-gate precision pass — roadmap item #6 ("perfect multi-language SAST").
|
|
2
|
+
//
|
|
3
|
+
// The engine produces two INDEPENDENT flow-proof signals that, until now,
|
|
4
|
+
// were either informational-only or wired in isolation:
|
|
5
|
+
//
|
|
6
|
+
// f.provenClean (proven-clean.js) — every reaching path to a SQL
|
|
7
|
+
// sink passes a parameterizer.
|
|
8
|
+
// f._provenUnreachable (exploit-prover.js) — a sanitizer on the path emits
|
|
9
|
+
// output that cannot contain the
|
|
10
|
+
// vuln family's metacharacters.
|
|
11
|
+
//
|
|
12
|
+
// This pass consolidates them into ONE verdict per finding and applies a
|
|
13
|
+
// precision demotion — the central idea of the precision gate:
|
|
14
|
+
//
|
|
15
|
+
// "Report only provably-feasible flows. A flow we can PROVE is clean or
|
|
16
|
+
// infeasible is demoted, not dropped — the auditor still sees it, and a
|
|
17
|
+
// severity-based CI gate still fires, but it stops dominating the risk
|
|
18
|
+
// ranking and stops tripping confidence filters."
|
|
19
|
+
//
|
|
20
|
+
// Output stamped on every finding:
|
|
21
|
+
// f.proof = { verdict, reasons[] }
|
|
22
|
+
// verdict ∈ 'proven-clean' | 'proven-infeasible' | 'feasible' | 'unproven'
|
|
23
|
+
// f.proofGated = true (only when a demotion was applied)
|
|
24
|
+
//
|
|
25
|
+
// Demotion policy is deliberately RECALL-PRESERVING:
|
|
26
|
+
// - lower `confidence` by DEMOTE_FACTOR and recompute its tier,
|
|
27
|
+
// - drop `confidenceTier` / `exploitabilityTier` one notch,
|
|
28
|
+
// - record an `exploitabilityFactors` breadcrumb,
|
|
29
|
+
// - leave `severity` UNTOUCHED. The proofs here are heuristic (path-
|
|
30
|
+
// existence / regex-exclusion), so they must never hide a finding from a
|
|
31
|
+
// severity gate. Confidence/exploitability are the safe levers.
|
|
32
|
+
|
|
33
|
+
const DEMOTE_FACTOR = 0.4;
|
|
34
|
+
|
|
35
|
+
// Tier ladders, lowest → highest. demoteTier moves one step down.
|
|
36
|
+
const CONFIDENCE_TIERS = ['very-low', 'low', 'medium', 'high'];
|
|
37
|
+
const EXPLOITABILITY_TIERS = ['low', 'medium', 'high', 'critical'];
|
|
38
|
+
|
|
39
|
+
function demoteTier(tier, ladder) {
|
|
40
|
+
const i = ladder.indexOf(tier);
|
|
41
|
+
if (i <= 0) return ladder[0];
|
|
42
|
+
return ladder[i - 1];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Decide the proof verdict for a single finding from the upstream signals.
|
|
46
|
+
export function verdictForFinding(f) {
|
|
47
|
+
if (f.provenClean === true) {
|
|
48
|
+
const sanitizers = (f.provenanceProof && f.provenanceProof.sanitizers) || [];
|
|
49
|
+
return { verdict: 'proven-clean', reason: sanitizers.length ? `parameterized via ${sanitizers.join(', ')}` : 'sql-parameterizer-on-path' };
|
|
50
|
+
}
|
|
51
|
+
if (f._provenUnreachable === true) {
|
|
52
|
+
return { verdict: 'proven-infeasible', reason: f._provenUnreachableReason || 'sanitizer-excludes-metacharacters' };
|
|
53
|
+
}
|
|
54
|
+
// A taint finding that reached a sink with attributed sources, for which we
|
|
55
|
+
// could NOT discharge a clean/infeasible proof, is "feasible" — the flows
|
|
56
|
+
// a precision-gated report should lead with. Non-taint findings are simply
|
|
57
|
+
// outside the proof model → "unproven" (no demotion, no claim).
|
|
58
|
+
if (f.parser === 'IR-TAINT') return { verdict: 'feasible', reason: 'reaches-sink, no clean/infeasible proof discharged' };
|
|
59
|
+
return { verdict: 'unproven', reason: 'no flow-proof applicable to this finding class' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Annotate findings in place. Returns the same array with a non-enumerable
|
|
63
|
+
// `_proofGateStats` for benchmarking.
|
|
64
|
+
export function annotateProofGate(findings, opts = {}) {
|
|
65
|
+
if (!Array.isArray(findings)) return findings;
|
|
66
|
+
const factor = typeof opts.demoteFactor === 'number' ? opts.demoteFactor : DEMOTE_FACTOR;
|
|
67
|
+
const stats = { gated: 0, feasible: 0, provenClean: 0, provenInfeasible: 0, unproven: 0 };
|
|
68
|
+
|
|
69
|
+
for (const f of findings) {
|
|
70
|
+
if (!f || typeof f !== 'object') continue;
|
|
71
|
+
const { verdict, reason } = verdictForFinding(f);
|
|
72
|
+
f.proof = { verdict, reasons: [reason] };
|
|
73
|
+
|
|
74
|
+
if (verdict === 'feasible') { stats.feasible++; continue; }
|
|
75
|
+
if (verdict === 'unproven') { stats.unproven++; continue; }
|
|
76
|
+
|
|
77
|
+
// proven-clean | proven-infeasible → recall-preserving demotion.
|
|
78
|
+
if (verdict === 'proven-clean') stats.provenClean++; else stats.provenInfeasible++;
|
|
79
|
+
// R13 (PRD §5): make "provably safe" a first-class, queryable verdict — not
|
|
80
|
+
// just a quiet confidence demotion. "We proved this can't fire" is a
|
|
81
|
+
// stronger, rarer claim than any confidence score, and lets the report
|
|
82
|
+
// optionally hide discharged findings (--hide-proven-safe).
|
|
83
|
+
f.provablySafe = true;
|
|
84
|
+
|
|
85
|
+
if (typeof f.confidence === 'number') {
|
|
86
|
+
f._confidenceBeforeProofGate = f.confidence;
|
|
87
|
+
f.confidence = Math.max(0.01, Number((f.confidence * factor).toFixed(4)));
|
|
88
|
+
}
|
|
89
|
+
if (f.confidenceTier) f.confidenceTier = demoteTier(f.confidenceTier, CONFIDENCE_TIERS);
|
|
90
|
+
if (f.exploitabilityTier) f.exploitabilityTier = demoteTier(f.exploitabilityTier, EXPLOITABILITY_TIERS);
|
|
91
|
+
if (Array.isArray(f.exploitabilityFactors)) f.exploitabilityFactors.push(`proof:${verdict}`);
|
|
92
|
+
f.proofGated = true;
|
|
93
|
+
stats.gated++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
Object.defineProperty(findings, '_proofGateStats', { value: stats, enumerable: false, configurable: true });
|
|
97
|
+
return findings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export const _internals = { DEMOTE_FACTOR, CONFIDENCE_TIERS, EXPLOITABILITY_TIERS, demoteTier };
|