@clear-capabilities/agentic-security-scanner 0.187.0 → 0.189.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.
@@ -1 +1 @@
1
- 347fbe455e8a916364d4ff2776ca5407f97e776d925bb5f2e3590d13f565a96b agentic-security.mjs
1
+ f5b8304272ffd3846d405b461331e001a6209c349b48d83353c266eeb34b9460 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.187.0",
3
+ "version": "0.189.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,15 +87,15 @@
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-reachability.test.js 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
- "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",
98
+ "test:language": "node --test test/bench-haskell-precision.test.js test/bench-independent-haskell-panel.test.js 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",
100
100
  "test:language-tools": "node --test test/haskell/haskell-web-auth.test.js test/haskell/haskell-support-gate.test.js",
101
101
  "bench:language-support:check": "node ../bench/language-support/check.mjs",
@@ -242,6 +242,9 @@
242
242
  "bench:sard:php:test-split": "node ../bench/sard/scripts/score-php.mjs --deep --split test --json",
243
243
  "bench:layer-recall": "node ../bench/layer-recall/runner.mjs",
244
244
  "bench:layer-recall:check": "node ../bench/layer-recall/runner.mjs --check",
245
+ "bench:haskell-precision:check": "node ../bench/haskell-precision/runner.mjs --check",
246
+ "bench:haskell-precision:release": "node ../bench/haskell-precision/runner.mjs --check --require-sealed",
247
+ "bench:haskell-precision:update": "node ../bench/haskell-precision/runner.mjs --update-baseline",
245
248
  "bench:layer-recall:update-baseline": "node ../bench/layer-recall/runner.mjs --update-baseline",
246
249
  "bench:dag": "node ../bench/dag/run.mjs",
247
250
  "bench:dag:quick": "node ../bench/dag/run.mjs --quick",
@@ -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).
@@ -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; }
@@ -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: [], _returnTainted: false,
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
- function _mergeSummaryFindings(callContext, callerQid, sum, via) {
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
- callContext._findings.push({ ...f, _funcQid: callerQid || null, _via: via });
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: [], _returnTainted: false,
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: [], _returnTainted: false,
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(),
@@ -2970,7 +3086,8 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
2970
3086
  if (fn.hs && (fn.hs.exported || /\.main$/.test(fn.name || ''))) continue;
2971
3087
  f._inDeadCode = true;
2972
3088
  const dg = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
2973
- if (dg[f.severity]) f.severity = dg[f.severity];
3089
+ // The severity the finding would have had is kept (a demotion nobody can see is indistinguishable from a finding that was never that serious).
3090
+ if (dg[f.severity]) { f.originalSeverity = f.originalSeverity || f.severity; f.severity = dg[f.severity]; }
2974
3091
  }
2975
3092
  Object.defineProperty(all, '_summaryCache', { value: summaryCache, enumerable: false });
2976
3093
  if (summaryCache._budgetHits) _noteIncomplete('summary-budget', `${summaryCache._budgetHits} summary requests after ${summaryCache._maxIter} computations`);
@@ -3080,10 +3197,15 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
3080
3197
  // 'source-from-network' factor permanently dead (it reads
3081
3198
  // t.provenance off chain/trace entries).
3082
3199
  sourceProvenance: f.sourceProvenance || null,
3200
+ // The ORIGINS behind the trace: an `interproc-return` hop is followed back through `from` to the sources that tainted its
3201
+ // arguments. Only a finding whose trace has such a hop gets the field, so every other finding is unchanged.
3202
+ ...(Array.isArray(f.trace) && f.trace.some((t) => t && t.from) ? { sourceRoots: _rootSources(f.trace) } : {}),
3083
3203
  source: f.trace && f.trace.length ? {
3084
3204
  file: fn.file,
3085
3205
  line: f.trace[0].line,
3086
3206
  label: f.trace[0].sourceLabel,
3207
+ // 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
3208
+ ...(f.trace[0].annotation ? { kind: 'parameter' } : {}),
3087
3209
  } : null,
