@clear-capabilities/agentic-security-scanner 0.136.2 → 0.137.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.
Files changed (117) hide show
  1. package/CHANGELOG.md +880 -0
  2. package/bin/agentic-security.js +189 -37
  3. package/dist/113.index.js +13 -4
  4. package/dist/178.index.js +1 -1
  5. package/dist/207.index.js +5 -4
  6. package/dist/238.index.js +1 -1
  7. package/dist/317.index.js +36 -6
  8. package/dist/384.index.js +1 -1
  9. package/dist/435.index.js +192 -15
  10. package/dist/444.index.js +20 -11
  11. package/dist/449.index.js +8 -1
  12. package/dist/526.index.js +3 -3
  13. package/dist/637.index.js +1 -1
  14. package/dist/agentic-security.mjs +15 -15
  15. package/dist/agentic-security.mjs.sha256 +1 -1
  16. package/dist/compliance-frameworks/nist-privacy-1-1.json +2 -2
  17. package/dist/compliance-frameworks/owasp-asvs-5.json +1 -1
  18. package/package.json +21 -13
  19. package/src/dataflow/CLAUDE.md +12 -4
  20. package/src/dataflow/builtin-summaries.js +1 -1
  21. package/src/dataflow/catalog-expanded.js +1 -0
  22. package/src/dataflow/catalog.js +157 -31
  23. package/src/dataflow/engine.js +639 -112
  24. package/src/dataflow/implicit-flow.js +68 -36
  25. package/src/dataflow/incremental.js +18 -3
  26. package/src/dataflow/index.js +17 -1
  27. package/src/dataflow/points-to.js +19 -6
  28. package/src/dataflow/proven-clean.js +41 -0
  29. package/src/dataflow/sanitizer-gate.js +35 -9
  30. package/src/dataflow/sanitizer-proof.js +21 -3
  31. package/src/dataflow/stub-aware-filter.js +36 -13
  32. package/src/dataflow/summaries.js +21 -2
  33. package/src/engine.js +430 -196
  34. package/src/ir/CLAUDE.md +16 -2
  35. package/src/ir/balanced-call.js +55 -0
  36. package/src/ir/class-hierarchy.js +57 -11
  37. package/src/ir/index.js +14 -2
  38. package/src/ir/parser-cs.js +513 -40
  39. package/src/ir/parser-go.js +29 -11
  40. package/src/ir/parser-java.js +300 -20
  41. package/src/ir/parser-js.js +300 -22
  42. package/src/ir/parser-kt.js +436 -18
  43. package/src/ir/parser-php.js +631 -38
  44. package/src/ir/parser-py.helper.py +32 -2
  45. package/src/ir/parser-py.js +31 -4
  46. package/src/ir/parser-rb.js +161 -26
  47. package/src/ir/ssa.js +6 -1
  48. package/src/lsp/server.js +35 -3
  49. package/src/mcp/CLAUDE.md +9 -2
  50. package/src/mcp/redact.js +26 -0
  51. package/src/mcp/tools.js +164 -15
  52. package/src/posture/CLAUDE.md +19 -7
  53. package/src/posture/accuracy-scorecard.js +9 -1
  54. package/src/posture/aibom.js +12 -8
  55. package/src/posture/auditor-walkthrough.js +102 -3
  56. package/src/posture/autopilot.js +8 -1
  57. package/src/posture/calibration-drift.js +11 -5
  58. package/src/posture/calibration.js +24 -2
  59. package/src/posture/clustering.js +12 -1
  60. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +2 -2
  61. package/src/posture/compliance-frameworks/owasp-asvs-5.json +1 -1
  62. package/src/posture/compliance-policy.js +33 -1
  63. package/src/posture/confidence.js +44 -10
  64. package/src/posture/corpus-enroll.js +9 -5
  65. package/src/posture/corpus-match.js +19 -0
  66. package/src/posture/csharp-analysis.js +62 -3
  67. package/src/posture/deploy-platform.js +4 -1
  68. package/src/posture/drift.js +7 -1
  69. package/src/posture/epss.js +13 -1
  70. package/src/posture/evidence-bundle.js +36 -6
  71. package/src/posture/exploitability-probability.js +13 -1
  72. package/src/posture/falsification.js +23 -2
  73. package/src/posture/fix-metrics.js +1 -1
  74. package/src/posture/fix-verify-loop.js +10 -1
  75. package/src/posture/iac-reachability.js +14 -8
  76. package/src/posture/integrity.js +25 -7
  77. package/src/posture/model-rescan.js +65 -0
  78. package/src/posture/mttr.js +5 -0
  79. package/src/posture/poc-inprocess.js +27 -8
  80. package/src/posture/regression-test-gen.js +23 -8
  81. package/src/posture/reverse-blast-radius.js +5 -1
  82. package/src/posture/risk-dollars.js +18 -1
  83. package/src/posture/sbom.js +2 -2
  84. package/src/posture/secret-history.js +20 -11
  85. package/src/posture/security-trend.js +7 -1
  86. package/src/posture/stack-playbook.js +22 -1
  87. package/src/posture/threat-model-grounding.js +2 -2
  88. package/src/posture/validator-metrics.js +10 -3
  89. package/src/posture/verifier.js +32 -57
  90. package/src/report/index.js +183 -14
  91. package/src/runScan.js +1 -1
  92. package/src/sast/_comment-strip.js +15 -4
  93. package/src/sast/_secret-entropy.js +1 -1
  94. package/src/sast/authz.js +6 -4
  95. package/src/sast/bench-shape/index.js +2 -7
  96. package/src/sast/claude-md-prompt-injection.js +14 -3
  97. package/src/sast/cloud-iam.js +60 -7
  98. package/src/sast/cpp-bench-extras.js +1 -1
  99. package/src/sast/csrf.js +7 -5
  100. package/src/sast/env-hygiene.js +5 -2
  101. package/src/sast/iac-terraform.js +25 -0
  102. package/src/sast/java-bench-extras.js +1 -1
  103. package/src/sast/java-constant-fold.js +5 -5
  104. package/src/sast/llm-owasp.js +4 -2
  105. package/src/sast/mcp-audit.js +7 -0
  106. package/src/sast/pipeline.js +8 -0
  107. package/src/sast/prompt-template.js +8 -6
  108. package/src/sast/prototype-pollution.js +6 -2
  109. package/src/sast/redos-nfa.js +6 -6
  110. package/src/sast/secret-concat.js +13 -2
  111. package/src/sast/ssrf-cloud-metadata.js +6 -3
  112. package/src/sast/xss-reflected-multilang.js +1 -1
  113. package/src/sast/xxe.js +1 -1
  114. package/src/sca/CLAUDE.md +3 -4
  115. package/src/sca/container.js +35 -3
  116. package/src/sca/dep-confusion.js +7 -0
  117. package/src/sca/sarif-ingest.js +0 -187
