@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.
- package/CHANGELOG.md +35 -0
- package/dist/{4fe5c024ca0626112826.json → 2f0e6869229198301b40.json} +3 -3
- package/dist/agentic-security.mjs +9 -9
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/language/CLAUDE.md +3 -0
- package/src/language/haskell-fix.js +271 -20
- package/src/language/haskell-ir.js +8 -1
- package/src/language/haskell-security-rules.js +41 -2
- package/src/language/haskell-syntax.js +3 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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.
|
|
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",
|
package/src/language/CLAUDE.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 (
|
|
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);
|