3088
3210
  sink: {
3089
3211
  file: fn.file,
package/src/engine.js CHANGED
@@ -320,6 +320,8 @@ 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';
324
+ import { applyHaskellReachability } from './language/haskell-reachability.js';
323
325
  import { annotateUnresolvedTargets, attachEvidenceChains } from './language/haskell-disclosure.js';
324
326
  import { analyzeHaskellRules } from './language/haskell-security-rules.js';
325
327
  import { scanLanguageSecretConcat, scanDependencyUrlCredentials, providerInfo as _secretProvider } from './language/secrets.js';
@@ -2556,6 +2558,10 @@ function scanStructuralVulns(fp, raw, _suppOut) {
2556
2558
  for (const pat of STRUCTURAL_VULN_PATTERNS) {
2557
2559
  if (!_ruleAppliesIn(pat, ctx)) { _supp.push({vuln:pat.vuln,file:fp,line:0,snippet:'',reason:'context-mismatch:'+ctx.kind}); continue; }
2558
2560
  if (pat.langScope && !pat.langScope.test(fp)) { continue; }
2561
+ // These patterns are written in the syntax of JavaScript, Python, Java and C-family code (`parseString(`, `$(document)`, `exec(`). Haskell and
2562
+ // Nix have their own analyzers; a pattern with no language scope has no meaning there, and one that fires (a record pattern named `String`
2563
+ // read as an XML parser call, a script inside an HTML quasi-quote) is an artefact of the text.
2564
+ if (!pat.langScope && isLanguageSource(fp)) continue;
2559
2565
  const re = new RegExp(pat.regex.source, pat.regex.flags);
2560
2566
  // Default: match against the string-stripped view so rule-library shapes
2561
2567
  // ('exec(' inside a fix-message) don't self-detect. Patterns that need to
@@ -9517,12 +9523,31 @@ function _deterministicFileTimings(timings) {
9517
9523
  const _hasJava = Object.keys(fc || {}).some(f => /\.java$/i.test(f));
9518
9524
  // DAG-05: with the cross-scan cache on, an unchanged JavaScript/TypeScript file is read back from the IR cache
9519
9525
  // instead of parsed again (dataflow/ir-cache.js; the content is the same, only the cost differs).
9526
+ // What the Haskell project-reachability verdict needs besides the sources (language/haskell-ir.js `projectReachability`): the manifests that say
9527
+ // which modules the package exposes, whether this file set is the whole project, and every gap the scan itself knows about. A Haskell source that
9528
+ // was offered to the scan and is not in the IR input (path policy, size, density, or a format the IR does not read) could hold a caller, so
9529
+ // each is a named hole and a verdict that needs a complete picture degrades to `unknown`.
9530
+ const _hsReachCtx = () => {
9531
+ const manifests = {};
9532
+ for (const [p, t] of Object.entries(depFileContents || {})) if (/\.cabal$|(?:^|\/)package\.yaml$/i.test(p) && typeof t === 'string') manifests[p] = t;
9533
+ const hsSrc = /\.(?:hs|lhs|hs-boot|lhs-boot|hsc|hsig)$/i;
9534
+ const holes = new Set();
9535
+ if ((policyExcluded || []).some((p) => hsSrc.test(String(p)))) holes.add('policy-excluded');
9536
+ if ((sizeSkipped || []).some((s) => hsSrc.test(String((s && s.file) || s)))) holes.add('size-skipped');
9537
+ for (const f of Object.keys(fileContents || {})) {
9538
+ if (!hsSrc.test(f)) continue;
9539
+ if (!/\.hs$/i.test(f)) holes.add('opaque-boundary'); // literate, hsc and boot files: the IR reads `.hs` only, so their calls are not in the graph
9540
+ else if (!(f in fc)) holes.add(typeof fileContents[f] === 'string' && fileContents[f].length > 500000 ? 'size-skipped' : 'policy-excluded');
9541
+ }
9542
+ return { manifests, completeScan: completeScan === true, scanHoles: [...holes] };
9543
+ };
9520
9544
  const _buildIR = async () => {
9521
9545
  let irCache = null;
9522
9546
  if (process.env.AGENTIC_SECURITY_INCREMENTAL === '1' && scanRoot) {
9523
9547
  try { irCache = openIrCache(scanRoot); } catch { irCache = null; }
9524
9548
  }
9525
- return _hasJava ? await buildProjectIRAsync(fc, { irCache }) : buildProjectIR(fc, { irCache });
9549
+ const haskell = _hsReachCtx();
9550
+ return _hasJava ? await buildProjectIRAsync(fc, { irCache, haskell }) : buildProjectIR(fc, { irCache, haskell });
9526
9551
  };
9527
9552
  const _irStatsTarget = irStatsTarget();
9528
9553
  if (_irStatsTarget) {
@@ -9615,7 +9640,7 @@ function _deterministicFileTimings(timings) {
9615
9640
  f.unvalidated = true;
9616
9641
  f.validator_verdict = 'unvalidated';
9617
9642
  }
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 */ }
9643
+ try { applyHaskellGuards(irFindings, perFile); applyTrustBoundary(irFindings, perFile); try { applyHaskellReachability(irFindings, perFile); } catch (_) { /* the verdict is metadata; the finding stands without it */ } consolidateFlowSinks(irFindings, perFile); annotateUnresolvedTargets(irFindings, perFile); attachEvidenceChains(irFindings, perFile); annotateHaskellFindings(irFindings, fc); annotateHaskellLlm(irFindings, fc); } catch (_) { /* metadata only; the finding stands without it */ }
9619
9644
  aF.push(...irFindings);
9620
9645
  } catch (e) {
9621
9646
  // Deep mode is best-effort. A parser blowup in one file shouldn't kill
@@ -9823,12 +9848,15 @@ function _deterministicFileTimings(timings) {
9823
9848
  } catch { /* Java SCA enrichment is best-effort */ }
9824
9849
  }
9825
9850
  // Haskell crypto / randomness / resource / XML / sensitive-logging rules (project-level: imports resolve across modules).
9826
- try{aF.push(...analyzeHaskellRules(fc).findings);}catch(_){}
9851
+ // An `unsafePerformIO` / `unsafeCoerce` / `unsafeInterleaveIO` boundary note says (in its own text) that it claims no vulnerability class: it marks where the
9852
+ // 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),
9853
+ // and the severity is lowered here with the original kept.
9854
+ try{aF.push(...analyzeHaskellRules(fc,{reachability:_hsReachCtx()}).findings.map((f)=>f&&f.rule==='hs-unsafe-io-boundary'?{...f,originalSeverity:f.severity,severity:'info'}:f));}catch(_){}
9827
9855
  try{aF.push(...analyzeHaskellLlmRules(fc));}catch(_){}
9828
9856
  try{const{analyzeLanguageFindings}=await import('./language/engine-pass.js');aF.push(...analyzeLanguageFindings(allFileContents).findings);}catch(_){}
9829
9857
  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(_){}
9830
9858
  try{for(const[_p,_t]of Object.entries(allFileContents))if(typeof _t==='string')aF.push(...scanDependencyUrlCredentials(_p,_t));}catch(_){}
9831
- try{const _hw=analyzeHaskellWeb(fc);aF.push(..._hw.findings);aR.push(...toEngineRoutes(_hw));}catch(_){}
9859
+ try{const _hw=analyzeHaskellWeb(fc,{reachability:_hsReachCtx()});aF.push(..._hw.findings);aR.push(...toEngineRoutes(_hw));}catch(_){}
9832
9860
  let finalFindings;try{finalFindings=dedupeFindingsWithEvidence(aF);}catch(_){finalFindings=dd(aF,f=>f.id);}
9833
9861
  // Inline `agentic-security-ignore` pragmas, pass 1 of 2. This covers every
9834
9862
  // finding that exists BY THIS POINT — the pattern detectors, the cross-file
@@ -10727,7 +10755,7 @@ function _deterministicFileTimings(timings) {
10727
10755
  // classes from one source" finding, not a duplicate of any single
10728
10756
  // existing finding) still gets its own standalone finding, tagged with
10729
10757
  // 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(_){}
10758
+ 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
10759
  // SCA transitive dedup: collapse duplicate CVEs across dep chains
10732
10760
  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
10761
  // ── World-class post-scan artifact emitters ──────────────────────────