@@ -1 +1 @@
1
- b66b38a74c6d14bcf4a30fad910739e3cb83c455f7bf0ae6ca76a1487f598f1a agentic-security.mjs
1
+ c8726408d88159f1954448e223340231893809e7cfeaa72d0fce196a39a1395c agentic-security.mjs
@@ -781,7 +781,7 @@
781
781
  "summary": "Software is maintained, replaced, and removed commensurate with risk.",
782
782
  "codeTestable": "partial",
783
783
  "mapsTo": [
784
- "family:vulnerable-dependency",
784
+ "family:vulnerable-dep",
785
785
  "family:dependency-drift"
786
786
  ]
787
787
  },
@@ -800,7 +800,7 @@
800
800
  "codeTestable": "yes",
801
801
  "mapsTo": [
802
802
  "family:dependency-confusion",
803
- "family:vulnerable-dependency"
803
+ "family:vulnerable-dep"
804
804
  ]
805
805
  },
806
806
  {
@@ -66,7 +66,7 @@
66
66
  "category": "Malicious Code",
67
67
  "summary": "Verify the application does not include known-malicious or compromised dependencies.",
68
68
  "evidence": [".agentic-security/sbom-history snapshots clean.", "No dependency-confusion or dependency-drift findings."],
69
- "mapsTo": ["family:vulnerable-dependency", "family:dependency-confusion", "family:dependency-drift"]
69
+ "mapsTo": ["family:vulnerable-dep", "family:dependency-confusion", "family:dependency-drift"]
70
70
  },
71
71
  {
72
72
  "id": "V14.1",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.136.2",
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.",
3
+ "version": "0.137.0",
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, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -21,7 +21,8 @@
21
21
  "./sast": "./src/sast/index.js",
22
22
  "./sca": "./src/sca/index.js",
23
23
  "./secrets": "./src/secrets/index.js",
24
- "./report": "./src/report/index.js"
24
+ "./report": "./src/report/index.js",
25
+ "./posture/fix-style-mirror.js": "./src/posture/fix-style-mirror.js"
25
26
  },
26
27
  "files": [
27
28
  "src/",
@@ -39,7 +40,7 @@
39
40
  "@babel/preset-react": "^8.0.1",
40
41
  "@babel/preset-typescript": "^8.0.1",
41
42
  "java-parser": "^3.0.1",
42
- "js-yaml": "^5.2.3",
43
+ "js-yaml": "^5.3.0",
43
44
  "safe-regex": "^2.1.1"
44
45
  },
45
46
  "optionalDependencies": {
@@ -48,7 +49,7 @@
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/node": "^26.2.0",
51
- "@vercel/ncc": "^0.44.1"
52
+ "@vercel/ncc": "^0.45.0"
52
53
  },
