@clear-capabilities/agentic-security-scanner 0.195.14 → 0.195.16

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
- bb2980af58a2c5183cbe0c14efd1d611922784ee2f051111e2f3bf03d1846e79 agentic-security.mjs
1
+ 10035213d4490ba0c69e465083a4322a6cb5e7717ea62b3e9eaabd048b468ac3 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.195.14",
3
+ "version": "0.195.16",
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",
@@ -85,7 +85,7 @@
85
85
  "test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
86
86
  "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
87
87
  "test:glob": "node --test test/glob-compat.test.js",
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",
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/vendor-java-parser-current.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/api-contract-scope.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
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",
@@ -93,7 +93,7 @@
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-reachability.test.js test/haskell/haskell-real-code-shapes.test.js test/haskell/haskell-unseen-v5-shapes.test.js test/haskell/haskell-unseen-v4-shapes.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-remediation-0190.test.js test/haskell/haskell-remediation-kdf.test.js test/haskell/haskell-remediation-html-text.test.js test/haskell/haskell-remediation-scotty-body.test.js test/haskell/haskell-taint-shapes.test.js test/haskell/haskell-real-advisory-shapes.test.js test/haskell/haskell-discovery-shapes.test.js test/haskell/haskell-quasiquote-interp.test.js test/haskell/haskell-record-dot.test.js test/haskell/haskell-global-cell.test.js test/haskell/haskell-syntax-extensions.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",
96
+ "test:haskell": "node --test test/haskell/haskell-reachability.test.js test/haskell/haskell-real-code-shapes.test.js test/haskell/haskell-unseen-v5-shapes.test.js test/haskell/haskell-unseen-v4-shapes.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-remediation-0190.test.js test/haskell/haskell-remediation-kdf.test.js test/haskell/haskell-remediation-html-text.test.js test/haskell/haskell-remediation-scotty-body.test.js test/haskell/haskell-remediation-entropy.test.js test/haskell/haskell-remediation-text-command.test.js test/haskell/haskell-remediation-path-contain.test.js test/haskell/haskell-remediation-0195-16.test.js test/haskell/haskell-taint-shapes.test.js test/haskell/haskell-real-advisory-shapes.test.js test/haskell/haskell-discovery-shapes.test.js test/haskell/haskell-quasiquote-interp.test.js test/haskell/haskell-record-dot.test.js test/haskell/haskell-global-cell.test.js test/haskell/haskell-syntax-extensions.test.js test/haskell/haskell-rule-precision.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/nix-development-shapes.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/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/frozen-splits.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",
@@ -66,6 +66,9 @@ 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
  - **A project definition of a modelled name keeps the registry's meaning (since 0.195.11).** `haskell-ir.js` `applyVar`/`lowerVarValue`: a resolution to a project `def` whose module-qualified name `isKnownApi` (the project IS the framework, or vendors it) is lowered as the LIBRARY call (`hs.status: 'external'`, `hs.modelledDefinition: qid`) and the definition is still referenced (`noteRef(..., 'modelled-definition')`, so it is never an uncalled entry point). Before this, IHP's own `IHP.Controller.Param.param` shadowed the catalog when IHP scanned itself, and a request parameter reaching `Process.system` through a helper was not reported (four lost true positives on the fresh set). A test stub named like a catalog module gets the same treatment, which is why the html-text remediation test keeps its scotty stub out of the scan. Pinned both ways in `test/haskell/haskell-taint-shapes.test.js`, with the three shapes below.
