@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/bin/agentic-security.js +6 -3
  3. package/dist/1340.index.js +11 -6
  4. package/dist/3500.index.js +943 -0
  5. package/dist/5616.index.js +146 -19
  6. package/dist/{c06a1b4c20155c06fb48.json → 7e58a58b8e6ccdde1659.json} +6 -5
  7. package/dist/9560.index.js +29 -5
  8. package/dist/agentic-security.mjs +2 -2
  9. package/dist/agentic-security.mjs.sha256 +1 -1
  10. package/package.json +6 -5
  11. package/src/egress/policy.js +79 -1
  12. package/src/lineage/CLAUDE.md +3 -1
  13. package/src/lineage/deployment/adapter-cloud.js +5 -2
  14. package/src/lineage/deployment/attack-paths.js +107 -30
  15. package/src/lineage/deployment/boundaries-run.js +2 -2
  16. package/src/lineage/deployment/iam-eval.js +105 -0
  17. package/src/lineage/deployment/projection.js +10 -4
  18. package/src/lineage/deployment/proof-binding.js +79 -0
  19. package/src/llm-validator/bounded-context.js +2 -1
  20. package/src/mcp/CLAUDE.md +6 -0
  21. package/src/mcp/tools.js +27 -3
  22. package/src/pipeline/evidence-record.js +1 -1
  23. package/src/posture/CLAUDE.md +2 -0
  24. package/src/posture/assurance/candidate-qualification.js +155 -0
  25. package/src/posture/assurance/release-package-cli.js +44 -2
  26. package/src/posture/auditor-walkthrough.js +2 -1
  27. package/src/posture/dnr/detection-ir.js +2 -2
  28. package/src/posture/dnr/hunt.js +22 -6
  29. package/src/posture/dnr/log-reader.js +47 -3
  30. package/src/posture/dnr/matchers.js +43 -3
  31. package/src/posture/dnr/outputs.js +16 -2
  32. package/src/posture/dnr/payload-classes.js +12 -2
  33. package/src/posture/dnr/validate.js +17 -4
  34. package/src/posture/evaluation/report.js +2 -1
  35. package/src/posture/evaluation/score.js +24 -7
  36. package/src/posture/logic-claims.js +15 -14
  37. package/src/posture/sibling-queue.js +69 -0
  38. package/src/posture/triage-memory.js +106 -26
  39. package/src/posture/verification-separation.js +55 -7
  40. package/src/report/index.js +4 -0
  41. package/src/runScan.js +5 -0
  42. package/src/scope/flags.js +1 -0
  43. package/src/scope/inventory.js +17 -1
  44. package/src/scope/release-scope.js +80 -0
@@ -1 +1 @@
1
- 73c7cdd64324fdc2acc75010555f85145a3512ba2331c52a59dcb34901736bf6 agentic-security.mjs
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.29",
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",
@@ -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 cfg = loadPolicyConfig(scanRoot);
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 };
@@ -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, and a blocked or unresolved path keeps that fact in an `oracle-confirmed-path-*` label. `exposureReach` is the path-free reachability the drift diff compares. |
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
- const attrs = { actions: actions.join(',').slice(0, 250) };
45
- if (conditioned) attrs.conditioned = true;
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
- export const BOUNDARY_CONTEXT_VERSION = '1.0.0';
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 hopState(h, from, to) {
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 state = hopState(h, from, to);
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
- for (const d of h.denies) {
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 truncated = false;
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 (truncated) return;
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) { truncated = true; return; }
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
- if (hopList.length >= limits.maxHops) return;
265
- for (const [to, h] of [...hopsFrom(ix, here)].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
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
- statement: `${n} path(s) found in the supplied configuration${truncated ? ' (search stopped at a limit; more may exist)' : ''}; a path that is not listed has not been shown to exist or to be absent`,
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
- out.push({ recordId: rec.id, outcome: rec.outcome, applicable: reasons.length === 0, reasons });
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
- return { label: 'exploitable-confirmed', recordId: ok.recordId, reason: 'a trusted oracle confirmed the effect in its declared scenario; deployment reachability comes from configuration or sampled observation, not from a production probe' };
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 popts = { now, maxHops, maxPaths };
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 p of exposure.paths) paths.push({ kind: 'exposure', ...p, exploitability: labelPath(p, assessments) });
401
- for (const p of impact.paths) paths.push({ kind: 'impact', ...p, exploitability: labelPath(p, assessments) });
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('The path search stopped at a limit; more paths may exist.');
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']);