import * as cdk from "aws-cdk-lib"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; import * as securityhub from "aws-cdk-lib/aws-securityhub"; import { Construct } from "constructs"; import { BedrockLoggingRegional } from "./bedrock-logging-regional"; /** * Regional security-baseline stack for secondary regions (INFRA-16, INFRA-91). * * The account baseline (lib/account-baseline-stack.ts) is us-east-1 only by * design. This stack extends a minimal detective/logging footprint into the * other regions where workloads or Bedrock traffic land, WITHOUT duplicating * the full us-east-1 stack. * * Composition is opt-in per region via props so one class serves both * secondary regions: * - bedrockLogging → INFRA-91 Bedrock invocation-logging destination + role * (us-west-2 + us-east-2; codifies live CLI state) * - configRecorder → INFRA-16 AWS Config recorder role + delivery bucket * (us-east-2; recorder/channel applied via CLI, see header) * - securityHub → INFRA-16 Security Hub + FSBP/CIS standards (us-east-2) * * GuardDuty and default-VPC flow logs already exist in us-east-2 (applied * out-of-band) and are intentionally NOT adopted here yet — importing live * resources of those L1 types risks a replace diff; they are tracked as a * follow-up so this reconciliation stays additive/non-destructive. */ export interface RegionalBaselineStackProps extends cdk.StackProps { /** INFRA-91: stand up Bedrock invocation-logging destination + delivery role. */ readonly bedrockLogging?: boolean; /** INFRA-16: stand up AWS Config recorder role + delivery bucket. */ readonly configRecorder?: boolean; /** INFRA-16: enable Security Hub with FSBP + CIS v3.0 standards. */ readonly securityHub?: boolean; } export class RegionalBaselineStack extends cdk.Stack { constructor(scope: Construct, id: string, props: RegionalBaselineStackProps) { super(scope, id, props); if (props.bedrockLogging) { // INFRA-91 — codifies live us-west-2 / us-east-2 Bedrock logging. new BedrockLoggingRegional(this, "BedrockLogging"); } if (props.configRecorder) { // INFRA-16 — AWS Config in us-east-2. Same pattern as the us-east-1 // DetectiveControls construct: the role + delivery bucket live in IaC; // the recorder + delivery channel are applied via CLI post-deploy because // the L1 ConfigurationRecorder/DeliveryChannel pair deadlocks CFN (the // recorder will not reach CREATE_COMPLETE without a channel, and the // channel cannot be created until the recorder completes — observed in // us-east-1 2026-06-01). Commands are documented in the README. const configBucket = new s3.Bucket(this, "ConfigBucket", { bucketName: `seahaven-config-${this.region}-${this.account}`, encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, versioned: true, lifecycleRules: [ { id: "expire-old-config", expiration: cdk.Duration.days(365), abortIncompleteMultipartUploadAfter: cdk.Duration.days(7), }, ], removalPolicy: cdk.RemovalPolicy.RETAIN, }); configBucket.addToResourcePolicy( new iam.PolicyStatement({ sid: "AWSConfigBucketPermissionsCheck", effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal("config.amazonaws.com")], actions: ["s3:GetBucketAcl", "s3:ListBucket"], resources: [configBucket.bucketArn], conditions: { StringEquals: { "aws:SourceAccount": this.account }, }, }) ); configBucket.addToResourcePolicy( new iam.PolicyStatement({ sid: "AWSConfigBucketDelivery", effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal("config.amazonaws.com")], actions: ["s3:PutObject"], resources: [ configBucket.arnForObjects(`AWSLogs/${this.account}/Config/*`), ], conditions: { StringEquals: { "s3:x-amz-acl": "bucket-owner-full-control", "aws:SourceAccount": this.account, }, }, }) ); // Region-suffixed role name so it does not collide with the us-east-1 // seahaven-config-recorder-role (IAM roles are global by name). const recorderRole = new iam.Role(this, "ConfigRecorderRole", { roleName: `seahaven-config-recorder-role-${this.region}`, assumedBy: new iam.ServicePrincipal("config.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "service-role/AWS_ConfigRole" ), ], }); recorderRole.addToPolicy( new iam.PolicyStatement({ sid: "ConfigDeliveryToBucket", effect: iam.Effect.ALLOW, actions: ["s3:PutObject"], resources: [ configBucket.arnForObjects(`AWSLogs/${this.account}/Config/*`), ], conditions: { StringEquals: { "s3:x-amz-acl": "bucket-owner-full-control" }, }, }) ); recorderRole.addToPolicy( new iam.PolicyStatement({ sid: "ConfigBucketAcl", effect: iam.Effect.ALLOW, actions: ["s3:GetBucketAcl"], resources: [configBucket.bucketArn], }) ); new cdk.CfnOutput(this, "ConfigRecorderRoleArn", { value: recorderRole.roleArn, }); new cdk.CfnOutput(this, "ConfigBucketName", { value: configBucket.bucketName, }); } if (props.securityHub) { // INFRA-16 — Security Hub (FSBP + CIS v3.0) in us-east-2. Mirrors the // us-east-1 DetectiveControls Security Hub block. Findings populate once // the Config recorder above is recording. const hub = new securityhub.CfnHub(this, "SecurityHub", { enableDefaultStandards: false, controlFindingGenerator: "SECURITY_CONTROL", autoEnableControls: true, }); const fsbpArn = cdk.Arn.format( { service: "securityhub", region: this.region, account: "", resource: "standards", resourceName: "aws-foundational-security-best-practices/v/1.0.0", }, this ); const cisArn = cdk.Arn.format( { service: "securityhub", region: this.region, account: "", resource: "standards", resourceName: "cis-aws-foundations-benchmark/v/3.0.0", }, this ); const fsbp = new securityhub.CfnStandard(this, "StandardFSBP", { standardsArn: fsbpArn, }); fsbp.node.addDependency(hub); const cis = new securityhub.CfnStandard(this, "StandardCIS", { standardsArn: cisArn, }); cis.node.addDependency(hub); } cdk.Tags.of(this).add("Project", "account-baseline"); cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); cdk.Tags.of(this).add("Environment", "prod"); cdk.Tags.of(this).add("ManagedBy", "cdk"); } }