@clear-capabilities/agentic-security-scanner 0.136.9 → 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.
- package/CHANGELOG.md +644 -0
- package/bin/agentic-security.js +3 -0
- package/dist/435.index.js +9 -1
- package/dist/agentic-security.mjs +3 -3
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -8
- package/src/dataflow/CLAUDE.md +3 -1
- package/src/dataflow/catalog-expanded.js +1 -0
- package/src/dataflow/catalog.js +157 -31
- package/src/dataflow/engine.js +318 -55
- package/src/dataflow/index.js +15 -0
- package/src/dataflow/points-to.js +19 -6
- package/src/engine.js +281 -207
- package/src/ir/CLAUDE.md +14 -3
- package/src/ir/class-hierarchy.js +57 -11
- package/src/ir/index.js +14 -2
- package/src/ir/parser-cs.js +451 -31
- package/src/ir/parser-java.js +205 -2
- package/src/ir/parser-js.js +149 -2
- package/src/ir/parser-kt.js +436 -18
- package/src/ir/parser-php.js +587 -29
- package/src/ir/parser-py.helper.py +32 -2
- package/src/ir/parser-py.js +31 -4
- package/src/ir/parser-rb.js +124 -19
- package/src/lsp/server.js +7 -1
- package/src/mcp/tools.js +9 -1
- package/src/posture/clustering.js +12 -1
- package/src/posture/sbom.js +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
c8726408d88159f1954448e223340231893809e7cfeaa72d0fce196a39a1395c 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.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": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@babel/preset-react": "^8.0.1",
|
|
41
41
|
"@babel/preset-typescript": "^8.0.1",
|
|
42
42
|
"java-parser": "^3.0.1",
|
|
43
|
-
"js-yaml": "^5.
|
|
43
|
+
"js-yaml": "^5.3.0",
|
|
44
44
|
"safe-regex": "^2.1.1"
|
|
45
45
|
},
|
|
46
46
|
"optionalDependencies": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^26.2.0",
|
|
52
|
-
"@vercel/ncc": "^0.
|
|
52
|
+
"@vercel/ncc": "^0.45.0"
|
|
53
53
|
},
|
|
54
54
|
"overrides": {
|
|
55
55
|
"lodash": "^4.18.1",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs && node ../scripts/release-check.mjs",
|
|
62
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
63
|
"test:python": "node ../scripts/run-python-tests.mjs",
|
|
64
|
-
"test:smoke": "node --test test/smoke.test.js",
|
|
64
|
+
"test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
|
|
65
65
|
"test:glob": "node --test test/glob-compat.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/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",
|
|
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",
|
|
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-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/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-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/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.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
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
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",
|
|
71
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",
|
package/src/dataflow/CLAUDE.md
CHANGED
|
@@ -6,7 +6,9 @@ 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.
|
|
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.
|
|
10
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`).
|
|
11
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.
|
|
12
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.
|
|
@@ -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', ['*']),
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -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'
|
|
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
|
-
|
|
177
|
-
//
|
|
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'
|
|
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
|
-
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
if (
|
|
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
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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 =>
|
|
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; }
|