@clear-capabilities/agentic-security-scanner 0.187.0 → 0.188.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 +9 -0
- package/dist/{deabc47d0e27e21bf99b.json → 92f78fd3e2dbe30fb58d.json} +3 -3
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/dataflow/CLAUDE.md +13 -0
- package/src/dataflow/catalog.js +14 -0
- package/src/dataflow/engine.js +131 -10
- package/src/engine.js +11 -3
- package/src/language/CLAUDE.md +7 -0
- package/src/language/haskell-comments.js +65 -0
- package/src/language/haskell-findings.js +2 -1
- package/src/language/haskell-fix.js +118 -2
- package/src/language/haskell-guards.js +2 -2
- package/src/language/haskell-ir.js +46 -3
- package/src/language/haskell-models.js +15 -4
- package/src/language/haskell-security-rules.js +113 -9
- package/src/language/haskell-trust.js +268 -0
- package/src/language/haskell-web.js +2 -1
- package/src/sast/cloud-iam.js +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
f70bb09f8b0030636bb4ba27a72de9beb7087bcedadb2d27127bad3cfb960f01 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.188.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -87,13 +87,13 @@
|
|
|
87
87
|
"test:glob": "node --test test/glob-compat.test.js",
|
|
88
88
|
"test:sast": "node --test test/dag-precision.test.js test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js 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/zip-slip-containment-guard.test.js test/zip-slip-go.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.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/java-structural-cross-file.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/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js test/pipeline-env-secret.test.js test/mark-used-vuln-functions.test.js",
|
|
89
89
|
"test:posture": "node --test test/contracts/root-cause-sweep.contract.test.js test/contracts/model-validation.contract.test.js test/contracts/agentic-discovery.contract.test.js test/contracts/local-models.contract.test.js test/contracts/dependency-finding.contract.test.js test/contracts/taint-flow.contract.test.js test/contracts/runtime-corroboration.contract.test.js test/compliance-coverage.test.js test/contracts/blast-radius.contract.test.js test/contracts/federation.contract.test.js test/contracts/eval-lab-report.contract.test.js test/contracts/fleet.contract.test.js test/contracts/rule-compiler.contract.test.js test/contracts/release-assurance.contract.test.js test/contracts/dnr.contract.test.js test/contracts/strategic-metrics.contract.test.js test/contracts/exploit-proof.contract.test.js test/contracts/artifact-provenance.contract.test.js test/dnr/artifact-join.test.js test/rule-pack-signing.test.js test/threat-model.test.js test/git-hardening.test.js 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/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-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/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.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/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.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/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.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/stdout-flush.test.js test/format-determinism.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 test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/code-boundaries.test.js test/fleet/job-runner.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/eval-lab/import-independent.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/window-store-view.test.js test/dnr/exploit-correlation.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/proof-fabric/generate-haskell.test.js test/proof-fabric/generate-haskell-widened.test.js test/cli/proof-lifecycle-demo.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/synthesis.test.js test/rule-compiler/structure.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js test/witness-bundle.test.js",
|
|
90
|
-
"test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js 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/chrome-probe.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-rust.test.js test/rust-taint-flow.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-cs-p1.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/return-sink-check.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 test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/rx-exec-receiver.test.js test/incremental-dependency.test.js test/incremental-integrity.test.js test/demand-query.test.js test/ir-cache.test.js test/canonicalize-parity.test.js",
|
|
90
|
+
"test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js 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/chrome-probe.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-rust.test.js test/rust-taint-flow.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-cs-p1.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/return-sink-check.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 test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/rx-exec-receiver.test.js test/incremental-dependency.test.js test/incremental-integrity.test.js test/demand-query.test.js test/ir-cache.test.js test/canonicalize-parity.test.js test/dataflow-source-attribution.test.js",
|
|
91
91
|
"test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.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 test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js test/capabilities/tools.test.js",
|
|
92
92
|
"test:report": "node --test test/report-scan-invariants.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/git-history-batch.test.js test/default-scan-drift.test.js test/bundle-optional-deps.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
|
|
93
93
|
"test:lineage": "node --test test/lineage/analysis-completion.test.js test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js test/lineage/runtime-assurance.test.js test/lineage/runtime-edge-states.test.js test/lineage/runtime-explorer-view.test.js test/cli/runtime-explorer-cli.test.js test/cli/runtime-identity-cli.test.js test/cli/runtime-twin-cli.test.js test/lineage/cut-planner.test.js test/lineage/scc.test.js test/lineage/scc-solver.test.js test/graph/graph-views.test.js test/lineage/dag07-completion.test.js test/lineage/owned-build.test.js test/dataflow/boundary-propagation.test.js test/lineage/boundary-contract.test.js test/lineage/correlation-v2.test.js test/lineage/path-control-evidence.test.js test/lineage/smt-z3-backend.test.js test/cli/dag-commands.test.js",
|
|
94
94
|
"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",
|
|
95
95
|
"test:server": "node --test test/server/graph-loader.test.js test/server/security.test.js test/server/routes.test.js test/server/http-server.test.js test/server/cmd-explore.test.js test/server/cmd-dataflow-export.test.js test/server/static-assets.test.js test/server/dataflow-scenario-cli.test.js test/server/dataflow-impact-cli.test.js",
|
|
96
|
-
"test:haskell": "node --test test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js",
|
|
96
|
+
"test:haskell": "node --test test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js test/haskell/haskell-trust-boundary.test.js test/haskell/haskell-comment-invariance.test.js test/haskell/haskell-remediation-classes.test.js test/haskell/haskell-trust-boundary-frameworks.test.js test/haskell/haskell-generic-detectors.test.js",
|
|
97
97
|
"test:nix": "node --test test/nix/nix-option-types.test.js test/nix/nixos-hardening-daemon.test.js test/nix/nixos-hardening-extended.test.js test/nix/nix-build-trust.test.js test/nix/nix-eval-isolation.test.js test/nix/nix-input-inventory.test.js test/nix/nix-parser-ir.test.js test/nix/nix-remediation.test.js test/nix/nix-resolved-closure.test.js test/nix/nix-sca-patches.test.js test/nix/nix-sca-severity.test.js test/nix/nix-haskell-closure-names.test.js test/nix/nix-script-taint.test.js test/nix/nix-secrets-store.test.js test/nix/nixos-hardening.test.js test/nix/nixos-module-resolution.test.js test/nix/nix-fix-multifile.test.js test/nix/nixos-evaluator-forms.test.js test/nix/nix-advisory-feed.test.js test/nix/nix-fix-prewrite.test.js test/nix/nix-boundary-scope.test.js",
|
|
98
98
|
"test:language": "node --test test/language/capability-ledger.test.js test/language/language-accuracy-regression.test.js test/language/language-aibom.test.js test/language/language-assurance.test.js test/language/language-bom.test.js test/language/language-compliance.test.js test/language/language-contracts.test.js test/language/language-corpora-integrity.test.js test/language/language-discovery.test.js test/language/language-doc-coverage.test.js test/language/language-evidence-posture.test.js test/language/language-explorer.test.js test/language/language-gap-fixes.test.js test/language/language-gap-fixes-2.test.js test/language/language-governance.test.js test/language/language-integrations.test.js test/language/language-llm-agent.test.js test/language/language-local-ai.test.js test/language/language-parity-haskell.test.js test/language/language-polyglot-bridges.test.js test/language/language-privacy-lineage.test.js test/language/language-proof-remediation.test.js test/language/language-report-formats.test.js test/language/language-secrets.test.js test/language/language-trust-boundary.test.js",
|
|
99
99
|
"test:language-stress": "node --test --test-concurrency=1 test/language/language-stress-offline.test.js",
|
package/src/dataflow/CLAUDE.md
CHANGED
|
@@ -38,6 +38,19 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
|
|
|
38
38
|
- **Flat-string callees — the one that mattered most.** The mutator rule tested `callee.kind === 'member'`, a shape ONLY `parser-js.js` (Babel) emits. The seven hand-rolled parsers emit a flat dot-joined string (`"items.append"`), so container writes in Python/Ruby/PHP/Go/Java/C#/Kotlin never matched at all. Same frontend duality `_calleeReceiverTainted` documents. This was invisible from the outside because a PY-SAST pattern rule caught the same sink, so the taint-layer miss looked like a detection.
|
|
39
39
|
- **Measured honestly:** `bench:layer-recall` is UNCHANGED at 116/215 after this landed — the corpus holds no container-shaped entries, which is exactly why the taint-recall-to-80% push closed at 83.9% (115/137 of the dataflow-shaped subset) "without Tier 5". This is capability proven by targeted tests (`test/container-taint.test.js`, including two precision cases that must NOT fire), not a measured recall gain. Do not quote it as one. Self-scan held at 427 with zero new findings across ~700 files.
|
|
40
40
|
|
|
41
|
+
## Source attribution (evidence chain origin)
|
|
42
|
+
|
|
43
|
+
A finding's `source`/`chain` come from `_taintSources`: records keyed by the variable a source call was assigned to. Before this was
|
|
44
|
+
extended, a value DERIVED from a source (`b = 'ls ' + a`), a value that crossed a call, or one returned from a function reached the sink with an
|
|
45
|
+
empty chain, so nothing downstream could tell a request read from a command-line argument. Now: `_inheritSources` copies the records reaching
|
|
46
|
+
a derived value; a summary computed under an entry-taint state names its tainted parameters with placeholder records (`_paramPlaceholders`,
|
|
47
|
+
`isParam`) that `_mergeSummaryFindings` resolves, at the call site that merges the finding, to the sources of the actual arguments
|
|
48
|
+
(`_resolveParams`); a function's return keeps the sources behind it (`returnSources`, followed through `from` into the finding's
|
|
49
|
+
`sourceRoots`). A parameter annotation that is a catalog source is recorded too (`matchAnnotationParamSources`, `annotation: true`, mapped to
|
|
50
|
+
`source.kind: 'parameter'`). The summary cache is keyed by entry state only, so the cached summary never carries a caller's sources: they
|
|
51
|
+
are added to a COPY at each merge. Pinned in `test/dataflow-source-attribution.test.js`. The multi-sink aggregation in `engine.js` ignores
|
|
52
|
+
`info` findings (a finding lowered by a guard or a trust verdict is not a flow worth aggregating).
|
|
53
|
+
|
|
41
54
|
## Scope — what we still do NOT model (today)
|
|
42
55
|
|
|
43
56
|
- **Call-string (k>1) context-sensitivity.** Context is the *value* abstraction — which params are tainted at entry — not the call stack. Two call paths that reach a helper with the same tainted-arg shape share a summary. Entry-state granularity is also param-level, not arbitrary access paths (`f(obj)` with `obj.a` tainted ≡ `obj.b` tainted).
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -2743,5 +2743,19 @@ export function matchAnnotationParams(paramAnnotations, file) {
|
|
|
2743
2743
|
return tainted;
|
|
2744
2744
|
}
|
|
2745
2745
|
|
|
2746
|
+
// The same lookup as matchAnnotationParams, keeping what each matched source says about itself so the engine can record an origin for the
|
|
2747
|
+
// parameter (an evidence chain that begins at "parameter x of f, request input" instead of at the sink).
|
|
2748
|
+
export function matchAnnotationParamSources(paramAnnotations, file) {
|
|
2749
|
+
const out = [];
|
|
2750
|
+
if (!paramAnnotations || !paramAnnotations.length) return out;
|
|
2751
|
+
for (const pa of paramAnnotations) {
|
|
2752
|
+
const raw = ANNOTATION_INDEX.get(pa.decorator);
|
|
2753
|
+
if (!raw) continue;
|
|
2754
|
+
const hit = filterByProvenance(raw).filter(h => _languageAllowed(h, file)).find(h => h.kind === 'source');
|
|
2755
|
+
if (hit) out.push({ name: pa.name, id: hit.id, label: hit.label || pa.decorator, provenance: hit.provenance || null });
|
|
2756
|
+
}
|
|
2757
|
+
return out;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2746
2760
|
// For tests and reflection.
|
|
2747
2761
|
export function _catalogSize() { return CATALOG.length; }
|
package/src/dataflow/engine.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
// sanitizer must never silently drop a real vulnerability, so the walk never
|
|
43
43
|
// treats a sanitizer call as clearing the tainted path on its own.
|
|
44
44
|
|
|
45
|
-
import { matchSource, matchSinkOrSanitizer, matchMemberWriteSink, matchAnnotationParams } from './catalog.js';
|
|
45
|
+
import { matchSource, matchSinkOrSanitizer, matchMemberWriteSink, matchAnnotationParams, matchAnnotationParamSources } from './catalog.js';
|
|
46
46
|
import { functionRecord } from '../ir/callgraph.js';
|
|
47
47
|
import { accessPathOf, isCoveredBy, hasTaintedDescendant, addPath, removePathAndDescendants, joinSets as joinAccessSets, setsEqual as accessSetsEqual, pathIsCoveredByPrefix } from './access-paths.js';
|
|
48
48
|
import { aliasesForVar } from './points-to.js';
|
|
@@ -394,7 +394,7 @@ function _nestedCallReturnTainted(calleeExpr, argExprs, state, callContext, argT
|
|
|
394
394
|
if (!sum && fn && fn.cfg) {
|
|
395
395
|
sum = callContext._summaryCache.compute(qid, entry, () => {
|
|
396
396
|
const inner = {
|
|
397
|
-
_findings: [], _taintSources:
|
|
397
|
+
_findings: [], _taintSources: _paramPlaceholders(fn, entry), _returnTainted: false,
|
|
398
398
|
_stack: new Set(), deadlineMs: callContext.deadlineMs,
|
|
399
399
|
_summaryCache: callContext._summaryCache,
|
|
400
400
|
_callGraph: callContext._callGraph,
|
|
@@ -404,6 +404,7 @@ function _nestedCallReturnTainted(calleeExpr, argExprs, state, callContext, argT
|
|
|
404
404
|
try { analyzeFunction(fn, _unionAnnotationTaint(fn, entry), inner); } catch {}
|
|
405
405
|
return {
|
|
406
406
|
returnTainted: !!inner._returnTainted,
|
|
407
|
+
returnSources: inner._returnSources || [],
|
|
407
408
|
mutatedParams: inner._mutatedParamsOut || new Set(),
|
|
408
409
|
mutatedThisFields: inner._mutatedThisFieldsOut || new Set(),
|
|
409
410
|
taintedGlobals: new Set(),
|
|
@@ -411,7 +412,7 @@ function _nestedCallReturnTainted(calleeExpr, argExprs, state, callContext, argT
|
|
|
411
412
|
};
|
|
412
413
|
});
|
|
413
414
|
}
|
|
414
|
-
_mergeSummaryFindings(callContext, callContext._currentFnQid, sum, 'interproc');
|
|
415
|
+
_mergeSummaryFindings(callContext, callContext._currentFnQid, sum, 'interproc', { args: argExprs || [], state });
|
|
415
416
|
// Next-gen taint capability #3: a single-field getter's return is tainted
|
|
416
417
|
// whenever THIS call's own receiver already carries that field as
|
|
417
418
|
// tainted, independent of `sum.returnTainted` (which reflects only the
|
|
@@ -1502,14 +1503,100 @@ function _memberWriteSinkFindings(hits, sourceExpr, state, callContext, line, ta
|
|
|
1502
1503
|
// list: a chain of 20 one-line wrappers produced 131072 copies of one finding, and the explosion ended in the whole result being dropped. The
|
|
1503
1504
|
// same sink reached again by the same finding is one finding, so a copy is skipped when this context already holds it.
|
|
1504
1505
|
function _findingKey(f) { return `${f.file ?? ''}|${f.line ?? ''}|${f.cwe ?? ''}|${f.vuln ?? ''}|${typeof f.callee === 'string' ? f.callee : ''}|${f.id ?? ''}`; }
|
|
1505
|
-
|
|
1506
|
+
// A variable that is TAINTED BY DERIVATION (`y = f(x)`, `(a : b) <- return x`) has no source of its own: the sources the engine recorded are
|
|
1507
|
+
// keyed by the variable a source call was assigned to. Without a record for `y` the evidence chain of a sink reached through `y` begins at the
|
|
1508
|
+
// sink, and nothing downstream can say where the data came from. The sources reaching the expression it was derived from are copied to it.
|
|
1509
|
+
// Bounded: at most 5 per assignment and 4000 records per analysis; the original record is untouched.
|
|
1510
|
+
// A callee is analysed once per ENTRY STATE (which parameters carry taint), not per caller, so inside that analysis a tainted parameter has no
|
|
1511
|
+
// origin of its own. Each tainted parameter gets a placeholder record (`isParam`); a finding or return that rests on it is resolved to the
|
|
1512
|
+
// sources of the arguments at the call site that merges it (`_resolveParams`), and a placeholder that no call site resolves is dropped.
|
|
1513
|
+
function _paramPlaceholders(fn, entry) {
|
|
1514
|
+
const out = [];
|
|
1515
|
+
if (!fn || !Array.isArray(fn.params) || !entry || !entry.size) return out;
|
|
1516
|
+
for (const p of fn.params) {
|
|
1517
|
+
if (typeof p !== 'string') continue;
|
|
1518
|
+
let hit = entry.has(p);
|
|
1519
|
+
if (!hit) for (const x of entry) if (typeof x === 'string' && x.startsWith(p + '.')) { hit = true; break; }
|
|
1520
|
+
if (hit) out.push({ varName: p, sourceId: `param:${fn.qid}:${p}`, sourceLabel: `parameter ${p}`, provenance: 'param', line: fn.line || 0, isParam: true });
|
|
1521
|
+
}
|
|
1522
|
+
return out;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
function _resolveParams(entries, replacement, depth = 0) {
|
|
1526
|
+
const out = [];
|
|
1527
|
+
const seen = new Set();
|
|
1528
|
+
const add = (e) => { const k = `${e.sourceId}|${e.line}`; if (!seen.has(k)) { seen.add(k); out.push(e); } };
|
|
1529
|
+
for (const e of entries || []) {
|
|
1530
|
+
if (!e) continue;
|
|
1531
|
+
if (e.isParam) { for (const r of replacement || []) add(r); continue; }
|
|
1532
|
+
if (Array.isArray(e.from) && e.from.length && depth < 8) add({ ...e, from: _resolveParams(e.from, replacement, depth + 1) });
|
|
1533
|
+
else add(e);
|
|
1534
|
+
}
|
|
1535
|
+
return out;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
function _inheritSources(callContext, target, exprs, state) {
|
|
1539
|
+
const list = callContext._taintSources;
|
|
1540
|
+
if (!target || !Array.isArray(list) || list.length === 0 || list.length > 4000) return;
|
|
1541
|
+
let n = 0;
|
|
1542
|
+
for (const ex of exprs) {
|
|
1543
|
+
for (const s of _sourcesReachingExpr(ex, state, list)) {
|
|
1544
|
+
if (n >= 5) return;
|
|
1545
|
+
if (s.varName === target) continue;
|
|
1546
|
+
if (list.some((x) => x.varName === target && x.sourceId === s.sourceId && x.line === s.line)) continue;
|
|
1547
|
+
list.push({ ...s, varName: target, derivedFrom: s.varName });
|
|
1548
|
+
n++;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function _rootSources(trace, depth = 0, out = [], seen = new Set()) {
|
|
1554
|
+
for (const t of trace || []) {
|
|
1555
|
+
if (!t) continue;
|
|
1556
|
+
if (Array.isArray(t.from) && t.from.length && depth < 8) { _rootSources(t.from, depth + 1, out, seen); continue; }
|
|
1557
|
+
const k = `${t.sourceLabel}|${t.line}|${t.provenance}`;
|
|
1558
|
+
if (!seen.has(k)) { seen.add(k); out.push({ label: t.sourceLabel, provenance: t.provenance || null, line: t.line, file: t.file || null }); }
|
|
1559
|
+
}
|
|
1560
|
+
return out;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function _mergeSummaryFindings(callContext, callerQid, sum, via, callSite) {
|
|
1506
1564
|
if (!sum || !Array.isArray(sum.findings) || !sum.findings.length) return;
|
|
1507
1565
|
if (!callContext._findingKeys) callContext._findingKeys = new Set(callContext._findings.map(_findingKey));
|
|
1566
|
+
// A callee's finding was computed under an entry-taint state (which parameters carry taint), not under any
|
|
1567
|
+
// particular caller, so the summary cannot name where that taint came from. THIS call site can: the sources the
|
|
1568
|
+
// caller's own analysis recorded for the arguments it passes. A finding with no source of its own inherits them
|
|
1569
|
+
// (a copy; the shared summary is untouched). Without this an evidence chain begins at the sink with no origin,
|
|
1570
|
+
// and nothing downstream can tell a request value from a command-line argument.
|
|
1571
|
+
let argSources = null;
|
|
1572
|
+
const inherited = () => {
|
|
1573
|
+
if (argSources) return argSources;
|
|
1574
|
+
argSources = [];
|
|
1575
|
+
if (callSite && Array.isArray(callSite.args)) {
|
|
1576
|
+
const seen = new Set();
|
|
1577
|
+
for (const a of callSite.args) {
|
|
1578
|
+
for (const src of _sourcesReachingExpr(a, callSite.state, callContext._taintSources)) {
|
|
1579
|
+
const k = `${src.sourceId}|${src.line}`;
|
|
1580
|
+
if (!seen.has(k)) { seen.add(k); argSources.push(src); }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return argSources;
|
|
1585
|
+
};
|
|
1508
1586
|
for (const f of sum.findings) {
|
|
1509
1587
|
const key = _findingKey(f);
|
|
1510
1588
|
if (callContext._findingKeys.has(key)) continue;
|
|
1511
1589
|
callContext._findingKeys.add(key);
|
|
1512
|
-
|
|
1590
|
+
let out = { ...f, _funcQid: callerQid || null, _via: via };
|
|
1591
|
+
const tr = Array.isArray(f.trace) ? f.trace : [];
|
|
1592
|
+
if (tr.some((t) => t && (t.isParam || (t.from && t.from.length)))) {
|
|
1593
|
+
const resolved = _resolveParams(tr, callSite ? inherited() : []).slice(0, 5);
|
|
1594
|
+
out = { ...out, trace: resolved, sourceProvenance: resolved[0] ? (resolved[0].provenance || null) : null };
|
|
1595
|
+
} else if (callSite && tr.length === 0) {
|
|
1596
|
+
const inh = inherited();
|
|
1597
|
+
if (inh.length) out = { ...out, trace: inh.slice(0, 5), sourceProvenance: inh[0].provenance || null };
|
|
1598
|
+
}
|
|
1599
|
+
callContext._findings.push(out);
|
|
1513
1600
|
}
|
|
1514
1601
|
}
|
|
1515
1602
|
|
|
@@ -1634,7 +1721,7 @@ function step(node, stateIn, callContext) {
|
|
|
1634
1721
|
// callee's internal noise.
|
|
1635
1722
|
sum = callContext._summaryCache.compute(qid, entry, () => {
|
|
1636
1723
|
const inner = {
|
|
1637
|
-
_findings: [], _taintSources:
|
|
1724
|
+
_findings: [], _taintSources: _paramPlaceholders(fn, entry), _returnTainted: false,
|
|
1638
1725
|
_stack: new Set(), deadlineMs: callContext.deadlineMs,
|
|
1639
1726
|
_summaryCache: callContext._summaryCache,
|
|
1640
1727
|
_callGraph: callContext._callGraph,
|
|
@@ -1644,6 +1731,7 @@ function step(node, stateIn, callContext) {
|
|
|
1644
1731
|
try { analyzeFunction(fn, _unionAnnotationTaint(fn, entry), inner); } catch {}
|
|
1645
1732
|
return {
|
|
1646
1733
|
returnTainted: !!inner._returnTainted,
|
|
1734
|
+
returnSources: inner._returnSources || [],
|
|
1647
1735
|
mutatedParams: inner._mutatedParamsOut || new Set(),
|
|
1648
1736
|
mutatedThisFields: inner._mutatedThisFieldsOut || new Set(),
|
|
1649
1737
|
taintedGlobals: new Set(),
|
|
@@ -1659,7 +1747,7 @@ function step(node, stateIn, callContext) {
|
|
|
1659
1747
|
};
|
|
1660
1748
|
});
|
|
1661
1749
|
}
|
|
1662
|
-
_mergeSummaryFindings(callContext, callContext._currentFnQid, sum, 'interproc');
|
|
1750
|
+
_mergeSummaryFindings(callContext, callContext._currentFnQid, sum, 'interproc', { args: callArgs, state: callerTainted });
|
|
1663
1751
|
// Next-gen taint capability #3: `x = bad.getData()` — a single-
|
|
1664
1752
|
// field getter's return is tainted whenever THIS call's own
|
|
1665
1753
|
// receiver already carries that field, independent of `sum`
|
|
@@ -1667,12 +1755,19 @@ function step(node, stateIn, callContext) {
|
|
|
1667
1755
|
const _getterTainted = _calleeGetterFieldTainted(node.source.callee, newState, callContext);
|
|
1668
1756
|
if ((sum && sum.returnTainted) || _getterTainted) {
|
|
1669
1757
|
newState = _addPathAliasAware(newState, target, callContext);
|
|
1758
|
+
// `from`: what made the arguments tainted. The return is the callee's, but the taint it carries began wherever the
|
|
1759
|
+
// caller's own arguments got theirs, and a reader (or a trust-boundary check) needs that origin, not the hop.
|
|
1760
|
+
const _from = [];
|
|
1761
|
+
for (const a of callArgs) for (const sr of _sourcesReachingExpr(a, callerTainted, callContext._taintSources)) _from.push(sr);
|
|
1762
|
+
if (sum && Array.isArray(sum.returnSources)) for (const sr of _resolveParams(sum.returnSources, _from.slice())) _from.push(sr);
|
|
1670
1763
|
callContext._taintSources.push({
|
|
1671
1764
|
varName: target,
|
|
1672
1765
|
sourceId: `interproc:${qid}`,
|
|
1673
1766
|
sourceLabel: `interproc-return:${calleeName}`,
|
|
1674
1767
|
provenance: 'interproc',
|
|
1675
1768
|
line: node.line,
|
|
1769
|
+
file: _currentFile || null,
|
|
1770
|
+
...(_from.length ? { from: _from.slice(0, 5) } : {}),
|
|
1676
1771
|
});
|
|
1677
1772
|
}
|
|
1678
1773
|
// applyAtCallSite — mutated params (and, next-gen capability #3,
|
|
@@ -1693,6 +1788,7 @@ function step(node, stateIn, callContext) {
|
|
|
1693
1788
|
const _argTainted = (node.source.args || []).some(a => exprTaint(a, newState, callContext));
|
|
1694
1789
|
if (builtin.returnTainted && _argTainted) {
|
|
1695
1790
|
newState = _addPathAliasAware(newState, target, callContext);
|
|
1791
|
+
_inheritSources(callContext, target, node.source.args || [], newState);
|
|
1696
1792
|
} else if (!builtin.returnTainted) {
|
|
1697
1793
|
// PRD R4b: a builtin summary saying returnTainted:false can mean
|
|
1698
1794
|
// two very different things — a genuinely non-deriving function
|
|
@@ -1754,7 +1850,7 @@ function step(node, stateIn, callContext) {
|
|
|
1754
1850
|
newState = _addPathAliasAware(newState, target, callContext);
|
|
1755
1851
|
const sourcePath = accessPathOf(node.source);
|
|
1756
1852
|
if (sourcePath) newState = addPath(newState, sourcePath);
|
|
1757
|
-
callContext._taintSources.push({ varName: target, sourceId: src.id, sourceLabel: src.label, provenance: src.provenance || null, line: node.line });
|
|
1853
|
+
callContext._taintSources.push({ varName: target, sourceId: src.id, sourceLabel: src.label, provenance: src.provenance || null, line: node.line, file: _currentFile || null });
|
|
1758
1854
|
} else if (exprTaint(node.source, newState, callContext)) {
|
|
1759
1855
|
// P1.1: when the source IS a pure access path (e.g., RHS is `obj.foo.bar`),
|
|
1760
1856
|
// taint the TARGET as well as transitively propagate the source path so
|
|
@@ -1764,6 +1860,7 @@ function step(node, stateIn, callContext) {
|
|
|
1764
1860
|
newState = _addPathAliasAware(newState, target, callContext);
|
|
1765
1861
|
const sourcePath = accessPathOf(node.source);
|
|
1766
1862
|
if (sourcePath && !isCoveredBy(newState, sourcePath)) newState = addPath(newState, sourcePath);
|
|
1863
|
+
_inheritSources(callContext, target, [node.source], newState);
|
|
1767
1864
|
}
|
|
1768
1865
|
} else {
|
|
1769
1866
|
// Re-assigning a previously-tainted var to a clean value clears it
|
|
@@ -1802,7 +1899,7 @@ function step(node, stateIn, callContext) {
|
|
|
1802
1899
|
if (!sum && entry.size && fn && fn.cfg) {
|
|
1803
1900
|
sum = callContext._summaryCache.compute(qid, entry, () => {
|
|
1804
1901
|
const inner = {
|
|
1805
|
-
_findings: [], _taintSources:
|
|
1902
|
+
_findings: [], _taintSources: _paramPlaceholders(fn, entry), _returnTainted: false,
|
|
1806
1903
|
_stack: new Set(), deadlineMs: callContext.deadlineMs,
|
|
1807
1904
|
_summaryCache: callContext._summaryCache,
|
|
1808
1905
|
_callGraph: callContext._callGraph,
|
|
@@ -1812,6 +1909,7 @@ function step(node, stateIn, callContext) {
|
|
|
1812
1909
|
try { analyzeFunction(fn, _unionAnnotationTaint(fn, entry), inner); } catch {}
|
|
1813
1910
|
return {
|
|
1814
1911
|
returnTainted: !!inner._returnTainted,
|
|
1912
|
+
returnSources: inner._returnSources || [],
|
|
1815
1913
|
mutatedParams: inner._mutatedParamsOut || new Set(),
|
|
1816
1914
|
mutatedThisFields: inner._mutatedThisFieldsOut || new Set(),
|
|
1817
1915
|
taintedGlobals: new Set(),
|
|
@@ -1822,7 +1920,7 @@ function step(node, stateIn, callContext) {
|
|
|
1822
1920
|
};
|
|
1823
1921
|
});
|
|
1824
1922
|
}
|
|
1825
|
-
_mergeSummaryFindings(callContext, callContext._currentFnQid, sum, 'interproc');
|
|
1923
|
+
_mergeSummaryFindings(callContext, callContext._currentFnQid, sum, 'interproc', { args: node.args || [], state });
|
|
1826
1924
|
// Next-gen taint capability #3: the plain-statement-position
|
|
1827
1925
|
// counterpart of the assign-from-call site above — `bad.setData(
|
|
1828
1926
|
// tainted);` never assigns anything, but the receiver's OWN field
|
|
@@ -2063,6 +2161,12 @@ function step(node, stateIn, callContext) {
|
|
|
2063
2161
|
}
|
|
2064
2162
|
if (exprTaint(node.value, state, callContext)) {
|
|
2065
2163
|
callContext._returnTainted = true;
|
|
2164
|
+
// Where the returned taint began, for the caller that will name this function's return as a source (`from`, below).
|
|
2165
|
+
const rs = _sourcesReachingExpr(node.value, state, callContext._taintSources);
|
|
2166
|
+
if (rs.length) {
|
|
2167
|
+
const keep = (callContext._returnSources ||= []);
|
|
2168
|
+
for (const r of rs) if (keep.length < 8 && !keep.some((k) => k.sourceId === r.sourceId && k.line === r.line)) keep.push(r);
|
|
2169
|
+
}
|
|
2066
2170
|
}
|
|
2067
2171
|
return { state, findings };
|
|
2068
2172
|
}
|
|
@@ -2196,6 +2300,13 @@ function analyzeFunction(fn, entryState, callContext) {
|
|
|
2196
2300
|
// unchanged and lets `step` consult `aliasesForVar` when callContext._pointsTo
|
|
2197
2301
|
// is present.
|
|
2198
2302
|
if (callContext) callContext._currentFnQid = fn.qid;
|
|
2303
|
+
// A parameter the language front end marked as an input source (a request value handed to a handler, a caller-supplied text) has an
|
|
2304
|
+
// origin like any other source read: record it, so the evidence chain of a finding that rests on it names where the value came from.
|
|
2305
|
+
if (callContext && Array.isArray(callContext._taintSources) && Array.isArray(fn.paramAnnotations) && fn.paramAnnotations.length) {
|
|
2306
|
+
for (const r of matchAnnotationParamSources(fn.paramAnnotations, fn.file)) {
|
|
2307
|
+
if (!callContext._taintSources.some((x) => x.varName === r.name && x.sourceId === r.id)) callContext._taintSources.push({ varName: r.name, sourceId: r.id, sourceLabel: r.label, provenance: r.provenance, line: fn.line || 0, file: fn.file || null, annotation: true });
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2199
2310
|
const deadlineMs = (callContext && typeof callContext.deadlineMs === 'number') ? callContext.deadlineMs : Infinity;
|
|
2200
2311
|
const visited = 0;
|
|
2201
2312
|
let iterations = 0;
|
|
@@ -2510,6 +2621,7 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2510
2621
|
exitStateByQid.set(fn.qid, ctx._exitState || new Set());
|
|
2511
2622
|
const next = {
|
|
2512
2623
|
returnTainted: !!ctx._returnTainted,
|
|
2624
|
+
returnSources: ctx._returnSources || [],
|
|
2513
2625
|
mutatedParams: ctx._mutatedParamsOut || new Set(),
|
|
2514
2626
|
mutatedThisFields: ctx._mutatedThisFieldsOut || new Set(),
|
|
2515
2627
|
taintedGlobals: new Set(),
|
|
@@ -2711,6 +2823,7 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2711
2823
|
// _mergeSummaryFindings — dedup in _collectFindings makes this safe.
|
|
2712
2824
|
const fieldSummary = {
|
|
2713
2825
|
returnTainted: !!ctx._returnTainted,
|
|
2826
|
+
returnSources: ctx._returnSources || [],
|
|
2714
2827
|
mutatedParams: ctx._mutatedParamsOut || new Set(),
|
|
2715
2828
|
mutatedThisFields: ctx._mutatedThisFieldsOut || new Set(),
|
|
2716
2829
|
taintedGlobals: new Set(),
|
|
@@ -2817,6 +2930,7 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2817
2930
|
_collectFindings(fn, ctx._findings);
|
|
2818
2931
|
summaryCache.set(fn.qid, qualifiedSeeds, {
|
|
2819
2932
|
returnTainted: !!ctx._returnTainted,
|
|
2933
|
+
returnSources: ctx._returnSources || [],
|
|
2820
2934
|
mutatedParams: ctx._mutatedParamsOut || new Set(),
|
|
2821
2935
|
mutatedThisFields: ctx._mutatedThisFieldsOut || new Set(),
|
|
2822
2936
|
taintedGlobals: new Set(),
|
|
@@ -2862,6 +2976,7 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2862
2976
|
// only place that used to merge findings) at all.
|
|
2863
2977
|
summaryCache.set(fn.qid, taintedEntry, {
|
|
2864
2978
|
returnTainted: !!ctx._returnTainted,
|
|
2979
|
+
returnSources: ctx._returnSources || [],
|
|
2865
2980
|
mutatedParams: ctx._mutatedParamsOut || new Set(),
|
|
2866
2981
|
mutatedThisFields: ctx._mutatedThisFieldsOut || new Set(),
|
|
2867
2982
|
taintedGlobals: new Set(),
|
|
@@ -2938,6 +3053,7 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2938
3053
|
try { analyzeFunction(cbFn, _unionAnnotationTaint(cbFn, cbEntry), inner); } catch {}
|
|
2939
3054
|
return {
|
|
2940
3055
|
returnTainted: !!inner._returnTainted,
|
|
3056
|
+
returnSources: inner._returnSources || [],
|
|
2941
3057
|
mutatedParams: inner._mutatedParamsOut || new Set(),
|
|
2942
3058
|
mutatedThisFields: inner._mutatedThisFieldsOut || new Set(),
|
|
2943
3059
|
taintedGlobals: new Set(),
|
|
@@ -3080,10 +3196,15 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
3080
3196
|
// 'source-from-network' factor permanently dead (it reads
|
|
3081
3197
|
// t.provenance off chain/trace entries).
|
|
3082
3198
|
sourceProvenance: f.sourceProvenance || null,
|
|
3199
|
+
// The ORIGINS behind the trace: an `interproc-return` hop is followed back through `from` to the sources that tainted its
|
|
3200
|
+
// arguments. Only a finding whose trace has such a hop gets the field, so every other finding is unchanged.
|
|
3201
|
+
...(Array.isArray(f.trace) && f.trace.some((t) => t && t.from) ? { sourceRoots: _rootSources(f.trace) } : {}),
|
|
3083
3202
|
source: f.trace && f.trace.length ? {
|
|
3084
3203
|
file: fn.file,
|
|
3085
3204
|
line: f.trace[0].line,
|
|
3086
3205
|
label: f.trace[0].sourceLabel,
|
|
3206
|
+
// a parameter annotated as a source is not part of a finding's stable identity (posture/stable-id.js): naming it must not rename the finding
|
|
3207
|
+
...(f.trace[0].annotation ? { kind: 'parameter' } : {}),
|
|
3087
3208
|
} : null,
|
|
3088
3209
|
sink: {
|
|
3089
3210
|
file: fn.file,
|
package/src/engine.js
CHANGED
|
@@ -320,6 +320,7 @@ import { annotateHaskellFindings } from './language/haskell-findings.js';
|
|
|
320
320
|
import { annotateHaskellLlm, analyzeHaskellLlmRules } from './language/haskell-llm.js';
|
|
321
321
|
import { languagePragmaOnLine as _languagePragmaOnLine, languagePragmaSuppresses as _languagePragmaSuppresses } from './language/pragma.js';
|
|
322
322
|
import { applyHaskellGuards, consolidateFlowSinks } from './language/haskell-guards.js';
|
|
323
|
+
import { applyTrustBoundary } from './language/haskell-trust.js';
|
|
323
324
|
import { annotateUnresolvedTargets, attachEvidenceChains } from './language/haskell-disclosure.js';
|
|
324
325
|
import { analyzeHaskellRules } from './language/haskell-security-rules.js';
|
|
325
326
|
import { scanLanguageSecretConcat, scanDependencyUrlCredentials, providerInfo as _secretProvider } from './language/secrets.js';
|
|
@@ -2556,6 +2557,10 @@ function scanStructuralVulns(fp, raw, _suppOut) {
|
|
|
2556
2557
|
for (const pat of STRUCTURAL_VULN_PATTERNS) {
|
|
2557
2558
|
if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
|
|
2558
2559
|
if (pat.langScope && !pat.langScope.test(fp)) { continue; }
|
|
2560
|
+
// These patterns are written in the syntax of JavaScript, Python, Java and C-family code (`parseString(`, `$(document)`, `exec(`). Haskell and
|
|
2561
|
+
// Nix have their own analyzers; a pattern with no language scope has no meaning there, and one that fires (a record pattern named `String`
|
|
2562
|
+
// read as an XML parser call, a script inside an HTML quasi-quote) is an artefact of the text.
|
|
2563
|
+
if (!pat.langScope && isLanguageSource(fp)) continue;
|
|
2559
2564
|
const re = new RegExp(pat.regex.source, pat.regex.flags);
|
|
2560
2565
|
// Default: match against the string-stripped view so rule-library shapes
|
|
2561
2566
|
// ('exec(' inside a fix-message) don't self-detect. Patterns that need to
|
|
@@ -9615,7 +9620,7 @@ function _deterministicFileTimings(timings) {
|
|
|
9615
9620
|
f.unvalidated = true;
|
|
9616
9621
|
f.validator_verdict = 'unvalidated';
|
|
9617
9622
|
}
|
|
9618
|
-
try { applyHaskellGuards(irFindings, perFile); consolidateFlowSinks(irFindings, perFile); annotateUnresolvedTargets(irFindings, perFile); attachEvidenceChains(irFindings, perFile); annotateHaskellFindings(irFindings, fc); annotateHaskellLlm(irFindings, fc); } catch (_) { /* metadata only; the finding stands without it */ }
|
|
9623
|
+
try { applyHaskellGuards(irFindings, perFile); applyTrustBoundary(irFindings, perFile); consolidateFlowSinks(irFindings, perFile); annotateUnresolvedTargets(irFindings, perFile); attachEvidenceChains(irFindings, perFile); annotateHaskellFindings(irFindings, fc); annotateHaskellLlm(irFindings, fc); } catch (_) { /* metadata only; the finding stands without it */ }
|
|
9619
9624
|
aF.push(...irFindings);
|
|
9620
9625
|
} catch (e) {
|
|
9621
9626
|
// Deep mode is best-effort. A parser blowup in one file shouldn't kill
|
|
@@ -9823,7 +9828,10 @@ function _deterministicFileTimings(timings) {
|
|
|
9823
9828
|
} catch { /* Java SCA enrichment is best-effort */ }
|
|
9824
9829
|
}
|
|
9825
9830
|
// Haskell crypto / randomness / resource / XML / sensitive-logging rules (project-level: imports resolve across modules).
|
|
9826
|
-
|
|
9831
|
+
// An `unsafePerformIO` / `unsafeCoerce` / `unsafeInterleaveIO` boundary note says (in its own text) that it claims no vulnerability class: it marks where the
|
|
9832
|
+
// type system is stepped around. In a scan it is information, not a finding that gates a release; the rules module keeps emitting it at `low` (its contract),
|
|
9833
|
+
// and the severity is lowered here with the original kept.
|
|
9834
|
+
try{aF.push(...analyzeHaskellRules(fc).findings.map((f)=>f&&f.rule==='hs-unsafe-io-boundary'?{...f,originalSeverity:f.severity,severity:'info'}:f));}catch(_){}
|
|
9827
9835
|
try{aF.push(...analyzeHaskellLlmRules(fc));}catch(_){}
|
|
9828
9836
|
try{const{analyzeLanguageFindings}=await import('./language/engine-pass.js');aF.push(...analyzeLanguageFindings(allFileContents).findings);}catch(_){}
|
|
9829
9837
|
let _languageBridges=null;try{if(Object.keys(allFileContents).some(f=>/\.(?:l?hs|nix)$/i.test(f))){const{analyzePolyglotBridges}=await import('./language/bridges.js');_languageBridges=analyzePolyglotBridges(allFileContents);}}catch(_){}
|
|
@@ -10727,7 +10735,7 @@ function _deterministicFileTimings(timings) {
|
|
|
10727
10735
|
// classes from one source" finding, not a duplicate of any single
|
|
10728
10736
|
// existing finding) still gets its own standalone finding, tagged with
|
|
10729
10737
|
// the honest generic CWE-20/'multi-sink-taint-chain' label.
|
|
10730
|
-
try{const srcGroups=new Map();for(const f of finalFindings){const src=f.chain?.[0]?.label;if(!src)continue;if(!srcGroups.has(src))srcGroups.set(src,[]);srcGroups.get(src).push(f);}for(const[src,group]of srcGroups){if(group.length<2)continue;const groupId=`multi-sink:${src}:${group.length}`;for(const f of group)f._multiSinkGroupId=groupId;const sharedFamily=group.every(f=>f.family&&f.family===group[0].family)?group[0].family:null;const sharedCwe=group.every(f=>f.cwe&&f.cwe===group[0].cwe)?group[0].cwe:null;const sinks=group.map(f=>({file:f.file,line:f.line,vuln:f.vuln}));if(sharedFamily&&sharedCwe){for(const f of group)f.multiSinkChain={source:src,sinkCount:group.length,sinks};continue;}finalFindings.push({id:groupId,file:group[0].file,line:group[0].line,vuln:`Multi-Sink Taint Chain — ${src} reaches ${group.length} sinks`,severity:group.some(f=>f.severity==='critical')?'critical':'high',cwe:'CWE-20',family:'multi-sink-taint-chain',parser:'MULTI-SINK',confidence:0.85,sinks,_aggregated:true});}}catch(_){}
|
|
10738
|
+
try{const srcGroups=new Map();for(const f of finalFindings){const src=f.chain?.[0]?.label;if(!src)continue;if(f.severity==='info')continue;if(!srcGroups.has(src))srcGroups.set(src,[]);srcGroups.get(src).push(f);}for(const[src,group]of srcGroups){if(group.length<2)continue;const groupId=`multi-sink:${src}:${group.length}`;for(const f of group)f._multiSinkGroupId=groupId;const sharedFamily=group.every(f=>f.family&&f.family===group[0].family)?group[0].family:null;const sharedCwe=group.every(f=>f.cwe&&f.cwe===group[0].cwe)?group[0].cwe:null;const sinks=group.map(f=>({file:f.file,line:f.line,vuln:f.vuln}));if(sharedFamily&&sharedCwe){for(const f of group)f.multiSinkChain={source:src,sinkCount:group.length,sinks};continue;}finalFindings.push({id:groupId,file:group[0].file,line:group[0].line,vuln:`Multi-Sink Taint Chain — ${src} reaches ${group.length} sinks`,severity:group.some(f=>f.severity==='critical')?'critical':'high',cwe:'CWE-20',family:'multi-sink-taint-chain',parser:'MULTI-SINK',confidence:0.85,sinks,_aggregated:true});}}catch(_){}
|
|
10731
10739
|
// SCA transitive dedup: collapse duplicate CVEs across dep chains
|
|
10732
10740
|
try{const osvGroups=new Map();for(const sc of supplyChain){if(sc.type!=='vulnerable_dep'||!sc.osvId)continue;if(!osvGroups.has(sc.osvId))osvGroups.set(sc.osvId,[]);osvGroups.get(sc.osvId).push(sc);}for(const[osvId,group]of osvGroups){if(group.length<=1)continue;const primary=group.find(s=>s.isDirect)||group[0];primary.dependents=group.filter(s=>s!==primary).map(s=>({name:s.name,version:s.version,depChain:s.depChain,isDirect:s.isDirect}));primary._transitiveDeduped=group.length-1;for(const dup of group){if(dup!==primary)dup._deduplicatedInto=primary.osvId;}supplyChain.splice(0,supplyChain.length,...supplyChain.filter(s=>!s._deduplicatedInto));}}catch(_){}
|
|
10733
10741
|
// ── World-class post-scan artifact emitters ──────────────────────────
|
package/src/language/CLAUDE.md
CHANGED
|
@@ -66,6 +66,12 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
|
|
|
66
66
|
- **Guards are recognised by what they do.** `haskell-guards.js`: a project function is an HTML escaper when it (or its local helpers) spells out the entities for `<`, `>` and `&` (`htmlEscapers`; never by name), and it neutralises a CWE-79 flow only when EVERY use of the value in the sink argument is under such a call (`allUsesEscaped`); a host allow-list may be a named constant and may sit in an `&&`; `sh -c SCRIPT arg0 arg1` is shell injection only when the SCRIPT is built from the value, and tainted positional parameters are data when the constant script expands them only inside double quotes after `--` (`quotedPositionals`, which decodes the Haskell string escapes the IR keeps raw).
|
|
67
67
|
- **Handlers named by the framework's convention are routes.** `haskell-web.js` adds `getFooR`/`postFooR`/`handleFooR` functions whose signature mentions `Handler` in a module importing Yesod when no route table in an analysed file names them (the table usually lives in `config/routes`). Scotty's rejection order is judged by `inlineOrder` as well as by line: a `case` lists the success branch first, so `status 40x >> finish` in the LAST alternative guards it (a bare `status` with no halting call does not).
|
|
68
68
|
- **IR idioms that must keep the action.** `m >>= \_ -> k` and `f <$> m` RUN `m` even when the lambda ignores its result (`betaReduce`, `via`); `maybe d f m` / `either f g e` are a two-way branch on the scrutinee (so the scrutinee is still examined); `\case` applied by `>>=` is a case run in place; `foldM`, `zipWithM_`, `uncurry`, `fst`/`snd`/`const` of a written-out tuple, `filter isAlphaNum`, and a let-bound lambda (`let go = \x -> ..`, treated as `go x = ..`) are lowered so their sinks are reached. The taint engine (`dataflow/engine.js`) is Haskell-scoped for three of these: a `union` handed to a combinator or bound as a statement is a sink site in each branch (`_nestedSinkFindings`, the `assign` case), and a `member` whose object is a call (the elements of `words s`) carries its object's taint.
|
|
69
|
+
- **A Haskell taint finding says who controls its input.** `haskell-trust.js` `applyTrustBoundary` (run in `engine.js` right after `applyHaskellGuards`) reads the origins the engine recorded (`sourceRoots`, or the evidence chain) and stamps `trustBoundary {verdict, reason, role, roots}`. Verdicts: `attacker-reachable` (request, socket, header, cookie, remote response; never lowered), `operator-controlled` (cli/env roots, and file-read roots in a command-line program; also an EMPTY origin in a command-line program, marked `inferred: true`), `api-contract` (a bare/whole-handoff parameter of an uncalled exported function reaching a process, CWE-78/88 only), `entry-parameter`, `data-input`, `unattributed`. `operator-controlled` and `api-contract` are lowered to `info` (never dropped; `originalSeverity`, `controls`, `guardNote` kept), the same convention `haskell-guards.js` uses. **Standard input is deliberately NOT covered by the command-line rule** (`getLine >>= callCommand` in `main` stays reported; `test/haskell/haskell-generalization.test.js` pins it), and **path wrappers are deliberately not `api-contract`** (`withFile name WriteMode` on a caller-named file is the vulnerable shape of three development splits). The role is `fn.hs.entryRole` from `assignEntryRoles` in `haskell-ir.js`: `cli` (reached only from `main`s whose reachable modules import no server framework or socket library), `service`, or `library` (no `main` reaches it; an incomplete call graph therefore yields `library`, never `cli`). Pinned both ways in `test/haskell/haskell-trust-boundary.test.js`.
|
|
70
|
+
- **Origins are recorded, not guessed** (`dataflow/engine.js`). The engine records a source only against the variable a source call was assigned to; three additions make an evidence chain begin at the real origin: a value derived from a source inherits its records (`_inheritSources`), a callee analysed under an entry-taint state names its tainted parameters with placeholder records (`_paramPlaceholders`) that `_mergeSummaryFindings` resolves to the sources of the arguments at the merging call site (`_resolveParams`), and a function return keeps the sources behind it (`returnSources`, followed through `from` into `sourceRoots`). A parameter annotated as a source (`hs:caller-controlled`, `hs:handler-param:*`) is recorded too (`matchAnnotationParamSources`, `source.kind: 'parameter'` so stable ids do not change). This is language-neutral (`test/dataflow-source-attribution.test.js` pins it on JavaScript). `System.IO.hGetContents`/`hGetLine`/`hGetChar` (and the Text/ByteString forms) are modelled as `file-read`, not `stdin`: they read whatever handle they are given.
|
|
71
|
+
- **Reachability must survive platform variants.** An import two modules both satisfy (the Posix and Windows variants behind an undecided CPP conditional) resolves to `unknown: ambiguous-import` with its `candidates`; `haskell-ir.js` records each candidate as a reference of the caller, so neither is an "uncalled entry point" whose parameters are caller-controlled. Without it every function of the platform module that was not chosen was reported.
|
|
72
|
+
- **Comments never change a verdict, an id or a cluster.** `haskell-comments.js` finds Haskell comments lexically; a finding's `snippet` is `codeOfLine(line)` (code only), because the snippet feeds the stable id and the clone-shape hash, and both changed when someone edited a trailing comment. `test/haskell/haskell-comment-invariance.test.js` scans the examples and corpus-shaped cases as written, with every comment blanked, and with every comment (and a trailing comment on every line) replaced by text that lies in each direction; `bench/dag/dev-haskell/comment-invariance.mjs` does the same to a real project. An engine that varies is wrong.
|
|
73
|
+
- **Rules that read the function around a call** (`haskell-security-rules.js`): `hs-weak-hash` is a vulnerability only where `weakDigestUse` finds a use that depends on the digest's strength (compared, hashed secret, credential-shaped binding or function, handed to a signing/verifying call); otherwise it is emitted at `info`. `hs-unbounded-input-read` needs a socket handle, or standard input in a function nothing calls (`readsOutsideInput`). Partial-function and allocation rules use a parameter as caller-supplied only for a framework-handler parameter or an uncalled function (`fn.hs.reached`). A salt is not an unpredictability-sensitive value. A cookie whose whole value is constant is not a credential. Own-property lookups only (`Object.hasOwn`): `constructor` and `toString` are legal Haskell names and were reported as "Unsafe boundary: function Object() { [native code] }".
|
|
74
|
+
- **Fixes name the library the file uses.** `sqlDriverOf` picks `Database.PostgreSQL.Simple` or `Database.SQLite.Simple` from the file's own imports and refuses anything else; `hs-html-escape` needs blaze-html or a re-exporter; `hs-hash-sha256` refuses when the algorithm is named on another line (a type); `hs-path-takefilename` rewrites only a String/FilePath parameter spliced under a fixed directory in a FILE operation. Classes with no safe rewrite refuse with a class-level reason (`CLASS_REASONS`), `proposal: 'model-assisted'`.
|
|
69
75
|
- **Pragma** (`pragma.js`): comment-aware, line-scoped, exact rule-id match, logged. A line-less finding cannot be suppressed.
|
|
70
76
|
- **Scale: no per-use scan of the project.** Pandoc and cabal-install (hundreds to thousands of modules) did not finish a scan because of four superlinear costs, now fixed; do not reintroduce the shapes. (1) `haskell-ir.js`: anything a lowering step needs "from every module" (a constructor by name, the record types, a definition by qid, a name's resolution) is an index on `Project` / a per-module memo (`findCon`, `recordTypes`, `defByQid`, `RESOLVE_MEMO`), built once, never a loop over `proj.modules` per use. Only a top-level resolution (depth 0) is memoised: a resolution inside a re-export chain depends on the path taken. (2) `aibom.js` looks a file's imports up in a map, not `Object.values(perFile).find`. (3) `sast/pipeline.js`: the workflow-wide `env:` secret rule is a one-pass matcher (`scanEnvSecrets`), because the single regular expression it replaced backtracked catastrophically on a comment-blanked workflow (blanking leaves runs of blank lines). A detector expression that runs on comment-blanked text must not let `\s*`, `\n` and a lazy line loop claim the same blank lines. (4) `dataflow/engine.js` `exprTaint`: a call's argument taint is computed once per visit (`_argTaint`); computing it twice re-walked the subtree and cost 2^N for N nested calls. Guards: `test/pipeline-env-secret.test.js`, `test/dataflow-expr-taint-scaling.test.js`, `test/haskell/haskell-ir-scaling.test.js`, `test/mark-used-vuln-functions.test.js` compare work counters between two sizes, never a clock.
|
|
71
77
|
|
|
@@ -81,6 +87,7 @@ or a capability.** Product-facing guides: `docs/guides/haskell.md`, `docs/guides
|
|
|
81
87
|
| Nix | `nix-parser.js`, `nix-grammar.js`, `nix-ir.js`, `nix-adapter.js`, `nixos-module-resolver.js`, `nixos-eval-forms.js`, `nixos-option-catalog.js`, `nixos-hardening.js`, `nix-build-trust.js`, `nix-secrets.js`, `nix-script-taint.js`, `nix-privacy.js`, `nix-agents.js`, `nix-inventory.js`, `nix-closure.js`, `nix-sca.js`, `nix-eval-isolation.js`, `nix-fix.js` |
|
|
82
88
|
| Haskell | `haskell-parser.js`, `haskell-grammar.js`, `haskell-adapter.js`, `haskell-ir.js`, `haskell-models.js` (the single registry of sources, sinks, sanitizers; `dataflow/catalog-haskell.js` is generated from it), `haskell-guards.js`, `haskell-security-rules.js`, `haskell-web.js`, `haskell-findings.js`, `haskell-syntax.js`, `haskell-sweep.js`, `haskell-disclosure.js`, `haskell-llm.js`, `haskell-manifests.js`, `haskell-resolved-graph.js`, `haskell-sca.js`, `haskell-advisory-feed.js` (the opt-in live Hackage feed: OSV fetch, per-package coverage, operator-only snapshot), `haskell-supply.js`, `haskell-fix.js` |
|
|
83
89
|
| Nix | `nix-parser.js`, `nix-grammar.js`, `nix-ir.js`, `nix-adapter.js`, `nixos-module-resolver.js`, `nixos-option-catalog.js`, `nixos-hardening.js`, `nix-build-trust.js`, `nix-secrets.js`, `nix-script-taint.js`, `nix-privacy.js`, `nix-agents.js`, `nix-inventory.js`, `nix-closure.js`, `nix-sca.js`, `nix-advisory-feed.js`, `nix-eval-isolation.js`, `nix-fix.js` |
|
|
90
|
+
| Haskell trust | `haskell-trust.js` (trust-boundary verdicts and the lowering of operator-controlled and pass-through findings), `haskell-comments.js` (lexical Haskell comments; the code-only snippet) |
|
|
84
91
|
| Measurement | `accuracy.js` (family-scoped scoring, per-layer), `support-registry.js` (status rules, promotion refusals, frozen-hash demotion) |
|
|
85
92
|
|
|
86
93
|
Related, outside this directory: `dataflow/catalog-haskell.js`, `lineage/haskell-view.js`, `lineage/nix-view.js`,
|