From 4a31549430715fcbd3688de39c5b532b4e16d953 Mon Sep 17 00:00:00 2001 From: Max Granat Date: Thu, 16 Mar 2023 16:57:48 -0400 Subject: [PATCH 1/2] Add tests for ssm doc rate limit aspect --- .../ssm-doc-rate-limit.test.ts.snap | 248 ++++++++++++++++++ source/lib/ssm-doc-rate-limit.test.ts | 93 +++++++ 2 files changed, 341 insertions(+) create mode 100644 source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap create mode 100644 source/lib/ssm-doc-rate-limit.test.ts diff --git a/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap b/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap new file mode 100644 index 00000000..9c370f31 --- /dev/null +++ b/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap @@ -0,0 +1,248 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SSM doc rate limit aspect configures dependencies for many documents 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "CreateWait0": { + "DeletionPolicy": "Delete", + "Properties": { + "CreateIntervalSeconds": 1, + "DeleteIntervalSeconds": 0, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 1, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "CreateWait1": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "CreateIntervalSeconds": 1, + "DeleteIntervalSeconds": 0, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 1, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "CreateWait2": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "CreateIntervalSeconds": 1, + "DeleteIntervalSeconds": 0, + "DocumentPropertiesHash": "3758b126bf0cb781dcfd65289a956ffe11d35b5948f020cef3efad3937173f1d", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 1, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "DeletWait0": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "Document0", + "Document1", + "Document2", + "Document3", + "Document4", + ], + "Properties": { + "CreateIntervalSeconds": 0, + "DeleteIntervalSeconds": 0.5, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 0, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "DeletWait1": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "DeletWait0", + "Document5", + "Document6", + "Document7", + "Document8", + "Document9", + ], + "Properties": { + "CreateIntervalSeconds": 0, + "DeleteIntervalSeconds": 0.5, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 0, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "DeletWait2": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "DeletWait1", + "Document10", + "Document11", + ], + "Properties": { + "CreateIntervalSeconds": 0, + "DeleteIntervalSeconds": 0.5, + "DocumentPropertiesHash": "3758b126bf0cb781dcfd65289a956ffe11d35b5948f020cef3efad3937173f1d", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 0, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "Document0": { + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document1": { + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document10": { + "DependsOn": [ + "CreateWait2", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document11": { + "DependsOn": [ + "CreateWait2", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document2": { + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document3": { + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document4": { + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document5": { + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document6": { + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document7": { + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document8": { + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document9": { + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; diff --git a/source/lib/ssm-doc-rate-limit.test.ts b/source/lib/ssm-doc-rate-limit.test.ts new file mode 100644 index 00000000..9d406c57 --- /dev/null +++ b/source/lib/ssm-doc-rate-limit.test.ts @@ -0,0 +1,93 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { CfnDocument } from 'aws-cdk-lib/aws-ssm'; +import { Aspects, Stack } from 'aws-cdk-lib'; +import SsmDocRateLimit from './ssm-doc-rate-limit'; +import { WaitProvider } from './wait-provider'; +import { Template } from 'aws-cdk-lib/assertions'; + +describe('SSM doc rate limit aspect', function () { + it('configures dependencies for single document', function () { + const stack = new Stack(); + const serviceToken = 'my-token'; + const waitProvider = WaitProvider.fromServiceToken(stack, 'WaitProvider', serviceToken); + Aspects.of(stack).add(new SsmDocRateLimit(waitProvider)); + const content = {}; + new CfnDocument(stack, 'Document', { content }); + const template = Template.fromStack(stack); + + const documents = template.findResources('AWS::SSM::Document', { Properties: { Content: content } }); + const documentLogicalIds = Object.getOwnPropertyNames(documents); + expect(documentLogicalIds).toHaveLength(1); + const documentLogicalId = documentLogicalIds[0]; + const document = documents[documentLogicalId]; + + const createWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 1, + UpdateIntervalSeconds: 1, + DeleteIntervalSeconds: 0, + ServiceToken: serviceToken, + }, + }); + const createWaitLogicalIds = Object.getOwnPropertyNames(createWaits); + expect(createWaitLogicalIds).toHaveLength(1); + const createWaitLogicalId = createWaitLogicalIds[0]; + expect(document.DependsOn).toEqual(expect.arrayContaining([createWaitLogicalId])); + + const deleteWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 0, + UpdateIntervalSeconds: 0, + DeleteIntervalSeconds: 0.5, + ServiceToken: serviceToken, + }, + }); + const deleteWaitLogicalIds = Object.getOwnPropertyNames(deleteWaits); + expect(deleteWaitLogicalIds).toHaveLength(1); + const deleteWait = deleteWaits[deleteWaitLogicalIds[0]]; + expect(deleteWait.DependsOn).toEqual(expect.arrayContaining([documentLogicalId])); + }); + + it('configures dependencies for many documents', function () { + const stack = new Stack(); + const serviceToken = 'my-token'; + const waitProvider = WaitProvider.fromServiceToken(stack, 'WaitProvider', serviceToken); + Aspects.of(stack).add(new SsmDocRateLimit(waitProvider)); + const content = {}; + const numDocuments = 12; + for (let i = 0; i < numDocuments; ++i) { + new CfnDocument(stack, `Document${i}`, { content }); + } + const template = Template.fromStack(stack); + expect(template).toMatchSnapshot(); + + const documents = template.findResources('AWS::SSM::Document', { Properties: { Content: content } }); + const documentLogicalIds = Object.getOwnPropertyNames(documents); + expect(documentLogicalIds).toHaveLength(numDocuments); + + const expectedBatchSize = 5; + + const createWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 1, + UpdateIntervalSeconds: 1, + DeleteIntervalSeconds: 0, + ServiceToken: serviceToken, + }, + }); + const createWaitLogicalIds = Object.getOwnPropertyNames(createWaits); + expect(createWaitLogicalIds).toHaveLength(Math.ceil(numDocuments / expectedBatchSize)); + + const deleteWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 0, + UpdateIntervalSeconds: 0, + DeleteIntervalSeconds: 0.5, + ServiceToken: serviceToken, + }, + }); + const deleteWaitLogicalIds = Object.getOwnPropertyNames(deleteWaits); + expect(deleteWaitLogicalIds).toHaveLength(Math.ceil(numDocuments / expectedBatchSize)); + }); +}); From 877334f89d11f603e88850f61e09e491dc073603 Mon Sep 17 00:00:00 2001 From: Max Granat Date: Wed, 22 Mar 2023 12:26:57 -0400 Subject: [PATCH 2/2] Add conditional behavior, add tests for conditional behavior --- .../ssm-doc-rate-limit.test.ts.snap | 509 +++++++++++++++++- source/lib/ssm-doc-rate-limit.test.ts | 277 +++++++--- source/lib/ssm-doc-rate-limit.ts | 25 +- .../__snapshots__/afsbp_stack.test.ts.snap | 36 +- .../test/__snapshots__/cis_stack.test.ts.snap | 36 +- .../test/__snapshots__/cis_stack.test.ts.snap | 36 +- .../newplaybook_stack.test.ts.snap | 16 +- .../__snapshots__/pci321_stack.test.ts.snap | 36 +- .../security_controls_stack.test.ts.snap | 504 +++++++++++++++-- 9 files changed, 1340 insertions(+), 135 deletions(-) diff --git a/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap b/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap index 9c370f31..93cae0c8 100644 --- a/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap +++ b/source/lib/__snapshots__/ssm-doc-rate-limit.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`SSM doc rate limit aspect configures dependencies for many documents 1`] = ` +exports[`SSM doc rate limit aspect matches snapshot 1`] = ` { "Parameters": { "BootstrapVersion": { @@ -246,3 +246,510 @@ exports[`SSM doc rate limit aspect configures dependencies for many documents 1` }, } `; + +exports[`SSM doc rate limit aspect with conditional documents matches snapshot 1`] = ` +{ + "Conditions": { + "Condition0": { + "Fn::Equals": [ + { + "Ref": "Parameter0", + }, + "asdf", + ], + }, + "Condition1": { + "Fn::Equals": [ + { + "Ref": "Parameter1", + }, + "asdf", + ], + }, + "Condition10": { + "Fn::Equals": [ + { + "Ref": "Parameter10", + }, + "asdf", + ], + }, + "Condition11": { + "Fn::Equals": [ + { + "Ref": "Parameter11", + }, + "asdf", + ], + }, + "Condition2": { + "Fn::Equals": [ + { + "Ref": "Parameter2", + }, + "asdf", + ], + }, + "Condition3": { + "Fn::Equals": [ + { + "Ref": "Parameter3", + }, + "asdf", + ], + }, + "Condition4": { + "Fn::Equals": [ + { + "Ref": "Parameter4", + }, + "asdf", + ], + }, + "Condition5": { + "Fn::Equals": [ + { + "Ref": "Parameter5", + }, + "asdf", + ], + }, + "Condition6": { + "Fn::Equals": [ + { + "Ref": "Parameter6", + }, + "asdf", + ], + }, + "Condition7": { + "Fn::Equals": [ + { + "Ref": "Parameter7", + }, + "asdf", + ], + }, + "Condition8": { + "Fn::Equals": [ + { + "Ref": "Parameter8", + }, + "asdf", + ], + }, + "Condition9": { + "Fn::Equals": [ + { + "Ref": "Parameter9", + }, + "asdf", + ], + }, + }, + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + "Parameter0": { + "Type": "String", + }, + "Parameter1": { + "Type": "String", + }, + "Parameter10": { + "Type": "String", + }, + "Parameter11": { + "Type": "String", + }, + "Parameter2": { + "Type": "String", + }, + "Parameter3": { + "Type": "String", + }, + "Parameter4": { + "Type": "String", + }, + "Parameter5": { + "Type": "String", + }, + "Parameter6": { + "Type": "String", + }, + "Parameter7": { + "Type": "String", + }, + "Parameter8": { + "Type": "String", + }, + "Parameter9": { + "Type": "String", + }, + }, + "Resources": { + "CreateWait0": { + "DeletionPolicy": "Delete", + "Properties": { + "CreateIntervalSeconds": 1, + "DeleteIntervalSeconds": 0, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 1, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "CreateWait1": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "CreateIntervalSeconds": 1, + "DeleteIntervalSeconds": 0, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 1, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "CreateWait2": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "CreateIntervalSeconds": 1, + "DeleteIntervalSeconds": 0, + "DocumentPropertiesHash": "3758b126bf0cb781dcfd65289a956ffe11d35b5948f020cef3efad3937173f1d", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 1, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "DeletWait0": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "Gate0", + ], + "Properties": { + "CreateIntervalSeconds": 0, + "DeleteIntervalSeconds": 0.5, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 0, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "DeletWait1": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "DeletWait0", + "Gate1", + ], + "Properties": { + "CreateIntervalSeconds": 0, + "DeleteIntervalSeconds": 0.5, + "DocumentPropertiesHash": "97f8546cc30d8ca122f75268f86704de82b2760257dd324937478bfa046400cf", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 0, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "DeletWait2": { + "DeletionPolicy": "Delete", + "DependsOn": [ + "DeletWait1", + "Gate2", + ], + "Properties": { + "CreateIntervalSeconds": 0, + "DeleteIntervalSeconds": 0.5, + "DocumentPropertiesHash": "3758b126bf0cb781dcfd65289a956ffe11d35b5948f020cef3efad3937173f1d", + "ServiceToken": "my-token", + "UpdateIntervalSeconds": 0, + }, + "Type": "Custom::Wait", + "UpdateReplacePolicy": "Delete", + }, + "Document0": { + "Condition": "Condition0", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document1": { + "Condition": "Condition1", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document10": { + "Condition": "Condition10", + "DependsOn": [ + "CreateWait2", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document11": { + "Condition": "Condition11", + "DependsOn": [ + "CreateWait2", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document2": { + "Condition": "Condition2", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document3": { + "Condition": "Condition3", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document4": { + "Condition": "Condition4", + "DependsOn": [ + "CreateWait0", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document5": { + "Condition": "Condition5", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document6": { + "Condition": "Condition6", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document7": { + "Condition": "Condition7", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document8": { + "Condition": "Condition8", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Document9": { + "Condition": "Condition9", + "DependsOn": [ + "CreateWait1", + ], + "Properties": { + "Content": {}, + }, + "Type": "AWS::SSM::Document", + }, + "Gate0": { + "Metadata": { + "Document0Ready": { + "Fn::If": [ + "Condition0", + { + "Ref": "Document0", + }, + "", + ], + }, + "Document1Ready": { + "Fn::If": [ + "Condition1", + { + "Ref": "Document1", + }, + "", + ], + }, + "Document2Ready": { + "Fn::If": [ + "Condition2", + { + "Ref": "Document2", + }, + "", + ], + }, + "Document3Ready": { + "Fn::If": [ + "Condition3", + { + "Ref": "Document3", + }, + "", + ], + }, + "Document4Ready": { + "Fn::If": [ + "Condition4", + { + "Ref": "Document4", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate1": { + "Metadata": { + "Document5Ready": { + "Fn::If": [ + "Condition5", + { + "Ref": "Document5", + }, + "", + ], + }, + "Document6Ready": { + "Fn::If": [ + "Condition6", + { + "Ref": "Document6", + }, + "", + ], + }, + "Document7Ready": { + "Fn::If": [ + "Condition7", + { + "Ref": "Document7", + }, + "", + ], + }, + "Document8Ready": { + "Fn::If": [ + "Condition8", + { + "Ref": "Document8", + }, + "", + ], + }, + "Document9Ready": { + "Fn::If": [ + "Condition9", + { + "Ref": "Document9", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate2": { + "Metadata": { + "Document10Ready": { + "Fn::If": [ + "Condition10", + { + "Ref": "Document10", + }, + "", + ], + }, + "Document11Ready": { + "Fn::If": [ + "Condition11", + { + "Ref": "Document11", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; diff --git a/source/lib/ssm-doc-rate-limit.test.ts b/source/lib/ssm-doc-rate-limit.test.ts index 9d406c57..45e6f3bd 100644 --- a/source/lib/ssm-doc-rate-limit.test.ts +++ b/source/lib/ssm-doc-rate-limit.test.ts @@ -1,93 +1,222 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { CfnDocument } from 'aws-cdk-lib/aws-ssm'; -import { Aspects, Stack } from 'aws-cdk-lib'; +import { Aspects, CfnCondition, CfnParameter, Fn, Stack } from 'aws-cdk-lib'; import SsmDocRateLimit from './ssm-doc-rate-limit'; import { WaitProvider } from './wait-provider'; import { Template } from 'aws-cdk-lib/assertions'; describe('SSM doc rate limit aspect', function () { - it('configures dependencies for single document', function () { - const stack = new Stack(); - const serviceToken = 'my-token'; - const waitProvider = WaitProvider.fromServiceToken(stack, 'WaitProvider', serviceToken); - Aspects.of(stack).add(new SsmDocRateLimit(waitProvider)); - const content = {}; - new CfnDocument(stack, 'Document', { content }); - const template = Template.fromStack(stack); - - const documents = template.findResources('AWS::SSM::Document', { Properties: { Content: content } }); - const documentLogicalIds = Object.getOwnPropertyNames(documents); - expect(documentLogicalIds).toHaveLength(1); - const documentLogicalId = documentLogicalIds[0]; - const document = documents[documentLogicalId]; - - const createWaits = template.findResources('Custom::Wait', { - Properties: { - CreateIntervalSeconds: 1, - UpdateIntervalSeconds: 1, - DeleteIntervalSeconds: 0, - ServiceToken: serviceToken, - }, + const stack = new Stack(); + const serviceToken = 'my-token'; + const waitProvider = WaitProvider.fromServiceToken(stack, 'WaitProvider', serviceToken); + Aspects.of(stack).add(new SsmDocRateLimit(waitProvider)); + const content = {}; + const numDocuments = 12; + for (let i = 0; i < numDocuments; ++i) { + new CfnDocument(stack, `Document${i}`, { content }); + } + const template = Template.fromStack(stack); + + it('matches snapshot', function () { + expect(template).toMatchSnapshot(); + }); + + const documents = template.findResources('AWS::SSM::Document', { Properties: { Content: content } }); + const documentLogicalIds = Object.getOwnPropertyNames(documents); + + const expectedBatchSize = 5; + + const createWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 1, + UpdateIntervalSeconds: 1, + DeleteIntervalSeconds: 0, + ServiceToken: serviceToken, + }, + }); + const createWaitLogicalIds = Object.getOwnPropertyNames(createWaits); + + it('has the correct number of create resources', function () { + expect(createWaitLogicalIds).toHaveLength(Math.ceil(numDocuments / expectedBatchSize)); + }); + + const deleteWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 0, + UpdateIntervalSeconds: 0, + DeleteIntervalSeconds: 0.5, + ServiceToken: serviceToken, + }, + }); + const deleteWaitLogicalIds = Object.getOwnPropertyNames(deleteWaits); + + it('has the correct number of delete resources', function () { + expect(deleteWaitLogicalIds).toHaveLength(Math.ceil(numDocuments / expectedBatchSize)); + }); + + it('create resources form dependency chain', function () { + expect(formDependencyChain(createWaits)).toStrictEqual(true); + }); + + it('delete resources form dependency chain', function () { + expect(formDependencyChain(deleteWaits)).toStrictEqual(true); + }); + + it('documents depend on create and delete depends on documents', function () { + const documentSets: string[][] = []; + deleteWaitLogicalIds.forEach(function (logicalId: string) { + documentSets.push( + (deleteWaits[logicalId].DependsOn as Array).filter(function (value: string) { + return documentLogicalIds.includes(value); + }) + ); }); - const createWaitLogicalIds = Object.getOwnPropertyNames(createWaits); - expect(createWaitLogicalIds).toHaveLength(1); - const createWaitLogicalId = createWaitLogicalIds[0]; - expect(document.DependsOn).toEqual(expect.arrayContaining([createWaitLogicalId])); - - const deleteWaits = template.findResources('Custom::Wait', { - Properties: { - CreateIntervalSeconds: 0, - UpdateIntervalSeconds: 0, - DeleteIntervalSeconds: 0.5, - ServiceToken: serviceToken, - }, + const remainingDocuments = { ...documents }; + documentSets.forEach(function (documentSet: string[]) { + // all documents depend on the same create resource + const expectedCreateResource = documents[documentSet[0]].DependsOn[0]; + documentSet.forEach(function (value: string) { + delete remainingDocuments[value]; + expect(documents[value].DependsOn).toHaveLength(1); + expect(documents[value].DependsOn[0]).toStrictEqual(expectedCreateResource); + }); }); - const deleteWaitLogicalIds = Object.getOwnPropertyNames(deleteWaits); - expect(deleteWaitLogicalIds).toHaveLength(1); - const deleteWait = deleteWaits[deleteWaitLogicalIds[0]]; - expect(deleteWait.DependsOn).toEqual(expect.arrayContaining([documentLogicalId])); - }); - - it('configures dependencies for many documents', function () { - const stack = new Stack(); - const serviceToken = 'my-token'; - const waitProvider = WaitProvider.fromServiceToken(stack, 'WaitProvider', serviceToken); - Aspects.of(stack).add(new SsmDocRateLimit(waitProvider)); - const content = {}; - const numDocuments = 12; - for (let i = 0; i < numDocuments; ++i) { - new CfnDocument(stack, `Document${i}`, { content }); - } - const template = Template.fromStack(stack); + // all documents in a set + expect(Object.getOwnPropertyNames(remainingDocuments)).toHaveLength(0); + }); +}); + +describe('SSM doc rate limit aspect with conditional documents', function () { + const stack = new Stack(); + const serviceToken = 'my-token'; + const waitProvider = WaitProvider.fromServiceToken(stack, 'WaitProvider', serviceToken); + Aspects.of(stack).add(new SsmDocRateLimit(waitProvider)); + const content = {}; + const numDocuments = 12; + for (let i = 0; i < numDocuments; ++i) { + const param = new CfnParameter(stack, `Parameter${i}`); + const condition = new CfnCondition(stack, `Condition${i}`, { expression: Fn.conditionEquals(param, 'asdf') }); + const doc = new CfnDocument(stack, `Document${i}`, { content }); + doc.cfnOptions.condition = condition; + } + const template = Template.fromStack(stack); + + it('matches snapshot', function () { expect(template).toMatchSnapshot(); + }); - const documents = template.findResources('AWS::SSM::Document', { Properties: { Content: content } }); - const documentLogicalIds = Object.getOwnPropertyNames(documents); - expect(documentLogicalIds).toHaveLength(numDocuments); + const documents = template.findResources('AWS::SSM::Document', { Properties: { Content: content } }); + const documentLogicalIds = Object.getOwnPropertyNames(documents); - const expectedBatchSize = 5; + const expectedBatchSize = 5; - const createWaits = template.findResources('Custom::Wait', { - Properties: { - CreateIntervalSeconds: 1, - UpdateIntervalSeconds: 1, - DeleteIntervalSeconds: 0, - ServiceToken: serviceToken, - }, - }); - const createWaitLogicalIds = Object.getOwnPropertyNames(createWaits); + const createWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 1, + UpdateIntervalSeconds: 1, + DeleteIntervalSeconds: 0, + ServiceToken: serviceToken, + }, + }); + const createWaitLogicalIds = Object.getOwnPropertyNames(createWaits); + + it('has the correct number of create resources', function () { expect(createWaitLogicalIds).toHaveLength(Math.ceil(numDocuments / expectedBatchSize)); + }); - const deleteWaits = template.findResources('Custom::Wait', { - Properties: { - CreateIntervalSeconds: 0, - UpdateIntervalSeconds: 0, - DeleteIntervalSeconds: 0.5, - ServiceToken: serviceToken, - }, - }); - const deleteWaitLogicalIds = Object.getOwnPropertyNames(deleteWaits); + const deleteWaits = template.findResources('Custom::Wait', { + Properties: { + CreateIntervalSeconds: 0, + UpdateIntervalSeconds: 0, + DeleteIntervalSeconds: 0.5, + ServiceToken: serviceToken, + }, + }); + const deleteWaitLogicalIds = Object.getOwnPropertyNames(deleteWaits); + + it('has the correct number of delete resources', function () { expect(deleteWaitLogicalIds).toHaveLength(Math.ceil(numDocuments / expectedBatchSize)); }); + + it('create resources form dependency chain', function () { + expect(formDependencyChain(createWaits)).toStrictEqual(true); + }); + + it('delete resources form dependency chain', function () { + expect(formDependencyChain(deleteWaits)).toStrictEqual(true); + }); + + const dummyResources = template.findResources('AWS::CloudFormation::WaitConditionHandle'); + const dummyResourceLogicalIds = Object.getOwnPropertyNames(dummyResources); + + it('documents depend on create and delete depends on documents', function () { + const documentSets: string[][] = []; + deleteWaitLogicalIds.forEach(function (logicalId: string) { + const documentSet: string[] = []; + const dependencies = deleteWaits[logicalId].DependsOn as Array; + dependencies.forEach(function (value: string) { + if (dummyResourceLogicalIds.includes(value)) { + const dummyResource = dummyResources[value]; + Object.entries(dummyResource.Metadata).forEach(function (meta: [string, unknown]) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + documentSet.push((meta[1] as { [_: string]: any })['Fn::If'][1].Ref); + }); + } + }); + documentSet.push( + ...dependencies.filter(function (value: string) { + return documentLogicalIds.includes(value); + }) + ); + documentSets.push(documentSet); + }); + const remainingDocuments = { ...documents }; + documentSets.forEach(function (documentSet: string[]) { + // all documents depend on the same create resource + const expectedCreateResource = documents[documentSet[0]].DependsOn[0]; + documentSet.forEach(function (value: string) { + delete remainingDocuments[value]; + expect(documents[value].DependsOn).toHaveLength(1); + expect(documents[value].DependsOn[0]).toStrictEqual(expectedCreateResource); + }); + }); + // all documents in a set + expect(Object.getOwnPropertyNames(remainingDocuments)).toHaveLength(0); + }); }); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Resources = { [_: string]: { [_: string]: any } }; + +// do the resources depend on each other in a serial manner +// this isn't foolproof, but it should be enough for simple cases +function formDependencyChain(resources: Resources): boolean { + const logicalIds = Object.getOwnPropertyNames(resources); + let dependencyChainFound = false; + // if so, there will be a resource which starts a chain that contains all the other resources + logicalIds.forEach(function (logicalId: string | undefined) { + const resourcesRemaining = { ...resources }; + while (logicalId) { + let dependencies = resourcesRemaining[logicalId].DependsOn; + // only check dependencies of the same resource type + if (dependencies) { + dependencies = (dependencies as Array).filter(function (value: string) { + return logicalIds.includes(value); + }); + } + delete resourcesRemaining[logicalId]; + if (dependencies && dependencies.length != 0) { + expect(dependencies).toHaveLength(1); + logicalId = dependencies[0]; + } else { + logicalId = undefined; + } + } + // if there are no resources left, this resource is the terminal resource + if (Object.getOwnPropertyNames(resourcesRemaining).length === 0) { + dependencyChainFound = true; + } + }); + return dependencyChainFound; +} diff --git a/source/lib/ssm-doc-rate-limit.ts b/source/lib/ssm-doc-rate-limit.ts index 21aa3a71..94991d9a 100644 --- a/source/lib/ssm-doc-rate-limit.ts +++ b/source/lib/ssm-doc-rate-limit.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { CfnCustomResource, CustomResource, IAspect, Stack } from 'aws-cdk-lib'; +import { CfnCustomResource, CfnWaitConditionHandle, CustomResource, Fn, IAspect, Stack } from 'aws-cdk-lib'; import { CfnDocument } from 'aws-cdk-lib/aws-ssm'; import { Construct, IConstruct } from 'constructs'; import { createHash, Hash } from 'crypto'; @@ -13,6 +13,7 @@ export default class SsmDocRateLimit implements IAspect { private previousCreateWaitResource: CustomResource | undefined; private currentDeleteWaitResource: CustomResource | undefined; private previousDeleteWaitResource: CustomResource | undefined; + private currentDummyResource: CfnWaitConditionHandle | undefined; private hash: Hash; @@ -51,6 +52,12 @@ export default class SsmDocRateLimit implements IAspect { } } + initDummyResource(scope: Construct): void { + if (!this.currentDummyResource) { + this.currentDummyResource = new CfnWaitConditionHandle(scope, `Gate${this.waitResourceIndex - 1}`); + } + } + visit(node: IConstruct): void { if (node instanceof CfnDocument) { const scope = Stack.of(node); @@ -67,7 +74,20 @@ export default class SsmDocRateLimit implements IAspect { updateWaitResourceHash(this.currentDeleteWaitResource, digest); node.addDependency(this.currentCreateWaitResource.node.defaultChild as CfnCustomResource); - this.currentDeleteWaitResource.node.addDependency(node); + + if (node.cfnOptions.condition) { + this.initDummyResource(scope); + if (!this.currentDummyResource) { + throw new Error('Dummy resource not initialized!'); + } + this.currentDummyResource.addMetadata( + `${node.logicalId}Ready`, + Fn.conditionIf(node.cfnOptions.condition.logicalId, Fn.ref(node.logicalId), '') + ); + this.currentDeleteWaitResource.node.addDependency(this.currentDummyResource); + } else { + this.currentDeleteWaitResource.node.addDependency(node); + } ++this.documentIndex; @@ -77,6 +97,7 @@ export default class SsmDocRateLimit implements IAspect { this.previousDeleteWaitResource = this.currentDeleteWaitResource; this.currentCreateWaitResource = undefined; this.currentDeleteWaitResource = undefined; + this.currentDummyResource = undefined; this.hash = createHash('sha256'); } } diff --git a/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap index a84bf677..75a3bcf2 100644 --- a/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap +++ b/source/playbooks/AFSBP/test/__snapshots__/afsbp_stack.test.ts.snap @@ -1131,9 +1131,7 @@ def parse_event(event, _): "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlAFSBPEC21", - "ControlAFSBPLambda1", - "ControlAFSBPRDS1", + "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, @@ -1147,6 +1145,38 @@ def parse_event(event, _): "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, + "Gate0": { + "Metadata": { + "ControlAFSBPEC21Ready": { + "Fn::If": [ + "EnableEC21Condition", + { + "Ref": "ControlAFSBPEC21", + }, + "", + ], + }, + "ControlAFSBPLambda1Ready": { + "Fn::If": [ + "EnableLambda1Condition", + { + "Ref": "ControlAFSBPLambda1", + }, + "", + ], + }, + "ControlAFSBPRDS1Ready": { + "Fn::If": [ + "EnableRDS1Condition", + { + "Ref": "ControlAFSBPRDS1", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, }, } `; diff --git a/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap index f2c94dd2..42007b59 100644 --- a/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap +++ b/source/playbooks/CIS120/test/__snapshots__/cis_stack.test.ts.snap @@ -1481,9 +1481,7 @@ def parse_event(event, _): "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlCIS13", - "ControlCIS15", - "ControlCIS21", + "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, @@ -1497,6 +1495,38 @@ def parse_event(event, _): "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, + "Gate0": { + "Metadata": { + "ControlCIS13Ready": { + "Fn::If": [ + "Enable13Condition", + { + "Ref": "ControlCIS13", + }, + "", + ], + }, + "ControlCIS15Ready": { + "Fn::If": [ + "Enable15Condition", + { + "Ref": "ControlCIS15", + }, + "", + ], + }, + "ControlCIS21Ready": { + "Fn::If": [ + "Enable21Condition", + { + "Ref": "ControlCIS21", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, "RemapCIS4245EB49A0": { "Properties": { "Description": "Remap the CIS 4.2 finding to CIS 4.1 remediation", diff --git a/source/playbooks/CIS140/test/__snapshots__/cis_stack.test.ts.snap b/source/playbooks/CIS140/test/__snapshots__/cis_stack.test.ts.snap index 627105fc..34468f0f 100644 --- a/source/playbooks/CIS140/test/__snapshots__/cis_stack.test.ts.snap +++ b/source/playbooks/CIS140/test/__snapshots__/cis_stack.test.ts.snap @@ -1481,9 +1481,7 @@ def parse_event(event, _): "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlCIS13", - "ControlCIS15", - "ControlCIS21", + "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, @@ -1497,6 +1495,38 @@ def parse_event(event, _): "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, + "Gate0": { + "Metadata": { + "ControlCIS13Ready": { + "Fn::If": [ + "Enable13Condition", + { + "Ref": "ControlCIS13", + }, + "", + ], + }, + "ControlCIS15Ready": { + "Fn::If": [ + "Enable15Condition", + { + "Ref": "ControlCIS15", + }, + "", + ], + }, + "ControlCIS21Ready": { + "Fn::If": [ + "Enable21Condition", + { + "Ref": "ControlCIS21", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, "RemapCIS4245EB49A0": { "Properties": { "Description": "Remap the CIS 4.2 finding to CIS 4.1 remediation", diff --git a/source/playbooks/NEWPLAYBOOK/test/__snapshots__/newplaybook_stack.test.ts.snap b/source/playbooks/NEWPLAYBOOK/test/__snapshots__/newplaybook_stack.test.ts.snap index f9cfecfe..d9a0422f 100644 --- a/source/playbooks/NEWPLAYBOOK/test/__snapshots__/newplaybook_stack.test.ts.snap +++ b/source/playbooks/NEWPLAYBOOK/test/__snapshots__/newplaybook_stack.test.ts.snap @@ -769,7 +769,7 @@ def verify_remediation(event, context): "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlNPBRDS6", + "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, @@ -783,6 +783,20 @@ def verify_remediation(event, context): "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, + "Gate0": { + "Metadata": { + "ControlNPBRDS6Ready": { + "Fn::If": [ + "EnableRDS6Condition", + { + "Ref": "ControlNPBRDS6", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, }, } `; diff --git a/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap index cb97f018..4436403f 100644 --- a/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap +++ b/source/playbooks/PCI321/test/__snapshots__/pci321_stack.test.ts.snap @@ -1478,9 +1478,7 @@ def parse_event(event, _): "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlPCIPCIAutoScaling1", - "ControlPCIPCIEC26", - "ControlPCIPCIIAM8", + "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, @@ -1494,6 +1492,38 @@ def parse_event(event, _): "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, + "Gate0": { + "Metadata": { + "ControlPCIPCIAutoScaling1Ready": { + "Fn::If": [ + "EnablePCIAutoScaling1Condition", + { + "Ref": "ControlPCIPCIAutoScaling1", + }, + "", + ], + }, + "ControlPCIPCIEC26Ready": { + "Fn::If": [ + "EnablePCIEC26Condition", + { + "Ref": "ControlPCIPCIEC26", + }, + "", + ], + }, + "ControlPCIPCIIAM8Ready": { + "Fn::If": [ + "EnablePCIIAM8Condition", + { + "Ref": "ControlPCIPCIIAM8", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, }, } `; diff --git a/source/playbooks/SC/test/__snapshots__/security_controls_stack.test.ts.snap b/source/playbooks/SC/test/__snapshots__/security_controls_stack.test.ts.snap index 60ad6836..197f4cd2 100644 --- a/source/playbooks/SC/test/__snapshots__/security_controls_stack.test.ts.snap +++ b/source/playbooks/SC/test/__snapshots__/security_controls_stack.test.ts.snap @@ -17732,11 +17732,7 @@ def parse_event(event, _): "DeletWait0": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksAutoScaling1BA109277", - "ControlRunbooksCloudFormation12CB945DB", - "ControlRunbooksCloudTrail1B15F1A13", - "ControlRunbooksCloudTrail2979D0B5D", - "ControlRunbooksCloudTrail4057F669F", + "Gate0", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17753,12 +17749,8 @@ def parse_event(event, _): "DeletWait1": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksCloudTrail54F5ED8E4", - "ControlRunbooksCloudTrail6526C5643", - "ControlRunbooksCloudTrail7C6D85038", - "ControlRunbooksCloudWatch1A05F543A", - "ControlRunbooksCodeBuild2A2751671", "DeletWait0", + "Gate1", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17775,12 +17767,8 @@ def parse_event(event, _): "DeletWait2": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksConfig1512B566F", - "ControlRunbooksEC214D3BB404", - "ControlRunbooksEC22ED852ADF", - "ControlRunbooksEC267E3087AE", - "ControlRunbooksEC277719A4CD", "DeletWait1", + "Gate2", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17797,12 +17785,8 @@ def parse_event(event, _): "DeletWait3": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksEC213D7C9C1EB", - "ControlRunbooksEC2153B43E7A8", - "ControlRunbooksIAM3DC25477E", - "ControlRunbooksIAM70A808F7C", - "ControlRunbooksIAM8632E03ED", "DeletWait2", + "Gate3", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17819,12 +17803,8 @@ def parse_event(event, _): "DeletWait4": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksIAM18ACE62321", - "ControlRunbooksIAM2280FCB95D", - "ControlRunbooksKMS41A22BB8D", - "ControlRunbooksLambda1F6ECACF8", - "ControlRunbooksRDS1D73701E9", "DeletWait3", + "Gate4", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17841,12 +17821,8 @@ def parse_event(event, _): "DeletWait5": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksRDS2FBE04686", - "ControlRunbooksRDS4C82F2410", - "ControlRunbooksRDS5CECD9314", - "ControlRunbooksRDS6082B0D6B", - "ControlRunbooksRDS715C0A01A", "DeletWait4", + "Gate5", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17863,12 +17839,8 @@ def parse_event(event, _): "DeletWait6": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksRDS13FCEA51BD", - "ControlRunbooksRDS16EB04DCBF", - "ControlRunbooksRDS89256480A", - "ControlRunbooksRedshift1789871EB", - "ControlRunbooksRedshift3106C10FF", "DeletWait5", + "Gate6", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17885,12 +17857,8 @@ def parse_event(event, _): "DeletWait7": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksRedshift475A78168", - "ControlRunbooksRedshift658631424", - "ControlRunbooksS311C5AAD45", - "ControlRunbooksS3260D6E897", - "ControlRunbooksS34F82DA9F1", "DeletWait6", + "Gate7", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17907,12 +17875,8 @@ def parse_event(event, _): "DeletWait8": { "DeletionPolicy": "Delete", "DependsOn": [ - "ControlRunbooksS356959B795", - "ControlRunbooksS360762680A", - "ControlRunbooksSNS145784CBB", - "ControlRunbooksSNS2112179CC", - "ControlRunbooksSQS173AA7C81", "DeletWait7", + "Gate8", ], "Properties": { "CreateIntervalSeconds": 0, @@ -17926,6 +17890,456 @@ def parse_event(event, _): "Type": "Custom::Wait", "UpdateReplacePolicy": "Delete", }, + "Gate0": { + "Metadata": { + "ControlRunbooksAutoScaling1BA109277Ready": { + "Fn::If": [ + "ControlRunbooksEnableAutoScaling1ConditionD5DF4981", + { + "Ref": "ControlRunbooksAutoScaling1BA109277", + }, + "", + ], + }, + "ControlRunbooksCloudFormation12CB945DBReady": { + "Fn::If": [ + "ControlRunbooksEnableCloudFormation1ConditionD8D32097", + { + "Ref": "ControlRunbooksCloudFormation12CB945DB", + }, + "", + ], + }, + "ControlRunbooksCloudTrail1B15F1A13Ready": { + "Fn::If": [ + "ControlRunbooksEnableCloudTrail1ConditionB7EBAA86", + { + "Ref": "ControlRunbooksCloudTrail1B15F1A13", + }, + "", + ], + }, + "ControlRunbooksCloudTrail2979D0B5DReady": { + "Fn::If": [ + "ControlRunbooksEnableCloudTrail2ConditionC182A10F", + { + "Ref": "ControlRunbooksCloudTrail2979D0B5D", + }, + "", + ], + }, + "ControlRunbooksCloudTrail4057F669FReady": { + "Fn::If": [ + "ControlRunbooksEnableCloudTrail4Condition587734A2", + { + "Ref": "ControlRunbooksCloudTrail4057F669F", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate1": { + "Metadata": { + "ControlRunbooksCloudTrail54F5ED8E4Ready": { + "Fn::If": [ + "ControlRunbooksEnableCloudTrail5Condition17B6B536", + { + "Ref": "ControlRunbooksCloudTrail54F5ED8E4", + }, + "", + ], + }, + "ControlRunbooksCloudTrail6526C5643Ready": { + "Fn::If": [ + "ControlRunbooksEnableCloudTrail6Condition486CC2C3", + { + "Ref": "ControlRunbooksCloudTrail6526C5643", + }, + "", + ], + }, + "ControlRunbooksCloudTrail7C6D85038Ready": { + "Fn::If": [ + "ControlRunbooksEnableCloudTrail7ConditionA4FF88B2", + { + "Ref": "ControlRunbooksCloudTrail7C6D85038", + }, + "", + ], + }, + "ControlRunbooksCloudWatch1A05F543AReady": { + "Fn::If": [ + "ControlRunbooksEnableCloudWatch1ConditionAB0DF2E5", + { + "Ref": "ControlRunbooksCloudWatch1A05F543A", + }, + "", + ], + }, + "ControlRunbooksCodeBuild2A2751671Ready": { + "Fn::If": [ + "ControlRunbooksEnableCodeBuild2ConditionB01F473D", + { + "Ref": "ControlRunbooksCodeBuild2A2751671", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate2": { + "Metadata": { + "ControlRunbooksConfig1512B566FReady": { + "Fn::If": [ + "ControlRunbooksEnableConfig1Condition8CEB8627", + { + "Ref": "ControlRunbooksConfig1512B566F", + }, + "", + ], + }, + "ControlRunbooksEC214D3BB404Ready": { + "Fn::If": [ + "ControlRunbooksEnableEC21ConditionD4F1277B", + { + "Ref": "ControlRunbooksEC214D3BB404", + }, + "", + ], + }, + "ControlRunbooksEC22ED852ADFReady": { + "Fn::If": [ + "ControlRunbooksEnableEC22ConditionB9E0D42E", + { + "Ref": "ControlRunbooksEC22ED852ADF", + }, + "", + ], + }, + "ControlRunbooksEC267E3087AEReady": { + "Fn::If": [ + "ControlRunbooksEnableEC26ConditionF1F880B0", + { + "Ref": "ControlRunbooksEC267E3087AE", + }, + "", + ], + }, + "ControlRunbooksEC277719A4CDReady": { + "Fn::If": [ + "ControlRunbooksEnableEC27ConditionC77CF056", + { + "Ref": "ControlRunbooksEC277719A4CD", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate3": { + "Metadata": { + "ControlRunbooksEC213D7C9C1EBReady": { + "Fn::If": [ + "ControlRunbooksEnableEC213Condition567EA275", + { + "Ref": "ControlRunbooksEC213D7C9C1EB", + }, + "", + ], + }, + "ControlRunbooksEC2153B43E7A8Ready": { + "Fn::If": [ + "ControlRunbooksEnableEC215Condition52A7DE4B", + { + "Ref": "ControlRunbooksEC2153B43E7A8", + }, + "", + ], + }, + "ControlRunbooksIAM3DC25477EReady": { + "Fn::If": [ + "ControlRunbooksEnableIAM3Condition3AA0E892", + { + "Ref": "ControlRunbooksIAM3DC25477E", + }, + "", + ], + }, + "ControlRunbooksIAM70A808F7CReady": { + "Fn::If": [ + "ControlRunbooksEnableIAM7ConditionDF8E776B", + { + "Ref": "ControlRunbooksIAM70A808F7C", + }, + "", + ], + }, + "ControlRunbooksIAM8632E03EDReady": { + "Fn::If": [ + "ControlRunbooksEnableIAM8Condition9CA5CB4B", + { + "Ref": "ControlRunbooksIAM8632E03ED", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate4": { + "Metadata": { + "ControlRunbooksIAM18ACE62321Ready": { + "Fn::If": [ + "ControlRunbooksEnableIAM18ConditionC6288150", + { + "Ref": "ControlRunbooksIAM18ACE62321", + }, + "", + ], + }, + "ControlRunbooksIAM2280FCB95DReady": { + "Fn::If": [ + "ControlRunbooksEnableIAM22Condition387158E7", + { + "Ref": "ControlRunbooksIAM2280FCB95D", + }, + "", + ], + }, + "ControlRunbooksKMS41A22BB8DReady": { + "Fn::If": [ + "ControlRunbooksEnableKMS4Condition710C0C5C", + { + "Ref": "ControlRunbooksKMS41A22BB8D", + }, + "", + ], + }, + "ControlRunbooksLambda1F6ECACF8Ready": { + "Fn::If": [ + "ControlRunbooksEnableLambda1Condition077CECAF", + { + "Ref": "ControlRunbooksLambda1F6ECACF8", + }, + "", + ], + }, + "ControlRunbooksRDS1D73701E9Ready": { + "Fn::If": [ + "ControlRunbooksEnableRDS1ConditionFAE5B7EA", + { + "Ref": "ControlRunbooksRDS1D73701E9", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate5": { + "Metadata": { + "ControlRunbooksRDS2FBE04686Ready": { + "Fn::If": [ + "ControlRunbooksEnableRDS2Condition4FD00FE6", + { + "Ref": "ControlRunbooksRDS2FBE04686", + }, + "", + ], + }, + "ControlRunbooksRDS4C82F2410Ready": { + "Fn::If": [ + "ControlRunbooksEnableRDS4Condition2E89346E", + { + "Ref": "ControlRunbooksRDS4C82F2410", + }, + "", + ], + }, + "ControlRunbooksRDS5CECD9314Ready": { + "Fn::If": [ + "ControlRunbooksEnableRDS5ConditionEC2574C3", + { + "Ref": "ControlRunbooksRDS5CECD9314", + }, + "", + ], + }, + "ControlRunbooksRDS6082B0D6BReady": { + "Fn::If": [ + "ControlRunbooksEnableRDS6Condition4A60A39B", + { + "Ref": "ControlRunbooksRDS6082B0D6B", + }, + "", + ], + }, + "ControlRunbooksRDS715C0A01AReady": { + "Fn::If": [ + "ControlRunbooksEnableRDS7ConditionE53509B0", + { + "Ref": "ControlRunbooksRDS715C0A01A", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate6": { + "Metadata": { + "ControlRunbooksRDS13FCEA51BDReady": { + "Fn::If": [ + "ControlRunbooksEnableRDS13Condition0E8A44B3", + { + "Ref": "ControlRunbooksRDS13FCEA51BD", + }, + "", + ], + }, + "ControlRunbooksRDS16EB04DCBFReady": { + "Fn::If": [ + "ControlRunbooksEnableRDS16ConditionCB5C3E8F", + { + "Ref": "ControlRunbooksRDS16EB04DCBF", + }, + "", + ], + }, + "ControlRunbooksRDS89256480AReady": { + "Fn::If": [ + "ControlRunbooksEnableRDS8Condition8F460AB5", + { + "Ref": "ControlRunbooksRDS89256480A", + }, + "", + ], + }, + "ControlRunbooksRedshift1789871EBReady": { + "Fn::If": [ + "ControlRunbooksEnableRedshift1Condition3449D560", + { + "Ref": "ControlRunbooksRedshift1789871EB", + }, + "", + ], + }, + "ControlRunbooksRedshift3106C10FFReady": { + "Fn::If": [ + "ControlRunbooksEnableRedshift3ConditionC65BAEF6", + { + "Ref": "ControlRunbooksRedshift3106C10FF", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate7": { + "Metadata": { + "ControlRunbooksRedshift475A78168Ready": { + "Fn::If": [ + "ControlRunbooksEnableRedshift4Condition2377F6B5", + { + "Ref": "ControlRunbooksRedshift475A78168", + }, + "", + ], + }, + "ControlRunbooksRedshift658631424Ready": { + "Fn::If": [ + "ControlRunbooksEnableRedshift6Condition5A51FC97", + { + "Ref": "ControlRunbooksRedshift658631424", + }, + "", + ], + }, + "ControlRunbooksS311C5AAD45Ready": { + "Fn::If": [ + "ControlRunbooksEnableS31Condition25C33B3F", + { + "Ref": "ControlRunbooksS311C5AAD45", + }, + "", + ], + }, + "ControlRunbooksS3260D6E897Ready": { + "Fn::If": [ + "ControlRunbooksEnableS32ConditionD6F8CCE9", + { + "Ref": "ControlRunbooksS3260D6E897", + }, + "", + ], + }, + "ControlRunbooksS34F82DA9F1Ready": { + "Fn::If": [ + "ControlRunbooksEnableS34ConditionC23F6623", + { + "Ref": "ControlRunbooksS34F82DA9F1", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, + "Gate8": { + "Metadata": { + "ControlRunbooksS356959B795Ready": { + "Fn::If": [ + "ControlRunbooksEnableS35ConditionD5E024B6", + { + "Ref": "ControlRunbooksS356959B795", + }, + "", + ], + }, + "ControlRunbooksS360762680AReady": { + "Fn::If": [ + "ControlRunbooksEnableS36ConditionD22273E2", + { + "Ref": "ControlRunbooksS360762680A", + }, + "", + ], + }, + "ControlRunbooksSNS145784CBBReady": { + "Fn::If": [ + "ControlRunbooksEnableSNS1Condition7720D1CC", + { + "Ref": "ControlRunbooksSNS145784CBB", + }, + "", + ], + }, + "ControlRunbooksSNS2112179CCReady": { + "Fn::If": [ + "ControlRunbooksEnableSNS2Condition69621468", + { + "Ref": "ControlRunbooksSNS2112179CC", + }, + "", + ], + }, + "ControlRunbooksSQS173AA7C81Ready": { + "Fn::If": [ + "ControlRunbooksEnableSQS1Condition3065B4F2", + { + "Ref": "ControlRunbooksSQS173AA7C81", + }, + "", + ], + }, + }, + "Type": "AWS::CloudFormation::WaitConditionHandle", + }, }, } `;