diff --git a/config/runner-config.ts b/config/runner-config.ts index e38f88f..ef94f7b 100644 --- a/config/runner-config.ts +++ b/config/runner-config.ts @@ -21,7 +21,7 @@ export interface RunnerType { availabilityZones: Array; } -export enum PlatformType { +export const enum PlatformType { WINDOWS = 'windows', MAC = 'mac', AMAZONLINUX = 'amazonlinux', diff --git a/lib/finch-pipeline-app-stage.ts b/lib/finch-pipeline-app-stage.ts index 6f9aa04..cbedb6b 100644 --- a/lib/finch-pipeline-app-stage.ts +++ b/lib/finch-pipeline-app-stage.ts @@ -10,6 +10,7 @@ import { ContinuousIntegrationStack } from './continuous-integration-stack'; import { ECRRepositoryStack } from './ecr-repo-stack'; import { EventBridgeScanNotifsStack } from './event-bridge-scan-notifs-stack'; import { PVREReportingStack } from './pvre-reporting-stack'; +import { SSMPatchingStack } from './ssm-patching-stack'; export enum ENVIRONMENT_STAGE { Beta, @@ -76,5 +77,7 @@ export class FinchPipelineAppStage extends cdk.Stage { } new PVREReportingStack(this, 'PVREReportingStack', { terminationProtection: true }); + + new SSMPatchingStack(this, 'SSMPatchingStack', { terminationProtection: true }); } } diff --git a/lib/ssm-patching-stack.ts b/lib/ssm-patching-stack.ts new file mode 100644 index 0000000..7ec4ec2 --- /dev/null +++ b/lib/ssm-patching-stack.ts @@ -0,0 +1,56 @@ +import * as cdk from 'aws-cdk-lib'; +import { CfnMaintenanceWindow, CfnMaintenanceWindowTarget, CfnMaintenanceWindowTask } from 'aws-cdk-lib/aws-ssm'; +import { Construct } from 'constructs'; +import { applyTerminationProtectionOnStacks } from './aspects/stack-termination-protection'; + +export class SSMPatchingStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + applyTerminationProtectionOnStacks([this]); + + const maintenanceWindow = new CfnMaintenanceWindow(this, 'MaintenanceWindow', { + name: `Patching-Window`, + allowUnassociatedTargets: false, + cutoff: 0, + duration: 2, + // Every Sunday at 3 AM + schedule: 'cron(0 3 ? * SUN *)' + }); + + const maintenanceTarget = new CfnMaintenanceWindowTarget(this, 'MaintenanceWindowTarget', { + name: 'All-Instances-Patch-Target', + windowId: maintenanceWindow.ref, + resourceType: 'INSTANCE', + targets: [ + { + key: 'tag:PVRE-Reporting', + values: ['SSM'] + } + ] + }); + + new CfnMaintenanceWindowTask(this, 'MaintenanceWindowTask', { + taskArn: 'AWS-RunPatchBaseline', + priority: 1, + taskType: 'RUN_COMMAND', + windowId: maintenanceWindow.ref, + name: 'Patch-Task', + targets: [ + { + key: 'WindowTargetIds', + values: [maintenanceTarget.ref] + } + ], + taskInvocationParameters: { + maintenanceWindowRunCommandParameters: { + parameters: { + Operation: ['Install'] + }, + documentVersion: '$LATEST' + } + }, + maxErrors: '0', + maxConcurrency: '1' + }); + } +} diff --git a/test/artifact-bucket-cloudfront.test.ts b/test/artifact-bucket-cloudfront.test.ts index 427c5e9..b048b45 100644 --- a/test/artifact-bucket-cloudfront.test.ts +++ b/test/artifact-bucket-cloudfront.test.ts @@ -1,5 +1,5 @@ import * as cdk from 'aws-cdk-lib'; -import { Template, Match } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; import { ArtifactBucketCloudfrontStack } from '../lib/artifact-bucket-cloudfront'; describe('ArtifactBucketCloudfrontStack', () => { diff --git a/test/cloudfront_cdn.test.ts b/test/cloudfront_cdn.test.ts index ca2bfa3..eedd57a 100644 --- a/test/cloudfront_cdn.test.ts +++ b/test/cloudfront_cdn.test.ts @@ -1,5 +1,5 @@ import * as cdk from 'aws-cdk-lib'; -import { Template, Match } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { CfnBucket } from 'aws-cdk-lib/aws-s3'; import { CloudfrontCdn } from '../lib/cloudfront_cdn'; diff --git a/test/ecr-repo-stack.test.ts b/test/ecr-repo-stack.test.ts index 3c23701..f40c906 100644 --- a/test/ecr-repo-stack.test.ts +++ b/test/ecr-repo-stack.test.ts @@ -1,5 +1,5 @@ import * as cdk from 'aws-cdk-lib'; -import { Template, Match } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; import { ECRRepositoryStack } from '../lib/ecr-repo-stack'; describe('ECRRepositoryStack', () => { diff --git a/test/event-bridge-scan-notifs-stack.test.ts b/test/event-bridge-scan-notifs-stack.test.ts index 7d92623..b1afd99 100644 --- a/test/event-bridge-scan-notifs-stack.test.ts +++ b/test/event-bridge-scan-notifs-stack.test.ts @@ -1,5 +1,5 @@ import * as cdk from 'aws-cdk-lib'; -import { Template, Match } from 'aws-cdk-lib/assertions'; +import { Match, Template } from 'aws-cdk-lib/assertions'; import { EventBridgeScanNotifsStack } from '../lib/event-bridge-scan-notifs-stack'; describe('EventBridgeScanNotifsStack', () => { diff --git a/test/finch-pipeline-stack.test.ts b/test/finch-pipeline-stack.test.ts index 7907bc6..4d5cd4c 100644 --- a/test/finch-pipeline-stack.test.ts +++ b/test/finch-pipeline-stack.test.ts @@ -1,7 +1,7 @@ import * as cdk from 'aws-cdk-lib'; import { Template } from 'aws-cdk-lib/assertions'; -import { FinchPipelineStack } from '../lib/finch-pipeline-stack'; import { EnvConfig } from '../config/env-config'; +import { FinchPipelineStack } from '../lib/finch-pipeline-stack'; describe('FinchPipelineStack', () => { test('synthesizes the way we expect', () => { diff --git a/test/ssm-patching-stack.test.ts b/test/ssm-patching-stack.test.ts new file mode 100644 index 0000000..b105977 --- /dev/null +++ b/test/ssm-patching-stack.test.ts @@ -0,0 +1,72 @@ +import * as cdk from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { SSMPatchingStack } from '../lib/ssm-patching-stack'; + +describe('SSMPatchingStack', () => { + test('synthesizes the way we expect', () => { + const app = new cdk.App(); + const ssmPatchingStack = new SSMPatchingStack(app, 'SSMPatchingStack'); + + const template = Template.fromStack(ssmPatchingStack); + + template.hasResource('AWS::SSM::MaintenanceWindow', { + Properties: { + AllowUnassociatedTargets: false, + Cutoff: 0, + Duration: 2, + Name: 'Patching-Window', + Schedule: 'cron(0 3 ? * SUN *)' + } + }); + + template.hasResource('AWS::SSM::MaintenanceWindowTarget', { + Properties: { + Name: 'All-Instances-Patch-Target', + ResourceType: 'INSTANCE', + Targets: [ + { + Key: 'tag:PVRE-Reporting', + Values: ['SSM'] + } + ], + WindowId: { + Ref: 'MaintenanceWindow' + } + } + }); + + template.hasResource('AWS::SSM::MaintenanceWindowTask', { + Properties: { + MaxConcurrency: '1', + MaxErrors: '0', + Name: 'Patch-Task', + Priority: 1, + Targets: [ + { + Key: 'WindowTargetIds', + Values: [ + { + Ref: 'MaintenanceWindowTarget' + } + ] + } + ], + TaskArn: 'AWS-RunPatchBaseline', + TaskInvocationParameters: { + MaintenanceWindowRunCommandParameters: { + Parameters: { + Operation: ['Install'] + }, + DocumentVersion: '$LATEST' + } + }, + TaskType: 'RUN_COMMAND', + WindowId: { + Ref: 'MaintenanceWindow' + } + } + }); + + expect(ssmPatchingStack.terminationProtection).toBeTruthy(); + }); +});