@clear-capabilities/agentic-security-scanner 0.195.29 → 0.195.30
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 +31 -0
- package/bin/agentic-security.js +6 -3
- package/dist/1340.index.js +11 -6
- package/dist/3500.index.js +943 -0
- package/dist/5616.index.js +146 -19
- package/dist/{c06a1b4c20155c06fb48.json → 7e58a58b8e6ccdde1659.json} +6 -5
- package/dist/9560.index.js +29 -5
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -5
- package/src/egress/policy.js +79 -1
- package/src/lineage/CLAUDE.md +3 -1
- package/src/lineage/deployment/adapter-cloud.js +5 -2
- package/src/lineage/deployment/attack-paths.js +107 -30
- package/src/lineage/deployment/boundaries-run.js +2 -2
- package/src/lineage/deployment/iam-eval.js +105 -0
- package/src/lineage/deployment/projection.js +10 -4
- package/src/lineage/deployment/proof-binding.js +79 -0
- package/src/llm-validator/bounded-context.js +2 -1
- package/src/mcp/CLAUDE.md +6 -0
- package/src/mcp/tools.js +27 -3
- package/src/pipeline/evidence-record.js +1 -1
- package/src/posture/CLAUDE.md +2 -0
- package/src/posture/assurance/candidate-qualification.js +155 -0
- package/src/posture/assurance/release-package-cli.js +44 -2
- package/src/posture/auditor-walkthrough.js +2 -1
- package/src/posture/dnr/detection-ir.js +2 -2
- package/src/posture/dnr/hunt.js +22 -6
- package/src/posture/dnr/log-reader.js +47 -3
- package/src/posture/dnr/matchers.js +43 -3
- package/src/posture/dnr/outputs.js +16 -2
- package/src/posture/dnr/payload-classes.js +12 -2
- package/src/posture/dnr/validate.js +17 -4
- package/src/posture/evaluation/report.js +2 -1
- package/src/posture/evaluation/score.js +24 -7
- package/src/posture/logic-claims.js +15 -14
- package/src/posture/sibling-queue.js +69 -0
- package/src/posture/triage-memory.js +106 -26
- package/src/posture/verification-separation.js +55 -7
- package/src/report/index.js +4 -0
- package/src/runScan.js +5 -0
- package/src/scope/flags.js +1 -0
- package/src/scope/inventory.js +17 -1
- package/src/scope/release-scope.js +80 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
49efaca1a4d9c735d54511f9e3b65837265fbc5341177c79a5fb6e9a4d3bea90 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.195.
|
|
3
|
+
"version": "0.195.30",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
|
|
88
88
|
"test:glob": "node --test test/glob-compat.test.js",
|
|
89
89
|
"test:sast": "node --test test/dag-precision.test.js test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/todo-marker-words.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/zip-slip-go.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/java-structural-cross-file.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/vendor-java-parser-current.test.js test/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js test/pipeline-env-secret.test.js test/mark-used-vuln-functions.test.js",
|
|
90
|
-
"test:posture": "node --test test/contracts/finding-provenance.contract.test.js test/contracts/root-cause-sweep.contract.test.js test/contracts/model-validation.contract.test.js test/contracts/agentic-discovery.contract.test.js test/contracts/local-models.contract.test.js test/contracts/dependency-finding.contract.test.js test/contracts/taint-flow.contract.test.js test/contracts/runtime-corroboration.contract.test.js test/compliance-coverage.test.js test/contracts/blast-radius.contract.test.js test/contracts/federation.contract.test.js test/contracts/eval-lab-report.contract.test.js test/contracts/fleet.contract.test.js test/contracts/rule-compiler.contract.test.js test/contracts/release-assurance.contract.test.js test/contracts/dnr.contract.test.js test/contracts/strategic-metrics.contract.test.js test/contracts/exploit-proof.contract.test.js test/contracts/artifact-provenance.contract.test.js test/dnr/artifact-join.test.js test/rule-pack-signing.test.js test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-transport.test.js test/local-preset-callers.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/scope/inventory.test.js test/scope/readtree-scope.test.js test/scope/cli-scope.test.js test/scope/watch-scope.test.js test/scope/watch-cli.test.js test/workflow-flow.test.js test/oidc-trust.test.js test/haskell-coverage/coverage.test.js test/validation-disposition.test.js test/claim-groups.test.js test/validation-bounded-context.test.js test/evidence-record.test.js test/report-disposition-view.test.js test/sca-basis.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/api-contract-scope.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/code-boundaries.test.js test/fleet/job-runner.test.js test/fleet/accounting.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/eval-lab/import-independent.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/window-store-view.test.js test/dnr/exploit-correlation.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/proof-fabric/generate-haskell.test.js test/proof-fabric/generate-haskell-widened.test.js test/cli/proof-lifecycle-demo.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/gates.test.js test/rule-compiler/ci-regression.test.js test/rule-compiler/synthesis.test.js test/rule-compiler/structure.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js test/witness-bundle.test.js",
|
|
90
|
+
"test:posture": "node --test test/contracts/finding-provenance.contract.test.js test/contracts/root-cause-sweep.contract.test.js test/contracts/model-validation.contract.test.js test/contracts/agentic-discovery.contract.test.js test/contracts/local-models.contract.test.js test/contracts/dependency-finding.contract.test.js test/contracts/taint-flow.contract.test.js test/contracts/runtime-corroboration.contract.test.js test/compliance-coverage.test.js test/contracts/blast-radius.contract.test.js test/contracts/federation.contract.test.js test/contracts/eval-lab-report.contract.test.js test/contracts/fleet.contract.test.js test/contracts/rule-compiler.contract.test.js test/contracts/release-assurance.contract.test.js test/contracts/dnr.contract.test.js test/contracts/strategic-metrics.contract.test.js test/contracts/exploit-proof.contract.test.js test/contracts/artifact-provenance.contract.test.js test/dnr/artifact-join.test.js test/rule-pack-signing.test.js test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/verification-independence.test.js test/candidate-qualification.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/triage-memory-scoped.test.js test/sibling-queue.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-profile.test.js test/mcp-approval-binding.test.js test/egress-transport.test.js test/local-preset-callers.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/scope/inventory.test.js test/scope/readtree-scope.test.js test/scope/cli-scope.test.js test/scope/scope-evidence.test.js test/scope/watch-scope.test.js test/workflow-flow.test.js test/oidc-trust.test.js test/haskell-coverage/coverage.test.js test/validation-disposition.test.js test/claim-groups.test.js test/validation-bounded-context.test.js test/evidence-record.test.js test/report-disposition-view.test.js test/sca-basis.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/api-contract-scope.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/code-boundaries.test.js test/fleet/job-runner.test.js test/fleet/accounting.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/eval-lab/import-independent.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/auth-truth-object-authz.test.js test/pilot/two-tenant-records.test.js test/pilot/app-to-cloud-and-db.test.js test/pilot/candidate-packet.test.js test/dnr/window-store-view.test.js test/dnr/exploit-correlation.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/proof-fabric/generate-haskell.test.js test/proof-fabric/generate-haskell-widened.test.js test/cli/proof-lifecycle-demo.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/gates.test.js test/rule-compiler/ci-regression.test.js test/rule-compiler/synthesis.test.js test/rule-compiler/structure.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js test/witness-bundle.test.js test/scope/watch-cli.test.js",
|
|
91
91
|
"test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/rx-exec-receiver.test.js test/incremental-dependency.test.js test/incremental-integrity.test.js test/demand-query.test.js test/ir-cache.test.js test/canonicalize-parity.test.js test/dataflow-source-attribution.test.js",
|
|
92
92
|
"test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js test/capabilities/tools.test.js",
|
|
93
93
|
"test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/git-history-batch.test.js test/default-scan-drift.test.js test/bundle-optional-deps.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"bench:language-support:check": "node ../bench/language-support/check.mjs",
|
|
103
103
|
"bench:live-feed": "node ../bench/live-feed/run.mjs",
|
|
104
104
|
"bench:language-support:perf": "node ../bench/language-support/perf.mjs",
|
|
105
|
-
"test:evaluation": "node --test test/evaluation/protocol.test.js test/evaluation/grouping.test.js test/evaluation/labels.test.js test/evaluation/custody.test.js test/evaluation/runner.test.js test/evaluation/gates-scorecard.test.js test/evaluation/reporting.test.js test/evaluation/why-missed.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/evaluation/release-gate.test.js test/evaluation/publication.test.js",
|
|
105
|
+
"test:evaluation": "node --test test/evaluation/protocol.test.js test/evaluation/grouping.test.js test/evaluation/labels.test.js test/evaluation/custody.test.js test/evaluation/runner.test.js test/evaluation/gates-scorecard.test.js test/evaluation/reporting.test.js test/evaluation/why-missed.test.js test/evaluation/scorer-reconciliation.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/evaluation/release-gate.test.js test/evaluation/publication.test.js",
|
|
106
106
|
"reproduce:mini": "node ../scripts/public-reproduction.mjs",
|
|
107
107
|
"reproduce:routing": "node ../scripts/routing-replay.mjs",
|
|
108
108
|
"verification:patch-example": "node ../scripts/patch-replay-example.mjs",
|
|
@@ -122,12 +122,13 @@
|
|
|
122
122
|
"test:release-closure": "node --test test/release-closure/closure-plan.test.js test/release-closure/closure-record.test.js test/release-closure/compatibility.test.js test/release-closure/rollout-stages.test.js test/release-closure/migration-readers.test.js test/release-closure/rollback.test.js test/release-closure/wrappers.test.js test/release-closure/final-closure.test.js test/release-closure/final-closure-controller.test.js test/release-closure/runner-hardening.test.js test/release-closure/remote-attestation.test.js test/release-closure/closure-parallel.test.js",
|
|
123
123
|
"release:closure:check": "node ../scripts/release-closure.mjs --run",
|
|
124
124
|
"release:closure:static": "node ../scripts/release-closure.mjs --static",
|
|
125
|
+
"pilot:qualify": "node ../bench/threat-model-pilot/qualify.mjs",
|
|
125
126
|
"test:documentation": "node --test test/documentation/drift-check.test.js test/documentation/generated-pages.test.js test/documentation/scope-and-examples.test.js test/documentation/policy-cards.test.js test/documentation/operations-and-review.test.js",
|
|
126
127
|
"test:capabilities": "node --test test/capabilities/manifest.test.js test/capabilities/filesystem.test.js test/capabilities/exec.test.js test/capabilities/network.test.js test/capabilities/canary.test.js test/capabilities/tools.test.js test/capabilities/recovery.test.js test/capabilities/receipts.test.js test/capabilities/adversarial.test.js test/capabilities/linux-enforcement.test.js test/supervise-async-sweep.test.js",
|
|
127
128
|
"test:verification": "node --test test/verification/verification-record.test.js test/verification/oracle-adapters.test.js test/verification/replay-manifest.test.js test/verification/patch-negative.test.js test/verification/non-taint-classes.test.js test/verification/interface-equivalence.test.js test/verification/advisory-gating.test.js test/verification/oracle-conformance.test.js",
|
|
128
|
-
"test:deployment": "node --test test/deployment/boundary-graph.test.js test/deployment/config-adapters.test.js test/deployment/trace-correlation.test.js test/deployment/cross-repo.test.js test/deployment/attack-paths.test.js test/deployment/drift.test.js test/deployment/boundary-presentation.test.js test/deployment/differentiation-ablation.test.js test/deployment/boundaries-cli.test.js",
|
|
129
|
+
"test:deployment": "node --test test/deployment/boundary-graph.test.js test/deployment/config-adapters.test.js test/deployment/trace-correlation.test.js test/deployment/cross-repo.test.js test/deployment/attack-paths.test.js test/deployment/iam-actions-and-limits.test.js test/deployment/proof-binding.test.js test/deployment/drift.test.js test/deployment/boundary-presentation.test.js test/deployment/differentiation-ablation.test.js test/deployment/boundaries-cli.test.js",
|
|
129
130
|
"test:portfolio": "node --test test/portfolio/manifest.test.js test/portfolio/bundle.test.js test/portfolio/signing.test.js test/portfolio/work-units.test.js test/portfolio/resume.test.js test/portfolio/scheduler.test.js test/portfolio/progress.test.js test/portfolio/retention.test.js test/portfolio/backend.test.js",
|
|
130
|
-
"test:invariants": "node --test test/invariants/invariant-schema.test.js test/invariants/invariant-lifecycle.test.js test/invariants/invariant-scenarios.test.js test/invariants/business-state-oracle.test.js test/invariants/scenario-shrinking.test.js test/invariants/invariant-repair.test.js test/invariants/business-coverage.test.js test/invariants/invariant-ablation.test.js",
|
|
131
|
+
"test:invariants": "node --test test/invariants/invariant-schema.test.js test/invariants/invariant-lifecycle.test.js test/invariants/invariant-scenarios.test.js test/invariants/business-state-oracle.test.js test/invariants/scenario-shrinking.test.js test/invariants/invariant-repair.test.js test/invariants/approval-workflow.test.js test/invariants/business-coverage.test.js test/invariants/invariant-ablation.test.js",
|
|
131
132
|
"bench:invariant-ablation": "node ../scripts/invariant-ablation.mjs",
|
|
132
133
|
"verification:conformance:check": "node ../scripts/verification-conformance-check.mjs",
|
|
133
134
|
"verification:conformance:static": "node ../scripts/verification-conformance-check.mjs --static",
|
package/src/egress/policy.js
CHANGED
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
// functionality nobody configured it to disable.
|
|
66
66
|
|
|
67
67
|
import * as fs from 'node:fs';
|
|
68
|
+
import * as crypto from 'node:crypto';
|
|
68
69
|
import { load as loadYaml } from '../util/yaml.js';
|
|
69
70
|
import { statePath } from '../posture/state-dir.js';
|
|
70
71
|
import { isLoopbackUrl } from '../llm-validator/local-endpoint.js';
|
|
@@ -211,10 +212,87 @@ export function evaluateEgress(ctx = {}) {
|
|
|
211
212
|
return { allowed: false, decision: 'deny', reason: "AGENTIC_SECURITY_EGRESS_DENY=1 is set", provider, policySource: 'env', purpose };
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
return _withProfile(ctx, provider);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── execution profiles (threat-model PRD FR-06.1, TM-08) ────────────────────────────────────────────────────────────────────────────
|
|
219
|
+
// `AGENTIC_SECURITY_EGRESS_PROFILE=restricted` makes the operator's policy authoritative: the file named by
|
|
220
|
+
// AGENTIC_SECURITY_EGRESS_POLICY must exist and be valid, or every model request is denied before transport. The repository's own
|
|
221
|
+
// `.agentic-security/egress-policy.yml` can only narrow it (a deny in either denies; a repository `mode: allow` cannot widen an
|
|
222
|
+
// operator `local-only`), and an invalid repository file also denies. An unknown profile or an unknown operator mode always fails
|
|
223
|
+
// closed. Outside the restricted profile the documented legacy default holds (no valid policy means allow), and an invalid policy
|
|
224
|
+
// file that is present is reported on the decision instead of being silently ignored. AGENTIC_SECURITY_EGRESS_DENY=1 always wins.
|
|
225
|
+
export const EGRESS_PROFILES = Object.freeze(['standard', 'restricted']);
|
|
226
|
+
export const EGRESS_MODES = Object.freeze(['allow', 'deny', 'local-only']);
|
|
227
|
+
const MAX_POLICY_BYTES = 256 * 1024;
|
|
228
|
+
|
|
229
|
+
/** Read a policy file and classify it: absent | valid | malformed | unreadable | unsupported. Never throws. */
|
|
230
|
+
export function loadPolicyState(fp) {
|
|
231
|
+
if (!fp) return { status: 'absent', doc: null, digest: null };
|
|
232
|
+
let raw;
|
|
233
|
+
// one open, then the size and type checks and the read on that same descriptor: the file cannot be swapped between check and use
|
|
234
|
+
let fd = null;
|
|
235
|
+
try {
|
|
236
|
+
fd = fs.openSync(fp, 'r');
|
|
237
|
+
const st = fs.fstatSync(fd);
|
|
238
|
+
if (!st.isFile() || st.size > MAX_POLICY_BYTES) return { status: 'unreadable', doc: null, digest: null };
|
|
239
|
+
const buf = Buffer.alloc(st.size);
|
|
240
|
+
fs.readSync(fd, buf, 0, st.size, 0);
|
|
241
|
+
raw = buf.toString('utf8');
|
|
242
|
+
} catch (e) { return { status: e && e.code === 'ENOENT' ? 'absent' : 'unreadable', doc: null, digest: null }; }
|
|
243
|
+
finally { if (fd !== null) try { fs.closeSync(fd); } catch { /* already closed */ } }
|
|
244
|
+
const digest = `sha256:${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
|
|
245
|
+
let doc;
|
|
246
|
+
try { doc = loadYaml(raw); } catch { return { status: 'malformed', doc: null, digest }; }
|
|
247
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return { status: 'malformed', doc: null, digest };
|
|
248
|
+
if (doc.mode !== undefined && !EGRESS_MODES.includes(doc.mode)) return { status: 'unsupported', doc, digest };
|
|
249
|
+
return { status: 'valid', doc, digest };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _withProfile(ctx, provider) {
|
|
253
|
+
const purpose = ctx.purpose ?? 'unknown';
|
|
254
|
+
const profile = process.env.AGENTIC_SECURITY_EGRESS_PROFILE || 'standard';
|
|
214
255
|
const envMode = process.env.AGENTIC_SECURITY_EGRESS_MODE;
|
|
215
|
-
const
|
|
256
|
+
const deny = (reasonCode, reason, extra = {}) => ({ allowed: false, decision: 'deny', reason, reasonCode, provider, policySource: 'operator', purpose, profile, enforcement: 'enforced', ...extra });
|
|
257
|
+
if (!EGRESS_PROFILES.includes(profile)) return deny('profile-unknown', 'the egress profile set by the operator is not recognised, so every request is denied');
|
|
258
|
+
if (envMode !== undefined && envMode !== '' && !EGRESS_MODES.includes(envMode)) return deny('operator-mode-unknown', 'the egress mode set by the operator is not recognised, so every request is denied');
|
|
259
|
+
let repoPath = null;
|
|
260
|
+
try { repoPath = statePath(ctx.scanRoot, POLICY_FILE); } catch { repoPath = null; }
|
|
261
|
+
const repo = loadPolicyState(repoPath);
|
|
262
|
+
|
|
263
|
+
if (profile === 'restricted') {
|
|
264
|
+
const op = loadPolicyState(process.env.AGENTIC_SECURITY_EGRESS_POLICY || null);
|
|
265
|
+
const policyState = { operator: op.status, repository: repo.status };
|
|
266
|
+
if (op.status !== 'valid') return deny(`operator-policy-${op.status}`, `the restricted profile requires a valid operator egress policy, and it is ${op.status}`, { policyState, policyIdentity: op.digest });
|
|
267
|
+
if (repo.status !== 'valid' && repo.status !== 'absent') return deny(`repository-policy-${repo.status}`, `the repository egress policy is ${repo.status}; under the restricted profile that denies`, { policyState, policyIdentity: op.digest });
|
|
268
|
+
const first = _decideOne(ctx, provider, op.doc, envMode || op.doc.mode || 'allow', envMode ? 'env' : 'operator');
|
|
269
|
+
const stamp = (d) => ({ ...d, reasonCode: d.allowed ? 'allowed' : (d.reasonCode || 'policy-deny'), profile, enforcement: 'enforced', policyState, policyIdentity: op.digest });
|
|
270
|
+
if (!first.allowed) return stamp(first);
|
|
271
|
+
if (repo.status === 'valid') {
|
|
272
|
+
const second = _decideOne(ctx, provider, repo.doc, repo.doc.mode || 'allow', 'config');
|
|
273
|
+
if (!second.allowed) return stamp(second);
|
|
274
|
+
}
|
|
275
|
+
return stamp(first);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const cfg = repo.status === 'valid' ? repo.doc : null;
|
|
216
279
|
const mode = envMode || (cfg && cfg.mode) || 'allow';
|
|
217
280
|
const policySource = envMode ? 'env' : ((cfg && cfg.mode) ? 'config' : 'default');
|
|
281
|
+
const d = _decideOne(ctx, provider, cfg, mode, policySource);
|
|
282
|
+
const invalid = repo.status !== 'valid' && repo.status !== 'absent';
|
|
283
|
+
return {
|
|
284
|
+
...d, reasonCode: d.allowed ? (invalid ? 'allowed-legacy-default' : 'allowed') : (d.reasonCode || 'policy-deny'), profile, enforcement: 'legacy-default', policyState: { repository: repo.status },
|
|
285
|
+
...(invalid ? { configurationWarning: `the repository egress policy is ${repo.status} and was ignored; under the standard profile the legacy default applies (select AGENTIC_SECURITY_EGRESS_PROFILE=restricted to fail closed)` } : {}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The checks of one policy document, unchanged from before profiles existed. */
|
|
290
|
+
function _decideOne(ctx, provider, cfg, mode, policySource) {
|
|
291
|
+
const {
|
|
292
|
+
purpose = 'unknown', endpoint,
|
|
293
|
+
model = null, role = null, region = null, repository = null,
|
|
294
|
+
path: filePath = null, dataClass = null, contextTokens = null,
|
|
295
|
+
} = ctx;
|
|
218
296
|
|
|
219
297
|
if (mode === 'deny') {
|
|
220
298
|
return { allowed: false, decision: 'deny', reason: "egress mode is 'deny'", provider, policySource, purpose };
|
package/src/lineage/CLAUDE.md
CHANGED
|
@@ -1254,7 +1254,9 @@ pure function of text. Suite: `npm run test:deployment`, with fixtures under `te
|
|
|
1254
1254
|
| `deployment/ingest.js` | `ingestDeploymentConfig` (pure: text in, graph out; an adapter that throws is contained to that file), `ingestDeploymentFiles` (reads named files under one root, refusing traversal, absolute paths, symlinks and oversize files; feature gated), `detectFormat` (HCL, proxy configuration and TOML/XML are typed unsupported syntax) and `invalidateChangedSources` (drops the relations a changed or removed file supported, using the recorded digests; a node only that file declared stops being resolved or is dropped). |
|
|
1255
1255
|
| `deployment/trace-correlation.js` | X-303. `importTraceLines` keeps an allowlist of topology metadata and removes every other field WITHOUT storing it, even its name (only counts, split credential-shaped, payload-shaped and other); strips query strings; turns opaque path segments into `:id`; rejects a record whose kept value is not identifier-shaped; refuses other-environment records instead of relabelling them; merges duplicates; and is bounded (bytes, line length, lines, distinct observations with oldest-first eviction reported). `correlateObservations` matches with numbered rules R1 to R4 (exact service pair, route chain, identity, naming convention); R4 yields an `inferred` edge, R1 to R3 a `runtime-observation` edge, and static edges are never edited. `coverage.trafficCoverage` is always `not-established`; observed, sampled, stale and missing are separate lists; a static edge with no observation stays `possible`. `applyCorrelation` folds the result into a new validated graph. It does NOT persist anything: callers store `exportObservationSet` output and read it back with `parseObservationSet` (closed world, bounded, digest-checked). This sits beside, not on top of, the existing data-flow `runtime-observation.js` records, whose graph-id oriented contract is unchanged; only `EVENT_COUNT_BANDS` is shared. |
|
|
1256
1256
|
| `deployment/cross-repo.js` | X-304. `resolveCrossRepoBoundaries` takes graph exports the operator supplies, declared dependencies (each binding an exact 40/64 hex revision for both ends) and an access scope; it has no network or file access (a test pins its import list). A link resolves only when both repositories are in scope and supplied, each graph validates and names the repository it was supplied as, the graph revision equals the bound revision, the declared service exists resolved in its repository and in no other supplied repository, and a declared entry route reaches it. Every failure is a typed gap (`access-denied`, `repository-unavailable`, `version-mismatch`, `revision-unknown`, `identity-collision`, `unresolved-identity`, `graph-integrity`, `malformed-input`) and the dependency is kept as an unresolved edge to an unresolved callee. A resolved link carries evidence (declaration file and digest, both graph digests, the declaring files on each side, the route chain) and transitions (entry route, caller identities, callee identity and its grants, boundary crossings). It extends the idea of `cross-repo-link.js` to this graph; that module and `federation-loader.js` are unchanged. |
|
|
1257
|
-
| `deployment/attack-paths.js` | X-305. `computeAttackPaths` walks bounded, cycle-safe simple paths from entry points (resolved public routes and services) to targets (resources an identity is granted, or in the privileged zone) over `routes-to`, `calls`, `depends-on`, `network-allows`, `assumes` and `grants`. A hop is `blocked` only when an explicit deny edge sits between its endpoints; a path is `blocked` if any hop is, `unresolved` if a hop or endpoint is unresolved or a tenant transition makes it CONDITIONAL (a hop whose endpoints carry different tenants, listed as a `tenant-binding` condition), `runtime-supported` only if every network hop was observed (identity hops stay configuration), else `possible`. Each path lists supporting edges (with source file, digest, revision), assumptions, blockers, environments, boundary crossings and a coverage block (observed, sampled, stale, unobserved hops; `trafficCoverage` is always `not-established`; staleness is judged only when the caller passes `now`). Path ids hash the edge ids along the path, not the state, so they survive reordering and state changes. `bindFindingToService` ties a finding to a service ONLY through an explicit path-prefix binding (longest prefix wins; a tie, an absent service or no binding is a typed non-bound status and nothing is guessed). `analyzeFindingBoundaries` builds the per-finding `boundary-context` (exposure paths to the bound service, impact paths onward, verification assessments, uncovered boundaries, warnings). **Exploitability is never derived from reachability**: `assessVerification` accepts a record only if it validates, its outcome is `confirmed`, its confirmation level RECOMPUTED from evidence is `runtime-confirmed`, and its oracle is the one the finding's non-taint class names (an unsupported class has none); the label is `exploitable-confirmed` only then
|
|
1257
|
+
| `deployment/attack-paths.js` | X-305. `computeAttackPaths` walks bounded, cycle-safe simple paths from entry points (resolved public routes and services) to targets (resources an identity is granted, or in the privileged zone) over `routes-to`, `calls`, `depends-on`, `network-allows`, `assumes` and `grants`. A hop is `blocked` only when an explicit deny edge sits between its endpoints; a path is `blocked` if any hop is, `unresolved` if a hop or endpoint is unresolved or a tenant transition makes it CONDITIONAL (a hop whose endpoints carry different tenants, listed as a `tenant-binding` condition), `runtime-supported` only if every network hop was observed (identity hops stay configuration), else `possible`. Each path lists supporting edges (with source file, digest, revision), assumptions, blockers, environments, boundary crossings and a coverage block (observed, sampled, stale, unobserved hops; `trafficCoverage` is always `not-established`; staleness is judged only when the caller passes `now`). Path ids hash the edge ids along the path, not the state, so they survive reordering and state changes. `bindFindingToService` ties a finding to a service ONLY through an explicit path-prefix binding (longest prefix wins; a tie, an absent service or no binding is a typed non-bound status and nothing is guessed). `analyzeFindingBoundaries` builds the per-finding `boundary-context` (exposure paths to the bound service, impact paths onward, verification assessments, uncovered boundaries, warnings). **Exploitability is never derived from reachability**: `assessVerification` accepts a record only if it validates, its outcome is `confirmed`, its confirmation level RECOMPUTED from evidence is `runtime-confirmed`, and its oracle is the one the finding's non-taint class names (an unsupported class has none); the label is `exploitable-confirmed` only then AND (threat-model FR-04) when the record is at the graph's revision and a signed `proof-binding.js` envelope ties it to that exact path and claim; without one the proof is reported as `exploitability.componentProof`. A blocked or unresolved path keeps that fact in an `oracle-confirmed-path-*` label. IAM hops are decided per action by `iam-eval.js` (threat-model FR-03): a deny blocks only a matching action, resource and holding condition; `truncation.reasons` records depth, result-count and expansion cutoffs; boundary context 1.1.0. `exposureReach` is the path-free reachability the drift diff compares. |
|
|
1258
|
+
| `deployment/iam-eval.js` | Threat-model FR-03.1. Action and resource globs, the decided condition subset (`StringEquals`, `StringNotEquals`, `Bool`, `ArnEquals` against a caller context), `statementAttrs` (what the IAM adapter stores on a grants edge, inside the attribute grammar) and `decideAction` (possible, blocked, unresolved, not-granted with the blocking rule and the unresolved reasons). An opaque deny (no action list) is unresolved, never blocking. |
|
|
1259
|
+
| `deployment/proof-binding.js` | Threat-model FR-04.1. The `agentic-security/proof-binding` v1 envelope: Ed25519-signed by an issuer in the operator's trust root, binding a verification record to one path, claim, artifact, policy digest, environment, tenant, actor, route, action and oracle version. `verifyProofBinding` and `judgePathPromotion` return distinct codes (malformed, unsupported-version, missing-binding, untrusted-issuer, tampered, expired, context-mismatch). |
|
|
1258
1260
|
| `deployment/drift.js` | X-306. `diffBoundaryGraphs` compares two validated graphs of one repository at stable ids: new exposure (newly reachable from an entry, by reachability not by edge), changed privilege (new or changed grant, new identity assumption), tenant weakening (a dropped tenant binding, found by (kind, name, environment) because the tenant is part of the node id, or a new cross-tenant edge) and removed controls (a removed deny) are `material`; other relation changes are reported but not material. It also diffs the SOURCES and names the edges each moved file supported. `planRescan` maps changes to hypotheses through the bound service: a hypothesis is affected when its service is a seed of a change or can reach or be reached from one (union of both revisions, bounded closure); everything else is `skipped` with a reason and NOT rescanned. A hypothesis that cannot be tied to a service cannot be proven independent, so it makes the plan `incomplete` (only when something changed), as does a graph gap that stands for a missing dependency (`DEPENDENCY_GAP_CODES`). `incrementalAnnotate` recomputes only the planned hypotheses; `compareScoped` checks the result against a complete recomputation on `scopedSignature` (which leaves out graph-wide metadata) and never reports an incomplete result as equivalent. `invalidatePathsForSources` reuses `invalidateChangedSources` and drops exactly the paths whose supporting edges or node declarations cite a changed file. |
|
|
1259
1261
|
| `deployment/projection.js` | X-307. The ONE projection of a boundary context, modelled on `posture/verification/projection.js`: `projectBoundaryContext` (closed-world validation first: an unknown field, a credential-shaped or payload-shaped string, a control character or an over-long string refuses the whole context), `filterBoundaryView` (service, repository, environment, tenant; keeps scope, uncovered boundaries and every earlier warning and adds a warning saying how many paths it hid), `boundaryFields` (the additive report fields; the context itself is echoed only when it validated), `boundaryCoverage` (run accounting) and `attachBoundaryContext`, the ONLY producer, gated by the `deployment-boundaries` feature (off returns the same findings array untouched). The text states what was and was not established, never says safe, protected or unreachable (a lint-style test sweeps every state), and a context with no path says "not shown to exist". Consumed by `report/index.js` (`normalizeFindings` adds `boundaryContext` and `boundaryView` only when a finding carries a context, a top-level `boundaryCoverage`, a Markdown and a terminal block) and by MCP `explain_finding`. Previous output schemas are pinned in `test/fixtures/boundary-compat/` (captured before the wiring). |
|
|
1260
1262
|
| `deployment/boundaries-run.js` | X-308 wiring. The one operator entry point: `agentic-security boundaries --from <dir> [--findings <last-scan.json>] [--environment] [--repository] [--revision] [--root] [--out] [--json] [--now]`. `runBoundaries` (gated by `deployment-boundaries`, off by default; off returns a typed `disabled` BEFORE any file is opened) lists configuration files under the directory (bounded walk, symlinks never followed or listed), reads the reserved inputs `service-bindings.json`, `identities.json` and `traces.jsonl`/`traces.ndjson` (never ingested as configuration), ingests through `ingestDeploymentFiles`, correlates sanitized traces, reads findings from a scan result (a sibling `.sig` that exists must verify, else exit 1; an absent one is read and labelled unsigned) or from memory, and attaches contexts with `attachBoundaryContext`. Local files only, no scan, no execution; the only write is `--out`, refused when the target is a symbolic link. Exit codes: 0 report, 1 failed, 2 usage, 3 feature off, blocked or unsupported. `renderBoundariesText` reuses each context's own projected text. Suite: `test/deployment/boundaries-cli.test.js`. |
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// and conditions it cannot interpret are reported, not approximated.
|
|
10
10
|
|
|
11
11
|
import { adapterContext, isObject, asArray, str } from './adapter-kit.js';
|
|
12
|
+
import { statementAttrs } from './iam-eval.js';
|
|
12
13
|
|
|
13
14
|
export const TERRAFORM_PARSER = 'terraform-plan-json';
|
|
14
15
|
export const TERRAFORM_PARSER_VERSION = '1';
|
|
@@ -39,10 +40,12 @@ function applyPolicy(kit, identity, policy, subject) {
|
|
|
39
40
|
const conditioned = isObject(st.Condition) && Object.keys(st.Condition).length > 0;
|
|
40
41
|
read += 1;
|
|
41
42
|
if (resources.length > MAX_RESOURCES_PER_STATEMENT) kit.gap('limit-exceeded', subject, `a statement lists ${resources.length} resources; only the first ${MAX_RESOURCES_PER_STATEMENT} are read`);
|
|
43
|
+
if (actions.join(',').length > 250) kit.gap('limit-exceeded', subject, 'a statement\'s action list is longer than the graph records; the clipped list leaves decisions about the missing actions unresolved');
|
|
44
|
+
if (conditioned && statementAttrs(actions, st.Condition).conditionSupported !== true) kit.gap('unsupported-syntax', subject, 'a statement condition uses an operator or value outside the supported subset (StringEquals, StringNotEquals, Bool, ArnEquals); decisions it can affect stay unresolved');
|
|
42
45
|
for (const r of resources.slice(0, MAX_RESOURCES_PER_STATEMENT)) {
|
|
43
46
|
const target = kit.node('resource', `aws/${r}`, { trustZone: 'internal', attrs: r === '*' ? { wildcard: true } : {} });
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
// threat-model FR-03.1: the action list (and whether it was clipped) and the condition, in a form the path search can decide
|
|
48
|
+
const attrs = statementAttrs(actions, conditioned ? st.Condition : null);
|
|
46
49
|
kit.edge('grants', identity, target, {
|
|
47
50
|
effect, confidence: conditioned ? 'low' : 'high', discriminator: `${actions.join(',')}${conditioned ? ';cond' : ''}`.slice(0, 250), attrs,
|
|
48
51
|
});
|
|
@@ -26,9 +26,12 @@ import { indexGraph, validateBoundaryGraph, boundaryCrossings } from './boundary
|
|
|
26
26
|
import { semanticId, hypothesisIdFromFinding } from '../../posture/assurance/identity.js';
|
|
27
27
|
import { validateVerificationRecord, deriveConfirmationLevel } from '../../posture/assurance/verification-record.js';
|
|
28
28
|
import { nonTaintClassOf, SCENARIO_CLASSES } from '../../posture/oracles/scenario-classes.js';
|
|
29
|
+
import { decideAction, resourceMatches, UNKNOWN_ACTION } from './iam-eval.js';
|
|
30
|
+
import { judgePathPromotion } from './proof-binding.js';
|
|
29
31
|
|
|
30
32
|
export const BOUNDARY_CONTEXT_SCHEMA = 'agentic-security/boundary-context';
|
|
31
|
-
|
|
33
|
+
// 1.1.0 (threat-model FR-03.3): optional `truncation` (why a search stopped) and `action` (the action a query asked about).
|
|
34
|
+
export const BOUNDARY_CONTEXT_VERSION = '1.1.0';
|
|
32
35
|
|
|
33
36
|
export const TRAVERSED_RELATIONS = Object.freeze(['routes-to', 'calls', 'depends-on', 'network-allows', 'assumes', 'grants']);
|
|
34
37
|
const TRAVERSED = new Set(TRAVERSED_RELATIONS);
|
|
@@ -122,24 +125,61 @@ function hopsFrom(ix, nodeId) {
|
|
|
122
125
|
else if (TRAVERSED.has(e.relation)) h.edges.push(e);
|
|
123
126
|
hops.set(e.to, h);
|
|
124
127
|
}
|
|
128
|
+
// threat-model FR-03.1: a deny statement whose resource is a pattern (`*`, `arn:aws:s3:::bucket/*`) is a node of its own, so before
|
|
129
|
+
// this it never reached the hop to the concrete resource it covers. It now applies to every resource its pattern matches.
|
|
130
|
+
const patternDenies = [];
|
|
131
|
+
for (const [to, h] of hops) {
|
|
132
|
+
const name = ix.nodesById.get(to)?.name ?? '';
|
|
133
|
+
if (/[*?]/.test(name)) for (const d of h.denies) if (d.relation === 'grants') patternDenies.push({ d, pattern: name });
|
|
134
|
+
}
|
|
135
|
+
for (const [to, h] of hops) {
|
|
136
|
+
const name = ix.nodesById.get(to)?.name ?? '';
|
|
137
|
+
for (const { d, pattern } of patternDenies) if (d.to !== to && resourceMatches(pattern, name) && !h.denies.includes(d)) h.denies.push(d);
|
|
138
|
+
}
|
|
125
139
|
for (const [k, h] of [...hops]) if (!h.edges.length) hops.delete(k);
|
|
126
140
|
return hops;
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
function
|
|
130
|
-
if (h.denies.length) return 'blocked';
|
|
143
|
+
function baseState(h, from, to) {
|
|
131
144
|
if (h.edges.some((e) => e.pathState === 'runtime-supported')) return 'runtime-supported';
|
|
132
145
|
if (!from.resolved || !to.resolved || h.edges.every((e) => e.pathState === 'unresolved' || e.pathState === 'blocked')) return 'unresolved';
|
|
133
146
|
return 'possible';
|
|
134
147
|
}
|
|
135
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Decide a hop. A network deny, or a deny on a relation other than an IAM grant, blocks it as before. An IAM hop is decided per
|
|
151
|
+
* action (threat-model FR-03.1): with a requested action, that action only; without one, every action the allow statements grant, and
|
|
152
|
+
* the hop is possible when any of them is. A deny blocks only an action it matches, under a condition that holds for the request
|
|
153
|
+
* context; a condition that cannot be evaluated leaves the action unresolved.
|
|
154
|
+
* @returns {{state:string, denies:object[], actions:object[]|null}}
|
|
155
|
+
*/
|
|
156
|
+
function hopDecision(h, from, to, q = null) {
|
|
157
|
+
const other = h.denies.filter((d) => d.relation !== 'grants');
|
|
158
|
+
if (other.length) return { state: 'blocked', denies: other.map((d) => ({ d, why: null })), actions: null };
|
|
159
|
+
const allows = h.edges.filter((e) => e.relation === 'grants' && e.effect === 'allow');
|
|
160
|
+
const iamDenies = h.denies.filter((d) => d.relation === 'grants');
|
|
161
|
+
const base = baseState(h, from, to);
|
|
162
|
+
if (!allows.length) return { state: base, denies: [], actions: null };
|
|
163
|
+
const recorded = [...new Set(allows.flatMap((e) => String(e.attrs?.actions ?? '').split(',').filter(Boolean)))].sort();
|
|
164
|
+
// with no IAM deny on the hop and no requested action there is nothing to decide per action: the grant stands as before
|
|
165
|
+
if (!iamDenies.length && !(q && q.action)) return { state: base, denies: [], actions: recorded.length ? recorded.map((action) => ({ action, outcome: base === 'unresolved' ? 'unresolved' : 'possible', blockedBy: [], unresolvedBy: [] })) : null };
|
|
166
|
+
const wanted = q && q.action ? [q.action] : (recorded.length ? recorded : [UNKNOWN_ACTION]);
|
|
167
|
+
const decisions = wanted.map((a) => decideAction(allows, iamDenies, a, q && q.context));
|
|
168
|
+
const pick = (o) => decisions.some((d) => d.outcome === o);
|
|
169
|
+
const state = pick('possible') ? base : pick('unresolved') ? 'unresolved' : pick('blocked') ? 'blocked' : 'not-granted';
|
|
170
|
+
const denies = state === 'blocked' ? decisions.flatMap((d) => d.blockers).map((b) => ({ d: iamDenies.find((x) => x.id === b.edgeId), why: b })) : [];
|
|
171
|
+
return { state, denies, actions: decisions.map(({ action, outcome, blockers, unresolvedBy }) => ({ action, outcome, blockedBy: blockers.map((b) => b.edgeId), unresolvedBy })) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function hopState(h, from, to, q = null) { return hopDecision(h, from, to, q).state; }
|
|
175
|
+
|
|
136
176
|
function isStale(e, nowMs, staleAfterMs) {
|
|
137
177
|
if (e.attrs?.stale === true) return true;
|
|
138
178
|
if (nowMs === null || !e.observationInterval) return false;
|
|
139
179
|
return nowMs - Date.parse(e.observationInterval.end) > staleAfterMs;
|
|
140
180
|
}
|
|
141
181
|
|
|
142
|
-
function buildPath(ix, seq, hopList, nowMs, staleAfterMs) {
|
|
182
|
+
function buildPath(ix, seq, hopList, nowMs, staleAfterMs, q = null) {
|
|
143
183
|
const nodes = seq.map((id) => ix.nodesById.get(id));
|
|
144
184
|
const hops = [];
|
|
145
185
|
const conditions = [];
|
|
@@ -151,16 +191,20 @@ function buildPath(ix, seq, hopList, nowMs, staleAfterMs) {
|
|
|
151
191
|
hopList.forEach((h, i) => {
|
|
152
192
|
from = nodes[i];
|
|
153
193
|
const to = nodes[i + 1];
|
|
154
|
-
const
|
|
194
|
+
const dec = hopDecision(h, from, to, q);
|
|
195
|
+
const state = dec.state;
|
|
155
196
|
const crossing = boundaryCrossings(h.edges[0], ix.nodesById);
|
|
156
197
|
for (const c of crossing) crossings.push({ hop: i, type: c.type, detail: c.detail });
|
|
157
198
|
hops.push({
|
|
158
199
|
index: i, from: nodeSummary(from), to: nodeSummary(to), relations: sortedUnique(h.edges.map((e) => e.relation)), state,
|
|
159
200
|
edges: h.edges.map(edgeSummary).sort((a, b) => (a.id < b.id ? -1 : 1)),
|
|
201
|
+
...(dec.actions ? { actions: dec.actions } : {}),
|
|
160
202
|
});
|
|
161
|
-
|
|
203
|
+
// each blocker is the rule that actually decided the hop, with the action and the condition it matched
|
|
204
|
+
for (const { d, why } of dec.denies) {
|
|
205
|
+
if (!d) continue;
|
|
162
206
|
blockers.push({ hop: i, edgeId: d.id, relation: d.relation, provenance: d.provenance, source: d.source ? { file: d.source.file, digest: d.source.digest } : null,
|
|
163
|
-
detail: `${d.relation} (deny) from ${from.name} to ${to.name}
|
|
207
|
+
detail: `${d.relation} (deny) from ${from.name} to ${to.name}`, ...(why ? { action: why.action, matchedActions: why.matchedActions, condition: why.condition } : {}) });
|
|
164
208
|
}
|
|
165
209
|
if (from.tenant !== null && to.tenant !== null && from.tenant !== to.tenant) {
|
|
166
210
|
conditions.push({ kind: 'tenant-binding', hop: i, detail: `${from.name} (tenant ${from.tenant}) reaches ${to.name} (tenant ${to.tenant}): reachable only if no tenant binding is enforced on this transition, and the configuration does not establish either` });
|
|
@@ -193,6 +237,10 @@ function buildPath(ix, seq, hopList, nowMs, staleAfterMs) {
|
|
|
193
237
|
else if (networkHops.length && networkHops.every((h) => h.state === 'runtime-supported')) state = 'runtime-supported';
|
|
194
238
|
else state = 'possible';
|
|
195
239
|
|
|
240
|
+
// threat-model FR-03.2: a path that carries several actions keeps their separate outcomes; one is the worst over its IAM hops
|
|
241
|
+
const actionOutcomes = {};
|
|
242
|
+
const RANK = { possible: 0, unresolved: 1, blocked: 2, 'not-granted': 3 };
|
|
243
|
+
for (const hp of hops) for (const a of hp.actions || []) if (!(a.action in actionOutcomes) || RANK[a.outcome] > RANK[actionOutcomes[a.action]]) actionOutcomes[a.action] = a.outcome;
|
|
196
244
|
const observedEnds = hops.flatMap((h) => h.edges).filter((e) => e.observationInterval).map((e) => e.observationInterval);
|
|
197
245
|
const supportingEdgeIds = sortedUnique(hops.flatMap((h) => h.edges.map((e) => e.id)));
|
|
198
246
|
const envs = sortedUnique([...hops.flatMap((h) => h.edges.map((e) => e.environment)), ...nodes.map((n) => n.environment)].filter(Boolean));
|
|
@@ -202,6 +250,8 @@ function buildPath(ix, seq, hopList, nowMs, staleAfterMs) {
|
|
|
202
250
|
id: semanticId('bpath', { hops: hops.map((h) => h.edges.map((e) => e.id).sort()) }, ['hops']),
|
|
203
251
|
entry: nodeSummary(nodes[0]), target: nodeSummary(nodes[nodes.length - 1]),
|
|
204
252
|
state, conditional: conditions.length > 0, conditions, blockers, assumptions: [...assumptions].sort(),
|
|
253
|
+
...(Object.keys(actionOutcomes).length ? { actionOutcomes: Object.fromEntries(Object.entries(actionOutcomes).sort()) } : {}),
|
|
254
|
+
...(q && q.action ? { action: q.action } : {}),
|
|
205
255
|
environments: envs, tenants,
|
|
206
256
|
provenance: sortedUnique(hops.flatMap((h) => h.edges.map((e) => e.provenance))),
|
|
207
257
|
crossings: dedupCross, supportingEdgeIds, hops,
|
|
@@ -239,7 +289,7 @@ function defaultTargets(graph) {
|
|
|
239
289
|
*/
|
|
240
290
|
export function computeAttackPaths(graph, o = {}) {
|
|
241
291
|
const v = validateBoundaryGraph(graph);
|
|
242
|
-
if (!v.ok) return { ok: false, paths: [], entries: [], targets: [], truncated: false, limits: { ...PATH_LIMITS }, statement: 'the graph failed validation, so no path was computed', errors: v.errors };
|
|
292
|
+
if (!v.ok) return { ok: false, paths: [], entries: [], targets: [], truncated: false, truncation: { reasons: [], depthCutoffs: 0, frontier: [], frontierCapped: false }, limits: { ...PATH_LIMITS }, statement: 'the graph failed validation, so no path was computed', errors: v.errors };
|
|
243
293
|
const limits = {
|
|
244
294
|
maxHops: Math.min(o.maxHops ?? PATH_LIMITS.maxHops, PATH_LIMITS.maxHops), maxPaths: Math.min(o.maxPaths ?? PATH_LIMITS.maxPaths, PATH_LIMITS.maxPaths),
|
|
245
295
|
maxExpansions: Math.min(o.maxExpansions ?? PATH_LIMITS.maxExpansions, PATH_LIMITS.maxExpansions),
|
|
@@ -252,25 +302,34 @@ export function computeAttackPaths(graph, o = {}) {
|
|
|
252
302
|
const staleAfterMs = o.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
253
303
|
const found = [];
|
|
254
304
|
let expansions = 0;
|
|
255
|
-
let
|
|
305
|
+
let stopped = false;
|
|
306
|
+
const q = o.action ? { action: String(o.action), context: o.context ?? {} } : (o.context ? { action: null, context: o.context } : null);
|
|
307
|
+
// threat-model FR-03.3 (TM-04): every bound that ended the search is recorded, so "no path" after a depth cutoff is never read as
|
|
308
|
+
// "no path exists". Reaching the depth bound on a node with no unvisited continuation is not a cutoff.
|
|
309
|
+
const cut = { depth: 0, results: false, expansions: false, frontier: [] };
|
|
310
|
+
const next = (seq, here) => [...hopsFrom(ix, here)].sort((a, b) => (a[0] < b[0] ? -1 : 1))
|
|
311
|
+
.filter(([to, h]) => !seq.includes(to) && !(q && q.action && hopState(h, ix.nodesById.get(here), ix.nodesById.get(to), q) === 'not-granted'));
|
|
256
312
|
|
|
257
313
|
const walk = (seq, hopList) => {
|
|
258
|
-
if (
|
|
314
|
+
if (stopped) return;
|
|
259
315
|
const here = seq[seq.length - 1];
|
|
260
316
|
if (hopList.length && targetSet.has(here)) {
|
|
261
|
-
found.push(buildPath(ix, seq, hopList, Number.isNaN(nowMs) ? null : nowMs, staleAfterMs));
|
|
262
|
-
if (found.length >= limits.maxPaths) {
|
|
317
|
+
found.push(buildPath(ix, seq, hopList, Number.isNaN(nowMs) ? null : nowMs, staleAfterMs, q));
|
|
318
|
+
if (found.length >= limits.maxPaths) { stopped = true; cut.results = true; return; }
|
|
319
|
+
}
|
|
320
|
+
const cont = next(seq, here);
|
|
321
|
+
if (hopList.length >= limits.maxHops) {
|
|
322
|
+
if (cont.length) { cut.depth++; if (cut.frontier.length < 20) cut.frontier.push({ node: here, depth: hopList.length, unexplored: cont.length }); }
|
|
323
|
+
return;
|
|
263
324
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (seq.includes(to)) continue;
|
|
267
|
-
if (++expansions > limits.maxExpansions) { truncated = true; return; }
|
|
325
|
+
for (const [to, h] of cont) {
|
|
326
|
+
if (++expansions > limits.maxExpansions) { stopped = true; cut.expansions = true; return; }
|
|
268
327
|
walk([...seq, to], [...hopList, h]);
|
|
269
328
|
}
|
|
270
329
|
};
|
|
271
330
|
for (const entry of entries) {
|
|
272
331
|
if (o.includeSelf && targetSet.has(entry)) {
|
|
273
|
-
const p = buildPath(ix, [entry], [], Number.isNaN(nowMs) ? null : nowMs, staleAfterMs);
|
|
332
|
+
const p = buildPath(ix, [entry], [], Number.isNaN(nowMs) ? null : nowMs, staleAfterMs, q);
|
|
274
333
|
p.assumptions = sortedUnique([...p.assumptions, 'the entry point is itself the target; its public trust zone is declared in configuration']);
|
|
275
334
|
found.push(p);
|
|
276
335
|
}
|
|
@@ -278,9 +337,13 @@ export function computeAttackPaths(graph, o = {}) {
|
|
|
278
337
|
}
|
|
279
338
|
found.sort((a, b) => STATE_RANK[a.state] - STATE_RANK[b.state] || a.hops.length - b.hops.length || (a.id < b.id ? -1 : 1));
|
|
280
339
|
const n = found.length;
|
|
340
|
+
const reasons = [...(cut.depth ? ['depth'] : []), ...(cut.results ? ['result-count'] : []), ...(cut.expansions ? ['expansion'] : [])];
|
|
341
|
+
const truncated = reasons.length > 0;
|
|
281
342
|
return {
|
|
282
343
|
ok: true, paths: found, entries, targets, truncated, limits,
|
|
283
|
-
|
|
344
|
+
truncation: { reasons, depthCutoffs: cut.depth, frontier: cut.frontier, frontierCapped: cut.depth > cut.frontier.length },
|
|
345
|
+
...(q && q.action ? { action: q.action } : {}),
|
|
346
|
+
statement: `${n} path(s) found in the supplied configuration${truncated ? ` (search stopped at a limit: ${reasons.join(', ')}; more may exist)` : ''}; a path that is not listed has not been shown to exist or to be absent`,
|
|
284
347
|
errors: [],
|
|
285
348
|
};
|
|
286
349
|
}
|
|
@@ -306,7 +369,7 @@ export function exposureReach(graph, { entries } = {}) {
|
|
|
306
369
|
const cur = best.get(id);
|
|
307
370
|
for (const [to, h] of hopsFrom(ix, id)) {
|
|
308
371
|
const st = hopState(h, ix.nodesById.get(id), ix.nodesById.get(to));
|
|
309
|
-
if (st === 'blocked') continue;
|
|
372
|
+
if (st === 'blocked' || st === 'not-granted') continue;
|
|
310
373
|
const state = cur.state === 'unresolved' || st === 'unresolved' ? 'unresolved' : 'definite';
|
|
311
374
|
const prev = best.get(to);
|
|
312
375
|
if (prev && (prev.state === 'definite' || state === 'unresolved')) continue;
|
|
@@ -324,7 +387,7 @@ export function exposureReach(graph, { entries } = {}) {
|
|
|
324
387
|
* own claim: the record is validated, its confirmation level is recomputed from its evidence, and its oracle must be the one the
|
|
325
388
|
* finding's class names (a finding of an unsupported class has no applicable oracle).
|
|
326
389
|
*/
|
|
327
|
-
export function assessVerification(finding, records, hypothesisId = hypothesisIdFromFinding(finding)) {
|
|
390
|
+
export function assessVerification(finding, records, hypothesisId = hypothesisIdFromFinding(finding), { revision = null } = {}) {
|
|
328
391
|
const out = [];
|
|
329
392
|
const cls = nonTaintClassOf(finding);
|
|
330
393
|
for (const rec of Array.isArray(records) ? records : []) {
|
|
@@ -340,12 +403,16 @@ export function assessVerification(finding, records, hypothesisId = hypothesisId
|
|
|
340
403
|
const adapter = SCENARIO_CLASSES.find((c) => c.id === cls.classId)?.adapterId;
|
|
341
404
|
if (!rec.oracle || rec.oracle.id !== adapter) reasons.push(`the record's oracle is not '${adapter}', the oracle for class '${cls.classId}'`);
|
|
342
405
|
}
|
|
343
|
-
|
|
406
|
+
// threat-model FR-04.1: an exact matching commit is the minimum for a CURRENT component proof; a record for another revision is
|
|
407
|
+
// kept, with the reason, as historical evidence and never applied to this graph
|
|
408
|
+
let historical = false;
|
|
409
|
+
if (revision && rec.commit !== revision) { historical = true; reasons.push(`the record was made at ${String(rec.commit).slice(0, 12)}, not this graph's revision ${String(revision).slice(0, 12)}; it is historical evidence for that revision only`); }
|
|
410
|
+
out.push({ recordId: rec.id, outcome: rec.outcome, applicable: reasons.length === 0, reasons, ...(historical ? { historical: true, commit: rec.commit } : {}) });
|
|
344
411
|
}
|
|
345
412
|
return out.sort((a, b) => String(a.recordId).localeCompare(String(b.recordId)));
|
|
346
413
|
}
|
|
347
414
|
|
|
348
|
-
function labelPath(path, assessments) {
|
|
415
|
+
function labelPath(path, assessments, pc = null) {
|
|
349
416
|
const ok = assessments.find((a) => a.applicable);
|
|
350
417
|
if (!ok) {
|
|
351
418
|
const why = assessments.length ? assessments.flatMap((a) => a.reasons).slice(0, 3).join('; ') : 'no verification record for this hypothesis was supplied';
|
|
@@ -353,7 +420,12 @@ function labelPath(path, assessments) {
|
|
|
353
420
|
}
|
|
354
421
|
if (path.state === 'blocked') return { label: 'oracle-confirmed-path-blocked', recordId: ok.recordId, reason: 'the oracle confirmed the effect in its declared scenario, but configuration explicitly denies a hop on this path' };
|
|
355
422
|
if (path.state === 'unresolved') return { label: 'oracle-confirmed-path-unresolved', recordId: ok.recordId, reason: 'the oracle confirmed the effect in its declared scenario, but deployment reachability along this path is unresolved' };
|
|
356
|
-
|
|
423
|
+
// threat-model FR-04.1 (TM-05): promotion to exploitable needs an issued binding of this proof to THIS path; a component proof alone
|
|
424
|
+
// is reported beside the path and does not promote it
|
|
425
|
+
const record = pc && pc.records.find((r) => r && r.id === ok.recordId);
|
|
426
|
+
const j = record ? judgePathPromotion({ path, kind: pc.kind, graph: pc.graph, record, bindings: pc.proofBindings, trustRoot: pc.trustRoot, now: pc.now, expect: pc.expect }) : { promote: false, code: 'component-only', reason: 'no binding was supplied', diagnostics: [] };
|
|
427
|
+
if (!j.promote) return { label: 'not-established', recordId: null, reason: `a trusted oracle confirmed the component, but it is not shown on this ${pc ? pc.kind : 'deployed'} path: ${j.reason}`, componentProof: { recordId: ok.recordId, code: j.code, diagnostics: j.diagnostics } };
|
|
428
|
+
return { label: 'exploitable-confirmed', recordId: ok.recordId, reason: 'a trusted oracle confirmed the effect in its declared scenario, and an issued binding ties that proof to this path; deployment reachability comes from configuration or sampled observation, not from a production probe', bindingId: j.bindingId };
|
|
357
429
|
}
|
|
358
430
|
|
|
359
431
|
// ------------------------------------------------------------ the per-finding context
|
|
@@ -384,21 +456,25 @@ function summarizeExposure(paths) {
|
|
|
384
456
|
* @param {Array} [args.records] version-1 verification records
|
|
385
457
|
* @param {number|string} [args.now]
|
|
386
458
|
*/
|
|
387
|
-
export function analyzeFindingBoundaries({ graph, finding, bindings = [], records = [], now, maxHops, maxPaths } = {}) {
|
|
459
|
+
export function analyzeFindingBoundaries({ graph, finding, bindings = [], records = [], now, maxHops, maxPaths, action, context: requestContext, proofBindings = [], trustRoot = {}, expect = {} } = {}) {
|
|
388
460
|
const v = validateBoundaryGraph(graph);
|
|
389
461
|
if (!v.ok) return { ok: false, context: null, errors: v.errors };
|
|
390
462
|
const hypothesisId = hypothesisIdFromFinding(finding);
|
|
391
463
|
const binding = bindFindingToService(finding, bindings, graph);
|
|
392
|
-
const assessments = assessVerification(finding, records, hypothesisId);
|
|
464
|
+
const assessments = assessVerification(finding, records, hypothesisId, { revision: graph.revision ?? null });
|
|
465
|
+
const clock = now === undefined || now === null ? Date.now() : (typeof now === 'number' ? now : Date.parse(now));
|
|
466
|
+
const pc = (kind) => ({ kind, graph, records: Array.isArray(records) ? records : [], proofBindings, trustRoot, now: clock, expect });
|
|
393
467
|
const paths = [];
|
|
394
468
|
let truncated = false;
|
|
395
|
-
const
|
|
469
|
+
const truncationReasons = new Set();
|
|
470
|
+
const popts = { now, maxHops, maxPaths, ...(action ? { action } : {}), ...(requestContext ? { context: requestContext } : {}) };
|
|
396
471
|
if (binding.status === 'bound') {
|
|
397
472
|
const exposure = computeAttackPaths(graph, { ...popts, targets: [binding.service.id], includeSelf: true });
|
|
398
473
|
const impact = computeAttackPaths(graph, { ...popts, entries: [binding.service.id] });
|
|
399
474
|
truncated = exposure.truncated || impact.truncated;
|
|
400
|
-
for (const
|
|
401
|
-
for (const p of
|
|
475
|
+
for (const r of [...exposure.truncation.reasons, ...impact.truncation.reasons]) truncationReasons.add(r);
|
|
476
|
+
for (const p of exposure.paths) paths.push({ kind: 'exposure', ...p, exploitability: labelPath(p, assessments, pc('exposure')) });
|
|
477
|
+
for (const p of impact.paths) paths.push({ kind: 'impact', ...p, exploitability: labelPath(p, assessments, pc('impact')) });
|
|
402
478
|
}
|
|
403
479
|
const exposurePaths = paths.filter((p) => p.kind === 'exposure');
|
|
404
480
|
const exposure = binding.status === 'bound' ? summarizeExposure(exposurePaths)
|
|
@@ -422,7 +498,7 @@ export function analyzeFindingBoundaries({ graph, finding, bindings = [], record
|
|
|
422
498
|
const warnings = ['Runtime traffic coverage is not established by any supplied observation; configured relationships were not exercised.'];
|
|
423
499
|
if (binding.status !== 'bound') warnings.push(`This finding is not tied to a deployed service (${binding.status}); no path was computed.`);
|
|
424
500
|
if (graph.gaps.length) warnings.push(`The boundary graph has ${graph.gaps.length} gap(s) (${sortedUnique(graph.gaps.map((g) => g.code)).join(', ')}); relationships behind them are missing, not absent.`);
|
|
425
|
-
if (truncated) warnings.push(
|
|
501
|
+
if (truncated) warnings.push(`The path search stopped at a limit (${[...truncationReasons].sort().join(', ')}); more paths may exist, so no listed absence is a proof of absence.`);
|
|
426
502
|
|
|
427
503
|
const context = {
|
|
428
504
|
schema: BOUNDARY_CONTEXT_SCHEMA, schemaVersion: BOUNDARY_CONTEXT_VERSION, id: '', hypothesisId,
|
|
@@ -435,7 +511,8 @@ export function analyzeFindingBoundaries({ graph, finding, bindings = [], record
|
|
|
435
511
|
tenants: sortedUnique(graph.nodes.map((n) => n.tenant).filter(Boolean)),
|
|
436
512
|
gapCodes: sortedUnique(graph.gaps.map((g) => g.code)), gapCount: graph.gaps.length,
|
|
437
513
|
},
|
|
438
|
-
exposure, paths, truncated,
|
|
514
|
+
exposure, paths, truncated, truncation: { reasons: [...truncationReasons].sort() },
|
|
515
|
+
...(action ? { action } : {}),
|
|
439
516
|
verification: assessments, uncovered, warnings,
|
|
440
517
|
};
|
|
441
518
|
context.id = semanticId('bctx', { hypothesisId, graph: graph.digest, binding: binding.binding, service: binding.service?.id ?? null, paths: paths.map((p) => p.id) }, ['hypothesisId', 'graph', 'binding', 'service', 'paths']);
|