From 50611ad195e1141f57f306b063b72b26ede9f56c Mon Sep 17 00:00:00 2001 From: watany <76135106+watany-dev@users.noreply.github.com> Date: Sun, 28 Aug 2022 11:44:43 +0000 Subject: [PATCH] #21441 add Custom Policy Rule Constructs --- packages/@aws-cdk/aws-config/README.md | 56 ++++++- packages/@aws-cdk/aws-config/lib/rule.ts | 137 +++++++++++++++++- packages/@aws-cdk/aws-config/package.json | 1 + .../aws-cdk-config-custompolicy.template.json | 32 ++++ ...aultTestDeployAssert4EE21D3A.template.json | 1 + .../test/custompolicy.integ.snapshot/cdk.out | 1 + .../custompolicy.integ.snapshot/integ.json | 11 ++ .../custompolicy.integ.snapshot/manifest.json | 37 +++++ .../custompolicy.integ.snapshot/tree.json | 113 +++++++++++++++ .../aws-config/test/integ.custompolicy.ts | 34 +++++ .../@aws-cdk/aws-config/test/rule.test.ts | 64 ++++++++ 11 files changed, 480 insertions(+), 7 deletions(-) create mode 100644 packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/aws-cdk-config-custompolicy.template.json create mode 100644 packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/awscdkconfigcustompolicyintegDefaultTestDeployAssert4EE21D3A.template.json create mode 100644 packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-config/test/integ.custompolicy.ts diff --git a/packages/@aws-cdk/aws-config/README.md b/packages/@aws-cdk/aws-config/README.md index 62800220da594..81959aa8c7bf1 100644 --- a/packages/@aws-cdk/aws-config/README.md +++ b/packages/@aws-cdk/aws-config/README.md @@ -116,8 +116,60 @@ new config.CloudFormationStackNotificationCheck(this, 'NotificationCheck', { ### Custom rules You can develop custom rules and add them to AWS Config. You associate each custom rule with an -AWS Lambda function, which contains the logic that evaluates whether your AWS resources comply -with the rule. +AWS Lambda function and Guard. + +#### Custom Lambda Rules + +Lambda function which contains the logic that evaluates whether your AWS resources comply with the rule. + +```ts +// Lambda function containing logic that evaluates compliance with the rule. +const evalComplianceFn = new lambda.Function(this, "CustomFunction", { + code: lambda.AssetCode.fromInline( + "exports.handler = (event) => console.log(event);" + ), + handler: "index.handler", + runtime: lambda.Runtime.NODEJS_14_X, +}); + +// A custom rule that runs on configuration changes of EC2 instances +const customRule = new config.CustomRule(this, "Custom", { + configurationChanges: true, + lambdaFunction: evalComplianceFn, + ruleScope: config.RuleScope.fromResource(config.ResourceType.EC2_INSTANCE), +}); +``` + +#### Custom Policy Rules + +Guard which contains the logic that evaluates whether your AWS resources comply with the rule. + +```ts +const samplePolicyText = ` +# This rule checks if point in time recovery (PITR) is enabled on active Amazon DynamoDB tables +let status = ['ACTIVE'] + +rule tableisactive when + resourceType == "AWS::DynamoDB::Table" { + configuration.tableStatus == %status +} + +rule checkcompliance when + resourceType == "AWS::DynamoDB::Table" + tableisactive { + let pitr = supplementaryConfiguration.ContinuousBackupsDescription.pointInTimeRecoveryDescription.pointInTimeRecoveryStatus + %pitr == "ENABLED" +} +`; + +new config.CustomPolicy(stack, "Custom", { + policyText: samplePolicyText, + enableDebugLog: true, + ruleScope: config.RuleScope.fromResources([ + config.ResourceType.DYNAMODB_TABLE, + ]), +}); +``` ### Triggers diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts index c4c6e14a2377c..ab185178810cc 100644 --- a/packages/@aws-cdk/aws-config/lib/rule.ts +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -281,6 +281,52 @@ export class ManagedRule extends RuleNew { } } +/** + * The type of notification that triggers AWS Config to run an evaluation for a rule. + */ +enum MessageType { + + /** + * Triggers an evaluation when AWS Config delivers a configuration item as a result of a resource change. + */ + CONFIGURATION_ITEM_CHANGE_NOTIFICATION = 'ConfigurationItemChangeNotification', + + /** + * Triggers an evaluation when AWS Config delivers an oversized configuration item. + */ + OVERSIZED_CONFIGURATION_ITEM_CHANGE_NOTIFICATION = 'OversizedConfigurationItemChangeNotification', + + /** + * Triggers a periodic evaluation at the frequency specified for MaximumExecutionFrequency. + */ + SCHEDULED_NOTIFICATION = 'ScheduledNotification', + + /** + * Triggers a periodic evaluation when AWS Config delivers a configuration snapshot. + */ + CONFIGURATION_SNAPSHOT_DELIVERY_COMPLETED = 'ConfigurationSnapshotDeliveryCompleted', +} + +/** + * Construction properties for a CustomRule. + */ +interface SourceDetail { + /** + * The source of the event, such as an AWS service, + * that triggers AWS Config to evaluate your AWS resources. + * + */ + readonly eventSource : string; + /** + * The frequency at which you want AWS Config to run evaluations for a custom rule with a periodic trigger. + */ + readonly maximumExecutionFrequency? : MaximumExecutionFrequency; + /** + * The type of notification that triggers AWS Config to run an evaluation for a rule. + */ + readonly messageType : MessageType; +} + /** * Construction properties for a CustomRule. */ @@ -331,17 +377,16 @@ export class CustomRule extends RuleNew { throw new Error('At least one of `configurationChanges` or `periodic` must be set to true.'); } - const sourceDetails: any[] = []; + const sourceDetails: SourceDetail[] = []; this.ruleScope = props.ruleScope; - if (props.configurationChanges) { sourceDetails.push({ eventSource: 'aws.config', - messageType: 'ConfigurationItemChangeNotification', + messageType: MessageType.CONFIGURATION_ITEM_CHANGE_NOTIFICATION, }); sourceDetails.push({ eventSource: 'aws.config', - messageType: 'OversizedConfigurationItemChangeNotification', + messageType: MessageType.OVERSIZED_CONFIGURATION_ITEM_CHANGE_NOTIFICATION, }); } @@ -349,7 +394,7 @@ export class CustomRule extends RuleNew { sourceDetails.push({ eventSource: 'aws.config', maximumExecutionFrequency: props.maximumExecutionFrequency, - messageType: 'ScheduledNotification', + messageType: MessageType.SCHEDULED_NOTIFICATION, }); } @@ -391,6 +436,88 @@ export class CustomRule extends RuleNew { } } +/** + * Construction properties for a CustomPolicy. + */ +export interface CustomPolicyProps extends RuleProps { + /** + * The policy definition containing the logic for your AWS Config Custom Policy rule. + */ + readonly policyText: string; + + /** + * The boolean expression for enabling debug logging for your AWS Config Custom Policy rule. + * + * @default false + */ + readonly enableDebugLog?: boolean; +} + +/** + * A new custom policy. + * + * @resource AWS::Config::ConfigRule + */ +export class CustomPolicy extends RuleNew { + /** @attribute */ + public readonly configRuleName: string; + + /** @attribute */ + public readonly configRuleArn: string; + + /** @attribute */ + public readonly configRuleId: string; + + /** @attribute */ + public readonly configRuleComplianceType: string; + + constructor(scope: Construct, id: string, props: CustomPolicyProps) { + super(scope, id, { + physicalName: props.configRuleName, + }); + + if (!props.policyText || [...props.policyText].length === 0) { + throw new Error('Policy Text cannot be empty.'); + } + if ( [...props.policyText].length > 10000 ) { + throw new Error('Policy Text is limited to 10,000 characters or less.'); + } + + const sourceDetails: SourceDetail[] = []; + this.ruleScope = props.ruleScope; + + sourceDetails.push({ + eventSource: 'aws.config', + messageType: MessageType.CONFIGURATION_ITEM_CHANGE_NOTIFICATION, + }); + sourceDetails.push({ + eventSource: 'aws.config', + messageType: MessageType.OVERSIZED_CONFIGURATION_ITEM_CHANGE_NOTIFICATION, + }); + const rule = new CfnConfigRule(this, 'Resource', { + configRuleName: this.physicalName, + description: props.description, + inputParameters: props.inputParameters, + scope: Lazy.any({ produce: () => renderScope(this.ruleScope) }), // scope can use values such as stack id (see CloudFormationStackDriftDetectionCheck) + source: { + owner: 'CUSTOM_POLICY', + sourceDetails, + customPolicyDetails: { + enableDebugLogDelivery: props.enableDebugLog, + policyRuntime: 'guard-2.x.x', + policyText: props.policyText, + }, + }, + }); + + this.configRuleName = rule.ref; + this.configRuleArn = rule.attrArn; + this.configRuleId = rule.attrConfigRuleId; + this.configRuleComplianceType = rule.attrComplianceType; + this.isCustomWithChanges = true; + } +} + /** * Managed rules that are supported by AWS Config. * @see https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 5e7a4ce801ee7..6cb36fa4c3ada 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -84,6 +84,7 @@ "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", "@types/jest": "^27.5.2", diff --git a/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/aws-cdk-config-custompolicy.template.json b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/aws-cdk-config-custompolicy.template.json new file mode 100644 index 0000000000000..6c7760dadaad9 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/aws-cdk-config-custompolicy.template.json @@ -0,0 +1,32 @@ +{ + "Resources": { + "Custom8166710A": { + "Type": "AWS::Config::ConfigRule", + "Properties": { + "Source": { + "CustomPolicyDetails": { + "EnableDebugLogDelivery": true, + "PolicyRuntime": "guard-2.x.x", + "PolicyText": "\n# This rule checks if point in time recovery (PITR) is enabled on active Amazon DynamoDB tables\nlet status = ['ACTIVE']\n\nrule tableisactive when\n resourceType == \"AWS::DynamoDB::Table\" {\n configuration.tableStatus == %status\n}\n\nrule checkcompliance when\n resourceType == \"AWS::DynamoDB::Table\"\n tableisactive {\n let pitr = supplementaryConfiguration.ContinuousBackupsDescription.pointInTimeRecoveryDescription.pointInTimeRecoveryStatus\n %pitr == \"ENABLED\"\n}\n" + }, + "Owner": "CUSTOM_POLICY", + "SourceDetails": [ + { + "EventSource": "aws.config", + "MessageType": "ConfigurationItemChangeNotification" + }, + { + "EventSource": "aws.config", + "MessageType": "OversizedConfigurationItemChangeNotification" + } + ] + }, + "Scope": { + "ComplianceResourceTypes": [ + "AWS::DynamoDB::Table" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/awscdkconfigcustompolicyintegDefaultTestDeployAssert4EE21D3A.template.json b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/awscdkconfigcustompolicyintegDefaultTestDeployAssert4EE21D3A.template.json new file mode 100644 index 0000000000000..9e26dfeeb6e64 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/awscdkconfigcustompolicyintegDefaultTestDeployAssert4EE21D3A.template.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..588d7b269d34f --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"20.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/integ.json b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/integ.json new file mode 100644 index 0000000000000..459df53ff3c44 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/integ.json @@ -0,0 +1,11 @@ +{ + "version": "20.0.0", + "testCases": { + "aws-cdk-config-custompolicy-integ/DefaultTest": { + "stacks": [ + "aws-cdk-config-custompolicy" + ], + "assertionStack": "aws-cdk-config-custompolicy-integ/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..bddeb4922cbee --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/manifest.json @@ -0,0 +1,37 @@ +{ + "version": "20.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-config-custompolicy": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-config-custompolicy.template.json", + "validateOnSynth": false + }, + "metadata": { + "/aws-cdk-config-custompolicy/Custom/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Custom8166710A" + } + ] + }, + "displayName": "aws-cdk-config-custompolicy" + }, + "awscdkconfigcustompolicyintegDefaultTestDeployAssert4EE21D3A": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "awscdkconfigcustompolicyintegDefaultTestDeployAssert4EE21D3A.template.json", + "validateOnSynth": false + }, + "displayName": "aws-cdk-config-custompolicy-integ/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/tree.json b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/tree.json new file mode 100644 index 0000000000000..67665e6141595 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/custompolicy.integ.snapshot/tree.json @@ -0,0 +1,113 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.78" + } + }, + "aws-cdk-config-custompolicy": { + "id": "aws-cdk-config-custompolicy", + "path": "aws-cdk-config-custompolicy", + "children": { + "Custom": { + "id": "Custom", + "path": "aws-cdk-config-custompolicy/Custom", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-config-custompolicy/Custom/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Config::ConfigRule", + "aws:cdk:cloudformation:props": { + "source": { + "owner": "CUSTOM_POLICY", + "sourceDetails": [ + { + "eventSource": "aws.config", + "messageType": "ConfigurationItemChangeNotification" + }, + { + "eventSource": "aws.config", + "messageType": "OversizedConfigurationItemChangeNotification" + } + ], + "customPolicyDetails": { + "enableDebugLogDelivery": true, + "policyRuntime": "guard-2.x.x", + "policyText": "\n# This rule checks if point in time recovery (PITR) is enabled on active Amazon DynamoDB tables\nlet status = ['ACTIVE']\n\nrule tableisactive when\n resourceType == \"AWS::DynamoDB::Table\" {\n configuration.tableStatus == %status\n}\n\nrule checkcompliance when\n resourceType == \"AWS::DynamoDB::Table\"\n tableisactive {\n let pitr = supplementaryConfiguration.ContinuousBackupsDescription.pointInTimeRecoveryDescription.pointInTimeRecoveryStatus\n %pitr == \"ENABLED\"\n}\n" + } + }, + "scope": { + "complianceResourceTypes": [ + "AWS::EC2::Instance" + ] + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-config.CfnConfigRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-config.CustomPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "aws-cdk-config-custompolicy-integ": { + "id": "aws-cdk-config-custompolicy-integ", + "path": "aws-cdk-config-custompolicy-integ", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "aws-cdk-config-custompolicy-integ/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "aws-cdk-config-custompolicy-integ/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.78" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "aws-cdk-config-custompolicy-integ/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/integ.custompolicy.ts b/packages/@aws-cdk/aws-config/test/integ.custompolicy.ts new file mode 100644 index 0000000000000..b5de4e7f36036 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/integ.custompolicy.ts @@ -0,0 +1,34 @@ +import * as cdk from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import * as config from '../lib'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-cdk-config-custompolicy'); + +const samplePolicyText = ` +# This rule checks if point in time recovery (PITR) is enabled on active Amazon DynamoDB tables +let status = ['ACTIVE'] + +rule tableisactive when + resourceType == "AWS::DynamoDB::Table" { + configuration.tableStatus == %status +} + +rule checkcompliance when + resourceType == "AWS::DynamoDB::Table" + tableisactive { + let pitr = supplementaryConfiguration.ContinuousBackupsDescription.pointInTimeRecoveryDescription.pointInTimeRecoveryStatus + %pitr == "ENABLED" +} +`; + +new config.CustomPolicy(stack, 'Custom', { + policyText: samplePolicyText, + enableDebugLog: true, + ruleScope: config.RuleScope.fromResources([config.ResourceType.DYNAMODB_TABLE]), +}); + +new integ.IntegTest(app, 'aws-cdk-config-custompolicy-integ', { + testCases: [stack], +}); +app.synth(); diff --git a/packages/@aws-cdk/aws-config/test/rule.test.ts b/packages/@aws-cdk/aws-config/test/rule.test.ts index 284354d302f9d..740ebf9220264 100644 --- a/packages/@aws-cdk/aws-config/test/rule.test.ts +++ b/packages/@aws-cdk/aws-config/test/rule.test.ts @@ -416,5 +416,69 @@ describe('rule', () => { }, }); }); + test('create a custom policy', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new config.CustomPolicy(stack, 'Rule', { + policyText: ` + let status = ['ACTIVE'] + + rule tableisactive when + resourceType == "AWS::DynamoDB::Table" { + configuration.tableStatus == %status + } + + rule checkcompliance when + resourceType == "AWS::DynamoDB::Table" + tableisactive { + let pitr = supplementaryConfiguration.ContinuousBackupsDescription.pointInTimeRecoveryDescription.pointInTimeRecoveryStatus + %pitr == "ENABLED" + }`, + description: 'really cool rule', + configRuleName: 'cool rule', + }); + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Config::ConfigRule', { + Source: { + Owner: 'CUSTOM_POLICY', + SourceDetails: [ + { + EventSource: 'aws.config', + MessageType: 'ConfigurationItemChangeNotification', + }, + { + EventSource: 'aws.config', + MessageType: 'OversizedConfigurationItemChangeNotification', + }, + ], + }, + ConfigRuleName: 'cool rule', + Description: 'really cool rule', + }); + }); + + test('create a 0 charactor policy', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + // THEN + expect(() => new config.CustomPolicy(stack, 'Rule', { + policyText: '', + })).toThrow('Policy Text cannot be empty.'); + }); + + test('create over 10000 charactor policy', () => { + // GIVEN + const stack = new cdk.Stack(); + const stringLen10001 = '0123456789'.repeat(1000) + 'a'; + // WHEN + // THEN + expect(() => new config.CustomPolicy(stack, 'Rule', { + policyText: stringLen10001, + })).toThrow('Policy Text is limited to 10,000 characters or less.'); + }); });