From b10e88497a5778dd430c54fb6e0eac1395c3134a Mon Sep 17 00:00:00 2001 From: Austin Vazquez Date: Wed, 12 Jun 2024 07:42:34 -0700 Subject: [PATCH] feat: enable termination protection on CloudFormation stacks Signed-off-by: Austin Vazquez --- lib/artifact-bucket-cloudfront.ts | 3 +++ lib/asg-runner-stack.ts | 2 ++ lib/aspects/stack-termination-protection.ts | 24 +++++++++++++++++++ lib/continuous-integration-stack.ts | 2 ++ lib/ecr-repo-stack.ts | 3 +++ lib/event-bridge-scan-notifs-stack.ts | 2 ++ lib/finch-pipeline-app-stage.ts | 4 ++-- lib/finch-pipeline-stack.ts | 3 +++ lib/pvre-reporting-stack.ts | 2 ++ test/artifact-bucket-cloudfront.test.ts | 2 ++ test/asg-runner-stack.test.ts | 6 +++++ .../stack-termination-protection.test.ts | 20 ++++++++++++++++ test/ecr-repo-stack.test.ts | 3 +++ test/event-bridge-scan-notifs-stack.test.ts | 2 ++ test/finch-pipeline-stack.test.ts | 2 ++ 15 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 lib/aspects/stack-termination-protection.ts create mode 100644 test/aspects/stack-termination-protection.test.ts diff --git a/lib/artifact-bucket-cloudfront.ts b/lib/artifact-bucket-cloudfront.ts index 3720df69..1119f50f 100644 --- a/lib/artifact-bucket-cloudfront.ts +++ b/lib/artifact-bucket-cloudfront.ts @@ -5,12 +5,15 @@ import { Construct } from 'constructs'; import { CloudfrontCdn } from './cloudfront_cdn'; import * as s3Deployment from 'aws-cdk-lib/aws-s3-deployment'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; export class ArtifactBucketCloudfrontStack extends cdk.Stack { public readonly urlOutput: CfnOutput; public readonly bucket: s3.Bucket; constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); + const bucketName = `finch-artifact-bucket-${stage.toLowerCase()}-${cdk.Stack.of(this)?.account}`; const artifactBucket = new s3.Bucket(this, 'ArtifactBucket', { bucketName, diff --git a/lib/asg-runner-stack.ts b/lib/asg-runner-stack.ts index 68c45b3c..d31a2745 100644 --- a/lib/asg-runner-stack.ts +++ b/lib/asg-runner-stack.ts @@ -8,6 +8,7 @@ import { PlatformType, RunnerType } from '../config/runner-config'; import { readFileSync } from 'fs'; import { ENVIRONMENT_STAGE } from './finch-pipeline-app-stage'; import { UpdatePolicy } from 'aws-cdk-lib/aws-autoscaling'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; interface ASGRunnerStackProps extends cdk.StackProps { env: cdk.Environment | undefined; @@ -26,6 +27,7 @@ interface ASGRunnerStackProps extends cdk.StackProps { export class ASGRunnerStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ASGRunnerStackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); const platform = props.type.platform; const arch = props.type.arch === 'arm' ? `arm64_${platform}` : `x86_64_${platform}`; diff --git a/lib/aspects/stack-termination-protection.ts b/lib/aspects/stack-termination-protection.ts new file mode 100644 index 00000000..3a30b33e --- /dev/null +++ b/lib/aspects/stack-termination-protection.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +// Finch Infrastructure +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +import { Aspects, IAspect, Stack } from "aws-cdk-lib"; +import { IConstruct } from "constructs"; + +/** + * Enable termination protection in all stacks. + */ +export class EnableTerminationProtectionOnStacks implements IAspect { + visit(construct: IConstruct): void { + if (Stack.isStack(construct)) { + (construct).terminationProtection = true; + } + } +} + +export function applyTerminationProtectionOnStacks(constructs: IConstruct[]) { + constructs.forEach((construct) => { + Aspects.of(construct).add(new EnableTerminationProtectionOnStacks()); + }) +} diff --git a/lib/continuous-integration-stack.ts b/lib/continuous-integration-stack.ts index 4ce3a432..9bdda6ce 100644 --- a/lib/continuous-integration-stack.ts +++ b/lib/continuous-integration-stack.ts @@ -5,6 +5,7 @@ import * as iam from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; import { CloudfrontCdn } from './cloudfront_cdn'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; interface ContinuousIntegrationStackProps extends cdk.StackProps { rootfsEcrRepository: ecr.Repository; @@ -14,6 +15,7 @@ interface ContinuousIntegrationStackProps extends cdk.StackProps { export class ContinuousIntegrationStack extends cdk.Stack { constructor(scope: Construct, id: string, stage: string, props: ContinuousIntegrationStackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); const githubDomain = 'token.actions.githubusercontent.com'; diff --git a/lib/ecr-repo-stack.ts b/lib/ecr-repo-stack.ts index ba67d001..8c5110af 100644 --- a/lib/ecr-repo-stack.ts +++ b/lib/ecr-repo-stack.ts @@ -2,12 +2,15 @@ import * as cdk from 'aws-cdk-lib'; import { CfnOutput } from 'aws-cdk-lib'; import * as ecr from 'aws-cdk-lib/aws-ecr'; import { Construct } from 'constructs'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; export class ECRRepositoryStack extends cdk.Stack { public readonly repositoryOutput: CfnOutput; public readonly repository: ecr.Repository; constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); + const repoName = `finch-rootfs-image-${stage.toLowerCase()}`; const ecrRepository = new ecr.Repository(this, 'finch-rootfs', { repositoryName:repoName, diff --git a/lib/event-bridge-scan-notifs-stack.ts b/lib/event-bridge-scan-notifs-stack.ts index 5c8c8ea7..20e4cc8a 100644 --- a/lib/event-bridge-scan-notifs-stack.ts +++ b/lib/event-bridge-scan-notifs-stack.ts @@ -7,10 +7,12 @@ import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; import * as targets from 'aws-cdk-lib/aws-events-targets'; import { Construct } from 'constructs'; import path from 'path'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; export class EventBridgeScanNotifsStack extends cdk.Stack { constructor(scope: Construct, id: string, stage: string, props?: cdk.StackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); const topic = new sns.Topic(this, 'ECR Image Inspector Findings'); diff --git a/lib/finch-pipeline-app-stage.ts b/lib/finch-pipeline-app-stage.ts index 2bcbf660..038ae84c 100644 --- a/lib/finch-pipeline-app-stage.ts +++ b/lib/finch-pipeline-app-stage.ts @@ -68,10 +68,10 @@ export class FinchPipelineAppStage extends cdk.Stage { this, 'FinchContinuousIntegrationStack', this.stageName, - {rootfsEcrRepository: this.ecrRepository} + { rootfsEcrRepository: this.ecrRepository } ); } - new PVREReportingStack(this, 'PVREReportingStack'); + new PVREReportingStack(this, 'PVREReportingStack', { terminationProtection:true }); } } diff --git a/lib/finch-pipeline-stack.ts b/lib/finch-pipeline-stack.ts index 8538dca5..1728edbc 100644 --- a/lib/finch-pipeline-stack.ts +++ b/lib/finch-pipeline-stack.ts @@ -5,10 +5,13 @@ import { Construct } from 'constructs'; import { EnvConfig } from '../config/env-config'; import { RunnerConfig } from '../config/runner-config'; import { ENVIRONMENT_STAGE, FinchPipelineAppStage } from './finch-pipeline-app-stage'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; +import { release } from 'os'; export class FinchPipelineStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); const source = CodePipelineSource.gitHub('runfinch/infrastructure', 'main', { authentication: cdk.SecretValue.secretsManager('pipeline-github-access-token') diff --git a/lib/pvre-reporting-stack.ts b/lib/pvre-reporting-stack.ts index 63cd84da..499da5c2 100644 --- a/lib/pvre-reporting-stack.ts +++ b/lib/pvre-reporting-stack.ts @@ -1,10 +1,12 @@ import * as cdk from 'aws-cdk-lib'; import * as cfninc from 'aws-cdk-lib/cloudformation-include'; import { Construct } from 'constructs'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; export class PVREReportingStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); + applyTerminationProtectionOnStacks([this]); new cfninc.CfnInclude(this, 'PVREReportingTemplate', { templateFile: 'lib/pvre-reporting-template.yml' diff --git a/test/artifact-bucket-cloudfront.test.ts b/test/artifact-bucket-cloudfront.test.ts index 45e0b32d..427c5e90 100644 --- a/test/artifact-bucket-cloudfront.test.ts +++ b/test/artifact-bucket-cloudfront.test.ts @@ -28,5 +28,7 @@ describe('ArtifactBucketCloudfrontStack', () => { // assert it creates the cloudfront distribution template.resourceCountIs('AWS::CloudFront::Distribution', 1); + + expect(cloudfront.terminationProtection).toBeTruthy(); }); }); diff --git a/test/asg-runner-stack.test.ts b/test/asg-runner-stack.test.ts index a60dc8b9..f839e831 100644 --- a/test/asg-runner-stack.test.ts +++ b/test/asg-runner-stack.test.ts @@ -54,4 +54,10 @@ describe('ASGRunnerStack test', () => { }); }); }); + + it('must have termination protection enabled', () => { + stacks.forEach((stack) => { + expect(stack.terminationProtection).toBeTruthy(); + }) + }); }); diff --git a/test/aspects/stack-termination-protection.test.ts b/test/aspects/stack-termination-protection.test.ts new file mode 100644 index 00000000..def0708d --- /dev/null +++ b/test/aspects/stack-termination-protection.test.ts @@ -0,0 +1,20 @@ +// Finch Infrastructure +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +import { App, Stack } from "aws-cdk-lib"; +import { Bucket } from "aws-cdk-lib/aws-s3"; +import { applyTerminationProtectionOnStacks } from "../../lib/aspects/stack-termination-protection"; + +describe('Stack termination protection aspect', () => { + it('must enable termination protection when applied to a stack and synthesized', () => { + const app = new App(); + const stack = new Stack(app, 'FooStack'); + // stack needs one resource + new Bucket(stack, 'BarBucket'); + + applyTerminationProtectionOnStacks([stack]); + app.synth(); + + expect(stack.terminationProtection).toBeTruthy(); + }); +}); diff --git a/test/ecr-repo-stack.test.ts b/test/ecr-repo-stack.test.ts index e80d9345..7dbeaaa2 100644 --- a/test/ecr-repo-stack.test.ts +++ b/test/ecr-repo-stack.test.ts @@ -1,6 +1,7 @@ import * as cdk from 'aws-cdk-lib'; import { Template, Match } from 'aws-cdk-lib/assertions'; import { ECRRepositoryStack } from '../lib/ecr-repo-stack'; +import { TerminationPolicy } from 'aws-cdk-lib/aws-autoscaling'; describe('ECRRepositoryStack', () => { test('synthesizes the way we expect', () => { @@ -21,5 +22,7 @@ describe('ECRRepositoryStack', () => { }, }, }); + + expect(ecrRepo.terminationProtection).toBeTruthy(); }); }) diff --git a/test/event-bridge-scan-notifs-stack.test.ts b/test/event-bridge-scan-notifs-stack.test.ts index a17e7349..7d926233 100644 --- a/test/event-bridge-scan-notifs-stack.test.ts +++ b/test/event-bridge-scan-notifs-stack.test.ts @@ -45,5 +45,7 @@ describe('EventBridgeScanNotifsStack', () => { ], } }); + + expect(eventBridgeStack.terminationProtection).toBeTruthy(); }); }) diff --git a/test/finch-pipeline-stack.test.ts b/test/finch-pipeline-stack.test.ts index 1f626228..7907bc68 100644 --- a/test/finch-pipeline-stack.test.ts +++ b/test/finch-pipeline-stack.test.ts @@ -57,5 +57,7 @@ describe('FinchPipelineStack', () => { 'Fn::GetAtt': ['FinchPipelineRole198D7E07', 'Arn'] } }); + + expect(stack.terminationProtection).toBeTruthy(); }); });