69
+ - **Fixers for the real true positives (0.195.16; the count in the older "Ten fixers" bullet is now fifteen: `hs-path-contain` and `hs-xml-no-entity-substitution` are new).** `hs-process-argv` reads a `Text` command (`system (cs t)` / `(T.unpack t)` of a `<>` concatenation, on the line or in a one-line `let` of the same function, which is removed; `concatParts` splits on `<>` when no `++` is present) and keeps the unpack per argument; `spawnCommand` is recognised; `CLASS_REASONS['CWE-78']` (new) names the decision when the program is a variable (IHP's editor from an environment variable stays refused: `code --goto` is not one program name). `hs-path-contain` (`fixPathContain`, `signatureAt`): a guard equation before the function's first equation, `| ".." \`elem\` splitDirectories p || isAbsolute p = pure Nothing`, only for exactly one String/FilePath parameter and a `Maybe` result (bare or under one monad); the shape is the one `haskell-guards.js` already classifies as a traversal check, so the rescan passes on semantics; effective kind `path-contain` (one added equation, the guard present). `hs-tls-validate` gains `fixOpenSslVerify`: `contextSetVerificationMode ctx VerifyNone` as a do-block statement becomes `contextSetDefaultVerifyPaths ctx` + `(VerifyPeer True True Nothing)` (HsOpenSSL 0.11.7.11), MITIGATION because the host name is not checked (`enableHostnameValidation` needs the connection object); effective kind `tls-openssl`. `hs-log-redact` no longer rewrites inside a string literal (the guide's captured example had shown `" "[REDACTED]"="`). `hs-xml-no-entity-substitution` (`fixXmlEntities`): `withSubstDTDEntities yes|True` to `no|False`, FULL, effective kind `hxt-entities`. `hs-pbkdf2-low-iterations` findings are routed to `hs-password-kdf-migrate` (the old count becomes the legacy verifier). Tests: `haskell-remediation-text-command.test.js`, `haskell-remediation-path-contain.test.js`, `haskell-remediation-0195-16.test.js`.
70
+ - **`hs-random-os-entropy` v2 (since 0.195.16; `test/haskell/haskell-remediation-entropy.test.js`).** `haskell-fix.js` `fixRandomLine`: three shapes from the real-code true positives, `replicateM n (randomRIO (c1, c2))` (n bytes mapped into the character range), `randomRIO (lo, hi)` with integer literals (8 bytes decoded big-endian, reduced into the range), `(take n . map f . randomRs (lo, hi)) <$> newStdGen` (n bytes reduced and mapped). The reduction is a modulus (bias stated in the consequences). `System.Entropy` is imported when missing, so the patch is a MITIGATION (the operator declares `entropy`) and FULL only when the file already imported it; the same relaxation applies to the older `name = randomIO` shape. The unused System.Random names are dropped from the import. Effective-check kind `random-line` (the edited line, located by content after the import removal may have moved it, reads `getEntropy n` and calls no System.Random name). Verified on the three real sites (scotty, gitit, hackage-server) with `entropy` installed for GHC 9.4.8 on the measuring host.
71
+ - **Rule precision from semantics (since 0.195.15; `test/haskell/haskell-rule-precision.test.js`).** `haskell-security-rules.js`: a partial list function (`NONEMPTY_TOTAL`) on a list that is a cons, a non-empty literal, or a concatenation with one of those (`nonEmptyList`) is total and not reported; the conditions a function tests include the first argument of `when`/`unless`/`guard` (`COND_COMBINATOR`), not only `if` nodes; a comparison of a list's LENGTH with a number (`case length xs of 3`, `length xs == 2`; `LENGTH_CALL`, `lengthVars` for the scratch variable the IR puts a case scrutinee in) tests the list. `hash x :: Digest MD5` / `hashlazy x :: Digest SHA1`: `haskell-syntax.js` keeps a `Digest <algo>` annotation as a `typed` node (`DIGEST_TYPE`), `haskell-ir.js` returns the inner call stamped `hs.digest`, and `hs-weak-hash` treats it like `hashWith MD5`. `hs-unsafe-io-boundary` is already lowered to `info` for every site by `engine.js` (`originalSeverity` kept), so no shape rule was added for it.
69
72
  - **Record dot, labels, global cells, lambda-form functions, more syntax (since 0.195.14).** `haskell-syntax.js` reads `r.field` / `(e).field` / `(.field)` as a field selection (`sel` token, `RECORD_DOT_PRAGMA`; the IR lowers `field r` to a member read of a known record field) and `#field` as a label value (`LABELS_PRAGMA`; the IR lowers it to a literal, so IHP's `get #f r` / `set #f v r` are library calls carrying the record's and the value's taint, not a field selection: a stated limit); both only under the LANGUAGE pragma. `\cases` (`lamcases`), `mdo` (a `do` under `RECDO_PRAGMA`; `rec` blocks are not read), GADT `data T where` constructors (names, record fields, arity by arrows) and `pattern P ..` (a `patsyn` declaration, not a function named `pattern`) parse. `haskell-ir.js` `lambdaBodied` rewrites `f = \x -> ..`, `f = \case ..`, `f = \cases ..` into `f x = ..` at the top level and in `let`/`where` (a call then resolves to the function instead of applying a closure value). `cellBodyOf` recognises a module-level cell (`name = unsafePerformIO (newIORef ..)`, MVar/TVar/STRef alike) and `lowerCellOp` names it `$cell:<qid>` for the whole project (a read is that ident with `hs.globalCell`, a write an assignment to it); `dataflow/engine.js` `_cellSeeds` records a cell any function assigned tainted data to and `_unionAnnotationTaint` seeds it at the entry of every Haskell function, the fixed-point pre-pass re-running when the set grows (flow-insensitive; the root is not carried across, so the trust verdict of the reader is `unattributed`). `aliasBodyOf` accepts a partial application of a library function to names or literals (`remember = writeIORef lastName`), with the argument names checked for shadowing. `haskell-models.js`: typed-process `proc` (CWE-78 program, CWE-88 arguments), esqueleto `rawSql`/`rawExecute`, optparse-applicative `execParser`/`customExecParser` as a `cli` source, versions recorded in `HS_PACKAGES`. Pinned in `test/haskell/haskell-{record-dot,global-cell,syntax-extensions}.test.js`.
70
73
  - **String-interpolating quasi-quoters (since 0.195.14; `test/haskell/haskell-quasiquote-interp.test.js`).** `haskell-parser.js` `INTERP_QUOTERS` maps a module to its quoters and interpolation syntax (`hash` `#{e}`: Data.String.Interpolate, Data.String.Interpolate.IsString, Text.Shakespeare.Text; `dollar` `$name`/`${e}`: NeatInterpolation; `brace` `{e}`/`{e:spec}`: PyF); `interpolationsOf` reads them (escapes `$$`, `{{`; a format spec after a top-level `:` is dropped). An accepted quote is rewritten in place, same length and newlines, as `(iQ (e1) (e2))` (kind `quasiquote-interp`, non-opaque, assumption `upstream-quoter`); a literal-only body stays `quasiquote-inert`; an unimported module or an unknown quoter stays `quasiquote` (opaque). `haskell-ir.js` `applyVar` lowers an UNBOUND `iQ` application to a concatenation (`binary +`, `hs.concat`, `hs.quasiquote`), so the taint engine follows it and a sanitizer inside an interpolation is on the path. The template text is not modelled (no markup literal for `htmlSkeleton` sinks).
71
74
  - **The discovery set and two layout rules (since 0.195.13; `bench/dag/discovery-haskell`, pinned in `test/haskell/haskell-discovery-shapes.test.js`).** Six projects the engine had never been run on (hoogle, the wai and servant monorepos, mighttpd2, postgres-websockets, the yesod scaffold), scanned once and every flagged site read and labelled (`labels.json`, `USE-LOG.md`): a DEVELOPMENT set, not the precision gate's fresh set and not the sealed set. Hoogle's request handler was dropped by two layout rules, fixed in BOTH parsers: a `where` that starts a line at an implicit block's own indent closes the block (Haskell's parse-error(t) rule; `haskell-parser.js` `openBlock`, as `haskell-syntax.js` `blockAfter` already did), and a `;` separates the items of an implicit block only on a line that starts at the block's indent and opened no nested block (`do`, `of`, `let`, `where`, `\case`) earlier on that line (`splitItems` in both; `s <- act pay; pure s` on one line of a nested `do`, `remod t = do (a,_) <- f t; pure a` in a `where`). The route-contract detector (`posture/api-contract.js` `contractScope`) now scopes a contract file to the package that holds it, which is what turned a servant monorepo's two example `swagger.json` files into 78 Haskell route findings.
@@ -65,7 +65,9 @@ const litValue = (t) => t.slice(1, -1);
65
65
  /** `"a" ++ n ++ "b"` -> [{lit:'a'},{var:'n'},{lit:'b'}], or null for anything we cannot prove simple. */
66
66
  function concatParts(expr) {
67
67
  const e = trimParens(expr);
68
- const raw = topLevelSplit(e, '++');
68
+ let raw = topLevelSplit(e, '++');
69
+ if (raw && raw.length === 1) { const t = topLevelSplit(e, '<>'); if (t && t.length > 1) raw = t; } // a Text (Semigroup) concatenation
70
+ else if (raw && topLevelSplit(e, '<>')?.length > 1) return null; // both operators: not a shape this reads
69
71
  if (!raw) return null;
70
72
  const out = [];
71
73
  for (const r of raw) {
@@ -188,15 +190,41 @@ function applyImportDelta(orig, withImports, body) {
188
190
 
189
191
  function fixProcess(text, f) {
190
192
  const lb = lineBounds(text, f.line);
191
- const call = findCall(lb.text, ['callCommand', 'system']);
193
+ const call = findCall(lb.text, ['callCommand', 'system', 'spawnCommand']);
192
194
  if (!call) return null;
193
195
  let aStart = call.after; while (lb.text[aStart] === ' ') aStart++;
194
196
  let aEnd;
195
197
  if (lb.text[aStart] === '$') { aStart++; while (lb.text[aStart] === ' ') aStart++; aEnd = lb.text.length; }
196
198
  else aEnd = atomEnd(lb.text, aStart);
197
199
  if (aEnd < 0) return null;
198
- const parts = concatParts(lb.text.slice(aStart, aEnd));
200
+ // Since 0.195.16: a Text command, `system (cs t)` / `(T.unpack t)`, whose Text is a `<>` concatenation on the line or in a one-line
201
+ // `let` of the same function used nowhere else. The unpack is kept per piece, so each argument is still a String.
202
+ let argText = trimParens(lb.text.slice(aStart, aEnd));
203
+ let unpackFn = null; let letLine = -1;
204
+ const um = /^(cs|(?:[A-Z][\w']*\.)*unpack)\s+(.+)$/.exec(argText);
205
+ if (um) { unpackFn = um[1]; argText = trimParens(um[2]); }
206
+ if (IDENT.test(argText)) {
207
+ const ls = text.split('\n');
208
+ let start = 0; for (let i = f.line - 1; i >= 0; i--) { const mm = TOP_DECL.exec(ls[i] || ''); if (mm && !NOT_A_DEF.has(mm[1])) { start = i; break; } }
209
+ let end = ls.length; for (let i = f.line; i < ls.length; i++) { const mm = TOP_DECL.exec(ls[i] || ''); if (mm && !NOT_A_DEF.has(mm[1])) { end = i; break; } }
210
+ const uses = new RegExp(`(?<![\\w'.])${argText}(?![\\w'])`, 'g');
211
+ const body = ls.slice(start, end);
212
+ let bound = null;
213
+ for (let i = 0; i < body.length; i++) { const mm = new RegExp(`^\\s+let\\s+${argText}\\s*=\\s*(.+)$`).exec(body[i]); if (mm && start + i < f.line - 1) { bound = { i: start + i, expr: mm[1].trim() }; } }
214
+ const count = (body.join('\n').match(uses) || []).length;
215
+ if (bound && count === 2 && !/^\s+let\b/.test(ls[bound.i + 1] || '') && !/^\s{2,}\S/.test(ls[bound.i + 1] || '') || (bound && count === 2 && !(ls[bound.i + 1] || '').trim())) { argText = trimParens(bound.expr); letLine = bound.i; }
216
+ else if (bound && count === 2) {
217
+ // the binding has no continuation only when the next line is not indented deeper than `let`'s binding
218
+ const letIndent = (/^\s*/.exec(ls[bound.i]) || [''])[0].length;
219
+ const nextIndent = (/^\s*/.exec(ls[bound.i + 1] || '') || [''])[0].length;
220
+ if ((ls[bound.i + 1] || '').trim() && nextIndent > letIndent) return null;
221
+ argText = trimParens(bound.expr); letLine = bound.i;
222
+ }
223
+ }
224
+ if (/(?<![\w'])(?:if|case|let|do)(?![\w'])/.test(argText)) return null;
225
+ const parts = concatParts(argText);
199
226
  if (!parts || !parts.some((p) => p.var)) return null; // a bare variable: no provable argv split
227
+ if (unpackFn === null && parts.some((p) => p.var) && /\bcs\b|unpack/.test(argText)) return null;
200
228
  // tokenise into words; a word is a list of pieces
201
229
  const words = []; let cur = [];
202
230
  const flush = () => { if (cur.length) { words.push(cur); cur = []; } };
@@ -209,7 +237,8 @@ function fixProcess(text, f) {
209
237
  if (!words.length || words[0].some((p) => p.var)) return null; // the program itself must be a literal
210
238
  if (/[|&;<>`$*?()\\]/.test(words[0].map((p) => p.lit).join('')) || words.slice(1).some((w) => w.every((p) => p.lit !== undefined) && /[|&;<>`$*?()]/.test(w.map((p) => p.lit).join('')))) return null; // shell syntax in the literal: not an argv
211
239
  const prog = words[0].map((p) => p.lit).join('');
212
- const argExpr = (w) => (w.length === 1 && w[0].lit !== undefined ? `"${w[0].lit}"` : (w.length === 1 ? w[0].var : `(${w.map((p) => (p.lit !== undefined ? `"${p.lit}"` : p.var)).join(' ++ ')})`));
240
+ const v = (name) => (unpackFn ? `${unpackFn} ${name}` : name);
241
+ const argExpr = (w) => (w.length === 1 && w[0].lit !== undefined ? `"${w[0].lit}"` : (w.length === 1 ? v(w[0].var) : `(${w.map((p) => (p.lit !== undefined ? `"${p.lit}"` : v(p.var))).join(' ++ ')})`));
213
242
  let args = words.slice(1);
214
243
  // End option parsing before the first value-bearing argument, but only when no earlier literal is itself an
215
244
  // option (it might take a value, and "--" would then be consumed as that value). Otherwise leave it to the gate.
@@ -222,7 +251,9 @@ function fixProcess(text, f) {
222
251
  const imp = ensureImport(text, 'System.Process', [fn]); if (!imp) return null;
223
252
  const repl = `${call.prefix || imp.prefix}${fn} "${prog}" [${args.map(argExpr).join(', ')}]`;
224
253
  const replaced = lb.text.slice(0, call.index) + repl + lb.text.slice(aEnd);
225
- const next = applyImportDelta(text, imp.text, splice(text, lb.start, lb.end, replaced));
254
+ let edited = splice(text, lb.start, lb.end, replaced);
255
+ if (letLine >= 0) { const ls = edited.split('\n'); ls.splice(letLine, 1); edited = ls.join('\n'); } // the one-line `let` is now unused: -Werror would reject it
256
+ const next = applyImportDelta(text, imp.text, edited);
226
257
  return { ruleId: 'hs-process-argv', label: 'MITIGATION', text: next, explanation: `Replaced a shell command string with a direct ${fn} of "${prog}" and an argument list; no shell parses the value${whole ? '' : ' (a value fused into an argument is still passed as data)'}. ${terminated ? 'A "--" now ends option parsing before the value (assumes the program follows that convention).' : 'A value that starts with "-" can still be read as an option by the program: add "--" or validate it.'}`, behavior: { kind: 'process', program: prog, argv: args.map((w) => w.map((p) => (p.lit !== undefined ? { lit: p.lit } : { var: p.var }))) } };
227
258
  }
228
259
 
@@ -279,6 +310,14 @@ function splitOperators(expr) {
279
310
 
280
311
  /** The declared types of the parameters of the top-level function containing `line`: { name -> type text }, from its signature and first clause. */
281
312
  function paramTypesAt(text, line) {
313
+ const sig = signatureAt(text, line);
314
+ if (!sig) return null;
315
+ const out = {};
316
+ sig.params.forEach((p, i) => { if (sig.types[i] !== undefined) out[p] = sig.types[i]; });
317
+ return out;
318
+ }
319
+ /** The top-level clause enclosing `line` (1-based) and its signature: { name, params, types (the last is the result), clause (0-based line) }. */
320
+ function signatureAt(text, line) {
282
321
  const lines = text.split('\n');
283
322
  let clause = -1;
284
323
  for (let i = Math.min(line - 1, lines.length - 1); i >= 0; i--) if (/^[a-z_][\w']*\b[^:\n]*=/.test(lines[i]) && !/^[a-z_][\w']*\s*::/.test(lines[i])) { clause = i; break; }
@@ -300,9 +339,45 @@ function paramTypesAt(text, line) {
300
339
  cur += c;
301
340
  }
302
341
  types.push(cur.trim());
303
- const out = {};
304
- params.forEach((p, i) => { if (types[i] !== undefined) out[p] = types[i]; });
305
- return out;
342
+ return { name, params, types, clause };
343
+ }
344
+
345
+ // hs-path-contain (0.195.16): a path parameter that reaches a file operation anywhere in its function (the sink may sit in a callee or a
346
+ // `where` helper: the two dhall test-server sites are a read and an existence test under one FilePath parameter). A guard CLAUSE is
347
+ // added before the function's first equation, rejecting a parent-directory segment or an absolute path, and falls through to the
348
+ // existing equations otherwise, so no line of the body moves. The check is the one the taint engine's guard recogniser already
349
+ // understands (`".." \`elem\` splitDirectories p`), so the rescan judges the same semantics, not a name. Only a function with exactly
350
+ // one String/FilePath parameter and a `Maybe` result (bare, or under one monad) is rewritten: `Nothing` is the only failure value the
351
+ // fixer can construct without inventing an error type. A MITIGATION: a symbolic link inside the base can still point outside it.
352
+ function fixPathContain(text, f) {
353
+ const sig = signatureAt(text, f.line);
354
+ if (!sig) return null;
355
+ const pathParams = sig.params.filter((p, i) => /^(?:String|FilePath)$/.test(sig.types[i] || ''));
356
+ if (pathParams.length !== 1) return null;
357
+ const param = pathParams[0];
358
+ const result = sig.types[sig.types.length - 1] || '';
359
+ const rm = /^(?:(?:[A-Z][\w'.]*\s+)?\(\s*Maybe\s+[^()]+\)|Maybe\s+[^()\s]+(?:\s+[^()\s]+)*)$/.exec(result.trim());
360
+ if (!rm) return null;
361
+ const monadic = /^[A-Z]/.test(result.trim()) && !/^Maybe\b/.test(result.trim());
362
+ const fail = monadic ? 'pure Nothing' : 'Nothing';
363
+ const lines = text.split('\n');
364
+ // the first equation of this function (the clause enclosing the line may be a later one)
365
+ let first = sig.clause; for (let i = 0; i < sig.clause; i++) if (new RegExp(`^${sig.name}(?![\\w'])`).test(lines[i]) && !new RegExp(`^${sig.name}\\s*::`).test(lines[i])) { first = i; break; }
366
+ if (new RegExp(`splitDirectories\\s+${param}(?![\\w'])`).test(stripComments(text))) return null; // already checked somewhere
367
+ const imp = ensureImport(text, 'System.FilePath', ['splitDirectories', 'isAbsolute']); if (!imp) return null;
368
+ const guard = `${sig.name} ${param}\n | ".." \`elem\` ${imp.prefix}splitDirectories ${param} || ${imp.prefix}isAbsolute ${param} = ${fail}`;
369
+ lines.splice(first, 0, guard);
370
+ const next = applyImportDelta(text, imp.text, lines.join('\n'));
371
+ return {
372
+ ruleId: 'hs-path-contain', label: 'MITIGATION', text: next,
373
+ explanation: `Added a guard equation to \`${sig.name}\` that answers ${fail} when \`${param}\` has a ".." segment or is absolute, before any file operation runs; every other call falls through to the existing equations unchanged.`,
374
+ consequences: [
375
+ `A request naming a parent directory or an absolute path now gets the function's ${fail} (its not-found answer) instead of the file; a symbolic link inside the served directory can still point outside it (resolve with canonicalizePath and compare the prefix when links are possible).`,
376
+ 'The check is on the parameter as received: a path decoded or joined later inside the function is not re-checked.',
377
+ ],
378
+ behavior: { kind: 'path-contain', fn: sig.name, param },
379
+ expectEffective: { kind: 'path-contain', fn: sig.name, param, fail },
380
+ };
306
381
  }
307
382
 
308
383
  function fixPath(text, f) {
@@ -336,17 +411,46 @@ function fixPath(text, f) {
336
411
  }
337
412
 
338
413
  const LOG_CALLS = ['putStrLn', 'putStr', 'hPutStrLn', 'hPutStr', 'print', 'trace', 'traceShow', 'traceShowId', 'logInfoN', 'logDebugN', 'logWarnN', 'logErrorN', 'logInfo', 'logDebug', 'logWarn', 'logError'];
414
+ // hxt (0.195.16): `withSubstDTDEntities yes` (or `True`) on the reported line becomes `no` (`False`): the parser no longer substitutes
415
+ // entities declared in a DTD, which is the whole of the finding, so the label is FULL. `yes`/`no` come from Text.XML.HXT.Core (hxt
416
+ // 9.3.1.22 re-exports the XmlState type definitions); an explicit import list that names `yes` but not `no` gets `no` added.
417
+ function fixXmlEntities(text, f) {
418
+ const lb = lineBounds(text, f.line);
419
+ const m = /(?<![\w'.])((?:[A-Z][\w']*\.)?)withSubstDTDEntities\s+((?:[A-Z][\w']*\.)?)(yes|True)(?![\w'])/.exec(lb.text);
420
+ if (!m) return null;
421
+ const [, , qual, val] = m;
422
+ const to = val === 'True' ? 'False' : `${qual}no`;
423
+ let t = text;
424
+ if (val === 'yes' && !qual) {
425
+ const info = importInfo(text, 'Text.XML.HXT.Core');
426
+ if (info && !info.qualified && info.items && !info.items.some((x) => x.trim() === 'no')) { const e = ensureImport(text, 'Text.XML.HXT.Core', ['no']); if (!e) return null; t = e.text; }
427
+ }
428
+ const shifted = t.split('\n').length - text.split('\n').length;
429
+ const lb2 = lineBounds(t, f.line + shifted);
430
+ if (lb2.text !== lb.text) return null;
431
+ const at = lb2.start + m.index + m[0].length - val.length;
432
+ const next = splice(t, at, at + val.length, to);
433
+ return {
434
+ ruleId: 'hs-xml-no-entity-substitution', label: 'FULL', text: next,
435
+ explanation: `withSubstDTDEntities now reads ${to}: entities declared in a document's DTD are no longer substituted, so an external or recursive entity in untrusted XML is not expanded by this parser configuration.`,
436
+ consequences: ['A document that relies on its own DTD entities for content now yields the entity references unexpanded; a document that needs them must be parsed with a separate, trusted configuration.'],
437
+ behavior: { kind: 'hxt-entities', line: f.line + shifted },
438
+ expectEffective: { kind: 'hxt-entities', line: f.line + shifted },
439
+ };
440
+ }
339
441
  function fixLogging(text, f) {
340
442
  const lb = lineBounds(text, f.line);
341
443
  const call = findCall(lb.text, LOG_CALLS);
342
444
  if (!call) return null;
343
445
  const head = lb.text.slice(0, call.after); let rest = lb.text.slice(call.after);
344
446
  let n = 0;
345
- rest = rest.replace(/(?<![\w'.])(?:show\s+)?((?:[A-Za-z_][\w']*\.)?[a-z_][\w']*)(?![\w'(])/g, (all, id) => {
447
+ // Only code is rewritten: a string literal is kept as it is (before 0.195.16 the word inside `" password="` was replaced too, which
448
+ // produced an unparseable line; the guide's captured example showed it).
449
+ rest = rest.split(/("(?:[^"\\]|\\.)*")/).map((seg, i) => (i % 2 === 1 ? seg : seg.replace(/(?<![\w'.])(?:show\s+)?((?:[A-Za-z_][\w']*\.)?[a-z_][\w']*)(?![\w'(])/g, (all, id) => {
346
450
  const bare = id.replace(/^.*\./, '');
347
451
  if (/^(?:show|pack|unpack|stderr|stdout)$/.test(bare) || !SENSITIVE.test(bare)) return all;
348
452
  n++; return '"[REDACTED]"';
349
- });
453
+ }))).join('');
350
454
  if (!n) return null;
351
455
  const next = splice(text, lb.start, lb.end, head + rest);
352
456
  return { ruleId: 'hs-log-redact', label: 'MITIGATION', text: next, explanation: `Replaced ${n} sensitive log argument(s) with "[REDACTED]". The value is no longer logged; the log line loses that detail.`, behavior: { kind: 'log', redacted: n } };
@@ -408,8 +512,52 @@ export function tlsSettingsOf(text) {
408
512
  return out;
409
513
  }
410
514
 
515
+ // HsOpenSSL (0.195.16, from IHP's OpenAI client): `contextSetVerificationMode ctx VerifyNone` as a statement of a do block becomes
516
+ // `contextSetDefaultVerifyPaths ctx` (the system CA locations; HsOpenSSL 0.11.7.11 documents it as "the default locations from which CA
517
+ // certificates are loaded") followed by `contextSetVerificationMode ctx (VerifyPeer True True Nothing)` (a certificate is required,
518
+ // requested once, no callback). A MITIGATION: the chain is now verified against the system store, but HsOpenSSL checks the host name
519
+ // only through `enableHostnameValidation` on the SSL connection object, which this context-level code never sees, so a valid
520
+ // certificate for another host is still accepted; the consequence says so.
521
+ function fixOpenSslVerify(text, f) {
522
+ const info = importInfo(text, 'OpenSSL.Session');
523
+ if (!info || info.hiding) return null;
524
+ const STMT = /^(\s+)((?:[A-Z][\w']*\.)?)contextSetVerificationMode\s+([a-z_][\w']*)\s+((?:[A-Z][\w']*\.)?)VerifyNone\s*$/;
525
+ const lines = text.split('\n');
526
+ const okQual = (qual, qual2) => (!qual || (info.qualified && (info.alias || 'OpenSSL.Session') + '.' === qual)) && (!qual2 || qual2 === qual);
527
+ // a statement: the nearest less-indented preceding non-blank line opens a do block (`\ctx -> do`, `= do`, `do`)
528
+ const isStatement = (i, indent) => { for (let j = i - 1; j >= 0; j--) { const l = lines[j]; if (!l.trim()) continue; const ind = (/^\s*/.exec(l) || [''])[0].length; if (ind < indent.length) return /\bdo\s*$/.test(stripComments(l)); } return false; };
529
+ // The reported line first; every other VerifyNone statement of the same shape in the file is rewritten with it (one logical fix: the
530
+ // rule reports the class once per file, so a second site left as it was would keep the finding and block the patch for a reason the
531
+ // reader could not see). A VerifyNone the shape does not cover stays, and the rescan then says so.
532
+ const m0 = STMT.exec(lines[f.line - 1] || '');
533
+ if (!m0 || !okQual(m0[2], m0[4]) || !isStatement(f.line - 1, m0[1])) return null;
534
+ const sites = [];
535
+ lines.forEach((l, i) => { const m = STMT.exec(l); if (m && okQual(m[2], m[4]) && isStatement(i, m[1])) sites.push({ i, indent: m[1], ctx: m[3], qual: m[2] }); });
536
+ let t = text; let prefix = m0[2];
537
+ if (!m0[2]) { const e = ensureImport(text, 'OpenSSL.Session', ['contextSetDefaultVerifyPaths', 'contextSetVerificationMode', 'VerificationMode(..)']); if (!e) return null; t = e.text; prefix = e.prefix; }
538
+ const shifted = t.split('\n').length - lines.length;
539
+ const out = t.split('\n');
540
+ for (const s of [...sites].reverse()) {
541
+ if (out[s.i + shifted] !== lines[s.i]) return null;
542
+ out.splice(s.i + shifted, 1, `${s.indent}${prefix}contextSetDefaultVerifyPaths ${s.ctx}`, `${s.indent}${prefix}contextSetVerificationMode ${s.ctx} (${prefix}VerifyPeer True True Nothing)`);
543
+ }
544
+ // the mode statement of each site in the patched file: the import shift, plus one line per site above it
545
+ const modeLines = sites.map((s, k) => s.i + shifted + k + 2);
546
+ const next = out.join('\n');
547
+ const where = sites.length === 1 ? 'The OpenSSL context' : `Each of the ${sites.length} OpenSSL contexts set up in this file`;
548
+ return {
549
+ ruleId: 'hs-tls-validate', label: 'MITIGATION', text: next,
550
+ explanation: `${where} now loads the system CA locations (contextSetDefaultVerifyPaths) and requires a peer certificate that verifies against them (VerifyPeer True True Nothing) instead of VerifyNone${sites.length > 1 ? ` (${sites.length} sites, one per client function; the rule reports the class once per file)` : ''}.`,
551
+ consequences: [
552
+ 'Connections to a server whose certificate is self-signed, expired or issued by an authority missing from the system store now fail.',
553
+ 'The host name is NOT verified by this change: HsOpenSSL checks it only through enableHostnameValidation on the SSL connection object, which this context-level code does not have, so a valid certificate issued for another host is still accepted.',
554
+ ],
555
+ behavior: { kind: 'tls-openssl', sites: sites.map((s, k) => ({ ctx: s.ctx, line: modeLines[k] })) },
556
+ expectEffective: { kind: 'tls-openssl', sites: sites.map((s, k) => ({ ctx: s.ctx, line: modeLines[k] })) }, // each mode statement; the paths statement is the line above it
557
+ };
558
+ }
411
559
  function fixTls(text, f) {
412
- if (!importInfo(text, 'Network.Connection') && !importInfo(text, 'Network.Connection.Types')) return null; // the detector's own precondition
560
+ if (!importInfo(text, 'Network.Connection') && !importInfo(text, 'Network.Connection.Types')) return fixOpenSslVerify(text, f); // the detector's own precondition
413
561
  const lb = lineBounds(text, f.line);
414
562
  const m = new RegExp(TLS_CTOR_RE.source).exec(lb.text);
415
563
  if (!m) return null;
@@ -616,16 +764,85 @@ function fixCookie(text, f) {
616
764
  // and ONLY when the file already imports System.Entropy (the dependency is therefore already declared). `randomRIO (lo, hi)`, a range, a generator
617
765
  // threaded through other code, a non-integer result and every other shape are refused: the result type of an OS CSPRNG differs from System.Random's.
618
766
  const FIXED_WIDTH = { Word8: 1, Word16: 2, Word32: 4, Word64: 8, Word: 8, Int8: 1, Int16: 2, Int32: 4, Int64: 8, Int: 8 };
767
+ // The three shapes of 0.195.16 (each from a real site labelled TP: scotty's session id, gitit's session key, hackage-server's digest nonce):
768
+ // replicateM N (randomRIO ('a', 'z')) -> N bytes, each mapped into the character range
769
+ // randomRIO (lo, hi) (integer literals) -> 8 bytes decoded big-endian, reduced into the range
770
+ // (take N . map f . randomRs (lo, hi)) <$> newStdGen -> N bytes, each reduced into the range and mapped by f
771
+ // The reduction is `b mod (hi - lo + 1)`: for a range that does not divide the byte space (26 letters, 10^9) the low values are chosen slightly
772
+ // more often (at most 2% for 26 of 256, negligible for 10^9 of 2^64), stated in the consequences; a rejection loop would need a different
773
+ // program shape. `System.Entropy` is imported when missing, which makes the patch a MITIGATION: the `entropy` package must be declared by
774
+ // the component, and only a build checks that.
775
+ const ENT_DECODE = "AgenticBS.foldl' (\\acc b -> acc * 256 + fromIntegral b) (0 :: Integer)";
776
+ function fixRandomLine(text, f) {
777
+ const lb = lineBounds(text, f.line);
778
+ const line = lb.text;
779
+ const chr = (s) => (s.length === 3 ? s[1] : s === "'\\''" ? "'" : null);
780
+ let m; let repl = null; let bytes = 0; let what = '';
781
+ const used = [];
782
+ if ((m = /(?<![\w'.])replicateM\s+(\d+)\s+\(\s*(?:System\.Random\.)?randomRIO\s+\(\s*('(?:[^'\\]|\\.)')\s*,\s*('(?:[^'\\]|\\.)')\s*\)\s*\)/.exec(line)) && chr(m[2]) !== null && chr(m[3]) !== null) {
783
+ bytes = Number(m[1]); used.push('randomRIO');
784
+ repl = `(map (\\b -> toEnum (fromEnum ${m[2]} + fromIntegral b \`mod\` (fromEnum ${m[3]} - fromEnum ${m[2]} + 1))) . AgenticBS.unpack <$> getEntropy ${bytes})`;
785
+ what = `${bytes} characters in the range ${m[2]} to ${m[3]}`;
786
+ } else if ((m = /(?<![\w'.])(?:System\.Random\.)?randomRIO\s+\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)/.exec(line))) {
787
+ const lo = BigInt(m[1]); const hi = BigInt(m[2]);
788
+ if (hi <= lo) return null;
789
+ bytes = 8; used.push('randomRIO');
790
+ repl = `((\\w -> ${m[1]} + fromIntegral (w \`mod\` ${(hi - lo + 1n).toString()})) . ${ENT_DECODE} <$> getEntropy 8)`;
791
+ what = `an integer in the range ${m[1]} to ${m[2]}`;
792
+ } else if ((m = /(?:fmap\s+\(\s*take\s+(\d+)\s*\.\s*map\s+([a-z_][\w'.]*)\s*\.\s*(?:System\.Random\.)?randomRs\s+\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)\s*\)\s+newStdGen|\(\s*take\s+(\d+)\s*\.\s*map\s+([a-z_][\w'.]*)\s*\.\s*(?:System\.Random\.)?randomRs\s+\(\s*(-?\d+)\s*,\s*(-?\d+)\s*\)\s*\)\s*<\$>\s*newStdGen)/.exec(line))) {
793
+ const n = m[1] || m[5]; const fn = m[2] || m[6]; const lo = BigInt(m[3] || m[7]); const hi = BigInt(m[4] || m[8]);
794
+ if (hi <= lo) return null;
795
+ bytes = Number(n); used.push('randomRs', 'newStdGen');
796
+ repl = `(map (${fn} . (\\b -> ${lo.toString()} + fromIntegral b \`mod\` ${(hi - lo + 1n).toString()})) . AgenticBS.unpack <$> getEntropy ${bytes})`;
797
+ what = `${bytes} values in the range ${lo} to ${hi} mapped by ${fn}`;
798
+ }
799
+ if (!repl || !(bytes > 0)) return null;
800
+ if (/\bAgenticBS\b/.test(text)) return null;
801
+ const entropyWasImported = !!importInfo(text, 'System.Entropy');
802
+ const e1 = ensureImport(text, 'System.Entropy', ['getEntropy']); if (!e1) return null;
803
+ let t2 = e1.text;
804
+ const lines = t2.split('\n'); let at = -1; stripComments(t2).split('\n').forEach((l, i) => { if (/^import\s/.test(l)) at = i; });
805
+ if (at < 0) return null;
806
+ lines.splice(at + 1, 0, 'import qualified Data.ByteString as AgenticBS');
807
+ t2 = lines.join('\n');
808
+ const newLineNo = f.line + (entropyWasImported ? 1 : 2); // the edited line moved down by the import(s) inserted above it
809
+ const lb2 = lineBounds(t2, newLineNo);
810
+ if (lb2.text !== line) return null;
811
+ const newLine = line.slice(0, m.index) + repl + line.slice(m.index + m[0].length);
812
+ let out = splice(t2, lb2.start, lb2.end, newLine);
813
+ // drop the System.Random names the file no longer uses (an unused import is an error under -Werror); the import goes when nothing is left
814
+ const code = () => out.split('\n').filter((l) => !/^import\s/.test(l)).join('\n');
815
+ const ri = importInfo(out, 'System.Random');
816
+ if (ri && !ri.qualified && ri.items) {
817
+ const keep = ri.items.filter((x) => !used.includes(x.replace(/\(.*$/, '').trim()) || new RegExp(`(?<![\\w'])${x.replace(/\(.*$/, '').trim()}(?![\\w'])`).test(code()));
818
+ if (!keep.length) out = out.replace(ri.line + '\n', '');
819
+ else if (keep.length !== ri.items.length) out = out.replace(ri.line, ri.line.replace(/\((.*)\)/, `(${keep.join(', ')})`));
820
+ }
821
+ const fnName = (() => { const ls = stripComments(text).split('\n'); let name = null; for (let i = 0; i < f.line; i++) { const mm = TOP_DECL.exec(ls[i] || ''); if (mm && !NOT_A_DEF.has(mm[1])) name = mm[1]; } return name; })();
822
+ const finalLine = out.split('\n').indexOf(newLine) + 1; // where the edited line sits once the unused import above it may have gone
823
+ if (finalLine < 1) return null;
824
+ return {
825
+ ruleId: 'hs-random-os-entropy', label: entropyWasImported ? 'FULL' : 'MITIGATION', text: out,
826
+ explanation: `${fnName || 'the action'} now draws ${what} from the operating system CSPRNG (System.Entropy.getEntropy, ${bytes} byte(s)) instead of System.Random's deterministic generator.`,
827
+ consequences: [
828
+ `Requires the \`entropy\` and \`bytestring\` packages to be declared by the component${entropyWasImported ? ' (this file already imported System.Entropy, so the first is)' : ' (the import of System.Entropy was ADDED: add `entropy` to build-depends)'}; the build is not checked against the cabal file.`,
829
+ 'Values are reduced into the range with a modulus, so for a range that does not divide the byte space the low values are drawn slightly more often (at most 2% for 26 letters over 256, negligible for an integer range over 2^64); the generated value is no longer predictable from a seed, which is what the finding is about.',
830
+ ],
831
+ behavior: { kind: 'random-line', fn: fnName, line: finalLine, bytes },
832
+ expectEffective: { kind: 'random-line', line: finalLine, bytes },
833
+ };
834
+ }
619
835
  function fixRandom(text, f) {
620
836
  const lb = lineBounds(text, f.line);
621
837
  const m = /^([a-z_][\w']*)\s*=\s*(?:System\.Random\.)?randomIO\s*$/.exec(lb.text);
622
- if (!m) return null;
838
+ if (!m) return fixRandomLine(text, f);
623
839
  const name = m[1];
624
840
  const sig = new RegExp(`^${name}\\s*::\\s*IO\\s+(${Object.keys(FIXED_WIDTH).join('|')})\\s*$`, 'm').exec(text);
625
841
  if (!sig) return null;
626
842
  if ((text.match(new RegExp(`^${name}\\s*=`, 'gm')) || []).length !== 1) return null;
627
843
  const bytes = FIXED_WIDTH[sig[1]];
628
- const ent = importInfo(text, 'System.Entropy'); if (!ent || ent.hiding) return null;
844
+ // Since 0.195.16 the import is added when missing (a MITIGATION: `entropy` must then be declared by the component).
845
+ const ent = importInfo(text, 'System.Entropy'); if (ent && ent.hiding) return null;
629
846
  const e1 = ensureImport(text, 'System.Entropy', ['getEntropy']); if (!e1) return null;
630
847
  if (/(?<![\w'.])AgenticBS\./.test(text)) return null;
631
848
  let t2 = e1.text;
@@ -642,8 +859,8 @@ function fixRandom(text, f) {
642
859
  if (!stillUsed && ri && !ri.qualified && ri.items && ri.items.length === 1 && ri.items[0] === 'randomIO') out = out.replace(ri.line + '\n', '');
643
860
  else if (!stillUsed && ri && !ri.qualified && ri.items && ri.items.includes('randomIO')) out = out.replace(ri.line, ri.line.replace(/\((.*)\)/, (_, inner) => `(${ri.items.filter((x) => x !== 'randomIO').join(', ')})`));
644
861
  return {
645
- ruleId: 'hs-random-os-entropy', label: 'FULL', text: out,
646
- explanation: `${name} now reads ${bytes} byte(s) from the operating system CSPRNG (System.Entropy.getEntropy) and decodes them big-endian into ${sig[1]}, instead of System.Random's deterministic generator.`,
862
+ ruleId: 'hs-random-os-entropy', label: ent ? 'FULL' : 'MITIGATION', text: out,
863
+ explanation: `${name} now reads ${bytes} byte(s) from the operating system CSPRNG (System.Entropy.getEntropy) and decodes them big-endian into ${sig[1]}, instead of System.Random's deterministic generator.${ent ? '' : ' The import of System.Entropy was added: add `entropy` to the component\'s build-depends.'}`,
647
864
  consequences: [`Requires the \`entropy\` and \`bytestring\` packages to be declared by the component (this file already imports System.Entropy, so the first is); the build is not checked against the cabal file.`, 'The value is uniform over the full range of its type; a caller that relied on a smaller range or on a seeded sequence changes behavior.'],
648
865
  behavior: { kind: 'random', fn: name, type: sig[1], bytes },
649
866
  expectEffective: { kind: 'random', fn: name, bytes },
@@ -1016,7 +1233,7 @@ function fixScottyBody(text, f) {
1016
1233
 
1017
1234
  const FIXERS = [
1018
1235
  { id: 'hs-scotty-body-limit', applies: (f) => f.rule === 'hs-unbounded-request-body' && /Web\.Scotty(?:\.Trans)?\.body\b/.test(String(f.description || f.detail || '')), run: fixScottyBody, reportedLineOnly: true },
1019
- { id: 'hs-password-kdf-migrate', applies: (f) => f.cwe === 'CWE-916' && (f.rule === 'hs-fast-password-hash' || /fast general-purpose hash/i.test(String(f.vuln || ''))), run: fixPasswordKdf },
1236
+ { id: 'hs-password-kdf-migrate', applies: (f) => f.cwe === 'CWE-916' && (f.rule === 'hs-fast-password-hash' || f.rule === 'hs-pbkdf2-low-iterations' || /fast general-purpose hash|too few iterations/i.test(String(f.vuln || ''))), run: fixPasswordKdf },
1020
1237
  { id: 'hs-tls-validate', applies: (f) => f.cwe === 'CWE-295' || f.rule === 'hs-tls-validation-disabled', run: fixTls, reportedLineOnly: true },
1021
1238
  { id: 'hs-cookie-attributes', applies: (f) => f.cwe === 'CWE-1004' || f.rule === 'hs-insecure-cookie', run: fixCookie, reportedLineOnly: true },
1022
1239
  { id: 'hs-random-os-entropy', applies: (f) => f.cwe === 'CWE-338' || f.rule === 'hs-predictable-random', run: fixRandom, reportedLineOnly: true },
@@ -1027,7 +1244,9 @@ const FIXERS = [
1027
1244
  { id: 'hs-html-escape-text', applies: (f) => f.cwe === 'CWE-79', run: fixHtmlText, reportedLineOnly: true },
1028
1245
  { id: 'hs-hash-sha256', applies: (f) => f.cwe === 'CWE-328' || f.rule === 'hs-weak-hash', run: fixWeakHash },
1029
1246
  { id: 'hs-log-redact', applies: (f) => f.cwe === 'CWE-532' || f.rule === 'hs-sensitive-logging', run: fixLogging },
1247
+ { id: 'hs-xml-no-entity-substitution', applies: (f) => f.cwe === 'CWE-611' || f.rule === 'hs-xml-entity-substitution', run: fixXmlEntities, reportedLineOnly: true },
1030
1248
  { id: 'hs-path-takefilename', applies: (f) => f.cwe === 'CWE-22', run: fixPath },
1249
+ { id: 'hs-path-contain', applies: (f) => f.cwe === 'CWE-22', run: fixPathContain, reportedLineOnly: true },
1031
1250
  ];
1032
1251
  export const SUPPORTED_FIX_IDS = Object.freeze(FIXERS.map((x) => x.id));
1033
1252
 
@@ -1098,18 +1317,19 @@ const DEFAULT_CONSEQUENCES = Object.freeze({
1098
1317
 
1099
1318
  // Classes whose repair needs a choice the code does not contain (a policy, a library, a limit). The scanner reports them; it does not guess.
1100
1319
  // Each reason ends with "The decision needed: ..." naming what the operator has to decide; planHaskellFix returns that part as `decision`.
1101
- const CLASS_REASONS = Object.freeze({
1320
+ export const CLASS_REASONS = Object.freeze({
1102
1321
  'CWE-918': 'server-side request forgery has no line-level repair: a check on the URL string is bypassed by redirects, alternate encodings and DNS. The decision needed: which hosts or networks this service may call (an allow-list of exact hosts, or an address policy that rejects loopback, link-local and private ranges after name resolution and on every redirect).',
1322
+ 'CWE-78': 'a shell command string is replaced by a direct call of the program with an argument list. The recognised shape is callCommand/system/spawnCommand of a ++ or <> concatenation (String, or Text under cs/unpack, on the line or in a one-line let of the same function) whose first word is a string literal naming the program. The decision needed: when the program itself is a variable (an editor or tool chosen through an environment variable, say), how that value splits into a program and its own arguments, because a configured command such as "code --goto" is not one program name; and for an argument built with if/case or a function call, the exact argument list.',
1103
1323
  'CWE-88': 'argument injection is repaired by a rule about what the called program accepts; inserting "--" only helps programs that honour it, so no patch is made. The decision needed: the permitted values for this argument (a closed list or a strict pattern), or the option of that program that ends option parsing.',
1104
1324
  'CWE-916': 'password hashing needs a password KDF (Argon2id, scrypt, bcrypt, or PBKDF2 with a high iteration count) from a library the project may not declare as a dependency, and every stored hash was produced by the old function: swapping it makes every existing password fail to verify and locks the users out. The one shape rewritten is a migration: a pure one-line `name salt password = ...` of one text type (String, Text or strict ByteString) in a file that already imports cryptonite or crypton and Data.Text or Data.ByteString, whose comparisons all sit in this file; it keeps the old function for verification, adds PBKDF2-HMAC-SHA256 and a verify helper, and this is not that shape. The decision needed: which KDF and library, with which cost parameters, and the migration (verify the old hash once, then re-hash with the new KDF at the next successful login, or force a password reset).',
1105
- 'CWE-338': 'a predictable generator is replaced by the operating system CSPRNG (System.Entropy or Crypto.Random), whose result type is bytes, not System.Random\'s: the surrounding code must change with it. Only a one-line action of type IO <fixed-width integer> equal to randomIO, in a file that already imports System.Entropy, is rewritten. The decision needed: the token\'s type and size (a 30-bit integer cannot be made unguessable by changing the generator) and the entropy library the project declares.',
1325
+ 'CWE-338': 'a predictable generator is replaced by the operating system CSPRNG (System.Entropy or Crypto.Random), whose result type is bytes, not System.Random\'s: the surrounding code must change with it. Four shapes are rewritten (System.Entropy imported when missing, which makes the patch a MITIGATION until `entropy` is declared): a one-line action of type IO <fixed-width integer> equal to randomIO; `replicateM n (randomRIO (c1, c2))` over a character range; `randomRIO (lo, hi)` over integer literals; `(take n . map f . randomRs (lo, hi)) <$> newStdGen`. Any other shape (a generator threaded through other code, a non-literal range, a non-integer result) is not. The decision needed: the token\'s type and size (a 30-bit integer cannot be made unguessable by changing the generator) and the entropy library the project declares.',
1106
1326
  'CWE-1004': 'cookie attributes live in different places per framework. The recognised shapes are a Set-Cookie header string literal, a Web.Cookie SetCookie record update, servant-auth cookieIsSecure, and scotty setSimpleCookie/makeSimpleCookie; this is none of them, or an attribute is set from an expression. The decision needed: which attributes to send (Secure, HttpOnly, SameSite) and whether this cookie must work over plain HTTP.',
1107
1327
  'CWE-306': 'a missing authentication check needs a policy; the scanner reports the handler and does not invent the guard. The decision needed: which credential authenticates this route (session, token, API key) and what an unauthenticated caller receives (401, a redirect to login).',
1108
1328
  'CWE-770': 'a resource limit is a number only the operator can choose (how many bytes a body may have, how many requests, how large an allocation). Two shapes are recognised, each with a declared default: wai strictRequestBody of a Request variable in a file importing Network.Wai and Data.ByteString.Lazy, which gets a bounded reader, and scotty body in a file whose ScottyM block contains the read or names the handler, which gets setMaxRequestBodySize as that block\'s first statement (scotty 0.12 and later; refused when the block is in another file or a cabal bound excludes those versions); this is neither shape. The decision needed: the limit for this input, and what happens past it (reject with 413, truncate, spool to disk).',
1109
- 'CWE-295': 'enabling certificate validation can break connections the project depends on. Only the connection package\'s TLSSettingsSimple with a literal True is flipped. The decision needed: the trust store (the system store, a pinned CA bundle, a private authority) and host-name verification; an OpenSSL VerifyNone is not rewritten because VerifyPeer verifies nothing until a CA bundle is loaded and does not check the host name.',
1329
+ 'CWE-295': 'enabling certificate validation can break connections the project depends on. Only the connection package\'s TLSSettingsSimple with a literal True is flipped. An HsOpenSSL `contextSetVerificationMode ctx VerifyNone` statement is rewritten to load the default CA locations and require a verified peer (a mitigation: the host name is still unchecked). The decision needed: the trust store (the system store, a pinned CA bundle, a private authority) and host-name verification.',
1110
1330
  'CWE-798': 'a hard-coded secret must be rotated and moved to a secret store; rewriting the line would leave the leaked value valid. The decision needed: where the secret is read from at run time (environment, a secrets manager, a file outside the repository) and rotating the exposed value, which no edit of this file can do.',
1111
1331
  'CWE-611': 'XML entity handling depends on the parser configuration in use. The decision needed: whether this document source is trusted and which entity and DTD features the parser may enable.',
1112
- 'CWE-22': 'path traversal is repaired by confining the final path to a base directory. takeFileName is applied only to a String/FilePath parameter spliced under a fixed directory in a file operation; this shape (several segments, a base chosen at run time, a directory operation, a path handed through other functions) is not that. The decision needed: the directory the files must stay in, and whether a name containing separators or ".." is rejected or reduced to its last segment.',
1332
+ 'CWE-22': 'path traversal is repaired by confining the final path to a base directory. takeFileName is applied only to a String/FilePath parameter spliced under a fixed directory in a file operation, and a guard equation rejecting ".." segments and absolute paths is added only to a function with exactly one String/FilePath parameter and a Maybe result (bare or under one monad); this shape (a base chosen at run time, several path parameters, a result with no not-found value, a path handed through other functions) is neither. The decision needed: the directory the files must stay in, and whether a name containing separators or ".." is rejected or reduced to its last segment.',
1113
1333
  'CWE-79': 'output encoding depends on the context the value is written into (HTML text, attribute, script, URL) and on the library that renders it; only blaze-html\'s preEscapedToHtml and scotty\'s html of concatenated text are rewritten (the latter with an escaper defined in the file, for HTML text and quoted attribute values). The decision needed: render the value through the project\'s escaping HTML library, or send it as text/plain.',
1114
1334
  'CWE-601': 'an open redirect is repaired by restricting where the redirect may go. The decision needed: the permitted destinations (an allow-list of hosts, or relative paths only).',
1115
1335
  });
@@ -1227,6 +1447,37 @@ export function haskellEffectiveCheck(plan, patched) {
1227
1447
  if (!new RegExp(`getEntropy\\s+${want.bytes}\\s*$`).test(d)) return { ok: false, ran: true, detail: `the action does not read ${want.bytes} entropy byte(s)` };
1228
1448
  return { ok: true, ran: true, detail: `${want.fn} reads ${want.bytes} byte(s) from getEntropy and no longer calls System.Random` };
1229
1449
  }
1450
+ if (want.kind === 'tls-openssl') {
1451
+ const ls = stripComments(after).split('\n');
1452
+ for (const s of want.sites) {
1453
+ const set = ls[s.line - 1] || ''; const paths = ls[s.line - 2] || '';
1454
+ if (!new RegExp(`contextSetVerificationMode\\s+${s.ctx}\\s+\\((?:[A-Z][\\w']*\\.)?VerifyPeer True True Nothing\\)\\s*$`).test(set)) return { ok: false, ran: true, detail: `line ${s.line}: the context is not set to VerifyPeer True True Nothing` };
1455
+ if (!new RegExp(`contextSetDefaultVerifyPaths\\s+${s.ctx}\\s*$`).test(paths)) return { ok: false, ran: true, detail: `line ${s.line}: the context does not load the default CA locations before the mode is set` };
1456
+ }
1457
+ const none = (t) => (stripComments(t).match(/(?<![\w'])VerifyNone(?![\w'])/g) || []).length; // qualified uses count too
1458
+ if (none(after) !== none(before) - want.sites.length) return { ok: false, ran: true, detail: `the patch did not remove exactly ${want.sites.length} VerifyNone` };
1459
+ return { ok: true, ran: true, detail: `${want.sites.map((s) => s.ctx).join(', ')} load the default CA locations and require a verified peer certificate (${want.sites.length} site(s); host name not checked by this patch)` };
1460
+ }
1461
+ if (want.kind === 'hxt-entities') {
1462
+ const l = stripComments(after).split('\n')[want.line - 1] || '';
1463
+ if (!/(?<![\w'.])withSubstDTDEntities\s+(?:(?:[A-Z][\w']*\.)?no|False)(?![\w'])/.test(l)) return { ok: false, ran: true, detail: 'the line does not set withSubstDTDEntities to no' };
1464
+ const yes = (t) => (stripComments(t).match(/withSubstDTDEntities\s+(?:(?:[A-Z][\w']*\.)?yes|True)(?![\w'])/g) || []).length;
1465
+ if (yes(after) !== yes(before) - 1) return { ok: false, ran: true, detail: 'the patch did not turn off exactly one entity substitution' };
1466
+ return { ok: true, ran: true, detail: `line ${want.line} no longer substitutes DTD entities` };
1467
+ }
1468
+ if (want.kind === 'path-contain') {
1469
+ const re = new RegExp(`^${want.fn} ${want.param}\\n\\s+\\| "\\.\\." \`elem\` (?:[A-Z][\\w']*\\.)?splitDirectories ${want.param} \\|\\| (?:[A-Z][\\w']*\\.)?isAbsolute ${want.param} = ${want.fail.replace(/ /g, '\\s+')}\\s*$`, 'm');
1470
+ if (!re.test(stripComments(after))) return { ok: false, ran: true, detail: `no guard equation on ${want.fn} rejects a ".." segment or an absolute ${want.param}` };
1471
+ const eqs = (t) => stripComments(t).split('\n').filter((l) => new RegExp(`^${want.fn}(?![\\w'])`).test(l) && !new RegExp(`^${want.fn}\\s*::`).test(l)).length;
1472
+ if (eqs(after) !== eqs(before) + 1) return { ok: false, ran: true, detail: 'the patch did not add exactly one equation' };
1473
+ return { ok: true, ran: true, detail: `${want.fn} answers ${want.fail} for a ".." segment or an absolute ${want.param} before its body runs` };
1474
+ }
1475
+ if (want.kind === 'random-line') {
1476
+ const d = (stripComments(after).split('\n')[want.line - 1] || '');
1477
+ if (/randomRIO|randomRs|newStdGen|randomIO|System\.Random/.test(d)) return { ok: false, ran: true, detail: 'the line still calls System.Random' };
1478
+ if (!new RegExp(`getEntropy\\s+${want.bytes}(?![\\d])`).test(d)) return { ok: false, ran: true, detail: `the line does not read ${want.bytes} entropy byte(s)` };
1479
+ return { ok: true, ran: true, detail: `line ${want.line} reads ${want.bytes} byte(s) from getEntropy and no longer calls System.Random` };
1480
+ }
1230
1481
  if (want.kind === 'scotty-body-limit') {
1231
1482
  const count = (t, re) => (stripComments(t).match(re) || []).length;
1232
1483
  const limits = (t) => count(t, /(?<![\w'])(?:[A-Z][\w']*\.)?setMaxRequestBodySize(?![\w'])/g);