From c1347fcdb524623437e05a41eda6c6805300ca91 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Thu, 23 Jul 2026 14:46:17 -0400 Subject: [PATCH] fix(scripts): account-id assertion in cfn-stack-decommission; complete the aws-cdk-lib 2.262.0 bump (patched brace-expansion); document CMK cutover trap - cfn-stack-decommission.sh: --account-id is now REQUIRED and asserted against sts get-caller-identity before anything runs. Stack names are no longer org-unique (seahaven-dynamodb-cmk now exists in mgmt AND prod), so a name-only lookup under the wrong ambient profile could report or delete the wrong account's stack (security-review LOGIC-001). - package.json/lock: PR #56's bump-for-patched-brace-expansion landed the commit title but not the pin; package.json still said 2.261.0 and the lockfile still resolved brace-expansion 5.0.6 (GHSA-3jxr-9vmj-r5cp HIGH, blocking the pre-commit scanner). Pin 2.262.0 and regenerate; npm audit now clean. - bin/app.ts comments: slack-bot cutover MUST grant the PROD key ARN, never the account-local mgmt SSM param (LOGIC-005); failed-first-create orphan CMK recovery note (LOGIC-004). --- bin/app.ts | 11 +++++++ package-lock.json | 53 +++++++++++++++++++------------ package.json | 2 +- scripts/cfn-stack-decommission.sh | 23 +++++++++++--- 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/bin/app.ts b/bin/app.ts index 14aee94..b8d1903 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -185,6 +185,17 @@ const prodDynamoCmk = new DynamoDbCmkStack(app, "dynamodb-cmk-prod", { // PrincipalArn is wildcarded by stack prefix because CFN role suffixes rotate; // each bot role must ALSO carry an identity-policy grant on this key ARN // (added in the slack-bot repo's cutover PR). +// +// ⚠️ CUTOVER TRAP: the slack-bot's existing pattern resolves the CMK via SSM +// /seahaven/dynamodb/cmk-arn — that parameter is ACCOUNT-LOCAL and in mgmt +// resolves the MGMT key forever. The cutover PR's identity grant must use the +// PROD key ARN (this stack's DynamoDbCmkArn output), never the mgmt param; +// reusing the SSM pattern grants the wrong key and fails only at runtime. +// +// Recovery note (failed FIRST create): the key is RETAIN, its alias/SSM param +// are not — a CREATE_FAILED rollback orphans an unaliased rotation-enabled +// key. Before re-running the deploy, list unaliased CMKs in prod and schedule +// deletion of the orphan. prodDynamoCmk.key.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowMgmtSlackBotReadViaDynamoDb", diff --git a/package-lock.json b/package-lock.json index 9493f42..c526c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { - "name": "seahaven-account-baseline", + "name": "seahaven-org-baseline", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "seahaven-account-baseline", + "name": "seahaven-org-baseline", "version": "1.0.0", "dependencies": { - "aws-cdk-lib": "2.261.0", + "aws-cdk-lib": "2.262.0", "constructs": "^10.0.0" }, "bin": { @@ -36,9 +36,9 @@ "license": "Apache-2.0" }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "54.2.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.2.0.tgz", - "integrity": "sha512-u3lFXmiXSBozxGBmKTCVD/2mTDsaXzLZH3KYiIQKcB+zPldXOeE5TnooBgKV9ih2jVTo8ML0HpkhfAq2eiv0eQ==", + "version": "54.13.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-54.13.0.tgz", + "integrity": "sha512-C6LS1YxugR7j6BPVjhVsWRu/VNN8eoGDOf7dHafPviz6Y5Nv/FjVIGIPoC6jJmgJcea7VOM9TPHbBEwapCUySg==", "bundleDependencies": [ "jsonschema", "semver" @@ -46,7 +46,7 @@ "license": "Apache-2.0", "dependencies": { "jsonschema": "^1.5.0", - "semver": "^7.8.1" + "semver": "^7.8.5" }, "engines": { "node": ">= 18.0.0" @@ -61,7 +61,7 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { - "version": "7.8.1", + "version": "7.8.5", "inBundle": true, "license": "ISC", "bin": { @@ -887,10 +887,11 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.261.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.261.0.tgz", - "integrity": "sha512-e52e3Abjg0HkuRWlWwtSv5+ZiMW1rhCDdL9ff7lzWXInU8xdfLJpuoimfa0IJwjiNGyphppgg52Azx9M80OA0g==", + "version": "2.262.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.262.0.tgz", + "integrity": "sha512-6zRVoWRd8kQs9ZZ9xhERSm36W8uWS2vW3/g9Zx0xlhvRRiUwTc80bzfJkohKGdT/5AOW+wy6fSDvohavG94pcA==", "bundleDependencies": [ + "@aws/cloudformation-validate", "@balena/dockerignore", "@aws-cdk/cloud-assembly-api", "case", @@ -907,17 +908,18 @@ "dependencies": { "@aws-cdk/asset-awscli-v1": "2.2.282", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.2", - "@aws-cdk/cloud-assembly-api": "^2.2.5", - "@aws-cdk/cloud-assembly-schema": "^54.0.0", + "@aws-cdk/cloud-assembly-api": "^2.2.6", + "@aws-cdk/cloud-assembly-schema": "^54.11.0", + "@aws/cloudformation-validate": "1.5.0-beta", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", - "fs-extra": "^11.3.5", + "fs-extra": "^11.3.6", "ignore": "^5.3.2", "jsonschema": "^1.5.0", "mime-types": "^2.1.35", "minimatch": "^10.2.5", "punycode": "^2.3.1", - "semver": "^7.8.1", + "semver": "^7.8.5", "yaml": "1.10.3" }, "engines": { @@ -928,18 +930,27 @@ } }, "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { - "version": "2.2.5", + "version": "2.2.6", "inBundle": true, "license": "Apache-2.0", "dependencies": { "jsonschema": "^1.5.0", - "semver": "^7.8.0" + "semver": "^7.8.4" }, "engines": { "node": ">= 18.0.0" }, "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": ">=53.28.0" + "@aws-cdk/cloud-assembly-schema": ">=54.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws/cloudformation-validate": { + "version": "1.5.0-beta", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^22.15.0", + "npm": ">=10.5.0" } }, "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { @@ -956,7 +967,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/brace-expansion": { - "version": "5.0.6", + "version": "5.0.7", "inBundle": true, "license": "MIT", "dependencies": { @@ -975,7 +986,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/fs-extra": { - "version": "11.3.5", + "version": "11.3.6", "inBundle": true, "license": "MIT", "dependencies": { @@ -1061,7 +1072,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/semver": { - "version": "7.8.1", + "version": "7.8.5", "inBundle": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 7ec5b7e..2e34a4b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "typescript": "~7.0.2" }, "dependencies": { - "aws-cdk-lib": "2.261.0", + "aws-cdk-lib": "2.262.0", "constructs": "^10.0.0" } } diff --git a/scripts/cfn-stack-decommission.sh b/scripts/cfn-stack-decommission.sh index 2dfe60f..1f29a31 100755 --- a/scripts/cfn-stack-decommission.sh +++ b/scripts/cfn-stack-decommission.sh @@ -12,28 +12,43 @@ # `feedback_cfn_decommission_and_remediation`. # # Usage: -# scripts/cfn-stack-decommission.sh [--profile NAME] [--execute] STACK +# scripts/cfn-stack-decommission.sh --account-id ID [--profile NAME] [--execute] STACK # +# --account-id REQUIRED. Asserted against the credentials' actual account +# before anything runs. Stack names are NOT org-unique +# (seahaven-dynamodb-cmk exists in both mgmt and prod), so a +# name-only lookup with the wrong ambient profile would +# report — or with --execute, DELETE — the wrong account's +# stack and then purge its Retain orphans. # (no --execute) REPORT only: termination protection, consumed exports, # Retain resources (orphans-to-be), in-stack S3 buckets. # --execute Disable termination protection, empty Delete-policy buckets, # delete the stack, wait, then delete the Retain orphans. # set -euo pipefail -PROFILE_ARG=(); EXECUTE=0; STACK="" +PROFILE_ARG=(); EXECUTE=0; STACK=""; EXPECTED_ACCOUNT="" while [[ $# -gt 0 ]]; do case "$1" in --profile) PROFILE_ARG=(--profile "$2"); shift 2 ;; + --account-id) EXPECTED_ACCOUNT="$2"; shift 2 ;; --execute) EXECUTE=1; shift ;; - -h|--help) sed -n '2,22p' "$0"; exit 0 ;; + -h|--help) sed -n '2,28p' "$0"; exit 0 ;; -*) echo "unknown flag: $1" >&2; exit 2 ;; *) STACK="$1"; shift ;; esac done -[[ -z "$STACK" ]] && { echo "usage: $0 [--profile NAME] [--execute] STACK" >&2; exit 2; } +[[ -z "$STACK" ]] && { echo "usage: $0 --account-id ID [--profile NAME] [--execute] STACK" >&2; exit 2; } +[[ -z "$EXPECTED_ACCOUNT" ]] && { echo "ERROR: --account-id is required (stack names are not org-unique)." >&2; exit 2; } aws_() { aws "${PROFILE_ARG[@]}" "$@"; } R="us-east-1" +CALLER_ACCOUNT="$(aws_ sts get-caller-identity --query Account --output text)" +if [[ "$CALLER_ACCOUNT" != "$EXPECTED_ACCOUNT" ]]; then + echo "ABORT: credentials resolve to account $CALLER_ACCOUNT, expected $EXPECTED_ACCOUNT." >&2 + exit 1 +fi +echo "== account: $CALLER_ACCOUNT (verified) ==" + echo "== stack: $STACK ==" aws_ cloudformation describe-stacks --stack-name "$STACK" --region "$R" \ --query 'Stacks[0].{Status:StackStatus,TermProt:EnableTerminationProtection}' --output table