@clear-capabilities/agentic-security-scanner 0.173.0 → 0.175.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- eab94f59f539296c01e17e2d914f419e131e2b4d1234865c011f755f11a64441 agentic-security.mjs
1
+ 9b99aab752e33a23af53d8562691f3e713d9e00c02bce5ce1d26edd1513479de agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.173.0",
3
+ "version": "0.175.0",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -84,7 +84,7 @@
84
84
  "test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
85
85
  "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
86
86
  "test:glob": "node --test test/glob-compat.test.js",
87
- "test:sast": "node --test 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/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",
87
+ "test:sast": "node --test 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
88
  "test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/proof-fabric/core.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/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/cli/artifact-provenance-cli.test.js",
89
89
  "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",
90
90
  "test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js test/capabilities/tools.test.js",
@@ -92,8 +92,8 @@
92
92
  "test:lineage": "node --test 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/cli/runtime-twin-cli.test.js",
93
93
  "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",
94
94
  "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",
95
- "test:haskell": "node --test test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js",
96
- "test:nix": "node --test 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",
95
+ "test:haskell": "node --test test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js",
96
+ "test:nix": "node --test 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",
97
97
  "test:language": "node --test test/language/capability-ledger.test.js test/language/language-accuracy-regression.test.js test/language/language-aibom.test.js test/language/language-assurance.test.js test/language/language-bom.test.js test/language/language-compliance.test.js test/language/language-contracts.test.js test/language/language-corpora-integrity.test.js test/language/language-discovery.test.js test/language/language-doc-coverage.test.js test/language/language-evidence-posture.test.js test/language/language-explorer.test.js test/language/language-gap-fixes.test.js test/language/language-gap-fixes-2.test.js test/language/language-governance.test.js test/language/language-integrations.test.js test/language/language-llm-agent.test.js test/language/language-local-ai.test.js test/language/language-polyglot-bridges.test.js test/language/language-privacy-lineage.test.js test/language/language-proof-remediation.test.js test/language/language-report-formats.test.js test/language/language-secrets.test.js test/language/language-trust-boundary.test.js",
98
98
  "test:language-stress": "node --test --test-concurrency=1 test/language/language-stress-offline.test.js",
99
99
  "test:language-tools": "node --test test/haskell/haskell-web-auth.test.js test/haskell/haskell-support-gate.test.js",