53
54
  "overrides": {
54
55
  "lodash": "^4.18.1",
@@ -58,16 +59,17 @@
58
59
  "build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && rm -rf dist/compliance-frameworks && mkdir -p dist/compliance-frameworks && cp src/posture/compliance-frameworks/*.json dist/compliance-frameworks/ && 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');\"",
59
60
  "prepare": "node ../scripts/pre-push-gate.mjs --install-hook",
60
61
  "prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs && node ../scripts/release-check.mjs",
61
- "test": "npm run test:smoke && npm run test:glob && 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 && npm run test:discovery && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
62
- "test:smoke": "node --test test/smoke.test.js",
62
+ "test": "npm run test:smoke && npm run test:glob && 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 && npm run test:discovery && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && npm run test:python",
63
+ "test:python": "node ../scripts/run-python-tests.mjs",
64
+ "test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
63
65
  "test:glob": "node --test test/glob-compat.test.js",
64
- "test:sast": "node --test test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.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/file-upload.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",
65
- "test:posture": "node --test test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/independent-population.test.js test/evidence-bundle.test.js test/discovery-memory.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/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.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-verify-tests.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/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.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/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js",
66
- "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/llm-validator-preset.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 test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js",
67
- "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",
68
- "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 test/report-render.test.js",
66
+ "test:sast": "node --test test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.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/file-upload.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/mcp-audit-scan.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/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.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 test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/reachability-missing-callgraph-data.test.js",
67
+ "test:posture": "node --test test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/independent-population.test.js test/evidence-bundle.test.js test/discovery-memory.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/epss-enrich.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/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.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-verify-tests.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/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.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/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js",
68
+ "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/llm-validator-preset.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-php-rb.test.js test/parser-php-control-flow.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/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.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 test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js",
69
+ "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 test/lsp-server.test.js",
70
+ "test:report": "node --test 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 test/report-render.test.js",
69
71
  "test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
70
- "test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js && node ../scripts/lint-command-descriptions.mjs",
72
+ "test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/dist-chunks-tracked.test.js && node ../scripts/lint-command-descriptions.mjs",
71
73
  "test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
72
74
  "test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js",
73
75
  "eval:independent": "node ../bench/independent-eval/runner.mjs",
@@ -95,9 +97,15 @@
95
97
  "bench:engine-reconnect": "node ../bench/engine-reconnect/measure.mjs",
96
98
  "bench:self-scan": "node ../bench/self-scan/measure.mjs",
97
99
  "bench:self-scan:check": "node ../bench/self-scan/check.mjs",
100
+ "bench:mutation:check": "node ../bench/mutation/runner.mjs",
101
+ "bench:layer-recall": "node ../bench/layer-recall/runner.mjs",
102
+ "bench:layer-recall:check": "node ../bench/layer-recall/runner.mjs --check",
103
+ "bench:layer-recall:update-baseline": "node ../bench/layer-recall/runner.mjs --update-baseline",
98
104
  "bench:self-scan:update-baseline": "node ../bench/self-scan/check.mjs --update-baseline",
99
105
  "bench:engine-recall": "node ../bench/engine-recall/measure.mjs",
100
106
  "scorecard": "node ../scripts/scorecard.mjs",
107
+ "check-doc-drift": "node ../scripts/check-doc-drift.mjs",
108
+ "harness-score": "node ../scripts/harness-score.cjs",
101
109
  "corpus:enroll": "node ../scripts/enroll-proven-finding.mjs",
102
110
  "corpus:provenance": "node ../scripts/corpus-provenance-check.mjs",
103
111
  "fleet": "node ../scripts/fleet.mjs",
@@ -6,8 +6,14 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
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
- - **Catalog-driven source/sink 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
- - **Sanitizer entries are NOT consulted by this engine.** `matchSinkOrSanitizer()` returns hits of both kinds, but every consumer in `dataflow/*.js` keeps only `e.kind === 'sink'` — there is no `'sanitizer'` branch anywhere in this directory. Taint dies here only on clean re-assignment of a variable (`removePathAndDescendants`), independent of whether the RHS is a catalog sanitizer. Consequence: `engine.js`'s `applySanitizerGate` call is **inert** (`sanitizersOnPath` is always `{}`) because there is nothing to populate it from. Making it live needs (1) a `kind === 'sanitizer'` branch at the call site in `engine.js`, and (2) the sanitizer callee threaded onto the finding.
9
+ - **Catalog-driven source/sink matching.** Add entries in `catalog.js`. Each entry: `kind` ∈ {source, sink, sanitizer}, plus language + framework + match shape. `match.type` ∈ {`call`, `member`, `global`, `annotation`} — 655 entries (149 source, 124 sink, 382 sanitizer — re-derive via `CATALOG.length` / a per-`kind` count rather than trusting a hardcoded number here again; this has already drifted once) spanning Express/Flask/FastAPI/Django/Rails/PHP/Go-net-http/Gin/Echo.
10
+ - **`match.type: 'call'`/`'member'`/`'global'`** are matched against an *expression* the taint-walker encounters while visiting a CFG node (`matchSource` / `matchSinkOrSanitizer`) — `call` against a call expression's callee, `member` against a property-access chain (`match.object` + `match.prop`), `global` against a bare identifier read.
11
+ - **`match.type: 'annotation'`** (R14(a): Spring `@RequestParam`/`@PathVariable`/`@RequestBody`/`@RequestHeader`, ASP.NET Core `[FromQuery]`/`[FromBody]`/`[FromForm]`/`[FromRoute]`/`[FromHeader]`, NestJS `@Query()`/`@Body()`/`@Param()`/`@Headers()`) works differently from the other three, and this is the one fact worth internalizing before touching it: **it is not consulted during the CFG walk.** There is no CFG node for "this function's own declared parameter list." Instead `matchAnnotationParams` (`catalog.js`) is called once per function — by `engine.js`'s `_unionAnnotationTaint`, before `analyzeFunction` runs — against the IR's `fn.paramAnnotations` side-channel (`{index, name, decorator}[]`, populated by `parser-cs.js`/`parser-js.js`/`parser-java.js`; see `../ir/CLAUDE.md`), and the resulting tainted param names are unioned into the entry taint-state for that function/call-site. A reader who assumes `annotation` entries get matched against expressions mid-walk, the way `call`/`member`/`global` do, will be wrong.
12
+ - **Sanitizer entries are RECORDED, never trusted to kill taint.** `_sanitizersForExpr()` collects the sanitizer callees applied to the value reaching each sink argument — inline in the argument, or inherited from the variable it reads (`_sanitizersByVar`, updated on every assignment and cleared when the RHS is clean). They are stamped on the finding as `_sanitizersOnPath` and consumed by `sanitizer-gate.js`, which sets `sanitized` + `sanitizerProof` **only when the sanitizer's `appliesTo` family covers the finding's threat class** (`*` covers everything). The proof gate then demotes. Taint itself still dies only on clean re-assignment (`removePathAndDescendants`).
13
+ - **Why the sanitizer does not kill the taint.** A blanket "any sanitizer clears the flow" rule scores well on benchmarks and silently drops a real SQL injection whenever the code applied an HTML escaper — `htmlspecialchars` neutralises XSS and does nothing for SQLi. Labelling demotes confidence; killing taint would delete the finding. Recall-preserving, same precedent as `falsification.js` / `proof-gate.js`: never removed, never severity-touched.
14
+ - **The sanitization signal is may-, not must-.** `_sanitizersByVar` is keyed per variable within a function and is not branch-sensitive, so a value sanitized on only one path still carries the label. That is deliberate and safe *because* the label only demotes — the same imprecision under a taint-killing design would be a false negative.
15
+ - **The gate runs twice**, for the same reason `_applyIgnorePragmas` does: pass 1 in `engine.js` runs ~2300 lines before deep-mode IR findings are appended, so pass 2 runs immediately after `finalFindings.push(...irFindings)` and is scoped to `irFindings` only (re-running `annotateProofGate` over already-gated findings would demote them twice).
16
+ - Guarded by `bench/mutation/runner.mjs` (`npm run bench:mutation:check`), which scores verdict-flip correctness over metamorphic rewrites (verdict must hold) and adversarial near-misses (verdict must flip). Removing the family check makes it exit non-zero.
11
17
  - **`match.receiver`** (optional, regex source string) constrains a call to call sites whose receiver chain has a matching segment. Callee matching is by *bare name*, so `callee: 'write'` otherwise matches `process.stdout.write` and `fh.write` as readily as `document.write`. Works for both sinks (via `matchSinkOrSanitizer`) and sources (via `matchSource`'s CALL-source path). `match.receiverBase` is an optional, independent second pattern (both `_receiverAllowed` checks must pass) — use it when `receiver` alone would still match an unrelated same-named local, e.g. `py-flask-args-get` requires *both* a property segment (`args`/`form`/`headers`/…) *and* a `request`/`req` segment, so `args = parse(); args.get(x)` doesn't fire just because some catalog entry happens to be named `get`.
12
18
  - **`language`** is enforced by `_languageAllowed` (Phase 2) via a table (`_LANG_EXT` in `catalog.js`) covering all nine catalog languages — `js`, `py`, `cs`, `kt`, `go`, `php`, `rb`, `java` by literal regex kept byte-identical to `ir/index.js`'s own dispatch regexes (pinned by `test/phase2-scoping.test.js`, which compares `.source` directly, not just "does this regex literal appear somewhere in the file"), and `cpp` by delegating to `ir/parser-cpp.js`'s `cppExtRe()` so it can't drift from the parser. A language with no table entry stays permissive (matches any file) — additive by design, so it can't regress a language before its entry exists.
13
19
  - **Path feasibility.** Constant-folds `if` conditions to prune unreachable branches.
@@ -49,5 +55,7 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
49
55
 
50
56
  - **Path attribution.** If you're adding a sink to the catalog, set `argIndex` carefully. `'all'` means "any tainted arg fires"; a numeric index pinpoints THE arg whose taint matters. Wrong here → noisy findings with confused traces.
51
57
  - **Cache invalidation.** `SummaryCache` is in-memory per-scan. Cross-scan persistence lives in `incremental.js` (FR-incremental) but it's behind a separate flag. Don't conflate the two.
52
- - **Recursion.** The cache returns a bottom summary (`_recursive: true`) when it hits a function already on the stack. The engine relies on fixed-point iteration to refine but `runTaintEngine` does only ONE pass today. Recursive cycles will under-approximate.
53
- - **`AGENTIC_SECURITY_BLIND_BENCH=1` disables the deep engine** along with everything else bench-shape. If you're trying to bench taint quality, run with both `AGENTIC_SECURITY_DEEP=1` and `AGENTIC_SECURITY_BLIND_BENCH=0` (the default).
58
+ - **Recursion.** The cache returns a bottom summary (`_recursive: true`) when it hits a function already on the stack. The engine relies on fixed-point iteration to refine, and `runTaintEngine` does run a multi-pass loop (`MAX_FP_ITERS = 3`, `engine.js`) this gotcha previously claimed a single pass, which contradicted the "Recursion via fixed point" bullet above and was stale. Cycles that have not converged within 3 iterations still under-approximate.
59
+ - **`AGENTIC_SECURITY_BLIND_BENCH=1` does NOT disable the deep engine.** It gates the bench-shape detectors; `_deepRequested` in `engine.js` keys on `AGENTIC_SECURITY_DEEP === '1'` alone, so deep mode runs regardless. (This bullet previously claimed the opposite — verified false while building `bench/layer-recall/`, where setting `BLIND_BENCH=1` left taint recall unchanged at 20/210.) To turn the taint engine off, set `AGENTIC_SECURITY_DEEP=0`.
60
+ - **The corpus barely exercises this engine.** `bench/cve-replay` enables deep mode only for its 6 `deep`-tier entries, and with the deep engine off entirely, corpus detection falls only 210 → 204. Taint quality is measured by `npm run bench:layer-recall` (which forces deep on for all 210), not by the corpus gate — see `docs/METRICS.md`.
61
+ - **Resolved: `stub-aware-filter.js` no longer mutates `severity`.** It used to (`f.severity = downgrade[f.severity]`, opt-in via `AGENTIC_SECURITY_TYPE_STUBS=1`), which violated the recall-preserving convention every other precision annotator in this directory follows. Flagged by documentation audit, then fixed on maintainer decision: it now demotes `confidence` (× 0.4, floor 0.01) + `confidenceTier` + `exploitabilityTier` by one step each, mirroring `proof-gate.js`'s own demotion shape exactly (`_confidenceBeforeStubFilter` records the pre-demotion value, a distinct field from `proof-gate.js`'s `_confidenceBeforeProofGate` so the two compose if both fire on the same finding). Severity is never touched.
@@ -125,7 +125,7 @@ export function lookupBuiltinSummary(calleeName) {
125
125
  if (lastDot > 0) {
126
126
  const short = calleeName.slice(lastDot + 1);
127
127
  const fallback = BUILTIN_SUMMARIES.get(short);
128
- if (fallback) return null;
128
+ if (fallback) return fallback;
129
129
  }
130
130
  return null;
131
131
  }
@@ -117,6 +117,7 @@ const JS_XML_JSON = [
117
117
  ];
118
118
 
119
119
  const JS_VALIDATORS = [
120
+ san('js', 'validator', 'validator.escape', ['xss']),
120
121
  san('js', 'validator', 'validator.isEmail', ['xss', 'sql']),
121
122
  san('js', 'validator', 'validator.isURL', ['url']),
122
123
  san('js', 'validator', 'validator.isUUID', ['*']),
@@ -63,10 +63,10 @@ export const CATALOG = [
63
63
 
64
64
  // ─── SINKS (JS/TS) ─────────────────────────────────────────────────────────
65
65
  // SQL.
66
- { kind: 'sink', id: 'js-sql-query', language: 'js', framework: 'sql', match: { type: 'call', callee: 'query' }, argIndex: 0,
66
+ { kind: 'sink', id: 'js-sql-query', language: 'js', framework: 'sql', match: { type: 'call', callee: 'query', receiverTypeIn: ['db|pool|conn(?:ection)?|client|sql|database|pg|mysql|sequelize|knex|prisma'] }, argIndex: 0,
67
67
  vuln: { name: 'SQL Injection (db.query)', severity: 'critical', cwe: 'CWE-89',
68
68
  remediation: 'Use parameterized queries: db.query("SELECT * FROM t WHERE id = ?", [id]). Never interpolate untrusted strings into SQL.' } },
69
- { kind: 'sink', id: 'js-sql-execute', language: 'js', framework: 'sql', match: { type: 'call', callee: 'execute' }, argIndex: 0,
69
+ { kind: 'sink', id: 'js-sql-execute', language: 'js', framework: 'sql', match: { type: 'call', callee: 'execute', receiverTypeIn: ['db|pool|conn(?:ection)?|client|sql|database|pg|mysql|sequelize|knex|prisma'] }, argIndex: 0,
70
70
  vuln: { name: 'SQL Injection (db.execute)', severity: 'critical', cwe: 'CWE-89',
71
71
  remediation: 'Use parameterized queries: db.execute("SELECT * FROM t WHERE id = ?", [id]).' } },
72
72
  // OS command.
@@ -173,8 +173,25 @@ export const CATALOG = [
173
173
  { kind: 'source', id: 'java-request-getReader', language: 'java', framework: 'servlet', match: { type: 'call', callee: 'getReader' }, label: 'request.getReader' },
174
174
  { kind: 'source', id: 'java-system-getenv', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'getenv' }, label: 'System.getenv' },
175
175
  { kind: 'source', id: 'java-system-getProperty', language: 'java', framework: 'stdlib', match: { type: 'call', callee: 'getProperty' }, label: 'System.getProperty' },
176
- // Spring annotation-style sources are detected per-rule, not as catalog
177
- // members (they're parameter decorators rather than callable shapes).
176
+
177
+ // ─── SOURCES (Annotation/Decorator-shaped) ────────────────────────────────
178
+ // R14(a): annotation/decorator-shaped framework sources (Spring @RequestParam,
179
+ // ASP.NET Core [FromQuery], NestJS @Query()). These are indexed and matched via
180
+ // matchAnnotationParams, which is called once per function against the IR's
181
+ // paramAnnotations side-channel.
182
+ { kind: 'source', id: 'java-spring-requestparam', language: 'java', framework: 'spring', match: { type: 'annotation', name: 'RequestParam' }, label: '@RequestParam (Spring)' },
183
+ { kind: 'source', id: 'java-spring-pathvariable', language: 'java', framework: 'spring', match: { type: 'annotation', name: 'PathVariable' }, label: '@PathVariable (Spring)' },
184
+ { kind: 'source', id: 'java-spring-requestbody', language: 'java', framework: 'spring', match: { type: 'annotation', name: 'RequestBody' }, label: '@RequestBody (Spring)' },
185
+ { kind: 'source', id: 'java-spring-requestheader', language: 'java', framework: 'spring', match: { type: 'annotation', name: 'RequestHeader' }, label: '@RequestHeader (Spring)' },
186
+ { kind: 'source', id: 'cs-aspnet-fromquery', language: 'cs', framework: 'aspnet', match: { type: 'annotation', name: 'FromQuery' }, label: '[FromQuery] (ASP.NET Core)' },
187
+ { kind: 'source', id: 'cs-aspnet-frombody', language: 'cs', framework: 'aspnet', match: { type: 'annotation', name: 'FromBody' }, label: '[FromBody] (ASP.NET Core)' },
188
+ { kind: 'source', id: 'cs-aspnet-fromform', language: 'cs', framework: 'aspnet', match: { type: 'annotation', name: 'FromForm' }, label: '[FromForm] (ASP.NET Core)' },
189
+ { kind: 'source', id: 'cs-aspnet-fromroute', language: 'cs', framework: 'aspnet', match: { type: 'annotation', name: 'FromRoute' }, label: '[FromRoute] (ASP.NET Core)' },
190
+ { kind: 'source', id: 'cs-aspnet-fromheader', language: 'cs', framework: 'aspnet', match: { type: 'annotation', name: 'FromHeader' }, label: '[FromHeader] (ASP.NET Core)' },
191
+ { kind: 'source', id: 'js-nestjs-query', language: 'js', framework: 'nestjs', match: { type: 'annotation', name: 'Query' }, label: '@Query() (NestJS)', provenance: 'url-param' },
192
+ { kind: 'source', id: 'js-nestjs-body', language: 'js', framework: 'nestjs', match: { type: 'annotation', name: 'Body' }, label: '@Body() (NestJS)', provenance: 'http-body' },
193
+ { kind: 'source', id: 'js-nestjs-param', language: 'js', framework: 'nestjs', match: { type: 'annotation', name: 'Param' }, label: '@Param() (NestJS)', provenance: 'path-param' },
194
+ { kind: 'source', id: 'js-nestjs-headers', language: 'js', framework: 'nestjs', match: { type: 'annotation', name: 'Headers' }, label: '@Headers() (NestJS)', provenance: 'header' },
178
195
 
179
196
  // ─── SOURCES (Go) ─────────────────────────────────────────────────────────
180
197
  { kind: 'source', id: 'go-r-form', language: 'go', framework: 'net/http', match: { type: 'member', object: 'r', prop: 'Form' }, label: 'r.Form' },
@@ -360,7 +377,7 @@ export const CATALOG = [
360
377
  remediation: 'Sanitize the __html field via DOMPurify before passing it to dangerouslySetInnerHTML — better, render text via children.' } },
361
378
 
362
379
  // ─── SINKS (HTTP outbound / SSRF) ─────────────────────────────────────────
363
- { kind: 'sink', id: 'py-requests-get', language: 'py', framework: 'requests', match: { type: 'call', callee: 'get' }, argIndex: 0,
380
+ { kind: 'sink', id: 'py-requests-get', language: 'py', framework: 'requests', match: { type: 'call', callee: 'get', receiverTypeIn: ['requests|session|client|http'] }, argIndex: 0,
364
381
  vuln: { name: 'SSRF (requests.get)', severity: 'high', cwe: 'CWE-918',
365
382
  remediation: 'Resolve the URL host and reject RFC1918 + metadata endpoints before fetching. Use an allow-list.' } },
366
383
  { kind: 'sink', id: 'py-requests-post', language: 'py', framework: 'requests', match: { type: 'call', callee: 'post' }, argIndex: 0,
@@ -414,7 +431,7 @@ export const CATALOG = [
414
431
  { kind: 'sink', id: 'py-jinja-from-string', language: 'py', framework: 'jinja2', match: { type: 'call', callee: 'from_string' }, argIndex: 0,
415
432
  vuln: { name: 'SSTI (Jinja2.from_string)', severity: 'critical', cwe: 'CWE-94',
416
433
  remediation: 'Never feed a user-supplied string into a template engine. Use pre-registered templates and pass values as variables.' } },
417
- { kind: 'sink', id: 'rb-erb-new', language: 'rb', framework: 'erb', match: { type: 'call', callee: 'new' }, argIndex: 0,
434
+ { kind: 'sink', id: 'rb-erb-new', language: 'rb', framework: 'erb', match: { type: 'call', callee: 'new', receiverTypeIn: ['^ERB$'] }, argIndex: 0,
418
435
  vuln: { name: 'SSTI (ERB.new)', severity: 'critical', cwe: 'CWE-94',
419
436
  remediation: 'Use pre-existing templates with binding/locals — never construct a template from user input.' } },
420
437
  { kind: 'sink', id: 'js-handlebars-compile',language: 'js', framework: 'handlebars', match: { type: 'call', callee: 'compile' }, argIndex: 0,
@@ -605,7 +622,7 @@ export const CATALOG = [
605
622
  vuln: { name: 'Unsafe Deserialization (yaml.load)', severity: 'high', cwe: 'CWE-502',
606
623
  remediation: 'Use yaml.safe_load instead of yaml.load on untrusted YAML.' } },
607
624
  // SSRF / HTTP-out.
608
- { kind: 'sink', id: 'py-requests-get-v2', language: 'py', framework: 'requests', match: { type: 'call', callee: 'get' }, argIndex: 0,
625
+ { kind: 'sink', id: 'py-requests-get-v2', language: 'py', framework: 'requests', match: { type: 'call', callee: 'get', receiverTypeIn: ['requests|session|client|http'] }, argIndex: 0,
609
626
  vuln: { name: 'SSRF (requests.get)', severity: 'high', cwe: 'CWE-918',
610
627
  remediation: 'Resolve the host first, reject 169.254.169.254 / RFC1918 / localhost; or proxy through a server-side allow-list.' } },
611
628
  { kind: 'sink', id: 'py-requests-post-v2', language: 'py', framework: 'requests', match: { type: 'call', callee: 'post' }, argIndex: 0,
@@ -903,6 +920,46 @@ function _receiverAllowed(entry, calleeExpr) {
903
920
  if (basePat && !segs.some((s) => new RegExp(basePat).test(String(s)))) return false;
904
921
  return true;
905
922
  }
923
+
924
+ // PRD R6 (docs/DETECTION_GAP_REMEDIATION_PRD.md): a SECOND, independent
925
+ // receiver constraint — this one checked against the CHA-INFERRED TYPE of
926
+ // the receiver (computed by the caller, engine.js's `_receiverTypeFor`), not
927
+ // the textual receiver-chain segments `_receiverAllowed` above checks. The
928
+ // two are complementary: `_receiverAllowed`'s `match.receiver` regex can only
929
+ // ever see the SOURCE TEXT of the call site (`db.query` vs `cache.query` —
930
+ // both pass any receiver regex that doesn't special-case exact names); this
931
+ // gate can additionally use a resolved class/variable-type hint, so a bare
932
+ // `.query()` on something confidently NOT database-shaped can be excluded
933
+ // without hand-listing every possible non-DB variable name as a `receiver`
934
+ // exclusion (which `_receiverAllowed`'s regex form cannot express at all —
935
+ // it only expresses required patterns, never forbidden ones).
936
+ //
937
+ // Unknown != clean: a null/undefined receiverType (CHA could not resolve
938
+ // anything for this call site) NEVER suppresses a match — only a receiver
939
+ // type that was confidently resolved and does not appear in the entry's
940
+ // `receiverTypeIn` allow-list does. An entry with no `receiverTypeIn` is
941
+ // completely unaffected by this gate (returns true unconditionally), exactly
942
+ // like `_receiverAllowed` when neither `receiver` nor `receiverBase` is set.
943
+ //
944
+ // The vocabulary patterns are deliberately UNANCHORED (substring, case-
945
+ // insensitive). They were originally written `^(?:db|pool|conn…)$` back when
946
+ // the value reaching this gate could be a bare variable/field NAME. It no
947
+ // longer can — after the whole-branch-review fix, `_receiverTypeFor` only
948
+ // ever yields a real `classOfVar`-resolved CLASS name — and real class names
949
+ // are compound: `DatabaseConnection`, `PrismaClient`, `MySQLConnection` all
950
+ // failed the exact-anchored form and were silently suppressed, while only a
951
+ // class literally named `Db` survived. Substring matching is the honest
952
+ // shape for a vocabulary check over compound identifiers. `rb-erb-new`
953
+ // stays anchored (`^ERB$`) because it names ONE exact class, not a
954
+ // vocabulary. Over-matching here is the safe direction: it only ever
955
+ // *permits* a match the pattern layer already made.
956
+ function _receiverTypeAllowed(entry, receiverType) {
957
+ const pats = entry.match && entry.match.receiverTypeIn;
958
+ if (!pats || !pats.length) return true;
959
+ if (!receiverType) return true;
960
+ return pats.some((p) => new RegExp(p, 'i').test(String(receiverType)));
961
+ }
962
+
906
963
  // Merge the expanded sanitizer catalog. We dedupe on `id` (case-insensitive)
907
964
  // so a base-catalog entry always wins over a same-id expanded one — the base
908
965
  // catalog is the curated/blessed surface; the expansion is additive coverage.
@@ -940,6 +997,7 @@ for (const e of CATALOG) {
940
997
  const CALLEE_INDEX = new Map();
941
998
  const MEMBER_INDEX = new Map();
942
999
  const GLOBAL_INDEX = new Map();
1000
+ const ANNOTATION_INDEX = new Map();
943
1001
  for (const e of CATALOG) {
944
1002
  if (!e.match) continue;
945
1003
  if (e.match.type === 'call' && e.match.callee && e.match.callee !== '*') {
@@ -958,6 +1016,10 @@ for (const e of CATALOG) {
958
1016
  const k = e.match.name;
959
1017
  if (!GLOBAL_INDEX.has(k)) GLOBAL_INDEX.set(k, []);
960
1018
  GLOBAL_INDEX.get(k).push(e);
1019
+ } else if (e.match.type === 'annotation' && e.match.name) {
1020
+ const k = e.match.name;
1021
+ if (!ANNOTATION_INDEX.has(k)) ANNOTATION_INDEX.set(k, []);
1022
+ ANNOTATION_INDEX.get(k).push(e);
961
1023
  }
962
1024
  }
963
1025
 
@@ -1007,6 +1069,35 @@ function _globalKey(name) {
1007
1069
  return typeof name === 'string' && name.charCodeAt(0) === 36 /* '$' */ ? name.slice(1) : name;
1008
1070
  }
1009
1071
 
1072
+ // PRD R4a (docs/DETECTION_GAP_REMEDIATION_PRD.md): this module's own shape
1073
+ // contract above documents two callee forms — 'name' (bare, matched by last
1074
+ // segment) and 'name.foo' (matched by FULL PATH) — but both lookup sites
1075
+ // used to reduce every callee to its last segment unconditionally, so an
1076
+ // entry indexed under a full dotted key (catalog-expanded.js's `san()`
1077
+ // helper builds many: 'Encode.forHtml', 'pg.escapeLiteral',
1078
+ // 'filepath.Clean', ...) could never be retrieved. An entry lives under
1079
+ // exactly one CALLEE_INDEX key (its own `match.callee`, dotted or bare), so
1080
+ // looking up both the full path and the last segment and concatenating
1081
+ // hits never double-counts — it just also finds entries the last-segment
1082
+ // key alone was missing.
1083
+ function _calleeIndexHits(calleeExpr) {
1084
+ let last = null;
1085
+ let full = null;
1086
+ if (typeof calleeExpr === 'string') {
1087
+ full = calleeExpr;
1088
+ last = calleeExpr.includes('.') ? calleeExpr.slice(calleeExpr.lastIndexOf('.') + 1) : calleeExpr;
1089
+ } else if (calleeExpr && calleeExpr.kind === 'member' && calleeExpr.prop) {
1090
+ last = calleeExpr.prop;
1091
+ if (calleeExpr.object && calleeExpr.object.kind === 'ident') full = `${calleeExpr.object.name}.${calleeExpr.prop}`;
1092
+ } else if (calleeExpr && calleeExpr.kind === 'ident') {
1093
+ last = calleeExpr.name || null;
1094
+ }
1095
+ const raw = [];
1096
+ if (full && full !== last) { const h = CALLEE_INDEX.get(full); if (h) raw.push(...h); }
1097
+ if (last) { const h = CALLEE_INDEX.get(last); if (h) raw.push(...h); }
1098
+ return raw;
1099
+ }
1100
+
1010
1101
  export function matchSource(expr, file) {
1011
1102
  if (!expr) return null;
1012
1103
  // Member sources (req.query): the original path — unchanged.
@@ -1041,41 +1132,76 @@ export function matchSource(expr, file) {
1041
1132
  // sources were never recognized at the assignment RHS. Match by callee last
1042
1133
  // segment (Go gives a dotted string; JS/Py a member/ident expr).
1043
1134
  if (expr.kind === 'call') {
1044
- let cn = null;
1045
- if (typeof expr.callee === 'string') cn = expr.callee.includes('.') ? expr.callee.slice(expr.callee.lastIndexOf('.') + 1) : expr.callee;
1046
- else if (expr.callee && expr.callee.kind === 'member') cn = expr.callee.prop;
1047
- else if (expr.callee && expr.callee.kind === 'ident') cn = expr.callee.name;
1048
- if (cn) {
1049
- const raw = CALLEE_INDEX.get(cn);
1050
- if (raw) {
1051
- const hits = filterByProvenance(raw)
1052
- .filter(h => _languageAllowed(h, file))
1053
- .filter(h => _receiverAllowed(h, expr.callee));
1054
- const s = hits.find(h => h.kind === 'source');
1055
- if (s) return s;
1056
- }
1135
+ const raw = _calleeIndexHits(expr.callee);
1136
+ if (raw.length) {
1137
+ const hits = filterByProvenance(raw)
1138
+ .filter(h => _languageAllowed(h, file))
1139
+ .filter(h => _receiverAllowed(h, expr.callee));
1140
+ const s = hits.find(h => h.kind === 'source');
1141
+ if (s) return s;
1057
1142
  }
1058
1143
  }
1059
1144
  return null;
1060
1145
  }
1061
1146
 
1062
- export function matchSinkOrSanitizer(calleeExpr, file) {
1147
+ export function matchSinkOrSanitizer(calleeExpr, file, receiverType) {
1063
1148
  if (!calleeExpr) return null;
1064
- let calleeName = null;
1065
- // R3 (PRD §5): the Go IR (and other string-callee parsers) represent a call
1066
- // target as a dotted STRING ("db.Query") rather than a member expr. Match on
1067
- // the last segment so those languages' sinks/sanitizers are recognized too.
1068
- if (typeof calleeExpr === 'string') calleeName = calleeExpr.includes('.') ? calleeExpr.slice(calleeExpr.lastIndexOf('.') + 1) : calleeExpr;
1069
- else if (calleeExpr.kind === 'ident') calleeName = calleeExpr.name;
1070
- else if (calleeExpr.kind === 'member') calleeName = calleeExpr.prop;
1071
- if (!calleeName) return null;
1072
- const raw = CALLEE_INDEX.get(calleeName);
1149
+ const raw = _calleeIndexHits(calleeExpr);
1150
+ if (!raw.length) return null;
1151
+ const hits = filterByProvenance(raw)
1152
+ .filter(h => _languageAllowed(h, file))
1153
+ .filter(h => _receiverAllowed(h, calleeExpr))
1154
+ .filter(h => _receiverTypeAllowed(h, receiverType));
1155
+ return hits.length ? hits : null;
1156
+ }
1157
+
1158
+ // PRD R13(a) (docs/DETECTION_GAP_REMEDIATION_PRD.md): three sink entries
1159
+ // (js-innerHTML-assign, js-outerHTML-assign, react-dangerouslySetInnerHTML)
1160
+ // have sat in MEMBER_INDEX under the object:'_any_' wildcard since they were
1161
+ // added, but nothing ever queried MEMBER_INDEX for an ASSIGNMENT TARGET —
1162
+ // matchSinkOrSanitizer only reads CALLEE_INDEX, and matchSource's MEMBER_INDEX
1163
+ // lookups are for READS keyed by a SPECIFIC object name (e.g. "document.cookie"),
1164
+ // not the "any receiver" wildcard a DOM sink needs. `targetPath` is the
1165
+ // flattened LHS access-path string parser-js.js's lhsPath already produces
1166
+ // for a member-expression assignment target (e.g. "el.innerHTML" for
1167
+ // `el.innerHTML = x`) — this function extracts the property name and looks
1168
+ // it up under the wildcard key the same way `object:'_any_'` entries are
1169
+ // indexed (catalog.js's own indexing loop keys every entry by
1170
+ // `${match.object}.${match.prop}` regardless of what object names are, so a
1171
+ // wildcard entry lives under the literal key "_any_.<prop>").
1172
+ export function matchMemberWriteSink(targetPath, file) {
1173
+ if (typeof targetPath !== 'string' || !targetPath.includes('.')) return null;
1174
+ const prop = targetPath.slice(targetPath.lastIndexOf('.') + 1);
1175
+ if (!prop) return null;
1176
+ const raw = MEMBER_INDEX.get(`_any_.${prop}`);
1073
1177
  if (!raw) return null;
1074
1178
  const hits = filterByProvenance(raw)
1075
1179
  .filter(h => _languageAllowed(h, file))
1076
- .filter(h => _receiverAllowed(h, calleeExpr));
1180
+ .filter(h => h.kind === 'sink');
1077
1181
  return hits.length ? hits : null;
1078
1182
  }
1079
1183
 
1184
+ // R14(a): annotation/decorator-shaped framework sources (Spring @RequestParam,
1185
+ // ASP.NET Core [FromQuery], NestJS @Query()). Unlike matchSource/matchSinkOrSanitizer,
1186
+ // this does not consult an expression encountered while walking a CFG node —
1187
+ // there is no CFG node for "this function's own declared parameter list." It is
1188
+ // consulted once per function, against the IR's paramAnnotations side-channel,
1189
+ // by engine.js's _unionAnnotationTaint before each analyzeFunction call.
1190
+ export function matchAnnotationParams(paramAnnotations, file) {
1191
+ const tainted = new Set();
1192
+ if (!paramAnnotations || !paramAnnotations.length) return tainted;
1193
+ for (const pa of paramAnnotations) {
1194
+ const raw = ANNOTATION_INDEX.get(pa.decorator);
1195
+ if (!raw) continue;
1196
+ const hits = filterByProvenance(raw)
1197
+ .filter(h => _languageAllowed(h, file))
1198
+ .filter(h => h.kind === 'source');
1199
+ if (hits.length) {
1200
+ tainted.add(pa.name);
1201
+ }
1202
+ }
1203
+ return tainted;
1204
+ }
1205
+
1080
1206
  // For tests and reflection.
1081
1207
  export function _catalogSize() { return CATALOG.length; }