diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d312a13..0353027a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.1] - 2024-04-10 + +### Changed + +- Changed order of CloudFormation parameters to emphasize the Security Control playbook +- Changed default for all playbooks other than SC to 'no' +- Updated descriptions of playbook parameters +- Updated architecture diagram + ## [2.1.0] - 2024-03-28 ### Added diff --git a/docs/architecture_diagram.png b/docs/architecture_diagram.png index 23c61fab..e96b583f 100644 Binary files a/docs/architecture_diagram.png and b/docs/architecture_diagram.png differ diff --git a/pyproject.toml b/pyproject.toml index 6375ba28..c75106b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "automated_security_response_on_aws" -version = "2.1.0" +version = "2.1.1" [tool.setuptools] package-dir = {"" = "source"} diff --git a/solution-manifest.yaml b/solution-manifest.yaml index 63a713d8..be9b3fb3 100644 --- a/solution-manifest.yaml +++ b/solution-manifest.yaml @@ -1,6 +1,6 @@ id: SO0111 name: security-hub-automated-response-and-remediation -version: 2.1.0 +version: 2.1.1 cloudformation_templates: - template: aws-sharr-deploy.template main_template: true diff --git a/source/lib/__snapshots__/member-stack.test.ts.snap b/source/lib/__snapshots__/member-stack.test.ts.snap index ffb123cc..309525af 100644 --- a/source/lib/__snapshots__/member-stack.test.ts.snap +++ b/source/lib/__snapshots__/member-stack.test.ts.snap @@ -103,7 +103,15 @@ exports[`member stack snapshot matches 1`] = ` }, { "Label": { - "default": "Playbooks", + "default": "Consolidated control finding Playbook", + }, + "Parameters": [ + "LoadSCMemberStack", + ], + }, + { + "Label": { + "default": "Security Standard Playbooks", }, "Parameters": [ "LoadAFSBPMemberStack", @@ -111,7 +119,15 @@ exports[`member stack snapshot matches 1`] = ` "LoadCIS140MemberStack", "LoadNIST80053MemberStack", "LoadPCI321MemberStack", - "LoadSCMemberStack", + ], + }, + { + "Label": { + "default": "Configuration", + }, + "Parameters": [ + "CreateS3BucketForRedshiftAuditLogging", + "SecHubAdminAccount", ], }, ], @@ -137,8 +153,8 @@ exports[`member stack snapshot matches 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load Playbook member stack for AFSBP?", + "Default": "no", + "Description": "Install the member components for automated remediation of AFSBP controls?", "Type": "String", }, "LoadCIS120MemberStack": { @@ -146,8 +162,8 @@ exports[`member stack snapshot matches 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load Playbook member stack for CIS120?", + "Default": "no", + "Description": "Install the member components for automated remediation of CIS120 controls?", "Type": "String", }, "LoadCIS140MemberStack": { @@ -155,8 +171,8 @@ exports[`member stack snapshot matches 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load Playbook member stack for CIS140?", + "Default": "no", + "Description": "Install the member components for automated remediation of CIS140 controls?", "Type": "String", }, "LoadNIST80053MemberStack": { @@ -164,8 +180,8 @@ exports[`member stack snapshot matches 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load Playbook member stack for NIST80053?", + "Default": "no", + "Description": "Install the member components for automated remediation of NIST80053 controls?", "Type": "String", }, "LoadPCI321MemberStack": { @@ -173,8 +189,8 @@ exports[`member stack snapshot matches 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load Playbook member stack for PCI321?", + "Default": "no", + "Description": "Install the member components for automated remediation of PCI321 controls?", "Type": "String", }, "LoadSCMemberStack": { @@ -183,7 +199,7 @@ exports[`member stack snapshot matches 1`] = ` "no", ], "Default": "yes", - "Description": "Load Playbook member stack for SC?", + "Description": "If the consolidated control findings feature is turned on in Security Hub, only enable the Security Control (SC) playbook. If the feature is not turned on, enable the playbooks for the security standards that are enabled in Security Hub. Enabling additional playbooks can result in reaching the quota for EventBridge Rules.", "Type": "String", }, "LogGroupName": { diff --git a/source/lib/admin-account-param.ts b/source/lib/admin-account-param.ts index 392d37d0..7d539aaf 100644 --- a/source/lib/admin-account-param.ts +++ b/source/lib/admin-account-param.ts @@ -4,6 +4,7 @@ import { CfnParameter } from 'aws-cdk-lib'; import { Construct } from 'constructs'; export default class AdminAccountParam extends Construct { + public readonly paramId: string; public readonly value: string; constructor(scope: Construct, id: string) { @@ -16,6 +17,7 @@ export default class AdminAccountParam extends Construct { allowedPattern: accountIdRegex.source, }); param.overrideLogicalId(`SecHubAdminAccount`); + this.paramId = param.logicalId; this.value = param.valueAsString; } diff --git a/source/lib/admin-playbook.ts b/source/lib/admin-playbook.ts new file mode 100644 index 00000000..ad6e1b93 --- /dev/null +++ b/source/lib/admin-playbook.ts @@ -0,0 +1,55 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CfnCondition, CfnParameter, CfnResource, Fn, NestedStack, Stack } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; + +export interface AdminPlaybookProps { + name: string; + stackDependencies?: CfnResource[]; + defaultState?: 'yes' | 'no'; + description?: string; +} + +export class AdminPlaybook { + parameterName = ''; + playbookStack: Stack; + + constructor(scope: Construct, props: AdminPlaybookProps) { + const templateFile = `${props.name}Stack.template`; + const illegalChars = /[\\._]/g; + const playbookName = props.name.replace(illegalChars, ''); + this.parameterName = `Load${playbookName}AdminStack`; + + //--------------------------------------------------------------------- + // Playbook Template Nested Stack + // + const stackOption = new CfnParameter(scope, `LoadAdminStack${playbookName}`, { + type: 'String', + description: + props.description ?? `Install the admin components for automated remediation of ${props.name} controls?`, + default: props.defaultState ?? 'no', + allowedValues: ['yes', 'no'], + }); + stackOption.overrideLogicalId(this.parameterName); + + this.playbookStack = new NestedStack(scope, `PlaybookAdminStack${playbookName}`); + const cfnStack = this.playbookStack.nestedStackResource as CfnResource; + cfnStack.addPropertyOverride( + 'TemplateURL', + 'https://' + + Fn.findInMap('SourceCode', 'General', 'S3Bucket') + + '-reference.s3.amazonaws.com/' + + Fn.findInMap('SourceCode', 'General', 'KeyPrefix') + + '/playbooks/' + + templateFile, + ); + cfnStack.cfnOptions.condition = new CfnCondition(scope, `load${playbookName}Cond`, { + expression: Fn.conditionEquals(stackOption, 'yes'), + }); + props.stackDependencies?.forEach((dependency) => { + cfnStack.node.addDependency(dependency); + }); + cfnStack.overrideLogicalId(`PlaybookAdminStack${props.name}`); + } +} diff --git a/source/lib/member-playbook.ts b/source/lib/member-playbook.ts new file mode 100644 index 00000000..e4283ad2 --- /dev/null +++ b/source/lib/member-playbook.ts @@ -0,0 +1,50 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { CfnCondition, CfnParameter, CfnResource, Fn, Stack } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { SerializedNestedStackFactory } from './cdk-helper/nested-stack'; + +export interface MemberPlaybookProps { + readonly name: string; + readonly nestedStackFactory: SerializedNestedStackFactory; + readonly parameters?: { [_: string]: string }; + readonly stackDependencies?: CfnResource[]; + readonly defaultState?: 'yes' | 'no'; + readonly description?: string; +} + +export class MemberPlaybook { + parameterName = ''; + playbookStack: Stack; + + constructor(scope: Construct, props: MemberPlaybookProps) { + const templateFile = `${props.name}MemberStack.template`; + const illegalChars = /[\\._]/g; + const playbookName = props.name.replace(illegalChars, ''); + this.parameterName = `Load${playbookName}MemberStack`; + + //--------------------------------------------------------------------- + // Playbook Template Nested Stack + // + const stackOption = new CfnParameter(scope, `LoadMemberStack${playbookName}`, { + type: 'String', + description: + props.description ?? `Install the member components for automated remediation of ${props.name} controls?`, + default: props.defaultState ?? 'no', + allowedValues: ['yes', 'no'], + }); + stackOption.overrideLogicalId(this.parameterName); + + this.playbookStack = props.nestedStackFactory.addNestedStack(`PlaybookMemberStack${playbookName}`, { + templateRelativePath: `playbooks/${templateFile}`, + parameters: props.parameters, + condition: new CfnCondition(scope, `load${playbookName}Cond`, { + expression: Fn.conditionEquals(stackOption, 'yes'), + }), + }); + + const cfnStack = this.playbookStack.nestedStackResource as CfnResource; + cfnStack.overrideLogicalId(`PlaybookMemberStack${props.name}`); + } +} diff --git a/source/lib/member-stack.test.ts b/source/lib/member-stack.test.ts index 689accf9..3184d8e6 100644 --- a/source/lib/member-stack.test.ts +++ b/source/lib/member-stack.test.ts @@ -348,7 +348,6 @@ describe('member stack', function () { const expectedTemplateParameterProperties = { AllowedValues: ['yes', 'no'], - Default: 'yes', Type: 'String', }; diff --git a/source/lib/member-stack.ts b/source/lib/member-stack.ts index 26012568..b0df7bdf 100644 --- a/source/lib/member-stack.ts +++ b/source/lib/member-stack.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { readdirSync } from 'fs'; -import { StackProps, Stack, App, CfnParameter, CfnCondition, Fn, CfnResource } from 'aws-cdk-lib'; +import { StackProps, Stack, App, CfnResource } from 'aws-cdk-lib'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import AdminAccountParam from './admin-account-param'; import { RedshiftAuditLogging } from './member/redshift-audit-logging'; @@ -11,6 +11,7 @@ import { MemberBucketEncryption } from './member/bucket-encryption'; import { MemberVersion } from './member/version'; import { SerializedNestedStackFactory } from './cdk-helper/nested-stack'; import { WaitProvider } from './wait-provider'; +import { MemberPlaybook } from './member-playbook'; export interface SolutionProps extends StackProps { solutionId: string; @@ -27,7 +28,7 @@ export class MemberStack extends Stack { const adminAccountParam = new AdminAccountParam(this, 'AdminAccountParameter'); - new RedshiftAuditLogging(this, 'RedshiftAuditLogging', { solutionId: props.solutionId }); + const redShiftLogging = new RedshiftAuditLogging(this, 'RedshiftAuditLogging', { solutionId: props.solutionId }); new MemberRemediationKey(this, 'MemberKey', { solutionId: props.solutionId }); @@ -60,40 +61,40 @@ export class MemberStack extends Stack { this.nestedStacks.push(nestedStackNoRoles as Stack); const playbookDirectory = `${__dirname}/../playbooks`; - const ignore = ['.DS_Store', 'common', '.pytest_cache', 'NEWPLAYBOOK', '.coverage']; - const illegalChars = /[\\._]/g; + const ignore = ['.DS_Store', 'common', '.pytest_cache', 'NEWPLAYBOOK', '.coverage', 'SC']; const listOfPlaybooks: string[] = []; const items = readdirSync(playbookDirectory); items.forEach((file) => { if (!ignore.includes(file)) { - const templateFile = `${file}MemberStack.template`; - - const parmname = file.replace(illegalChars, ''); - const memberStackOption = new CfnParameter(this, `LoadMemberStack${parmname}`, { - type: 'String', - description: `Load Playbook member stack for ${file}?`, - default: 'yes', - allowedValues: ['yes', 'no'], - }); - memberStackOption.overrideLogicalId(`Load${parmname}MemberStack`); - listOfPlaybooks.push(memberStackOption.logicalId); - - const nestedStack = nestedStackFactory.addNestedStack(`PlaybookMemberStack${file}`, { - templateRelativePath: `playbooks/${templateFile}`, + const playbook = new MemberPlaybook(this, { + name: file, + defaultState: 'no', + nestedStackFactory, parameters: { SecHubAdminAccount: adminAccountParam.value, WaitProviderServiceToken: waitProvider.serviceToken, }, - condition: new CfnCondition(this, `load${file}Cond`, { - expression: Fn.conditionEquals(memberStackOption, 'yes'), - }), }); - const cfnResource = nestedStack.nestedStackResource as CfnResource; - cfnResource.overrideLogicalId(`PlaybookMemberStack${file}`); - this.nestedStacks.push(nestedStack as Stack); + + listOfPlaybooks.push(playbook.parameterName); + this.nestedStacks.push(playbook.playbookStack); } }); + const scPlaybook = new MemberPlaybook(this, { + name: 'SC', + defaultState: 'yes', + description: + 'If the consolidated control findings feature is turned on in Security Hub, only enable the Security Control (SC) playbook. If the feature is not turned on, enable the playbooks for the security standards that are enabled in Security Hub. Enabling additional playbooks can result in reaching the quota for EventBridge Rules.', + nestedStackFactory, + parameters: { + SecHubAdminAccount: adminAccountParam.value, + WaitProviderServiceToken: waitProvider.serviceToken, + }, + }); + + this.nestedStacks.push(scPlaybook.playbookStack); + /******************** ** Metadata ********************/ @@ -105,9 +106,17 @@ export class MemberStack extends Stack { Parameters: [memberLogGroup.paramId], }, { - Label: { default: 'Playbooks' }, + Label: { default: 'Consolidated control finding Playbook' }, + Parameters: [scPlaybook.parameterName], + }, + { + Label: { default: 'Security Standard Playbooks' }, Parameters: listOfPlaybooks, }, + { + Label: { default: 'Configuration' }, + Parameters: [redShiftLogging.paramId, adminAccountParam.paramId], + }, ], ParameterLabels: { [memberLogGroup.paramId]: { diff --git a/source/lib/member/redshift-audit-logging.ts b/source/lib/member/redshift-audit-logging.ts index 84a3c990..7d50b6a0 100644 --- a/source/lib/member/redshift-audit-logging.ts +++ b/source/lib/member/redshift-audit-logging.ts @@ -15,6 +15,8 @@ export interface RedshiftAuditLoggingProps { } export class RedshiftAuditLogging extends Construct { + public readonly paramId: string; + constructor(scope: Construct, id: string, props: RedshiftAuditLoggingProps) { super(scope, id); @@ -25,6 +27,7 @@ export class RedshiftAuditLogging extends Construct { allowedValues: [ChoiceParam.Yes, ChoiceParam.No], description: 'Create S3 Bucket For Redshift Cluster Audit Logging.', }); + this.paramId = templateParam.logicalId; const condition = new CfnCondition(scope, 'EnableS3BucketForRedShift4', { expression: Fn.conditionEquals(templateParam.valueAsString, ChoiceParam.Yes), diff --git a/source/lib/solution_deploy-stack.ts b/source/lib/solution_deploy-stack.ts index 4d2b49c9..bb86565a 100644 --- a/source/lib/solution_deploy-stack.ts +++ b/source/lib/solution_deploy-stack.ts @@ -25,6 +25,7 @@ import { OrchestratorConstruct } from './common-orchestrator-construct'; import { CfnStateMachine, StateMachine } from 'aws-cdk-lib/aws-stepfunctions'; import { OneTrigger } from './ssmplaybook'; import { CloudWatchMetrics } from './cloudwatch_metrics'; +import { AdminPlaybook } from './admin-playbook'; export interface SHARRStackProps extends cdk.StackProps { solutionId: string; @@ -736,49 +737,40 @@ export class SolutionDeployStack extends cdk.Stack { // Loop through all of the Playbooks and create an option to load each // const PB_DIR = `${__dirname}/../playbooks`; - const ignore = ['.DS_Store', 'common', 'python_lib', 'python_tests', '.pytest_cache', 'NEWPLAYBOOK', '.coverage']; - const illegalChars = /[\\._]/g; + const ignore = [ + '.DS_Store', + 'common', + 'python_lib', + 'python_tests', + '.pytest_cache', + 'NEWPLAYBOOK', + '.coverage', + 'SC', + ]; const standardLogicalNames: string[] = []; const items = fs.readdirSync(PB_DIR); items.forEach((file) => { if (!ignore.includes(file)) { - const template_file = `${file}Stack.template`; - - //--------------------------------------------------------------------- - // Playbook Admin Template Nested Stack - // - const parmname = file.replace(illegalChars, ''); - const adminStackOption = new cdk.CfnParameter(this, `LoadAdminStack${parmname}`, { - type: 'String', - description: `Load CloudWatch Event Rules for ${file}?`, - default: 'yes', - allowedValues: ['yes', 'no'], + const playbook = new AdminPlaybook(this, { + name: file, + stackDependencies: [stateMachineConstruct, orchestratorArn], + defaultState: 'no', }); - adminStackOption.overrideLogicalId(`Load${parmname}AdminStack`); - standardLogicalNames.push(`Load${parmname}AdminStack`); - - const adminStack = new cdk.NestedStack(this, `PlaybookAdminStack${file}`); - const cfnStack = adminStack.nestedStackResource as cdk.CfnResource; - cfnStack.addPropertyOverride( - 'TemplateURL', - 'https://' + - cdk.Fn.findInMap('SourceCode', 'General', 'S3Bucket') + - '-reference.s3.amazonaws.com/' + - cdk.Fn.findInMap('SourceCode', 'General', 'KeyPrefix') + - '/playbooks/' + - template_file, - ); - cfnStack.cfnOptions.condition = new cdk.CfnCondition(this, `load${file}Cond`, { - expression: cdk.Fn.conditionEquals(adminStackOption, 'yes'), - }); - cfnStack.node.addDependency(stateMachineConstruct); - cfnStack.node.addDependency(orchestratorArn); - cfnStack.overrideLogicalId(`PlaybookAdminStack${file}`); - this.nestedStacks.push(adminStack as cdk.Stack); + standardLogicalNames.push(playbook.parameterName); + this.nestedStacks.push(playbook.playbookStack); } }); + const scPlaybook = new AdminPlaybook(this, { + name: 'SC', + stackDependencies: [stateMachineConstruct, orchestratorArn], + defaultState: 'yes', + description: + 'If the consolidated control findings feature is turned on in Security Hub, only enable the Security Control (SC) playbook. If the feature is not turned on, enable the playbooks for the security standards that are enabled in Security Hub. Enabling additional playbooks can result in reaching the quota for EventBridge Rules.', + }); + this.nestedStacks.push(scPlaybook.playbookStack); + //--------------------------------------------------------------------- // Scheduling Table for SQS Remediation Throttling // @@ -916,6 +908,10 @@ export class SolutionDeployStack extends cdk.Stack { stack.templateOptions.metadata = { 'AWS::CloudFormation::Interface': { ParameterGroups: [ + { + Label: { default: 'Consolidated Control Findings Playbook' }, + Parameters: [scPlaybook.parameterName], + }, { Label: { default: 'Security Standard Playbooks' }, Parameters: standardLogicalNames, diff --git a/source/package.json b/source/package.json index 3b2cf64a..4f2ec479 100644 --- a/source/package.json +++ b/source/package.json @@ -1,6 +1,6 @@ { "name": "aws-security-hub-automated-response-and-remediation", - "version": "2.1.0", + "version": "2.1.1", "description": "Automated remediation for AWS Security Hub (SO0111)", "bin": { "solution_deploy": "bin/solution_deploy.js" diff --git a/source/test/__snapshots__/solution_deploy.test.ts.snap b/source/test/__snapshots__/solution_deploy.test.ts.snap index 7ee2b6e8..9ad52b9f 100644 --- a/source/test/__snapshots__/solution_deploy.test.ts.snap +++ b/source/test/__snapshots__/solution_deploy.test.ts.snap @@ -172,6 +172,14 @@ exports[`Test if the Stack has all the resources. 1`] = ` "Metadata": { "AWS::CloudFormation::Interface": { "ParameterGroups": [ + { + "Label": { + "default": "Consolidated Control Findings Playbook", + }, + "Parameters": [ + "LoadSCAdminStack", + ], + }, { "Label": { "default": "Security Standard Playbooks", @@ -182,7 +190,6 @@ exports[`Test if the Stack has all the resources. 1`] = ` "LoadCIS140AdminStack", "LoadNIST80053AdminStack", "LoadPCI321AdminStack", - "LoadSCAdminStack", ], }, { @@ -223,8 +230,8 @@ exports[`Test if the Stack has all the resources. 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load CloudWatch Event Rules for AFSBP?", + "Default": "no", + "Description": "Install the admin components for automated remediation of AFSBP controls?", "Type": "String", }, "LoadCIS120AdminStack": { @@ -232,8 +239,8 @@ exports[`Test if the Stack has all the resources. 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load CloudWatch Event Rules for CIS120?", + "Default": "no", + "Description": "Install the admin components for automated remediation of CIS120 controls?", "Type": "String", }, "LoadCIS140AdminStack": { @@ -241,8 +248,8 @@ exports[`Test if the Stack has all the resources. 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load CloudWatch Event Rules for CIS140?", + "Default": "no", + "Description": "Install the admin components for automated remediation of CIS140 controls?", "Type": "String", }, "LoadNIST80053AdminStack": { @@ -250,8 +257,8 @@ exports[`Test if the Stack has all the resources. 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load CloudWatch Event Rules for NIST80053?", + "Default": "no", + "Description": "Install the admin components for automated remediation of NIST80053 controls?", "Type": "String", }, "LoadPCI321AdminStack": { @@ -259,8 +266,8 @@ exports[`Test if the Stack has all the resources. 1`] = ` "yes", "no", ], - "Default": "yes", - "Description": "Load CloudWatch Event Rules for PCI321?", + "Default": "no", + "Description": "Install the admin components for automated remediation of PCI321 controls?", "Type": "String", }, "LoadSCAdminStack": { @@ -269,7 +276,7 @@ exports[`Test if the Stack has all the resources. 1`] = ` "no", ], "Default": "yes", - "Description": "Load CloudWatch Event Rules for SC?", + "Description": "If the consolidated control findings feature is turned on in Security Hub, only enable the Security Control (SC) playbook. If the feature is not turned on, enable the playbooks for the security standards that are enabled in Security Hub. Enabling additional playbooks can result in reaching the quota for EventBridge Rules.", "Type": "String", }, "ReuseOrchestratorLogGroup": {