@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.
- package/CHANGELOG.md +18 -0
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -4
- package/src/dataflow/engine.js +10 -4
- package/src/language/haskell-findings.js +2 -0
- package/src/language/haskell-guards.js +1 -1
- package/src/language/haskell-ir.js +3 -1
- package/src/language/haskell-models.js +94 -0
- package/src/language/haskell-security-rules.js +21 -1
- package/src/language/nixos-hardening.js +109 -0
- package/src/sast/zip-slip.js +79 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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.
|
|
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",
|
package/src/dataflow/engine.js
CHANGED
|
@@ -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
|
-
|
|
1432
|
-
findings.push(...
|
|
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,
|
|
2028
|
-
findings.push(..._nestedSinkFindings(node.value.args, state, callContext,
|
|
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
|
-
|
|
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'));
|