@@ -1423,13 +1423,18 @@ function _sinkFindingsForCall(calleeExpr, argExprs, cat, argTaints, state, callC
1423
1423
  // real-world nesting. Deliberately does NOT re-check the TOP-LEVEL call
1424
1424
  // itself (the caller already did that via its own _matchCallCatalog +
1425
1425
  // _sinkFindingsForCall), so this never duplicates a finding.
1426
+ // Haskell only: a call written on its own line inside a larger expression (`get "/r" $ do ... liftIO (callCommand c)`) carries that line, while the node that
1427
+ // finally executes it sits on the line the handler starts. The sink is where the call is written. Other IRs lower nested calls differently and keep the node's line.
1428
+ const _sinkLine = (call, fallback) => (/\.(?:hs|lhs)$/i.test(_currentFile || '') && call && Number.isInteger(call.line) && call.line >= 1 ? call.line : fallback);
1429
+
1426
1430
  function _nestedSinkFindings(argExprs, state, callContext, line) {
1427
1431
  const findings = [];
1428
1432
  for (const arg of (argExprs || [])) {
1429
1433
  if (!arg || arg.kind !== 'call') continue;
1430
1434
  const { cat, argTaints } = _matchCallCatalog(arg.callee, arg.args, state, callContext);
1431
- findings.push(..._sinkFindingsForCall(arg.callee, arg.args, cat, argTaints, state, callContext, line).findings);
1432
- findings.push(..._nestedSinkFindings(arg.args, state, callContext, line));
1435
+ const at = _sinkLine(arg, line);
1436
+ findings.push(..._sinkFindingsForCall(arg.callee, arg.args, cat, argTaints, state, callContext, at).findings);
1437
+ findings.push(..._nestedSinkFindings(arg.args, state, callContext, at));
1433
1438
  }
1434
1439
  return findings;
1435
1440
  }
@@ -2023,9 +2028,10 @@ function step(node, stateIn, callContext) {
2023
2028
  // expression is directly a sink.
2024
2029
  if (node.value && node.value.kind === 'call') {
2025
2030
  const { cat, argTaints } = _matchCallCatalog(node.value.callee, node.value.args, state, callContext);
2031
+ const _at = _sinkLine(node.value, node.line);
2026
2032
  findings.push(..._sinkFindingsForCall(
2027
- node.value.callee, node.value.args, cat, argTaints, state, callContext, node.line, node.value.kwargs).findings);
2028
- findings.push(..._nestedSinkFindings(node.value.args, state, callContext, node.line));
2033
+ node.value.callee, node.value.args, cat, argTaints, state, callContext, _at, node.value.kwargs).findings);
2034
+ findings.push(..._nestedSinkFindings(node.value.args, state, callContext, _at));
2029
2035
  } else if (node.value && node.value.kind === 'union' && /\.(?:hs|lhs)$/i.test(_currentFile || '')) {
2030
2036
  // Haskell: `if c then A else B` and guards return a union of the branch ACTIONS, each of which
2031
2037
  // runs when its branch is taken, so every branch call is a sink site in its own right. (Scoped
@@ -15,6 +15,8 @@ export const HS_FAMILY_BY_CWE = Object.freeze({
15
15
  'CWE-89': 'sql-injection',
16
16
  'CWE-918': 'ssrf',
17
17
  'CWE-79': 'xss',
18
+ 'CWE-601': 'open-redirect',
19
+ 'CWE-94': 'code-injection',
18
20
  });
19
21
 
20
22
  export const isHaskellFile = (file) => typeof file === 'string' && HS_FILE.test(file);
@@ -26,7 +26,7 @@ const IS_MEMBER = /^Data\.(?:Set|HashSet|Map|Map\.Strict|HashMap\.Strict)\.membe
26
26
  const NOT_MEMBER = /^Data\.(?:Set|HashSet|Map|Map\.Strict|HashMap\.Strict)\.notMember$/;
27
27
  const CHARCLASS = new Set(['Data.Char.isAlphaNum', 'Data.Char.isAlpha', 'Data.Char.isDigit', 'Data.Char.isAscii', 'isAlphaNum', 'isAlpha', 'isDigit']);
28
28
 
29
- const FAMILY_BY_CWE = { 'CWE-22': 'path', 'CWE-918': 'url', 'CWE-78': 'cmd', 'CWE-88': 'cmd', 'CWE-89': 'sql', 'CWE-79': 'xss', 'CWE-1427': 'prompt' };
29
+ const FAMILY_BY_CWE = { 'CWE-22': 'path', 'CWE-918': 'url', 'CWE-78': 'cmd', 'CWE-88': 'cmd', 'CWE-89': 'sql', 'CWE-79': 'xss', 'CWE-601': 'redirect', 'CWE-94': 'code', 'CWE-1427': 'prompt' };
30
30
 
31
31
  const isStr = (e) => e && e.kind === 'literal' && typeof e.value === 'string';
32
32
 
@@ -761,7 +761,9 @@ class FnLowerer {
761
761
  // ── statements / control flow ──────────────────────────────────────────────
762
762
  addCallNode(expr, ln) {
763
763
  if (!expr) return;
764
- if (expr.kind === 'call') this.cfg.add('call', ln, { callee: expr.callee, args: expr.args, hs: expr.hs });
764
+ // The call expression carries the line it was written on. The context that finally executes it (the last statement of a `do` handed to `get "/r" $`) is on an
765
+ // earlier line, and reporting that line would place the finding on the start of the whole handler instead of on the sink.
766
+ if (expr.kind === 'call') this.cfg.add('call', Number.isInteger(expr.line) && expr.line >= 1 ? expr.line : ln, { callee: expr.callee, args: expr.args, hs: expr.hs });
765
767
  else if (expr.kind !== 'literal' && expr.kind !== 'ident') this.cfg.add('assign', ln, { target: `$e${this.tmp++}`, source: expr, hs: { demand: 'executed' } });
766
768
  }
767
769
 
@@ -49,6 +49,28 @@ export const HS_PACKAGES = Object.freeze({
49
49
  random: { modules: ['System.Random'], tested: ['1.2'] },
50
50
  hxt: { modules: ['Text.XML.HXT.Core'], tested: ['9.3'] },
51
51
  bytestring: { modules: ['Data.ByteString', 'Data.ByteString.Char8', 'Data.ByteString.Lazy'], tested: ['0.11', '0.12'] },
52
+ // Written from each library's documented API and checked only against the development cases in test/haskell/haskell-models-extended.test.js, which are
53
+ // plain source scans: no GHC build against these packages was run, so `tested` is empty and `modelStatus` reports every version as untested.
54
+ process_legacy: { modules: ['System.Cmd'], tested: [] },
55
+ turtle: { modules: ['Turtle'], tested: [] },
56
+ shelly: { modules: ['Shelly', 'Shelly.Lifted'], tested: [] },
57
+ hint: { modules: ['Language.Haskell.Interpreter'], tested: [] },
58
+ HDBC: { modules: ['Database.HDBC'], tested: [] },
59
+ 'direct-sqlite': { modules: ['Database.SQLite3'], tested: [] },
60
+ 'postgresql-libpq': { modules: ['Database.PostgreSQL.LibPQ'], tested: [] },
61
+ 'persistent-backends': { modules: ['Database.Persist.Postgresql', 'Database.Persist.Sqlite', 'Database.Persist.MySQL', 'Database.Persist'], tested: [] },
62
+ req: { modules: ['Network.HTTP.Req'], tested: [] },
63
+ HTTP: { modules: ['Network.HTTP'], tested: [] },
64
+ snap: { modules: ['Snap.Core', 'Snap'], tested: [] },
65
+ Spock: { modules: ['Web.Spock', 'Web.Spock.Core'], tested: [] },
66
+ happstack: { modules: ['Happstack.Server', 'Happstack.Server.Internal.Types'], tested: [] },
67
+ network: { modules: ['Network.Socket.ByteString', 'Network.Socket.ByteString.Lazy'], tested: [] },
68
+ unix: { modules: ['System.Posix.Files'], tested: [] },
69
+ jwt: { modules: ['Web.JWT'], tested: [] },
70
+ 'servant-auth-server': { modules: ['Servant.Auth.Server'], tested: [] },
71
+ 'clientsession': { modules: ['Web.ClientSession'], tested: [] },
72
+ connection: { modules: ['Network.Connection'], tested: [] },
73
+ HsOpenSSL: { modules: ['OpenSSL.Session'], tested: [] },
52
74
  });
53
75
 
54
76
  // ── sources ─────────────────────────────────────────────────────────────────
@@ -85,6 +107,30 @@ src('Yesod', ['lookupGetParam', 'lookupGetParams', 'lookupPostParam', 'lookupPos
85
107
  // HTTP responses are untrusted data too when they come back from a remote service the app calls
86
108
  src('Network.HTTP.Simple', ['getResponseBody'], 'network', 'remote HTTP response', { package: 'http-conduit' });
87
109
  src('Network.HTTP.Client', ['responseBody'], 'network', 'remote HTTP response', { package: 'http-client' });
110
+ // More file readers, and more web frameworks. Each reads a value the client (or the file's author) controls.
111
+ src('Data.ByteString.Char8', ['readFile'], 'file-read', 'file content', { package: 'bytestring' });
112
+ src('Data.ByteString.Lazy.Char8', ['readFile'], 'file-read', 'file content', { package: 'bytestring' });
113
+ src('Data.Text.Lazy.IO', ['readFile'], 'file-read', 'file content', { package: 'text' });
114
+ src('Network.Socket.ByteString', ['recv', 'recvFrom'], 'network', 'bytes read from a socket', { package: 'network' });
115
+ src('Network.Socket.ByteString.Lazy', ['recv', 'getContents'], 'network', 'bytes read from a socket', { package: 'network' });
116
+ src('Network.HTTP', ['getResponseBody'], 'network', 'remote HTTP response', { package: 'HTTP' });
117
+ // Snap
118
+ src('Snap.Core', ['getParam', 'getPostParam', 'getQueryParam', 'getParams', 'getPostParams', 'getQueryParams'], 'url-param', 'snap request parameter', { package: 'snap', framework: 'snap' });
119
+ src('Snap.Core', ['readRequestBody', 'getRequestBody'], 'http-body', 'snap request body', { package: 'snap', framework: 'snap' });
120
+ src('Snap', ['getParam', 'getPostParam', 'getQueryParam', 'getParams', 'getPostParams', 'getQueryParams'], 'url-param', 'snap request parameter', { package: 'snap', framework: 'snap' });
121
+ // Spock
122
+ src('Web.Spock', ['param', "param'", 'params'], 'url-param', 'spock request parameter', { package: 'Spock', framework: 'spock' });
123
+ src('Web.Spock', ['header', 'cookie', 'cookies'], 'header', 'spock request header or cookie', { package: 'Spock', framework: 'spock' });
124
+ src('Web.Spock', ['body', 'jsonBody', "jsonBody'", 'files'], 'http-body', 'spock request body', { package: 'Spock', framework: 'spock' });
125
+ src('Web.Spock.Core', ['param', "param'", 'params'], 'url-param', 'spock request parameter', { package: 'Spock', framework: 'spock' });
126
+ // Happstack
127
+ src('Happstack.Server', ['look', 'lookText', "lookText'", 'lookBS', 'lookRead', 'lookPairs', 'lookPairsBS', 'lookCookieValue'], 'url-param', 'happstack request input', { package: 'happstack', framework: 'happstack' });
128
+ // Yesod: the whole request value and its body
129
+ src('Yesod.Core', ['getRequest', 'rawRequestBody', 'runRequestBody', 'lookupFile', 'lookupFiles'], 'http-body', 'yesod request', { package: 'yesod', framework: 'yesod' });
130
+ src('Yesod', ['getRequest', 'rawRequestBody', 'runRequestBody', 'lookupFile', 'lookupFiles'], 'http-body', 'yesod request', { package: 'yesod', framework: 'yesod' });
131
+ // Scotty: the underlying WAI request
132
+ src('Web.Scotty', ['request'], 'http-body', 'scotty WAI request', { package: 'scotty', framework: 'scotty' });
133
+ src('Web.Scotty.Trans', ['request'], 'http-body', 'scotty WAI request', { package: 'scotty', framework: 'scotty' });
88
134
 
89
135
  // ── sinks ───────────────────────────────────────────────────────────────────
90
136
  // family: the taint family the existing engine already uses (cmd | sql | path | url | xss)
@@ -161,6 +207,52 @@ sink('Prelude', ['putStrLn', 'putStr'], XSS_TEXT);
161
207
  sink('System.IO', ['putStrLn', 'putStr'], XSS_TEXT);
162
208
  sink('Data.Text.IO', ['putStrLn', 'putStr'], { ...XSS_TEXT, package: 'text' });
163
209
 
210
+ // ── more sinks (written from the libraries' documented behaviour) ───────────────────────────────────────────────────────
211
+ // CWE-78. The legacy System.Cmd module, Turtle and Shelly. A shell string is parsed by /bin/sh; a program name is not, but a tainted one is arbitrary execution.
212
+ sink('System.Cmd', ['system'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'process_legacy', vuln: 'OS Command Injection (shell string)', remediation: 'Do not build a shell command from input. Use `proc`/`callProcess` with a fixed program and pass each value as its own argument.' });
213
+ sink('System.Cmd', ['rawSystem'], { family: 'cmd', cwe: 'CWE-78', severity: 'high', argIndex: 0, shell: false, package: 'process_legacy', vuln: 'OS Command Injection (program name)', remediation: 'Use a fixed program name; never take it from input.' });
214
+ sink('System.Cmd', ['rawSystem'], { family: 'cmd', cwe: 'CWE-88', severity: 'medium', argIndex: 1, shell: false, argv: true, package: 'process_legacy', vuln: 'Argument Injection (process arguments)', remediation: 'Validate each argument against an allow-list, and put `--` before values that could start with a dash.' });
215
+ sink('Turtle', ['shell', 'inshell', 'shells', 'inshells'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'turtle', vuln: 'OS Command Injection (Turtle shell command)', remediation: 'Use `proc`/`procs` with a fixed program and a list of arguments, not a shell string built from input.' });
216
+ sink('Turtle', ['proc', 'procs', 'inproc', 'inprocs'], { family: 'cmd', cwe: 'CWE-78', severity: 'high', argIndex: 0, shell: false, package: 'turtle', vuln: 'OS Command Injection (program name)', remediation: 'Use a fixed program name; never take it from input.' });
217
+ sink('Turtle', ['proc', 'procs', 'inproc', 'inprocs'], { family: 'cmd', cwe: 'CWE-88', severity: 'medium', argIndex: 1, shell: false, argv: true, package: 'turtle', vuln: 'Argument Injection (process arguments)', remediation: 'Validate each argument against an allow-list, and put `--` before values that could start with a dash.' });
218
+ for (const mod of ['Shelly', 'Shelly.Lifted']) {
219
+ sink(mod, ['run', 'run_', 'runFoldLines', 'command', 'command_'], { family: 'cmd', cwe: 'CWE-78', severity: 'high', argIndex: 0, shell: false, package: 'shelly', vuln: 'OS Command Injection (program name)', remediation: 'Use a fixed program name; never take it from input.' });
220
+ sink(mod, ['run', 'run_', 'runFoldLines'], { family: 'cmd', cwe: 'CWE-88', severity: 'medium', argIndex: 1, shell: false, argv: true, package: 'shelly', vuln: 'Argument Injection (process arguments)', remediation: 'Validate each argument against an allow-list, and put `--` before values that could start with a dash.' });
221
+ sink(mod, ['bash', 'bash_', 'bashPipeFail'], { family: 'cmd', cwe: 'CWE-78', severity: 'critical', argIndex: 0, shell: true, package: 'shelly', vuln: 'OS Command Injection (bash script text)', remediation: 'Do not build a script from input. Call the program directly with `run` and a list of arguments.' });
222
+ }
223
+
224
+ // CWE-94. Evaluating Haskell source text at run time (the hint package) runs whatever code the text holds.
225
+ sink('Language.Haskell.Interpreter', ['interpret', 'eval', 'runStmt'], { family: 'code', cwe: 'CWE-94', severity: 'critical', argIndex: 0, package: 'hint', vuln: 'Code Injection (Haskell source evaluated at run time)', remediation: 'Do not evaluate text that came from a user. Parse the input into a closed data type and interpret that instead.' });
226
+
227
+ // CWE-89. HDBC, direct-sqlite, postgresql-libpq, and the persistent backends that re-export rawSql. The query TEXT is the sink; a `?` placeholder with bound values is the safe form.
228
+ sink('Database.HDBC', ['run', 'quickQuery', "quickQuery'", 'prepare', 'runRaw'], { ...SQL, argIndex: 1, package: 'HDBC', vuln: 'SQL Injection (HDBC query text)' });
229
+ sink('Database.SQLite3', ['exec', 'prepare'], { ...SQL, argIndex: 1, package: 'direct-sqlite', vuln: 'SQL Injection (SQLite statement text)' });
230
+ sink('Database.PostgreSQL.LibPQ', ['exec', 'execParams', 'prepare'], { ...SQL, argIndex: 1, package: 'postgresql-libpq', vuln: 'SQL Injection (libpq command text)' });
231
+ for (const mod of ['Database.Persist.Postgresql', 'Database.Persist.Sqlite', 'Database.Persist.MySQL']) sink(mod, ['rawSql', 'rawExecute', 'rawQuery'], { ...SQL, argIndex: 0, package: 'persistent-backends', vuln: 'SQL Injection (raw SQL text)' });
232
+
233
+ // CWE-918. Req builds a URL from typed pieces, so the HOST is the attacker-steered part; HTTP's request constructors take the URL text.
234
+ sink('Network.HTTP.Req', ['https', 'http'], { ...SSRF, argIndex: 0, package: 'req', vuln: 'Server-Side Request Forgery (request host)' });
235
+ sink('Network.HTTP', ['getRequest', 'postRequest', 'headRequest', 'simpleHTTP'], { ...SSRF, argIndex: 0, package: 'HTTP' });
236
+
237
+ // CWE-601. Redirecting to a location taken from the request sends the user to a site the attacker names.
238
+ const REDIRECT = { family: 'redirect', cwe: 'CWE-601', severity: 'medium', argIndex: 0, vuln: 'Open Redirect', remediation: 'Redirect only to a fixed path or to a value found in an allow-list; never to a request value as given.' };
239
+ sink('Web.Scotty', ['redirect'], { ...REDIRECT, package: 'scotty', framework: 'scotty' });
240
+ sink('Web.Scotty.Trans', ['redirect'], { ...REDIRECT, package: 'scotty', framework: 'scotty' });
241
+ sink('Yesod.Core', ['redirect'], { ...REDIRECT, package: 'yesod', framework: 'yesod' });
242
+ sink('Yesod.Core', ['redirectWith'], { ...REDIRECT, argIndex: 1, package: 'yesod', framework: 'yesod' });
243
+ sink('Yesod', ['redirect'], { ...REDIRECT, package: 'yesod', framework: 'yesod' });
244
+ sink('Snap.Core', ['redirect', "redirect'"], { ...REDIRECT, package: 'snap', framework: 'snap' });
245
+ sink('Snap', ['redirect', "redirect'"], { ...REDIRECT, package: 'snap', framework: 'snap' });
246
+ sink('Web.Spock', ['redirect'], { ...REDIRECT, package: 'Spock', framework: 'spock' });
247
+ sink('Happstack.Server', ['seeOther', 'tempRedirect', 'movedPermanently'], { ...REDIRECT, package: 'happstack', framework: 'happstack' });
248
+
249
+ // CWE-22. The same path operations through the other string types, and the POSIX file calls.
250
+ sink('Data.ByteString.Char8', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'bytestring', vuln: 'Path Traversal', remediation: 'Confirm the canonical path stays under an allowed directory before opening it.' });
251
+ sink('Data.ByteString.Lazy.Char8', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'bytestring', vuln: 'Path Traversal', remediation: 'Confirm the canonical path stays under an allowed directory before opening it.' });
252
+ sink('Data.Text.Lazy.IO', ['readFile', 'writeFile', 'appendFile'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'text', vuln: 'Path Traversal', remediation: 'Confirm the canonical path stays under an allowed directory before opening it.' });
253
+ sink('System.Posix.Files', ['removeLink', 'createSymbolicLink', 'setFileMode', 'setOwnerAndGroup', 'readSymbolicLink'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 0, package: 'unix', vuln: 'Path Traversal (file operation)', remediation: 'Confirm the canonical path stays under an allowed directory before the operation.' });
254
+ sink('System.Posix.Files', ['rename'], { family: 'path', cwe: 'CWE-22', severity: 'high', argIndex: 'all', package: 'unix', vuln: 'Path Traversal (rename)', remediation: 'Confirm both canonical paths stay under an allowed directory.' });
255
+
164
256
  // ── sanitizers (context-specific; `appliesTo` is the family each one actually neutralises) ──
165
257
  const SAN = [];
166
258
  const san = (module, names, appliesTo, note, extra = {}) => { for (const e of E(module, names)) SAN.push({ ...e, appliesTo, note, ...extra }); };
@@ -219,6 +311,8 @@ export const HS_RULE_APIS = Object.freeze([
219
311
  ...E('Data.Text.IO', ['getContents', 'hGetContents']), ...E('Data.Text.Lazy.IO', ['getContents', 'hGetContents']),
220
312
  ...E('Data.Text', ['replicate', 'take']), ...E('Data.Vector', ['replicate']),
221
313
  ...E('Web.Cookie', ['defaultSetCookie']),
314
+ ...E('Web.JWT', ['hmacSecret', 'secret']), ...E('Servant.Auth.Server', ['fromSecret']), ...E('Web.ClientSession', ['initKey']),
315
+ ...E('Network.HTTP.Client.TLS', ['mkManagerSettings']), ...E('OpenSSL.Session', ['contextSetVerificationMode']),
222
316
  ...E('Text.XML.HXT.Core', ['withSubstDTDEntities']),
223
317
  ...E('System.IO.Unsafe', ['unsafePerformIO', 'unsafeInterleaveIO', 'unsafeDupablePerformIO']),
224
318
  ...E('Unsafe.Coerce', ['unsafeCoerce']),
@@ -33,6 +33,8 @@ export const RULES = Object.freeze({
33
33
  'hs-unbounded-input-read': { cwe: 'CWE-770', severity: 'medium', family: 'resource-exhaustion', vuln: 'Whole input read into memory without a size limit', remediation: 'Bound the read (`BL.take n`, `hGetSome`, a request-size limit) before the data is consumed.' },
34
34
  'hs-unbounded-allocation': { cwe: 'CWE-770', severity: 'medium', family: 'resource-exhaustion', vuln: 'Allocation sized by parsed caller text without an upper bound', remediation: 'Clamp the size (`min limit n`) before allocating.' },
35
35
  'hs-partial-function-on-input': { cwe: 'CWE-248', severity: 'low', family: 'unhandled-exception', vuln: 'Partial function applied to caller-supplied text', remediation: 'Use the total variant (`readMaybe`, `listToMaybe`, pattern matching) and handle the failure.' },
36
+ 'hs-tls-validation-disabled': { cwe: 'CWE-295', severity: 'high', family: 'tls-validation', vuln: 'TLS certificate validation is disabled', remediation: 'Do not disable certificate validation. Use the default settings (`tlsManagerSettings`, `TLSSettingsSimple False False False`, `VerifyPeer`) and, for a private certificate authority, add that authority to the trust store instead.' },
37
+ 'hs-hardcoded-secret': { cwe: 'CWE-798', severity: 'high', family: 'hardcoded-credentials', vuln: 'Signing or encryption secret written in the source', remediation: 'Read the secret from the environment or a secret store at start-up, and rotate the one that was committed: it is now in the repository history.' },
36
38
  'hs-unsafe-io-boundary': { cwe: 'CWE-676', severity: 'low', family: 'unsafe-boundary', vuln: 'Unsafe IO / coercion boundary', remediation: 'Confine `unsafePerformIO`/`unsafeCoerce` to audited, well-tested wrappers.' },
37
39
  });
38
40
 
@@ -44,7 +46,7 @@ export const BOUNDARIES = Object.freeze({
44
46
  unsafeCoerce: 'Reinterprets a value as another type without a check. Modelled as a boundary only.',
45
47
  });
46
48
 
47
- const WEAK_HASH_CALLS = new Set(['Crypto.Hash.MD5.hash', 'Crypto.Hash.MD5.hashlazy', 'Crypto.Hash.SHA1.hash', 'Crypto.Hash.SHA1.hashlazy', 'Data.Digest.Pure.MD5.md5', 'Data.Digest.Pure.SHA.sha1']);
49
+ const WEAK_HASH_CALLS = new Set(['Crypto.Hash.MD5.hash', 'Crypto.Hash.MD5.hashlazy', 'Crypto.Hash.SHA1.hash', 'Crypto.Hash.SHA1.hashlazy', 'Data.Digest.Pure.MD5.md5', 'Data.Digest.Pure.MD5.md5DigestBytes', 'Data.Digest.Pure.SHA.sha1', 'Data.Digest.Pure.SHA.sha1Incremental']);
48
50
  const WEAK_ALGO_CONS = new Set(['MD5', 'MD4', 'MD2', 'SHA1']);
49
51
  const ANY_HASH_CALLS = new Set([...WEAK_HASH_CALLS, 'Crypto.Hash.hash', 'Crypto.Hash.hashlazy', 'Crypto.Hash.hashWith', 'Crypto.Hash.SHA256.hash', 'Crypto.Hash.SHA256.hashlazy', 'Crypto.Hash.SHA512.hash', 'Crypto.Hash.SHA512.hashlazy', 'Data.Digest.Pure.SHA.sha256', 'Data.Digest.Pure.SHA.sha512']);
50
52
  const WEAK_CIPHER_MODULES = new Set(['Crypto.Cipher.DES', 'Crypto.Cipher.RC4', 'Crypto.Cipher.RC2', 'Crypto.Cipher.Blowfish']);
@@ -56,6 +58,11 @@ const PBKDF2_CALLS = new Set(['Crypto.KDF.PBKDF2.generate', 'Crypto.KDF.PBKDF2.f
56
58
  const STRONG_KDF = /^(?:Crypto\.KDF\.BCrypt\.|Crypto\.Argon2\.|Crypto\.Scrypt\.|Crypto\.KDF\.Scrypt\.|Crypto\.PasswordStore\.|Crypto\.BCrypt\.)/;
57
59
  const UNSAFE_CALLS = { 'System.IO.Unsafe.unsafePerformIO': 'unsafePerformIO', 'System.IO.Unsafe.unsafeInterleaveIO': 'unsafeInterleaveIO', 'System.IO.Unsafe.unsafeDupablePerformIO': 'unsafeDupablePerformIO', 'Unsafe.Coerce.unsafeCoerce': 'unsafeCoerce' };
58
60
 
61
+ // TLS: the first field of TLSSettingsSimple (Network.Connection) is settingDisableCertificateValidation, and `VerifyNone` (OpenSSL.Session) skips peer verification.
62
+ // The IR keeps a constructor's name in `hs.con` but not the module it came from, so a constructor counts only in a file that imports the module defining it.
63
+ // Calls whose argument is a key or signing secret.
64
+ const SECRET_KEY_CALLS = new Set(['Web.JWT.hmacSecret', 'Web.JWT.secret', 'Servant.Auth.Server.fromSecret', 'Web.ClientSession.initKey']);
65
+ const isTrueLit = (e) => !!e && ((e.kind === 'literal' && /^true$/i.test(String(e.value))) || (e.kind === 'ident' && /(?:^|\.)True$/.test(e.name || '')));
59
66
  const TIME_SOURCES = new Set(['Data.Time.Clock.POSIX.getPOSIXTime', 'Data.Time.Clock.getCurrentTime', 'System.CPUTime.getCPUTime']);
60
67
  const SECURITY_FN_SUBSTR = /(?:token|nonce|secret|otp|csrf|session|salt|apikey|resetcode|passcode|verification)/i;
61
68
  // Short credential words that would over-match as substrings (`spin`, `mapping`, `keyboard`): matched as whole camelCase / snake_case words.
@@ -215,6 +222,16 @@ export function analyzeHaskellRules(files, opts = {}) {
215
222
  }
216
223
  continue;
217
224
  }
225
+ // TLS certificate validation switched off, by constructor
226
+ if (e.hs && e.hs.con) {
227
+ const importsFrom = (mods) => (fir.imports || []).some((im) => mods.includes(im.module));
228
+ if (e.hs.con === 'TLSSettingsSimple' && importsFrom(['Network.Connection', 'Network.Connection.Types'])) {
229
+ const positional = e.kind === 'array' && isTrueLit(e.elements[0]);
230
+ const named = e.kind === 'object' && (e.props || []).some((p) => p && p.key === 'settingDisableCertificateValidation' && isTrueLit(p.value));
231
+ if (positional || named) emit('hs-tls-validation-disabled', file, e.line || node.line, { detail: 'TLSSettingsSimple with settingDisableCertificateValidation = True', evidence: { setting: 'settingDisableCertificateValidation' } });
232
+ }
233
+ if (e.hs.con === 'VerifyNone' && e.kind === 'literal' && importsFrom(['OpenSSL.Session'])) emit('hs-tls-validation-disabled', file, e.line || node.line, { detail: 'OpenSSL verification mode VerifyNone', evidence: { mode: 'VerifyNone' } });
234
+ }
218
235
  // `xs !! n` raises on a short list: a partial function, written as an operator, applied to caller-supplied text
219
236
  if (e.kind === 'binary' && e.op === '!!' && e.left && mentionsText(e.left)) {
220
237
  emit('hs-partial-function-on-input', file, e.line || node.line, { detail: '!! on a caller-supplied list', evidence: { partial: '!!' } });
@@ -225,6 +242,9 @@ export function analyzeHaskellRules(files, opts = {}) {
225
242
  const line = e.line || node.line;
226
243
  const args = e.args || [];
227
244
 
245
+ // TLS certificate validation switched off
246
+ // a signing/encryption secret that is a constant in the source
247
+ if (SECRET_KEY_CALLS.has(c) && args[0] && isLiteralLike(args[0]) && !(args[0].kind === 'literal' && String(args[0].value).length < 8)) emit('hs-hardcoded-secret', file, line, { detail: `${c} given a constant`, confidence: 0.85, evidence: { call: c } });
228
248
  // weak hash
229
249
  if (WEAK_HASH_CALLS.has(c) || (c === 'Crypto.Hash.hashWith' && args[0] && args[0].kind === 'literal' && WEAK_ALGO_CONS.has(args[0].value))) {
230
250
  const pw = args.some((a) => [...names(a)].some((n) => PASSWORD_VAR.test(n)));
@@ -54,6 +54,17 @@ export const HARDENING_RULES = Object.freeze({
54
54
  'tls-not-enforced': { family: 'tls-runtime', cwe: 'CWE-319', severity: 'medium', ruleVersion: 1, vuln: 'Virtual host explicitly serves cleartext HTTP (forceSSL = false)', why: 'forceSSL = false is a deliberate choice to keep the plain-HTTP listener open for the whole host, so credentials and session data cross the network unencrypted.', fix: 'Set `forceSSL = true` (with `enableACME = true` or a certificate) so HTTP is redirected to HTTPS.' },
55
55
  'container-privileged': { family: 'container-declaration', cwe: 'CWE-250', severity: 'high', ruleVersion: 1, vuln: 'OCI container is declared privileged or with a dangerous mount', why: '--privileged, SYS_ADMIN/ALL capabilities and the container runtime socket give the container control of the host. Image layers are NOT scanned.', fix: 'Remove --privileged and the runtime socket mount; add only the specific capability the workload needs.' },
56
56
  'container-host-network': { family: 'container-declaration', cwe: 'CWE-668', severity: 'medium', ruleVersion: 1, vuln: 'OCI container shares the host network namespace', why: 'Host networking removes network isolation, so the container can reach and bind host-local services. Image layers are NOT scanned.', fix: 'Use the default bridge and publish only the needed ports on a specific address.' },
57
+ 'user-root-equivalent-group': { family: 'privilege-escalation', cwe: 'CWE-269', severity: 'medium', ruleVersion: 1, vuln: 'A user is a member of a group that is equivalent to root', why: 'Membership of docker or lxd lets the user start a container with the host filesystem mounted, and membership of disk gives raw read and write access to the block devices. Each is root on the machine, without sudo and without a password prompt.', fix: 'Remove the group. Use rootless containers (virtualisation.docker.rootless, podman) or run the tool through sudo with a narrow rule.' },
58
+ 'user-uid-zero': { family: 'privilege-escalation', cwe: 'CWE-250', severity: 'high', ruleVersion: 1, vuln: 'A user other than root has user id 0', why: 'User id 0 is root: the kernel does not look at the name. A second account with uid 0 is a root login that audit logs, sudo rules and per-user controls do not treat as root.', fix: 'Remove the uid assignment so the account gets a normal id, or use sudo for the cases that need root.' },
59
+ 'getty-autologin': { family: 'login-access', cwe: 'CWE-306', severity: 'high', ruleVersion: 1, vuln: 'The console logs a user in without a password', why: 'services.getty.autologinUser starts a session on every virtual console with no authentication. For root that is a root shell for anyone with the keyboard, a serial line or a hypervisor console.', fix: 'Remove services.getty.autologinUser. Keep autologin to throwaway virtual machines and tests.' },
60
+ 'display-autologin': { family: 'login-access', cwe: 'CWE-306', severity: 'low', ruleVersion: 1, vuln: 'The display manager logs a user in without a password', why: 'Automatic login skips authentication at the graphical login, so anyone who can power the machine on gets the session. Disk encryption is the only remaining barrier.', fix: 'Disable autoLogin, or keep it only where physical access is controlled and the disk is encrypted.' },
61
+ 'pam-null-password': { family: 'login-access', cwe: 'CWE-258', severity: 'high', ruleVersion: 1, vuln: 'A PAM service accepts accounts with an empty password', why: 'allowNullPassword lets an account with no password log in through this service with an empty answer, which turns every passwordless account into an open login.', fix: 'Remove allowNullPassword (it defaults to false) and give every account a password hash or a key.' },
62
+ 'nix-sandbox-off': { family: 'nix-daemon-trust', cwe: 'CWE-693', severity: 'medium', ruleVersion: 1, vuln: 'The Nix build sandbox is disabled', why: 'With the sandbox off, a build can read the host filesystem and the network, so a malicious or impure derivation can steal files and secrets or change its output depending on the host.', fix: 'Remove nix.settings.sandbox = false (the default is true). Fix the one derivation that needs impurity instead of weakening every build.' },
63
+ 'nix-signatures-off': { family: 'nix-daemon-trust', cwe: 'CWE-347', severity: 'high', ruleVersion: 1, vuln: 'Nix accepts unsigned store paths', why: 'require-sigs = false lets the daemon import any store path from a client or substituter without a trusted signature, so whoever can push a path can run code as every user of that path.', fix: 'Remove nix.settings.require-sigs = false and sign the paths you copy with a key listed in trusted-public-keys.' },
64
+ 'nix-trusted-users-wildcard': { family: 'nix-daemon-trust', cwe: 'CWE-269', severity: 'high', ruleVersion: 1, vuln: 'Every user is a Nix trusted user', why: 'A trusted user can import unsigned paths and set daemon options, which is root on the machine. Listing "*" or a broad group extends that to everyone who can reach the daemon.', fix: 'Keep nix.settings.trusted-users to root (and named administrators). Give other users nix.settings.allowed-users instead.' },
65
+ 'kernel-sysctl-weakened': { family: 'kernel-hardening', cwe: 'CWE-693', severity: 'medium', ruleVersion: 1, vuln: 'A kernel hardening sysctl is set to a weaker value', why: 'Turning off address space randomisation, kernel pointer hiding or the unprivileged BPF restriction removes a mitigation that makes local exploits harder.', fix: 'Remove the boot.kernel.sysctl entry or restore the hardened value (randomize_va_space = 2, kptr_restrict = 1 or 2, unprivileged_bpf_disabled = 1).' },
66
+ 'nfs-no-root-squash': { family: 'network-filesystem', cwe: 'CWE-269', severity: 'high', ruleVersion: 1, vuln: 'NFS export keeps remote root as root', why: 'no_root_squash maps a client\'s root user to root on the exported files, so anyone with root on a client can plant setuid binaries on the server.', fix: 'Drop no_root_squash (root_squash is the default) and restrict the client list to specific hosts.' },
67
+ 'docker-tcp-no-tls': { family: 'container-declaration', cwe: 'CWE-306', severity: 'high', ruleVersion: 1, vuln: 'The Docker daemon listens on an unauthenticated TCP socket', why: 'A tcp:// host without TLS client verification gives anyone who can reach the port root on the machine, since the Docker API starts privileged containers.', fix: 'Remove the tcp:// host, or enable TLS with --tlsverify and a CA, and bind only to a private address.' },
57
68
  });
58
69
 
59
70
  /** Which rules explain each privileged entry-point kind (reconciliation join). */
@@ -368,6 +379,72 @@ export function analyzeNixosHardening(opts, extra = {}) {
368
379
  }
369
380
  }
370
381
 
382
+ // ── accounts and console login ──
383
+ // Root-equivalent groups: each of these gives the member control of the host without sudo.
384
+ const ROOT_EQUIVALENT_GROUPS = new Map([['docker', 'start a container with the host root mounted'], ['lxd', 'start a privileged container with the host root mounted'], ['disk', 'read and write the raw block devices']]);
385
+ for (const o of cfg.options.filter((x) => x.namespace === 'nixos' && /^users\.users\.[^.]+\.extraGroups$/.test(x.path))) {
386
+ const name = o.path.split('.')[2];
387
+ if (name === 'root') continue;
388
+ const v = view(note(o, 'accounts'));
389
+ const items = v.state === 'known' ? asList(v.value) : v.state === 'conditional' ? asList(v.definiteItems) : [];
390
+ const bad = items.filter((g) => typeof g === 'string' && ROOT_EQUIVALENT_GROUPS.has(g));
391
+ if (bad.length) addFinding('user-root-equivalent-group', { anchor: anchorOf(o), scope: `users.users.${name}`, state: v.state === 'conditional' ? 'conditional' : 'active', uncertain: !v.definite, evidence: [evidenceOf(o)], extra: { groups: bad, effect: bad.map((g) => `${g}: ${ROOT_EQUIVALENT_GROUPS.get(g)}`) } });
392
+ }
393
+ for (const o of cfg.options.filter((x) => x.namespace === 'nixos' && /^users\.users\.[^.]+\.uid$/.test(x.path))) {
394
+ const name = o.path.split('.')[2];
395
+ if (name === 'root') continue;
396
+ const j = judge(note(o, 'accounts'), (x) => x === 0);
397
+ if (j === 'yes' || j === 'conditional') addFinding('user-uid-zero', { anchor: anchorOf(o), scope: `users.users.${name}`, state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(o).definite, evidence: [evidenceOf(o)] });
398
+ }
399
+ const gettyRes = note(nixos('services.getty.autologinUser'), 'login');
400
+ const gettyV = view(gettyRes);
401
+ if (gettyV.state === 'known' && typeof gettyV.value === 'string' && gettyV.value) {
402
+ addFinding('getty-autologin', { severity: gettyV.value === 'root' ? 'high' : 'low', anchor: anchorOf(gettyRes), scope: 'services.getty.autologinUser', state: 'active', uncertain: !gettyV.definite, evidence: [evidenceOf(gettyRes)], extra: { user: gettyV.value } });
403
+ }
404
+ for (const base of ['services.displayManager.autoLogin', 'services.xserver.displayManager.autoLogin']) {
405
+ const en = note(nixos(`${base}.enable`), 'login');
406
+ const j = judge(en, (x) => x === true);
407
+ if (j === 'yes' || j === 'conditional') {
408
+ const userV = view(nixos(`${base}.user`));
409
+ addFinding('display-autologin', { severity: userV.state === 'known' && userV.value === 'root' ? 'high' : 'low', anchor: anchorOf(en), scope: base, state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(en).definite, evidence: [evidenceOf(en)], extra: userV.state === 'known' ? { user: userV.value } : {} });
410
+ }
411
+ }
412
+ for (const o of cfg.options.filter((x) => x.namespace === 'nixos' && /^security\.pam\.services\.[^.]+\.allowNullPassword$/.test(x.path))) {
413
+ const j = judge(note(o, 'login'), (x) => x === true);
414
+ if (j === 'yes' || j === 'conditional') addFinding('pam-null-password', { anchor: anchorOf(o), scope: o.path.replace(/\.allowNullPassword$/, ''), state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(o).definite, evidence: [evidenceOf(o)] });
415
+ }
416
+
417
+ // ── nix daemon trust, kernel sysctls, network filesystems, docker socket ──
418
+ const sandboxRes = note(nixos('nix.settings.sandbox'), 'nix');
419
+ { const j = judge(sandboxRes, (x) => x === false); if (j === 'yes' || j === 'conditional') addFinding('nix-sandbox-off', { anchor: anchorOf(sandboxRes), scope: 'nix.settings.sandbox', state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(sandboxRes).definite, evidence: [evidenceOf(sandboxRes)] }); }
420
+ const sigsRes = note(nixos('nix.settings.require-sigs'), 'nix');
421
+ { const j = judge(sigsRes, (x) => x === false); if (j === 'yes' || j === 'conditional') addFinding('nix-signatures-off', { anchor: anchorOf(sigsRes), scope: 'nix.settings.require-sigs', state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(sigsRes).definite, evidence: [evidenceOf(sigsRes)] }); }
422
+ const trustedRes = note(nixos('nix.settings.trusted-users'), 'nix');
423
+ { const v = view(trustedRes);
424
+ const items = v.state === 'known' ? asList(v.value) : v.state === 'conditional' ? asList(v.definiteItems) : [];
425
+ if (items.some((u) => u === '*' || u === '@users' || u === '@everyone')) addFinding('nix-trusted-users-wildcard', { anchor: anchorOf(trustedRes), scope: 'nix.settings.trusted-users', state: v.state === 'conditional' ? 'conditional' : 'active', uncertain: !v.definite, evidence: [evidenceOf(trustedRes)] }); }
426
+ const SYSCTL_WEAK = new Map([['kernel.randomize_va_space', (x) => String(x) === '0'], ['kernel.kptr_restrict', (x) => String(x) === '0'], ['kernel.unprivileged_bpf_disabled', (x) => String(x) === '0'], ['kernel.dmesg_restrict', (x) => String(x) === '0'], ['kernel.yama.ptrace_scope', (x) => String(x) === '0'], ['fs.protected_symlinks', (x) => String(x) === '0'], ['fs.protected_hardlinks', (x) => String(x) === '0']]);
427
+ for (const o of cfg.options.filter((x) => x.namespace === 'nixos' && /^boot\.kernel\.sysctl\./.test(x.path))) {
428
+ const key = o.path.slice('boot.kernel.sysctl.'.length).replace(/^"|"$/g, '');
429
+ const weak = SYSCTL_WEAK.get(key);
430
+ if (!weak) continue;
431
+ const j = judge(note(o, 'kernel'), weak);
432
+ if (j === 'yes' || j === 'conditional') addFinding('kernel-sysctl-weakened', { anchor: anchorOf(o), scope: `boot.kernel.sysctl.${key}`, state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(o).definite, evidence: [evidenceOf(o)], extra: { sysctl: key } });
433
+ }
434
+ const nfsRes = note(nixos('services.nfs.server.exports'), 'nfs');
435
+ { const j = judge(nfsRes, (x) => typeof x === 'string' && /(^|[\s,(])no_root_squash(?=[\s,)]|$)/.test(x.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n')));
436
+ if (j === 'yes' || j === 'conditional') addFinding('nfs-no-root-squash', { anchor: anchorOf(nfsRes), scope: 'services.nfs.server.exports', state: j === 'conditional' ? 'conditional' : 'active', uncertain: !view(nfsRes).definite, evidence: [evidenceOf(nfsRes)] }); }
437
+ { const dockerOn = judge(nixos('virtualisation.docker.enable'), (x) => x === true);
438
+ if (dockerOn === 'yes' || dockerOn === 'conditional') {
439
+ const optRes = nixos('virtualisation.docker.extraOptions');
440
+ const hostsRes = nixos('virtualisation.docker.daemon.settings.hosts');
441
+ const tlsOn = ['tls', 'tlsverify'].some((k) => view(nixos(`virtualisation.docker.daemon.settings.${k}`)).value === true);
442
+ const j1 = judge(optRes, (x) => typeof x === 'string' && /(?:-H|--host)[ =]+tcp:\/\//.test(x) && !/--tlsverify|--tls\b/.test(x));
443
+ const j2 = tlsOn ? 'no' : judge(hostsRes, (x) => Array.isArray(x) && x.some((h) => typeof h === 'string' && /^tcp:\/\//.test(h)));
444
+ const j = j1 === 'yes' ? 'yes' : j2 === 'yes' ? 'yes' : j1 === 'conditional' || j2 === 'conditional' ? 'conditional' : 'no';
445
+ if (j === 'yes' || j === 'conditional') { const res = j1 !== 'no' ? optRes : hostsRes; addFinding('docker-tcp-no-tls', { anchor: anchorOf(res), scope: 'virtualisation.docker', state: j === 'conditional' || dockerOn === 'conditional' ? 'conditional' : 'active', uncertain: false, evidence: [evidenceOf(res)] }); }
446
+ } }
447
+
371
448
  // ── databases / listeners ──
372
449
  if (hasDefs('services.postgresql.')) {
373
450
  const enableRes = nixos('services.postgresql.enable');
@@ -398,6 +475,38 @@ export function analyzeNixosHardening(opts, extra = {}) {
398
475
  inventory.push({ kind: 'declared-service', name: 'nginx', state, listening: state === 'active' ? 'unknown' : state === 'conditional' ? 'unknown' : 'no', ports: [], ingress: 'unknown', reachability: { internet: 'unknown', reason: 'listen addresses are generated by the module and external topology is not modelled' }, evidence: [evidenceOf(enableRes)] });
399
476
  }
400
477
 
478
+ // ── other services that choose their bind address ──
479
+ // The same decision postgresql makes above. Only an EXPLICIT wildcard (or, for redis, an explicit null, which the module defines as "every interface") is reported:
480
+ // an unset option keeps the module's own default, which the catalog does not record for these services and which is never guessed.
481
+ const WILDCARD = (x, nullIsAll) => (x === null ? nullIsAll === true : typeof x === 'string' && x.split(/[\s,]+/).filter(Boolean).some((a) => a === '*' || a === '0.0.0.0' || a === '::' || a === '[::]'));
482
+ const LISTENERS = [
483
+ { name: 'redis', pattern: /^services\.redis\.servers\.([^.]+)\.bind$/, enable: (m) => `services.redis.servers.${m[1]}.enable`, portOpt: (m) => `services.redis.servers.${m[1]}.port`, port: 6379, nullIsAll: true, scope: (m) => `services.redis.servers.${m[1]}` },
484
+ { name: 'mysql', pattern: /^services\.mysql\.settings\.mysqld\.(?:bind-address|bind_address)$/, enable: () => 'services.mysql.enable', port: 3306, scope: () => 'services.mysql' },
485
+ { name: 'mongodb', pattern: /^services\.mongodb\.bind_ip$/, enable: () => 'services.mongodb.enable', port: 27017, scope: () => 'services.mongodb' },
486
+ { name: 'memcached', pattern: /^services\.memcached\.listen$/, enable: () => 'services.memcached.enable', portOpt: () => 'services.memcached.port', port: 11211, scope: () => 'services.memcached' },
487
+ { name: 'elasticsearch', pattern: /^services\.elasticsearch\.listenAddress$/, enable: () => 'services.elasticsearch.enable', portOpt: () => 'services.elasticsearch.port', port: 9200, scope: () => 'services.elasticsearch' },
488
+ { name: 'grafana', pattern: /^services\.grafana\.settings\.server\.http_addr$/, enable: () => 'services.grafana.enable', port: 3000, scope: () => 'services.grafana' },
489
+ ];
490
+ for (const L of LISTENERS) {
491
+ for (const o of cfg.options.filter((x) => x.namespace === 'nixos' && L.pattern.test(x.path))) {
492
+ const m = L.pattern.exec(o.path);
493
+ const listenRes = note(o, 'listener');
494
+ const wild = judge(listenRes, (x) => WILDCARD(x, L.nullIsAll));
495
+ if (wild !== 'yes' && wild !== 'conditional') continue;
496
+ const enableRes = note(nixos(L.enable(m)), 'listener');
497
+ const state = stateOf(enableRes);
498
+ if (state !== 'active' && state !== 'conditional') continue;
499
+ let port = L.port;
500
+ if (L.portOpt) { const pv = view(nixos(L.portOpt(m))); if (pv.state === 'known' && Number.isInteger(pv.value) && pv.value > 0) port = pv.value; }
501
+ const ingress = ingressFor(port);
502
+ const scope = L.scope(m);
503
+ inventory.push({ kind: 'declared-service', name: L.name, state, listening: 'yes', ports: [port], ingress, reachability: { internet: 'unknown', reason: 'external network topology is not part of the configuration' }, evidence: [evidenceOf(enableRes), evidenceOf(listenRes)] });
504
+ const st = state === 'conditional' || wild === 'conditional' ? 'conditional' : 'active';
505
+ const restricted = ingress === 'firewall-closed' || ingress === 'restricted-interface';
506
+ addFinding('listener-all-interfaces', { severity: restricted ? 'low' : ingress === 'open-in-firewall' || ingress === 'firewall-disabled' ? 'high' : 'medium', anchor: anchorOf(listenRes), scope, state: st, uncertain: !view(listenRes).definite, evidence: [evidenceOf(listenRes), evidenceOf(enableRes)], extra: { exposure: { enabled: true, listening: 'yes', ingress, reachable: 'unknown' } }, mitigations: restricted ? [{ kind: 'restrictive-ingress', ingress }] : [] });
507
+ }
508
+ }
509
+
401
510
  // ── TLS material and enforcement on nginx virtual hosts ──
402
511
  if (hasDefs('services.nginx.')) {
403
512
  const nginxState = stateOf(nixos('services.nginx.enable'));