@empire-builder-kit/nx 1.0.0-beta.9 → 1.0.0-rc.2
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/NOTICE +6 -0
- package/README.md +113 -3
- package/executors.json +30 -0
- package/package.json +84 -6
- package/src/api-contract/baseline-update.d.ts +82 -0
- package/src/api-contract/baseline-update.js +225 -0
- package/src/api-contract/baseline-update.js.map +1 -0
- package/src/api-contract/compatibility.d.ts +30 -0
- package/src/api-contract/compatibility.js +643 -0
- package/src/api-contract/compatibility.js.map +1 -0
- package/src/api-contract/contract-graph.d.ts +35 -0
- package/src/api-contract/contract-graph.js +141 -0
- package/src/api-contract/contract-graph.js.map +1 -0
- package/src/api-contract/evidence.d.ts +23 -0
- package/src/api-contract/evidence.js +87 -0
- package/src/api-contract/evidence.js.map +1 -0
- package/src/api-contract/index.d.ts +6 -0
- package/src/api-contract/index.js +10 -0
- package/src/api-contract/index.js.map +1 -0
- package/src/api-contract/model.d.ts +144 -0
- package/src/api-contract/model.fixtures.d.ts +2 -0
- package/src/api-contract/model.fixtures.js +81 -0
- package/src/api-contract/model.fixtures.js.map +1 -0
- package/src/api-contract/model.js +993 -0
- package/src/api-contract/model.js.map +1 -0
- package/src/api-contract/rollout.d.ts +32 -0
- package/src/api-contract/rollout.js +88 -0
- package/src/api-contract/rollout.js.map +1 -0
- package/src/authorization/compatibility.d.ts +62 -0
- package/src/authorization/compatibility.js +175 -0
- package/src/authorization/compatibility.js.map +1 -0
- package/src/authorization/index.d.ts +1 -0
- package/src/authorization/index.js +5 -0
- package/src/authorization/index.js.map +1 -0
- package/src/blueprints/execution.d.ts +25 -1
- package/src/blueprints/execution.js +246 -83
- package/src/blueprints/execution.js.map +1 -1
- package/src/blueprints/external-sandbox.d.ts +53 -0
- package/src/blueprints/external-sandbox.js +1182 -0
- package/src/blueprints/external-sandbox.js.map +1 -0
- package/src/blueprints/manifest.d.ts +42 -2
- package/src/blueprints/manifest.js +82 -27
- package/src/blueprints/manifest.js.map +1 -1
- package/src/blueprints/resolver.d.ts +14 -5
- package/src/blueprints/resolver.js +228 -68
- package/src/blueprints/resolver.js.map +1 -1
- package/src/cli/agent-command.d.ts +30 -0
- package/src/cli/agent-command.js +204 -0
- package/src/cli/agent-command.js.map +1 -0
- package/src/cli/agent-runtime.d.ts +25 -0
- package/src/cli/agent-runtime.js +488 -0
- package/src/cli/agent-runtime.js.map +1 -0
- package/src/cli/blueprint-authority.d.ts +9 -0
- package/src/cli/blueprint-authority.js +43 -0
- package/src/cli/blueprint-authority.js.map +1 -0
- package/src/cli/blueprint-command.d.ts +24 -0
- package/src/cli/blueprint-command.js +63 -0
- package/src/cli/blueprint-command.js.map +1 -0
- package/src/cli/bootstrap.d.ts +31 -0
- package/src/cli/bootstrap.js +165 -0
- package/src/cli/bootstrap.js.map +1 -0
- package/src/cli/dev-command.d.ts +92 -0
- package/src/cli/dev-command.js +561 -0
- package/src/cli/dev-command.js.map +1 -0
- package/src/cli/main.d.ts +131 -0
- package/src/cli/main.js +1199 -0
- package/src/cli/main.js.map +1 -0
- package/src/cli/output.d.ts +19 -0
- package/src/cli/output.js +51 -0
- package/src/cli/output.js.map +1 -0
- package/src/cli/personal-secret-runtime.d.ts +81 -0
- package/src/cli/personal-secret-runtime.js +567 -0
- package/src/cli/personal-secret-runtime.js.map +1 -0
- package/src/cli/secret-terminal.d.ts +12 -0
- package/src/cli/secret-terminal.js +128 -0
- package/src/cli/secret-terminal.js.map +1 -0
- package/src/container-images.d.ts +1 -0
- package/src/container-images.js +5 -0
- package/src/container-images.js.map +1 -0
- package/src/data-lifecycle/index.d.ts +3 -0
- package/src/data-lifecycle/index.js +7 -0
- package/src/data-lifecycle/index.js.map +1 -0
- package/src/data-lifecycle/model.d.ts +172 -0
- package/src/data-lifecycle/model.fixtures.d.ts +2 -0
- package/src/data-lifecycle/model.fixtures.js +244 -0
- package/src/data-lifecycle/model.fixtures.js.map +1 -0
- package/src/data-lifecycle/model.js +95 -0
- package/src/data-lifecycle/model.js.map +1 -0
- package/src/data-lifecycle/sensitive-artifacts.d.ts +28 -0
- package/src/data-lifecycle/sensitive-artifacts.js +146 -0
- package/src/data-lifecycle/sensitive-artifacts.js.map +1 -0
- package/src/data-lifecycle/validation.d.ts +32 -0
- package/src/data-lifecycle/validation.js +837 -0
- package/src/data-lifecycle/validation.js.map +1 -0
- package/src/database-retirement/contract.d.ts +68 -0
- package/src/database-retirement/contract.js +166 -0
- package/src/database-retirement/contract.js.map +1 -0
- package/src/database-retirement/index.d.ts +1 -0
- package/src/database-retirement/index.js +5 -0
- package/src/database-retirement/index.js.map +1 -0
- package/src/deterministic-order.d.ts +2 -0
- package/src/deterministic-order.js +7 -0
- package/src/deterministic-order.js.map +1 -0
- package/src/developer-agent/context.d.ts +27 -0
- package/src/developer-agent/context.js +497 -0
- package/src/developer-agent/context.js.map +1 -0
- package/src/developer-agent/generator-planning.d.ts +34 -0
- package/src/developer-agent/generator-planning.js +645 -0
- package/src/developer-agent/generator-planning.js.map +1 -0
- package/src/developer-agent/generator-preview.d.ts +8 -0
- package/src/developer-agent/generator-preview.js +650 -0
- package/src/developer-agent/generator-preview.js.map +1 -0
- package/src/developer-agent/git-inspection.d.ts +108 -0
- package/src/developer-agent/git-inspection.js +815 -0
- package/src/developer-agent/git-inspection.js.map +1 -0
- package/src/developer-agent/handoff.d.ts +43 -0
- package/src/developer-agent/handoff.js +698 -0
- package/src/developer-agent/handoff.js.map +1 -0
- package/src/developer-agent/index.d.ts +10 -0
- package/src/developer-agent/index.js +14 -0
- package/src/developer-agent/index.js.map +1 -0
- package/src/developer-agent/model.d.ts +481 -0
- package/src/developer-agent/model.js +143 -0
- package/src/developer-agent/model.js.map +1 -0
- package/src/developer-agent/operation-registry.d.ts +45 -0
- package/src/developer-agent/operation-registry.js +1255 -0
- package/src/developer-agent/operation-registry.js.map +1 -0
- package/src/developer-agent/ordering.d.ts +9 -0
- package/src/developer-agent/ordering.js +15 -0
- package/src/developer-agent/ordering.js.map +1 -0
- package/src/developer-agent/planning.d.ts +18 -0
- package/src/developer-agent/planning.js +447 -0
- package/src/developer-agent/planning.js.map +1 -0
- package/src/developer-agent/preview-changes.d.ts +26 -0
- package/src/developer-agent/preview-changes.js +203 -0
- package/src/developer-agent/preview-changes.js.map +1 -0
- package/src/developer-agent/preview-source.d.ts +2 -0
- package/src/developer-agent/preview-source.js +16 -0
- package/src/developer-agent/preview-source.js.map +1 -0
- package/src/developer-agent/validation.d.ts +18 -0
- package/src/developer-agent/validation.js +2617 -0
- package/src/developer-agent/validation.js.map +1 -0
- package/src/developer-agent/workspace-inspection.d.ts +20 -0
- package/src/developer-agent/workspace-inspection.js +1134 -0
- package/src/developer-agent/workspace-inspection.js.map +1 -0
- package/src/developer-agent/workspace-path.d.ts +6 -0
- package/src/developer-agent/workspace-path.js +22 -0
- package/src/developer-agent/workspace-path.js.map +1 -0
- package/src/executors/blueprint-trust/blueprint-trust.d.ts +29 -0
- package/src/executors/blueprint-trust/blueprint-trust.js +115 -0
- package/src/executors/blueprint-trust/blueprint-trust.js.map +1 -0
- package/src/executors/blueprint-trust/schema.json +27 -0
- package/src/executors/blueprint-verification/blueprint-verification.d.ts +13 -0
- package/src/executors/blueprint-verification/blueprint-verification.js +110 -0
- package/src/executors/blueprint-verification/blueprint-verification.js.map +1 -0
- package/src/executors/blueprint-verification/schema.json +44 -0
- package/src/executors/deployment-plan/deployment-plan.js +1 -0
- package/src/executors/deployment-plan/deployment-plan.js.map +1 -1
- package/src/executors/dev-doctor/dev-doctor.d.ts +19 -5
- package/src/executors/dev-doctor/dev-doctor.js +186 -32
- package/src/executors/dev-doctor/dev-doctor.js.map +1 -1
- package/src/executors/dev-doctor/schema.json +1 -6
- package/src/executors/dev-setup/dev-setup.d.ts +7 -1
- package/src/executors/dev-setup/dev-setup.js +14 -3
- package/src/executors/dev-setup/dev-setup.js.map +1 -1
- package/src/executors/developer-context/developer-context.d.ts +38 -0
- package/src/executors/developer-context/developer-context.js +78 -0
- package/src/executors/developer-context/developer-context.js.map +1 -0
- package/src/executors/developer-context/schema.json +87 -0
- package/src/executors/fleet/fleet.d.ts +12 -43
- package/src/executors/fleet/fleet.js +120 -500
- package/src/executors/fleet/fleet.js.map +1 -1
- package/src/executors/fleet/schema.json +64 -14
- package/src/executors/github-bootstrap/github-bootstrap.d.ts +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js +1 -0
- package/src/executors/github-bootstrap/github-bootstrap.js.map +1 -1
- package/src/executors/github-bootstrap/schema.json +5 -0
- package/src/executors/infrastructure-plan/infrastructure-plan.d.ts +144 -0
- package/src/executors/infrastructure-plan/infrastructure-plan.js +3320 -0
- package/src/executors/infrastructure-plan/infrastructure-plan.js.map +1 -0
- package/src/executors/infrastructure-plan/schema.json +78 -0
- package/src/executors/secret-guardrails/schema.json +6 -0
- package/src/executors/secret-guardrails/secret-guardrails.d.ts +3 -0
- package/src/executors/secret-guardrails/secret-guardrails.js +44 -0
- package/src/executors/secret-guardrails/secret-guardrails.js.map +1 -0
- package/src/executors/secret-metadata/schema.json +31 -0
- package/src/executors/secret-metadata/secret-metadata.d.ts +32 -0
- package/src/executors/secret-metadata/secret-metadata.js +131 -0
- package/src/executors/secret-metadata/secret-metadata.js.map +1 -0
- package/src/executors/sst-lifecycle/logging-preflight.d.ts +17 -0
- package/src/executors/sst-lifecycle/logging-preflight.js +196 -0
- package/src/executors/sst-lifecycle/logging-preflight.js.map +1 -0
- package/src/executors/sst-lifecycle/run-sst-command.d.ts +23 -3
- package/src/executors/sst-lifecycle/run-sst-command.js +113 -10
- package/src/executors/sst-lifecycle/run-sst-command.js.map +1 -1
- package/src/executors/sst-lifecycle/schema.json +9 -5
- package/src/executors/sst-lifecycle/secret-preflight.d.ts +25 -0
- package/src/executors/sst-lifecycle/secret-preflight.js +119 -0
- package/src/executors/sst-lifecycle/secret-preflight.js.map +1 -0
- package/src/foundation/discovery.d.ts +17 -1
- package/src/foundation/discovery.js +16 -1
- package/src/foundation/discovery.js.map +1 -1
- package/src/foundation/index.d.ts +3 -0
- package/src/foundation/index.js +3 -0
- package/src/foundation/index.js.map +1 -1
- package/src/foundation/local-database-contract.d.ts +1 -0
- package/src/foundation/local-database-contract.js +3 -0
- package/src/foundation/local-database-contract.js.map +1 -1
- package/src/foundation/logging-preflight.d.ts +30 -0
- package/src/foundation/logging-preflight.js +138 -0
- package/src/foundation/logging-preflight.js.map +1 -0
- package/src/foundation/logging.d.ts +70 -0
- package/src/foundation/logging.js +133 -0
- package/src/foundation/logging.js.map +1 -0
- package/src/foundation/preflight.d.ts +9 -0
- package/src/foundation/preflight.js +58 -2
- package/src/foundation/preflight.js.map +1 -1
- package/src/foundation/provision-database-bindings.d.ts +9 -1
- package/src/foundation/provision-database-bindings.js +25 -5
- package/src/foundation/provision-database-bindings.js.map +1 -1
- package/src/foundation/provision-logical-database.d.ts +10 -2
- package/src/foundation/provision-logical-database.js +68 -113
- package/src/foundation/provision-logical-database.js.map +1 -1
- package/src/foundation/router-origin-proof.d.ts +2 -0
- package/src/foundation/router-origin-proof.js +25 -0
- package/src/foundation/router-origin-proof.js.map +1 -0
- package/src/foundation/stages.d.ts +1 -0
- package/src/foundation/stages.js +9 -1
- package/src/foundation/stages.js.map +1 -1
- package/src/frontend-security/index.d.ts +1 -0
- package/src/frontend-security/index.js +5 -0
- package/src/frontend-security/index.js.map +1 -0
- package/src/frontend-security/security-headers.d.ts +50 -0
- package/src/frontend-security/security-headers.js +418 -0
- package/src/frontend-security/security-headers.js.map +1 -0
- package/src/generators/feature-registry.d.ts +13 -0
- package/src/generators/feature-registry.js +73 -0
- package/src/generators/feature-registry.js.map +1 -0
- package/src/generators/function/function.d.ts +1 -1
- package/src/generators/function/function.js +51 -6
- package/src/generators/function/function.js.map +1 -1
- package/src/generators/function/schema.d.ts +1 -0
- package/src/generators/function/schema.json +6 -1
- package/src/generators/logging-registry.d.ts +13 -0
- package/src/generators/logging-registry.js +62 -0
- package/src/generators/logging-registry.js.map +1 -0
- package/src/generators/post-generation-install.d.ts +67 -0
- package/src/generators/post-generation-install.js +3308 -0
- package/src/generators/post-generation-install.js.map +1 -0
- package/src/generators/preset/generator.js +3 -0
- package/src/generators/preset/generator.js.map +1 -1
- package/src/generators/preset/tool-versions.d.ts +18 -0
- package/src/generators/preset/tool-versions.js +22 -0
- package/src/generators/preset/tool-versions.js.map +1 -0
- package/src/generators/preset/workspace-files-developer-agent.d.ts +14 -0
- package/src/generators/preset/workspace-files-developer-agent.js +511 -0
- package/src/generators/preset/workspace-files-developer-agent.js.map +1 -0
- package/src/generators/preset/workspace-files-foundation-workload-roles.d.ts +6 -0
- package/src/generators/preset/workspace-files-foundation-workload-roles.js +406 -0
- package/src/generators/preset/workspace-files-foundation-workload-roles.js.map +1 -0
- package/src/generators/preset/workspace-files-performance-cost.d.ts +66 -0
- package/src/generators/preset/workspace-files-performance-cost.js +866 -0
- package/src/generators/preset/workspace-files-performance-cost.js.map +1 -0
- package/src/generators/preset/workspace-files-performance-load.d.ts +73 -0
- package/src/generators/preset/workspace-files-performance-load.js +801 -0
- package/src/generators/preset/workspace-files-performance-load.js.map +1 -0
- package/src/generators/preset/workspace-files-workload-role-baseline.d.ts +9 -0
- package/src/generators/preset/workspace-files-workload-role-baseline.js +1102 -0
- package/src/generators/preset/workspace-files-workload-role-baseline.js.map +1 -0
- package/src/generators/preset/workspace-files.d.ts +14 -1
- package/src/generators/preset/workspace-files.js +8189 -245
- package/src/generators/preset/workspace-files.js.map +1 -1
- package/src/generators/repository-policy/repository-policy.js +3 -47
- package/src/generators/repository-policy/repository-policy.js.map +1 -1
- package/src/generators/slice/built-in-plan-api-contract-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js +2656 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-api-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-api-files.js +1186 -0
- package/src/generators/slice/built-in-plan-api-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-async-work-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-async-work-files.js +7903 -0
- package/src/generators/slice/built-in-plan-async-work-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-authorization-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-authorization-files.js +2314 -0
- package/src/generators/slice/built-in-plan-authorization-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js +559 -0
- package/src/generators/slice/built-in-plan-authorization-infrastructure-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-deletion-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-deletion-files.js +1101 -0
- package/src/generators/slice/built-in-plan-data-deletion-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-export-infrastructure-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js +249 -0
- package/src/generators/slice/built-in-plan-data-export-infrastructure-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-files.js +1530 -0
- package/src/generators/slice/built-in-plan-data-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-files.js +750 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js +828 -0
- package/src/generators/slice/built-in-plan-data-lifecycle-runtime-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-retention-job-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-retention-job-files.js +2604 -0
- package/src/generators/slice/built-in-plan-data-retention-job-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-retention-launcher-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-retention-launcher-files.js +144 -0
- package/src/generators/slice/built-in-plan-data-retention-launcher-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-data-rights-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-data-rights-files.js +4628 -0
- package/src/generators/slice/built-in-plan-data-rights-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1901 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js +485 -0
- package/src/generators/slice/built-in-plan-database-retirement-rehearsal-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-foundation-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-foundation-files.js +184 -0
- package/src/generators/slice/built-in-plan-foundation-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-frontend-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-frontend-files.js +2233 -0
- package/src/generators/slice/built-in-plan-frontend-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +3038 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js +312 -0
- package/src/generators/slice/built-in-plan-infrastructure-safety-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-metadata-files.d.ts +3 -0
- package/src/generators/slice/built-in-plan-metadata-files.js +219 -0
- package/src/generators/slice/built-in-plan-metadata-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-operations-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-operations-files.js +630 -0
- package/src/generators/slice/built-in-plan-operations-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-performance-files.d.ts +3 -0
- package/src/generators/slice/built-in-plan-performance-files.js +3293 -0
- package/src/generators/slice/built-in-plan-performance-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-project-files.d.ts +3 -0
- package/src/generators/slice/built-in-plan-project-files.js +979 -0
- package/src/generators/slice/built-in-plan-project-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-release-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-release-files.js +6387 -0
- package/src/generators/slice/built-in-plan-release-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-reliability-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-reliability-files.js +4499 -0
- package/src/generators/slice/built-in-plan-reliability-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-secret-runtime-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-secret-runtime-files.js +1004 -0
- package/src/generators/slice/built-in-plan-secret-runtime-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-shared.d.ts +53 -0
- package/src/generators/slice/built-in-plan-shared.js +113 -0
- package/src/generators/slice/built-in-plan-shared.js.map +1 -0
- package/src/generators/slice/built-in-plan-testing-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-testing-files.js +2356 -0
- package/src/generators/slice/built-in-plan-testing-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-verification-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-verification-files.js +358 -0
- package/src/generators/slice/built-in-plan-verification-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-web-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-web-files.js +430 -0
- package/src/generators/slice/built-in-plan-web-files.js.map +1 -0
- package/src/generators/slice/built-in-plan-workload-role-files.d.ts +2 -0
- package/src/generators/slice/built-in-plan-workload-role-files.js +2356 -0
- package/src/generators/slice/built-in-plan-workload-role-files.js.map +1 -0
- package/src/generators/slice/built-in-plan.d.ts +8 -0
- package/src/generators/slice/built-in-plan.js +264 -0
- package/src/generators/slice/built-in-plan.js.map +1 -0
- package/src/generators/slice/schema.json +1 -1
- package/src/generators/slice/slice.d.ts +1 -22
- package/src/generators/slice/slice.js +185 -1510
- package/src/generators/slice/slice.js.map +1 -1
- package/src/generators/upgrade/schema.d.ts +2 -0
- package/src/generators/upgrade/schema.json +11 -1
- package/src/generators/upgrade/upgrade.d.ts +51 -1
- package/src/generators/upgrade/upgrade.js +820 -82
- package/src/generators/upgrade/upgrade.js.map +1 -1
- package/src/generators/workload/frozen-container.d.ts +8 -0
- package/src/generators/workload/frozen-container.js +36 -0
- package/src/generators/workload/frozen-container.js.map +1 -0
- package/src/generators/workload/performance-registration.d.ts +32 -0
- package/src/generators/workload/performance-registration.js +226 -0
- package/src/generators/workload/performance-registration.js.map +1 -0
- package/src/generators/workload/reliability-registration.d.ts +16 -0
- package/src/generators/workload/reliability-registration.js +150 -0
- package/src/generators/workload/reliability-registration.js.map +1 -0
- package/src/generators/workload/schema.d.ts +1 -0
- package/src/generators/workload/schema.json +6 -1
- package/src/generators/workload/service-performance-files.d.ts +4 -0
- package/src/generators/workload/service-performance-files.js +1846 -0
- package/src/generators/workload/service-performance-files.js.map +1 -0
- package/src/generators/workload/workload.d.ts +3 -2
- package/src/generators/workload/workload.js +1083 -18
- package/src/generators/workload/workload.js.map +1 -1
- package/src/git-policy/action-pins.d.ts +10 -0
- package/src/git-policy/action-pins.js +10 -0
- package/src/git-policy/action-pins.js.map +1 -1
- package/src/git-policy/github-bootstrap.d.ts +14 -1
- package/src/git-policy/github-bootstrap.js +274 -46
- package/src/git-policy/github-bootstrap.js.map +1 -1
- package/src/index.d.ts +2 -0
- package/src/index.js +2 -0
- package/src/index.js.map +1 -1
- package/src/infrastructure-change/convergence.d.ts +13 -0
- package/src/infrastructure-change/convergence.js +85 -0
- package/src/infrastructure-change/convergence.js.map +1 -0
- package/src/infrastructure-change/drift.d.ts +13 -0
- package/src/infrastructure-change/drift.js +112 -0
- package/src/infrastructure-change/drift.js.map +1 -0
- package/src/infrastructure-change/index.d.ts +7 -0
- package/src/infrastructure-change/index.js +11 -0
- package/src/infrastructure-change/index.js.map +1 -0
- package/src/infrastructure-change/model.d.ts +253 -0
- package/src/infrastructure-change/model.js +28 -0
- package/src/infrastructure-change/model.js.map +1 -0
- package/src/infrastructure-change/normalize.d.ts +12 -0
- package/src/infrastructure-change/normalize.js +245 -0
- package/src/infrastructure-change/normalize.js.map +1 -0
- package/src/infrastructure-change/plan.d.ts +40 -0
- package/src/infrastructure-change/plan.js +361 -0
- package/src/infrastructure-change/plan.js.map +1 -0
- package/src/infrastructure-change/risk.d.ts +7 -0
- package/src/infrastructure-change/risk.js +269 -0
- package/src/infrastructure-change/risk.js.map +1 -0
- package/src/infrastructure-change/summary.d.ts +20 -0
- package/src/infrastructure-change/summary.js +130 -0
- package/src/infrastructure-change/summary.js.map +1 -0
- package/src/local-dev/context-discovery.d.ts +2 -0
- package/src/local-dev/context-discovery.js +59 -0
- package/src/local-dev/context-discovery.js.map +1 -0
- package/src/local-dev/contract-validation.d.ts +15 -0
- package/src/local-dev/contract-validation.js +84 -0
- package/src/local-dev/contract-validation.js.map +1 -0
- package/src/local-dev/contracts.d.ts +10 -1
- package/src/local-dev/contracts.js +57 -5
- package/src/local-dev/contracts.js.map +1 -1
- package/src/local-dev/control-contract.d.ts +67 -0
- package/src/local-dev/control-contract.js +180 -0
- package/src/local-dev/control-contract.js.map +1 -0
- package/src/local-dev/dev-command-core.d.ts +60 -0
- package/src/local-dev/dev-command-core.js +89 -0
- package/src/local-dev/dev-command-core.js.map +1 -0
- package/src/local-dev/effects.d.ts +16 -0
- package/src/local-dev/effects.js +63 -0
- package/src/local-dev/effects.js.map +1 -0
- package/src/local-dev/environment-inspector.d.ts +125 -0
- package/src/local-dev/environment-inspector.js +687 -0
- package/src/local-dev/environment-inspector.js.map +1 -0
- package/src/local-dev/environment-state.d.ts +113 -0
- package/src/local-dev/environment-state.js +463 -0
- package/src/local-dev/environment-state.js.map +1 -0
- package/src/local-dev/errors.d.ts +6 -0
- package/src/local-dev/errors.js +41 -0
- package/src/local-dev/errors.js.map +1 -0
- package/src/local-dev/fleet-client.d.ts +33 -0
- package/src/local-dev/fleet-client.js +156 -0
- package/src/local-dev/fleet-client.js.map +1 -0
- package/src/local-dev/fleet-configuration.d.ts +13 -0
- package/src/local-dev/fleet-configuration.js +71 -0
- package/src/local-dev/fleet-configuration.js.map +1 -0
- package/src/local-dev/fleet-inventory.d.ts +56 -0
- package/src/local-dev/fleet-inventory.js +469 -0
- package/src/local-dev/fleet-inventory.js.map +1 -0
- package/src/local-dev/fleet-ipc.d.ts +28 -0
- package/src/local-dev/fleet-ipc.js +267 -0
- package/src/local-dev/fleet-ipc.js.map +1 -0
- package/src/local-dev/fleet-launcher.d.ts +53 -0
- package/src/local-dev/fleet-launcher.js +379 -0
- package/src/local-dev/fleet-launcher.js.map +1 -0
- package/src/local-dev/fleet-lock.d.ts +41 -0
- package/src/local-dev/fleet-lock.js +135 -0
- package/src/local-dev/fleet-lock.js.map +1 -0
- package/src/local-dev/fleet-logs.d.ts +66 -0
- package/src/local-dev/fleet-logs.js +542 -0
- package/src/local-dev/fleet-logs.js.map +1 -0
- package/src/local-dev/fleet-process.d.ts +50 -0
- package/src/local-dev/fleet-process.js +289 -0
- package/src/local-dev/fleet-process.js.map +1 -0
- package/src/local-dev/fleet-runtime.d.ts +22 -0
- package/src/local-dev/fleet-runtime.js +368 -0
- package/src/local-dev/fleet-runtime.js.map +1 -0
- package/src/local-dev/fleet-session.d.ts +70 -0
- package/src/local-dev/fleet-session.js +301 -0
- package/src/local-dev/fleet-session.js.map +1 -0
- package/src/local-dev/fleet-slice-runner-daemon.d.ts +1 -0
- package/src/local-dev/fleet-slice-runner-daemon.js +21 -0
- package/src/local-dev/fleet-slice-runner-daemon.js.map +1 -0
- package/src/local-dev/fleet-startup.d.ts +22 -0
- package/src/local-dev/fleet-startup.js +114 -0
- package/src/local-dev/fleet-startup.js.map +1 -0
- package/src/local-dev/fleet-state.d.ts +62 -0
- package/src/local-dev/fleet-state.js +297 -0
- package/src/local-dev/fleet-state.js.map +1 -0
- package/src/local-dev/fleet-supervisor-daemon.d.ts +1 -0
- package/src/local-dev/fleet-supervisor-daemon.js +41 -0
- package/src/local-dev/fleet-supervisor-daemon.js.map +1 -0
- package/src/local-dev/fleet-supervisor.d.ts +52 -0
- package/src/local-dev/fleet-supervisor.js +384 -0
- package/src/local-dev/fleet-supervisor.js.map +1 -0
- package/src/local-dev/gateway.js +1 -1
- package/src/local-dev/gateway.js.map +1 -1
- package/src/local-dev/identity.js +4 -6
- package/src/local-dev/identity.js.map +1 -1
- package/src/local-dev/index.d.ts +1 -0
- package/src/local-dev/index.js +1 -0
- package/src/local-dev/index.js.map +1 -1
- package/src/local-dev/local-context.d.ts +62 -0
- package/src/local-dev/local-context.js +299 -0
- package/src/local-dev/local-context.js.map +1 -0
- package/src/local-dev/nx-workspace.d.ts +45 -0
- package/src/local-dev/nx-workspace.js +228 -0
- package/src/local-dev/nx-workspace.js.map +1 -0
- package/src/local-dev/personal-secret-ledger.d.ts +75 -0
- package/src/local-dev/personal-secret-ledger.js +391 -0
- package/src/local-dev/personal-secret-ledger.js.map +1 -0
- package/src/local-dev/personal-stage.d.ts +18 -0
- package/src/local-dev/personal-stage.js +129 -0
- package/src/local-dev/personal-stage.js.map +1 -0
- package/src/local-dev/private-state.d.ts +17 -0
- package/src/local-dev/private-state.js +254 -0
- package/src/local-dev/private-state.js.map +1 -0
- package/src/local-dev/process-identity.d.ts +9 -0
- package/src/local-dev/process-identity.js +79 -0
- package/src/local-dev/process-identity.js.map +1 -0
- package/src/local-dev/runtime-paths.d.ts +17 -0
- package/src/local-dev/runtime-paths.js +33 -0
- package/src/local-dev/runtime-paths.js.map +1 -0
- package/src/local-dev/secret-slice-claim.d.ts +17 -0
- package/src/local-dev/secret-slice-claim.js +75 -0
- package/src/local-dev/secret-slice-claim.js.map +1 -0
- package/src/local-dev/slice-claim.d.ts +36 -0
- package/src/local-dev/slice-claim.js +199 -0
- package/src/local-dev/slice-claim.js.map +1 -0
- package/src/local-dev/slice-runner.d.ts +121 -0
- package/src/local-dev/slice-runner.js +715 -0
- package/src/local-dev/slice-runner.js.map +1 -0
- package/src/local-dev/tls.js +51 -17
- package/src/local-dev/tls.js.map +1 -1
- package/src/ownership/composition.d.ts +5 -2
- package/src/ownership/composition.js +26 -3
- package/src/ownership/composition.js.map +1 -1
- package/src/ownership/hash.d.ts +1 -1
- package/src/ownership/hash.js +3 -1
- package/src/ownership/hash.js.map +1 -1
- package/src/ownership/record.d.ts +32 -1
- package/src/ownership/record.js +139 -2
- package/src/ownership/record.js.map +1 -1
- package/src/performance/cost-collection.d.ts +225 -0
- package/src/performance/cost-collection.js +825 -0
- package/src/performance/cost-collection.js.map +1 -0
- package/src/performance/cost-controls.d.ts +131 -0
- package/src/performance/cost-controls.js +336 -0
- package/src/performance/cost-controls.js.map +1 -0
- package/src/performance/evidence.d.ts +140 -0
- package/src/performance/evidence.js +599 -0
- package/src/performance/evidence.js.map +1 -0
- package/src/performance/index.d.ts +10 -0
- package/src/performance/index.js +14 -0
- package/src/performance/index.js.map +1 -0
- package/src/performance/load-safety.d.ts +84 -0
- package/src/performance/load-safety.js +346 -0
- package/src/performance/load-safety.js.map +1 -0
- package/src/performance/model.d.ts +129 -0
- package/src/performance/model.js +452 -0
- package/src/performance/model.js.map +1 -0
- package/src/performance/pr-artifact-budget.d.ts +42 -0
- package/src/performance/pr-artifact-budget.js +151 -0
- package/src/performance/pr-artifact-budget.js.map +1 -0
- package/src/performance/runner.d.ts +75 -0
- package/src/performance/runner.js +227 -0
- package/src/performance/runner.js.map +1 -0
- package/src/performance/scenario-endpoint.d.ts +1 -0
- package/src/performance/scenario-endpoint.js +5 -0
- package/src/performance/scenario-endpoint.js.map +1 -0
- package/src/performance/scenario.d.ts +193 -0
- package/src/performance/scenario.js +736 -0
- package/src/performance/scenario.js.map +1 -0
- package/src/performance/test-fixtures.d.ts +7 -0
- package/src/performance/test-fixtures.js +169 -0
- package/src/performance/test-fixtures.js.map +1 -0
- package/src/performance/unit-cost.d.ts +105 -0
- package/src/performance/unit-cost.js +364 -0
- package/src/performance/unit-cost.js.map +1 -0
- package/src/release/index.d.ts +4 -0
- package/src/release/index.js +8 -0
- package/src/release/index.js.map +1 -0
- package/src/release/lifecycle.d.ts +132 -0
- package/src/release/lifecycle.js +411 -0
- package/src/release/lifecycle.js.map +1 -0
- package/src/release/manifest.d.ts +148 -0
- package/src/release/manifest.js +503 -0
- package/src/release/manifest.js.map +1 -0
- package/src/release/rollout.d.ts +170 -0
- package/src/release/rollout.js +578 -0
- package/src/release/rollout.js.map +1 -0
- package/src/release/source-authority.d.ts +19 -0
- package/src/release/source-authority.js +195 -0
- package/src/release/source-authority.js.map +1 -0
- package/src/repository-policy/deployment-plan.d.ts +2 -1
- package/src/repository-policy/deployment-plan.js +2 -0
- package/src/repository-policy/deployment-plan.js.map +1 -1
- package/src/repository-policy/policy.d.ts +1 -1
- package/src/repository-policy/policy.js +29 -25
- package/src/repository-policy/policy.js.map +1 -1
- package/src/schemas/aggregate-count-evidence-v1.json +135 -0
- package/src/schemas/api-contract-baseline-v1.json +674 -0
- package/src/schemas/api-contract-compatibility-report-v1.json +126 -0
- package/src/schemas/aws-billing-actual-evidence-v1.json +407 -0
- package/src/schemas/blueprint-v1.json +18 -1
- package/src/schemas/data-lifecycle-policy-v1.json +517 -0
- package/src/schemas/generated-ownership-v2.json +22 -0
- package/src/schemas/performance-policy-v1.json +335 -0
- package/src/schemas/repository-policy-v1.json +1 -0
- package/src/schemas/testing-evidence-quarantine-v1.json +168 -0
- package/src/schemas/testing-evidence-result-v1.json +319 -0
- package/src/secrets/adapters.d.ts +49 -0
- package/src/secrets/adapters.js +184 -0
- package/src/secrets/adapters.js.map +1 -0
- package/src/secrets/aws-control-plane.d.ts +20 -0
- package/src/secrets/aws-control-plane.js +255 -0
- package/src/secrets/aws-control-plane.js.map +1 -0
- package/src/secrets/declaration-verification.d.ts +11 -0
- package/src/secrets/declaration-verification.js +68 -0
- package/src/secrets/declaration-verification.js.map +1 -0
- package/src/secrets/guardrails.d.ts +26 -0
- package/src/secrets/guardrails.js +328 -0
- package/src/secrets/guardrails.js.map +1 -0
- package/src/secrets/index.d.ts +12 -0
- package/src/secrets/index.js +16 -0
- package/src/secrets/index.js.map +1 -0
- package/src/secrets/metadata.d.ts +48 -0
- package/src/secrets/metadata.js +171 -0
- package/src/secrets/metadata.js.map +1 -0
- package/src/secrets/model.d.ts +49 -0
- package/src/secrets/model.js +125 -0
- package/src/secrets/model.js.map +1 -0
- package/src/secrets/operator.d.ts +101 -0
- package/src/secrets/operator.js +229 -0
- package/src/secrets/operator.js.map +1 -0
- package/src/secrets/permissions.d.ts +34 -0
- package/src/secrets/permissions.js +86 -0
- package/src/secrets/permissions.js.map +1 -0
- package/src/secrets/personal-command.d.ts +83 -0
- package/src/secrets/personal-command.js +197 -0
- package/src/secrets/personal-command.js.map +1 -0
- package/src/secrets/personal-ledger.d.ts +21 -0
- package/src/secrets/personal-ledger.js +135 -0
- package/src/secrets/personal-ledger.js.map +1 -0
- package/src/secrets/registry.d.ts +9 -0
- package/src/secrets/registry.js +86 -0
- package/src/secrets/registry.js.map +1 -0
- package/src/secrets/verification.d.ts +18 -0
- package/src/secrets/verification.js +139 -0
- package/src/secrets/verification.js.map +1 -0
- package/src/secrets/workload-access.d.ts +46 -0
- package/src/secrets/workload-access.js +163 -0
- package/src/secrets/workload-access.js.map +1 -0
- package/src/supply-chain/action-pins.d.ts +26 -0
- package/src/supply-chain/action-pins.js +208 -0
- package/src/supply-chain/action-pins.js.map +1 -0
- package/src/supply-chain/blueprint-trust-git.d.ts +130 -0
- package/src/supply-chain/blueprint-trust-git.js +1581 -0
- package/src/supply-chain/blueprint-trust-git.js.map +1 -0
- package/src/supply-chain/blueprint-trust-process.d.ts +4 -0
- package/src/supply-chain/blueprint-trust-process.js +88 -0
- package/src/supply-chain/blueprint-trust-process.js.map +1 -0
- package/src/supply-chain/blueprint-trust.d.ts +354 -0
- package/src/supply-chain/blueprint-trust.js +1558 -0
- package/src/supply-chain/blueprint-trust.js.map +1 -0
- package/src/supply-chain/blueprint-verification-authority.d.ts +62 -0
- package/src/supply-chain/blueprint-verification-authority.js +588 -0
- package/src/supply-chain/blueprint-verification-authority.js.map +1 -0
- package/src/supply-chain/blueprint-verification-execution.d.ts +68 -0
- package/src/supply-chain/blueprint-verification-execution.js +624 -0
- package/src/supply-chain/blueprint-verification-execution.js.map +1 -0
- package/src/supply-chain/build-output.d.ts +35 -0
- package/src/supply-chain/build-output.js +398 -0
- package/src/supply-chain/build-output.js.map +1 -0
- package/src/supply-chain/common.d.ts +21 -0
- package/src/supply-chain/common.js +107 -0
- package/src/supply-chain/common.js.map +1 -0
- package/src/supply-chain/container-artifact-contract.d.ts +23 -0
- package/src/supply-chain/container-artifact-contract.js +208 -0
- package/src/supply-chain/container-artifact-contract.js.map +1 -0
- package/src/supply-chain/container-release.d.ts +119 -0
- package/src/supply-chain/container-release.js +496 -0
- package/src/supply-chain/container-release.js.map +1 -0
- package/src/supply-chain/ecr-applicability.d.ts +19 -0
- package/src/supply-chain/ecr-applicability.js +103 -0
- package/src/supply-chain/ecr-applicability.js.map +1 -0
- package/src/supply-chain/ecr-scanning.d.ts +83 -0
- package/src/supply-chain/ecr-scanning.js +321 -0
- package/src/supply-chain/ecr-scanning.js.map +1 -0
- package/src/supply-chain/ecs-deployed-image.d.ts +85 -0
- package/src/supply-chain/ecs-deployed-image.js +300 -0
- package/src/supply-chain/ecs-deployed-image.js.map +1 -0
- package/src/supply-chain/errors.d.ts +6 -0
- package/src/supply-chain/errors.js +15 -0
- package/src/supply-chain/errors.js.map +1 -0
- package/src/supply-chain/generated-blueprint-trust-command.d.ts +4 -0
- package/src/supply-chain/generated-blueprint-trust-command.js +1362 -0
- package/src/supply-chain/generated-blueprint-trust-command.js.map +1 -0
- package/src/supply-chain/generated-command.d.ts +1 -0
- package/src/supply-chain/generated-command.js +687 -0
- package/src/supply-chain/generated-command.js.map +1 -0
- package/src/supply-chain/generated-container-release-command.d.ts +16 -0
- package/src/supply-chain/generated-container-release-command.js +2087 -0
- package/src/supply-chain/generated-container-release-command.js.map +1 -0
- package/src/supply-chain/generated-ecr-collector-command.d.ts +21 -0
- package/src/supply-chain/generated-ecr-collector-command.js +1338 -0
- package/src/supply-chain/generated-ecr-collector-command.js.map +1 -0
- package/src/supply-chain/generated-files.d.ts +13 -0
- package/src/supply-chain/generated-files.js +401 -0
- package/src/supply-chain/generated-files.js.map +1 -0
- package/src/supply-chain/generated-scanner-command.d.ts +1 -0
- package/src/supply-chain/generated-scanner-command.js +718 -0
- package/src/supply-chain/generated-scanner-command.js.map +1 -0
- package/src/supply-chain/index.d.ts +28 -0
- package/src/supply-chain/index.js +35 -0
- package/src/supply-chain/index.js.map +1 -0
- package/src/supply-chain/inventory.d.ts +79 -0
- package/src/supply-chain/inventory.js +717 -0
- package/src/supply-chain/inventory.js.map +1 -0
- package/src/supply-chain/license-expression.d.ts +7 -0
- package/src/supply-chain/license-expression.js +128 -0
- package/src/supply-chain/license-expression.js.map +1 -0
- package/src/supply-chain/pnpm-toolchain-authority.d.ts +31 -0
- package/src/supply-chain/pnpm-toolchain-authority.js +316 -0
- package/src/supply-chain/pnpm-toolchain-authority.js.map +1 -0
- package/src/supply-chain/policy.d.ts +105 -0
- package/src/supply-chain/policy.js +301 -0
- package/src/supply-chain/policy.js.map +1 -0
- package/src/supply-chain/publication.d.ts +48 -0
- package/src/supply-chain/publication.js +85 -0
- package/src/supply-chain/publication.js.map +1 -0
- package/src/supply-chain/sbom.d.ts +228 -0
- package/src/supply-chain/sbom.js +826 -0
- package/src/supply-chain/sbom.js.map +1 -0
- package/src/supply-chain/scanner.d.ts +77 -0
- package/src/supply-chain/scanner.js +502 -0
- package/src/supply-chain/scanner.js.map +1 -0
- package/src/supply-chain/vendored-source.d.ts +95 -0
- package/src/supply-chain/vendored-source.js +315 -0
- package/src/supply-chain/vendored-source.js.map +1 -0
- package/src/testing-evidence/fixture-scope.d.ts +52 -0
- package/src/testing-evidence/fixture-scope.js +140 -0
- package/src/testing-evidence/fixture-scope.js.map +1 -0
- package/src/testing-evidence/index.d.ts +5 -0
- package/src/testing-evidence/index.js +9 -0
- package/src/testing-evidence/index.js.map +1 -0
- package/src/testing-evidence/quarantine.d.ts +48 -0
- package/src/testing-evidence/quarantine.js +253 -0
- package/src/testing-evidence/quarantine.js.map +1 -0
- package/src/testing-evidence/result.d.ts +108 -0
- package/src/testing-evidence/result.js +571 -0
- package/src/testing-evidence/result.js.map +1 -0
- package/src/testing-evidence/runner-adapter.d.ts +51 -0
- package/src/testing-evidence/runner-adapter.js +288 -0
- package/src/testing-evidence/runner-adapter.js.map +1 -0
- package/src/testing-evidence/sensitive-artifacts.d.ts +13 -0
- package/src/testing-evidence/sensitive-artifacts.js +93 -0
- package/src/testing-evidence/sensitive-artifacts.js.map +1 -0
- package/src/work-markers/work-markers.js +4 -3
- package/src/work-markers/work-markers.js.map +1 -1
- package/src/workload-roles/account-baseline.d.ts +132 -0
- package/src/workload-roles/account-baseline.js +743 -0
- package/src/workload-roles/account-baseline.js.map +1 -0
- package/src/workload-roles/authority.d.ts +38 -0
- package/src/workload-roles/authority.js +156 -0
- package/src/workload-roles/authority.js.map +1 -0
- package/src/workload-roles/aws-adapter.d.ts +99 -0
- package/src/workload-roles/aws-adapter.js +416 -0
- package/src/workload-roles/aws-adapter.js.map +1 -0
- package/src/workload-roles/aws-internal-handler.d.ts +27 -0
- package/src/workload-roles/aws-internal-handler.js +58 -0
- package/src/workload-roles/aws-internal-handler.js.map +1 -0
- package/src/workload-roles/aws-s3-state.d.ts +52 -0
- package/src/workload-roles/aws-s3-state.js +164 -0
- package/src/workload-roles/aws-s3-state.js.map +1 -0
- package/src/workload-roles/aws-sdk-control-plane.d.ts +12 -0
- package/src/workload-roles/aws-sdk-control-plane.js +291 -0
- package/src/workload-roles/aws-sdk-control-plane.js.map +1 -0
- package/src/workload-roles/boundary.d.ts +33 -0
- package/src/workload-roles/boundary.js +130 -0
- package/src/workload-roles/boundary.js.map +1 -0
- package/src/workload-roles/convergence.d.ts +66 -0
- package/src/workload-roles/convergence.js +186 -0
- package/src/workload-roles/convergence.js.map +1 -0
- package/src/workload-roles/effective-policy.d.ts +51 -0
- package/src/workload-roles/effective-policy.js +183 -0
- package/src/workload-roles/effective-policy.js.map +1 -0
- package/src/workload-roles/handler.d.ts +43 -0
- package/src/workload-roles/handler.js +130 -0
- package/src/workload-roles/handler.js.map +1 -0
- package/src/workload-roles/history.d.ts +47 -0
- package/src/workload-roles/history.js +324 -0
- package/src/workload-roles/history.js.map +1 -0
- package/src/workload-roles/index.d.ts +17 -0
- package/src/workload-roles/index.js +21 -0
- package/src/workload-roles/index.js.map +1 -0
- package/src/workload-roles/injection.d.ts +119 -0
- package/src/workload-roles/injection.js +495 -0
- package/src/workload-roles/injection.js.map +1 -0
- package/src/workload-roles/manifest.d.ts +54 -0
- package/src/workload-roles/manifest.js +343 -0
- package/src/workload-roles/manifest.js.map +1 -0
- package/src/workload-roles/provisioner.d.ts +70 -0
- package/src/workload-roles/provisioner.js +231 -0
- package/src/workload-roles/provisioner.js.map +1 -0
- package/src/workload-roles/registration.d.ts +101 -0
- package/src/workload-roles/registration.js +457 -0
- package/src/workload-roles/registration.js.map +1 -0
- package/src/workload-roles/stage-boundary.d.ts +31 -0
- package/src/workload-roles/stage-boundary.js +208 -0
- package/src/workload-roles/stage-boundary.js.map +1 -0
- package/src/workload-roles/standard-slice.d.ts +54 -0
- package/src/workload-roles/standard-slice.js +252 -0
- package/src/workload-roles/standard-slice.js.map +1 -0
- package/migrations/standard-pre-v1-to-v1.md +0 -11
- package/src/foundation/aurora-logical-database.d.ts +0 -76
- package/src/foundation/aurora-logical-database.js +0 -419
- package/src/foundation/aurora-logical-database.js.map +0 -1
|
@@ -0,0 +1,3320 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractPrivateImportIdentifierDigests = extractPrivateImportIdentifierDigests;
|
|
4
|
+
exports.approvalCandidateSourceCommit = approvalCandidateSourceCommit;
|
|
5
|
+
exports.createCandidateBoundSstChildEnvironment = createCandidateBoundSstChildEnvironment;
|
|
6
|
+
exports.assessPulumiRefreshPreview = assessPulumiRefreshPreview;
|
|
7
|
+
exports.standardRemoveProbeForResource = standardRemoveProbeForResource;
|
|
8
|
+
exports.probeStandardRemoveResource = probeStandardRemoveResource;
|
|
9
|
+
exports.observeInfrastructureState = observeInfrastructureState;
|
|
10
|
+
exports.validateGovernanceEvidence = validateGovernanceEvidence;
|
|
11
|
+
exports.runInfrastructurePlanExecutor = runInfrastructurePlanExecutor;
|
|
12
|
+
exports.revalidateInfrastructureMutationAuthorization = revalidateInfrastructureMutationAuthorization;
|
|
13
|
+
exports.recordInfrastructureMutationConvergence = recordInfrastructureMutationConvergence;
|
|
14
|
+
exports.finalizeInfrastructureMutationConvergence = finalizeInfrastructureMutationConvergence;
|
|
15
|
+
const deterministic_order_js_1 = require("../../deterministic-order.js");
|
|
16
|
+
const node_fs_1 = require("node:fs");
|
|
17
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
|
+
const node_child_process_1 = require("node:child_process");
|
|
19
|
+
const node_path_1 = require("node:path");
|
|
20
|
+
const node_os_1 = require("node:os");
|
|
21
|
+
const devkit_1 = require("@nx/devkit");
|
|
22
|
+
const canonical_json_1 = require("@empire-builder-kit/runtime/canonical-json");
|
|
23
|
+
const index_js_1 = require("../../infrastructure-change/index.js");
|
|
24
|
+
const index_js_2 = require("../../repository-policy/index.js");
|
|
25
|
+
const run_sst_command_js_1 = require("../sst-lifecycle/run-sst-command.js");
|
|
26
|
+
function repositoryInfrastructureApprovalMode(context, stage) {
|
|
27
|
+
return stage === 'production'
|
|
28
|
+
? (0, index_js_2.readRepositoryOperatingPolicy)(context.root).productionApproval.mode
|
|
29
|
+
: 'promotion-pr';
|
|
30
|
+
}
|
|
31
|
+
function projectEvidence(context, projects) {
|
|
32
|
+
return Object.fromEntries(projects.map((project) => {
|
|
33
|
+
const configured = context.projectsConfigurations?.projects[project]?.root;
|
|
34
|
+
if (!configured) {
|
|
35
|
+
throw new Error(`Infrastructure plan summary references unknown project ${project}.`);
|
|
36
|
+
}
|
|
37
|
+
const evidenceDirectory = (0, node_path_1.join)((0, node_path_1.isAbsolute)(configured) ? configured : (0, node_path_1.join)(context.root, configured), '.ebk', 'evidence');
|
|
38
|
+
return [
|
|
39
|
+
project,
|
|
40
|
+
{
|
|
41
|
+
authorization: record(readJsonFile((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), `${project} infrastructure authorization`), `${project} infrastructure authorization`),
|
|
42
|
+
plan: record(readJsonFile((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), `${project} infrastructure plan`), `${project} infrastructure plan`),
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
48
|
+
const SOURCE_COMMIT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
49
|
+
const MAX_JSON_BYTES = 5 * 1024 * 1024;
|
|
50
|
+
const STANDARD_REMOVE_CLOUD_CONTROL_TYPES = {
|
|
51
|
+
'aws:acm/certificate:Certificate': 'AWS::CertificateManager::Certificate',
|
|
52
|
+
'aws:apigatewayv2/api:Api': 'AWS::ApiGatewayV2::Api',
|
|
53
|
+
'aws:apigatewayv2/apiMapping:ApiMapping': 'AWS::ApiGatewayV2::ApiMapping',
|
|
54
|
+
'aws:apigatewayv2/authorizer:Authorizer': 'AWS::ApiGatewayV2::Authorizer',
|
|
55
|
+
'aws:apigatewayv2/domainName:DomainName': 'AWS::ApiGatewayV2::DomainName',
|
|
56
|
+
'aws:apigatewayv2/integration:Integration': 'AWS::ApiGatewayV2::Integration',
|
|
57
|
+
'aws:apigatewayv2/route:Route': 'AWS::ApiGatewayV2::Route',
|
|
58
|
+
'aws:apigatewayv2/stage:Stage': 'AWS::ApiGatewayV2::Stage',
|
|
59
|
+
'aws:appconfig/application:Application': 'AWS::AppConfig::Application',
|
|
60
|
+
'aws:appconfig/configurationProfile:ConfigurationProfile': 'AWS::AppConfig::ConfigurationProfile',
|
|
61
|
+
'aws:appconfig/deployment:Deployment': 'AWS::AppConfig::Deployment',
|
|
62
|
+
'aws:appconfig/deploymentStrategy:DeploymentStrategy': 'AWS::AppConfig::DeploymentStrategy',
|
|
63
|
+
'aws:appconfig/environment:Environment': 'AWS::AppConfig::Environment',
|
|
64
|
+
'aws:appconfig/hostedConfigurationVersion:HostedConfigurationVersion': 'AWS::AppConfig::HostedConfigurationVersion',
|
|
65
|
+
'aws:cloudformation/stack:Stack': 'AWS::CloudFormation::Stack',
|
|
66
|
+
'aws:cloudfront/cachePolicy:CachePolicy': 'AWS::CloudFront::CachePolicy',
|
|
67
|
+
'aws:cloudfront/distribution:Distribution': 'AWS::CloudFront::Distribution',
|
|
68
|
+
'aws:cloudfront/function:Function': 'AWS::CloudFront::Function',
|
|
69
|
+
'aws:cloudfront/originAccessControl:OriginAccessControl': 'AWS::CloudFront::OriginAccessControl',
|
|
70
|
+
'aws:cloudfront/originRequestPolicy:OriginRequestPolicy': 'AWS::CloudFront::OriginRequestPolicy',
|
|
71
|
+
'aws:cloudfront/responseHeadersPolicy:ResponseHeadersPolicy': 'AWS::CloudFront::ResponseHeadersPolicy',
|
|
72
|
+
'aws:cloudwatch/dashboard:Dashboard': 'AWS::CloudWatch::Dashboard',
|
|
73
|
+
'aws:cloudwatch/eventRule:EventRule': 'AWS::Events::Rule',
|
|
74
|
+
'aws:cloudwatch/logGroup:LogGroup': 'AWS::Logs::LogGroup',
|
|
75
|
+
'aws:cloudwatch/metricAlarm:MetricAlarm': 'AWS::CloudWatch::Alarm',
|
|
76
|
+
'aws:cognito/userPool:UserPool': 'AWS::Cognito::UserPool',
|
|
77
|
+
'aws:cognito/userPoolClient:UserPoolClient': 'AWS::Cognito::UserPoolClient',
|
|
78
|
+
'aws:dynamodb/table:Table': 'AWS::DynamoDB::Table',
|
|
79
|
+
'aws:ec2/eip:Eip': 'AWS::EC2::EIP',
|
|
80
|
+
'aws:ec2/internetGateway:InternetGateway': 'AWS::EC2::InternetGateway',
|
|
81
|
+
'aws:ec2/natGateway:NatGateway': 'AWS::EC2::NatGateway',
|
|
82
|
+
'aws:ec2/route:Route': 'AWS::EC2::Route',
|
|
83
|
+
'aws:ec2/routeTable:RouteTable': 'AWS::EC2::RouteTable',
|
|
84
|
+
'aws:ec2/securityGroup:SecurityGroup': 'AWS::EC2::SecurityGroup',
|
|
85
|
+
'aws:ec2/subnet:Subnet': 'AWS::EC2::Subnet',
|
|
86
|
+
'aws:ec2/vpc:Vpc': 'AWS::EC2::VPC',
|
|
87
|
+
'aws:ecr/repository:Repository': 'AWS::ECR::Repository',
|
|
88
|
+
'aws:ecs/cluster:Cluster': 'AWS::ECS::Cluster',
|
|
89
|
+
'aws:ecs/service:Service': 'AWS::ECS::Service',
|
|
90
|
+
'aws:ecs/taskDefinition:TaskDefinition': 'AWS::ECS::TaskDefinition',
|
|
91
|
+
'aws:events/archive:Archive': 'AWS::Events::Archive',
|
|
92
|
+
'aws:events/eventBus:EventBus': 'AWS::Events::EventBus',
|
|
93
|
+
'aws:events/rule:Rule': 'AWS::Events::Rule',
|
|
94
|
+
'aws:iam/instanceProfile:InstanceProfile': 'AWS::IAM::InstanceProfile',
|
|
95
|
+
'aws:iam/managedPolicy:ManagedPolicy': 'AWS::IAM::ManagedPolicy',
|
|
96
|
+
'aws:iam/policy:Policy': 'AWS::IAM::ManagedPolicy',
|
|
97
|
+
'aws:iam/role:Role': 'AWS::IAM::Role',
|
|
98
|
+
'aws:lambda/function:Function': 'AWS::Lambda::Function',
|
|
99
|
+
'aws:lambda/layerVersion:LayerVersion': 'AWS::Lambda::LayerVersion',
|
|
100
|
+
'aws:lb/loadBalancer:LoadBalancer': 'AWS::ElasticLoadBalancingV2::LoadBalancer',
|
|
101
|
+
'aws:lb/listener:Listener': 'AWS::ElasticLoadBalancingV2::Listener',
|
|
102
|
+
'aws:lb/targetGroup:TargetGroup': 'AWS::ElasticLoadBalancingV2::TargetGroup',
|
|
103
|
+
'aws:rds/cluster:Cluster': 'AWS::RDS::DBCluster',
|
|
104
|
+
'aws:rds/clusterInstance:ClusterInstance': 'AWS::RDS::DBInstance',
|
|
105
|
+
'aws:rds/subnetGroup:SubnetGroup': 'AWS::RDS::DBSubnetGroup',
|
|
106
|
+
'aws:sfn/stateMachine:StateMachine': 'AWS::StepFunctions::StateMachine',
|
|
107
|
+
'aws:sns/topic:Topic': 'AWS::SNS::Topic',
|
|
108
|
+
'aws:sqs/queue:Queue': 'AWS::SQS::Queue',
|
|
109
|
+
};
|
|
110
|
+
function digest(value) {
|
|
111
|
+
return (0, node_crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
|
|
112
|
+
}
|
|
113
|
+
function requiredEnvironment(environment, name, pattern) {
|
|
114
|
+
const value = environment[name]?.trim();
|
|
115
|
+
if (!value || (pattern && !pattern.test(value))) {
|
|
116
|
+
throw new Error(`${name} is missing or invalid.`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function parseJson(value, label) {
|
|
121
|
+
if (Buffer.byteLength(value, 'utf8') > MAX_JSON_BYTES) {
|
|
122
|
+
throw new Error(`${label} exceeds the 5 MiB metadata limit.`);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(value);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new Error(`${label} is not valid JSON.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function record(value, label) {
|
|
132
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
133
|
+
throw new Error(`${label} must be one JSON object.`);
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function requireExactKeys(value, keys, label) {
|
|
138
|
+
if (JSON.stringify(Object.keys(value).sort()) !==
|
|
139
|
+
JSON.stringify([...keys].sort())) {
|
|
140
|
+
throw new Error(`${label} has an invalid exact field set.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function infrastructureResourceKey(input) {
|
|
144
|
+
return `${input.logicalId}\u0000${input.resourceType}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Reads the Pulumi StepEventStateMetadata `new.id` emitted by SST's pinned
|
|
148
|
+
* JSON diff for imports. Only its digest leaves this private boundary.
|
|
149
|
+
*/
|
|
150
|
+
function extractPrivateImportIdentifierDigests(diffStdout, diff) {
|
|
151
|
+
const imports = diff.resources.filter(({ operation }) => operation === 'import');
|
|
152
|
+
if (imports.length === 0)
|
|
153
|
+
return {};
|
|
154
|
+
const raw = parseJson(diffStdout, 'SST import diff');
|
|
155
|
+
if (!Array.isArray(raw)) {
|
|
156
|
+
throw new Error('SST import diff does not expose an exact private import identifier.');
|
|
157
|
+
}
|
|
158
|
+
const identifiers = {};
|
|
159
|
+
for (const resource of imports) {
|
|
160
|
+
const entry = raw[resource.sourceIndex];
|
|
161
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
162
|
+
throw new Error(`Infrastructure import ${resource.logicalId} does not expose an exact private import identifier.`);
|
|
163
|
+
}
|
|
164
|
+
const next = entry['new'];
|
|
165
|
+
if (typeof next !== 'object' || next === null || Array.isArray(next)) {
|
|
166
|
+
throw new Error(`Infrastructure import ${resource.logicalId} does not expose an exact private import identifier.`);
|
|
167
|
+
}
|
|
168
|
+
const identifier = next['id'];
|
|
169
|
+
if (typeof identifier !== 'string' || identifier.trim().length === 0) {
|
|
170
|
+
throw new Error(`Infrastructure import ${resource.logicalId} does not expose an exact private import identifier.`);
|
|
171
|
+
}
|
|
172
|
+
const key = infrastructureResourceKey(resource);
|
|
173
|
+
if (identifiers[key]) {
|
|
174
|
+
throw new Error(`Infrastructure import ${resource.logicalId} has an ambiguous private import identifier.`);
|
|
175
|
+
}
|
|
176
|
+
identifiers[key] = digest(identifier);
|
|
177
|
+
}
|
|
178
|
+
return Object.fromEntries(Object.entries(identifiers).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right)));
|
|
179
|
+
}
|
|
180
|
+
function stringArray(value, label) {
|
|
181
|
+
if (!Array.isArray(value) ||
|
|
182
|
+
value.some((entry) => typeof entry !== 'string' || entry.length === 0)) {
|
|
183
|
+
throw new Error(`${label} must be an array of non-empty strings.`);
|
|
184
|
+
}
|
|
185
|
+
return [...new Set(value)].sort();
|
|
186
|
+
}
|
|
187
|
+
function readJsonFile(path, label) {
|
|
188
|
+
return parseJson((0, node_fs_1.readFileSync)(path, 'utf8'), label);
|
|
189
|
+
}
|
|
190
|
+
function projectRoot(context, options) {
|
|
191
|
+
const configured = options.cwd ??
|
|
192
|
+
(context.projectName
|
|
193
|
+
? context.projectsConfigurations?.projects[context.projectName]?.root
|
|
194
|
+
: undefined) ??
|
|
195
|
+
context.root;
|
|
196
|
+
return (0, node_path_1.isAbsolute)(configured) ? configured : (0, node_path_1.join)(context.root, configured);
|
|
197
|
+
}
|
|
198
|
+
function relativeFile(root, value) {
|
|
199
|
+
if ((0, node_path_1.isAbsolute)(value)) {
|
|
200
|
+
throw new Error('Infrastructure metadata paths must be project-relative.');
|
|
201
|
+
}
|
|
202
|
+
const absoluteRoot = (0, node_path_1.resolve)(root);
|
|
203
|
+
const result = (0, node_path_1.resolve)(absoluteRoot, value);
|
|
204
|
+
const relativePath = (0, node_path_1.relative)(absoluteRoot, result);
|
|
205
|
+
if (relativePath === '..' ||
|
|
206
|
+
relativePath.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) ||
|
|
207
|
+
(0, node_path_1.isAbsolute)(relativePath)) {
|
|
208
|
+
throw new Error('Infrastructure metadata paths must remain in the project.');
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
function readDescriptor(path, project) {
|
|
213
|
+
const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
214
|
+
const descriptor = record(parseJson(raw, 'Infrastructure descriptor'), 'Infrastructure descriptor');
|
|
215
|
+
const descriptorProject = descriptor.project ?? descriptor.slice;
|
|
216
|
+
if (descriptor.schemaVersion !== 1 ||
|
|
217
|
+
descriptorProject !== project ||
|
|
218
|
+
descriptor.planner?.version !== index_js_1.SST_DIFF_TOOL_VERSION) {
|
|
219
|
+
throw new Error(`Infrastructure descriptor must bind ${project} to SST ${index_js_1.SST_DIFF_TOOL_VERSION}.`);
|
|
220
|
+
}
|
|
221
|
+
const policy = descriptor.policy;
|
|
222
|
+
if (!policy) {
|
|
223
|
+
throw new Error('Infrastructure descriptor omitted its risk policy.');
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
policy: {
|
|
227
|
+
additiveLowRiskTypes: stringArray(policy.additiveLowRiskTypes, 'policy.additiveLowRiskTypes'),
|
|
228
|
+
costOrQuotaTypes: stringArray(policy.costOrQuotaTypes, 'policy.costOrQuotaTypes'),
|
|
229
|
+
statefulTypes: stringArray(policy.statefulTypes, 'policy.statefulTypes'),
|
|
230
|
+
},
|
|
231
|
+
raw,
|
|
232
|
+
standingPolicyRiskClasses: stringArray(policy.standingPolicyRiskClasses, 'policy.standingPolicyRiskClasses'),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function readOptionalArray(environment, name) {
|
|
236
|
+
const path = environment[name]?.trim();
|
|
237
|
+
if (!path)
|
|
238
|
+
return [];
|
|
239
|
+
const value = readJsonFile(path, name);
|
|
240
|
+
if (!Array.isArray(value)) {
|
|
241
|
+
throw new Error(`${name} must reference one JSON array.`);
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
function planAuthorityPolicyDigest(environment) {
|
|
246
|
+
const parseArray = (name) => {
|
|
247
|
+
const raw = requiredEnvironment(environment, name);
|
|
248
|
+
if (Buffer.byteLength(raw, 'utf8') > 128 * 1024) {
|
|
249
|
+
throw new Error(`${name} exceeds the bounded authority metadata size.`);
|
|
250
|
+
}
|
|
251
|
+
const value = parseJson(raw, name);
|
|
252
|
+
if (Array.isArray(value) && value.length > 0)
|
|
253
|
+
return value;
|
|
254
|
+
throw new Error(`${name} must contain one non-empty exact JSON array.`);
|
|
255
|
+
};
|
|
256
|
+
const providerPolicy = parseArray('AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON');
|
|
257
|
+
if (providerPolicy.some((statement) => typeof statement !== 'object' ||
|
|
258
|
+
statement === null ||
|
|
259
|
+
Array.isArray(statement))) {
|
|
260
|
+
throw new Error('AWS_PLAN_PROVIDER_REFRESH_POLICY_JSON must contain exact policy statements.');
|
|
261
|
+
}
|
|
262
|
+
return digest(canonicalJson({
|
|
263
|
+
applications: parseArray('AWS_PLAN_SST_APPS_JSON'),
|
|
264
|
+
generatedObjectArns: parseArray('AWS_PLAN_GENERATED_OBJECT_ARNS_JSON'),
|
|
265
|
+
generatedParameterArns: parseArray('AWS_PLAN_GENERATED_PARAMETER_ARNS_JSON'),
|
|
266
|
+
generatedSecretArns: parseArray('AWS_PLAN_GENERATED_SECRET_ARNS_JSON'),
|
|
267
|
+
planRoleArn: requiredEnvironment(environment, 'AWS_PLAN_ROLE_ARN', /^arn:aws:iam::\d{12}:role\/[A-Za-z0-9+=,.@_/-]{1,512}$/),
|
|
268
|
+
providerPolicy,
|
|
269
|
+
stateBucketArn: requiredEnvironment(environment, 'AWS_PLAN_STATE_BUCKET_ARN', /^arn:aws:s3:::[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/),
|
|
270
|
+
stateKmsKeyArn: requiredEnvironment(environment, 'AWS_PLAN_STATE_KMS_KEY_ARN', /^arn:aws:kms:[a-z0-9-]+:\d{12}:key\/[A-Za-z0-9-]+$/),
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
function frameworkVersion(root) {
|
|
274
|
+
const value = record(readJsonFile((0, node_path_1.join)(root, 'node_modules', '@empire-builder-kit', 'nx', 'package.json'), '@empire-builder-kit/nx package metadata'), '@empire-builder-kit/nx package metadata');
|
|
275
|
+
if (typeof value['version'] !== 'string' || !value['version']) {
|
|
276
|
+
throw new Error('Installed framework version is missing.');
|
|
277
|
+
}
|
|
278
|
+
return value['version'];
|
|
279
|
+
}
|
|
280
|
+
function releaseCandidateBinding(input) {
|
|
281
|
+
const path = (0, node_path_1.join)(input.workspaceRoot, '.ebk', 'evidence', 'release-candidate', 'packages', input.project, 'candidate.json');
|
|
282
|
+
if (!(0, node_fs_1.existsSync)(path)) {
|
|
283
|
+
throw new Error(`Exact immutable release candidate is absent for ${input.project}.`);
|
|
284
|
+
}
|
|
285
|
+
const raw = (0, node_fs_1.readFileSync)(path, 'utf8');
|
|
286
|
+
const candidate = record(parseJson(raw, `${input.project} release candidate`), `${input.project} release candidate`);
|
|
287
|
+
const provenance = record(candidate['provenance'], `${input.project} release candidate provenance`);
|
|
288
|
+
if (candidate['kind'] !== 'ebk-release-candidate' ||
|
|
289
|
+
candidate['schemaVersion'] !== 1 ||
|
|
290
|
+
candidate['slice'] !== input.project ||
|
|
291
|
+
candidate['sourceCommit'] !== input.sourceCommit ||
|
|
292
|
+
provenance['kind'] !== 'ebk-release-candidate-provenance' ||
|
|
293
|
+
provenance['schemaVersion'] !== 1 ||
|
|
294
|
+
provenance['repository'] !== input.repository ||
|
|
295
|
+
provenance['sourceCommit'] !== input.sourceCommit) {
|
|
296
|
+
throw new Error(`Release candidate for ${input.project} does not bind the exact source and repository.`);
|
|
297
|
+
}
|
|
298
|
+
return { digest: digest(raw), path };
|
|
299
|
+
}
|
|
300
|
+
/** @internal */
|
|
301
|
+
function approvalCandidateSourceCommit(context, options, environment) {
|
|
302
|
+
const project = context.projectName;
|
|
303
|
+
const selectedSource = environment['EBK_RELEASE_SOURCE_COMMIT']?.trim();
|
|
304
|
+
if (selectedSource !== undefined && !SOURCE_COMMIT.test(selectedSource)) {
|
|
305
|
+
throw new Error('EBK_RELEASE_SOURCE_COMMIT must be one exact Git object ID when set.');
|
|
306
|
+
}
|
|
307
|
+
if (!project)
|
|
308
|
+
return selectedSource;
|
|
309
|
+
const approvalPath = relativeFile(context.root, options.approvalPath ?? 'operations/infrastructure-approval.json');
|
|
310
|
+
if (!(0, node_fs_1.existsSync)(approvalPath))
|
|
311
|
+
return selectedSource;
|
|
312
|
+
let batch;
|
|
313
|
+
try {
|
|
314
|
+
batch = record(readJsonFile(approvalPath, 'Infrastructure approval candidate binding'), 'Infrastructure approval candidate binding');
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return selectedSource;
|
|
318
|
+
}
|
|
319
|
+
const action = options.action ?? 'deploy';
|
|
320
|
+
const stage = options.stage?.trim();
|
|
321
|
+
if (batch['kind'] !== 'ebk-infrastructure-approval-batch' ||
|
|
322
|
+
batch['schemaVersion'] !== 1 ||
|
|
323
|
+
batch['action'] !== action ||
|
|
324
|
+
stage === undefined ||
|
|
325
|
+
batch['stage'] !== stage) {
|
|
326
|
+
return selectedSource;
|
|
327
|
+
}
|
|
328
|
+
let approvedSource;
|
|
329
|
+
try {
|
|
330
|
+
const projects = record(batch['projects'], 'Infrastructure approval candidate projects');
|
|
331
|
+
const reviewed = record(projects[project], `Infrastructure approval candidate project ${project}`);
|
|
332
|
+
const planned = record(reviewed['planned'], `Infrastructure approval candidate plan ${project}`);
|
|
333
|
+
approvedSource = planned['sourceCommit'];
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return selectedSource;
|
|
337
|
+
}
|
|
338
|
+
if (typeof approvedSource !== 'string' ||
|
|
339
|
+
!SOURCE_COMMIT.test(approvedSource)) {
|
|
340
|
+
return selectedSource;
|
|
341
|
+
}
|
|
342
|
+
if (selectedSource === undefined) {
|
|
343
|
+
throw new Error('Approval-resume infrastructure planning requires a workflow-selected release source.');
|
|
344
|
+
}
|
|
345
|
+
if (selectedSource !== undefined && selectedSource !== approvedSource) {
|
|
346
|
+
throw new Error('Infrastructure approval candidate differs from the workflow-selected release source.');
|
|
347
|
+
}
|
|
348
|
+
return approvedSource;
|
|
349
|
+
}
|
|
350
|
+
/** @internal */
|
|
351
|
+
function createCandidateBoundSstChildEnvironment(environment, invocationEnvironment, candidateSourceCommit) {
|
|
352
|
+
if (!SOURCE_COMMIT.test(candidateSourceCommit)) {
|
|
353
|
+
throw new Error('Infrastructure lifecycle candidate must be one exact Git object ID.');
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
...(0, run_sst_command_js_1.createSstChildEnvironment)(environment, invocationEnvironment),
|
|
357
|
+
EBK_RELEASE_SOURCE_COMMIT: candidateSourceCommit,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function frameworkAndBlueprintVersions(workspaceRoot, projectRoot) {
|
|
361
|
+
const workspacePackage = record(readJsonFile((0, node_path_1.join)(workspaceRoot, 'package.json'), 'Workspace package'), 'Workspace package');
|
|
362
|
+
const declared = {
|
|
363
|
+
...record(workspacePackage['dependencies'] ?? {}, 'Workspace dependencies'),
|
|
364
|
+
...record(workspacePackage['devDependencies'] ?? {}, 'Workspace development dependencies'),
|
|
365
|
+
};
|
|
366
|
+
const versions = {};
|
|
367
|
+
for (const name of Object.keys(declared)
|
|
368
|
+
.filter((candidate) => candidate.startsWith('@empire-builder-kit/'))
|
|
369
|
+
.sort()) {
|
|
370
|
+
const installed = record(readJsonFile((0, node_path_1.join)(workspaceRoot, 'node_modules', ...name.split('/'), 'package.json'), `${name} installed package`), `${name} installed package`);
|
|
371
|
+
if (typeof installed['version'] !== 'string' || !installed['version']) {
|
|
372
|
+
throw new Error(`${name} installed version is missing.`);
|
|
373
|
+
}
|
|
374
|
+
versions[name] = installed['version'];
|
|
375
|
+
}
|
|
376
|
+
const workspaceOwnershipPath = (0, node_path_1.join)(workspaceRoot, '.ebk', 'ownership.json');
|
|
377
|
+
if ((0, node_fs_1.existsSync)(workspaceOwnershipPath)) {
|
|
378
|
+
const ownership = record(readJsonFile(workspaceOwnershipPath, 'Workspace ownership'), 'Workspace ownership');
|
|
379
|
+
if (ownership['kind'] !== 'workspace' ||
|
|
380
|
+
typeof ownership['frameworkVersion'] !== 'string') {
|
|
381
|
+
throw new Error('Workspace ownership framework identity is invalid.');
|
|
382
|
+
}
|
|
383
|
+
versions['ownership:workspace-framework'] = ownership['frameworkVersion'];
|
|
384
|
+
}
|
|
385
|
+
const ownershipPath = (0, node_path_1.join)(projectRoot, '.ebk', 'ownership.json');
|
|
386
|
+
if (!(0, node_fs_1.existsSync)(ownershipPath)) {
|
|
387
|
+
throw new Error('Infrastructure planning requires exact project ownership version metadata.');
|
|
388
|
+
}
|
|
389
|
+
const ownership = record(readJsonFile(ownershipPath, 'Project ownership'), 'Project ownership');
|
|
390
|
+
if (ownership['kind'] === 'foundation') {
|
|
391
|
+
if (typeof ownership['frameworkVersion'] !== 'string') {
|
|
392
|
+
throw new Error('Foundation ownership framework identity is invalid.');
|
|
393
|
+
}
|
|
394
|
+
versions['ownership:foundation-framework'] = ownership['frameworkVersion'];
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
const blueprint = record(ownership['blueprint'], 'Product Slice Blueprint ownership');
|
|
398
|
+
if (typeof blueprint['name'] !== 'string' ||
|
|
399
|
+
typeof blueprint['version'] !== 'string' ||
|
|
400
|
+
!blueprint['name'] ||
|
|
401
|
+
!blueprint['version']) {
|
|
402
|
+
throw new Error('Product Slice Blueprint version identity is invalid.');
|
|
403
|
+
}
|
|
404
|
+
versions[`blueprint:${blueprint['name']}`] = blueprint['version'];
|
|
405
|
+
}
|
|
406
|
+
return versions;
|
|
407
|
+
}
|
|
408
|
+
function assertCheckedOutSourceCommit(spawn, root, environment) {
|
|
409
|
+
const expected = requiredEnvironment(environment, 'EBK_SOURCE_COMMIT', SOURCE_COMMIT);
|
|
410
|
+
const result = (spawn ?? defaultSpawn)('git', ['rev-parse', '--verify', 'HEAD'], {
|
|
411
|
+
cwd: root,
|
|
412
|
+
encoding: 'utf8',
|
|
413
|
+
env: environment,
|
|
414
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
415
|
+
shell: false,
|
|
416
|
+
});
|
|
417
|
+
const actual = result.stdout.trim().toLowerCase();
|
|
418
|
+
if (result.status !== 0 ||
|
|
419
|
+
!SOURCE_COMMIT.test(actual) ||
|
|
420
|
+
actual !== expected) {
|
|
421
|
+
throw new Error('EBK_SOURCE_COMMIT does not match the checked-out Git HEAD.');
|
|
422
|
+
}
|
|
423
|
+
return actual;
|
|
424
|
+
}
|
|
425
|
+
function sourceChanges(spawn, root, environment, plannedCommit) {
|
|
426
|
+
const current = requiredEnvironment(environment, 'EBK_SOURCE_COMMIT', SOURCE_COMMIT);
|
|
427
|
+
if (plannedCommit === current)
|
|
428
|
+
return [];
|
|
429
|
+
const ancestry = (spawn ?? defaultSpawn)('git', ['merge-base', '--is-ancestor', plannedCommit, current], {
|
|
430
|
+
cwd: root,
|
|
431
|
+
encoding: 'utf8',
|
|
432
|
+
env: environment,
|
|
433
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
434
|
+
shell: false,
|
|
435
|
+
});
|
|
436
|
+
if (ancestry.status !== 0) {
|
|
437
|
+
throw new Error('Reviewed infrastructure plan commit is not an exact ancestor of the checked-out source.');
|
|
438
|
+
}
|
|
439
|
+
const result = (spawn ?? defaultSpawn)('git', ['diff', '--name-only', plannedCommit, current, '--'], {
|
|
440
|
+
cwd: root,
|
|
441
|
+
encoding: 'utf8',
|
|
442
|
+
env: environment,
|
|
443
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
444
|
+
shell: false,
|
|
445
|
+
});
|
|
446
|
+
if (result.status !== 0) {
|
|
447
|
+
throw new Error('Unable to verify source changes since the reviewed plan.');
|
|
448
|
+
}
|
|
449
|
+
return result.stdout.split(/\r?\n/).filter(Boolean).sort();
|
|
450
|
+
}
|
|
451
|
+
function defaultSpawn(executable, args, options) {
|
|
452
|
+
const result = (0, node_child_process_1.spawnSync)(executable, [...args], options);
|
|
453
|
+
if (result.error)
|
|
454
|
+
throw result.error;
|
|
455
|
+
return {
|
|
456
|
+
status: result.status,
|
|
457
|
+
stderr: result.stderr ?? '',
|
|
458
|
+
stdout: result.stdout ?? '',
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function canonicalJson(value) {
|
|
462
|
+
return (0, canonical_json_1.canonicalJson)(value, { omitUndefinedObjectValues: true });
|
|
463
|
+
}
|
|
464
|
+
function stateResources(value) {
|
|
465
|
+
const root = record(value, 'SST state export');
|
|
466
|
+
const deployment = typeof root['deployment'] === 'object' && root['deployment'] !== null
|
|
467
|
+
? record(root['deployment'], 'SST state deployment')
|
|
468
|
+
: root;
|
|
469
|
+
if (!Array.isArray(deployment['resources'])) {
|
|
470
|
+
throw new Error('SST state export does not contain a Pulumi deployment resource list.');
|
|
471
|
+
}
|
|
472
|
+
return deployment['resources'].map((resource, index) => record(resource, `SST state resource ${index}`));
|
|
473
|
+
}
|
|
474
|
+
function stringRecord(value) {
|
|
475
|
+
if (typeof value !== 'object' ||
|
|
476
|
+
value === null ||
|
|
477
|
+
Array.isArray(value) ||
|
|
478
|
+
Object.values(value).some((entry) => typeof entry !== 'string')) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
function resourceArn(resource) {
|
|
484
|
+
for (const material of [resource.outputs, resource.inputs]) {
|
|
485
|
+
if (typeof material !== 'object' || material === null)
|
|
486
|
+
continue;
|
|
487
|
+
const value = material;
|
|
488
|
+
for (const key of [
|
|
489
|
+
'arn',
|
|
490
|
+
'resourceArn',
|
|
491
|
+
'roleArn',
|
|
492
|
+
'queueArn',
|
|
493
|
+
'topicArn',
|
|
494
|
+
]) {
|
|
495
|
+
if (typeof value[key] === 'string' && value[key].startsWith('arn:aws:')) {
|
|
496
|
+
return value[key];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
function removePhysicalSelector(resource) {
|
|
503
|
+
const type = typeof resource.type === 'string' ? resource.type : '';
|
|
504
|
+
const material = [resource.inputs, resource.outputs].filter((value) => typeof value === 'object' && value !== null && !Array.isArray(value));
|
|
505
|
+
const first = (...keys) => {
|
|
506
|
+
for (const value of material) {
|
|
507
|
+
for (const key of keys) {
|
|
508
|
+
if (typeof value[key] === 'string' && value[key].length > 0) {
|
|
509
|
+
return value[key];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return undefined;
|
|
514
|
+
};
|
|
515
|
+
if (type === 'aws:cloudwatch/eventTarget:EventTarget') {
|
|
516
|
+
const rule = first('rule');
|
|
517
|
+
const targetId = first('targetId');
|
|
518
|
+
return rule && targetId
|
|
519
|
+
? {
|
|
520
|
+
...(first('eventBusName')
|
|
521
|
+
? { eventBusName: first('eventBusName') }
|
|
522
|
+
: {}),
|
|
523
|
+
rule,
|
|
524
|
+
targetId,
|
|
525
|
+
}
|
|
526
|
+
: undefined;
|
|
527
|
+
}
|
|
528
|
+
if (type === 'aws:iam/rolePolicy:RolePolicy') {
|
|
529
|
+
const policyName = first('name');
|
|
530
|
+
const roleName = first('role');
|
|
531
|
+
return policyName && roleName ? { policyName, roleName } : undefined;
|
|
532
|
+
}
|
|
533
|
+
if (type === 'aws:lambda/functionEventInvokeConfig:FunctionEventInvokeConfig') {
|
|
534
|
+
const functionName = first('functionName', 'function');
|
|
535
|
+
return functionName
|
|
536
|
+
? {
|
|
537
|
+
functionName,
|
|
538
|
+
...(first('qualifier') ? { qualifier: first('qualifier') } : {}),
|
|
539
|
+
}
|
|
540
|
+
: undefined;
|
|
541
|
+
}
|
|
542
|
+
if (type === 'aws:lambda/permission:Permission') {
|
|
543
|
+
const functionName = first('function', 'functionName');
|
|
544
|
+
const statementId = first('statementId');
|
|
545
|
+
return functionName && statementId
|
|
546
|
+
? { functionName, statementId }
|
|
547
|
+
: undefined;
|
|
548
|
+
}
|
|
549
|
+
if (type === 'aws:cloudwatch/logDataProtectionPolicy:LogDataProtectionPolicy') {
|
|
550
|
+
const logGroupIdentifier = first('logGroupIdentifier', 'logGroupName');
|
|
551
|
+
return logGroupIdentifier ? { logGroupIdentifier } : undefined;
|
|
552
|
+
}
|
|
553
|
+
if (type === 'aws:s3/bucketObjectv2:BucketObjectv2') {
|
|
554
|
+
const bucket = first('bucket');
|
|
555
|
+
const key = first('key');
|
|
556
|
+
return bucket && key ? { bucket, key } : undefined;
|
|
557
|
+
}
|
|
558
|
+
if (type === 'aws:sqs/queuePolicy:QueuePolicy') {
|
|
559
|
+
const queueUrl = first('queueUrl');
|
|
560
|
+
return queueUrl ? { queueUrl } : undefined;
|
|
561
|
+
}
|
|
562
|
+
const id = typeof resource.id === 'string' && resource.id.length > 0
|
|
563
|
+
? resource.id
|
|
564
|
+
: undefined;
|
|
565
|
+
const cloudControlIdentifier = (() => {
|
|
566
|
+
if (!id)
|
|
567
|
+
return undefined;
|
|
568
|
+
if ([
|
|
569
|
+
'aws:apigatewayv2/authorizer:Authorizer',
|
|
570
|
+
'aws:apigatewayv2/integration:Integration',
|
|
571
|
+
'aws:apigatewayv2/route:Route',
|
|
572
|
+
].includes(type)) {
|
|
573
|
+
const apiId = first('apiId');
|
|
574
|
+
return apiId ? `${apiId}|${id}` : undefined;
|
|
575
|
+
}
|
|
576
|
+
if (type === 'aws:apigatewayv2/stage:Stage') {
|
|
577
|
+
const apiId = first('apiId');
|
|
578
|
+
const stageName = first('name');
|
|
579
|
+
return apiId && stageName ? `${apiId}|${stageName}` : undefined;
|
|
580
|
+
}
|
|
581
|
+
if (type === 'aws:apigatewayv2/apiMapping:ApiMapping') {
|
|
582
|
+
const domainName = first('domainName');
|
|
583
|
+
return domainName ? `${domainName}|${id}` : undefined;
|
|
584
|
+
}
|
|
585
|
+
if ([
|
|
586
|
+
'aws:appconfig/configurationProfile:ConfigurationProfile',
|
|
587
|
+
'aws:appconfig/environment:Environment',
|
|
588
|
+
].includes(type)) {
|
|
589
|
+
const applicationId = first('applicationId');
|
|
590
|
+
return applicationId ? `${applicationId}|${id}` : undefined;
|
|
591
|
+
}
|
|
592
|
+
if (type ===
|
|
593
|
+
'aws:appconfig/hostedConfigurationVersion:HostedConfigurationVersion') {
|
|
594
|
+
const applicationId = first('applicationId');
|
|
595
|
+
const configurationProfileId = first('configurationProfileId');
|
|
596
|
+
return applicationId && configurationProfileId
|
|
597
|
+
? `${applicationId}|${configurationProfileId}|${id}`
|
|
598
|
+
: undefined;
|
|
599
|
+
}
|
|
600
|
+
if (type === 'aws:appconfig/deployment:Deployment') {
|
|
601
|
+
const applicationId = first('applicationId');
|
|
602
|
+
const environmentId = first('environmentId');
|
|
603
|
+
return applicationId && environmentId
|
|
604
|
+
? `${applicationId}|${environmentId}|${id}`
|
|
605
|
+
: undefined;
|
|
606
|
+
}
|
|
607
|
+
if (type === 'aws:cognito/userPoolClient:UserPoolClient') {
|
|
608
|
+
const userPoolId = first('userPoolId');
|
|
609
|
+
return userPoolId ? `${userPoolId}|${id}` : undefined;
|
|
610
|
+
}
|
|
611
|
+
if (type === 'aws:ecs/service:Service') {
|
|
612
|
+
const cluster = first('cluster');
|
|
613
|
+
return cluster ? `${cluster}|${id}` : undefined;
|
|
614
|
+
}
|
|
615
|
+
return undefined;
|
|
616
|
+
})();
|
|
617
|
+
if (cloudControlIdentifier)
|
|
618
|
+
return { cloudControlIdentifier };
|
|
619
|
+
return undefined;
|
|
620
|
+
}
|
|
621
|
+
function expectedTags(resource) {
|
|
622
|
+
for (const material of [resource.inputs, resource.outputs]) {
|
|
623
|
+
if (typeof material !== 'object' || material === null)
|
|
624
|
+
continue;
|
|
625
|
+
const tags = stringRecord(material['tags']);
|
|
626
|
+
if (tags)
|
|
627
|
+
return tags;
|
|
628
|
+
}
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
function isPulumiBookkeepingResource(resource) {
|
|
632
|
+
const type = typeof resource.type === 'string' ? resource.type : '';
|
|
633
|
+
return type === 'pulumi:pulumi:Stack' || type.startsWith('pulumi:providers:');
|
|
634
|
+
}
|
|
635
|
+
function materialPropertyPaths(resource) {
|
|
636
|
+
if (typeof resource.inputs !== 'object' ||
|
|
637
|
+
resource.inputs === null ||
|
|
638
|
+
Array.isArray(resource.inputs)) {
|
|
639
|
+
return [];
|
|
640
|
+
}
|
|
641
|
+
return Object.keys(resource.inputs)
|
|
642
|
+
.filter((key) => key !== 'tags' && key !== 'tagsAll')
|
|
643
|
+
.sort()
|
|
644
|
+
.map((key) => `inputs.${key}`);
|
|
645
|
+
}
|
|
646
|
+
const PULUMI_REFRESH_PREVIEW_VERSION = '3.215.0';
|
|
647
|
+
const REFRESH_PREVIEW_PROPERTY_OVERLAYS = {
|
|
648
|
+
'aws:secretsmanager/secretVersion:SecretVersion': ['inputs.secretString'],
|
|
649
|
+
};
|
|
650
|
+
function normalizedDiffMaterial(diff) {
|
|
651
|
+
return diff.resources.map((resource) => Object.fromEntries(Object.entries(resource).filter(([key]) => key !== 'sourceIndex')));
|
|
652
|
+
}
|
|
653
|
+
function assessPulumiRefreshPreview(input) {
|
|
654
|
+
const sstPath = input.invocation.args[0];
|
|
655
|
+
if (!sstPath)
|
|
656
|
+
throw new Error('Workspace SST path is missing.');
|
|
657
|
+
const result = input.spawn(input.invocation.executable, [sstPath, 'diff', ...input.invocation.args.slice(2), '--json'], {
|
|
658
|
+
cwd: input.invocation.cwd,
|
|
659
|
+
encoding: 'utf8',
|
|
660
|
+
env: {
|
|
661
|
+
...input.childEnvironment,
|
|
662
|
+
PULUMI_OPTION_REFRESH: 'true',
|
|
663
|
+
PULUMI_OPTION_RUN_PROGRAM: 'true',
|
|
664
|
+
},
|
|
665
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
666
|
+
shell: false,
|
|
667
|
+
});
|
|
668
|
+
let raw;
|
|
669
|
+
try {
|
|
670
|
+
raw = parseJson(result.stdout, 'SST refresh preview');
|
|
671
|
+
}
|
|
672
|
+
catch {
|
|
673
|
+
raw = null;
|
|
674
|
+
}
|
|
675
|
+
const entries = Array.isArray(raw) ? raw : [];
|
|
676
|
+
const refreshEntries = [];
|
|
677
|
+
const desiredEntries = [];
|
|
678
|
+
let shapeComplete = Array.isArray(raw);
|
|
679
|
+
const observedRefreshUrns = new Set();
|
|
680
|
+
for (const entry of entries) {
|
|
681
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
682
|
+
shapeComplete = false;
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const recordEntry = entry;
|
|
686
|
+
const operation = recordEntry['op'];
|
|
687
|
+
if (operation === 'refresh') {
|
|
688
|
+
const urn = recordEntry['urn'];
|
|
689
|
+
if (typeof urn !== 'string' || observedRefreshUrns.has(urn)) {
|
|
690
|
+
shapeComplete = false;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
observedRefreshUrns.add(urn);
|
|
694
|
+
const hasOld = Object.prototype.hasOwnProperty.call(recordEntry, 'old');
|
|
695
|
+
const hasNew = Object.prototype.hasOwnProperty.call(recordEntry, 'new');
|
|
696
|
+
const detailedDiff = recordEntry['detailedDiff'];
|
|
697
|
+
const diffs = recordEntry['diffs'];
|
|
698
|
+
const hasDetailedDiff = typeof detailedDiff === 'object' &&
|
|
699
|
+
detailedDiff !== null &&
|
|
700
|
+
!Array.isArray(detailedDiff) &&
|
|
701
|
+
Object.keys(detailedDiff).length > 0;
|
|
702
|
+
const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
|
|
703
|
+
if (hasNew && recordEntry['new'] === null) {
|
|
704
|
+
refreshEntries.push({ ...recordEntry, op: 'delete' });
|
|
705
|
+
}
|
|
706
|
+
else if (hasDetailedDiff ||
|
|
707
|
+
hasDiffs ||
|
|
708
|
+
(hasOld &&
|
|
709
|
+
hasNew &&
|
|
710
|
+
canonicalJson(recordEntry['old']) !==
|
|
711
|
+
canonicalJson(recordEntry['new']))) {
|
|
712
|
+
refreshEntries.push({ ...recordEntry, op: 'update' });
|
|
713
|
+
}
|
|
714
|
+
else if (hasOld &&
|
|
715
|
+
hasNew &&
|
|
716
|
+
canonicalJson(recordEntry['old']) === canonicalJson(recordEntry['new'])) {
|
|
717
|
+
// Refresh events with identical old/new material are provider reads,
|
|
718
|
+
// not drift. SST filters Pulumi "same" steps, so this is the explicit
|
|
719
|
+
// no-change refresh shape.
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
shapeComplete = false;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
else if (operation !== 'same') {
|
|
727
|
+
desiredEntries.push(entry);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
shapeComplete = false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
const drift = (0, index_js_1.normalizeSstDiffV4_17_1)({
|
|
734
|
+
exitCode: result.status ?? 1,
|
|
735
|
+
stdout: refreshEntries,
|
|
736
|
+
toolVersion: input.toolVersion,
|
|
737
|
+
});
|
|
738
|
+
const desired = (0, index_js_1.normalizeSstDiffV4_17_1)({
|
|
739
|
+
exitCode: result.status ?? 1,
|
|
740
|
+
stdout: desiredEntries,
|
|
741
|
+
toolVersion: input.toolVersion,
|
|
742
|
+
});
|
|
743
|
+
const commandSucceeded = result.status === 0;
|
|
744
|
+
const desiredMatchesOrdinary = desired.complete &&
|
|
745
|
+
input.ordinaryDiff.complete &&
|
|
746
|
+
canonicalJson(normalizedDiffMaterial(desired)) ===
|
|
747
|
+
canonicalJson(normalizedDiffMaterial(input.ordinaryDiff));
|
|
748
|
+
const complete = commandSucceeded &&
|
|
749
|
+
shapeComplete &&
|
|
750
|
+
drift.complete &&
|
|
751
|
+
desired.complete &&
|
|
752
|
+
input.toolVersion === index_js_1.SST_DIFF_TOOL_VERSION &&
|
|
753
|
+
PULUMI_REFRESH_PREVIEW_VERSION === '3.215.0';
|
|
754
|
+
return {
|
|
755
|
+
commandSucceeded,
|
|
756
|
+
complete,
|
|
757
|
+
desiredMatchesOrdinary,
|
|
758
|
+
digest: digest(canonicalJson({
|
|
759
|
+
desired: normalizedDiffMaterial(desired),
|
|
760
|
+
desiredMatchesOrdinary,
|
|
761
|
+
drift: normalizedDiffMaterial(drift),
|
|
762
|
+
pulumiVersion: PULUMI_REFRESH_PREVIEW_VERSION,
|
|
763
|
+
sstVersion: input.toolVersion,
|
|
764
|
+
})),
|
|
765
|
+
drift,
|
|
766
|
+
observedRefreshUrns: [...observedRefreshUrns].sort(),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function costQuotaEvidence(input) {
|
|
770
|
+
const quotaByType = {
|
|
771
|
+
'aws:lambda/function:Function': {
|
|
772
|
+
quotaCode: 'L-B99A9384',
|
|
773
|
+
serviceCode: 'lambda',
|
|
774
|
+
},
|
|
775
|
+
};
|
|
776
|
+
const entries = input.diff.resources
|
|
777
|
+
.filter(({ operation, resourceType }) => ['create', 'import', 'replace', 'update'].includes(operation) &&
|
|
778
|
+
input.policy.costOrQuotaTypes.includes(resourceType))
|
|
779
|
+
.map(({ logicalId, resourceType }) => {
|
|
780
|
+
const mapped = quotaByType[resourceType];
|
|
781
|
+
let quota = {
|
|
782
|
+
adjustable: null,
|
|
783
|
+
headroom: null,
|
|
784
|
+
quotaCode: mapped?.quotaCode ?? null,
|
|
785
|
+
serviceCode: mapped?.serviceCode ?? null,
|
|
786
|
+
status: mapped ? 'query-failed' : 'not-service-quotas-backed',
|
|
787
|
+
value: null,
|
|
788
|
+
};
|
|
789
|
+
if (mapped) {
|
|
790
|
+
const result = input.spawn('aws', [
|
|
791
|
+
'service-quotas',
|
|
792
|
+
'get-service-quota',
|
|
793
|
+
'--service-code',
|
|
794
|
+
mapped.serviceCode,
|
|
795
|
+
'--quota-code',
|
|
796
|
+
mapped.quotaCode,
|
|
797
|
+
'--output',
|
|
798
|
+
'json',
|
|
799
|
+
], {
|
|
800
|
+
cwd: input.invocation.cwd,
|
|
801
|
+
encoding: 'utf8',
|
|
802
|
+
env: input.childEnvironment,
|
|
803
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
804
|
+
shell: false,
|
|
805
|
+
});
|
|
806
|
+
try {
|
|
807
|
+
const response = record(parseJson(result.stdout, `${logicalId} service quota`), `${logicalId} service quota`);
|
|
808
|
+
const observed = record(response['Quota'], `${logicalId} service quota`);
|
|
809
|
+
if (result.status === 0 &&
|
|
810
|
+
observed['ServiceCode'] === mapped.serviceCode &&
|
|
811
|
+
observed['QuotaCode'] === mapped.quotaCode &&
|
|
812
|
+
typeof observed['Value'] === 'number' &&
|
|
813
|
+
Number.isFinite(observed['Value']) &&
|
|
814
|
+
typeof observed['Adjustable'] === 'boolean') {
|
|
815
|
+
quota = {
|
|
816
|
+
adjustable: observed['Adjustable'],
|
|
817
|
+
headroom: null,
|
|
818
|
+
quotaCode: mapped.quotaCode,
|
|
819
|
+
serviceCode: mapped.serviceCode,
|
|
820
|
+
status: 'observed-limit-usage-unavailable',
|
|
821
|
+
value: observed['Value'],
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
// The evidence remains query-failed and therefore review-blocking.
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
cost: {
|
|
831
|
+
assumptions: [
|
|
832
|
+
'Usage, duration, storage, data transfer, and request volume are not inferred from infrastructure source.',
|
|
833
|
+
'A product owner must supply a reviewed bound before protected mutation.',
|
|
834
|
+
],
|
|
835
|
+
maximumUsdMonthly: null,
|
|
836
|
+
minimumUsdMonthly: null,
|
|
837
|
+
status: 'unknown',
|
|
838
|
+
},
|
|
839
|
+
logicalId,
|
|
840
|
+
quota,
|
|
841
|
+
resourceType,
|
|
842
|
+
};
|
|
843
|
+
});
|
|
844
|
+
return {
|
|
845
|
+
accountFingerprint: digest(input.accountId),
|
|
846
|
+
entries,
|
|
847
|
+
project: input.project,
|
|
848
|
+
region: input.region,
|
|
849
|
+
sourceCommit: input.sourceCommit,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function iamPolicyDocuments(input) {
|
|
853
|
+
let entries;
|
|
854
|
+
try {
|
|
855
|
+
entries = parseJson(input.diffStdout, 'SST IAM plan');
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return [];
|
|
859
|
+
}
|
|
860
|
+
if (!Array.isArray(entries))
|
|
861
|
+
return [];
|
|
862
|
+
const validations = [];
|
|
863
|
+
for (const resource of input.diff.resources) {
|
|
864
|
+
const policyProperty = resource.resourceType === 'aws:iam/role:Role'
|
|
865
|
+
? { kind: 'trust', name: 'assumeRolePolicy' }
|
|
866
|
+
: resource.resourceType === 'aws:iam/policy:Policy' ||
|
|
867
|
+
resource.resourceType === 'aws:iam/rolePolicy:RolePolicy'
|
|
868
|
+
? { kind: 'identity', name: 'policy' }
|
|
869
|
+
: undefined;
|
|
870
|
+
if (!policyProperty)
|
|
871
|
+
continue;
|
|
872
|
+
if (resource.operation === 'delete')
|
|
873
|
+
continue;
|
|
874
|
+
const rawEntry = entries[resource.sourceIndex];
|
|
875
|
+
if (typeof rawEntry !== 'object' ||
|
|
876
|
+
rawEntry === null ||
|
|
877
|
+
Array.isArray(rawEntry)) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const rawNewValue = rawEntry['new'];
|
|
881
|
+
if (typeof rawNewValue !== 'object' ||
|
|
882
|
+
rawNewValue === null ||
|
|
883
|
+
Array.isArray(rawNewValue)) {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
const rawNew = rawNewValue;
|
|
887
|
+
const rawInputs = record(rawNew['inputs'], `${resource.logicalId} planned IAM inputs`);
|
|
888
|
+
const rawPolicy = rawInputs[policyProperty.name];
|
|
889
|
+
let policy = rawPolicy;
|
|
890
|
+
if (typeof rawPolicy === 'string') {
|
|
891
|
+
try {
|
|
892
|
+
policy = parseJson(rawPolicy, `${resource.logicalId} planned IAM policy`);
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (typeof policy !== 'object' ||
|
|
899
|
+
policy === null ||
|
|
900
|
+
Array.isArray(policy)) {
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const policyDocument = canonicalJson(policy);
|
|
904
|
+
const result = input.spawn('aws', [
|
|
905
|
+
'accessanalyzer',
|
|
906
|
+
'validate-policy',
|
|
907
|
+
'--policy-document',
|
|
908
|
+
policyDocument,
|
|
909
|
+
'--policy-type',
|
|
910
|
+
policyProperty.kind === 'trust' ? 'RESOURCE_POLICY' : 'IDENTITY_POLICY',
|
|
911
|
+
...(policyProperty.kind === 'trust'
|
|
912
|
+
? [
|
|
913
|
+
'--validate-policy-resource-type',
|
|
914
|
+
'AWS::IAM::AssumeRolePolicyDocument',
|
|
915
|
+
]
|
|
916
|
+
: []),
|
|
917
|
+
'--output',
|
|
918
|
+
'json',
|
|
919
|
+
], {
|
|
920
|
+
cwd: input.invocation.cwd,
|
|
921
|
+
encoding: 'utf8',
|
|
922
|
+
env: input.childEnvironment,
|
|
923
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
924
|
+
shell: false,
|
|
925
|
+
});
|
|
926
|
+
let response;
|
|
927
|
+
try {
|
|
928
|
+
response = record(parseJson(result.stdout, `${resource.logicalId} IAM validation`), `${resource.logicalId} IAM validation`);
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
response = undefined;
|
|
932
|
+
}
|
|
933
|
+
const rawFindings = response?.['findings'];
|
|
934
|
+
const findingsShapeComplete = Array.isArray(rawFindings) &&
|
|
935
|
+
rawFindings.every((finding) => {
|
|
936
|
+
if (typeof finding !== 'object' ||
|
|
937
|
+
finding === null ||
|
|
938
|
+
Array.isArray(finding)) {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
const value = finding;
|
|
942
|
+
return (typeof value['findingType'] === 'string' &&
|
|
943
|
+
['ERROR', 'SECURITY_WARNING', 'SUGGESTION', 'WARNING'].includes(value['findingType']) &&
|
|
944
|
+
typeof value['issueCode'] === 'string');
|
|
945
|
+
});
|
|
946
|
+
const findings = findingsShapeComplete
|
|
947
|
+
? rawFindings.map((value) => ({
|
|
948
|
+
code: digest(canonicalJson(value)),
|
|
949
|
+
findingType: value['findingType']
|
|
950
|
+
.toLowerCase()
|
|
951
|
+
.replace('_', '-'),
|
|
952
|
+
issueCode: value['issueCode'],
|
|
953
|
+
}))
|
|
954
|
+
: [];
|
|
955
|
+
validations.push({
|
|
956
|
+
accessChange: 'expanded',
|
|
957
|
+
accountFingerprint: digest(input.accountId),
|
|
958
|
+
analyzer: 'aws-iam-access-analyzer-validate-policy',
|
|
959
|
+
findings,
|
|
960
|
+
logicalId: resource.logicalId,
|
|
961
|
+
policyDigest: digest(policyDocument),
|
|
962
|
+
policyKind: policyProperty.kind,
|
|
963
|
+
policyResourceId: resource.logicalId,
|
|
964
|
+
region: input.region,
|
|
965
|
+
sourceCommit: input.sourceCommit,
|
|
966
|
+
status: result.status === 0 && findingsShapeComplete ? 'complete' : 'failed',
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return validations;
|
|
970
|
+
}
|
|
971
|
+
function exactSstStateLocation(environment) {
|
|
972
|
+
const bucketArn = requiredEnvironment(environment, 'AWS_PLAN_STATE_BUCKET_ARN', /^arn:aws:s3:::[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/);
|
|
973
|
+
const applicationsValue = parseJson(requiredEnvironment(environment, 'AWS_PLAN_SST_APPS_JSON'), 'AWS_PLAN_SST_APPS_JSON');
|
|
974
|
+
if (!Array.isArray(applicationsValue) ||
|
|
975
|
+
applicationsValue.length === 0 ||
|
|
976
|
+
new Set(applicationsValue).size !== applicationsValue.length ||
|
|
977
|
+
applicationsValue.some((application) => typeof application !== 'string' ||
|
|
978
|
+
!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(application))) {
|
|
979
|
+
throw new Error('AWS_PLAN_SST_APPS_JSON must list unique exact SST application names.');
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
applications: [...applicationsValue].sort(),
|
|
983
|
+
bucketArn,
|
|
984
|
+
bucketName: bucketArn.slice('arn:aws:s3:::'.length),
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function preflightSstBackendParameter(input) {
|
|
988
|
+
let parameter;
|
|
989
|
+
try {
|
|
990
|
+
parameter = record(runJsonCommand(input.spawn, 'aws', [
|
|
991
|
+
'ssm',
|
|
992
|
+
'get-parameter',
|
|
993
|
+
'--name',
|
|
994
|
+
input.name,
|
|
995
|
+
'--query',
|
|
996
|
+
'Parameter.{Name:Name,Type:Type,Version:Version}',
|
|
997
|
+
'--output',
|
|
998
|
+
'json',
|
|
999
|
+
], {
|
|
1000
|
+
cwd: input.invocation.cwd,
|
|
1001
|
+
env: input.childEnvironment,
|
|
1002
|
+
label: `SST backend parameter ${input.name}`,
|
|
1003
|
+
}), `SST backend parameter ${input.name}`);
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
throw new Error(`SST backend parameter ${input.name} is missing or outdated.`);
|
|
1007
|
+
}
|
|
1008
|
+
if (parameter['Name'] !== input.name ||
|
|
1009
|
+
parameter['Type'] !== input.expectedType ||
|
|
1010
|
+
typeof parameter['Version'] !== 'number' ||
|
|
1011
|
+
!Number.isSafeInteger(parameter['Version']) ||
|
|
1012
|
+
parameter['Version'] < 1) {
|
|
1013
|
+
throw new Error(`SST backend parameter ${input.name} is missing or outdated.`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function preflightSstBootstrap(input) {
|
|
1017
|
+
let parameter;
|
|
1018
|
+
try {
|
|
1019
|
+
parameter = record(runJsonCommand(input.spawn, 'aws', [
|
|
1020
|
+
'ssm',
|
|
1021
|
+
'get-parameter',
|
|
1022
|
+
'--name',
|
|
1023
|
+
'/sst/bootstrap',
|
|
1024
|
+
'--query',
|
|
1025
|
+
'Parameter.{Name:Name,Type:Type,Version:Version,Value:Value}',
|
|
1026
|
+
'--output',
|
|
1027
|
+
'json',
|
|
1028
|
+
], {
|
|
1029
|
+
cwd: input.invocation.cwd,
|
|
1030
|
+
env: input.childEnvironment,
|
|
1031
|
+
label: 'SST bootstrap parameter',
|
|
1032
|
+
}), 'SST bootstrap parameter');
|
|
1033
|
+
}
|
|
1034
|
+
catch {
|
|
1035
|
+
throw new Error('SST bootstrap parameter is missing, malformed, or outdated.');
|
|
1036
|
+
}
|
|
1037
|
+
if (parameter['Name'] !== '/sst/bootstrap' ||
|
|
1038
|
+
parameter['Type'] !== 'String' ||
|
|
1039
|
+
typeof parameter['Version'] !== 'number' ||
|
|
1040
|
+
!Number.isSafeInteger(parameter['Version']) ||
|
|
1041
|
+
parameter['Version'] < 1 ||
|
|
1042
|
+
typeof parameter['Value'] !== 'string') {
|
|
1043
|
+
throw new Error('SST bootstrap parameter is missing, malformed, or outdated.');
|
|
1044
|
+
}
|
|
1045
|
+
let bootstrap;
|
|
1046
|
+
try {
|
|
1047
|
+
bootstrap = record(parseJson(parameter['Value'], 'SST v4.17.1 bootstrap value'), 'SST v4.17.1 bootstrap value');
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
throw new Error('SST bootstrap parameter is missing, malformed, or outdated.');
|
|
1051
|
+
}
|
|
1052
|
+
const exactKeys = [
|
|
1053
|
+
'appsyncHttp',
|
|
1054
|
+
'appsyncRealtime',
|
|
1055
|
+
'asset',
|
|
1056
|
+
'assetEcrRegistryId',
|
|
1057
|
+
'assetEcrUrl',
|
|
1058
|
+
'state',
|
|
1059
|
+
'version',
|
|
1060
|
+
];
|
|
1061
|
+
const bucketName = /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/;
|
|
1062
|
+
const expectedEcrUrl = `${input.accountId}.dkr.ecr.${input.region}.amazonaws.com/sst-asset`;
|
|
1063
|
+
if (Object.keys(bootstrap).sort().join(',') !== exactKeys.join(',') ||
|
|
1064
|
+
bootstrap['version'] !== 5 ||
|
|
1065
|
+
bootstrap['state'] !== input.stateBucketName ||
|
|
1066
|
+
typeof bootstrap['asset'] !== 'string' ||
|
|
1067
|
+
!bucketName.test(bootstrap['asset']) ||
|
|
1068
|
+
bootstrap['assetEcrRegistryId'] !== input.accountId ||
|
|
1069
|
+
bootstrap['assetEcrUrl'] !== expectedEcrUrl ||
|
|
1070
|
+
typeof bootstrap['appsyncHttp'] !== 'string' ||
|
|
1071
|
+
typeof bootstrap['appsyncRealtime'] !== 'string') {
|
|
1072
|
+
throw new Error('SST bootstrap parameter does not match the exact v4.17.1 schema and declared state backend.');
|
|
1073
|
+
}
|
|
1074
|
+
return digest(canonicalJson({
|
|
1075
|
+
bootstrap,
|
|
1076
|
+
parameterRevision: parameter['Version'],
|
|
1077
|
+
}));
|
|
1078
|
+
}
|
|
1079
|
+
function exactS3KeyExists(input) {
|
|
1080
|
+
const result = record(runJsonCommand(input.spawn, 'aws', [
|
|
1081
|
+
's3api',
|
|
1082
|
+
'list-objects-v2',
|
|
1083
|
+
'--bucket',
|
|
1084
|
+
input.stateBucketName,
|
|
1085
|
+
'--prefix',
|
|
1086
|
+
input.key,
|
|
1087
|
+
'--max-keys',
|
|
1088
|
+
'1',
|
|
1089
|
+
'--output',
|
|
1090
|
+
'json',
|
|
1091
|
+
], {
|
|
1092
|
+
cwd: input.invocation.cwd,
|
|
1093
|
+
env: input.childEnvironment,
|
|
1094
|
+
label: input.label,
|
|
1095
|
+
}), input.label);
|
|
1096
|
+
const contents = result['Contents'];
|
|
1097
|
+
const commonPrefixes = result['CommonPrefixes'];
|
|
1098
|
+
if (result['Prefix'] !== input.key ||
|
|
1099
|
+
!Number.isSafeInteger(result['KeyCount']) ||
|
|
1100
|
+
(contents !== undefined && !Array.isArray(contents)) ||
|
|
1101
|
+
(commonPrefixes !== undefined && !Array.isArray(commonPrefixes)) ||
|
|
1102
|
+
(Array.isArray(commonPrefixes) && commonPrefixes.length > 0)) {
|
|
1103
|
+
throw new Error(`${input.label} returned an ambiguous exact-key listing.`);
|
|
1104
|
+
}
|
|
1105
|
+
if (result['KeyCount'] === 0 &&
|
|
1106
|
+
(contents === undefined ||
|
|
1107
|
+
(Array.isArray(contents) && contents.length === 0))) {
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
if (result['KeyCount'] === 1 &&
|
|
1111
|
+
Array.isArray(contents) &&
|
|
1112
|
+
contents.length === 1 &&
|
|
1113
|
+
record(contents[0], input.label)['Key'] === input.key) {
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
throw new Error(`${input.label} returned an unexpected key or prefix.`);
|
|
1117
|
+
}
|
|
1118
|
+
function preflightProtectedSstSecrets(input) {
|
|
1119
|
+
for (const key of [
|
|
1120
|
+
`secret/${input.application}/${input.stage}.json`,
|
|
1121
|
+
`secret/${input.application}/_fallback.json`,
|
|
1122
|
+
]) {
|
|
1123
|
+
if (exactS3KeyExists({
|
|
1124
|
+
childEnvironment: input.childEnvironment,
|
|
1125
|
+
invocation: input.invocation,
|
|
1126
|
+
key,
|
|
1127
|
+
label: `Protected SST secret absence preflight ${key}`,
|
|
1128
|
+
spawn: input.spawn,
|
|
1129
|
+
stateBucketName: input.stateBucketName,
|
|
1130
|
+
})) {
|
|
1131
|
+
throw new Error(`Protected SST secret object ${key} exists; protected SST secrets are forbidden.`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function observeSstStateLock(input) {
|
|
1136
|
+
const key = `lock/${input.app}/${input.stage}.json`;
|
|
1137
|
+
if (!exactS3KeyExists({
|
|
1138
|
+
childEnvironment: input.childEnvironment,
|
|
1139
|
+
invocation: input.invocation,
|
|
1140
|
+
key,
|
|
1141
|
+
label: `Exact SST state lock inventory ${key}`,
|
|
1142
|
+
spawn: input.spawn,
|
|
1143
|
+
stateBucketName: input.stateBucketName,
|
|
1144
|
+
})) {
|
|
1145
|
+
return {
|
|
1146
|
+
digest: digest(canonicalJson({ key, status: 'absent' })),
|
|
1147
|
+
key,
|
|
1148
|
+
locked: false,
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
const temporaryDirectory = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'ebk-sst-lock-'));
|
|
1152
|
+
const lockPath = (0, node_path_1.join)(temporaryDirectory, 'lock.json');
|
|
1153
|
+
try {
|
|
1154
|
+
const result = input.spawn('aws', [
|
|
1155
|
+
's3api',
|
|
1156
|
+
'get-object',
|
|
1157
|
+
'--bucket',
|
|
1158
|
+
input.stateBucketName,
|
|
1159
|
+
'--key',
|
|
1160
|
+
key,
|
|
1161
|
+
'--output',
|
|
1162
|
+
'json',
|
|
1163
|
+
lockPath,
|
|
1164
|
+
], {
|
|
1165
|
+
cwd: input.invocation.cwd,
|
|
1166
|
+
encoding: 'utf8',
|
|
1167
|
+
env: input.childEnvironment,
|
|
1168
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
1169
|
+
shell: false,
|
|
1170
|
+
});
|
|
1171
|
+
if (result.status !== 0) {
|
|
1172
|
+
throw new Error(`Exact SST state lock observation failed${result.stderr.trim() ? `: ${result.stderr.trim()}` : '.'}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!(0, node_fs_1.existsSync)(lockPath)) {
|
|
1175
|
+
throw new Error('Exact SST state lock response body is absent.');
|
|
1176
|
+
}
|
|
1177
|
+
const contents = (0, node_fs_1.readFileSync)(lockPath, 'utf8');
|
|
1178
|
+
const lock = record(parseJson(contents, 'Exact SST state lock'), 'Exact SST state lock');
|
|
1179
|
+
const expectedKeys = ['command', 'created', 'ignore', 'runID', 'updateID'];
|
|
1180
|
+
if (Object.keys(lock).sort().join(',') !== expectedKeys.join(',') ||
|
|
1181
|
+
typeof lock['created'] !== 'string' ||
|
|
1182
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(lock['created']) ||
|
|
1183
|
+
!Number.isFinite(Date.parse(lock['created'])) ||
|
|
1184
|
+
typeof lock['updateID'] !== 'string' ||
|
|
1185
|
+
typeof lock['runID'] !== 'string' ||
|
|
1186
|
+
typeof lock['command'] !== 'string' ||
|
|
1187
|
+
typeof lock['ignore'] !== 'boolean') {
|
|
1188
|
+
throw new Error('Exact SST state lock has an invalid v4.17.1 shape.');
|
|
1189
|
+
}
|
|
1190
|
+
const createdAt = new Date(lock['created']).toISOString();
|
|
1191
|
+
const zeroCreatedAt = new Date('0001-01-01T00:00:00.000Z').toISOString();
|
|
1192
|
+
if (createdAt === zeroCreatedAt ||
|
|
1193
|
+
lock['updateID'].length === 0 ||
|
|
1194
|
+
lock['runID'].length === 0 ||
|
|
1195
|
+
lock['command'].length === 0) {
|
|
1196
|
+
throw new Error('Exact SST state lock is zero-valued or inconsistent; only exact-key absence is unlocked.');
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
digest: digest(canonicalJson({ key, lock })),
|
|
1200
|
+
key,
|
|
1201
|
+
locked: true,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
finally {
|
|
1205
|
+
(0, node_fs_1.rmSync)(temporaryDirectory, { force: true, recursive: true });
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function runJsonCommand(spawn, executable, args, options) {
|
|
1209
|
+
const result = spawn(executable, args, {
|
|
1210
|
+
cwd: options.cwd,
|
|
1211
|
+
encoding: 'utf8',
|
|
1212
|
+
env: options.env,
|
|
1213
|
+
maxBuffer: options.maxBuffer ?? MAX_JSON_BYTES,
|
|
1214
|
+
shell: false,
|
|
1215
|
+
});
|
|
1216
|
+
if (result.status !== 0) {
|
|
1217
|
+
throw new Error(`${options.label} failed under the read-only plan authority${result.stderr.trim() ? `: ${result.stderr.trim()}` : '.'}`);
|
|
1218
|
+
}
|
|
1219
|
+
return parseJson(result.stdout, options.label);
|
|
1220
|
+
}
|
|
1221
|
+
const STANDARD_RETAINED_S3_TYPES = new Set([
|
|
1222
|
+
'aws:s3/bucket:Bucket',
|
|
1223
|
+
'aws:s3/bucketLifecycleConfigurationV2:BucketLifecycleConfigurationV2',
|
|
1224
|
+
'aws:s3/bucketOwnershipControls:BucketOwnershipControls',
|
|
1225
|
+
'aws:s3/bucketPolicy:BucketPolicy',
|
|
1226
|
+
'aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock',
|
|
1227
|
+
'aws:s3/bucketServerSideEncryptionConfigurationV2:BucketServerSideEncryptionConfigurationV2',
|
|
1228
|
+
'aws:s3/bucketVersioningV2:BucketVersioningV2',
|
|
1229
|
+
]);
|
|
1230
|
+
function hasControlCharacters(value) {
|
|
1231
|
+
return [...value].some((character) => {
|
|
1232
|
+
const codePoint = character.codePointAt(0);
|
|
1233
|
+
return codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f);
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
function standardRemoveProbeForResource(resource) {
|
|
1237
|
+
const physicalId = resource.physicalId?.trim();
|
|
1238
|
+
const physicalArn = resource.physicalArn?.trim();
|
|
1239
|
+
if ((physicalId &&
|
|
1240
|
+
(physicalId.length > 2_048 || hasControlCharacters(physicalId))) ||
|
|
1241
|
+
(physicalArn &&
|
|
1242
|
+
(physicalArn.length > 2_048 ||
|
|
1243
|
+
hasControlCharacters(physicalArn) ||
|
|
1244
|
+
!/^arn:aws(?:-[a-z]+)*:/.test(physicalArn)))) {
|
|
1245
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} has an ambiguous physical identity.`);
|
|
1246
|
+
}
|
|
1247
|
+
if (resource.resourceType === 'aws:ssm/parameter:Parameter') {
|
|
1248
|
+
if (!physicalId) {
|
|
1249
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} has no exact SSM parameter identity.`);
|
|
1250
|
+
}
|
|
1251
|
+
return {
|
|
1252
|
+
identifier: physicalId,
|
|
1253
|
+
kind: 'aws-ssm-parameter',
|
|
1254
|
+
protectionProfile: /registration/i.test(resource.logicalId)
|
|
1255
|
+
? 'slice-registration'
|
|
1256
|
+
: 'slice-parameter',
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (resource.resourceType === 'aws:secretsmanager/secret:Secret') {
|
|
1260
|
+
if (!physicalArn && !physicalId) {
|
|
1261
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} has no exact secret identity.`);
|
|
1262
|
+
}
|
|
1263
|
+
return {
|
|
1264
|
+
identifier: physicalArn ?? physicalId,
|
|
1265
|
+
kind: 'aws-secrets-manager-secret',
|
|
1266
|
+
protectionProfile: 'slice-secret',
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
if (STANDARD_RETAINED_S3_TYPES.has(resource.resourceType)) {
|
|
1270
|
+
if (!physicalId) {
|
|
1271
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} has no exact S3 identity.`);
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
identifier: physicalId,
|
|
1275
|
+
kind: 'aws-s3-retained',
|
|
1276
|
+
protectionProfile: resource.resourceType,
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
if (resource.resourceType === 'aws:kms/key:Key' ||
|
|
1280
|
+
resource.resourceType === 'aws:kms/alias:Alias') {
|
|
1281
|
+
if (!physicalId && !physicalArn) {
|
|
1282
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} has no exact KMS identity.`);
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
identifier: physicalId ?? physicalArn,
|
|
1286
|
+
kind: 'aws-kms-retained',
|
|
1287
|
+
protectionProfile: resource.resourceType,
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
const selector = resource.physicalSelector;
|
|
1291
|
+
const selectorProbeKinds = {
|
|
1292
|
+
'aws:cloudwatch/eventTarget:EventTarget': 'aws-events-target',
|
|
1293
|
+
'aws:cloudwatch/logDataProtectionPolicy:LogDataProtectionPolicy': 'aws-logs-data-protection-policy',
|
|
1294
|
+
'aws:iam/rolePolicy:RolePolicy': 'aws-iam-role-policy',
|
|
1295
|
+
'aws:lambda/functionEventInvokeConfig:FunctionEventInvokeConfig': 'aws-lambda-event-invoke-config',
|
|
1296
|
+
'aws:lambda/permission:Permission': 'aws-lambda-permission',
|
|
1297
|
+
'aws:s3/bucketObjectv2:BucketObjectv2': 'aws-s3-object',
|
|
1298
|
+
'aws:sqs/queuePolicy:QueuePolicy': 'aws-sqs-queue-policy',
|
|
1299
|
+
};
|
|
1300
|
+
const selectorKind = selectorProbeKinds[resource.resourceType];
|
|
1301
|
+
if (selectorKind) {
|
|
1302
|
+
if (!physicalId || !selector || Object.keys(selector).length === 0) {
|
|
1303
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} lacks its exact Standard selector.`);
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
identifier: physicalId,
|
|
1307
|
+
kind: selectorKind,
|
|
1308
|
+
selector,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const typeName = STANDARD_REMOVE_CLOUD_CONTROL_TYPES[resource.resourceType];
|
|
1312
|
+
const cloudControlIdentifier = resource.physicalSelector?.['cloudControlIdentifier'] ?? physicalId;
|
|
1313
|
+
if (!typeName || !cloudControlIdentifier) {
|
|
1314
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} uses unmapped Standard type ${resource.resourceType} or lacks its exact provider ID.`);
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
identifier: cloudControlIdentifier,
|
|
1318
|
+
kind: 'aws-cloud-control',
|
|
1319
|
+
typeName,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
function runRemoveProbeCommand(input) {
|
|
1323
|
+
const result = input.spawn('aws', input.args, {
|
|
1324
|
+
cwd: input.invocation.cwd,
|
|
1325
|
+
encoding: 'utf8',
|
|
1326
|
+
env: input.childEnvironment,
|
|
1327
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
1328
|
+
shell: false,
|
|
1329
|
+
});
|
|
1330
|
+
if (result.status !== 0) {
|
|
1331
|
+
if (input.notFound.test(result.stderr)) {
|
|
1332
|
+
return { found: false };
|
|
1333
|
+
}
|
|
1334
|
+
throw new Error(`${input.label} failed under the protected remove authority; raw provider diagnostics were suppressed.`);
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
found: true,
|
|
1338
|
+
value: result.stdout.trim()
|
|
1339
|
+
? parseJson(result.stdout, input.label)
|
|
1340
|
+
: { status: 'present' },
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function probeStandardRemoveResource(input) {
|
|
1344
|
+
const { probe } = input;
|
|
1345
|
+
if (probe.kind === 'aws-cloud-control') {
|
|
1346
|
+
const response = runRemoveProbeCommand({
|
|
1347
|
+
...input,
|
|
1348
|
+
args: [
|
|
1349
|
+
'cloudcontrol',
|
|
1350
|
+
'get-resource',
|
|
1351
|
+
'--type-name',
|
|
1352
|
+
probe.typeName,
|
|
1353
|
+
'--identifier',
|
|
1354
|
+
probe.identifier,
|
|
1355
|
+
'--output',
|
|
1356
|
+
'json',
|
|
1357
|
+
],
|
|
1358
|
+
label: 'AWS Cloud Control exact remove observation',
|
|
1359
|
+
notFound: /ResourceNotFound(?:Exception)?/,
|
|
1360
|
+
});
|
|
1361
|
+
if (!response.found)
|
|
1362
|
+
return { present: false, protectionDigest: null };
|
|
1363
|
+
const description = record(record(response.value, 'AWS Cloud Control response')['ResourceDescription'], 'AWS Cloud Control resource description');
|
|
1364
|
+
if (description['Identifier'] !== probe.identifier) {
|
|
1365
|
+
throw new Error('AWS Cloud Control remove observation returned another resource identity.');
|
|
1366
|
+
}
|
|
1367
|
+
const properties = typeof description['Properties'] === 'string'
|
|
1368
|
+
? parseJson(description['Properties'], 'AWS Cloud Control resource properties')
|
|
1369
|
+
: description['Properties'];
|
|
1370
|
+
if (properties === undefined) {
|
|
1371
|
+
throw new Error('AWS Cloud Control resource properties are absent.');
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
present: true,
|
|
1375
|
+
protectionDigest: digest(canonicalJson(properties)),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
if (probe.kind === 'aws-ssm-parameter') {
|
|
1379
|
+
const response = runRemoveProbeCommand({
|
|
1380
|
+
...input,
|
|
1381
|
+
args: [
|
|
1382
|
+
'ssm',
|
|
1383
|
+
'get-parameter',
|
|
1384
|
+
'--name',
|
|
1385
|
+
probe.identifier,
|
|
1386
|
+
'--output',
|
|
1387
|
+
'json',
|
|
1388
|
+
],
|
|
1389
|
+
label: 'Exact SSM parameter remove observation',
|
|
1390
|
+
notFound: /ParameterNotFound/,
|
|
1391
|
+
});
|
|
1392
|
+
if (!response.found)
|
|
1393
|
+
return { present: false, protectionDigest: null };
|
|
1394
|
+
const parameter = record(record(response.value, 'SSM parameter response')['Parameter'], 'SSM parameter');
|
|
1395
|
+
if (parameter['Name'] !== probe.identifier) {
|
|
1396
|
+
throw new Error('SSM parameter remove observation returned another resource identity.');
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
present: true,
|
|
1400
|
+
protectionDigest: digest(canonicalJson({
|
|
1401
|
+
dataType: parameter['DataType'],
|
|
1402
|
+
tier: parameter['Tier'],
|
|
1403
|
+
type: parameter['Type'],
|
|
1404
|
+
})),
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
if (probe.kind === 'aws-secrets-manager-secret') {
|
|
1408
|
+
const response = runRemoveProbeCommand({
|
|
1409
|
+
...input,
|
|
1410
|
+
args: [
|
|
1411
|
+
'secretsmanager',
|
|
1412
|
+
'describe-secret',
|
|
1413
|
+
'--secret-id',
|
|
1414
|
+
probe.identifier,
|
|
1415
|
+
'--output',
|
|
1416
|
+
'json',
|
|
1417
|
+
],
|
|
1418
|
+
label: 'Exact Secrets Manager remove observation',
|
|
1419
|
+
notFound: /ResourceNotFoundException/,
|
|
1420
|
+
});
|
|
1421
|
+
if (!response.found)
|
|
1422
|
+
return { present: false, protectionDigest: null };
|
|
1423
|
+
const secret = record(response.value, 'Secrets Manager secret');
|
|
1424
|
+
if (secret['ARN'] !== probe.identifier &&
|
|
1425
|
+
secret['Name'] !== probe.identifier) {
|
|
1426
|
+
throw new Error('Secrets Manager remove observation returned another resource identity.');
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
present: true,
|
|
1430
|
+
protectionDigest: digest(canonicalJson({
|
|
1431
|
+
kmsKeyId: secret['KmsKeyId'] ?? null,
|
|
1432
|
+
primaryRegion: secret['PrimaryRegion'] ?? null,
|
|
1433
|
+
replicationStatus: secret['ReplicationStatus'] ?? [],
|
|
1434
|
+
})),
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
if ([
|
|
1438
|
+
'aws-events-target',
|
|
1439
|
+
'aws-iam-role-policy',
|
|
1440
|
+
'aws-lambda-event-invoke-config',
|
|
1441
|
+
'aws-lambda-permission',
|
|
1442
|
+
'aws-logs-data-protection-policy',
|
|
1443
|
+
'aws-s3-object',
|
|
1444
|
+
'aws-sqs-queue-policy',
|
|
1445
|
+
].includes(probe.kind)) {
|
|
1446
|
+
const selector = probe.selector;
|
|
1447
|
+
const requiredSelectorKeys = {
|
|
1448
|
+
'aws-events-target': ['rule', 'targetId'],
|
|
1449
|
+
'aws-iam-role-policy': ['policyName', 'roleName'],
|
|
1450
|
+
'aws-lambda-event-invoke-config': ['functionName'],
|
|
1451
|
+
'aws-lambda-permission': ['functionName', 'statementId'],
|
|
1452
|
+
'aws-logs-data-protection-policy': ['logGroupIdentifier'],
|
|
1453
|
+
'aws-s3-object': ['bucket', 'key'],
|
|
1454
|
+
'aws-sqs-queue-policy': ['queueUrl'],
|
|
1455
|
+
};
|
|
1456
|
+
const allowedOptionalKeys = {
|
|
1457
|
+
'aws-events-target': ['eventBusName'],
|
|
1458
|
+
'aws-lambda-event-invoke-config': ['qualifier'],
|
|
1459
|
+
};
|
|
1460
|
+
const requiredKeys = requiredSelectorKeys[probe.kind] ?? [];
|
|
1461
|
+
const allowedKeys = [
|
|
1462
|
+
...requiredKeys,
|
|
1463
|
+
...(allowedOptionalKeys[probe.kind] ?? []),
|
|
1464
|
+
];
|
|
1465
|
+
if (!selector ||
|
|
1466
|
+
requiredKeys.some((key) => typeof selector[key] !== 'string' || !selector[key]) ||
|
|
1467
|
+
Object.keys(selector).some((key) => !allowedKeys.includes(key) ||
|
|
1468
|
+
hasControlCharacters(selector[key] ?? ''))) {
|
|
1469
|
+
throw new Error('Standard composite remove selector is absent.');
|
|
1470
|
+
}
|
|
1471
|
+
let args;
|
|
1472
|
+
let notFound;
|
|
1473
|
+
if (probe.kind === 'aws-events-target') {
|
|
1474
|
+
args = [
|
|
1475
|
+
'events',
|
|
1476
|
+
'list-targets-by-rule',
|
|
1477
|
+
'--rule',
|
|
1478
|
+
selector['rule'],
|
|
1479
|
+
...(selector['eventBusName']
|
|
1480
|
+
? ['--event-bus-name', selector['eventBusName']]
|
|
1481
|
+
: []),
|
|
1482
|
+
'--output',
|
|
1483
|
+
'json',
|
|
1484
|
+
];
|
|
1485
|
+
notFound = /ResourceNotFoundException/;
|
|
1486
|
+
}
|
|
1487
|
+
else if (probe.kind === 'aws-iam-role-policy') {
|
|
1488
|
+
args = [
|
|
1489
|
+
'iam',
|
|
1490
|
+
'get-role-policy',
|
|
1491
|
+
'--role-name',
|
|
1492
|
+
selector['roleName'],
|
|
1493
|
+
'--policy-name',
|
|
1494
|
+
selector['policyName'],
|
|
1495
|
+
'--output',
|
|
1496
|
+
'json',
|
|
1497
|
+
];
|
|
1498
|
+
notFound = /NoSuchEntity/;
|
|
1499
|
+
}
|
|
1500
|
+
else if (probe.kind === 'aws-lambda-event-invoke-config') {
|
|
1501
|
+
args = [
|
|
1502
|
+
'lambda',
|
|
1503
|
+
'get-function-event-invoke-config',
|
|
1504
|
+
'--function-name',
|
|
1505
|
+
selector['functionName'],
|
|
1506
|
+
...(selector['qualifier']
|
|
1507
|
+
? ['--qualifier', selector['qualifier']]
|
|
1508
|
+
: []),
|
|
1509
|
+
'--output',
|
|
1510
|
+
'json',
|
|
1511
|
+
];
|
|
1512
|
+
notFound = /ResourceNotFoundException/;
|
|
1513
|
+
}
|
|
1514
|
+
else if (probe.kind === 'aws-lambda-permission') {
|
|
1515
|
+
args = [
|
|
1516
|
+
'lambda',
|
|
1517
|
+
'get-policy',
|
|
1518
|
+
'--function-name',
|
|
1519
|
+
selector['functionName'],
|
|
1520
|
+
'--output',
|
|
1521
|
+
'json',
|
|
1522
|
+
];
|
|
1523
|
+
notFound = /ResourceNotFoundException/;
|
|
1524
|
+
}
|
|
1525
|
+
else if (probe.kind === 'aws-logs-data-protection-policy') {
|
|
1526
|
+
args = [
|
|
1527
|
+
'logs',
|
|
1528
|
+
'get-data-protection-policy',
|
|
1529
|
+
'--log-group-identifier',
|
|
1530
|
+
selector['logGroupIdentifier'],
|
|
1531
|
+
'--output',
|
|
1532
|
+
'json',
|
|
1533
|
+
];
|
|
1534
|
+
notFound = /ResourceNotFoundException/;
|
|
1535
|
+
}
|
|
1536
|
+
else if (probe.kind === 'aws-s3-object') {
|
|
1537
|
+
args = [
|
|
1538
|
+
's3api',
|
|
1539
|
+
'head-object',
|
|
1540
|
+
'--bucket',
|
|
1541
|
+
selector['bucket'],
|
|
1542
|
+
'--key',
|
|
1543
|
+
selector['key'],
|
|
1544
|
+
'--output',
|
|
1545
|
+
'json',
|
|
1546
|
+
];
|
|
1547
|
+
notFound = /NoSuchKey|NoSuchBucket|NotFound|404/;
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
args = [
|
|
1551
|
+
'sqs',
|
|
1552
|
+
'get-queue-attributes',
|
|
1553
|
+
'--queue-url',
|
|
1554
|
+
selector['queueUrl'],
|
|
1555
|
+
'--attribute-names',
|
|
1556
|
+
'Policy',
|
|
1557
|
+
'--output',
|
|
1558
|
+
'json',
|
|
1559
|
+
];
|
|
1560
|
+
notFound = /AWS\.SimpleQueueService\.NonExistentQueue|QueueDoesNotExist/;
|
|
1561
|
+
}
|
|
1562
|
+
const response = runRemoveProbeCommand({
|
|
1563
|
+
...input,
|
|
1564
|
+
args,
|
|
1565
|
+
label: 'Exact Standard composite remove observation',
|
|
1566
|
+
notFound,
|
|
1567
|
+
});
|
|
1568
|
+
if (!response.found)
|
|
1569
|
+
return { present: false, protectionDigest: null };
|
|
1570
|
+
const value = record(response.value, 'Standard composite remove response');
|
|
1571
|
+
let material = value;
|
|
1572
|
+
let present = true;
|
|
1573
|
+
if (probe.kind === 'aws-events-target') {
|
|
1574
|
+
const targets = value['Targets'];
|
|
1575
|
+
if (!Array.isArray(targets)) {
|
|
1576
|
+
throw new Error('Event target remove observation is malformed.');
|
|
1577
|
+
}
|
|
1578
|
+
const exact = targets.filter((target) => typeof target === 'object' &&
|
|
1579
|
+
target !== null &&
|
|
1580
|
+
!Array.isArray(target) &&
|
|
1581
|
+
target['Id'] === selector['targetId']);
|
|
1582
|
+
if (exact.length > 1) {
|
|
1583
|
+
throw new Error('Event target remove observation is ambiguous.');
|
|
1584
|
+
}
|
|
1585
|
+
present = exact.length === 1;
|
|
1586
|
+
material = exact[0] ?? null;
|
|
1587
|
+
}
|
|
1588
|
+
else if (probe.kind === 'aws-iam-role-policy') {
|
|
1589
|
+
if (value['RoleName'] !== selector['roleName'] ||
|
|
1590
|
+
value['PolicyName'] !== selector['policyName']) {
|
|
1591
|
+
throw new Error('IAM role policy observation returned another identity.');
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
else if (probe.kind === 'aws-lambda-permission') {
|
|
1595
|
+
if (typeof value['Policy'] !== 'string') {
|
|
1596
|
+
throw new Error('Lambda permission observation is malformed.');
|
|
1597
|
+
}
|
|
1598
|
+
const policy = record(parseJson(value['Policy'], 'Lambda function policy'), 'Lambda function policy');
|
|
1599
|
+
const statements = Array.isArray(policy['Statement'])
|
|
1600
|
+
? policy['Statement']
|
|
1601
|
+
: [];
|
|
1602
|
+
const exact = statements.filter((statement) => typeof statement === 'object' &&
|
|
1603
|
+
statement !== null &&
|
|
1604
|
+
!Array.isArray(statement) &&
|
|
1605
|
+
statement['Sid'] ===
|
|
1606
|
+
selector['statementId']);
|
|
1607
|
+
if (exact.length > 1) {
|
|
1608
|
+
throw new Error('Lambda permission observation is ambiguous.');
|
|
1609
|
+
}
|
|
1610
|
+
present = exact.length === 1;
|
|
1611
|
+
material = exact[0] ?? null;
|
|
1612
|
+
}
|
|
1613
|
+
else if (probe.kind === 'aws-logs-data-protection-policy') {
|
|
1614
|
+
if (value['logGroupIdentifier'] !== selector['logGroupIdentifier'] &&
|
|
1615
|
+
value['logGroupArn'] !== selector['logGroupIdentifier']) {
|
|
1616
|
+
throw new Error('CloudWatch Logs protection observation returned another identity.');
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
else if (probe.kind === 'aws-sqs-queue-policy') {
|
|
1620
|
+
const attributes = record(value['Attributes'] ?? {}, 'SQS queue policy attributes');
|
|
1621
|
+
present =
|
|
1622
|
+
typeof attributes['Policy'] === 'string' &&
|
|
1623
|
+
attributes['Policy'].length > 0;
|
|
1624
|
+
material = present
|
|
1625
|
+
? parseJson(attributes['Policy'], 'SQS queue policy document')
|
|
1626
|
+
: null;
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
present,
|
|
1630
|
+
protectionDigest: present ? digest(canonicalJson(material)) : null,
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
if (probe.kind === 'aws-kms-retained') {
|
|
1634
|
+
const response = runRemoveProbeCommand({
|
|
1635
|
+
...input,
|
|
1636
|
+
args: [
|
|
1637
|
+
'kms',
|
|
1638
|
+
'describe-key',
|
|
1639
|
+
'--key-id',
|
|
1640
|
+
probe.identifier,
|
|
1641
|
+
'--output',
|
|
1642
|
+
'json',
|
|
1643
|
+
],
|
|
1644
|
+
label: 'Exact KMS remove observation',
|
|
1645
|
+
notFound: /NotFoundException/,
|
|
1646
|
+
});
|
|
1647
|
+
if (!response.found)
|
|
1648
|
+
return { present: false, protectionDigest: null };
|
|
1649
|
+
const metadata = record(record(response.value, 'KMS describe-key response')['KeyMetadata'], 'KMS key metadata');
|
|
1650
|
+
if (metadata['KeyState'] !== 'Enabled' ||
|
|
1651
|
+
metadata['Enabled'] !== true ||
|
|
1652
|
+
(metadata['Arn'] !== probe.identifier &&
|
|
1653
|
+
metadata['KeyId'] !== probe.identifier &&
|
|
1654
|
+
probe.protectionProfile !== 'aws:kms/alias:Alias')) {
|
|
1655
|
+
throw new Error('Retained KMS identity is missing, disabled, or pending deletion.');
|
|
1656
|
+
}
|
|
1657
|
+
const rotation = runRemoveProbeCommand({
|
|
1658
|
+
...input,
|
|
1659
|
+
args: [
|
|
1660
|
+
'kms',
|
|
1661
|
+
'get-key-rotation-status',
|
|
1662
|
+
'--key-id',
|
|
1663
|
+
String(metadata['KeyId']),
|
|
1664
|
+
'--output',
|
|
1665
|
+
'json',
|
|
1666
|
+
],
|
|
1667
|
+
label: 'Exact KMS rotation protection observation',
|
|
1668
|
+
notFound: /NotFoundException/,
|
|
1669
|
+
});
|
|
1670
|
+
if (!rotation.found ||
|
|
1671
|
+
record(rotation.value, 'KMS rotation status')['KeyRotationEnabled'] !==
|
|
1672
|
+
true) {
|
|
1673
|
+
throw new Error('Retained KMS key rotation protection is absent.');
|
|
1674
|
+
}
|
|
1675
|
+
return {
|
|
1676
|
+
present: true,
|
|
1677
|
+
protectionDigest: digest(canonicalJson({ metadata, rotation: rotation.value })),
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const s3Commands = {
|
|
1681
|
+
'aws:s3/bucket:Bucket': ['head-bucket', '--bucket', probe.identifier],
|
|
1682
|
+
'aws:s3/bucketLifecycleConfigurationV2:BucketLifecycleConfigurationV2': [
|
|
1683
|
+
'get-bucket-lifecycle-configuration',
|
|
1684
|
+
'--bucket',
|
|
1685
|
+
probe.identifier,
|
|
1686
|
+
],
|
|
1687
|
+
'aws:s3/bucketOwnershipControls:BucketOwnershipControls': [
|
|
1688
|
+
'get-bucket-ownership-controls',
|
|
1689
|
+
'--bucket',
|
|
1690
|
+
probe.identifier,
|
|
1691
|
+
],
|
|
1692
|
+
'aws:s3/bucketPolicy:BucketPolicy': [
|
|
1693
|
+
'get-bucket-policy',
|
|
1694
|
+
'--bucket',
|
|
1695
|
+
probe.identifier,
|
|
1696
|
+
],
|
|
1697
|
+
'aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock': [
|
|
1698
|
+
'get-public-access-block',
|
|
1699
|
+
'--bucket',
|
|
1700
|
+
probe.identifier,
|
|
1701
|
+
],
|
|
1702
|
+
'aws:s3/bucketServerSideEncryptionConfigurationV2:BucketServerSideEncryptionConfigurationV2': ['get-bucket-encryption', '--bucket', probe.identifier],
|
|
1703
|
+
'aws:s3/bucketVersioningV2:BucketVersioningV2': [
|
|
1704
|
+
'get-bucket-versioning',
|
|
1705
|
+
'--bucket',
|
|
1706
|
+
probe.identifier,
|
|
1707
|
+
],
|
|
1708
|
+
};
|
|
1709
|
+
const operation = s3Commands[probe.protectionProfile ?? ''];
|
|
1710
|
+
if (!operation) {
|
|
1711
|
+
throw new Error('Retained S3 remove adapter profile is unmapped.');
|
|
1712
|
+
}
|
|
1713
|
+
const response = runRemoveProbeCommand({
|
|
1714
|
+
...input,
|
|
1715
|
+
args: ['s3api', ...operation, '--output', 'json'],
|
|
1716
|
+
label: 'Exact S3 retention protection observation',
|
|
1717
|
+
notFound: /NoSuchBucket|NoSuchLifecycleConfiguration|NoSuchPublicAccessBlockConfiguration|NotFound|404/,
|
|
1718
|
+
});
|
|
1719
|
+
if (!response.found)
|
|
1720
|
+
return { present: false, protectionDigest: null };
|
|
1721
|
+
const value = record(response.value, 'S3 retention protection');
|
|
1722
|
+
const serialized = canonicalJson(value);
|
|
1723
|
+
if ((probe.protectionProfile ===
|
|
1724
|
+
'aws:s3/bucketOwnershipControls:BucketOwnershipControls' &&
|
|
1725
|
+
!serialized.includes('BucketOwnerEnforced')) ||
|
|
1726
|
+
(probe.protectionProfile ===
|
|
1727
|
+
'aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock' &&
|
|
1728
|
+
![
|
|
1729
|
+
'BlockPublicAcls',
|
|
1730
|
+
'BlockPublicPolicy',
|
|
1731
|
+
'IgnorePublicAcls',
|
|
1732
|
+
'RestrictPublicBuckets',
|
|
1733
|
+
].every((key) => serialized.includes(`"${key}":true`))) ||
|
|
1734
|
+
(probe.protectionProfile ===
|
|
1735
|
+
'aws:s3/bucketServerSideEncryptionConfigurationV2:BucketServerSideEncryptionConfigurationV2' &&
|
|
1736
|
+
!serialized.includes('"SSEAlgorithm":"aws:kms"')) ||
|
|
1737
|
+
(probe.protectionProfile ===
|
|
1738
|
+
'aws:s3/bucketVersioningV2:BucketVersioningV2' &&
|
|
1739
|
+
!serialized.includes('"Status":"Enabled"')) ||
|
|
1740
|
+
(probe.protectionProfile ===
|
|
1741
|
+
'aws:s3/bucketLifecycleConfigurationV2:BucketLifecycleConfigurationV2' &&
|
|
1742
|
+
![
|
|
1743
|
+
'confidential-export-fallback-expiry',
|
|
1744
|
+
'confidential-export-delete-markers',
|
|
1745
|
+
].every((id) => serialized.includes(id)))) {
|
|
1746
|
+
throw new Error('Retained S3 protection no longer matches Standard policy.');
|
|
1747
|
+
}
|
|
1748
|
+
return {
|
|
1749
|
+
present: true,
|
|
1750
|
+
protectionDigest: digest(serialized),
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
function observeInfrastructureState(input) {
|
|
1754
|
+
const stage = requiredEnvironment(input.childEnvironment, 'EBK_CONSUMER_STAGE');
|
|
1755
|
+
const region = requiredEnvironment(input.childEnvironment, 'AWS_REGION');
|
|
1756
|
+
const expectedAccount = requiredEnvironment(input.environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/);
|
|
1757
|
+
const caller = record(runJsonCommand(input.spawn, 'aws', ['sts', 'get-caller-identity', '--output', 'json'], {
|
|
1758
|
+
cwd: input.invocation.cwd,
|
|
1759
|
+
env: input.childEnvironment,
|
|
1760
|
+
label: 'AWS caller identity',
|
|
1761
|
+
}), 'AWS caller identity');
|
|
1762
|
+
if (caller['Account'] !== expectedAccount ||
|
|
1763
|
+
typeof caller['Arn'] !== 'string' ||
|
|
1764
|
+
!caller['Arn']) {
|
|
1765
|
+
throw new Error('Read-only infrastructure observer does not match the expected AWS account.');
|
|
1766
|
+
}
|
|
1767
|
+
const stateLocation = exactSstStateLocation(input.environment);
|
|
1768
|
+
const stateKey = `app/${input.project}/${stage}.json`;
|
|
1769
|
+
const stateExists = exactS3KeyExists({
|
|
1770
|
+
childEnvironment: input.childEnvironment,
|
|
1771
|
+
invocation: input.invocation,
|
|
1772
|
+
key: stateKey,
|
|
1773
|
+
label: `Exact SST application state inventory ${stateKey}`,
|
|
1774
|
+
spawn: input.spawn,
|
|
1775
|
+
stateBucketName: stateLocation.bucketName,
|
|
1776
|
+
});
|
|
1777
|
+
const sstPath = input.invocation.args[0];
|
|
1778
|
+
if (!sstPath)
|
|
1779
|
+
throw new Error('Workspace SST path is missing.');
|
|
1780
|
+
const exported = stateExists
|
|
1781
|
+
? runJsonCommand(input.spawn, input.invocation.executable, [
|
|
1782
|
+
sstPath,
|
|
1783
|
+
'state',
|
|
1784
|
+
'export',
|
|
1785
|
+
'--stage',
|
|
1786
|
+
stage,
|
|
1787
|
+
...(input.invocation.args.includes('--profile')
|
|
1788
|
+
? input.invocation.args.slice(input.invocation.args.indexOf('--profile'), input.invocation.args.indexOf('--profile') + 2)
|
|
1789
|
+
: []),
|
|
1790
|
+
], {
|
|
1791
|
+
cwd: input.invocation.cwd,
|
|
1792
|
+
env: input.childEnvironment,
|
|
1793
|
+
label: 'SST state export',
|
|
1794
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
1795
|
+
})
|
|
1796
|
+
: null;
|
|
1797
|
+
const resources = stateExists ? stateResources(exported) : [];
|
|
1798
|
+
const stackIdentities = new Set();
|
|
1799
|
+
for (const resource of resources) {
|
|
1800
|
+
const urn = typeof resource.urn === 'string' ? resource.urn : '';
|
|
1801
|
+
const match = /^urn:pulumi:([^:]+)::([^:]+)::/.exec(urn);
|
|
1802
|
+
if (match)
|
|
1803
|
+
stackIdentities.add(`${match[2]}/${match[1]}`);
|
|
1804
|
+
}
|
|
1805
|
+
if (stateExists && stackIdentities.size !== 1) {
|
|
1806
|
+
throw new Error('SST state export does not identify exactly one Pulumi application and stack.');
|
|
1807
|
+
}
|
|
1808
|
+
const stackIdentity = stateExists
|
|
1809
|
+
? [...stackIdentities][0]
|
|
1810
|
+
: `${input.project}/${stage}`;
|
|
1811
|
+
const [application, observedStage] = stackIdentity.split('/');
|
|
1812
|
+
if (!application || observedStage !== stage) {
|
|
1813
|
+
throw new Error('SST state export stack identity does not match the requested stage.');
|
|
1814
|
+
}
|
|
1815
|
+
if (application !== input.project ||
|
|
1816
|
+
!stateLocation.applications.includes(application)) {
|
|
1817
|
+
throw new Error('SST state export application is outside AWS_PLAN_SST_APPS_JSON.');
|
|
1818
|
+
}
|
|
1819
|
+
const stateLock = observeSstStateLock({
|
|
1820
|
+
app: application,
|
|
1821
|
+
childEnvironment: input.childEnvironment,
|
|
1822
|
+
invocation: input.invocation,
|
|
1823
|
+
spawn: input.spawn,
|
|
1824
|
+
stage,
|
|
1825
|
+
stateBucketName: stateLocation.bucketName,
|
|
1826
|
+
});
|
|
1827
|
+
const nonBookkeepingResources = resources.filter((resource) => !isPulumiBookkeepingResource(resource));
|
|
1828
|
+
const deployableResources = nonBookkeepingResources.filter((resource) => resource.custom === true);
|
|
1829
|
+
const incompleteInventory = nonBookkeepingResources
|
|
1830
|
+
.filter((resource) => resource.custom !== true && resource.custom !== false)
|
|
1831
|
+
.map((resource) => ({
|
|
1832
|
+
type: typeof resource.type === 'string' ? resource.type : 'unknown',
|
|
1833
|
+
urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1834
|
+
}));
|
|
1835
|
+
const refreshPreview = assessPulumiRefreshPreview({
|
|
1836
|
+
childEnvironment: input.childEnvironment,
|
|
1837
|
+
invocation: input.invocation,
|
|
1838
|
+
ordinaryDiff: input.ordinaryDiff,
|
|
1839
|
+
spawn: input.spawn,
|
|
1840
|
+
toolVersion: input.toolVersion,
|
|
1841
|
+
});
|
|
1842
|
+
const refreshInventoryComplete = deployableResources.every((resource) => typeof resource.urn === 'string' &&
|
|
1843
|
+
refreshPreview.observedRefreshUrns.includes(resource.urn));
|
|
1844
|
+
const structuralResources = deployableResources
|
|
1845
|
+
.map((resource) => ({
|
|
1846
|
+
dependencies: Array.isArray(resource.dependencies)
|
|
1847
|
+
? [...resource.dependencies]
|
|
1848
|
+
.filter((value) => typeof value === 'string')
|
|
1849
|
+
.sort()
|
|
1850
|
+
: [],
|
|
1851
|
+
idPresent: typeof resource.id === 'string' && resource.id.length > 0,
|
|
1852
|
+
parent: typeof resource.parent === 'string' ? resource.parent : null,
|
|
1853
|
+
provider: typeof resource.provider === 'string' ? resource.provider : null,
|
|
1854
|
+
type: typeof resource.type === 'string' ? resource.type : 'unknown',
|
|
1855
|
+
urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1856
|
+
}))
|
|
1857
|
+
.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.urn, right.urn));
|
|
1858
|
+
const tagged = deployableResources
|
|
1859
|
+
.map((resource) => ({
|
|
1860
|
+
arn: resourceArn(resource),
|
|
1861
|
+
tags: expectedTags(resource),
|
|
1862
|
+
urn: typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1863
|
+
}))
|
|
1864
|
+
.filter((value) => value.arn !== undefined && value.tags !== undefined);
|
|
1865
|
+
const observedTags = new Map();
|
|
1866
|
+
for (let index = 0; index < tagged.length; index += 100) {
|
|
1867
|
+
const arns = tagged.slice(index, index + 100).map(({ arn }) => arn);
|
|
1868
|
+
const result = record(runJsonCommand(input.spawn, 'aws', [
|
|
1869
|
+
'resourcegroupstaggingapi',
|
|
1870
|
+
'get-resources',
|
|
1871
|
+
'--resource-arn-list',
|
|
1872
|
+
...arns,
|
|
1873
|
+
'--output',
|
|
1874
|
+
'json',
|
|
1875
|
+
], {
|
|
1876
|
+
cwd: input.invocation.cwd,
|
|
1877
|
+
env: input.childEnvironment,
|
|
1878
|
+
label: 'AWS tagged-resource observation',
|
|
1879
|
+
}), 'AWS tagged-resource observation');
|
|
1880
|
+
if (!Array.isArray(result['ResourceTagMappingList'])) {
|
|
1881
|
+
throw new Error('AWS tagged-resource observation is malformed.');
|
|
1882
|
+
}
|
|
1883
|
+
for (const item of result['ResourceTagMappingList']) {
|
|
1884
|
+
const mapping = record(item, 'AWS tagged-resource mapping');
|
|
1885
|
+
if (typeof mapping['ResourceARN'] !== 'string' ||
|
|
1886
|
+
!Array.isArray(mapping['Tags'])) {
|
|
1887
|
+
throw new Error('AWS tagged-resource mapping is malformed.');
|
|
1888
|
+
}
|
|
1889
|
+
observedTags.set(mapping['ResourceARN'], Object.fromEntries(mapping['Tags'].map((tag) => {
|
|
1890
|
+
const value = record(tag, 'AWS resource tag');
|
|
1891
|
+
if (typeof value['Key'] !== 'string' ||
|
|
1892
|
+
typeof value['Value'] !== 'string') {
|
|
1893
|
+
throw new Error('AWS resource tag is malformed.');
|
|
1894
|
+
}
|
|
1895
|
+
return [value['Key'], value['Value']];
|
|
1896
|
+
})));
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
const drifted = tagged
|
|
1900
|
+
.filter(({ arn, tags }) => {
|
|
1901
|
+
const observed = observedTags.get(arn);
|
|
1902
|
+
return observed && canonicalJson(observed) !== canonicalJson(tags);
|
|
1903
|
+
})
|
|
1904
|
+
.map(({ arn, urn }) => ({ arnDigest: digest(arn), urn }));
|
|
1905
|
+
const taggedByUrn = new Map(tagged.map((resource) => [resource.urn, resource]));
|
|
1906
|
+
const resourceByUrn = new Map(deployableResources.map((resource) => [
|
|
1907
|
+
typeof resource.urn === 'string' ? resource.urn : 'unknown',
|
|
1908
|
+
resource,
|
|
1909
|
+
]));
|
|
1910
|
+
const coverage = structuralResources.map(({ type, urn }) => {
|
|
1911
|
+
const taggedResource = taggedByUrn.get(urn);
|
|
1912
|
+
const observed = taggedResource !== undefined && observedTags.has(taggedResource.arn);
|
|
1913
|
+
const materialProperties = materialPropertyPaths(resourceByUrn.get(urn) ?? {});
|
|
1914
|
+
const overlayProperties = [
|
|
1915
|
+
...(REFRESH_PREVIEW_PROPERTY_OVERLAYS[type] ?? []),
|
|
1916
|
+
...(type === 'aws:ssm/parameter:Parameter' &&
|
|
1917
|
+
record(resourceByUrn.get(urn)?.inputs ?? {}, `${urn} inputs`)['type'] !== 'String'
|
|
1918
|
+
? ['inputs.value']
|
|
1919
|
+
: []),
|
|
1920
|
+
].filter((path) => materialProperties.includes(path));
|
|
1921
|
+
const refreshCoversResource = refreshPreview.complete &&
|
|
1922
|
+
refreshPreview.desiredMatchesOrdinary &&
|
|
1923
|
+
incompleteInventory.length === 0 &&
|
|
1924
|
+
refreshPreview.observedRefreshUrns.includes(urn);
|
|
1925
|
+
return {
|
|
1926
|
+
adapter: refreshCoversResource
|
|
1927
|
+
? 'sst-pulumi-refresh-preview'
|
|
1928
|
+
: taggedResource === undefined
|
|
1929
|
+
? 'none'
|
|
1930
|
+
: 'aws-resource-groups-tagging-api',
|
|
1931
|
+
arnDigest: taggedResource === undefined ? null : digest(taggedResource.arn),
|
|
1932
|
+
coveredProperties: refreshCoversResource
|
|
1933
|
+
? [
|
|
1934
|
+
'existence',
|
|
1935
|
+
...materialProperties.filter((path) => !overlayProperties.includes(path)),
|
|
1936
|
+
]
|
|
1937
|
+
: observed
|
|
1938
|
+
? ['existence', 'tags']
|
|
1939
|
+
: [],
|
|
1940
|
+
type,
|
|
1941
|
+
unobservedMaterialProperties: refreshCoversResource
|
|
1942
|
+
? overlayProperties
|
|
1943
|
+
: materialProperties.length > 0
|
|
1944
|
+
? materialProperties
|
|
1945
|
+
: ['service-native-configuration'],
|
|
1946
|
+
urn,
|
|
1947
|
+
};
|
|
1948
|
+
});
|
|
1949
|
+
const unsupported = coverage
|
|
1950
|
+
.filter(({ coveredProperties, unobservedMaterialProperties }) => coveredProperties.length < 2 || unobservedMaterialProperties.length > 0)
|
|
1951
|
+
.map(({ adapter, coveredProperties, type, unobservedMaterialProperties, urn, }) => ({
|
|
1952
|
+
adapter,
|
|
1953
|
+
coveredProperties,
|
|
1954
|
+
type,
|
|
1955
|
+
unobservedMaterialProperties,
|
|
1956
|
+
urn,
|
|
1957
|
+
}));
|
|
1958
|
+
if (!refreshPreview.complete ||
|
|
1959
|
+
!refreshPreview.desiredMatchesOrdinary ||
|
|
1960
|
+
!refreshInventoryComplete ||
|
|
1961
|
+
incompleteInventory.length > 0) {
|
|
1962
|
+
unsupported.push({
|
|
1963
|
+
adapter: 'sst-pulumi-refresh-preview',
|
|
1964
|
+
coveredProperties: [],
|
|
1965
|
+
type: 'full-stack-refresh-preview',
|
|
1966
|
+
unobservedMaterialProperties: [
|
|
1967
|
+
...(!refreshPreview.complete ? ['preview.complete'] : []),
|
|
1968
|
+
...(!refreshPreview.desiredMatchesOrdinary
|
|
1969
|
+
? ['preview.desired-plan-equivalence']
|
|
1970
|
+
: []),
|
|
1971
|
+
...(!refreshInventoryComplete ? ['preview.refresh-inventory'] : []),
|
|
1972
|
+
...(incompleteInventory.length > 0 ? ['state.custom-inventory'] : []),
|
|
1973
|
+
],
|
|
1974
|
+
urn: 'full-stack',
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
const generatedAt = input.now.toISOString();
|
|
1978
|
+
const expiresAt = new Date(input.now.getTime() + 60 * 60_000).toISOString();
|
|
1979
|
+
const observerBinding = {
|
|
1980
|
+
accountFingerprint: digest(expectedAccount),
|
|
1981
|
+
backendBootstrapDigest: input.backendBootstrapDigest,
|
|
1982
|
+
project: input.project,
|
|
1983
|
+
region,
|
|
1984
|
+
sourceCommit: input.sourceCommit,
|
|
1985
|
+
stage,
|
|
1986
|
+
stateLocationDigest: digest(canonicalJson({
|
|
1987
|
+
bucketArn: stateLocation.bucketArn,
|
|
1988
|
+
lockKey: stateLock.key,
|
|
1989
|
+
stateKey: `app/${application}/${stage}.json`,
|
|
1990
|
+
})),
|
|
1991
|
+
};
|
|
1992
|
+
const driftStatus = !refreshPreview.commandSucceeded
|
|
1993
|
+
? 'failed'
|
|
1994
|
+
: refreshPreview.drift.resources.length > 0 || drifted.length > 0
|
|
1995
|
+
? 'drift-detected'
|
|
1996
|
+
: unsupported.length > 0
|
|
1997
|
+
? 'partial-coverage'
|
|
1998
|
+
: 'no-drift';
|
|
1999
|
+
// The full export is canonicalized and hashed in memory. Only the digest is
|
|
2000
|
+
// retained, so changes to IDs, inputs, outputs, protection, or provider
|
|
2001
|
+
// metadata invalidate the state identity without persisting raw state.
|
|
2002
|
+
const stateDigest = digest(canonicalJson(stateExists
|
|
2003
|
+
? { exportedState: exported, observerBinding }
|
|
2004
|
+
: { observerBinding, stateKey, status: 'absent' }));
|
|
2005
|
+
const resourceInventory = deployableResources
|
|
2006
|
+
.map((resource) => {
|
|
2007
|
+
const urn = typeof resource.urn === 'string' ? resource.urn : '';
|
|
2008
|
+
const logicalId = urn.split('::').at(-1) ?? '';
|
|
2009
|
+
const id = typeof resource.id === 'string' && resource.id.length > 0
|
|
2010
|
+
? resource.id
|
|
2011
|
+
: undefined;
|
|
2012
|
+
const arn = resourceArn(resource);
|
|
2013
|
+
const selector = removePhysicalSelector(resource);
|
|
2014
|
+
return {
|
|
2015
|
+
dependencyDigests: Array.isArray(resource.dependencies)
|
|
2016
|
+
? resource.dependencies
|
|
2017
|
+
.filter((dependency) => typeof dependency === 'string' && dependency.length > 0)
|
|
2018
|
+
.map((dependency) => digest(dependency))
|
|
2019
|
+
.sort()
|
|
2020
|
+
: [],
|
|
2021
|
+
logicalId,
|
|
2022
|
+
...(arn ? { physicalArn: arn } : {}),
|
|
2023
|
+
...(id ? { physicalId: id } : {}),
|
|
2024
|
+
...(selector ? { physicalSelector: selector } : {}),
|
|
2025
|
+
physicalIdentityDigest: id || arn
|
|
2026
|
+
? digest(canonicalJson({
|
|
2027
|
+
...(arn ? { arn } : {}),
|
|
2028
|
+
...(id ? { id } : {}),
|
|
2029
|
+
}))
|
|
2030
|
+
: null,
|
|
2031
|
+
protected: typeof resource.protect === 'boolean' ? resource.protect : null,
|
|
2032
|
+
resourceType: typeof resource.type === 'string' ? resource.type : 'unknown',
|
|
2033
|
+
retainedOnDelete: typeof resource.retainOnDelete === 'boolean'
|
|
2034
|
+
? resource.retainOnDelete
|
|
2035
|
+
: null,
|
|
2036
|
+
urnDigest: digest(urn),
|
|
2037
|
+
};
|
|
2038
|
+
})
|
|
2039
|
+
.sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
2040
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
2041
|
+
return {
|
|
2042
|
+
callInventory: [
|
|
2043
|
+
'resourcegroupstaggingapi:GetResources',
|
|
2044
|
+
's3:GetObject',
|
|
2045
|
+
's3:ListBucket',
|
|
2046
|
+
'sst:DiffRefreshPreview',
|
|
2047
|
+
'ssm:GetParameter',
|
|
2048
|
+
'sts:GetCallerIdentity',
|
|
2049
|
+
],
|
|
2050
|
+
drift: {
|
|
2051
|
+
coverage,
|
|
2052
|
+
digest: digest(canonicalJson({
|
|
2053
|
+
coverage,
|
|
2054
|
+
drifted,
|
|
2055
|
+
incompleteInventory,
|
|
2056
|
+
observerBinding,
|
|
2057
|
+
refreshPreviewDigest: refreshPreview.digest,
|
|
2058
|
+
refreshPreviewDrift: normalizedDiffMaterial(refreshPreview.drift),
|
|
2059
|
+
refreshPreviewInventory: refreshPreview.observedRefreshUrns,
|
|
2060
|
+
unsupported,
|
|
2061
|
+
})),
|
|
2062
|
+
expiresAt,
|
|
2063
|
+
generatedAt,
|
|
2064
|
+
status: driftStatus,
|
|
2065
|
+
},
|
|
2066
|
+
observerArnDigest: digest(caller['Arn']),
|
|
2067
|
+
resources: resourceInventory,
|
|
2068
|
+
state: {
|
|
2069
|
+
backend: stateExists
|
|
2070
|
+
? 'sst-pulumi-state-export'
|
|
2071
|
+
: 'sst-pulumi-state-absent',
|
|
2072
|
+
digest: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
|
|
2073
|
+
expiresAt,
|
|
2074
|
+
locked: stateLock.locked,
|
|
2075
|
+
observedAt: generatedAt,
|
|
2076
|
+
stack: stackIdentity,
|
|
2077
|
+
version: digest(canonicalJson({ lock: stateLock.digest, stateDigest })),
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function writeGithubOutputs(environment, values) {
|
|
2082
|
+
const path = environment.GITHUB_OUTPUT?.trim();
|
|
2083
|
+
if (!path)
|
|
2084
|
+
return;
|
|
2085
|
+
(0, node_fs_1.appendFileSync)(path, `${Object.entries(values)
|
|
2086
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
2087
|
+
.join('\n')}\n`, 'utf8');
|
|
2088
|
+
}
|
|
2089
|
+
function writePrivateEvidence(path, contents) {
|
|
2090
|
+
(0, node_fs_1.writeFileSync)(path, contents, { encoding: 'utf8', mode: 0o600 });
|
|
2091
|
+
(0, node_fs_1.chmodSync)(path, 0o600);
|
|
2092
|
+
}
|
|
2093
|
+
function actionRisk(risk, action) {
|
|
2094
|
+
if (action === 'deploy')
|
|
2095
|
+
return risk;
|
|
2096
|
+
const code = action === 'refresh'
|
|
2097
|
+
? 'protected-state-refresh'
|
|
2098
|
+
: 'protected-stack-removal';
|
|
2099
|
+
const message = action === 'refresh'
|
|
2100
|
+
? 'State refresh mutates recorded infrastructure state and requires an exact drift decision and recovery record.'
|
|
2101
|
+
: 'Stack removal is destructive and requires exact recovery evidence and a point-of-no-return acknowledgement.';
|
|
2102
|
+
const requiredApprovals = new Set(risk.requiredApprovals);
|
|
2103
|
+
requiredApprovals.add('point-of-no-return');
|
|
2104
|
+
if (action === 'refresh') {
|
|
2105
|
+
requiredApprovals.add('drift-decision-and-state-recovery');
|
|
2106
|
+
}
|
|
2107
|
+
else {
|
|
2108
|
+
requiredApprovals.add('data-owner-and-recovery');
|
|
2109
|
+
}
|
|
2110
|
+
return {
|
|
2111
|
+
aggregate: [
|
|
2112
|
+
'unknown',
|
|
2113
|
+
'protection-reduction',
|
|
2114
|
+
'security-expansion',
|
|
2115
|
+
].includes(risk.aggregate)
|
|
2116
|
+
? risk.aggregate
|
|
2117
|
+
: 'stateful-or-destructive',
|
|
2118
|
+
applyAllowed: false,
|
|
2119
|
+
findings: [
|
|
2120
|
+
...risk.findings,
|
|
2121
|
+
{
|
|
2122
|
+
class: 'stateful-or-destructive',
|
|
2123
|
+
code,
|
|
2124
|
+
disposition: 'block',
|
|
2125
|
+
message,
|
|
2126
|
+
},
|
|
2127
|
+
],
|
|
2128
|
+
requiredApprovals: [...requiredApprovals].sort(),
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
function validateGovernanceEvidence(reviewed, plan, observedResources = [], importIdentifierDigests = {}) {
|
|
2132
|
+
const imports = plan.diff.resources.filter(({ operation }) => operation === 'import');
|
|
2133
|
+
const replacements = plan.diff.resources.filter(({ operation, replacement }) => operation === 'replace' || replacement);
|
|
2134
|
+
const requiresGovernance = plan.action !== 'deploy' || imports.length > 0 || replacements.length > 0;
|
|
2135
|
+
if (!requiresGovernance)
|
|
2136
|
+
return {};
|
|
2137
|
+
if (!reviewed) {
|
|
2138
|
+
throw new Error('Governed infrastructure mutation evidence is absent from the exact approval record.');
|
|
2139
|
+
}
|
|
2140
|
+
const governance = record(reviewed.governance, 'Infrastructure approval governance evidence');
|
|
2141
|
+
const digests = {};
|
|
2142
|
+
const common = (value, kind, label, extraKeys) => {
|
|
2143
|
+
requireExactKeys(value, [
|
|
2144
|
+
'kind',
|
|
2145
|
+
'materialDigest',
|
|
2146
|
+
'project',
|
|
2147
|
+
'schemaVersion',
|
|
2148
|
+
'sourceCommit',
|
|
2149
|
+
'stage',
|
|
2150
|
+
'stateDigest',
|
|
2151
|
+
...extraKeys,
|
|
2152
|
+
], label);
|
|
2153
|
+
if (value['kind'] !== kind ||
|
|
2154
|
+
value['schemaVersion'] !== 1 ||
|
|
2155
|
+
value['materialDigest'] !== plan.materialDigest ||
|
|
2156
|
+
value['project'] !== plan.projects[0] ||
|
|
2157
|
+
value['sourceCommit'] !== plan.sourceCommit ||
|
|
2158
|
+
value['stage'] !== plan.target.stage ||
|
|
2159
|
+
value['stateDigest'] !== plan.state.digest) {
|
|
2160
|
+
throw new Error(`${label} is stale or belongs to another exact plan.`);
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
if (plan.action === 'refresh') {
|
|
2164
|
+
const decision = record(governance['driftDecision'], 'Refresh drift decision');
|
|
2165
|
+
common(decision, 'ebk-infrastructure-drift-decision', 'Refresh drift decision', [
|
|
2166
|
+
'actor',
|
|
2167
|
+
'approvalRef',
|
|
2168
|
+
'decision',
|
|
2169
|
+
'driftEvidenceDigest',
|
|
2170
|
+
'reviewedSourceChangeRef',
|
|
2171
|
+
'runbookRef',
|
|
2172
|
+
]);
|
|
2173
|
+
if (decision['decision'] !== 'adopt' ||
|
|
2174
|
+
decision['driftEvidenceDigest'] !== plan.driftEvidenceDigest ||
|
|
2175
|
+
decision['reviewedSourceChangeRef'] !== plan.sourceCommit ||
|
|
2176
|
+
!['actor', 'approvalRef', 'runbookRef'].every((key) => typeof decision[key] === 'string' &&
|
|
2177
|
+
decision[key].trim().length > 0)) {
|
|
2178
|
+
throw new Error('Protected refresh requires an exact reviewed adopt decision bound to source, state, observation, and runbook.');
|
|
2179
|
+
}
|
|
2180
|
+
digests['drift-decision'] = digest(canonicalJson(decision));
|
|
2181
|
+
}
|
|
2182
|
+
if (plan.action === 'remove') {
|
|
2183
|
+
const removal = record(governance['removal'], 'Infrastructure removal governance');
|
|
2184
|
+
common(removal, 'ebk-infrastructure-removal-governance', 'Infrastructure removal governance', [
|
|
2185
|
+
'cleanupEvidenceRef',
|
|
2186
|
+
'dataExportEvidenceRef',
|
|
2187
|
+
'dependentsDrained',
|
|
2188
|
+
'foundationPreserved',
|
|
2189
|
+
'resources',
|
|
2190
|
+
'secretRevocationEvidenceRef',
|
|
2191
|
+
]);
|
|
2192
|
+
const resources = removal['resources'];
|
|
2193
|
+
const targets = [...observedResources].sort((left, right) => (0, deterministic_order_js_1.compareUtf16)(left.logicalId, right.logicalId) ||
|
|
2194
|
+
(0, deterministic_order_js_1.compareUtf16)(left.resourceType, right.resourceType));
|
|
2195
|
+
if (targets.length === 0 ||
|
|
2196
|
+
targets.some(({ logicalId, physicalIdentityDigest, resourceType, urnDigest }) => !logicalId ||
|
|
2197
|
+
resourceType === 'unknown' ||
|
|
2198
|
+
!physicalIdentityDigest ||
|
|
2199
|
+
!SHA256.test(physicalIdentityDigest) ||
|
|
2200
|
+
!SHA256.test(urnDigest)) ||
|
|
2201
|
+
!Array.isArray(resources) ||
|
|
2202
|
+
resources.length !== targets.length ||
|
|
2203
|
+
removal['dependentsDrained'] !== true ||
|
|
2204
|
+
removal['foundationPreserved'] !== true ||
|
|
2205
|
+
![
|
|
2206
|
+
'cleanupEvidenceRef',
|
|
2207
|
+
'dataExportEvidenceRef',
|
|
2208
|
+
'secretRevocationEvidenceRef',
|
|
2209
|
+
].every((key) => typeof removal[key] === 'string' &&
|
|
2210
|
+
removal[key].trim().length > 0)) {
|
|
2211
|
+
throw new Error('Protected remove lacks an exact deletion inventory, dependency drain, retained Foundation, data export, secret revocation, or cleanup proof.');
|
|
2212
|
+
}
|
|
2213
|
+
const byIdentity = new Map(resources.map((entry, index) => {
|
|
2214
|
+
const value = record(entry, `Infrastructure removal resource ${index}`);
|
|
2215
|
+
requireExactKeys(value, [
|
|
2216
|
+
'dependencyDigests',
|
|
2217
|
+
'disposition',
|
|
2218
|
+
'logicalId',
|
|
2219
|
+
'physicalIdentityDigest',
|
|
2220
|
+
'protected',
|
|
2221
|
+
'resourceType',
|
|
2222
|
+
'retainedOnDelete',
|
|
2223
|
+
'urnDigest',
|
|
2224
|
+
], `Infrastructure removal resource ${index}`);
|
|
2225
|
+
return [
|
|
2226
|
+
`${value['logicalId']}\u0000${value['resourceType']}`,
|
|
2227
|
+
value,
|
|
2228
|
+
];
|
|
2229
|
+
}));
|
|
2230
|
+
for (const target of targets) {
|
|
2231
|
+
const evidence = byIdentity.get(`${target.logicalId}\u0000${target.resourceType}`);
|
|
2232
|
+
const disposition = target.retainedOnDelete === true ? 'retain' : 'delete';
|
|
2233
|
+
if (!evidence ||
|
|
2234
|
+
evidence['physicalIdentityDigest'] !== target.physicalIdentityDigest ||
|
|
2235
|
+
evidence['urnDigest'] !== target.urnDigest ||
|
|
2236
|
+
canonicalJson(evidence['dependencyDigests']) !==
|
|
2237
|
+
canonicalJson(target.dependencyDigests) ||
|
|
2238
|
+
evidence['protected'] !== target.protected ||
|
|
2239
|
+
evidence['retainedOnDelete'] !== target.retainedOnDelete ||
|
|
2240
|
+
evidence['disposition'] !== disposition) {
|
|
2241
|
+
throw new Error(`Infrastructure remove target ${target.logicalId} does not match the exact live state inventory and retention behavior.`);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (byIdentity.size !== targets.length) {
|
|
2245
|
+
throw new Error('Infrastructure removal target inventory is ambiguous.');
|
|
2246
|
+
}
|
|
2247
|
+
digests.removal = digest(canonicalJson(removal));
|
|
2248
|
+
}
|
|
2249
|
+
if (imports.length > 0) {
|
|
2250
|
+
const importEvidence = record(governance['import'], 'Infrastructure import governance');
|
|
2251
|
+
common(importEvidence, 'ebk-infrastructure-import-governance', 'Infrastructure import governance', ['resources']);
|
|
2252
|
+
const resources = importEvidence['resources'];
|
|
2253
|
+
if (!Array.isArray(resources) || resources.length !== imports.length) {
|
|
2254
|
+
throw new Error('Infrastructure import governance must cover every exact imported resource.');
|
|
2255
|
+
}
|
|
2256
|
+
const byResource = new Map(resources.map((entry, index) => {
|
|
2257
|
+
const value = record(entry, `Infrastructure import resource ${index}`);
|
|
2258
|
+
requireExactKeys(value, [
|
|
2259
|
+
'competingControllerCheck',
|
|
2260
|
+
'logicalId',
|
|
2261
|
+
'ownershipEvidenceRef',
|
|
2262
|
+
'privateIdentifierDigest',
|
|
2263
|
+
'removalBehavior',
|
|
2264
|
+
'removalTestEvidenceRef',
|
|
2265
|
+
'resourceType',
|
|
2266
|
+
], `Infrastructure import resource ${index}`);
|
|
2267
|
+
return [
|
|
2268
|
+
`${value['logicalId']}\u0000${value['resourceType']}`,
|
|
2269
|
+
value,
|
|
2270
|
+
];
|
|
2271
|
+
}));
|
|
2272
|
+
for (const resource of imports) {
|
|
2273
|
+
const key = infrastructureResourceKey(resource);
|
|
2274
|
+
const evidence = byResource.get(key);
|
|
2275
|
+
if (!evidence ||
|
|
2276
|
+
evidence['resourceType'] !== resource.resourceType ||
|
|
2277
|
+
evidence['competingControllerCheck'] !== 'passed' ||
|
|
2278
|
+
!['retain', 'delete'].includes(String(evidence['removalBehavior'])) ||
|
|
2279
|
+
evidence['privateIdentifierDigest'] !== importIdentifierDigests[key] ||
|
|
2280
|
+
!['ownershipEvidenceRef', 'removalTestEvidenceRef'].every((key) => typeof evidence[key] === 'string' &&
|
|
2281
|
+
evidence[key].trim().length > 0)) {
|
|
2282
|
+
throw new Error(`Infrastructure import ${resource.logicalId} lacks exact source-bound private identity, ownership, controller, and tested removal evidence.`);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (byResource.size !== imports.length ||
|
|
2286
|
+
Object.keys(importIdentifierDigests).length !== imports.length) {
|
|
2287
|
+
throw new Error('Infrastructure import governance is ambiguous.');
|
|
2288
|
+
}
|
|
2289
|
+
digests.import = digest(canonicalJson(importEvidence));
|
|
2290
|
+
}
|
|
2291
|
+
if (replacements.length > 0) {
|
|
2292
|
+
const replacementEvidence = record(governance['replacement'], 'Infrastructure replacement governance');
|
|
2293
|
+
common(replacementEvidence, 'ebk-infrastructure-replacement-governance', 'Infrastructure replacement governance', ['resources']);
|
|
2294
|
+
const resources = replacementEvidence['resources'];
|
|
2295
|
+
if (!Array.isArray(resources) || resources.length !== replacements.length) {
|
|
2296
|
+
throw new Error('Infrastructure replacement governance must cover every exact replacement.');
|
|
2297
|
+
}
|
|
2298
|
+
const byLogicalId = new Map(resources.map((entry, index) => {
|
|
2299
|
+
const value = record(entry, `Infrastructure replacement resource ${index}`);
|
|
2300
|
+
requireExactKeys(value, [
|
|
2301
|
+
'dataCopyEvidenceRef',
|
|
2302
|
+
'dependencyCutoverRef',
|
|
2303
|
+
'logicalId',
|
|
2304
|
+
'nameCollisionCheck',
|
|
2305
|
+
'recoveryRunbookRef',
|
|
2306
|
+
'resourceType',
|
|
2307
|
+
'rollbackLimit',
|
|
2308
|
+
'strategy',
|
|
2309
|
+
], `Infrastructure replacement resource ${index}`);
|
|
2310
|
+
return [value['logicalId'], value];
|
|
2311
|
+
}));
|
|
2312
|
+
for (const resource of replacements) {
|
|
2313
|
+
const evidence = byLogicalId.get(resource.logicalId);
|
|
2314
|
+
if (!evidence ||
|
|
2315
|
+
evidence['resourceType'] !== resource.resourceType ||
|
|
2316
|
+
evidence['strategy'] !== resource.replacementStrategy ||
|
|
2317
|
+
evidence['nameCollisionCheck'] !== 'passed' ||
|
|
2318
|
+
![
|
|
2319
|
+
'dataCopyEvidenceRef',
|
|
2320
|
+
'dependencyCutoverRef',
|
|
2321
|
+
'recoveryRunbookRef',
|
|
2322
|
+
'rollbackLimit',
|
|
2323
|
+
].every((key) => typeof evidence[key] === 'string' &&
|
|
2324
|
+
evidence[key].trim().length > 0)) {
|
|
2325
|
+
throw new Error(`Infrastructure replacement ${resource.logicalId} lacks exact cutover, data, rollback, collision, or recovery evidence.`);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (byLogicalId.size !== replacements.length) {
|
|
2329
|
+
throw new Error('Infrastructure replacement governance is ambiguous.');
|
|
2330
|
+
}
|
|
2331
|
+
digests.replacement = digest(canonicalJson(replacementEvidence));
|
|
2332
|
+
}
|
|
2333
|
+
return Object.fromEntries(Object.entries(digests).sort(([left], [right]) => (0, deterministic_order_js_1.compareUtf16)(left, right)));
|
|
2334
|
+
}
|
|
2335
|
+
function computeCurrentInfrastructurePlan(input) {
|
|
2336
|
+
const { action, context, dependencies, environment, invocation, options } = input;
|
|
2337
|
+
const project = context.projectName;
|
|
2338
|
+
if (!project) {
|
|
2339
|
+
throw new Error('Infrastructure planning requires an Nx project name.');
|
|
2340
|
+
}
|
|
2341
|
+
const root = projectRoot(context, options);
|
|
2342
|
+
const descriptorPath = relativeFile(root, options.descriptorPath ?? 'operations/infrastructure-change.json');
|
|
2343
|
+
const descriptor = readDescriptor(descriptorPath, project);
|
|
2344
|
+
const configurationPaths = options.configurationPaths ??
|
|
2345
|
+
(options.configurationPath
|
|
2346
|
+
? [options.configurationPath]
|
|
2347
|
+
: [
|
|
2348
|
+
'configuration/declarations.json',
|
|
2349
|
+
'configuration/declarations.ts',
|
|
2350
|
+
'configuration/local-values.json',
|
|
2351
|
+
]);
|
|
2352
|
+
const configurationInputs = [...new Set(configurationPaths)]
|
|
2353
|
+
.sort()
|
|
2354
|
+
.map((path) => ({
|
|
2355
|
+
contents: (0, node_fs_1.readFileSync)(relativeFile(root, path), 'utf8'),
|
|
2356
|
+
path,
|
|
2357
|
+
}));
|
|
2358
|
+
const spawn = dependencies.spawn ?? defaultSpawn;
|
|
2359
|
+
const sourceCommit = assertCheckedOutSourceCommit(dependencies.spawn, context.root, environment);
|
|
2360
|
+
const repository = requiredEnvironment(environment, 'GITHUB_REPOSITORY');
|
|
2361
|
+
const candidateSourceCommit = input.candidateSourceCommit ?? sourceCommit;
|
|
2362
|
+
const candidate = releaseCandidateBinding({
|
|
2363
|
+
project,
|
|
2364
|
+
repository,
|
|
2365
|
+
sourceCommit: candidateSourceCommit,
|
|
2366
|
+
workspaceRoot: context.root,
|
|
2367
|
+
});
|
|
2368
|
+
const childEnvironment = {
|
|
2369
|
+
...createCandidateBoundSstChildEnvironment(environment, invocation.environment, candidateSourceCommit),
|
|
2370
|
+
EBK_CANDIDATE_IDENTITY: candidate.digest,
|
|
2371
|
+
};
|
|
2372
|
+
let backendBootstrapDigest = digest('dependency-supplied-observer');
|
|
2373
|
+
if (dependencies.observe === undefined) {
|
|
2374
|
+
const stateLocation = exactSstStateLocation(environment);
|
|
2375
|
+
if (!stateLocation.applications.includes(project)) {
|
|
2376
|
+
throw new Error('Current project is outside AWS_PLAN_SST_APPS_JSON.');
|
|
2377
|
+
}
|
|
2378
|
+
const stage = requiredEnvironment(childEnvironment, 'EBK_CONSUMER_STAGE');
|
|
2379
|
+
backendBootstrapDigest = preflightSstBootstrap({
|
|
2380
|
+
accountId: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
|
|
2381
|
+
childEnvironment,
|
|
2382
|
+
invocation,
|
|
2383
|
+
region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
|
|
2384
|
+
spawn,
|
|
2385
|
+
stateBucketName: stateLocation.bucketName,
|
|
2386
|
+
});
|
|
2387
|
+
preflightSstBackendParameter({
|
|
2388
|
+
childEnvironment,
|
|
2389
|
+
expectedType: 'SecureString',
|
|
2390
|
+
invocation,
|
|
2391
|
+
name: `/sst/passphrase/${project}/${stage}`,
|
|
2392
|
+
spawn,
|
|
2393
|
+
});
|
|
2394
|
+
preflightProtectedSstSecrets({
|
|
2395
|
+
application: project,
|
|
2396
|
+
childEnvironment,
|
|
2397
|
+
invocation,
|
|
2398
|
+
spawn,
|
|
2399
|
+
stage,
|
|
2400
|
+
stateBucketName: stateLocation.bucketName,
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
const sstPath = invocation.args[0];
|
|
2404
|
+
if (!sstPath)
|
|
2405
|
+
throw new Error('Workspace SST path is missing.');
|
|
2406
|
+
const versionResult = spawn(invocation.executable, [sstPath, 'version'], {
|
|
2407
|
+
cwd: invocation.cwd,
|
|
2408
|
+
encoding: 'utf8',
|
|
2409
|
+
env: childEnvironment,
|
|
2410
|
+
maxBuffer: MAX_JSON_BYTES,
|
|
2411
|
+
shell: false,
|
|
2412
|
+
});
|
|
2413
|
+
const version = `${versionResult.stdout}\n${versionResult.stderr}`.match(/(?:^|\s)v?(\d+\.\d+\.\d+)(?:\s|$)/)?.[1] ?? 'unknown';
|
|
2414
|
+
const diffResult = spawn(invocation.executable, [sstPath, 'diff', ...invocation.args.slice(2), '--json'], {
|
|
2415
|
+
cwd: invocation.cwd,
|
|
2416
|
+
encoding: 'utf8',
|
|
2417
|
+
env: childEnvironment,
|
|
2418
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
2419
|
+
shell: false,
|
|
2420
|
+
});
|
|
2421
|
+
const diff = (0, index_js_1.normalizeSstDiffV4_17_1)({
|
|
2422
|
+
exitCode: diffResult.status ?? 1,
|
|
2423
|
+
stdout: diffResult.stdout,
|
|
2424
|
+
toolVersion: version,
|
|
2425
|
+
});
|
|
2426
|
+
const importIdentifierDigests = extractPrivateImportIdentifierDigests(diffResult.stdout, diff);
|
|
2427
|
+
const iamValidations = iamPolicyDocuments({
|
|
2428
|
+
accountId: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
|
|
2429
|
+
childEnvironment,
|
|
2430
|
+
diff,
|
|
2431
|
+
diffStdout: diffResult.stdout,
|
|
2432
|
+
invocation,
|
|
2433
|
+
region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
|
|
2434
|
+
sourceCommit: candidateSourceCommit,
|
|
2435
|
+
spawn,
|
|
2436
|
+
});
|
|
2437
|
+
const boundedCostQuotaEvidence = costQuotaEvidence({
|
|
2438
|
+
accountId: requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/),
|
|
2439
|
+
childEnvironment,
|
|
2440
|
+
diff,
|
|
2441
|
+
invocation,
|
|
2442
|
+
policy: descriptor.policy,
|
|
2443
|
+
project,
|
|
2444
|
+
region: requiredEnvironment(childEnvironment, 'AWS_REGION'),
|
|
2445
|
+
sourceCommit: candidateSourceCommit,
|
|
2446
|
+
spawn,
|
|
2447
|
+
});
|
|
2448
|
+
const policySignals = readOptionalArray(environment, 'EBK_INFRA_POLICY_SIGNALS_PATH');
|
|
2449
|
+
const policyBearingIamResources = diff.resources.filter(({ operation, resourceType }) => operation !== 'delete' &&
|
|
2450
|
+
[
|
|
2451
|
+
'aws:iam/policy:Policy',
|
|
2452
|
+
'aws:iam/role:Role',
|
|
2453
|
+
'aws:iam/rolePolicy:RolePolicy',
|
|
2454
|
+
].includes(resourceType));
|
|
2455
|
+
if (policyBearingIamResources.some(({ logicalId }) => !iamValidations.some((validation) => validation.logicalId === logicalId &&
|
|
2456
|
+
validation.analyzer === 'aws-iam-access-analyzer-validate-policy' &&
|
|
2457
|
+
validation.status === 'complete'))) {
|
|
2458
|
+
policySignals.push({
|
|
2459
|
+
class: 'unknown',
|
|
2460
|
+
code: 'iam-access-analyzer-evidence-missing',
|
|
2461
|
+
resourceId: 'iam-change-set',
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
const risk = actionRisk((0, index_js_1.classifyInfrastructureRisk)({
|
|
2465
|
+
diff,
|
|
2466
|
+
iamValidations,
|
|
2467
|
+
policy: descriptor.policy,
|
|
2468
|
+
policySignals,
|
|
2469
|
+
}), action);
|
|
2470
|
+
const now = (dependencies.now ?? (() => new Date()))();
|
|
2471
|
+
const observation = (dependencies.observe ?? observeInfrastructureState)({
|
|
2472
|
+
backendBootstrapDigest,
|
|
2473
|
+
childEnvironment,
|
|
2474
|
+
environment,
|
|
2475
|
+
invocation,
|
|
2476
|
+
now,
|
|
2477
|
+
ordinaryDiff: diff,
|
|
2478
|
+
project,
|
|
2479
|
+
sourceCommit,
|
|
2480
|
+
spawn,
|
|
2481
|
+
toolVersion: version,
|
|
2482
|
+
});
|
|
2483
|
+
const drift = observation.drift;
|
|
2484
|
+
const state = observation.state;
|
|
2485
|
+
const expiresAt = new Date(Math.min(now.getTime() + 60 * 60_000, Date.parse(drift.expiresAt), Date.parse(state.expiresAt)));
|
|
2486
|
+
const account = requiredEnvironment(environment, 'AWS_EXPECTED_ACCOUNT_ID', /^\d{12}$/);
|
|
2487
|
+
const framework = frameworkVersion(context.root);
|
|
2488
|
+
const planAuthorityDigest = planAuthorityPolicyDigest(environment);
|
|
2489
|
+
return {
|
|
2490
|
+
costQuotaEvidence: boundedCostQuotaEvidence,
|
|
2491
|
+
descriptor,
|
|
2492
|
+
diffStderr: diffResult.stderr,
|
|
2493
|
+
diffStdout: diffResult.stdout,
|
|
2494
|
+
iamValidations,
|
|
2495
|
+
importIdentifierDigests,
|
|
2496
|
+
observation,
|
|
2497
|
+
plan: (0, index_js_1.createInfrastructurePlan)({
|
|
2498
|
+
action,
|
|
2499
|
+
configurationMetadataDigest: digest(canonicalJson({
|
|
2500
|
+
configurationInputs,
|
|
2501
|
+
releaseCandidateDigest: candidate.digest,
|
|
2502
|
+
})),
|
|
2503
|
+
diff,
|
|
2504
|
+
driftEvidenceDigest: drift.digest,
|
|
2505
|
+
driftEvidenceExpiresAt: drift.expiresAt,
|
|
2506
|
+
driftEvidenceGeneratedAt: drift.generatedAt,
|
|
2507
|
+
driftStatus: drift.status,
|
|
2508
|
+
expiresAt: expiresAt.toISOString(),
|
|
2509
|
+
frameworkVersion: framework,
|
|
2510
|
+
generatedAt: now.toISOString(),
|
|
2511
|
+
policyProfileDigest: digest(descriptor.raw),
|
|
2512
|
+
projects: [project],
|
|
2513
|
+
repository,
|
|
2514
|
+
risk,
|
|
2515
|
+
sourceCommit,
|
|
2516
|
+
state,
|
|
2517
|
+
target: {
|
|
2518
|
+
accountFingerprint: digest(account),
|
|
2519
|
+
region: requiredEnvironment(childEnvironment, 'AWS_REGION', /^[a-z]{2}(?:-gov)?-[a-z]+(?:-[a-z]+)*-\d$/),
|
|
2520
|
+
stage: requiredEnvironment(childEnvironment, 'EBK_CONSUMER_STAGE'),
|
|
2521
|
+
},
|
|
2522
|
+
versions: {
|
|
2523
|
+
'@empire-builder-kit/nx': framework,
|
|
2524
|
+
...frameworkAndBlueprintVersions(context.root, root),
|
|
2525
|
+
'infrastructure-observer': digest(canonicalJson({
|
|
2526
|
+
callInventory: observation.callInventory,
|
|
2527
|
+
resourceInventory: (observation.resources ?? []).map(({ dependencyDigests, logicalId, physicalIdentityDigest, protected: protectedResource, resourceType, retainedOnDelete, urnDigest, }) => ({
|
|
2528
|
+
dependencyDigests,
|
|
2529
|
+
logicalId,
|
|
2530
|
+
physicalIdentityDigest,
|
|
2531
|
+
protected: protectedResource,
|
|
2532
|
+
resourceType,
|
|
2533
|
+
retainedOnDelete,
|
|
2534
|
+
urnDigest,
|
|
2535
|
+
})),
|
|
2536
|
+
})),
|
|
2537
|
+
'plan-authority-policy': planAuthorityDigest,
|
|
2538
|
+
'iam-validation-evidence': digest(canonicalJson(iamValidations)),
|
|
2539
|
+
'import-private-identifiers': digest(canonicalJson(importIdentifierDigests)),
|
|
2540
|
+
'cost-quota-evidence': digest(canonicalJson(boundedCostQuotaEvidence)),
|
|
2541
|
+
'release-candidate': candidate.digest,
|
|
2542
|
+
sst: index_js_1.SST_DIFF_TOOL_VERSION,
|
|
2543
|
+
},
|
|
2544
|
+
}),
|
|
2545
|
+
planAuthorityDigest,
|
|
2546
|
+
toolVersion: version,
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
async function runInfrastructurePlanExecutor(options, context, environment = process.env, dependencies = {}) {
|
|
2550
|
+
const action = options.action ?? 'deploy';
|
|
2551
|
+
const root = projectRoot(context, options);
|
|
2552
|
+
const invocation = (0, run_sst_command_js_1.buildSstCommandInvocation)(action, options, context, environment);
|
|
2553
|
+
const current = computeCurrentInfrastructurePlan({
|
|
2554
|
+
action,
|
|
2555
|
+
candidateSourceCommit: approvalCandidateSourceCommit(context, {
|
|
2556
|
+
...options,
|
|
2557
|
+
action,
|
|
2558
|
+
}, environment),
|
|
2559
|
+
context,
|
|
2560
|
+
dependencies,
|
|
2561
|
+
environment,
|
|
2562
|
+
invocation,
|
|
2563
|
+
options,
|
|
2564
|
+
});
|
|
2565
|
+
const { descriptor, plan } = current;
|
|
2566
|
+
const project = context.projectName;
|
|
2567
|
+
if (!project) {
|
|
2568
|
+
throw new Error('Infrastructure planning requires an Nx project name.');
|
|
2569
|
+
}
|
|
2570
|
+
const now = (dependencies.now ?? (() => new Date()))();
|
|
2571
|
+
let planned = plan;
|
|
2572
|
+
let approval;
|
|
2573
|
+
let reviewedApproval;
|
|
2574
|
+
let changes = [];
|
|
2575
|
+
if (descriptor.standingPolicyRiskClasses.includes(plan.aggregateRisk) &&
|
|
2576
|
+
plan.requiredApprovals.length === 0) {
|
|
2577
|
+
approval = {
|
|
2578
|
+
approvedAt: now.toISOString(),
|
|
2579
|
+
approver: 'committed-standing-policy',
|
|
2580
|
+
author: 'committed-standing-policy',
|
|
2581
|
+
codeownerReviewed: true,
|
|
2582
|
+
evidenceRefs: {},
|
|
2583
|
+
expiresAt: plan.expiresAt,
|
|
2584
|
+
materialDigest: plan.materialDigest,
|
|
2585
|
+
mode: 'standing-policy',
|
|
2586
|
+
stage: plan.target.stage,
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
else {
|
|
2590
|
+
const approvalRecordPath = options.approvalPath ?? 'operations/infrastructure-approval.json';
|
|
2591
|
+
const approvalPath = relativeFile(context.root, approvalRecordPath);
|
|
2592
|
+
if ((0, node_fs_1.existsSync)(approvalPath)) {
|
|
2593
|
+
const batch = record(readJsonFile(approvalPath, 'Infrastructure approval'), 'Infrastructure approval');
|
|
2594
|
+
const expectedProjects = requiredEnvironment(environment, 'ORDERED_PROJECTS')
|
|
2595
|
+
.split(',')
|
|
2596
|
+
.filter(Boolean);
|
|
2597
|
+
if (batch.kind !== 'ebk-infrastructure-approval-batch' ||
|
|
2598
|
+
batch.schemaVersion !== 1 ||
|
|
2599
|
+
batch.action !== action ||
|
|
2600
|
+
batch.stage !== plan.target.stage ||
|
|
2601
|
+
!Array.isArray(batch.orderedProjects) ||
|
|
2602
|
+
JSON.stringify(batch.orderedProjects) !==
|
|
2603
|
+
JSON.stringify(expectedProjects) ||
|
|
2604
|
+
typeof batch.projects !== 'object' ||
|
|
2605
|
+
batch.projects === null ||
|
|
2606
|
+
Array.isArray(batch.projects) ||
|
|
2607
|
+
Object.keys(batch.projects).sort().join(',') !==
|
|
2608
|
+
[...expectedProjects].sort().join(',')) {
|
|
2609
|
+
throw new Error('Infrastructure approval batch does not bind the exact ordered project set, action, and stage.');
|
|
2610
|
+
}
|
|
2611
|
+
const reviewed = batch.projects[project];
|
|
2612
|
+
if (!reviewed) {
|
|
2613
|
+
throw new Error(`Infrastructure approval batch omitted project ${project}.`);
|
|
2614
|
+
}
|
|
2615
|
+
if (reviewed.approval.recordPath !== approvalRecordPath) {
|
|
2616
|
+
throw new Error('Infrastructure approval batch uses an invalid approval record path.');
|
|
2617
|
+
}
|
|
2618
|
+
reviewedApproval = reviewed;
|
|
2619
|
+
planned = reviewed.planned;
|
|
2620
|
+
approval = reviewed.approval;
|
|
2621
|
+
changes = sourceChanges(dependencies.spawn, context.root, environment, planned.sourceCommit);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const validation = (0, index_js_1.validateInfrastructurePlanForApply)({
|
|
2625
|
+
...(approval ? { approval } : {}),
|
|
2626
|
+
now: now.toISOString(),
|
|
2627
|
+
planned,
|
|
2628
|
+
recomputed: plan,
|
|
2629
|
+
sourceChangesSincePlan: changes,
|
|
2630
|
+
});
|
|
2631
|
+
const governanceEvidenceDigests = approval
|
|
2632
|
+
? validateGovernanceEvidence(reviewedApproval, planned, current.observation.resources, current.importIdentifierDigests)
|
|
2633
|
+
: {};
|
|
2634
|
+
const authorization = {
|
|
2635
|
+
action,
|
|
2636
|
+
...(approval ? { approval } : {}),
|
|
2637
|
+
...(Object.keys(governanceEvidenceDigests).length > 0
|
|
2638
|
+
? { governanceEvidenceDigests }
|
|
2639
|
+
: {}),
|
|
2640
|
+
kind: 'ebk-infrastructure-authorization',
|
|
2641
|
+
plannedMaterialDigest: planned.materialDigest,
|
|
2642
|
+
recomputedMaterialDigest: plan.materialDigest,
|
|
2643
|
+
schemaVersion: 1,
|
|
2644
|
+
sourceChangesSincePlan: changes,
|
|
2645
|
+
validation,
|
|
2646
|
+
};
|
|
2647
|
+
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
2648
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
2649
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
2650
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.raw.json'), current.diffStdout);
|
|
2651
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.private-stderr.txt'), current.diffStderr);
|
|
2652
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), `${JSON.stringify(plan, null, 2)}\n`);
|
|
2653
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), `${JSON.stringify(authorization, null, 2)}\n`);
|
|
2654
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-observer-authority.json'), `${JSON.stringify({
|
|
2655
|
+
kind: 'ebk-infrastructure-observer-authority',
|
|
2656
|
+
observerArnDigest: current.observation.observerArnDigest,
|
|
2657
|
+
planAuthorityDigest: current.planAuthorityDigest,
|
|
2658
|
+
project,
|
|
2659
|
+
schemaVersion: 1,
|
|
2660
|
+
sourceCommit: plan.sourceCommit,
|
|
2661
|
+
stage: plan.target.stage,
|
|
2662
|
+
}, null, 2)}\n`);
|
|
2663
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-command.json'), `${JSON.stringify({
|
|
2664
|
+
action,
|
|
2665
|
+
rawDigest: digest(current.diffStdout),
|
|
2666
|
+
schemaVersion: 1,
|
|
2667
|
+
stderrDigest: digest(current.diffStderr),
|
|
2668
|
+
toolVersion: current.toolVersion,
|
|
2669
|
+
}, null, 2)}\n`);
|
|
2670
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-iam-validation.json'), `${JSON.stringify({
|
|
2671
|
+
kind: 'ebk-infrastructure-iam-validation',
|
|
2672
|
+
schemaVersion: 1,
|
|
2673
|
+
validations: current.iamValidations,
|
|
2674
|
+
}, null, 2)}\n`);
|
|
2675
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-cost-quota.json'), `${JSON.stringify({
|
|
2676
|
+
...current.costQuotaEvidence,
|
|
2677
|
+
kind: 'ebk-infrastructure-cost-quota',
|
|
2678
|
+
schemaVersion: 1,
|
|
2679
|
+
}, null, 2)}\n`);
|
|
2680
|
+
writeGithubOutputs(environment, {
|
|
2681
|
+
material_digest: plan.materialDigest,
|
|
2682
|
+
outcome: validation.outcome,
|
|
2683
|
+
});
|
|
2684
|
+
devkit_1.logger.info(`[ebk-infrastructure-plan] ${validation.outcome}\n${JSON.stringify({ plan, validation }, null, 2)}`);
|
|
2685
|
+
const approvalMode = repositoryInfrastructureApprovalMode(context, plan.target.stage);
|
|
2686
|
+
const waitsForEnvironmentReviewer = approvalMode === 'environment-reviewer' &&
|
|
2687
|
+
plan.target.stage === 'production' &&
|
|
2688
|
+
(0, index_js_1.isEnvironmentReviewerPendingAuthorization)(authorization);
|
|
2689
|
+
return { success: validation.allowed || waitsForEnvironmentReviewer };
|
|
2690
|
+
}
|
|
2691
|
+
async function revalidateInfrastructureMutationAuthorization(action, options, context, invocation, environment = process.env, dependencies = {}) {
|
|
2692
|
+
const root = projectRoot(context, options);
|
|
2693
|
+
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
2694
|
+
const retainedPlan = record(readJsonFile((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), 'Retained infrastructure plan'), 'Retained infrastructure plan');
|
|
2695
|
+
const authorizationRaw = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan-authorization.json'), 'utf8');
|
|
2696
|
+
const retainedAuthorization = record(parseJson(authorizationRaw, 'Retained infrastructure authorization'), 'Retained infrastructure authorization');
|
|
2697
|
+
const stage = invocation.environment.EBK_CONSUMER_STAGE;
|
|
2698
|
+
const approvalMode = repositoryInfrastructureApprovalMode(context, stage);
|
|
2699
|
+
const reviewedSummaryDigest = environment.EBK_ENVIRONMENT_REVIEWED_PLAN_DIGEST?.trim();
|
|
2700
|
+
if (reviewedSummaryDigest && approvalMode !== 'environment-reviewer') {
|
|
2701
|
+
throw new Error('An environment-reviewed plan digest is invalid unless committed policy selects environment-reviewer.');
|
|
2702
|
+
}
|
|
2703
|
+
let environmentAdmission;
|
|
2704
|
+
if (approvalMode === 'environment-reviewer') {
|
|
2705
|
+
if (!reviewedSummaryDigest || !SHA256.test(reviewedSummaryDigest)) {
|
|
2706
|
+
throw new Error(`Protected ${action} requires the exact digest displayed at the production environment reviewer pause.`);
|
|
2707
|
+
}
|
|
2708
|
+
const retainedSummary = record(readJsonFile((0, node_path_1.join)(context.root, '.ebk', 'evidence', 'infrastructure-plan-summary.json'), 'Retained infrastructure plan summary'), 'Retained infrastructure plan summary');
|
|
2709
|
+
const validatedSummary = (0, index_js_1.revalidateInfrastructurePlanSummary)({
|
|
2710
|
+
evidence: projectEvidence(context, retainedSummary.projects.map(({ project }) => project)),
|
|
2711
|
+
expectedDigest: reviewedSummaryDigest,
|
|
2712
|
+
retained: retainedSummary,
|
|
2713
|
+
});
|
|
2714
|
+
if (validatedSummary.action !== action ||
|
|
2715
|
+
validatedSummary.approvalMode !== approvalMode ||
|
|
2716
|
+
validatedSummary.sourceCommit !==
|
|
2717
|
+
requiredEnvironment(environment, 'EBK_SOURCE_COMMIT', SOURCE_COMMIT) ||
|
|
2718
|
+
validatedSummary.stage !== stage ||
|
|
2719
|
+
!validatedSummary.projects.some(({ project }) => project === context.projectName)) {
|
|
2720
|
+
throw new Error('The environment-reviewed infrastructure plan digest does not match the current action, source, stage, or project.');
|
|
2721
|
+
}
|
|
2722
|
+
environmentAdmission = {
|
|
2723
|
+
mode: 'environment-reviewer',
|
|
2724
|
+
planSummaryDigest: reviewedSummaryDigest,
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
const retainedPlanClean = retainedAuthorization.validation?.allowed === true &&
|
|
2728
|
+
retainedAuthorization.validation.outcome === 'plan-clean' &&
|
|
2729
|
+
retainedAuthorization.approval !== undefined;
|
|
2730
|
+
const retainedEnvironmentPending = environmentAdmission !== undefined &&
|
|
2731
|
+
(0, index_js_1.isEnvironmentReviewerPendingAuthorization)(retainedAuthorization);
|
|
2732
|
+
if (retainedAuthorization.kind !== 'ebk-infrastructure-authorization' ||
|
|
2733
|
+
retainedAuthorization.schemaVersion !== 1 ||
|
|
2734
|
+
retainedAuthorization.action !== action ||
|
|
2735
|
+
(!retainedPlanClean && !retainedEnvironmentPending) ||
|
|
2736
|
+
retainedAuthorization.recomputedMaterialDigest !==
|
|
2737
|
+
retainedPlan.materialDigest ||
|
|
2738
|
+
retainedPlan.action !== action ||
|
|
2739
|
+
retainedPlan.target.stage !== invocation.environment.EBK_CONSUMER_STAGE ||
|
|
2740
|
+
!retainedPlan.projects.includes(context.projectName ?? '')) {
|
|
2741
|
+
throw new Error(`Protected ${action} requires the exact retained plan and plan-clean authorization for the current project and stage.`);
|
|
2742
|
+
}
|
|
2743
|
+
const planOptions = {
|
|
2744
|
+
...options,
|
|
2745
|
+
action,
|
|
2746
|
+
};
|
|
2747
|
+
const candidateSourceCommit = approvalCandidateSourceCommit(context, planOptions, environment) ??
|
|
2748
|
+
retainedPlan.sourceCommit;
|
|
2749
|
+
const recomputedCurrent = computeCurrentInfrastructurePlan({
|
|
2750
|
+
action,
|
|
2751
|
+
candidateSourceCommit,
|
|
2752
|
+
context,
|
|
2753
|
+
dependencies,
|
|
2754
|
+
environment,
|
|
2755
|
+
invocation,
|
|
2756
|
+
options: planOptions,
|
|
2757
|
+
});
|
|
2758
|
+
const recomputed = recomputedCurrent.plan;
|
|
2759
|
+
let reviewedApproval;
|
|
2760
|
+
const approvalRecordPath = retainedAuthorization.approval?.recordPath;
|
|
2761
|
+
if (approvalRecordPath) {
|
|
2762
|
+
const batch = record(readJsonFile(relativeFile(context.root, approvalRecordPath), 'Retained infrastructure approval governance'), 'Retained infrastructure approval governance');
|
|
2763
|
+
reviewedApproval = batch.projects[context.projectName ?? ''];
|
|
2764
|
+
}
|
|
2765
|
+
const governanceEvidenceDigests = validateGovernanceEvidence(reviewedApproval, reviewedApproval?.planned ?? retainedPlan, recomputedCurrent.observation.resources, recomputedCurrent.importIdentifierDigests);
|
|
2766
|
+
if (canonicalJson(governanceEvidenceDigests) !==
|
|
2767
|
+
canonicalJson(retainedAuthorization.governanceEvidenceDigests ?? {})) {
|
|
2768
|
+
throw new Error('Governed infrastructure mutation evidence changed after plan authorization.');
|
|
2769
|
+
}
|
|
2770
|
+
const changes = sourceChanges(dependencies.spawn, context.root, environment, retainedPlan.sourceCommit);
|
|
2771
|
+
const now = (dependencies.now ?? (() => new Date()))().toISOString();
|
|
2772
|
+
const validationPlan = retainedAuthorization.approval?.mode === 'human-record'
|
|
2773
|
+
? {
|
|
2774
|
+
...retainedPlan,
|
|
2775
|
+
// The retained authorization already proved the reviewer acted after
|
|
2776
|
+
// the originally reviewed plan. A resume recomputation has a later
|
|
2777
|
+
// generatedAt, which is deliberately non-material.
|
|
2778
|
+
generatedAt: retainedAuthorization.approval.approvedAt,
|
|
2779
|
+
}
|
|
2780
|
+
: retainedPlan;
|
|
2781
|
+
const validation = (0, index_js_1.validateInfrastructurePlanForApply)({
|
|
2782
|
+
...(retainedAuthorization.approval
|
|
2783
|
+
? { approval: retainedAuthorization.approval }
|
|
2784
|
+
: {}),
|
|
2785
|
+
now,
|
|
2786
|
+
planned: validationPlan,
|
|
2787
|
+
recomputed,
|
|
2788
|
+
sourceChangesSincePlan: changes,
|
|
2789
|
+
});
|
|
2790
|
+
const environmentReviewerAllowed = environmentAdmission !== undefined &&
|
|
2791
|
+
(0, index_js_1.isEnvironmentReviewerPendingAuthorization)({
|
|
2792
|
+
...retainedAuthorization,
|
|
2793
|
+
plannedMaterialDigest: validationPlan.materialDigest,
|
|
2794
|
+
recomputedMaterialDigest: recomputed.materialDigest,
|
|
2795
|
+
validation,
|
|
2796
|
+
});
|
|
2797
|
+
const binding = {
|
|
2798
|
+
action,
|
|
2799
|
+
kind: 'ebk-infrastructure-mutation-binding',
|
|
2800
|
+
recomputedMaterialDigest: recomputed.materialDigest,
|
|
2801
|
+
retainedAuthorizationDigest: digest(authorizationRaw),
|
|
2802
|
+
retainedMaterialDigest: retainedPlan.materialDigest,
|
|
2803
|
+
schemaVersion: 1,
|
|
2804
|
+
sourceCommit: recomputed.sourceCommit,
|
|
2805
|
+
stage: recomputed.target.stage,
|
|
2806
|
+
...(Object.keys(governanceEvidenceDigests).length > 0
|
|
2807
|
+
? { governanceEvidenceDigests }
|
|
2808
|
+
: {}),
|
|
2809
|
+
...(environmentAdmission ? { environmentAdmission } : {}),
|
|
2810
|
+
validation,
|
|
2811
|
+
verifiedAt: now,
|
|
2812
|
+
};
|
|
2813
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
2814
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
2815
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-mutation-binding.json'), `${JSON.stringify(binding, null, 2)}\n`);
|
|
2816
|
+
if (!validation.allowed && !environmentReviewerAllowed) {
|
|
2817
|
+
throw new Error(`Protected ${action} authorization became stale before mutation: ${validation.findings
|
|
2818
|
+
.map(({ code }) => code)
|
|
2819
|
+
.join(', ')}.`);
|
|
2820
|
+
}
|
|
2821
|
+
if (action === 'remove') {
|
|
2822
|
+
const spawn = dependencies.spawn ?? defaultSpawn;
|
|
2823
|
+
const childEnvironment = createCandidateBoundSstChildEnvironment(environment, invocation.environment, candidateSourceCommit);
|
|
2824
|
+
const resources = (recomputedCurrent.observation.resources ?? []).map((resource) => {
|
|
2825
|
+
const probe = standardRemoveProbeForResource(resource);
|
|
2826
|
+
const observation = probeStandardRemoveResource({
|
|
2827
|
+
childEnvironment,
|
|
2828
|
+
invocation,
|
|
2829
|
+
probe,
|
|
2830
|
+
spawn,
|
|
2831
|
+
});
|
|
2832
|
+
if (!observation.present ||
|
|
2833
|
+
(resource.retainedOnDelete === true && !observation.protectionDigest)) {
|
|
2834
|
+
throw new Error(`Infrastructure remove target ${resource.logicalId} is absent or lacks exact pre-remove retention protection.`);
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
...resource,
|
|
2838
|
+
adapter: probe,
|
|
2839
|
+
disposition: resource.retainedOnDelete === true ? 'retain' : 'delete',
|
|
2840
|
+
ownership: 'slice-stack',
|
|
2841
|
+
preRemoveProtectionDigest: observation.protectionDigest,
|
|
2842
|
+
};
|
|
2843
|
+
});
|
|
2844
|
+
if (resources.length === 0) {
|
|
2845
|
+
throw new Error('Protected remove requires a non-empty exact private resource inventory.');
|
|
2846
|
+
}
|
|
2847
|
+
const removeInventoryContents = `${JSON.stringify({
|
|
2848
|
+
action,
|
|
2849
|
+
foundation: {
|
|
2850
|
+
disposition: 'excluded-retained',
|
|
2851
|
+
ownership: 'separate-foundation-stack',
|
|
2852
|
+
},
|
|
2853
|
+
kind: 'ebk-infrastructure-remove-inventory',
|
|
2854
|
+
materialDigest: recomputed.materialDigest,
|
|
2855
|
+
project: context.projectName,
|
|
2856
|
+
resources,
|
|
2857
|
+
schemaVersion: 1,
|
|
2858
|
+
secretAndRegistrationChecks: resources
|
|
2859
|
+
.filter(({ adapter }) => adapter.kind === 'aws-secrets-manager-secret' ||
|
|
2860
|
+
adapter.kind === 'aws-ssm-parameter')
|
|
2861
|
+
.map(({ adapter, logicalId, physicalIdentityDigest }) => ({
|
|
2862
|
+
identityDigest: physicalIdentityDigest,
|
|
2863
|
+
kind: adapter.protectionProfile,
|
|
2864
|
+
logicalId,
|
|
2865
|
+
})),
|
|
2866
|
+
sourceCommit: recomputed.sourceCommit,
|
|
2867
|
+
stage: recomputed.target.stage,
|
|
2868
|
+
stateDigest: recomputed.state.digest,
|
|
2869
|
+
}, null, 2)}\n`;
|
|
2870
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-remove-inventory.json'), removeInventoryContents);
|
|
2871
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-mutation-binding.json'), `${JSON.stringify({
|
|
2872
|
+
...binding,
|
|
2873
|
+
removeInventoryDigest: digest(removeInventoryContents),
|
|
2874
|
+
}, null, 2)}\n`);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
async function recordInfrastructureMutationConvergence(action, options, context, invocation, applySucceeded, environment = process.env, dependencies = {}) {
|
|
2878
|
+
const root = projectRoot(context, options);
|
|
2879
|
+
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
2880
|
+
const retainedPlan = record(readJsonFile((0, node_path_1.join)(evidenceDirectory, 'infrastructure-plan.json'), 'Retained infrastructure plan'), 'Retained infrastructure plan');
|
|
2881
|
+
const mutationBindingContents = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-mutation-binding.json'), 'utf8');
|
|
2882
|
+
const mutationBinding = record(parseJson(mutationBindingContents, 'Retained infrastructure mutation binding'), 'Retained infrastructure mutation binding');
|
|
2883
|
+
const project = context.projectName;
|
|
2884
|
+
if (!project ||
|
|
2885
|
+
mutationBinding['kind'] !== 'ebk-infrastructure-mutation-binding' ||
|
|
2886
|
+
mutationBinding['schemaVersion'] !== 1 ||
|
|
2887
|
+
mutationBinding['action'] !== action ||
|
|
2888
|
+
mutationBinding['retainedMaterialDigest'] !== retainedPlan.materialDigest ||
|
|
2889
|
+
mutationBinding['sourceCommit'] !== retainedPlan.sourceCommit ||
|
|
2890
|
+
mutationBinding['stage'] !== retainedPlan.target.stage ||
|
|
2891
|
+
retainedPlan.projects.length !== 1 ||
|
|
2892
|
+
retainedPlan.projects[0] !== project) {
|
|
2893
|
+
throw new Error('Infrastructure convergence requires the exact retained mutation binding.');
|
|
2894
|
+
}
|
|
2895
|
+
const evidenceBinding = {
|
|
2896
|
+
accountFingerprint: retainedPlan.target.accountFingerprint,
|
|
2897
|
+
authorizedMaterialDigest: String(mutationBinding['recomputedMaterialDigest']),
|
|
2898
|
+
mutationBindingDigest: digest(mutationBindingContents),
|
|
2899
|
+
plannedMaterialDigest: retainedPlan.materialDigest,
|
|
2900
|
+
project,
|
|
2901
|
+
region: retainedPlan.target.region,
|
|
2902
|
+
sourceCommit: retainedPlan.sourceCommit,
|
|
2903
|
+
stage: retainedPlan.target.stage,
|
|
2904
|
+
};
|
|
2905
|
+
let result;
|
|
2906
|
+
let converged = false;
|
|
2907
|
+
try {
|
|
2908
|
+
const planOptions = {
|
|
2909
|
+
...options,
|
|
2910
|
+
action,
|
|
2911
|
+
};
|
|
2912
|
+
const candidateSourceCommit = approvalCandidateSourceCommit(context, planOptions, environment) ??
|
|
2913
|
+
retainedPlan.sourceCommit;
|
|
2914
|
+
const current = computeCurrentInfrastructurePlan({
|
|
2915
|
+
action,
|
|
2916
|
+
candidateSourceCommit,
|
|
2917
|
+
context,
|
|
2918
|
+
dependencies,
|
|
2919
|
+
environment,
|
|
2920
|
+
invocation,
|
|
2921
|
+
options: planOptions,
|
|
2922
|
+
});
|
|
2923
|
+
if (action === 'remove') {
|
|
2924
|
+
const removeInventoryContents = (0, node_fs_1.readFileSync)((0, node_path_1.join)(evidenceDirectory, 'infrastructure-remove-inventory.json'), 'utf8');
|
|
2925
|
+
const removeInventory = record(parseJson(removeInventoryContents, 'Retained infrastructure remove inventory'), 'Retained infrastructure remove inventory');
|
|
2926
|
+
const inventoryResources = Array.isArray(removeInventory['resources'])
|
|
2927
|
+
? removeInventory['resources']
|
|
2928
|
+
: [];
|
|
2929
|
+
const foundation = record(removeInventory['foundation'], 'Retained Foundation exclusion');
|
|
2930
|
+
const secretAndRegistrationChecks = Array.isArray(removeInventory['secretAndRegistrationChecks'])
|
|
2931
|
+
? removeInventory['secretAndRegistrationChecks']
|
|
2932
|
+
: [];
|
|
2933
|
+
const inventoryValid = removeInventory['kind'] === 'ebk-infrastructure-remove-inventory' &&
|
|
2934
|
+
removeInventory['schemaVersion'] === 1 &&
|
|
2935
|
+
removeInventory['action'] === 'remove' &&
|
|
2936
|
+
removeInventory['materialDigest'] === retainedPlan.materialDigest &&
|
|
2937
|
+
removeInventory['project'] === project &&
|
|
2938
|
+
removeInventory['sourceCommit'] === retainedPlan.sourceCommit &&
|
|
2939
|
+
removeInventory['stage'] === retainedPlan.target.stage &&
|
|
2940
|
+
mutationBinding['removeInventoryDigest'] ===
|
|
2941
|
+
digest(removeInventoryContents) &&
|
|
2942
|
+
inventoryResources.length > 0 &&
|
|
2943
|
+
foundation['disposition'] === 'excluded-retained' &&
|
|
2944
|
+
foundation['ownership'] === 'separate-foundation-stack';
|
|
2945
|
+
const childEnvironment = createCandidateBoundSstChildEnvironment(environment, invocation.environment, candidateSourceCommit);
|
|
2946
|
+
const spawn = dependencies.spawn ?? defaultSpawn;
|
|
2947
|
+
const findings = [
|
|
2948
|
+
...(applySucceeded ? [] : ['SST remove did not complete.']),
|
|
2949
|
+
...(inventoryValid
|
|
2950
|
+
? []
|
|
2951
|
+
: [
|
|
2952
|
+
'The retained pre-remove deletion inventory is absent or invalid.',
|
|
2953
|
+
]),
|
|
2954
|
+
];
|
|
2955
|
+
const observations = [];
|
|
2956
|
+
if (inventoryValid) {
|
|
2957
|
+
const expectedSensitiveChecks = new Map(secretAndRegistrationChecks.map((entry, index) => {
|
|
2958
|
+
const check = record(entry, `Secret or registration remove check ${index}`);
|
|
2959
|
+
return [check['logicalId'], check];
|
|
2960
|
+
}));
|
|
2961
|
+
for (const [index, value] of inventoryResources.entries()) {
|
|
2962
|
+
try {
|
|
2963
|
+
const resource = record(value, `Retained remove resource ${index}`);
|
|
2964
|
+
const adapter = record(resource['adapter'], `Retained remove adapter ${index}`);
|
|
2965
|
+
const logicalId = typeof resource['logicalId'] === 'string'
|
|
2966
|
+
? resource['logicalId']
|
|
2967
|
+
: `resource-${index}`;
|
|
2968
|
+
const disposition = resource['disposition'];
|
|
2969
|
+
const expectedProtection = resource['preRemoveProtectionDigest'];
|
|
2970
|
+
if (resource['ownership'] !== 'slice-stack' ||
|
|
2971
|
+
!['delete', 'retain'].includes(String(disposition)) ||
|
|
2972
|
+
typeof adapter.identifier !== 'string' ||
|
|
2973
|
+
!adapter.identifier ||
|
|
2974
|
+
![
|
|
2975
|
+
'aws-cloud-control',
|
|
2976
|
+
'aws-events-target',
|
|
2977
|
+
'aws-iam-role-policy',
|
|
2978
|
+
'aws-kms-retained',
|
|
2979
|
+
'aws-lambda-event-invoke-config',
|
|
2980
|
+
'aws-lambda-permission',
|
|
2981
|
+
'aws-logs-data-protection-policy',
|
|
2982
|
+
'aws-s3-retained',
|
|
2983
|
+
'aws-s3-object',
|
|
2984
|
+
'aws-secrets-manager-secret',
|
|
2985
|
+
'aws-sqs-queue-policy',
|
|
2986
|
+
'aws-ssm-parameter',
|
|
2987
|
+
].includes(adapter.kind) ||
|
|
2988
|
+
(adapter.kind === 'aws-cloud-control' &&
|
|
2989
|
+
(typeof adapter.typeName !== 'string' || !adapter.typeName))) {
|
|
2990
|
+
throw new Error('private adapter binding is malformed');
|
|
2991
|
+
}
|
|
2992
|
+
const sensitive = adapter.kind === 'aws-secrets-manager-secret' ||
|
|
2993
|
+
adapter.kind === 'aws-ssm-parameter';
|
|
2994
|
+
if (sensitive) {
|
|
2995
|
+
const check = expectedSensitiveChecks.get(logicalId);
|
|
2996
|
+
if (!check ||
|
|
2997
|
+
check['identityDigest'] !==
|
|
2998
|
+
resource['physicalIdentityDigest'] ||
|
|
2999
|
+
check['kind'] !== adapter.protectionProfile) {
|
|
3000
|
+
throw new Error('secret or registration check is absent or stale');
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
const observation = probeStandardRemoveResource({
|
|
3004
|
+
childEnvironment,
|
|
3005
|
+
invocation,
|
|
3006
|
+
probe: adapter,
|
|
3007
|
+
spawn,
|
|
3008
|
+
});
|
|
3009
|
+
const exact = disposition === 'delete'
|
|
3010
|
+
? !observation.present
|
|
3011
|
+
: observation.present &&
|
|
3012
|
+
typeof expectedProtection === 'string' &&
|
|
3013
|
+
SHA256.test(expectedProtection) &&
|
|
3014
|
+
observation.protectionDigest === expectedProtection;
|
|
3015
|
+
if (!exact) {
|
|
3016
|
+
throw new Error(disposition === 'delete'
|
|
3017
|
+
? 'resource still exists'
|
|
3018
|
+
: 'retained identity or protection changed');
|
|
3019
|
+
}
|
|
3020
|
+
observations.push({
|
|
3021
|
+
adapter: adapter.kind,
|
|
3022
|
+
disposition: String(disposition),
|
|
3023
|
+
logicalId,
|
|
3024
|
+
status: disposition === 'delete'
|
|
3025
|
+
? 'absence-confirmed'
|
|
3026
|
+
: 'retention-confirmed',
|
|
3027
|
+
});
|
|
3028
|
+
}
|
|
3029
|
+
catch (error) {
|
|
3030
|
+
findings.push(`Remove verification failed for resource ${index}: ${error instanceof Error ? error.message : String(error)}.`);
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
if (expectedSensitiveChecks.size !==
|
|
3034
|
+
inventoryResources.filter((value) => {
|
|
3035
|
+
const resource = record(value, 'Sensitive remove resource');
|
|
3036
|
+
const adapter = record(resource['adapter'], 'Sensitive remove adapter');
|
|
3037
|
+
return (adapter['kind'] === 'aws-secrets-manager-secret' ||
|
|
3038
|
+
adapter['kind'] === 'aws-ssm-parameter');
|
|
3039
|
+
}).length) {
|
|
3040
|
+
findings.push('Secret and registration removal checks are incomplete or ambiguous.');
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
const stateAbsent = current.observation.resources?.length === 0 &&
|
|
3044
|
+
current.observation.state.backend === 'sst-pulumi-state-absent';
|
|
3045
|
+
if (!stateAbsent) {
|
|
3046
|
+
findings.push('The exact Slice stack state or resource inventory remains present after remove.');
|
|
3047
|
+
}
|
|
3048
|
+
converged = findings.length === 0;
|
|
3049
|
+
result = {
|
|
3050
|
+
action,
|
|
3051
|
+
...evidenceBinding,
|
|
3052
|
+
converged,
|
|
3053
|
+
findings,
|
|
3054
|
+
inventoryDigest: digest(canonicalJson(removeInventory)),
|
|
3055
|
+
kind: 'ebk-infrastructure-convergence',
|
|
3056
|
+
observations,
|
|
3057
|
+
outcome: converged ? 'converged' : 'cleanup-incomplete',
|
|
3058
|
+
postPlan: current.plan.diff,
|
|
3059
|
+
recomputedMaterialDigest: current.plan.materialDigest,
|
|
3060
|
+
providerCoverage: 'fixed-standard-service-native-adapters',
|
|
3061
|
+
schemaVersion: 1,
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
3064
|
+
else {
|
|
3065
|
+
const postPlanEmpty = current.plan.diff.complete && current.plan.diff.resources.length === 0;
|
|
3066
|
+
const requiredCheckIds = action === 'deploy'
|
|
3067
|
+
? ['deployed-smoke', 'alarm-health', 'data-integrity']
|
|
3068
|
+
: [];
|
|
3069
|
+
const convergence = (0, index_js_1.evaluatePostApplyConvergence)({
|
|
3070
|
+
apply: {
|
|
3071
|
+
// A failed provider operation may have partially applied any
|
|
3072
|
+
// resource in the authorized plan. The empty recomputed post-plan
|
|
3073
|
+
// cannot prove that no provider mutation occurred.
|
|
3074
|
+
changedResourceCount: retainedPlan.diff.resources.length,
|
|
3075
|
+
status: applySucceeded ? 'succeeded' : 'failed',
|
|
3076
|
+
},
|
|
3077
|
+
checks: [],
|
|
3078
|
+
cleanupDebt: [],
|
|
3079
|
+
drift: {
|
|
3080
|
+
findings: [],
|
|
3081
|
+
status: current.plan.driftStatus,
|
|
3082
|
+
unsupported: [],
|
|
3083
|
+
},
|
|
3084
|
+
postPlan: current.plan.diff,
|
|
3085
|
+
requiredCheckIds,
|
|
3086
|
+
});
|
|
3087
|
+
converged = convergence.converged;
|
|
3088
|
+
result = {
|
|
3089
|
+
action,
|
|
3090
|
+
...evidenceBinding,
|
|
3091
|
+
...convergence,
|
|
3092
|
+
kind: 'ebk-infrastructure-convergence',
|
|
3093
|
+
postPlan: current.plan.diff,
|
|
3094
|
+
postApplyDrift: {
|
|
3095
|
+
evidenceDigest: current.plan.driftEvidenceDigest,
|
|
3096
|
+
evidenceExpiresAt: current.plan.driftEvidenceExpiresAt,
|
|
3097
|
+
evidenceGeneratedAt: current.plan.driftEvidenceGeneratedAt,
|
|
3098
|
+
status: current.plan.driftStatus,
|
|
3099
|
+
},
|
|
3100
|
+
recomputedMaterialDigest: current.plan.materialDigest,
|
|
3101
|
+
...(action === 'refresh'
|
|
3102
|
+
? {
|
|
3103
|
+
driftBasis: postPlanEmpty && current.plan.driftStatus === 'no-drift'
|
|
3104
|
+
? 'live-observer-no-drift-and-empty-source-state-plan'
|
|
3105
|
+
: 'live-observer-not-converged',
|
|
3106
|
+
}
|
|
3107
|
+
: {}),
|
|
3108
|
+
schemaVersion: 1,
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
catch (error) {
|
|
3113
|
+
result = {
|
|
3114
|
+
action,
|
|
3115
|
+
...evidenceBinding,
|
|
3116
|
+
converged: false,
|
|
3117
|
+
findings: [error instanceof Error ? error.message : String(error)],
|
|
3118
|
+
kind: 'ebk-infrastructure-convergence',
|
|
3119
|
+
outcome: 'recovery-required',
|
|
3120
|
+
schemaVersion: 1,
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
3124
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
3125
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-convergence.json'), `${JSON.stringify(result, null, 2)}\n`);
|
|
3126
|
+
// A successful deploy intentionally hands off nonterminal
|
|
3127
|
+
// apply-complete-unverified evidence to deployed smoke. The lifecycle
|
|
3128
|
+
// command succeeded, but no caller may treat this return value as terminal
|
|
3129
|
+
// convergence; only finalizeInfrastructureMutationConvergence can write it.
|
|
3130
|
+
return (converged ||
|
|
3131
|
+
(action === 'deploy' &&
|
|
3132
|
+
applySucceeded &&
|
|
3133
|
+
result['outcome'] === 'apply-complete-unverified'));
|
|
3134
|
+
}
|
|
3135
|
+
async function finalizeInfrastructureMutationConvergence(options, context, environment = process.env, dependencies = {}) {
|
|
3136
|
+
const root = projectRoot(context, options);
|
|
3137
|
+
const project = context.projectName;
|
|
3138
|
+
if (!project) {
|
|
3139
|
+
throw new Error('Infrastructure convergence finalization requires one exact Nx project.');
|
|
3140
|
+
}
|
|
3141
|
+
const evidenceDirectory = (0, node_path_1.join)(root, '.ebk', 'evidence');
|
|
3142
|
+
const selectedPath = (value, fallback) => relativeFile(root, value ?? (0, node_path_1.join)('.ebk', 'evidence', fallback));
|
|
3143
|
+
const pendingPath = selectedPath(options.pendingConvergencePath, 'infrastructure-convergence.json');
|
|
3144
|
+
const mutationPath = selectedPath(options.mutationBindingPath, 'infrastructure-mutation-binding.json');
|
|
3145
|
+
const smokePath = selectedPath(options.smokeEvidencePath, 'smoke-deployed.json');
|
|
3146
|
+
const smokeBindingPath = selectedPath(options.smokeBindingPath, 'deployment-binding.json');
|
|
3147
|
+
const operationalPath = selectedPath(options.operationalEvidencePath, 'infrastructure-operational-evidence.json');
|
|
3148
|
+
const pendingContents = (0, node_fs_1.readFileSync)(pendingPath, 'utf8');
|
|
3149
|
+
const mutationContents = (0, node_fs_1.readFileSync)(mutationPath, 'utf8');
|
|
3150
|
+
const smokeContents = (0, node_fs_1.readFileSync)(smokePath, 'utf8');
|
|
3151
|
+
const smokeBindingContents = (0, node_fs_1.readFileSync)(smokeBindingPath, 'utf8');
|
|
3152
|
+
const operationalContents = (0, node_fs_1.readFileSync)(operationalPath, 'utf8');
|
|
3153
|
+
const pending = record(parseJson(pendingContents, 'Pending infrastructure convergence'), 'Pending infrastructure convergence');
|
|
3154
|
+
const mutation = record(parseJson(mutationContents, 'Infrastructure mutation binding'), 'Infrastructure mutation binding');
|
|
3155
|
+
const smoke = record(parseJson(smokeContents, 'Deployed smoke evidence'), 'Deployed smoke evidence');
|
|
3156
|
+
const smokeBinding = record(parseJson(smokeBindingContents, 'Deployed smoke binding'), 'Deployed smoke binding');
|
|
3157
|
+
const operational = record(parseJson(operationalContents, 'Infrastructure operational evidence'), 'Infrastructure operational evidence');
|
|
3158
|
+
const postPlan = record(pending['postPlan'], 'Pending infrastructure post-apply plan');
|
|
3159
|
+
const postApplyDrift = record(pending['postApplyDrift'], 'Pending infrastructure post-apply drift');
|
|
3160
|
+
const smokeSource = record(smoke['source'], 'Deployed smoke source');
|
|
3161
|
+
const smokeEnvironment = record(smoke['environment'], 'Deployed smoke environment');
|
|
3162
|
+
const smokeOperation = record(smoke['operation'], 'Deployed smoke operation');
|
|
3163
|
+
const smokeCleanup = record(smoke['cleanup'], 'Deployed smoke cleanup');
|
|
3164
|
+
const smokeCounts = record(smoke['counts'], 'Deployed smoke counts');
|
|
3165
|
+
const deployment = record(smokeBinding['deployment'], 'Deployed smoke deployment identity');
|
|
3166
|
+
const smokeIdentity = record(smokeBinding['smoke'], 'Deployed smoke workflow identity');
|
|
3167
|
+
const operationalSmoke = record(operational['smoke'], 'Operational smoke binding');
|
|
3168
|
+
const operationalData = record(operational['data'], 'Operational data binding');
|
|
3169
|
+
const operationalFrontend = record(operational['frontend'], 'Operational frontend binding');
|
|
3170
|
+
const alarms = operational['alarms'];
|
|
3171
|
+
const sourceCommit = requiredEnvironment(environment, 'EBK_SOURCE_COMMIT', SOURCE_COMMIT);
|
|
3172
|
+
const stage = requiredEnvironment(environment, 'EBK_STAGE', /^(staging|production)$/);
|
|
3173
|
+
const now = (dependencies.now ?? (() => new Date()))();
|
|
3174
|
+
const driftExpiry = typeof postApplyDrift['evidenceExpiresAt'] === 'string'
|
|
3175
|
+
? Date.parse(postApplyDrift['evidenceExpiresAt'])
|
|
3176
|
+
: Number.NaN;
|
|
3177
|
+
const exactBinding = pending['kind'] === 'ebk-infrastructure-convergence' &&
|
|
3178
|
+
pending['schemaVersion'] === 1 &&
|
|
3179
|
+
pending['action'] === 'deploy' &&
|
|
3180
|
+
pending['converged'] === false &&
|
|
3181
|
+
pending['outcome'] === 'apply-complete-unverified' &&
|
|
3182
|
+
pending['project'] === project &&
|
|
3183
|
+
pending['sourceCommit'] === sourceCommit &&
|
|
3184
|
+
pending['stage'] === stage &&
|
|
3185
|
+
pending['mutationBindingDigest'] === digest(mutationContents) &&
|
|
3186
|
+
mutation['kind'] === 'ebk-infrastructure-mutation-binding' &&
|
|
3187
|
+
mutation['schemaVersion'] === 1 &&
|
|
3188
|
+
mutation['action'] === 'deploy' &&
|
|
3189
|
+
mutation['sourceCommit'] === sourceCommit &&
|
|
3190
|
+
mutation['stage'] === stage;
|
|
3191
|
+
const exactPostApply = postPlan.kind === 'ebk-sst-diff' &&
|
|
3192
|
+
postPlan.schemaVersion === 1 &&
|
|
3193
|
+
postPlan.tool?.version === index_js_1.SST_DIFF_TOOL_VERSION &&
|
|
3194
|
+
postPlan.complete === true &&
|
|
3195
|
+
postPlan.resources.length === 0 &&
|
|
3196
|
+
postApplyDrift['status'] === 'no-drift' &&
|
|
3197
|
+
typeof postApplyDrift['evidenceDigest'] === 'string' &&
|
|
3198
|
+
SHA256.test(postApplyDrift['evidenceDigest']) &&
|
|
3199
|
+
Number.isFinite(driftExpiry) &&
|
|
3200
|
+
driftExpiry > now.getTime();
|
|
3201
|
+
const exactSmoke = smoke['kind'] === 'testing-evidence-result' &&
|
|
3202
|
+
smoke['schemaVersion'] === 1 &&
|
|
3203
|
+
smoke['outcome'] === 'passed' &&
|
|
3204
|
+
smoke['proofStarted'] === true &&
|
|
3205
|
+
smoke['slice'] === project &&
|
|
3206
|
+
Array.isArray(smoke['findings']) &&
|
|
3207
|
+
smoke['findings'].length === 0 &&
|
|
3208
|
+
smokeSource['commit'] === sourceCommit &&
|
|
3209
|
+
smokeEnvironment['deployed'] === true &&
|
|
3210
|
+
smokeEnvironment['stage'] === stage &&
|
|
3211
|
+
smokeOperation['project'] === project &&
|
|
3212
|
+
smokeOperation['proofLayer'] === 'deployed-smoke' &&
|
|
3213
|
+
smokeOperation['target'] === 'smoke-deployed' &&
|
|
3214
|
+
smokeCleanup['outcome'] === 'passed' &&
|
|
3215
|
+
smokeCounts['failed'] === 0 &&
|
|
3216
|
+
smokeCounts['quarantined'] === 0 &&
|
|
3217
|
+
smokeCounts['retried'] === 0 &&
|
|
3218
|
+
smokeCounts['skipped'] === 0 &&
|
|
3219
|
+
typeof smokeCounts['passed'] === 'number' &&
|
|
3220
|
+
smokeCounts['passed'] > 0;
|
|
3221
|
+
const exactSmokeBinding = smokeBinding['kind'] === 'ebk-deployed-smoke-binding' &&
|
|
3222
|
+
smokeBinding['schemaVersion'] === 1 &&
|
|
3223
|
+
smokeBinding['project'] === project &&
|
|
3224
|
+
smokeBinding['sourceCommit'] === sourceCommit &&
|
|
3225
|
+
smokeBinding['stage'] === stage &&
|
|
3226
|
+
deployment['conclusion'] === 'success' &&
|
|
3227
|
+
deployment['headSha'] === sourceCommit &&
|
|
3228
|
+
deployment['workflow'] === 'Deploy' &&
|
|
3229
|
+
smokeIdentity['workflow'] === 'Deployed smoke';
|
|
3230
|
+
const exactOperational = operational['kind'] === 'ebk-infrastructure-operational-evidence' &&
|
|
3231
|
+
operational['schemaVersion'] === 1 &&
|
|
3232
|
+
operational['project'] === project &&
|
|
3233
|
+
operational['sourceCommit'] === sourceCommit &&
|
|
3234
|
+
operational['stage'] === stage &&
|
|
3235
|
+
operationalSmoke['evidenceDigest'] === digest(smokeContents) &&
|
|
3236
|
+
String(operationalSmoke['runId']) === String(smokeIdentity['runId']) &&
|
|
3237
|
+
String(operationalSmoke['runAttempt']) ===
|
|
3238
|
+
String(smokeIdentity['runAttempt']) &&
|
|
3239
|
+
operationalData['evidenceDigest'] === digest(smokeContents) &&
|
|
3240
|
+
operationalData['status'] === 'passed' &&
|
|
3241
|
+
operationalFrontend['status'] === 'passed' &&
|
|
3242
|
+
typeof operationalFrontend['headerEvidenceDigest'] === 'string' &&
|
|
3243
|
+
SHA256.test(operationalFrontend['headerEvidenceDigest']) &&
|
|
3244
|
+
typeof operationalFrontend['visualEvidenceDigest'] === 'string' &&
|
|
3245
|
+
SHA256.test(operationalFrontend['visualEvidenceDigest']) &&
|
|
3246
|
+
typeof operationalFrontend['visualArtifactCount'] === 'number' &&
|
|
3247
|
+
Number.isSafeInteger(operationalFrontend['visualArtifactCount']) &&
|
|
3248
|
+
operationalFrontend['visualArtifactCount'] > 0 &&
|
|
3249
|
+
Array.isArray(alarms) &&
|
|
3250
|
+
alarms.length > 0 &&
|
|
3251
|
+
alarms.every((alarm) => {
|
|
3252
|
+
if (typeof alarm !== 'object' || alarm === null || Array.isArray(alarm)) {
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
3255
|
+
const value = alarm;
|
|
3256
|
+
return (typeof value['idDigest'] === 'string' &&
|
|
3257
|
+
SHA256.test(value['idDigest']) &&
|
|
3258
|
+
value['status'] === 'passed');
|
|
3259
|
+
});
|
|
3260
|
+
const findings = [
|
|
3261
|
+
...(!exactBinding
|
|
3262
|
+
? ['Exact pending mutation and authorization binding is invalid.']
|
|
3263
|
+
: []),
|
|
3264
|
+
...(!exactPostApply
|
|
3265
|
+
? ['Empty post-plan and current no-drift evidence is invalid.']
|
|
3266
|
+
: []),
|
|
3267
|
+
...(!exactSmoke
|
|
3268
|
+
? ['Exact deployed journey and data proof did not pass.']
|
|
3269
|
+
: []),
|
|
3270
|
+
...(!exactSmokeBinding
|
|
3271
|
+
? ['Deployed smoke is not bound to the exact deployment.']
|
|
3272
|
+
: []),
|
|
3273
|
+
...(!exactOperational
|
|
3274
|
+
? ['Alarm and data-integrity evidence is incomplete or stale.']
|
|
3275
|
+
: []),
|
|
3276
|
+
];
|
|
3277
|
+
const converged = findings.length === 0;
|
|
3278
|
+
const result = {
|
|
3279
|
+
action: 'deploy',
|
|
3280
|
+
accountFingerprint: pending['accountFingerprint'],
|
|
3281
|
+
alarmEvidenceDigest: digest(operationalContents),
|
|
3282
|
+
authorizedMaterialDigest: pending['authorizedMaterialDigest'],
|
|
3283
|
+
converged,
|
|
3284
|
+
dataEvidenceDigest: digest(operationalContents),
|
|
3285
|
+
finalizedAt: now.toISOString(),
|
|
3286
|
+
findings,
|
|
3287
|
+
kind: 'ebk-infrastructure-convergence',
|
|
3288
|
+
mutationBindingDigest: pending['mutationBindingDigest'],
|
|
3289
|
+
outcome: converged ? 'converged' : 'recovery-required',
|
|
3290
|
+
pendingConvergenceDigest: digest(pendingContents),
|
|
3291
|
+
plannedMaterialDigest: pending['plannedMaterialDigest'],
|
|
3292
|
+
postApplyDrift,
|
|
3293
|
+
postPlan,
|
|
3294
|
+
project,
|
|
3295
|
+
region: pending['region'],
|
|
3296
|
+
schemaVersion: 1,
|
|
3297
|
+
smokeBindingDigest: digest(smokeBindingContents),
|
|
3298
|
+
smokeEvidenceDigest: digest(smokeContents),
|
|
3299
|
+
sourceCommit,
|
|
3300
|
+
stage,
|
|
3301
|
+
};
|
|
3302
|
+
(0, node_fs_1.mkdirSync)(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
3303
|
+
(0, node_fs_1.chmodSync)(evidenceDirectory, 0o700);
|
|
3304
|
+
writePrivateEvidence((0, node_path_1.join)(evidenceDirectory, 'infrastructure-convergence.json'), `${JSON.stringify(result, null, 2)}\n`);
|
|
3305
|
+
return { success: converged };
|
|
3306
|
+
}
|
|
3307
|
+
const executor = async (options, context) => {
|
|
3308
|
+
try {
|
|
3309
|
+
if (options.operation === 'finalize-convergence') {
|
|
3310
|
+
return await finalizeInfrastructureMutationConvergence(options, context);
|
|
3311
|
+
}
|
|
3312
|
+
return await runInfrastructurePlanExecutor(options, context);
|
|
3313
|
+
}
|
|
3314
|
+
catch (error) {
|
|
3315
|
+
devkit_1.logger.error(error instanceof Error ? error.message : String(error));
|
|
3316
|
+
return { success: false };
|
|
3317
|
+
}
|
|
3318
|
+
};
|
|
3319
|
+
exports.default = executor;
|
|
3320
|
+
//# sourceMappingURL=infrastructure-plan.js.map
|