diff --git a/cdk/bootstrap/BOOTSTRAP_HASH b/cdk/bootstrap/BOOTSTRAP_HASH new file mode 100644 index 00000000..167e5888 --- /dev/null +++ b/cdk/bootstrap/BOOTSTRAP_HASH @@ -0,0 +1 @@ +4892570024965c2e99ef0d9f7ef0a61e4b939ba69c5df52e4bc1647522dad283 diff --git a/cdk/bootstrap/BOOTSTRAP_VERSION b/cdk/bootstrap/BOOTSTRAP_VERSION new file mode 100644 index 00000000..3eefcb9d --- /dev/null +++ b/cdk/bootstrap/BOOTSTRAP_VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/cdk/bootstrap/policies/application.json b/cdk/bootstrap/policies/application.json new file mode 100644 index 00000000..a822dd0f --- /dev/null +++ b/cdk/bootstrap/policies/application.json @@ -0,0 +1,169 @@ +{ + "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" +} diff --git a/cdk/bootstrap/policies/infrastructure.json b/cdk/bootstrap/policies/infrastructure.json new file mode 100644 index 00000000..c89c97b0 --- /dev/null +++ b/cdk/bootstrap/policies/infrastructure.json @@ -0,0 +1,177 @@ +{ + "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" +} diff --git a/cdk/bootstrap/policies/observability.json b/cdk/bootstrap/policies/observability.json new file mode 100644 index 00000000..306c9bf1 --- /dev/null +++ b/cdk/bootstrap/policies/observability.json @@ -0,0 +1,165 @@ +{ + "Statement": [ + { + "Action": "bedrock-agentcore:*", + "Effect": "Allow", + "Resource": "*", + "Sid": "BedrockAgentCore" + }, + { + "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" +} diff --git a/cdk/scripts/generate-bootstrap-artifacts.ts b/cdk/scripts/generate-bootstrap-artifacts.ts new file mode 100644 index 00000000..78f35b76 --- /dev/null +++ b/cdk/scripts/generate-bootstrap-artifacts.ts @@ -0,0 +1,44 @@ +/** + * 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 { mkdirSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { infrastructurePolicy, applicationPolicy, observabilityPolicy } from '../src/bootstrap/policies'; +import { BOOTSTRAP_VERSION, computeBootstrapHash } from '../src/bootstrap/version'; + +const outDir = join(__dirname, '..', 'bootstrap', 'policies'); +mkdirSync(outDir, { recursive: true }); + +const policies = [ + { name: 'infrastructure', fn: infrastructurePolicy }, + { name: 'application', fn: applicationPolicy }, + { name: 'observability', fn: observabilityPolicy }, +]; + +for (const { name, fn } of policies) { + const json = JSON.stringify(fn().toJSON(), null, 2) + '\n'; + writeFileSync(join(outDir, `${name}.json`), json); +} + +const bootstrapDir = join(__dirname, '..', 'bootstrap'); +writeFileSync(join(bootstrapDir, 'BOOTSTRAP_VERSION'), BOOTSTRAP_VERSION + '\n'); +writeFileSync(join(bootstrapDir, 'BOOTSTRAP_HASH'), computeBootstrapHash() + '\n'); + +console.log(`Generated bootstrap artifacts (v${BOOTSTRAP_VERSION})`); diff --git a/cdk/src/bootstrap/index.ts b/cdk/src/bootstrap/index.ts new file mode 100644 index 00000000..68432c5a --- /dev/null +++ b/cdk/src/bootstrap/index.ts @@ -0,0 +1,21 @@ +/** + * 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. + */ + +export { infrastructurePolicy, applicationPolicy, observabilityPolicy, allPolicies } from './policies'; +export { BOOTSTRAP_VERSION, computeBootstrapHash } from './version'; diff --git a/cdk/src/bootstrap/policies/application.ts b/cdk/src/bootstrap/policies/application.ts new file mode 100644 index 00000000..22632dc0 --- /dev/null +++ b/cdk/src/bootstrap/policies/application.ts @@ -0,0 +1,208 @@ +/** + * 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. + */ + +/* eslint-disable @cdklabs/no-literal-partition */ +// ARN partitions are intentionally literal — this policy is a bootstrap +// template matching the exact resource patterns in DEPLOYMENT_ROLES.md. + +import { aws_iam as iam } from 'aws-cdk-lib'; + +/** + * Returns the IAM PolicyDocument for the IaCRole-ABCA-Application role. + * + * Covers: DynamoDB, Lambda, API Gateway, Cognito, WAFv2, EventBridge, + * Secrets Manager permissions. + */ +export function applicationPolicy(): iam.PolicyDocument { + return new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + sid: 'DynamoDB', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: ['arn:aws:dynamodb:*:*:table/backgroundagent-dev-*'], + }), + + new iam.PolicyStatement({ + sid: 'Lambda', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: [ + 'arn:aws:lambda:*:*:function:backgroundagent-dev-*', + 'arn:aws:lambda:*:*:function:backgroundagent-dev-AWS*', + ], + }), + + new iam.PolicyStatement({ + sid: 'APIGateway', + effect: iam.Effect.ALLOW, + actions: [ + 'apigateway:POST', + 'apigateway:GET', + 'apigateway:PUT', + 'apigateway:PATCH', + 'apigateway:DELETE', + 'apigateway:TagResource', + 'apigateway:UntagResource', + 'apigateway:SetWebACL', + 'apigateway:UpdateRestApiPolicy', + ], + resources: [ + 'arn:aws:apigateway:*::/restapis', + 'arn:aws:apigateway:*::/restapis/*', + 'arn:aws:apigateway:*::/account', + 'arn:aws:apigateway:*::/tags/*', + ], + }), + + new iam.PolicyStatement({ + sid: 'Cognito', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: ['arn:aws:cognito-idp:*:*:userpool/*'], + }), + + new iam.PolicyStatement({ + sid: 'WAFv2', + effect: iam.Effect.ALLOW, + actions: [ + 'wafv2:CreateWebACL', + 'wafv2:DeleteWebACL', + 'wafv2:GetWebACL', + 'wafv2:UpdateWebACL', + 'wafv2:AssociateWebACL', + 'wafv2:DisassociateWebACL', + 'wafv2:ListTagsForResource', + 'wafv2:TagResource', + 'wafv2:UntagResource', + 'wafv2:GetWebACLForResource', + ], + resources: [ + 'arn:aws:wafv2:*:*:regional/webacl/*', + 'arn:aws:wafv2:*:*:regional/managedruleset/*', + ], + }), + + new iam.PolicyStatement({ + sid: 'EventBridge', + effect: iam.Effect.ALLOW, + actions: [ + 'events:PutRule', + 'events:DeleteRule', + 'events:DescribeRule', + 'events:PutTargets', + 'events:RemoveTargets', + 'events:ListTargetsByRule', + 'events:TagResource', + 'events:UntagResource', + 'events:ListTagsForResource', + ], + resources: ['arn:aws:events:*:*:rule/backgroundagent-dev-*'], + }), + + new iam.PolicyStatement({ + sid: 'SecretsManager', + effect: iam.Effect.ALLOW, + actions: [ + 'secretsmanager:CreateSecret', + 'secretsmanager:DeleteSecret', + 'secretsmanager:DescribeSecret', + 'secretsmanager:GetSecretValue', + 'secretsmanager:PutSecretValue', + 'secretsmanager:UpdateSecret', + 'secretsmanager:TagResource', + 'secretsmanager:UntagResource', + 'secretsmanager:GetResourcePolicy', + 'secretsmanager:PutResourcePolicy', + 'secretsmanager:DeleteResourcePolicy', + ], + resources: [ + 'arn:aws:secretsmanager:*:*:secret:backgroundagent-*', + 'arn:aws:secretsmanager:*:*:secret:GitHubTokenSecret*', + ], + }), + + new iam.PolicyStatement({ + sid: 'SecretsManagerAccountLevel', + effect: iam.Effect.ALLOW, + actions: ['secretsmanager:GetRandomPassword'], + resources: ['*'], + }), + ], + }); +} diff --git a/cdk/src/bootstrap/policies/index.ts b/cdk/src/bootstrap/policies/index.ts new file mode 100644 index 00000000..f8487482 --- /dev/null +++ b/cdk/src/bootstrap/policies/index.ts @@ -0,0 +1,35 @@ +/** + * 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'; + +import { applicationPolicy } from './application'; +import { infrastructurePolicy } from './infrastructure'; +import { observabilityPolicy } from './observability'; + +export { applicationPolicy } from './application'; +export { infrastructurePolicy } from './infrastructure'; +export { observabilityPolicy } from './observability'; + +/** + * Returns all three bootstrap IAM PolicyDocuments as an array. + */ +export function allPolicies(): iam.PolicyDocument[] { + return [infrastructurePolicy(), applicationPolicy(), observabilityPolicy()]; +} diff --git a/cdk/src/bootstrap/policies/infrastructure.ts b/cdk/src/bootstrap/policies/infrastructure.ts new file mode 100644 index 00000000..7ede06d0 --- /dev/null +++ b/cdk/src/bootstrap/policies/infrastructure.ts @@ -0,0 +1,213 @@ +/** + * 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. + */ + +/* eslint-disable @cdklabs/no-literal-partition */ +// ARN partitions are intentionally literal — this policy is a bootstrap +// template matching the exact resource patterns in DEPLOYMENT_ROLES.md. + +import { aws_iam as iam } from 'aws-cdk-lib'; + +/** + * Returns the IAM PolicyDocument for the IaCRole-ABCA-Infrastructure role. + * + * Covers: CloudFormation, IAM roles/policies, VPC networking, and + * Route 53 Resolver DNS Firewall permissions. + */ +export function infrastructurePolicy(): iam.PolicyDocument { + return new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + sid: 'CloudFormationSelf', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: [ + 'arn:aws:cloudformation:*:*:stack/backgroundagent-dev/*', + 'arn:aws:cloudformation:*:*:stack/CDKToolkit/*', + ], + }), + + new iam.PolicyStatement({ + sid: 'IAMRolesAndPolicies', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: [ + 'arn:aws:iam::*:role/backgroundagent-dev-*', + 'arn:aws:iam::*:policy/backgroundagent-dev-*', + 'arn:aws:iam::*:role/aws-service-role/*', + ], + }), + + new iam.PolicyStatement({ + sid: 'IAMPassRole', + effect: iam.Effect.ALLOW, + actions: ['iam:PassRole'], + resources: ['arn:aws:iam::*:role/backgroundagent-dev-*'], + conditions: { + 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', + ], + }, + }, + }), + + new iam.PolicyStatement({ + sid: 'VPCNetworking', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: ['*'], + }), + + new iam.PolicyStatement({ + sid: 'Route53ResolverDNSFirewall', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: ['*'], + }), + ], + }); +} diff --git a/cdk/src/bootstrap/policies/observability.ts b/cdk/src/bootstrap/policies/observability.ts new file mode 100644 index 00000000..0c45cbe8 --- /dev/null +++ b/cdk/src/bootstrap/policies/observability.ts @@ -0,0 +1,207 @@ +/** + * 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. + */ + +/* eslint-disable @cdklabs/no-literal-partition */ +// ARN partitions are intentionally literal — this policy is a bootstrap +// template matching the exact resource patterns in DEPLOYMENT_ROLES.md. + +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. + */ +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, + actions: [ + 'bedrock:CreateGuardrail', + 'bedrock:DeleteGuardrail', + 'bedrock:GetGuardrail', + 'bedrock:UpdateGuardrail', + 'bedrock:CreateGuardrailVersion', + 'bedrock:ListGuardrails', + 'bedrock:TagResource', + 'bedrock:UntagResource', + 'bedrock:ListTagsForResource', + 'bedrock:PutModelInvocationLoggingConfiguration', + 'bedrock:DeleteModelInvocationLoggingConfiguration', + 'bedrock:GetModelInvocationLoggingConfiguration', + ], + resources: ['*'], + }), + + new iam.PolicyStatement({ + sid: 'CloudWatchLogsAndDashboards', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: ['*'], + }), + + new iam.PolicyStatement({ + sid: 'S3CDKAssets', + effect: iam.Effect.ALLOW, + actions: [ + 's3:GetObject', + 's3:PutObject', + 's3:GetBucketLocation', + 's3:ListBucket', + ], + resources: [ + 'arn:aws:s3:::cdk-hnb659fds-assets-*', + 'arn:aws:s3:::cdk-hnb659fds-assets-*/*', + ], + }), + + new iam.PolicyStatement({ + sid: 'KMSForCDKAssets', + effect: iam.Effect.ALLOW, + actions: [ + 'kms:CreateGrant', + 'kms:Decrypt', + 'kms:DescribeKey', + 'kms:Encrypt', + 'kms:GenerateDataKey', + ], + resources: ['*'], + }), + + new iam.PolicyStatement({ + sid: 'ECRForDockerAssets', + effect: iam.Effect.ALLOW, + actions: [ + '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', + ], + resources: [ + 'arn:aws:ecr:*:*:repository/cdk-hnb659fds-container-assets-*', + 'arn:aws:ecr:*:*:repository/backgroundagent-*', + ], + }), + + new iam.PolicyStatement({ + sid: 'ECRAuthToken', + effect: iam.Effect.ALLOW, + actions: ['ecr:GetAuthorizationToken'], + resources: ['*'], + }), + + new iam.PolicyStatement({ + sid: 'XRay', + effect: iam.Effect.ALLOW, + actions: [ + 'xray:UpdateTraceSegmentDestination', + 'xray:GetTraceSegmentDestination', + 'xray:ListResourcePolicies', + 'xray:PutResourcePolicy', + ], + resources: ['*'], + }), + + new iam.PolicyStatement({ + sid: 'SSMParameterStoreForCDK', + effect: iam.Effect.ALLOW, + actions: [ + 'ssm:GetParameter', + 'ssm:GetParameters', + 'ssm:PutParameter', + 'ssm:DeleteParameter', + ], + resources: ['arn:aws:ssm:*:*:parameter/cdk-bootstrap/*'], + }), + + new iam.PolicyStatement({ + sid: 'STSForCDK', + effect: iam.Effect.ALLOW, + actions: [ + 'sts:AssumeRole', + 'sts:GetCallerIdentity', + ], + resources: ['arn:aws:iam::*:role/cdk-hnb659fds-*'], + }), + ], + }); +} diff --git a/cdk/src/bootstrap/version.ts b/cdk/src/bootstrap/version.ts new file mode 100644 index 00000000..5a07b9c1 --- /dev/null +++ b/cdk/src/bootstrap/version.ts @@ -0,0 +1,40 @@ +/** + * 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 { createHash } from 'node:crypto'; + +import { allPolicies } from './policies'; + +/** Semantic version of the bootstrap policy bundle. */ +export const BOOTSTRAP_VERSION = '1.0.0'; + +/** + * Computes a SHA-256 hash over all bootstrap policies. + * The hash is deterministic: policies are serialized with sorted keys + * so that object property ordering does not affect the digest. + */ +export function computeBootstrapHash(): string { + const policies = allPolicies(); + const normalized = policies.map((p) => { + const json = p.toJSON(); + return JSON.stringify(json, Object.keys(json).sort()); + }); + const payload = JSON.stringify(normalized); + return createHash('sha256').update(payload).digest('hex'); +} diff --git a/cdk/test/bootstrap/__snapshots__/version.test.ts.snap b/cdk/test/bootstrap/__snapshots__/version.test.ts.snap new file mode 100644 index 00000000..c3a8e637 --- /dev/null +++ b/cdk/test/bootstrap/__snapshots__/version.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`bootstrap version module hash is stable 1`] = `"4892570024965c2e99ef0d9f7ef0a61e4b939ba69c5df52e4bc1647522dad283"`; diff --git a/cdk/test/bootstrap/artifact-sync.test.ts b/cdk/test/bootstrap/artifact-sync.test.ts new file mode 100644 index 00000000..95fb9a9c --- /dev/null +++ b/cdk/test/bootstrap/artifact-sync.test.ts @@ -0,0 +1,55 @@ +/** + * 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 { infrastructurePolicy, applicationPolicy, observabilityPolicy } from '../../src/bootstrap/policies'; +import { BOOTSTRAP_VERSION, computeBootstrapHash } from '../../src/bootstrap/version'; + +const artifactsDir = join(__dirname, '..', '..', 'bootstrap'); + +describe('Bootstrap artifact sync', () => { + it('committed BOOTSTRAP_HASH matches computed hash', () => { + const committed = readFileSync(join(artifactsDir, 'BOOTSTRAP_HASH'), 'utf-8').trim(); + expect(committed).toBe(computeBootstrapHash()); + }); + + it('committed BOOTSTRAP_VERSION matches source constant', () => { + const committed = readFileSync(join(artifactsDir, 'BOOTSTRAP_VERSION'), 'utf-8').trim(); + expect(committed).toBe(BOOTSTRAP_VERSION); + }); + + describe('committed JSON artifacts match current policy output', () => { + const cases = [ + { name: 'infrastructure', fn: infrastructurePolicy }, + { name: 'application', fn: applicationPolicy }, + { name: 'observability', fn: observabilityPolicy }, + ] as const; + + for (const { name, fn } of cases) { + it(`${name}.json is in sync`, () => { + const committed = JSON.parse( + readFileSync(join(artifactsDir, 'policies', `${name}.json`), 'utf-8'), + ); + const generated = fn().toJSON(); + expect(committed).toEqual(generated); + }); + } + }); +}); diff --git a/cdk/test/bootstrap/golden-baseline.test.ts b/cdk/test/bootstrap/golden-baseline.test.ts new file mode 100644 index 00000000..b884d5e3 --- /dev/null +++ b/cdk/test/bootstrap/golden-baseline.test.ts @@ -0,0 +1,119 @@ +/** + * 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 { Stack } from 'aws-cdk-lib'; + +import { applicationPolicy, infrastructurePolicy, observabilityPolicy } from '../../src/bootstrap/policies'; + +/** + * Extracts all ```json ... ``` fenced code blocks from a markdown string. + */ +function extractJsonBlocks(markdown: string): string[] { + const blocks: string[] = []; + const regex = /```json\n([\s\S]*?)```/g; + let match: RegExpExecArray | null; + while ((match = regex.exec(markdown)) !== null) { + blocks.push(match[1].trim()); + } + return blocks; +} + +interface NormalizedStatement { + sid: string; + actions: string[]; + resources: string[]; +} + +/** + * Normalizes policy statements for comparison by sorting actions and resources. + */ +function normalizeStatements( + statements: Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }>, +): NormalizedStatement[] { + return statements.map((s) => { + const actions = Array.isArray(s.Action) ? [...s.Action] : [s.Action ?? '']; + const resources = Array.isArray(s.Resource) ? [...s.Resource] : [s.Resource ?? '']; + return { + sid: s.Sid ?? '', + actions: actions.sort(), + resources: resources.sort(), + }; + }); +} + +describe('Golden-file parity: TypeScript policies match DEPLOYMENT_ROLES.md', () => { + const stack = new Stack(); + + // Read the source-of-truth markdown + const markdownPath = join(__dirname, '..', '..', '..', 'docs', 'design', 'DEPLOYMENT_ROLES.md'); + const markdown = readFileSync(markdownPath, 'utf-8'); + + // Extract JSON blocks: [0]=trust, [1]=infrastructure, [2]=application, [3]=observability, [4]=ECS + const jsonBlocks = extractJsonBlocks(markdown); + const goldenInfra = JSON.parse(jsonBlocks[1]); + const goldenApp = JSON.parse(jsonBlocks[2]); + const goldenObs = JSON.parse(jsonBlocks[3]); + + // Resolve TypeScript policies via CDK Stack + const tsInfra = stack.resolve(infrastructurePolicy()); + const tsApp = stack.resolve(applicationPolicy()); + const tsObs = stack.resolve(observabilityPolicy()); + + const testCases: 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) { + describe(`${name} policy`, () => { + const goldenNorm = normalizeStatements( + golden.Statement as Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }>, + ); + const tsNorm = normalizeStatements( + typescript.Statement as Array<{ Sid?: string; Action?: string | string[]; Resource?: string | string[] }>, + ); + + it('has the same SIDs in the same order', () => { + 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); + } + }); + }); + } +}); diff --git a/cdk/test/bootstrap/policies.test.ts b/cdk/test/bootstrap/policies.test.ts new file mode 100644 index 00000000..70fe5a67 --- /dev/null +++ b/cdk/test/bootstrap/policies.test.ts @@ -0,0 +1,241 @@ +/** + * 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 { Stack } from 'aws-cdk-lib'; +import { allPolicies } from '../../src/bootstrap/policies'; +import { applicationPolicy } from '../../src/bootstrap/policies/application'; +import { infrastructurePolicy } from '../../src/bootstrap/policies/infrastructure'; +import { observabilityPolicy } from '../../src/bootstrap/policies/observability'; + +describe('infrastructurePolicy', () => { + const stack = new Stack(); + const doc = infrastructurePolicy(); + 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([ + 'CloudFormationSelf', + 'IAMRolesAndPolicies', + 'IAMPassRole', + 'VPCNetworking', + 'Route53ResolverDNSFirewall', + ]); + }); + + 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([ + 'cloudformation', + 'iam', + 'ec2', + 'route53resolver', + ]), + ); + }); +}); + +describe('IaCRole-ABCA-Application', () => { + const stack = new Stack(); + const doc = applicationPolicy(); + 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([ + 'DynamoDB', + 'Lambda', + 'APIGateway', + 'Cognito', + 'WAFv2', + 'EventBridge', + 'SecretsManager', + 'SecretsManagerAccountLevel', + ]); + }); + + 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([ + 'apigateway', + 'cognito-idp', + 'dynamodb', + 'events', + 'lambda', + 'secretsmanager', + 'wafv2', + ]), + ); + }); +}); + +describe('IaCRole-ABCA-Observability', () => { + const stack = new Stack(); + const doc = observabilityPolicy(); + 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', + 'BedrockGuardrailsAndLogging', + 'CloudWatchLogsAndDashboards', + 'S3CDKAssets', + 'KMSForCDKAssets', + 'ECRForDockerAssets', + 'ECRAuthToken', + 'XRay', + 'SSMParameterStoreForCDK', + 'STSForCDK', + ]); + }); + + 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', + 'bedrock-agentcore', + 'cloudwatch', + 'ecr', + 'kms', + 'logs', + 's3', + 'ssm', + 'sts', + 'xray', + ]), + ); + }); +}); + +describe('Cross-policy validation', () => { + const stack = new Stack(); + const policies = allPolicies(); + + it('all SIDs are globally unique across all three policies', () => { + const allSids: string[] = []; + + for (const policy of policies) { + const resolved = stack.resolve(policy); + const statements = resolved.Statement as Array<{ Sid: string }>; + allSids.push(...statements.map((s) => s.Sid)); + } + + const unique = new Set(allSids); + expect(unique.size).toBe(allSids.length); + }); + + it('returns exactly 3 policies', () => { + expect(policies).toHaveLength(3); + }); + + it('every policy is under 6144 character limit', () => { + for (const policy of policies) { + const rendered = JSON.stringify(policy.toJSON()); + expect(rendered.length).toBeLessThan(6144); + } + }); +}); diff --git a/cdk/test/bootstrap/version.test.ts b/cdk/test/bootstrap/version.test.ts new file mode 100644 index 00000000..1c1bce5c --- /dev/null +++ b/cdk/test/bootstrap/version.test.ts @@ -0,0 +1,42 @@ +/** + * 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 { BOOTSTRAP_VERSION, computeBootstrapHash } from '../../src/bootstrap/version'; + +describe('bootstrap version module', () => { + it('BOOTSTRAP_VERSION matches semver format', () => { + expect(BOOTSTRAP_VERSION).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('computeBootstrapHash returns a 64-char hex string (SHA256)', () => { + const hash = computeBootstrapHash(); + expect(hash).toMatch(/^[0-9a-f]{64}$/); + }); + + it('hash is deterministic (calling twice gives same result)', () => { + const hash1 = computeBootstrapHash(); + const hash2 = computeBootstrapHash(); + expect(hash1).toBe(hash2); + }); + + it('hash is stable', () => { + const hash = computeBootstrapHash(); + expect(hash).toMatchSnapshot(); + }); +});