diff --git a/lib/account-baseline-stack.ts b/lib/account-baseline-stack.ts index 606ed48..5126d06 100644 --- a/lib/account-baseline-stack.ts +++ b/lib/account-baseline-stack.ts @@ -31,6 +31,11 @@ export interface AccountBaselineStackProps extends cdk.StackProps { readonly monthlyBudgetUsd: number; /** Email for budget threshold alerts (M-10). */ readonly budgetAlertEmail: string; + /** + * VPC ids to attach ALL-traffic flow logs to (H-14). Logical IDs are + * index-derived — only append, never reorder (see lib/flow-logs.ts). + */ + readonly flowLogVpcIds: string[]; } export class AccountBaselineStack extends cdk.Stack { @@ -225,9 +230,12 @@ export class AccountBaselineStack extends cdk.Stack { // ── Day 1 detective layer + governance toggles ── // Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5). - new DetectiveControls(this, "DetectiveControls"); + new DetectiveControls(this, "DetectiveControls", { + namePrefix: "seahaven", + }); // Monthly cost budget (M-10). Other governance toggles are CLI + documented. new GovernanceToggles(this, "GovernanceToggles", { + budgetName: "seahaven-monthly-cost", monthlyLimitUsd: props.monthlyBudgetUsd, alertEmail: props.budgetAlertEmail, }); @@ -239,7 +247,10 @@ export class AccountBaselineStack extends cdk.Stack { alarmEmail: props.budgetAlertEmail, trailLogGroup, }); - new FlowLogs(this, "FlowLogs"); + new FlowLogs(this, "FlowLogs", { + namePrefix: "seahaven", + vpcIds: props.flowLogVpcIds, + }); new SesMonitoring(this, "SesMonitoring"); // Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks. new AppWebAcl(this, "AppWebAcl"); diff --git a/lib/detective-controls.ts b/lib/detective-controls.ts index 8bc164f..a3fa3bd 100644 --- a/lib/detective-controls.ts +++ b/lib/detective-controls.ts @@ -16,14 +16,29 @@ import { Construct } from "constructs"; * H-4 Security Hub with AWS FSBP + CIS v3.0 standards * M-5 IAM Access Analyzer (account-scoped external-access analyzer) * + * Serves both the management-account baseline (namePrefix "seahaven") and + * member-account baselines (e.g. "seahaven-extdev") — physical resource names + * are prefix-parameterized, structure is identical. + * * Scope is us-east-1 only — all workloads live here (Adam's call, Day 1). * Multi-region coverage is a documented follow-up. */ +export interface DetectiveControlsProps { + /** + * Physical-name prefix for account-scoped resources (bucket, roles, recorder, + * delivery channel, analyzer). Also baked into the custom resources' + * PhysicalResourceId strings — changing it on a deployed stack REPLACES the + * custom resources; keep it stable per account. + */ + readonly namePrefix: string; +} + export class DetectiveControls extends Construct { - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: DetectiveControlsProps) { super(scope, id); const stack = cdk.Stack.of(this); + const prefix = props.namePrefix; // ────────────────────────────────────────────────────────────────────── // H-2 AWS Config @@ -33,7 +48,7 @@ export class DetectiveControls extends Construct { // versioned, SSE-S3 (Config writes here; SSE-S3 avoids a second KMS-grant // failure mode and is sufficient — CIS does not require a CMK here). const configBucket = new s3.Bucket(this, "ConfigBucket", { - bucketName: `seahaven-config-${stack.account}`, + bucketName: `${prefix}-config-${stack.account}`, encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, @@ -85,7 +100,7 @@ export class DetectiveControls extends Construct { // grants delivery to the bucket above. **This role is the Day 1 cross-review // item (IAM change per CLAUDE.md).** const recorderRole = new iam.Role(this, "ConfigRecorderRole", { - roleName: "seahaven-config-recorder-role", + roleName: `${prefix}-config-recorder-role`, assumedBy: new iam.ServicePrincipal("config.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWS_ConfigRole"), @@ -143,7 +158,7 @@ export class DetectiveControls extends Construct { this, "ConfigCustomResourceRole", { - roleName: "seahaven-config-custom-resource-role", + roleName: `${prefix}-config-custom-resource-role`, assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( @@ -191,7 +206,7 @@ export class DetectiveControls extends Construct { action: "putConfigurationRecorder", parameters: { ConfigurationRecorder: { - name: "seahaven-config-recorder", + name: `${prefix}-config-recorder`, roleARN: recorderRole.roleArn, recordingGroup: { allSupported: true, @@ -200,7 +215,7 @@ export class DetectiveControls extends Construct { }, }, // No meaningful response data to extract. - physicalResourceId: cr.PhysicalResourceId.of("seahaven-config-recorder"), + physicalResourceId: cr.PhysicalResourceId.of(`${prefix}-config-recorder`), }; const putChannelCall: cr.AwsSdkCall = { @@ -208,7 +223,7 @@ export class DetectiveControls extends Construct { action: "putDeliveryChannel", parameters: { DeliveryChannel: { - name: "seahaven-config-delivery", + name: `${prefix}-config-delivery`, s3BucketName: configBucket.bucketName, configSnapshotDeliveryProperties: { deliveryFrequency: "TwentyFour_Hours", @@ -216,7 +231,7 @@ export class DetectiveControls extends Construct { }, }, physicalResourceId: cr.PhysicalResourceId.of( - "seahaven-config-delivery" + `${prefix}-config-delivery` ), }; @@ -224,10 +239,10 @@ export class DetectiveControls extends Construct { service: "ConfigService", action: "startConfigurationRecorder", parameters: { - ConfigurationRecorderName: "seahaven-config-recorder", + ConfigurationRecorderName: `${prefix}-config-recorder`, }, physicalResourceId: cr.PhysicalResourceId.of( - "seahaven-config-recorder-start" + `${prefix}-config-recorder-start` ), }; @@ -265,10 +280,10 @@ export class DetectiveControls extends Construct { service: "ConfigService", action: "stopConfigurationRecorder", parameters: { - ConfigurationRecorderName: "seahaven-config-recorder", + ConfigurationRecorderName: `${prefix}-config-recorder`, }, physicalResourceId: cr.PhysicalResourceId.of( - "seahaven-config-recorder-stop" + `${prefix}-config-recorder-stop` ), }, role: configCustomResourceRole, @@ -336,7 +351,7 @@ export class DetectiveControls extends Construct { // M-5 IAM Access Analyzer (free, account-scoped external-access) // ────────────────────────────────────────────────────────────────────── new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", { - analyzerName: "seahaven-account-analyzer", + analyzerName: `${prefix}-account-analyzer`, type: "ACCOUNT", }); diff --git a/lib/flow-logs.ts b/lib/flow-logs.ts index 75205d6..151dd84 100644 --- a/lib/flow-logs.ts +++ b/lib/flow-logs.ts @@ -5,32 +5,39 @@ import * as ec2 from "aws-cdk-lib/aws-ec2"; import { Construct } from "constructs"; /** - * VPC flow logs for every VPC, delivered to a hardened S3 bucket (audit H-14, - * CIS 3.9/5.6). S3 destination (not CloudWatch Logs) for cost — query - * forensically via Athena. ALL traffic (accept + reject). + * VPC flow logs delivered to a hardened S3 bucket (audit H-14, CIS 3.9/5.6). + * S3 destination (not CloudWatch Logs) for cost — query forensically via + * Athena. ALL traffic (accept + reject). + * + * Serves both the management-account baseline and member-account baselines: + * VPC ids are passed via props (the management account pins its 5 audited + * VPCs in bin/app.ts; member accounts source theirs from cdk context because + * their VPCs change over time). Pass an empty list to create the hardened + * destination bucket without any flow logs attached yet. * * S3 delivery needs no IAM role; instead the bucket policy grants the * `delivery.logs.amazonaws.com` service principal write access, scoped to this * account. That bucket policy is the Day 2 cross-review item. */ - -// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. -const VPC_IDS = [ - "vpc-061d66990b6a4d1fb", - "vpc-0542a9e934b417d23", - "vpc-062d200c68bd4ca0e", - "vpc-0d3d4b67bd0cf8a68", - "vpc-02c10a89d66f6f9b8", -]; +export interface FlowLogsProps { + /** Physical-name prefix for the destination bucket (e.g. "seahaven" or "seahaven-extdev"). */ + readonly namePrefix: string; + /** + * VPC ids to attach ALL-traffic flow logs to. May be empty. Logical IDs are + * index-derived (FlowLog0, FlowLog1, ...) — REORDERING this list replaces + * deployed flow logs; only append. + */ + readonly vpcIds: string[]; +} export class FlowLogs extends Construct { - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: FlowLogsProps) { super(scope, id); const stack = cdk.Stack.of(this); const bucket = new s3.Bucket(this, "FlowLogsBucket", { - bucketName: `seahaven-vpc-flow-logs-${stack.account}`, + bucketName: `${props.namePrefix}-vpc-flow-logs-${stack.account}`, encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, @@ -90,7 +97,7 @@ export class FlowLogs extends Construct { }) ); - VPC_IDS.forEach((vpcId, i) => { + props.vpcIds.forEach((vpcId, i) => { const flowLog = new ec2.CfnFlowLog(this, `FlowLog${i}`, { resourceId: vpcId, resourceType: "VPC", diff --git a/lib/governance-toggles.ts b/lib/governance-toggles.ts index e1544e6..d198726 100644 --- a/lib/governance-toggles.ts +++ b/lib/governance-toggles.ts @@ -3,6 +3,8 @@ import * as budgets from "aws-cdk-lib/aws-budgets"; import { Construct } from "constructs"; export interface GovernanceTogglesProps { + /** Physical name of the AWS Budget (account-scoped, e.g. "seahaven-monthly-cost"). */ + readonly budgetName: string; /** Monthly cost budget ceiling in USD. */ readonly monthlyLimitUsd: number; /** Email that receives the budget threshold alerts. */ @@ -11,7 +13,8 @@ export interface GovernanceTogglesProps { /** * Account-level governance toggles that *are* expressible as CloudFormation - * (audit Day 1). + * (audit Day 1). Serves both the management-account baseline and member-account + * baselines (budget name parameterized per account). * * Closes: * M-10 Monthly AWS Budget with 80% / 100% actual + 100% forecast alerts @@ -37,7 +40,7 @@ export class GovernanceToggles extends Construct { new budgets.CfnBudget(this, "MonthlyCostBudget", { budget: { - budgetName: "seahaven-monthly-cost", + budgetName: props.budgetName, budgetType: "COST", timeUnit: "MONTHLY", budgetLimit: {