From b8bbadefcba8db993855e54a5055f14e1841fa1e Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Thu, 21 May 2026 01:17:44 +0000 Subject: [PATCH 1/4] feat(bootstrap): extract compute-agentcore, add compute-ecs, bump to v1.1.0 Move bedrock-agentcore:* from observability policy into dedicated compute-agentcore policy. Add compute-ecs policy from DEPLOYMENT_ROLES.md. This enables per-compute-variant bootstrap configuration. Closes: part of #123 Co-Authored-By: Claude Opus 4.6 (1M context) --- cdk/bootstrap/BOOTSTRAP_HASH | 2 +- cdk/bootstrap/BOOTSTRAP_VERSION | 2 +- cdk/bootstrap/policies/compute-agentcore.json | 11 ++ cdk/bootstrap/policies/compute-ecs.json | 26 +++++ cdk/bootstrap/policies/observability.json | 6 - cdk/scripts/generate-bootstrap-artifacts.ts | 10 +- .../bootstrap/policies/compute-agentcore.ts | 39 +++++++ cdk/src/bootstrap/policies/compute-ecs.ts | 54 +++++++++ cdk/src/bootstrap/policies/index.ts | 14 ++- cdk/src/bootstrap/policies/observability.ts | 13 +-- cdk/src/bootstrap/version.ts | 2 +- .../__snapshots__/version.test.ts.snap | 2 +- cdk/test/bootstrap/golden-baseline.test.ts | 104 +++++++++++++++++- cdk/test/bootstrap/policies.test.ts | 98 ++++++++++++++++- 14 files changed, 351 insertions(+), 32 deletions(-) create mode 100644 cdk/bootstrap/policies/compute-agentcore.json create mode 100644 cdk/bootstrap/policies/compute-ecs.json create mode 100644 cdk/src/bootstrap/policies/compute-agentcore.ts create mode 100644 cdk/src/bootstrap/policies/compute-ecs.ts diff --git a/cdk/bootstrap/BOOTSTRAP_HASH b/cdk/bootstrap/BOOTSTRAP_HASH index 167e588..13ad210 100644 --- a/cdk/bootstrap/BOOTSTRAP_HASH +++ b/cdk/bootstrap/BOOTSTRAP_HASH @@ -1 +1 @@ -4892570024965c2e99ef0d9f7ef0a61e4b939ba69c5df52e4bc1647522dad283 +a24d14dde94c546fdf94c7839492e92f612ae91def4660aaacab48d8e8da3146 diff --git a/cdk/bootstrap/BOOTSTRAP_VERSION b/cdk/bootstrap/BOOTSTRAP_VERSION index 3eefcb9..9084fa2 100644 --- a/cdk/bootstrap/BOOTSTRAP_VERSION +++ b/cdk/bootstrap/BOOTSTRAP_VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/cdk/bootstrap/policies/compute-agentcore.json b/cdk/bootstrap/policies/compute-agentcore.json new file mode 100644 index 0000000..cff781d --- /dev/null +++ b/cdk/bootstrap/policies/compute-agentcore.json @@ -0,0 +1,11 @@ +{ + "Statement": [ + { + "Action": "bedrock-agentcore:*", + "Effect": "Allow", + "Resource": "*", + "Sid": "BedrockAgentCore" + } + ], + "Version": "2012-10-17" +} diff --git a/cdk/bootstrap/policies/compute-ecs.json b/cdk/bootstrap/policies/compute-ecs.json new file mode 100644 index 0000000..0825788 --- /dev/null +++ b/cdk/bootstrap/policies/compute-ecs.json @@ -0,0 +1,26 @@ +{ + "Statement": [ + { + "Action": [ + "ecs:CreateCluster", + "ecs:DeleteCluster", + "ecs:DescribeClusters", + "ecs:UpdateCluster", + "ecs:UpdateClusterSettings", + "ecs:PutClusterCapacityProviders", + "ecs:RegisterTaskDefinition", + "ecs:DeregisterTaskDefinition", + "ecs:DescribeTaskDefinition", + "ecs:ListTaskDefinitions", + "ecs:TagResource", + "ecs:UntagResource", + "ecs:ListTagsForResource", + "ecs:PutAccountSetting" + ], + "Effect": "Allow", + "Resource": "*", + "Sid": "ECS" + } + ], + "Version": "2012-10-17" +} diff --git a/cdk/bootstrap/policies/observability.json b/cdk/bootstrap/policies/observability.json index 306c9bf..5e8ed18 100644 --- a/cdk/bootstrap/policies/observability.json +++ b/cdk/bootstrap/policies/observability.json @@ -1,11 +1,5 @@ { "Statement": [ - { - "Action": "bedrock-agentcore:*", - "Effect": "Allow", - "Resource": "*", - "Sid": "BedrockAgentCore" - }, { "Action": [ "bedrock:CreateGuardrail", diff --git a/cdk/scripts/generate-bootstrap-artifacts.ts b/cdk/scripts/generate-bootstrap-artifacts.ts index 78f35b7..7d08935 100644 --- a/cdk/scripts/generate-bootstrap-artifacts.ts +++ b/cdk/scripts/generate-bootstrap-artifacts.ts @@ -20,7 +20,13 @@ import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { infrastructurePolicy, applicationPolicy, observabilityPolicy } from '../src/bootstrap/policies'; +import { + applicationPolicy, + computeAgentcorePolicy, + computeEcsPolicy, + infrastructurePolicy, + observabilityPolicy, +} from '../src/bootstrap/policies'; import { BOOTSTRAP_VERSION, computeBootstrapHash } from '../src/bootstrap/version'; const outDir = join(__dirname, '..', 'bootstrap', 'policies'); @@ -30,6 +36,8 @@ const policies = [ { name: 'infrastructure', fn: infrastructurePolicy }, { name: 'application', fn: applicationPolicy }, { name: 'observability', fn: observabilityPolicy }, + { name: 'compute-agentcore', fn: computeAgentcorePolicy }, + { name: 'compute-ecs', fn: computeEcsPolicy }, ]; for (const { name, fn } of policies) { diff --git a/cdk/src/bootstrap/policies/compute-agentcore.ts b/cdk/src/bootstrap/policies/compute-agentcore.ts new file mode 100644 index 0000000..b13732f --- /dev/null +++ b/cdk/src/bootstrap/policies/compute-agentcore.ts @@ -0,0 +1,39 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { aws_iam as iam } from 'aws-cdk-lib'; + +/** + * Returns the IAM PolicyDocument for the IaCRole-ABCA-Compute-AgentCore role. + * + * Covers: Bedrock AgentCore permissions (extracted from Observability policy + * to enable per-compute-variant bootstrap configuration). + */ +export function computeAgentcorePolicy(): iam.PolicyDocument { + return new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + sid: 'BedrockAgentCore', + effect: iam.Effect.ALLOW, + actions: ['bedrock-agentcore:*'], + resources: ['*'], + }), + ], + }); +} diff --git a/cdk/src/bootstrap/policies/compute-ecs.ts b/cdk/src/bootstrap/policies/compute-ecs.ts new file mode 100644 index 0000000..652b5f7 --- /dev/null +++ b/cdk/src/bootstrap/policies/compute-ecs.ts @@ -0,0 +1,54 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { aws_iam as iam } from 'aws-cdk-lib'; + +/** + * Returns the IAM PolicyDocument for the IaCRole-ABCA-Compute-ECS role. + * + * Covers: ECS cluster and task definition management for the Fargate + * compute backend. + */ +export function computeEcsPolicy(): iam.PolicyDocument { + return new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + sid: 'ECS', + effect: iam.Effect.ALLOW, + actions: [ + 'ecs:CreateCluster', + 'ecs:DeleteCluster', + 'ecs:DescribeClusters', + 'ecs:UpdateCluster', + 'ecs:UpdateClusterSettings', + 'ecs:PutClusterCapacityProviders', + 'ecs:RegisterTaskDefinition', + 'ecs:DeregisterTaskDefinition', + 'ecs:DescribeTaskDefinition', + 'ecs:ListTaskDefinitions', + 'ecs:TagResource', + 'ecs:UntagResource', + 'ecs:ListTagsForResource', + 'ecs:PutAccountSetting', + ], + resources: ['*'], + }), + ], + }); +} diff --git a/cdk/src/bootstrap/policies/index.ts b/cdk/src/bootstrap/policies/index.ts index f848748..ef89b5b 100644 --- a/cdk/src/bootstrap/policies/index.ts +++ b/cdk/src/bootstrap/policies/index.ts @@ -20,16 +20,26 @@ import { aws_iam as iam } from 'aws-cdk-lib'; import { applicationPolicy } from './application'; +import { computeAgentcorePolicy } from './compute-agentcore'; +import { computeEcsPolicy } from './compute-ecs'; import { infrastructurePolicy } from './infrastructure'; import { observabilityPolicy } from './observability'; export { applicationPolicy } from './application'; +export { computeAgentcorePolicy } from './compute-agentcore'; +export { computeEcsPolicy } from './compute-ecs'; export { infrastructurePolicy } from './infrastructure'; export { observabilityPolicy } from './observability'; /** - * Returns all three bootstrap IAM PolicyDocuments as an array. + * Returns all bootstrap IAM PolicyDocuments as an array. */ export function allPolicies(): iam.PolicyDocument[] { - return [infrastructurePolicy(), applicationPolicy(), observabilityPolicy()]; + return [ + infrastructurePolicy(), + applicationPolicy(), + observabilityPolicy(), + computeAgentcorePolicy(), + computeEcsPolicy(), + ]; } diff --git a/cdk/src/bootstrap/policies/observability.ts b/cdk/src/bootstrap/policies/observability.ts index 0c45cbe..89f2a39 100644 --- a/cdk/src/bootstrap/policies/observability.ts +++ b/cdk/src/bootstrap/policies/observability.ts @@ -26,20 +26,13 @@ import { aws_iam as iam } from 'aws-cdk-lib'; /** * Returns the IAM PolicyDocument for the IaCRole-ABCA-Observability role. * - * Covers: Bedrock AgentCore, Bedrock guardrails/logging, CloudWatch Logs - * and Dashboards, CDK asset buckets (S3), KMS for CDK assets, ECR for - * Docker assets, X-Ray, SSM Parameter Store, and STS for CDK. + * Covers: Bedrock guardrails/logging, CloudWatch Logs and Dashboards, + * CDK asset buckets (S3), KMS for CDK assets, ECR for Docker assets, + * X-Ray, SSM Parameter Store, and STS for CDK. */ export function observabilityPolicy(): iam.PolicyDocument { return new iam.PolicyDocument({ statements: [ - new iam.PolicyStatement({ - sid: 'BedrockAgentCore', - effect: iam.Effect.ALLOW, - actions: ['bedrock-agentcore:*'], - resources: ['*'], - }), - new iam.PolicyStatement({ sid: 'BedrockGuardrailsAndLogging', effect: iam.Effect.ALLOW, diff --git a/cdk/src/bootstrap/version.ts b/cdk/src/bootstrap/version.ts index 5a07b9c..344886d 100644 --- a/cdk/src/bootstrap/version.ts +++ b/cdk/src/bootstrap/version.ts @@ -22,7 +22,7 @@ import { createHash } from 'node:crypto'; import { allPolicies } from './policies'; /** Semantic version of the bootstrap policy bundle. */ -export const BOOTSTRAP_VERSION = '1.0.0'; +export const BOOTSTRAP_VERSION = '1.1.0'; /** * Computes a SHA-256 hash over all bootstrap policies. diff --git a/cdk/test/bootstrap/__snapshots__/version.test.ts.snap b/cdk/test/bootstrap/__snapshots__/version.test.ts.snap index c3a8e63..74cd620 100644 --- a/cdk/test/bootstrap/__snapshots__/version.test.ts.snap +++ b/cdk/test/bootstrap/__snapshots__/version.test.ts.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`bootstrap version module hash is stable 1`] = `"4892570024965c2e99ef0d9f7ef0a61e4b939ba69c5df52e4bc1647522dad283"`; +exports[`bootstrap version module hash is stable 1`] = `"a24d14dde94c546fdf94c7839492e92f612ae91def4660aaacab48d8e8da3146"`; diff --git a/cdk/test/bootstrap/golden-baseline.test.ts b/cdk/test/bootstrap/golden-baseline.test.ts index b884d5e..ead2b97 100644 --- a/cdk/test/bootstrap/golden-baseline.test.ts +++ b/cdk/test/bootstrap/golden-baseline.test.ts @@ -22,7 +22,13 @@ import { join } from 'node:path'; import { Stack } from 'aws-cdk-lib'; -import { applicationPolicy, infrastructurePolicy, observabilityPolicy } from '../../src/bootstrap/policies'; +import { + applicationPolicy, + computeAgentcorePolicy, + computeEcsPolicy, + infrastructurePolicy, + observabilityPolicy, +} from '../../src/bootstrap/policies'; /** * Extracts all ```json ... ``` fenced code blocks from a markdown string. @@ -72,23 +78,26 @@ describe('Golden-file parity: TypeScript policies match DEPLOYMENT_ROLES.md', () const goldenInfra = JSON.parse(jsonBlocks[1]); const goldenApp = JSON.parse(jsonBlocks[2]); const goldenObs = JSON.parse(jsonBlocks[3]); + const goldenEcs = JSON.parse(jsonBlocks[4]); // Resolve TypeScript policies via CDK Stack const tsInfra = stack.resolve(infrastructurePolicy()); const tsApp = stack.resolve(applicationPolicy()); const tsObs = stack.resolve(observabilityPolicy()); + const tsComputeAgentcore = stack.resolve(computeAgentcorePolicy()); + const tsComputeEcs = stack.resolve(computeEcsPolicy()); - const testCases: Array<{ + // --- Infrastructure and Application: direct parity --- + const directTestCases: Array<{ name: string; golden: { Statement: Array> }; typescript: { Statement: Array> }; }> = [ { name: 'Infrastructure', golden: goldenInfra, typescript: tsInfra }, { name: 'Application', golden: goldenApp, typescript: tsApp }, - { name: 'Observability', golden: goldenObs, typescript: tsObs }, ]; - for (const { name, golden, typescript } of testCases) { + for (const { name, golden, typescript } of directTestCases) { describe(`${name} policy`, () => { const goldenNorm = normalizeStatements( golden.Statement as Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }>, @@ -116,4 +125,91 @@ describe('Golden-file parity: TypeScript policies match DEPLOYMENT_ROLES.md', () }); }); } + + // --- Observability: golden includes BedrockAgentCore which was extracted --- + describe('Observability policy', () => { + // Filter out the BedrockAgentCore statement from the golden baseline + const goldenObsStatements = goldenObs.Statement as Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }>; + const goldenObsWithoutAgentCore = goldenObsStatements.filter((s) => s.Sid !== 'BedrockAgentCore'); + + const goldenNorm = normalizeStatements(goldenObsWithoutAgentCore); + const tsNorm = normalizeStatements( + (tsObs as { Statement: Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }> }).Statement, + ); + + it('has the same SIDs in the same order (excluding extracted BedrockAgentCore)', () => { + const goldenSids = goldenNorm.map((s) => s.sid); + const tsSids = tsNorm.map((s) => s.sid); + expect(tsSids).toEqual(goldenSids); + }); + + it('has identical actions per statement (sorted)', () => { + for (let i = 0; i < goldenNorm.length; i++) { + expect(tsNorm[i].actions).toEqual(goldenNorm[i].actions); + } + }); + + it('has identical resources per statement (sorted)', () => { + for (let i = 0; i < goldenNorm.length; i++) { + expect(tsNorm[i].resources).toEqual(goldenNorm[i].resources); + } + }); + }); + + // --- Compute-AgentCore: extracted BedrockAgentCore matches golden observability --- + describe('Compute-AgentCore policy', () => { + const goldenObsStatements = goldenObs.Statement as Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }>; + const goldenAgentCoreStatement = goldenObsStatements.filter((s) => s.Sid === 'BedrockAgentCore'); + + const goldenNorm = normalizeStatements(goldenAgentCoreStatement); + const tsNorm = normalizeStatements( + (tsComputeAgentcore as { Statement: Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }> }).Statement, + ); + + it('has the same SIDs', () => { + const goldenSids = goldenNorm.map((s) => s.sid); + const tsSids = tsNorm.map((s) => s.sid); + expect(tsSids).toEqual(goldenSids); + }); + + it('has identical actions (sorted)', () => { + for (let i = 0; i < goldenNorm.length; i++) { + expect(tsNorm[i].actions).toEqual(goldenNorm[i].actions); + } + }); + + it('has identical resources (sorted)', () => { + for (let i = 0; i < goldenNorm.length; i++) { + expect(tsNorm[i].resources).toEqual(goldenNorm[i].resources); + } + }); + }); + + // --- Compute-ECS: matches block 4 (ECS conditional) from markdown --- + describe('Compute-ECS policy', () => { + // Block 4 is a single statement object, not a full policy document + const goldenEcsStatement = goldenEcs as { Sid?: string; Action?: string | string[]; Resource?: string | string[] }; + const goldenNorm = normalizeStatements([goldenEcsStatement]); + const tsNorm = normalizeStatements( + (tsComputeEcs as { Statement: Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }> }).Statement, + ); + + it('has the same SIDs', () => { + const goldenSids = goldenNorm.map((s) => s.sid); + const tsSids = tsNorm.map((s) => s.sid); + expect(tsSids).toEqual(goldenSids); + }); + + it('has identical actions (sorted)', () => { + for (let i = 0; i < goldenNorm.length; i++) { + expect(tsNorm[i].actions).toEqual(goldenNorm[i].actions); + } + }); + + it('has identical resources (sorted)', () => { + for (let i = 0; i < goldenNorm.length; i++) { + expect(tsNorm[i].resources).toEqual(goldenNorm[i].resources); + } + }); + }); }); diff --git a/cdk/test/bootstrap/policies.test.ts b/cdk/test/bootstrap/policies.test.ts index 70fe5a6..7d61ac0 100644 --- a/cdk/test/bootstrap/policies.test.ts +++ b/cdk/test/bootstrap/policies.test.ts @@ -20,6 +20,8 @@ import { Stack } from 'aws-cdk-lib'; import { allPolicies } from '../../src/bootstrap/policies'; import { applicationPolicy } from '../../src/bootstrap/policies/application'; +import { computeAgentcorePolicy } from '../../src/bootstrap/policies/compute-agentcore'; +import { computeEcsPolicy } from '../../src/bootstrap/policies/compute-ecs'; import { infrastructurePolicy } from '../../src/bootstrap/policies/infrastructure'; import { observabilityPolicy } from '../../src/bootstrap/policies/observability'; @@ -164,7 +166,6 @@ describe('IaCRole-ABCA-Observability', () => { const sids = statements.map((s) => s.Sid); expect(sids).toEqual([ - 'BedrockAgentCore', 'BedrockGuardrailsAndLogging', 'CloudWatchLogsAndDashboards', 'S3CDKAssets', @@ -197,7 +198,6 @@ describe('IaCRole-ABCA-Observability', () => { expect(prefixes).toEqual( new Set([ 'bedrock', - 'bedrock-agentcore', 'cloudwatch', 'ecr', 'kms', @@ -211,11 +211,99 @@ describe('IaCRole-ABCA-Observability', () => { }); }); +describe('IaCRole-ABCA-Compute-AgentCore', () => { + const stack = new Stack(); + const doc = computeAgentcorePolicy(); + const json = doc.toJSON(); + const rendered = JSON.stringify(json); + + it('produces valid JSON', () => { + expect(() => JSON.parse(rendered)).not.toThrow(); + }); + + it('is under 6144 characters when serialized', () => { + // AWS managed policy size limit + expect(rendered.length).toBeLessThan(6144); + }); + + it('contains the expected SIDs', () => { + const resolvedDoc = stack.resolve(doc); + const statements = resolvedDoc.Statement as Array<{ Sid: string }>; + const sids = statements.map((s) => s.Sid); + + expect(sids).toEqual(['BedrockAgentCore']); + }); + + it('has unique SIDs', () => { + const resolvedDoc = stack.resolve(doc); + const statements = resolvedDoc.Statement as Array<{ Sid: string }>; + const sids = statements.map((s) => s.Sid); + const unique = new Set(sids); + + expect(unique.size).toBe(sids.length); + }); + + it('covers the expected service prefixes', () => { + const resolvedDoc = stack.resolve(doc); + const statements = resolvedDoc.Statement as Array<{ Action: string | string[] }>; + const allActions = statements.flatMap((s) => + Array.isArray(s.Action) ? s.Action : [s.Action], + ); + const prefixes = new Set(allActions.map((a) => a.split(':')[0])); + + expect(prefixes).toEqual(new Set(['bedrock-agentcore'])); + }); +}); + +describe('IaCRole-ABCA-Compute-ECS', () => { + const stack = new Stack(); + const doc = computeEcsPolicy(); + const json = doc.toJSON(); + const rendered = JSON.stringify(json); + + it('produces valid JSON', () => { + expect(() => JSON.parse(rendered)).not.toThrow(); + }); + + it('is under 6144 characters when serialized', () => { + // AWS managed policy size limit + expect(rendered.length).toBeLessThan(6144); + }); + + it('contains the expected SIDs', () => { + const resolvedDoc = stack.resolve(doc); + const statements = resolvedDoc.Statement as Array<{ Sid: string }>; + const sids = statements.map((s) => s.Sid); + + expect(sids).toEqual(['ECS']); + }); + + it('has unique SIDs', () => { + const resolvedDoc = stack.resolve(doc); + const statements = resolvedDoc.Statement as Array<{ Sid: string }>; + const sids = statements.map((s) => s.Sid); + const unique = new Set(sids); + + expect(unique.size).toBe(sids.length); + }); + + it('covers the expected service prefixes', () => { + const resolvedDoc = stack.resolve(doc); + const statements = resolvedDoc.Statement as Array<{ Action: string | string[] }>; + const allActions = statements.flatMap((s) => + Array.isArray(s.Action) ? s.Action : [s.Action], + ); + const prefixes = new Set(allActions.map((a) => a.split(':')[0])); + + expect(prefixes).toEqual(new Set(['ecs'])); + }); +}); + describe('Cross-policy validation', () => { const stack = new Stack(); const policies = allPolicies(); - it('all SIDs are globally unique across all three policies', () => { + it('all SIDs are globally unique across all policies', () => { const allSids: string[] = []; for (const policy of policies) { @@ -228,8 +316,8 @@ describe('Cross-policy validation', () => { expect(unique.size).toBe(allSids.length); }); - it('returns exactly 3 policies', () => { - expect(policies).toHaveLength(3); + it('returns exactly 5 policies', () => { + expect(policies).toHaveLength(5); }); it('every policy is under 6144 character limit', () => { From 6c2fc9ebe5904222a8bdaec4f2a46ced1b4e977c Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Thu, 21 May 2026 01:25:20 +0000 Subject: [PATCH 2/4] feat(bootstrap): add custom template generator with compute-variant support Generates a custom CDK bootstrap template that replaces AdministratorAccess with inline least-privilege policies. Supports per-compute-variant selection via ComputeTypes parameter. Adds PolicyVersion/Hash/Set CF outputs. Co-Authored-By: Claude Opus 4.6 (1M context) --- cdk/bootstrap/bootstrap-template.yaml | 1326 +++++++++++++++++ cdk/package.json | 4 +- cdk/scripts/generate-bootstrap-template.ts | 233 +++ cdk/test/bootstrap/bootstrap-template.test.ts | 215 +++ yarn.lock | 90 +- 5 files changed, 1854 insertions(+), 14 deletions(-) create mode 100644 cdk/bootstrap/bootstrap-template.yaml create mode 100644 cdk/scripts/generate-bootstrap-template.ts create mode 100644 cdk/test/bootstrap/bootstrap-template.test.ts diff --git a/cdk/bootstrap/bootstrap-template.yaml b/cdk/bootstrap/bootstrap-template.yaml new file mode 100644 index 0000000..fbe054f --- /dev/null +++ b/cdk/bootstrap/bootstrap-template.yaml @@ -0,0 +1,1326 @@ +# GENERATED FILE - DO NOT EDIT DIRECTLY +# This template is generated by: npx tsx scripts/generate-bootstrap-template.ts +# ABCA Bootstrap Policy Version: 1.1.0 +# ABCA Bootstrap Policy Hash: a24d14dde94c546fdf94c7839492e92f612ae91def4660aaacab48d8e8da3146 +# +# Based on the default CDK bootstrap template with the following modifications: +# - BootstrapVariant set to "ABCA: Least-Privilege Bootstrap" +# - ComputeTypes parameter added for compute-variant selection +# - IncludeComputeEcs condition added +# - 5 inline AWS::IAM::ManagedPolicy resources replace AdministratorAccess +# - CloudFormationExecutionRole references our least-privilege policies +# - BootstrapPolicyVersion, BootstrapPolicyHash, BootstrapPolicySet outputs added +Description: This stack includes resources needed to deploy AWS CDK apps into this environment +Parameters: + TrustedAccounts: + Description: List of AWS accounts that are trusted to publish assets and deploy stacks to this environment + Default: '' + Type: CommaDelimitedList + TrustedAccountsForLookup: + Description: List of AWS accounts that are trusted to look up values in this environment + Default: '' + Type: CommaDelimitedList + CloudFormationExecutionPolicies: + Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation deployment role + Default: '' + Type: CommaDelimitedList + FileAssetsBucketName: + Description: The name of the S3 bucket used for file assets + Default: '' + Type: String + FileAssetsBucketKmsKeyId: + Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed S3 key, or the ID/ARN of an existing key. + Default: '' + Type: String + ContainerAssetsRepositoryName: + Description: A user-provided custom name to use for the container assets ECR repository + Default: '' + Type: String + Qualifier: + Description: An identifier to distinguish multiple bootstrap stacks in the same environment + Default: hnb659fds + Type: String + AllowedPattern: '[A-Za-z0-9_-]{1,10}' + ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters + PublicAccessBlockConfiguration: + Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration + Default: 'true' + Type: String + AllowedValues: + - 'true' + - 'false' + InputPermissionsBoundary: + Description: Whether or not to use either the CDK supplied or custom permissions boundary + Default: '' + Type: String + UseExamplePermissionsBoundary: + Default: 'false' + AllowedValues: + - 'true' + - 'false' + Type: String + BootstrapVariant: + Type: String + Default: 'ABCA: Least-Privilege Bootstrap' + Description: >- + Describe the provenance of the resources in this bootstrap stack. Change this when you customize the template. To + prevent accidents, the CDK CLI will not overwrite bootstrap stacks with a different variant. + DenyExternalId: + Type: String + Default: 'true' + AllowedValues: + - 'true' + - 'false' + Description: >- + Whether to deny AssumeRole calls with an ExternalId. This prevents calls that are intended to be deputized from + accidentally assuming CDK Roles. + ComputeTypes: + Type: CommaDelimitedList + Default: agentcore + Description: 'Comma-separated list of compute backends to enable. Valid values: agentcore, ecs.' +Conditions: + HasTrustedAccounts: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccounts + HasTrustedAccountsForLookup: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccountsForLookup + HasCloudFormationExecutionPolicies: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: CloudFormationExecutionPolicies + HasCustomFileAssetsBucketName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: FileAssetsBucketName + CreateNewKey: + Fn::Equals: + - '' + - Ref: FileAssetsBucketKmsKeyId + UseAwsManagedKey: + Fn::Equals: + - AWS_MANAGED_KEY + - Ref: FileAssetsBucketKmsKeyId + ShouldCreatePermissionsBoundary: + Fn::Equals: + - 'true' + - Ref: UseExamplePermissionsBoundary + PermissionsBoundarySet: + Fn::Not: + - Fn::Equals: + - '' + - Ref: InputPermissionsBoundary + HasCustomContainerAssetsRepositoryName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: ContainerAssetsRepositoryName + UsePublicAccessBlockConfiguration: + Fn::Equals: + - 'true' + - Ref: PublicAccessBlockConfiguration + ShouldDenyExternalId: + Fn::Equals: + - 'true' + - Ref: DenyExternalId + IncludeComputeEcs: + Fn::Not: + - Fn::Equals: + - Fn::Select: + - 0 + - Fn::Split: + - ecs + - Fn::Join: + - '' + - Ref: ComputeTypes + - Fn::Join: + - '' + - Ref: ComputeTypes +Resources: + FileAssetsBucketEncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Statement: + - Action: + - kms:Create* + - kms:Describe* + - kms:Enable* + - kms:List* + - kms:Put* + - kms:Update* + - kms:Revoke* + - kms:Disable* + - kms:Get* + - kms:Delete* + - kms:ScheduleKeyDeletion + - kms:CancelKeyDeletion + - kms:GenerateDataKey + - kms:TagResource + - kms:UntagResource + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Resource: '*' + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: '*' + Resource: '*' + Condition: + StringEquals: + kms:CallerAccount: + Ref: AWS::AccountId + kms:ViaService: + - Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: + Fn::Sub: ${FilePublishingRole.Arn} + Resource: '*' + Condition: CreateNewKey + UpdateReplacePolicy: Delete + DeletionPolicy: Delete + FileAssetsBucketEncryptionKeyAlias: + Condition: CreateNewKey + Type: AWS::KMS::Alias + Properties: + AliasName: + Fn::Sub: alias/cdk-${Qualifier}-assets-key + TargetKeyId: + Ref: FileAssetsBucketEncryptionKey + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::If: + - HasCustomFileAssetsBucketName + - Fn::Sub: ${FileAssetsBucketName} + - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: aws:kms + KMSMasterKeyID: + Fn::If: + - CreateNewKey + - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn} + - Fn::If: + - UseAwsManagedKey + - Ref: AWS::NoValue + - Fn::Sub: ${FileAssetsBucketKmsKeyId} + PublicAccessBlockConfiguration: + Fn::If: + - UsePublicAccessBlockConfiguration + - BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + - Ref: AWS::NoValue + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + - Id: CleanupOldVersions + Status: Enabled + NoncurrentVersionExpiration: + NoncurrentDays: 30 + - Id: AbortIncompleteMultipartUploads + Status: Enabled + AbortIncompleteMultipartUpload: + DaysAfterInitiation: 1 + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + StagingBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: + Ref: StagingBucket + PolicyDocument: + Id: AccessControl + Version: '2012-10-17' + Statement: + - Sid: AllowSSLRequestsOnly + Action: s3:* + Effect: Deny + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + Condition: + Bool: + aws:SecureTransport: 'false' + Principal: '*' + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + ImageTagMutability: IMMUTABLE + LifecyclePolicy: + LifecyclePolicyText: | + { + "rules": [ + { + "rulePriority": 1, + "description": "Untagged images should not exist, but expire any older than one year", + "selection": { + "tagStatus": "untagged", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 365 + }, + "action": { "type": "expire" } + } + ] + } + RepositoryName: + Fn::If: + - HasCustomContainerAssetsRepositoryName + - Fn::Sub: ${ContainerAssetsRepositoryName} + - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + RepositoryPolicyText: + Version: '2012-10-17' + Statement: + - Sid: LambdaECRImageRetrievalPolicy + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Condition: + StringLike: + aws:sourceArn: + Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:* + - Sid: EmrServerlessImageRetrievalPolicy + Effect: Allow + Principal: + Service: emr-serverless.amazonaws.com + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + - ecr:DescribeImages + Condition: + StringLike: + aws:sourceArn: + Fn::Sub: arn:${AWS::Partition}:emr-serverless:${AWS::Region}:${AWS::AccountId}:/applications/* + FilePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: file-publishing + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: image-publishing + LookupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccountsForLookup + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccountsForLookup + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccountsForLookup + - Action: + - sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: TrustedAccountsForLookup + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + ManagedPolicyArns: + - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess + Policies: + - PolicyDocument: + Statement: + - Sid: DontReadSecrets + Effect: Deny + Action: + - kms:Decrypt + Resource: '*' + Version: '2012-10-17' + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup + FilePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:GetEncryptionConfiguration + - s3:List* + - s3:DeleteObject* + - s3:PutObject* + - s3:Abort* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + Condition: + StringEquals: + aws:ResourceAccount: + - Fn::Sub: ${AWS::AccountId} + Effect: Allow + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: + Fn::If: + - CreateNewKey + - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn} + - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: '2012-10-17' + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - ecr:BatchCheckLayerAvailability + - ecr:DescribeRepositories + - ecr:DescribeImages + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Resource: + Fn::Sub: ${ContainerAssetsRepository.Arn} + Effect: Allow + - Action: + - ecr:GetAuthorizationToken + Resource: '*' + Effect: Allow + Version: '2012-10-17' + Roles: + - Ref: ImagePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + DeploymentActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + Condition: + Fn::If: + - ShouldDenyExternalId + - 'Null': + sts:ExternalId: 'true' + - Ref: AWS::NoValue + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: + - sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + Policies: + - PolicyDocument: + Statement: + - Sid: CloudFormationPermissions + Effect: Allow + Action: + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:DescribeStacks + - cloudformation:DescribeEvents + - cloudformation:ExecuteChangeSet + - cloudformation:CreateStack + - cloudformation:UpdateStack + - cloudformation:RollbackStack + - cloudformation:ContinueUpdateRollback + Resource: '*' + - Sid: PipelineCrossAccountArtifactsBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:Abort* + - s3:DeleteObject* + - s3:PutObject* + Resource: '*' + Condition: + StringNotEquals: + s3:ResourceAccount: + Ref: AWS::AccountId + - Sid: PipelineCrossAccountArtifactsKey + Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Resource: '*' + Condition: + StringEquals: + kms:ViaService: + Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: iam:PassRole + Resource: + Fn::Sub: ${CloudFormationExecutionRole.Arn} + Effect: Allow + - Sid: CliPermissions + Action: + - cloudformation:DescribeStackEvents + - cloudformation:GetTemplate + - cloudformation:DeleteStack + - cloudformation:UpdateTerminationProtection + - sts:GetCallerIdentity + - cloudformation:GetTemplateSummary + Resource: '*' + Effect: Allow + - Sid: CliStagingBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + - Sid: ReadVersion + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters + Resource: + - Fn::Sub: arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion} + - Sid: Refactor + Effect: Allow + Action: + - cloudformation:CreateStackRefactor + - cloudformation:DescribeStackRefactor + - cloudformation:ExecuteStackRefactor + - cloudformation:ListStackRefactorActions + - cloudformation:ListStackRefactors + - cloudformation:ListStacks + Resource: '*' + Version: '2012-10-17' + PolicyName: default + RoleName: + Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: deploy + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + Fn::If: + - HasCloudFormationExecutionPolicies + - Ref: CloudFormationExecutionPolicies + - - Ref: IaCRoleABCAInfrastructure + - Ref: IaCRoleABCAApplication + - Ref: IaCRoleABCAObservability + - Ref: IaCRoleABCAComputeAgentcore + - Fn::If: + - IncludeComputeEcs + - Ref: IaCRoleABCAComputeEcs + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + PermissionsBoundary: + Fn::If: + - PermissionsBoundarySet + - Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary} + - Ref: AWS::NoValue + CdkBoostrapPermissionsBoundaryPolicy: + Condition: ShouldCreatePermissionsBoundary + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Statement: + - Sid: ExplicitAllowAll + Action: + - '*' + Effect: Allow + Resource: '*' + - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied + Action: + - iam:CreateUser + - iam:CreateRole + - iam:PutRolePermissionsBoundary + - iam:PutUserPermissionsBoundary + Condition: + StringNotEquals: + iam:PermissionsBoundary: + Fn::Sub: >- + arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Effect: Deny + Resource: '*' + - Sid: DenyPermBoundaryIAMPolicyAlteration + Action: + - iam:CreatePolicyVersion + - iam:DeletePolicy + - iam:DeletePolicyVersion + - iam:SetDefaultPolicyVersion + Effect: Deny + Resource: + Fn::Sub: >- + arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole + Action: + - iam:DeleteUserPermissionsBoundary + - iam:DeleteRolePermissionsBoundary + Effect: Deny + Resource: '*' + Version: '2012-10-17' + Description: Bootstrap Permission Boundary + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Path: / + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: /cdk-bootstrap/${Qualifier}/version + Value: '30' + IaCRoleABCAInfrastructure: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-IaCRole-ABCA-Infrastructure-${AWS::AccountId}-${AWS::Region} + PolicyDocument: + Statement: + - Action: + - cloudformation:CreateStack + - cloudformation:UpdateStack + - cloudformation:DeleteStack + - cloudformation:DescribeStacks + - cloudformation:DescribeStackEvents + - cloudformation:DescribeStackResources + - cloudformation:GetTemplate + - cloudformation:GetTemplateSummary + - cloudformation:ListStackResources + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:ExecuteChangeSet + - cloudformation:SetStackPolicy + - cloudformation:ValidateTemplate + - cloudformation:ListChangeSets + Effect: Allow + Resource: + - arn:aws:cloudformation:*:*:stack/backgroundagent-dev/* + - arn:aws:cloudformation:*:*:stack/CDKToolkit/* + Sid: CloudFormationSelf + - Action: + - iam:CreateRole + - iam:DeleteRole + - iam:GetRole + - iam:UpdateRole + - iam:TagRole + - iam:UntagRole + - iam:ListRoleTags + - iam:AttachRolePolicy + - iam:DetachRolePolicy + - iam:PutRolePolicy + - iam:DeleteRolePolicy + - iam:GetRolePolicy + - iam:ListRolePolicies + - iam:ListAttachedRolePolicies + - iam:CreatePolicy + - iam:DeletePolicy + - iam:GetPolicy + - iam:GetPolicyVersion + - iam:CreatePolicyVersion + - iam:DeletePolicyVersion + - iam:ListPolicyVersions + - iam:TagPolicy + - iam:CreateServiceLinkedRole + - iam:ListInstanceProfilesForRole + Effect: Allow + Resource: + - arn:aws:iam::*:role/backgroundagent-dev-* + - arn:aws:iam::*:policy/backgroundagent-dev-* + - arn:aws:iam::*:role/aws-service-role/* + Sid: IAMRolesAndPolicies + - Action: iam:PassRole + Condition: + StringEquals: + iam:PassedToService: + - lambda.amazonaws.com + - ecs-tasks.amazonaws.com + - ecs.amazonaws.com + - apigateway.amazonaws.com + - logs.amazonaws.com + - bedrock.amazonaws.com + - bedrock-agentcore.amazonaws.com + - events.amazonaws.com + - vpc-flow-logs.amazonaws.com + Effect: Allow + Resource: arn:aws:iam::*:role/backgroundagent-dev-* + Sid: IAMPassRole + - Action: + - ec2:CreateVpc + - ec2:DeleteVpc + - ec2:DescribeVpcs + - ec2:ModifyVpcAttribute + - ec2:CreateSubnet + - ec2:DeleteSubnet + - ec2:DescribeSubnets + - ec2:CreateInternetGateway + - ec2:DeleteInternetGateway + - ec2:AttachInternetGateway + - ec2:DetachInternetGateway + - ec2:DescribeInternetGateways + - ec2:AllocateAddress + - ec2:ReleaseAddress + - ec2:DescribeAddresses + - ec2:CreateNatGateway + - ec2:DeleteNatGateway + - ec2:DescribeNatGateways + - ec2:CreateRouteTable + - ec2:DeleteRouteTable + - ec2:DescribeRouteTables + - ec2:AssociateRouteTable + - ec2:DisassociateRouteTable + - ec2:CreateRoute + - ec2:DeleteRoute + - ec2:CreateSecurityGroup + - ec2:DeleteSecurityGroup + - ec2:DescribeSecurityGroups + - ec2:AuthorizeSecurityGroupEgress + - ec2:RevokeSecurityGroupEgress + - ec2:AuthorizeSecurityGroupIngress + - ec2:RevokeSecurityGroupIngress + - ec2:CreateVpcEndpoint + - ec2:DeleteVpcEndpoints + - ec2:DescribeVpcEndpoints + - ec2:ModifyVpcEndpoint + - ec2:CreateFlowLogs + - ec2:DeleteFlowLogs + - ec2:DescribeFlowLogs + - ec2:CreateTags + - ec2:DeleteTags + - ec2:DescribeTags + - ec2:DescribeAvailabilityZones + - ec2:DescribeNetworkInterfaces + - ec2:DescribePrefixLists + - ec2:DescribeNetworkAcls + - ec2:DescribeVpcAttribute + - ec2:ModifySubnetAttribute + Effect: Allow + Resource: '*' + Sid: VPCNetworking + - Action: + - route53resolver:CreateFirewallRuleGroup + - route53resolver:DeleteFirewallRuleGroup + - route53resolver:GetFirewallRuleGroup + - route53resolver:CreateFirewallRule + - route53resolver:DeleteFirewallRule + - route53resolver:ListFirewallRules + - route53resolver:UpdateFirewallRule + - route53resolver:CreateFirewallDomainList + - route53resolver:DeleteFirewallDomainList + - route53resolver:GetFirewallDomainList + - route53resolver:UpdateFirewallDomains + - route53resolver:AssociateFirewallRuleGroup + - route53resolver:DisassociateFirewallRuleGroup + - route53resolver:GetFirewallRuleGroupAssociation + - route53resolver:ListFirewallRuleGroupAssociations + - route53resolver:UpdateFirewallConfig + - route53resolver:GetFirewallConfig + - route53resolver:TagResource + - route53resolver:UntagResource + - route53resolver:ListTagsForResource + - route53resolver:CreateResolverQueryLogConfig + - route53resolver:DeleteResolverQueryLogConfig + - route53resolver:GetResolverQueryLogConfig + - route53resolver:AssociateResolverQueryLogConfig + - route53resolver:DisassociateResolverQueryLogConfig + - route53resolver:GetResolverQueryLogConfigAssociation + - route53resolver:ListResolverQueryLogConfigAssociations + - route53resolver:ListResolverQueryLogConfigs + Effect: Allow + Resource: '*' + Sid: Route53ResolverDNSFirewall + Version: '2012-10-17' + Description: 'ABCA Bootstrap: IaCRole-ABCA-Infrastructure permissions for CloudFormation execution role' + IaCRoleABCAApplication: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-IaCRole-ABCA-Application-${AWS::AccountId}-${AWS::Region} + PolicyDocument: + Statement: + - Action: + - dynamodb:CreateTable + - dynamodb:DeleteTable + - dynamodb:DescribeTable + - dynamodb:DescribeTimeToLive + - dynamodb:UpdateTimeToLive + - dynamodb:UpdateTable + - dynamodb:UpdateContinuousBackups + - dynamodb:DescribeContinuousBackups + - dynamodb:TagResource + - dynamodb:UntagResource + - dynamodb:ListTagsOfResource + - dynamodb:PutItem + - dynamodb:UpdateItem + - dynamodb:DescribeContributorInsights + - dynamodb:DescribeKinesisStreamingDestination + - dynamodb:GetResourcePolicy + Effect: Allow + Resource: arn:aws:dynamodb:*:*:table/backgroundagent-dev-* + Sid: DynamoDB + - Action: + - lambda:CreateFunction + - lambda:DeleteFunction + - lambda:GetFunction + - lambda:GetFunctionConfiguration + - lambda:UpdateFunctionCode + - lambda:UpdateFunctionConfiguration + - lambda:AddPermission + - lambda:RemovePermission + - lambda:GetPolicy + - lambda:TagResource + - lambda:UntagResource + - lambda:ListTags + - lambda:PublishVersion + - lambda:CreateAlias + - lambda:DeleteAlias + - lambda:GetAlias + - lambda:UpdateAlias + - lambda:PutFunctionEventInvokeConfig + - lambda:DeleteFunctionEventInvokeConfig + - lambda:GetFunctionEventInvokeConfig + - lambda:PutFunctionConcurrency + - lambda:DeleteFunctionConcurrency + - lambda:GetFunctionCodeSigningConfig + - lambda:GetFunctionRecursionConfig + - lambda:GetProvisionedConcurrencyConfig + - lambda:GetRuntimeManagementConfig + - lambda:ListVersionsByFunction + - lambda:InvokeFunction + Effect: Allow + Resource: + - arn:aws:lambda:*:*:function:backgroundagent-dev-* + - arn:aws:lambda:*:*:function:backgroundagent-dev-AWS* + Sid: Lambda + - Action: + - apigateway:POST + - apigateway:GET + - apigateway:PUT + - apigateway:PATCH + - apigateway:DELETE + - apigateway:TagResource + - apigateway:UntagResource + - apigateway:SetWebACL + - apigateway:UpdateRestApiPolicy + Effect: Allow + Resource: + - arn:aws:apigateway:*::/restapis + - arn:aws:apigateway:*::/restapis/* + - arn:aws:apigateway:*::/account + - arn:aws:apigateway:*::/tags/* + Sid: APIGateway + - Action: + - cognito-idp:CreateUserPool + - cognito-idp:DeleteUserPool + - cognito-idp:DescribeUserPool + - cognito-idp:UpdateUserPool + - cognito-idp:CreateUserPoolClient + - cognito-idp:DeleteUserPoolClient + - cognito-idp:DescribeUserPoolClient + - cognito-idp:UpdateUserPoolClient + - cognito-idp:TagResource + - cognito-idp:UntagResource + - cognito-idp:ListTagsForResource + - cognito-idp:GetUserPoolMfaConfig + Effect: Allow + Resource: arn:aws:cognito-idp:*:*:userpool/* + Sid: Cognito + - Action: + - wafv2:CreateWebACL + - wafv2:DeleteWebACL + - wafv2:GetWebACL + - wafv2:UpdateWebACL + - wafv2:AssociateWebACL + - wafv2:DisassociateWebACL + - wafv2:ListTagsForResource + - wafv2:TagResource + - wafv2:UntagResource + - wafv2:GetWebACLForResource + Effect: Allow + Resource: + - arn:aws:wafv2:*:*:regional/webacl/* + - arn:aws:wafv2:*:*:regional/managedruleset/* + Sid: WAFv2 + - Action: + - events:PutRule + - events:DeleteRule + - events:DescribeRule + - events:PutTargets + - events:RemoveTargets + - events:ListTargetsByRule + - events:TagResource + - events:UntagResource + - events:ListTagsForResource + Effect: Allow + Resource: arn:aws:events:*:*:rule/backgroundagent-dev-* + Sid: EventBridge + - Action: + - secretsmanager:CreateSecret + - secretsmanager:DeleteSecret + - secretsmanager:DescribeSecret + - secretsmanager:GetSecretValue + - secretsmanager:PutSecretValue + - secretsmanager:UpdateSecret + - secretsmanager:TagResource + - secretsmanager:UntagResource + - secretsmanager:GetResourcePolicy + - secretsmanager:PutResourcePolicy + - secretsmanager:DeleteResourcePolicy + Effect: Allow + Resource: + - arn:aws:secretsmanager:*:*:secret:backgroundagent-* + - arn:aws:secretsmanager:*:*:secret:GitHubTokenSecret* + Sid: SecretsManager + - Action: secretsmanager:GetRandomPassword + Effect: Allow + Resource: '*' + Sid: SecretsManagerAccountLevel + Version: '2012-10-17' + Description: 'ABCA Bootstrap: IaCRole-ABCA-Application permissions for CloudFormation execution role' + IaCRoleABCAObservability: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-IaCRole-ABCA-Observability-${AWS::AccountId}-${AWS::Region} + PolicyDocument: + Statement: + - Action: + - bedrock:CreateGuardrail + - bedrock:DeleteGuardrail + - bedrock:GetGuardrail + - bedrock:UpdateGuardrail + - bedrock:CreateGuardrailVersion + - bedrock:ListGuardrails + - bedrock:TagResource + - bedrock:UntagResource + - bedrock:ListTagsForResource + - bedrock:PutModelInvocationLoggingConfiguration + - bedrock:DeleteModelInvocationLoggingConfiguration + - bedrock:GetModelInvocationLoggingConfiguration + Effect: Allow + Resource: '*' + Sid: BedrockGuardrailsAndLogging + - Action: + - logs:CreateLogGroup + - logs:DeleteLogGroup + - logs:DescribeLogGroups + - logs:PutRetentionPolicy + - logs:DeleteRetentionPolicy + - logs:TagLogGroup + - logs:UntagLogGroup + - logs:TagResource + - logs:UntagResource + - logs:ListTagsForResource + - logs:ListTagsLogGroup + - logs:PutResourcePolicy + - logs:DeleteResourcePolicy + - logs:DescribeResourcePolicies + - cloudwatch:PutDashboard + - cloudwatch:DeleteDashboards + - cloudwatch:GetDashboard + - cloudwatch:PutMetricAlarm + - cloudwatch:DeleteAlarms + - cloudwatch:DescribeAlarms + - cloudwatch:TagResource + - cloudwatch:UntagResource + - logs:CreateDelivery + - logs:DescribeDeliveries + - logs:GetDelivery + - logs:GetDeliveryDestination + - logs:GetDeliveryDestinationPolicy + - logs:GetDeliverySource + - logs:PutDeliveryDestination + - logs:PutDeliverySource + - logs:DescribeIndexPolicies + - cloudwatch:ListTagsForResource + - logs:CreateLogDelivery + - logs:DeleteLogDelivery + - logs:GetLogDelivery + - logs:UpdateLogDelivery + - logs:ListLogDeliveries + - logs:DeleteDelivery + - logs:DeleteDeliverySource + - logs:DeleteDeliveryDestination + Effect: Allow + Resource: '*' + Sid: CloudWatchLogsAndDashboards + - Action: + - s3:GetObject + - s3:PutObject + - s3:GetBucketLocation + - s3:ListBucket + Effect: Allow + Resource: + - arn:aws:s3:::cdk-hnb659fds-assets-* + - arn:aws:s3:::cdk-hnb659fds-assets-*/* + Sid: S3CDKAssets + - Action: + - kms:CreateGrant + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:GenerateDataKey + Effect: Allow + Resource: '*' + Sid: KMSForCDKAssets + - Action: + - ecr:CreateRepository + - ecr:DescribeRepositories + - ecr:GetAuthorizationToken + - ecr:BatchCheckLayerAvailability + - ecr:GetDownloadUrlForLayer + - ecr:BatchGetImage + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - ecr:SetRepositoryPolicy + - ecr:GetRepositoryPolicy + - ecr:DeleteRepository + - ecr:ListTagsForResource + - ecr:TagResource + Effect: Allow + Resource: + - arn:aws:ecr:*:*:repository/cdk-hnb659fds-container-assets-* + - arn:aws:ecr:*:*:repository/backgroundagent-* + Sid: ECRForDockerAssets + - Action: ecr:GetAuthorizationToken + Effect: Allow + Resource: '*' + Sid: ECRAuthToken + - Action: + - xray:UpdateTraceSegmentDestination + - xray:GetTraceSegmentDestination + - xray:ListResourcePolicies + - xray:PutResourcePolicy + Effect: Allow + Resource: '*' + Sid: XRay + - Action: + - ssm:GetParameter + - ssm:GetParameters + - ssm:PutParameter + - ssm:DeleteParameter + Effect: Allow + Resource: arn:aws:ssm:*:*:parameter/cdk-bootstrap/* + Sid: SSMParameterStoreForCDK + - Action: + - sts:AssumeRole + - sts:GetCallerIdentity + Effect: Allow + Resource: arn:aws:iam::*:role/cdk-hnb659fds-* + Sid: STSForCDK + Version: '2012-10-17' + Description: 'ABCA Bootstrap: IaCRole-ABCA-Observability permissions for CloudFormation execution role' + IaCRoleABCAComputeAgentcore: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-IaCRole-ABCA-Compute-Agentcore-${AWS::AccountId}-${AWS::Region} + PolicyDocument: + Statement: + - Action: bedrock-agentcore:* + Effect: Allow + Resource: '*' + Sid: BedrockAgentCore + Version: '2012-10-17' + Description: 'ABCA Bootstrap: IaCRole-ABCA-Compute-Agentcore permissions for CloudFormation execution role' + IaCRoleABCAComputeEcs: + Type: AWS::IAM::ManagedPolicy + Properties: + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-IaCRole-ABCA-Compute-ECS-${AWS::AccountId}-${AWS::Region} + PolicyDocument: + Statement: + - Action: + - ecs:CreateCluster + - ecs:DeleteCluster + - ecs:DescribeClusters + - ecs:UpdateCluster + - ecs:UpdateClusterSettings + - ecs:PutClusterCapacityProviders + - ecs:RegisterTaskDefinition + - ecs:DeregisterTaskDefinition + - ecs:DescribeTaskDefinition + - ecs:ListTaskDefinitions + - ecs:TagResource + - ecs:UntagResource + - ecs:ListTagsForResource + - ecs:PutAccountSetting + Effect: Allow + Resource: '*' + Sid: ECS + Version: '2012-10-17' + Description: 'ABCA Bootstrap: IaCRole-ABCA-Compute-ECS permissions for CloudFormation execution role' + Condition: IncludeComputeEcs +Outputs: + BucketName: + Description: The name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: ${StagingBucket} + BucketDomainName: + Description: The domain name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: ${StagingBucket.RegionalDomainName} + FileAssetKeyArn: + Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) + Value: + Fn::If: + - CreateNewKey + - Fn::Sub: ${FileAssetsBucketEncryptionKey.Arn} + - Fn::Sub: ${FileAssetsBucketKmsKeyId} + Export: + Name: + Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: ${ContainerAssetsRepository} + BootstrapVersion: + Description: The version of the bootstrap resources that are currently mastered in this stack + Value: '30' + BootstrapPolicyVersion: + Description: The version of the ABCA bootstrap policy bundle + Value: 1.1.0 + BootstrapPolicyHash: + Description: SHA-256 hash of the ABCA bootstrap policy bundle for drift detection + Value: a24d14dde94c546fdf94c7839492e92f612ae91def4660aaacab48d8e8da3146 + BootstrapPolicySet: + Description: Comma-separated list of active ABCA bootstrap policy names + Value: + Fn::Join: + - ',' + - - Infrastructure + - Application + - Observability + - Compute-Agentcore + - Fn::If: + - IncludeComputeEcs + - Compute-ECS + - Ref: AWS::NoValue diff --git a/cdk/package.json b/cdk/package.json index e3d71a2..0495f01 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -19,8 +19,8 @@ "@aws-cdk/mixins-preview": "2.238.0-alpha.0", "@aws-sdk/client-bedrock-agentcore": "^3.1046.0", "@aws-sdk/client-bedrock-runtime": "^3.1021.0", - "@aws-sdk/client-ecs": "^3.1021.0", "@aws-sdk/client-dynamodb": "^3.1021.0", + "@aws-sdk/client-ecs": "^3.1021.0", "@aws-sdk/client-lambda": "^3.1021.0", "@aws-sdk/client-s3": "^3.1021.0", "@aws-sdk/client-secrets-manager": "^3.1021.0", @@ -31,6 +31,7 @@ "aws-cdk-lib": "^2.238.0", "cdk-nag": "^2.37.55", "constructs": "^10.3.0", + "js-yaml": "^4.1.1", "ulid": "^3.0.2" }, "devDependencies": { @@ -38,6 +39,7 @@ "@stylistic/eslint-plugin": "^2", "@types/aws-lambda": "^8.10.161", "@types/jest": "^30.0.0", + "@types/js-yaml": "^4.0.9", "@types/node": "^20", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", diff --git a/cdk/scripts/generate-bootstrap-template.ts b/cdk/scripts/generate-bootstrap-template.ts new file mode 100644 index 0000000..732e7cb --- /dev/null +++ b/cdk/scripts/generate-bootstrap-template.ts @@ -0,0 +1,233 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * Generates a custom CDK bootstrap template that replaces AdministratorAccess + * with ABCA least-privilege managed policies. The template supports per-compute-variant + * selection via the ComputeTypes parameter. + * + * Usage: npx tsx scripts/generate-bootstrap-template.ts + * Output: cdk/bootstrap/bootstrap-template.yaml + */ + +import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +import * as yaml from 'js-yaml'; + +import { + applicationPolicy, + computeAgentcorePolicy, + computeEcsPolicy, + infrastructurePolicy, + observabilityPolicy, +} from '../src/bootstrap/policies'; +import { BOOTSTRAP_VERSION, computeBootstrapHash } from '../src/bootstrap/version'; + +// --- Paths --- +// aws-cdk is hoisted to the workspace root node_modules; use require.resolve to find it +const awsCdkDir = join(require.resolve('aws-cdk/package.json'), '..'); +const cdkBootstrapTemplatePath = join( + awsCdkDir, + 'lib', + 'api', + 'bootstrap', + 'bootstrap-template.yaml', +); +const outputDir = join(__dirname, '..', 'bootstrap'); +const outputPath = join(outputDir, 'bootstrap-template.yaml'); + +// --- Read and parse the default template --- +const rawTemplate = readFileSync(cdkBootstrapTemplatePath, 'utf-8'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const template: any = yaml.load(rawTemplate); + +// --- Step 1: Update BootstrapVariant default --- +template.Parameters.BootstrapVariant.Default = 'ABCA: Least-Privilege Bootstrap'; + +// --- Step 2: Add ComputeTypes parameter --- +template.Parameters.ComputeTypes = { + Type: 'CommaDelimitedList', + Default: 'agentcore', + Description: + 'Comma-separated list of compute backends to enable. Valid values: agentcore, ecs.', +}; + +// --- Step 3: Add conditions --- +template.Conditions.IncludeComputeEcs = { + 'Fn::Not': [ + { + 'Fn::Equals': [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + 'ecs', + { 'Fn::Join': ['', { Ref: 'ComputeTypes' }] }, + ], + }, + ], + }, + { 'Fn::Join': ['', { Ref: 'ComputeTypes' }] }, + ], + }, + ], +}; + +// --- Step 4: Add managed policy resources --- +interface PolicyDef { + logicalId: string; + policyName: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + policyFn: () => any; + condition?: string; +} + +const policyDefs: PolicyDef[] = [ + { + logicalId: 'IaCRoleABCAInfrastructure', + policyName: 'IaCRole-ABCA-Infrastructure', + policyFn: infrastructurePolicy, + }, + { + logicalId: 'IaCRoleABCAApplication', + policyName: 'IaCRole-ABCA-Application', + policyFn: applicationPolicy, + }, + { + logicalId: 'IaCRoleABCAObservability', + policyName: 'IaCRole-ABCA-Observability', + policyFn: observabilityPolicy, + }, + { + logicalId: 'IaCRoleABCAComputeAgentcore', + policyName: 'IaCRole-ABCA-Compute-Agentcore', + policyFn: computeAgentcorePolicy, + }, + { + logicalId: 'IaCRoleABCAComputeEcs', + policyName: 'IaCRole-ABCA-Compute-ECS', + policyFn: computeEcsPolicy, + condition: 'IncludeComputeEcs', + }, +]; + +for (const { logicalId, policyName, policyFn, condition } of policyDefs) { + const policyDoc = policyFn().toJSON(); + // Ensure Version is present in the policy document + if (!policyDoc.Version) { + policyDoc.Version = '2012-10-17'; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resource: any = { + Type: 'AWS::IAM::ManagedPolicy', + Properties: { + ManagedPolicyName: { + 'Fn::Sub': `cdk-\${Qualifier}-${policyName}-\${AWS::AccountId}-\${AWS::Region}`, + }, + PolicyDocument: policyDoc, + Description: `ABCA Bootstrap: ${policyName} permissions for CloudFormation execution role`, + }, + }; + + if (condition) { + resource.Condition = condition; + } + + template.Resources[logicalId] = resource; +} + +// --- Step 5: Modify CloudFormationExecutionRole ManagedPolicyArns --- +// Replace the conditional that falls back to AdministratorAccess with our inline policies. +// Keep the CloudFormationExecutionPolicies parameter override for flexibility. +const coreRefs = [ + { Ref: 'IaCRoleABCAInfrastructure' }, + { Ref: 'IaCRoleABCAApplication' }, + { Ref: 'IaCRoleABCAObservability' }, + { Ref: 'IaCRoleABCAComputeAgentcore' }, + { 'Fn::If': ['IncludeComputeEcs', { Ref: 'IaCRoleABCAComputeEcs' }, { Ref: 'AWS::NoValue' }] }, +]; + +template.Resources.CloudFormationExecutionRole.Properties.ManagedPolicyArns = { + 'Fn::If': [ + 'HasCloudFormationExecutionPolicies', + { Ref: 'CloudFormationExecutionPolicies' }, + coreRefs, + ], +}; + +// --- Step 6: Add outputs --- +template.Outputs.BootstrapPolicyVersion = { + Description: 'The version of the ABCA bootstrap policy bundle', + Value: BOOTSTRAP_VERSION, +}; + +template.Outputs.BootstrapPolicyHash = { + Description: 'SHA-256 hash of the ABCA bootstrap policy bundle for drift detection', + Value: computeBootstrapHash(), +}; + +template.Outputs.BootstrapPolicySet = { + Description: 'Comma-separated list of active ABCA bootstrap policy names', + Value: { + 'Fn::Join': [ + ',', + [ + 'Infrastructure', + 'Application', + 'Observability', + 'Compute-Agentcore', + { 'Fn::If': ['IncludeComputeEcs', 'Compute-ECS', { Ref: 'AWS::NoValue' }] }, + ], + ], + }, +}; + +// --- Step 7: Write output --- +mkdirSync(outputDir, { recursive: true }); + +const yamlOutput = yaml.dump(template, { + lineWidth: 120, + noRefs: true, + quotingType: "'", + forceQuotes: false, +}); + +// Add a header comment +const header = [ + '# GENERATED FILE - DO NOT EDIT DIRECTLY', + '# This template is generated by: npx tsx scripts/generate-bootstrap-template.ts', + `# ABCA Bootstrap Policy Version: ${BOOTSTRAP_VERSION}`, + `# ABCA Bootstrap Policy Hash: ${computeBootstrapHash()}`, + '#', + '# Based on the default CDK bootstrap template with the following modifications:', + '# - BootstrapVariant set to "ABCA: Least-Privilege Bootstrap"', + '# - ComputeTypes parameter added for compute-variant selection', + '# - IncludeComputeEcs condition added', + '# - 5 inline AWS::IAM::ManagedPolicy resources replace AdministratorAccess', + '# - CloudFormationExecutionRole references our least-privilege policies', + '# - BootstrapPolicyVersion, BootstrapPolicyHash, BootstrapPolicySet outputs added', + '', +].join('\n'); + +writeFileSync(outputPath, header + yamlOutput); + +console.log(`Generated bootstrap template (v${BOOTSTRAP_VERSION}) -> ${outputPath}`); diff --git a/cdk/test/bootstrap/bootstrap-template.test.ts b/cdk/test/bootstrap/bootstrap-template.test.ts new file mode 100644 index 0000000..2aa5593 --- /dev/null +++ b/cdk/test/bootstrap/bootstrap-template.test.ts @@ -0,0 +1,215 @@ +/** + * MIT No Attribution + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import * as yaml from 'js-yaml'; + +import { BOOTSTRAP_VERSION, computeBootstrapHash } from '../../src/bootstrap/version'; + +const templatePath = join(__dirname, '..', '..', 'bootstrap', 'bootstrap-template.yaml'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const template: any = yaml.load(readFileSync(templatePath, 'utf-8')); + +describe('Bootstrap template', () => { + describe('Parameters', () => { + it('has ComputeTypes parameter with correct defaults', () => { + expect(template.Parameters.ComputeTypes).toBeDefined(); + expect(template.Parameters.ComputeTypes.Type).toBe('CommaDelimitedList'); + expect(template.Parameters.ComputeTypes.Default).toBe('agentcore'); + }); + + it('has BootstrapVariant set to ABCA variant', () => { + expect(template.Parameters.BootstrapVariant.Default).toBe( + 'ABCA: Least-Privilege Bootstrap', + ); + }); + }); + + describe('Conditions', () => { + it('has IncludeComputeEcs condition', () => { + expect(template.Conditions.IncludeComputeEcs).toBeDefined(); + expect(template.Conditions.IncludeComputeEcs['Fn::Not']).toBeDefined(); + }); + }); + + describe('Managed policy resources', () => { + const expectedPolicies = [ + 'IaCRoleABCAInfrastructure', + 'IaCRoleABCAApplication', + 'IaCRoleABCAObservability', + 'IaCRoleABCAComputeAgentcore', + 'IaCRoleABCAComputeEcs', + ]; + + for (const logicalId of expectedPolicies) { + it(`has ${logicalId} resource`, () => { + expect(template.Resources[logicalId]).toBeDefined(); + expect(template.Resources[logicalId].Type).toBe('AWS::IAM::ManagedPolicy'); + expect(template.Resources[logicalId].Properties.PolicyDocument).toBeDefined(); + expect(template.Resources[logicalId].Properties.PolicyDocument.Statement).toBeDefined(); + expect( + template.Resources[logicalId].Properties.PolicyDocument.Statement.length, + ).toBeGreaterThan(0); + }); + } + + it('IaCRoleABCAComputeEcs has IncludeComputeEcs condition', () => { + expect(template.Resources.IaCRoleABCAComputeEcs.Condition).toBe('IncludeComputeEcs'); + }); + + it('non-ECS policies do not have a condition', () => { + const nonEcs = expectedPolicies.filter((p) => p !== 'IaCRoleABCAComputeEcs'); + for (const logicalId of nonEcs) { + expect(template.Resources[logicalId].Condition).toBeUndefined(); + } + }); + + it('each policy has a qualified ManagedPolicyName using Fn::Sub', () => { + for (const logicalId of expectedPolicies) { + const name = template.Resources[logicalId].Properties.ManagedPolicyName; + expect(name).toBeDefined(); + expect(name['Fn::Sub']).toMatch(/^cdk-\$\{Qualifier\}-IaCRole-ABCA-/); + } + }); + }); + + describe('CloudFormationExecutionRole', () => { + it('exists and is an IAM Role', () => { + expect(template.Resources.CloudFormationExecutionRole).toBeDefined(); + expect(template.Resources.CloudFormationExecutionRole.Type).toBe('AWS::IAM::Role'); + }); + + it('ManagedPolicyArns references our policies (not AdministratorAccess)', () => { + const managed = + template.Resources.CloudFormationExecutionRole.Properties.ManagedPolicyArns; + expect(managed).toBeDefined(); + + // Should be an Fn::If with HasCloudFormationExecutionPolicies + expect(managed['Fn::If']).toBeDefined(); + expect(managed['Fn::If'][0]).toBe('HasCloudFormationExecutionPolicies'); + + // The fallback (index 2) should be an array referencing our policies + const fallback = managed['Fn::If'][2]; + expect(Array.isArray(fallback)).toBe(true); + expect(fallback).toContainEqual({ Ref: 'IaCRoleABCAInfrastructure' }); + expect(fallback).toContainEqual({ Ref: 'IaCRoleABCAApplication' }); + expect(fallback).toContainEqual({ Ref: 'IaCRoleABCAObservability' }); + expect(fallback).toContainEqual({ Ref: 'IaCRoleABCAComputeAgentcore' }); + + // ECS should be conditional + const ecsEntry = fallback.find( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (item: any) => item['Fn::If'] && item['Fn::If'][0] === 'IncludeComputeEcs', + ); + expect(ecsEntry).toBeDefined(); + expect(ecsEntry['Fn::If'][1]).toEqual({ Ref: 'IaCRoleABCAComputeEcs' }); + expect(ecsEntry['Fn::If'][2]).toEqual({ Ref: 'AWS::NoValue' }); + }); + + it('does not reference AdministratorAccess', () => { + const serialized = JSON.stringify( + template.Resources.CloudFormationExecutionRole.Properties.ManagedPolicyArns, + ); + expect(serialized).not.toContain('AdministratorAccess'); + }); + }); + + describe('Outputs', () => { + it('has BootstrapPolicyVersion output matching source constant', () => { + expect(template.Outputs.BootstrapPolicyVersion).toBeDefined(); + expect(template.Outputs.BootstrapPolicyVersion.Value).toBe(BOOTSTRAP_VERSION); + }); + + it('has BootstrapPolicyHash output matching computed hash', () => { + expect(template.Outputs.BootstrapPolicyHash).toBeDefined(); + expect(template.Outputs.BootstrapPolicyHash.Value).toBe(computeBootstrapHash()); + }); + + it('has BootstrapPolicySet output with conditional ECS', () => { + expect(template.Outputs.BootstrapPolicySet).toBeDefined(); + const value = template.Outputs.BootstrapPolicySet.Value; + expect(value['Fn::Join']).toBeDefined(); + + // Should contain the core policy names + const items = value['Fn::Join'][1]; + expect(items).toContain('Infrastructure'); + expect(items).toContain('Application'); + expect(items).toContain('Observability'); + expect(items).toContain('Compute-Agentcore'); + + // ECS should be conditional + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ecsItem = items.find((item: any) => item['Fn::If']); + expect(ecsItem).toBeDefined(); + expect(ecsItem['Fn::If'][0]).toBe('IncludeComputeEcs'); + expect(ecsItem['Fn::If'][1]).toBe('Compute-ECS'); + }); + }); + + describe('Default resources preserved', () => { + const expectedResources = [ + 'StagingBucket', + 'StagingBucketPolicy', + 'ContainerAssetsRepository', + 'FileAssetsBucketEncryptionKey', + 'FileAssetsBucketEncryptionKeyAlias', + 'FilePublishingRole', + 'ImagePublishingRole', + 'LookupRole', + 'CloudFormationExecutionRole', + 'DeploymentActionRole', + 'CdkBootstrapVersion', + ]; + + for (const resourceId of expectedResources) { + it(`retains ${resourceId}`, () => { + expect(template.Resources[resourceId]).toBeDefined(); + }); + } + }); + + describe('Template validity', () => { + it('has Description', () => { + expect(template.Description).toBeDefined(); + expect(typeof template.Description).toBe('string'); + }); + + it('has Parameters section', () => { + expect(template.Parameters).toBeDefined(); + expect(Object.keys(template.Parameters).length).toBeGreaterThan(0); + }); + + it('has Conditions section', () => { + expect(template.Conditions).toBeDefined(); + expect(Object.keys(template.Conditions).length).toBeGreaterThan(0); + }); + + it('has Resources section', () => { + expect(template.Resources).toBeDefined(); + expect(Object.keys(template.Resources).length).toBeGreaterThan(0); + }); + + it('has Outputs section', () => { + expect(template.Outputs).toBeDefined(); + expect(Object.keys(template.Outputs).length).toBeGreaterThan(0); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 814102f..9854966 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5835,7 +5835,7 @@ baseline-browser-mapping@^2.10.12: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz#5a154cc4589193015a274e3d18319b0d76b9224e" integrity sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw== -basic-ftp@^5.0.2, basic-ftp@^5.2.2: +basic-ftp@^5.0.2: version "5.3.1" resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.3.1.tgz#3148ee9af43c0522514a4f973fecb1d3cbb6d71e" integrity sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw== @@ -7000,7 +7000,7 @@ fast-wrap-ansi@^0.1.3: dependencies: fast-string-width "^1.1.0" -fast-xml-builder@^1.1.5: +fast-xml-builder@^1.1.4, fast-xml-builder@^1.1.5, fast-xml-builder@^1.1.7: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz#abd2363145a7625d9789ad96da375fabe3cff28c" integrity sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q== @@ -7008,7 +7008,16 @@ fast-xml-builder@^1.1.5: path-expression-matcher "^1.5.0" xml-naming "^0.1.0" -fast-xml-parser@5.5.8, fast-xml-parser@5.7.2, fast-xml-parser@5.7.3, fast-xml-parser@^5.7.0: +fast-xml-parser@5.5.8: + version "5.5.8" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz#929571ed8c5eb96e6d9bd572ba14fc4b84875716" + integrity sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ== + dependencies: + fast-xml-builder "^1.1.4" + path-expression-matcher "^1.2.0" + strnum "^2.2.0" + +fast-xml-parser@5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz#fecd0b054c6c132fc03dab994a413da781e0eb9f" integrity sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w== @@ -7018,6 +7027,16 @@ fast-xml-parser@5.5.8, fast-xml-parser@5.7.2, fast-xml-parser@5.7.3, fast-xml-pa path-expression-matcher "^1.5.0" strnum "^2.2.3" +fast-xml-parser@5.7.3: + version "5.7.3" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz#309b04b08d835defc62ab657a0bb340c0e0fbe6a" + integrity sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg== + dependencies: + "@nodable/entities" "^2.1.0" + fast-xml-builder "^1.1.7" + path-expression-matcher "^1.5.0" + strnum "^2.2.3" + fb-watchman@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" @@ -9698,7 +9717,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-expression-matcher@^1.5.0: +path-expression-matcher@^1.2.0, path-expression-matcher@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz#3b98545dc88ffebb593e2d8458d0929da9275f4a" integrity sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ== @@ -9778,7 +9797,7 @@ postcss-selector-parser@^6.1.1: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.4.38, postcss@^8.5.10, postcss@^8.5.6: +postcss@^8.4.38, postcss@^8.5.6: version "8.5.12" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb" integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA== @@ -10564,7 +10583,16 @@ string-length@^4.0.2: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -10622,7 +10650,14 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -10656,6 +10691,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strnum@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.3.0.tgz#81bfbfef53db8c3217ea62a98c026886ec4a2761" + integrity sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q== + strnum@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.3.tgz#0119fce02749a11bb126a4d686ac5dbdf6e57586" @@ -11142,10 +11182,15 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^14.0.0, uuid@^8.3.2, uuid@^9.0.1: - version "14.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d" - integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== v8-compile-cache-lib@^3.0.1: version "3.0.1" @@ -11429,7 +11474,16 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -11502,7 +11556,17 @@ yaml-language-server@~1.20.0: vscode-uri "^3.0.2" yaml "2.7.1" -yaml@1.10.3, yaml@2.7.1, yaml@^2.8.2, yaml@^2.8.3: +yaml@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3" + integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA== + +yaml@2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" + integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ== + +yaml@^2.8.2: version "2.8.3" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== From a4d6258d88c4da45319e2fc0ebc2ecd74beb9fd0 Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Thu, 21 May 2026 01:27:05 +0000 Subject: [PATCH 3/4] feat(bootstrap): add mise bootstrap:generate task and update bootstrap command mise //cdk:bootstrap now uses the custom least-privilege template. mise //cdk:bootstrap:generate regenerates all artifacts (policies JSON, template YAML, version/hash files) from source. Co-Authored-By: Claude Opus 4.6 (1M context) --- cdk/mise.toml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cdk/mise.toml b/cdk/mise.toml index 869a0da..b53b91c 100644 --- a/cdk/mise.toml +++ b/cdk/mise.toml @@ -32,9 +32,14 @@ description = "cdk deploy (pass args after --)" run = "npx cdk deploy" [tasks.bootstrap] -description = "Bootstrap CDK in the current AWS account/region (one-time)" +description = "Bootstrap CDK with least-privilege policies (pass -- --context ComputeTypes=agentcore,ecs for ECS)" +depends = [":install", ":bootstrap:generate"] +run = "npx cdk bootstrap --template bootstrap/bootstrap-template.yaml" + +[tasks."bootstrap:generate"] +description = "Regenerate all bootstrap artifacts (policies JSON, template YAML, version/hash files)" depends = [":install"] -run = "npx cdk bootstrap" +run = "npx tsx scripts/generate-bootstrap-artifacts.ts && npx tsx scripts/generate-bootstrap-template.ts" [tasks.destroy] description = "cdk destroy" From 0a64e5c110f2abe8c4a69e052dee680790eb25fd Mon Sep 17 00:00:00 2001 From: bgagent <345885+scottschreckengaust@users.noreply.github.com> Date: Thu, 21 May 2026 01:48:52 +0000 Subject: [PATCH 4/4] chore(deps): deduplicate yarn.lock after js-yaml addition CI yarn install deduplicates transitive deps (yaml, uuid) which mutates the lockfile if it wasn't committed in deduplicated form. Co-Authored-By: Claude Opus 4.6 (1M context) --- yarn.lock | 94 +++++++++++++++++-------------------------------------- 1 file changed, 28 insertions(+), 66 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9854966..9f71fca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5835,7 +5835,7 @@ baseline-browser-mapping@^2.10.12: resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz#5a154cc4589193015a274e3d18319b0d76b9224e" integrity sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw== -basic-ftp@^5.0.2: +basic-ftp@^5.0.2, basic-ftp@^5.2.2: version "5.3.1" resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.3.1.tgz#3148ee9af43c0522514a4f973fecb1d3cbb6d71e" integrity sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw== @@ -7000,7 +7000,7 @@ fast-wrap-ansi@^0.1.3: dependencies: fast-string-width "^1.1.0" -fast-xml-builder@^1.1.4, fast-xml-builder@^1.1.5, fast-xml-builder@^1.1.7: +fast-xml-builder@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz#abd2363145a7625d9789ad96da375fabe3cff28c" integrity sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q== @@ -7008,34 +7008,16 @@ fast-xml-builder@^1.1.4, fast-xml-builder@^1.1.5, fast-xml-builder@^1.1.7: path-expression-matcher "^1.5.0" xml-naming "^0.1.0" -fast-xml-parser@5.5.8: - version "5.5.8" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz#929571ed8c5eb96e6d9bd572ba14fc4b84875716" - integrity sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ== - dependencies: - fast-xml-builder "^1.1.4" - path-expression-matcher "^1.2.0" - strnum "^2.2.0" - -fast-xml-parser@5.7.2: - version "5.7.2" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz#fecd0b054c6c132fc03dab994a413da781e0eb9f" - integrity sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w== +fast-xml-parser@5.5.8, fast-xml-parser@5.7.2, fast-xml-parser@5.7.3, fast-xml-parser@^5.7.0: + version "5.8.0" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz#64d71f0f8d4bf23621dffd762aef7e98c1884fc1" + integrity sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg== dependencies: "@nodable/entities" "^2.1.0" - fast-xml-builder "^1.1.5" + fast-xml-builder "^1.2.0" path-expression-matcher "^1.5.0" - strnum "^2.2.3" - -fast-xml-parser@5.7.3: - version "5.7.3" - resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz#309b04b08d835defc62ab657a0bb340c0e0fbe6a" - integrity sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg== - dependencies: - "@nodable/entities" "^2.1.0" - fast-xml-builder "^1.1.7" - path-expression-matcher "^1.5.0" - strnum "^2.2.3" + strnum "^2.3.0" + xml-naming "^0.1.0" fb-watchman@^2.0.2: version "2.0.2" @@ -9344,10 +9326,10 @@ muggle-string@^0.4.1: resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328" integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ== -nanoid@^3.3.11: - version "3.3.11" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" - integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +nanoid@^3.3.12: + version "3.3.12" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.12.tgz#ab3d912e217a6d0a514f00a72a16543a28982c05" + integrity sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ== napi-postinstall@^0.3.0: version "0.3.4" @@ -9717,7 +9699,7 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-expression-matcher@^1.2.0, path-expression-matcher@^1.5.0: +path-expression-matcher@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz#3b98545dc88ffebb593e2d8458d0929da9275f4a" integrity sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ== @@ -9797,12 +9779,12 @@ postcss-selector-parser@^6.1.1: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.4.38, postcss@^8.5.6: - version "8.5.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.12.tgz#cd0c0f667f7cb0521e2313234ea6e707a9ec1ddb" - integrity sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA== +postcss@^8.4.38, postcss@^8.5.10, postcss@^8.5.6: + version "8.5.15" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.15.tgz#d1eaf677a324e9ec02196da2d3fecf4a0b9a735c" + integrity sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A== dependencies: - nanoid "^3.3.11" + nanoid "^3.3.12" picocolors "^1.1.1" source-map-js "^1.2.1" @@ -10691,16 +10673,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strnum@^2.2.0: +strnum@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.3.0.tgz#81bfbfef53db8c3217ea62a98c026886ec4a2761" integrity sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q== -strnum@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.3.tgz#0119fce02749a11bb126a4d686ac5dbdf6e57586" - integrity sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg== - style-to-js@^1.0.0: version "1.1.21" resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.21.tgz#2908941187f857e79e28e9cd78008b9a0b3e0e8d" @@ -11182,15 +11159,10 @@ util-deprecate@^1.0.2: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uuid@^14.0.0, uuid@^8.3.2, uuid@^9.0.1: + version "14.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-14.0.0.tgz#0af883220163d264ffe0c084f6b8a89b9666966d" + integrity sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg== v8-compile-cache-lib@^3.0.1: version "3.0.1" @@ -11556,20 +11528,10 @@ yaml-language-server@~1.20.0: vscode-uri "^3.0.2" yaml "2.7.1" -yaml@1.10.3: - version "1.10.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.3.tgz#76e407ed95c42684fb8e14641e5de62fe65bbcb3" - integrity sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA== - -yaml@2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" - integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ== - -yaml@^2.8.2: - version "2.8.3" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.3.tgz#a0d6bd2efb3dd03c59370223701834e60409bd7d" - integrity sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg== +yaml@1.10.3, yaml@2.7.1, yaml@^2.8.2, yaml@^2.8.3: + version "2.9.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4" + integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA== yargs-parser@^21.1.1: version "21.1.1"