diff --git a/packages/@aws-cdk/aws-config/README.md b/packages/@aws-cdk/aws-config/README.md index 57d24dbfd9e16..98c190ed0b719 100644 --- a/packages/@aws-cdk/aws-config/README.md +++ b/packages/@aws-cdk/aws-config/README.md @@ -1,2 +1,72 @@ ## The CDK Construct Library for AWS Config This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. + +Supported: +* Config rules + +Not supported +* Configuration recoder +* Delivery channel +* Aggregation + +### Rules + +#### AWS managed rules +To set up a managed rule, define a `ManagedRule` and specify its identifier: + +```ts +new ManagedRule(this, 'AccessKeysRotated', { + identifier: 'ACCESS_KEYS_ROTATED' +}); +``` + +Available identifiers and parameters are listed in the [List of AWS Config Managed Rules](https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html). + + +Higher level constructs for managed rules are available, see [Managed Rules](https://github.com/awslabs/aws-cdk/blob/master/packages/%40aws-cdk/aws-config/lib/managed-rules.ts). Prefer to use those constructs when available (PRs welcome to add more of those). + +#### Custom rules +To set up a custom rule, define a `CustomRule` and specify the Lambda Function to run and the trigger types: + +```ts +new CustomRule(this, 'CustomRule', { + lambdaFunction: myFn, + configurationChanges: true, + periodic: true +}); +``` + +#### Restricting the scope +By default rules are triggered by changes to all [resources](https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources). Use the `scopeToResource()`, `scopeToResources()` or `scopeToTag()` methods to restrict the scope of both managed and custom rules: + +```ts +const sshRule = new ManagedRule(this, 'SSH', { + identifier: 'INCOMING_SSH_DISABLED' +}); + +// Restrict to a specific security group +rule.scopeToResource('AWS::EC2::SecurityGroup', 'sg-1234567890abcdefgh'); + +const customRule = new CustomRule(this, 'CustomRule', { + lambdaFunction: myFn, + configurationChanges: true +}); + +// Restrict to a specific tag +customRule.scopeToTag('Cost Center', 'MyApp'); +``` + +Only one type of scope restriction can be added to a rule (the last call to `scopeToXxx()` sets the scope). + +#### Events +To define Amazon CloudWatch event rules, use the `onComplianceChange()` or `onReEvaluationStatus()` methods: + +```ts +const rule = new CloudFormationStackDriftDetectionCheck(this, 'Drift'); +rule.onComplianceChange(topic); +``` + +#### Example +Creating custom and managed rules with scope restriction and events: + +[example of setting up rules](test/integ.rule.lit.ts) diff --git a/packages/@aws-cdk/aws-config/lib/index.ts b/packages/@aws-cdk/aws-config/lib/index.ts index de950dfdb59af..73e3044e50c42 100644 --- a/packages/@aws-cdk/aws-config/lib/index.ts +++ b/packages/@aws-cdk/aws-config/lib/index.ts @@ -1,2 +1,5 @@ +export * from './rule'; +export * from './managed-rules'; + // AWS::Config CloudFormation Resources: export * from './config.generated'; diff --git a/packages/@aws-cdk/aws-config/lib/managed-rules.ts b/packages/@aws-cdk/aws-config/lib/managed-rules.ts new file mode 100644 index 0000000000000..0c54803799966 --- /dev/null +++ b/packages/@aws-cdk/aws-config/lib/managed-rules.ts @@ -0,0 +1,130 @@ +import iam = require('@aws-cdk/aws-iam'); +import sns = require('@aws-cdk/aws-sns'); +import { Construct, Token } from '@aws-cdk/cdk'; +import { ManagedRule, RuleProps } from './rule'; + +/** + * Construction properties for a AccessKeysRotated + */ +export interface AccessKeysRotatedProps extends RuleProps { + /** + * The maximum number of days within which the access keys must be rotated. + * + * @default 90 days + */ + readonly maxDays?: number; +} + +/** + * Checks whether the active access keys are rotated within the number of days + * specified in `maxDays`. + * + * @see https://docs.aws.amazon.com/config/latest/developerguide/access-keys-rotated.html + * + * @resource AWS::Config::ConfigRule + */ +export class AccessKeysRotated extends ManagedRule { + constructor(scope: Construct, id: string, props: AccessKeysRotatedProps = {}) { + super(scope, id, { + ...props, + identifier: 'ACCESS_KEYS_ROTATED', + inputParameters: { + ...props.maxDays + ? { + maxAccessKeyAge: props.maxDays + } + : {} + } + }); + } +} + +/** + * Construction properties for a CloudFormationStackDriftDetectionCheck + */ +export interface CloudFormationStackDriftDetectionCheckProps extends RuleProps { + /** + * Whether to check only the stack where this rule is deployed. + * + * @default false + */ + readonly ownStackOnly?: boolean; + + /** + * The IAM role to use for this rule. It must have permissions to detect drift + * for AWS CloudFormation stacks. Ensure to attach `config.amazonaws.com` trusted + * permissions and `ReadOnlyAccess` policy permissions. For specific policy permissions, + * refer to https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html. + * + * @default a role will be created + */ + readonly role?: iam.IRole; +} + +/** + * Checks whether your CloudFormation stacks' actual configuration differs, or + * has drifted, from its expected configuration. + * + * @see https://docs.aws.amazon.com/config/latest/developerguide/cloudformation-stack-drift-detection-check.html + * + * @resource AWS::Config::ConfigRule + */ +export class CloudFormationStackDriftDetectionCheck extends ManagedRule { + private readonly role: iam.IRole; + + constructor(scope: Construct, id: string, props: CloudFormationStackDriftDetectionCheckProps = {}) { + super(scope, id, { + ...props, + identifier: 'CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK', + inputParameters: { + cloudformationRoleArn: new Token(() => this.role.roleArn) + } + }); + + this.scopeToResource('AWS::CloudFormation::Stack', props.ownStackOnly ? this.node.stack.stackId : undefined); + + this.role = props.role || new iam.Role(this, 'Role', { + assumedBy: new iam.ServicePrincipal('config.amazonaws.com'), + managedPolicyArns: [ + new iam.AwsManagedPolicy('ReadOnlyAccess', this).policyArn, + ] + }); + } +} + +/** + * Construction properties for a CloudFormationStackNotificationCheck. + */ +export interface CloudFormationStackNotificationCheckProps extends RuleProps { + /** + * A list of allowed topics. At most 5 topics. + */ + readonly topics?: sns.ITopic[]; +} + +/** + * Checks whether your CloudFormation stacks are sending event notifications to + * a SNS topic. Optionally checks whether specified SNS topics are used. + * + * @see https://docs.aws.amazon.com/config/latest/developerguide/cloudformation-stack-notification-check.html + * + * @resource AWS::Config::ConfigRule + */ +export class CloudFormationStackNotificationCheck extends ManagedRule { + constructor(scope: Construct, id: string, props: CloudFormationStackNotificationCheckProps = {}) { + if (props.topics && props.topics.length > 5) { + throw new Error('At most 5 topics can be specified.'); + } + + super(scope, id, { + ...props, + identifier: 'CLOUDFORMATION_STACK_NOTIFICATION_CHECK', + inputParameters: props.topics && props.topics.reduce( + (params, topic, idx) => ({ ...params, [`snsTopic${idx + 1}`]: topic.topicArn }), + {} + ) + }); + + this.scopeToResource('AWS::CloudFormation::Stack'); + } +} diff --git a/packages/@aws-cdk/aws-config/lib/rule.ts b/packages/@aws-cdk/aws-config/lib/rule.ts new file mode 100644 index 0000000000000..3d9b742763d88 --- /dev/null +++ b/packages/@aws-cdk/aws-config/lib/rule.ts @@ -0,0 +1,378 @@ +import events = require('@aws-cdk/aws-events'); +import iam = require('@aws-cdk/aws-iam'); +import lambda = require('@aws-cdk/aws-lambda'); +import { Construct, IResource, Resource, Token } from '@aws-cdk/cdk'; +import { CfnConfigRule } from './config.generated'; + +/** + * A config rule. + */ +export interface IRule extends IResource { + /** + * The name of the rule. + * + * @attribute + */ + readonly configRuleName: string; + + /** + * Defines a CloudWatch event rule which triggers for rule events. Use + * `rule.addEventPattern(pattern)` to specify a filter. + */ + onEvent(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + + /** + * Defines a CloudWatch event rule which triggers for rule compliance events. + */ + onComplianceChange(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; + + /** + * Defines a CloudWatch event rule which triggers for rule re-evaluation status events. + */ + onReEvaluationStatus(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule; +} + +/** + * Reference to an existing rule. + */ +export interface RuleAttributes { + /** + * The rule name. + */ + readonly configRuleName: string; +} + +/** + * A new or imported rule. + */ +abstract class RuleBase extends Resource implements IRule { + /** + * Imports an existing rule. + * + * @param configRuleName the name of the rule + */ + public static fromConfigRuleName(scope: Construct, id: string, configRuleName: string): IRule { + class Import extends RuleBase { + public readonly configRuleName = configRuleName; + } + + return new Import(scope, id); + } + + public abstract readonly configRuleName: string; + + /** + * Defines a CloudWatch event rule which triggers for rule events. Use + * `rule.addEventPattern(pattern)` to specify a filter. + */ + public onEvent(id: string, target?: events.IRuleTarget, options?: events.RuleProps) { + const rule = new events.Rule(this, id, options); + rule.addEventPattern({ + source: ['aws.config'], + detail: { + configRuleName: [this.configRuleName] + } + }); + rule.addTarget(target); + return rule; + } + + /** + * Defines a CloudWatch event rule which triggers for rule compliance events. + */ + public onComplianceChange(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { + const rule = this.onEvent(id, target, options); + rule.addEventPattern({ + detailType: [ 'Config Rules Compliance Change' ], + }); + return rule; + } + + /** + * Defines a CloudWatch event rule which triggers for rule re-evaluation status events. + */ + public onReEvaluationStatus(id: string, target?: events.IRuleTarget, options?: events.RuleProps): events.Rule { + const rule = this.onEvent(id, target, options); + rule.addEventPattern({ + detailType: [ 'Config Rules Re-evaluation Status' ], + }); + return rule; + } +} + +/** + * A new managed or custom rule. + */ +abstract class RuleNew extends RuleBase { + /** + * The arn of the rule. + */ + public abstract readonly configRuleArn: string; + + /** + * The id of the rule. + */ + public abstract readonly configRuleId: string; + + /** + * The compliance status of the rule. + */ + public abstract readonly configRuleComplianceType: string; + + protected scope?: CfnConfigRule.ScopeProperty; + protected isManaged?: boolean; + protected isCustomWithChanges?: boolean; + + /** + * Restrict scope of changes to a specific resource. + * + * @see https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources + * + * @param type the resource type + * @param identifier the resource identifier + */ + public scopeToResource(type: string, identifier?: string) { + this.scopeTo({ + complianceResourceId: identifier, + complianceResourceTypes: [type], + }); + } + + /** + * Restrict scope of changes to specific resource types. + * + * @see https://docs.aws.amazon.com/config/latest/developerguide/resource-config-reference.html#supported-resources + * + * @param types resource types + */ + public scopeToResources(...types: string[]) { + this.scopeTo({ + complianceResourceTypes: types + }); + } + + /** + * Restrict scope of changes to a specific tag. + * + * @param key the tag key + * @param value the tag value + */ + public scopeToTag(key: string, value?: string) { + this.scopeTo({ + tagKey: key, + tagValue: value + }); + } + + private scopeTo(scope: CfnConfigRule.ScopeProperty) { + if (!this.isManaged && !this.isCustomWithChanges) { + throw new Error('Cannot scope rule when `configurationChanges` is set to false.'); + } + + this.scope = scope; + } +} + +/** + * The maximum frequency at which the AWS Config rule runs evaluations. + */ +export enum MaximumExecutionFrequency { + ONE_HOUR = 'One_Hour', + THREE_HOURS = 'Three_Hours', + SIX_HOURS = 'Six_Hours', + TWELVE_HOURS = 'Twelve_Hours', + TWENTY_FOUR_HOURS = 'TwentyFour_Hours' +} + +/** + * Construction properties for a new rule. + */ +export interface RuleProps { + /** + * A name for the AWS Config rule. + * + * @default a CloudFormation generated name + */ + readonly name?: string; + + /** + * A description about this AWS Config rule. + * + * @default no description + */ + readonly description?: string; + + /** + * Input parameter values that are passed to the AWS Config rule. + * + * @default no input parameters + */ + readonly inputParameters?: { [key: string]: any }; + + /** + * The maximum frequency at which the AWS Config rule runs evaluations. + * + * @default 24 hours + */ + readonly maximumExecutionFrequency?: MaximumExecutionFrequency +} + +/** + * Construction properties for a ManagedRule. + */ +export interface ManagedRuleProps extends RuleProps { + /** + * The identifier of the AWS managed rule. + * + * @see https://docs.aws.amazon.com/config/latest/developerguide/managed-rules-by-aws-config.html + */ + readonly identifier: string; +} + +/** + * A new managed rule. + * + * @resource AWS::Config::ConfigRule + */ +export class ManagedRule 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: ManagedRuleProps) { + super(scope, id); + + const rule = new CfnConfigRule(this, 'Resource', { + configRuleName: props.name, + description: props.description, + inputParameters: props.inputParameters, + maximumExecutionFrequency: props.maximumExecutionFrequency, + scope: new Token(() => this.scope), + source: { + owner: 'AWS', + sourceIdentifier: props.identifier + } + }); + + this.configRuleName = rule.configRuleName; + this.configRuleArn = rule.configRuleArn; + this.configRuleId = rule.configRuleId; + this.configRuleComplianceType = rule.configRuleComplianceType; + + this.isManaged = true; + } +} + +/** + * Consruction properties for a CustomRule. + */ +export interface CustomRuleProps extends RuleProps { + /** + * The Lambda function to run. + */ + readonly lambdaFunction: lambda.IFunction; + + /** + * Whether to run the rule on configuration changes. + * + * @default false + */ + readonly configurationChanges?: boolean; + + /** + * Whether to run the rule on a fixed frequency. + * + * @default false + */ + readonly periodic?: boolean; +} +/** + * A new custom rule. + * + * @resource AWS::Config::ConfigRule + */ +export class CustomRule 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: CustomRuleProps) { + super(scope, id); + + if (!props.configurationChanges && !props.periodic) { + throw new Error('At least one of `configurationChanges` or `periodic` must be set to true.'); + } + + const sourceDetails: any[] = []; + + if (props.configurationChanges) { + sourceDetails.push({ + eventSource: 'aws.config', + messageType: 'ConfigurationItemChangeNotification' + }); + sourceDetails.push({ + eventSource: 'aws.config', + messageType: 'OversizedConfigurationItemChangeNotification' + }); + } + + if (props.periodic) { + sourceDetails.push({ + eventSource: 'aws.config', + maximumExecutionFrequency: props.maximumExecutionFrequency, + messageType: 'ScheduledNotification' + }); + } + + props.lambdaFunction.addPermission('Permission', { + principal: new iam.ServicePrincipal('config.amazonaws.com') + }); + + if (props.lambdaFunction.role) { + props.lambdaFunction.role.attachManagedPolicy( + new iam.AwsManagedPolicy('service-role/AWSConfigRulesExecutionRole', this).policyArn + ); + } + + // The lambda permission must be created before the rule + this.node.addDependency(props.lambdaFunction); + + const rule = new CfnConfigRule(this, 'Resource', { + configRuleName: props.name, + description: props.description, + inputParameters: props.inputParameters, + maximumExecutionFrequency: props.maximumExecutionFrequency, + scope: new Token(() => this.scope), + source: { + owner: 'CUSTOM_LAMBDA', + sourceDetails, + sourceIdentifier: props.lambdaFunction.functionArn + } + }); + + this.configRuleName = rule.configRuleName; + this.configRuleArn = rule.configRuleArn; + this.configRuleId = rule.configRuleId; + this.configRuleComplianceType = rule.configRuleComplianceType; + + if (props.configurationChanges) { + this.isCustomWithChanges = true; + } + } +} diff --git a/packages/@aws-cdk/aws-config/package.json b/packages/@aws-cdk/aws-config/package.json index 1d3fcf4036763..a140f135fa15c 100644 --- a/packages/@aws-cdk/aws-config/package.json +++ b/packages/@aws-cdk/aws-config/package.json @@ -59,15 +59,25 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.31.0", + "@aws-cdk/aws-events-targets": "^0.31.0", "cdk-build-tools": "^0.31.0", + "cdk-integ-tools": "^0.31.0", "cfn2ts": "^0.31.0", "pkglint": "^0.31.0" }, "dependencies": { + "@aws-cdk/aws-events": "^0.31.0", + "@aws-cdk/aws-iam": "^0.31.0", + "@aws-cdk/aws-lambda": "^0.31.0", + "@aws-cdk/aws-sns": "^0.31.0", "@aws-cdk/cdk": "^0.31.0" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-events": "^0.31.0", + "@aws-cdk/aws-iam": "^0.31.0", + "@aws-cdk/aws-lambda": "^0.31.0", + "@aws-cdk/aws-sns": "^0.31.0", "@aws-cdk/cdk": "^0.31.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-config/test/integ.rule.lit.expected.json b/packages/@aws-cdk/aws-config/test/integ.rule.lit.expected.json new file mode 100644 index 0000000000000..eff352b6a34f1 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/integ.rule.lit.expected.json @@ -0,0 +1,254 @@ +{ + "Resources": { + "CustomFunctionServiceRoleD3F73B79": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "lambda.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSConfigRulesExecutionRole" + ] + ] + } + ] + } + }, + "CustomFunctionBADD59E7": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "exports.handler = (event) => console.log(event);" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomFunctionServiceRoleD3F73B79", + "Arn" + ] + }, + "Runtime": "nodejs8.10" + }, + "DependsOn": [ + "CustomFunctionServiceRoleD3F73B79" + ] + }, + "CustomFunctionPermission41887A5E": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "CustomFunctionBADD59E7", + "Arn" + ] + }, + "Principal": "config.amazonaws.com" + } + }, + "Custom8166710A": { + "Type": "AWS::Config::ConfigRule", + "Properties": { + "Source": { + "Owner": "CUSTOM_LAMBDA", + "SourceDetails": [ + { + "EventSource": "aws.config", + "MessageType": "ConfigurationItemChangeNotification" + }, + { + "EventSource": "aws.config", + "MessageType": "OversizedConfigurationItemChangeNotification" + } + ], + "SourceIdentifier": { + "Fn::GetAtt": [ + "CustomFunctionBADD59E7", + "Arn" + ] + } + }, + "Scope": { + "ComplianceResourceTypes": [ + "AWS::EC2::Instance" + ] + } + }, + "DependsOn": [ + "CustomFunctionPermission41887A5E", + "CustomFunctionBADD59E7", + "CustomFunctionServiceRoleD3F73B79" + ] + }, + "Drift34D3210F": { + "Type": "AWS::Config::ConfigRule", + "Properties": { + "Source": { + "Owner": "AWS", + "SourceIdentifier": "CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK" + }, + "InputParameters": { + "cloudformationRoleArn": { + "Fn::GetAtt": [ + "DriftRole8A5FB833", + "Arn" + ] + } + }, + "Scope": { + "ComplianceResourceTypes": [ + "AWS::CloudFormation::Stack" + ] + } + } + }, + "DriftRole8A5FB833": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "config.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/ReadOnlyAccess" + ] + ] + } + ] + } + }, + "DriftComplianceChange4C0F2B82": { + "Type": "AWS::Events::Rule", + "Properties": { + "EventPattern": { + "source": [ + "aws.config" + ], + "detail": { + "configRuleName": [ + { + "Ref": "Drift34D3210F" + } + ] + }, + "detail-type": [ + "Config Rules Compliance Change" + ] + }, + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Ref": "ComplianceTopic0229448B" + }, + "Id": "ComplianceTopic" + } + ] + } + }, + "ComplianceTopic0229448B": { + "Type": "AWS::SNS::Topic" + }, + "ComplianceTopicPolicyF8BC46F0": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "events.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + }, + "Resource": { + "Ref": "ComplianceTopic0229448B" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "ComplianceTopic0229448B" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts b/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts new file mode 100644 index 0000000000000..db58cda3f4979 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/integ.rule.lit.ts @@ -0,0 +1,41 @@ +import targets = require('@aws-cdk/aws-events-targets'); +import lambda = require('@aws-cdk/aws-lambda'); +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import config = require('../lib'); + +const app = new cdk.App(); + +class ConfigStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /// !show + // A custom rule that runs on configuration changes of EC2 instances + const fn = new lambda.Function(this, 'CustomFunction', { + code: lambda.AssetCode.inline('exports.handler = (event) => console.log(event);'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + const customRule = new config.CustomRule(this, 'Custom', { + configurationChanges: true, + lambdaFunction: fn + }); + + customRule.scopeToResource('AWS::EC2::Instance'); + + // A rule to detect stacks drifts + const driftRule = new config.CloudFormationStackDriftDetectionCheck(this, 'Drift'); + + // Topic for compliance events + const complianceTopic = new sns.Topic(this, 'ComplianceTopic'); + + // Send notification on compliance change + driftRule.onComplianceChange('ComplianceChange', new targets.SnsTopic(complianceTopic)); + /// !hide + } +} + +new ConfigStack(app, 'aws-cdk-config-rule-integ'); +app.run(); diff --git a/packages/@aws-cdk/aws-config/test/test.config.ts b/packages/@aws-cdk/aws-config/test/test.config.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-config/test/test.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -}); diff --git a/packages/@aws-cdk/aws-config/test/test.managed-rules.ts b/packages/@aws-cdk/aws-config/test/test.managed-rules.ts new file mode 100644 index 0000000000000..6ff4ce2c02ee2 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/test.managed-rules.ts @@ -0,0 +1,143 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import sns = require('@aws-cdk/aws-sns'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import config = require('../lib'); + +export = { + 'access keys rotated'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new config.AccessKeysRotated(stack, 'AccessKeys'); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Source: { + Owner: 'AWS', + SourceIdentifier: 'ACCESS_KEYS_ROTATED' + }, + })); + + test.done(); + }, + + 'cloudformation stack drift detection check'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new config.CloudFormationStackDriftDetectionCheck(stack, 'Drift'); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Source: { + Owner: 'AWS', + SourceIdentifier: 'CLOUDFORMATION_STACK_DRIFT_DETECTION_CHECK' + }, + InputParameters: { + cloudformationRoleArn: { + 'Fn::GetAtt': [ + 'DriftRole8A5FB833', + 'Arn' + ] + } + }, + Scope: { + ComplianceResourceTypes: [ + 'AWS::CloudFormation::Stack' + ] + } + })); + + expect(stack).to(haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: { + 'Fn::Join': [ + '', + [ + 'config.', + { + Ref: 'AWS::URLSuffix' + } + ] + ] + } + } + } + ], + Version: '2012-10-17' + }, + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':iam::aws:policy/ReadOnlyAccess' + ] + ] + } + ] + })); + + test.done(); + }, + + 'cloudformation stack notification check'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic1 = new sns.Topic(stack, 'AllowedTopic1'); + const topic2 = new sns.Topic(stack, 'AllowedTopic2'); + + // WHEN + new config.CloudFormationStackNotificationCheck(stack, 'Notification', { + topics: [topic1, topic2] + }); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Source: { + Owner: 'AWS', + SourceIdentifier: 'CLOUDFORMATION_STACK_NOTIFICATION_CHECK' + }, + InputParameters: { + snsTopic1: { + Ref: 'AllowedTopic10C9144F9' + }, + snsTopic2: { + Ref: 'AllowedTopic24ECF6C0D' + } + }, + Scope: { + ComplianceResourceTypes: [ + 'AWS::CloudFormation::Stack' + ] + } + })); + + test.done(); + }, + + 'cloudformation stack notification check throws with more than 5 topics'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'AllowedTopic1'); + + // THEN + test.throws(() => new config.CloudFormationStackNotificationCheck(stack, 'Notification', { + topics: [topic, topic, topic, topic, topic, topic] + }), /5 topics/); + + test.done(); + } +}; diff --git a/packages/@aws-cdk/aws-config/test/test.rule.ts b/packages/@aws-cdk/aws-config/test/test.rule.ts new file mode 100644 index 0000000000000..38d8c9b2099f0 --- /dev/null +++ b/packages/@aws-cdk/aws-config/test/test.rule.ts @@ -0,0 +1,281 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import targets = require('@aws-cdk/aws-events-targets'); +import lambda = require('@aws-cdk/aws-lambda'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import config = require('../lib'); + +export = { + 'create a managed rule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new config.ManagedRule(stack, 'Rule', { + description: 'really cool rule', + identifier: 'AWS_SUPER_COOL', + inputParameters: { + key: 'value' + }, + maximumExecutionFrequency: config.MaximumExecutionFrequency.THREE_HOURS, + name: 'cool rule' + }); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Source: { + Owner: 'AWS', + SourceIdentifier: 'AWS_SUPER_COOL' + }, + ConfigRuleName: 'cool rule', + Description: 'really cool rule', + InputParameters: { + key: 'value' + }, + MaximumExecutionFrequency: 'Three_Hours' + })); + + test.done(); + }, + + 'create a custom rule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.AssetCode.inline('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + // WHEN + new config.CustomRule(stack, 'Rule', { + configurationChanges: true, + description: 'really cool rule', + inputParameters: { + key: 'value' + }, + lambdaFunction: fn, + maximumExecutionFrequency: config.MaximumExecutionFrequency.SIX_HOURS, + name: 'cool rule', + periodic: true + }); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Properties: { + Source: { + Owner: 'CUSTOM_LAMBDA', + SourceDetails: [ + { + EventSource: 'aws.config', + MessageType: 'ConfigurationItemChangeNotification' + }, + { + EventSource: 'aws.config', + MessageType: 'OversizedConfigurationItemChangeNotification' + }, + { + EventSource: 'aws.config', + MaximumExecutionFrequency: 'Six_Hours', + MessageType: 'ScheduledNotification' + } + ], + SourceIdentifier: { + 'Fn::GetAtt': [ + 'Function76856677', + 'Arn' + ] + } + }, + ConfigRuleName: 'cool rule', + Description: 'really cool rule', + InputParameters: { + key: 'value' + }, + MaximumExecutionFrequency: 'Six_Hours' + }, + DependsOn: [ + 'FunctionPermissionEC8FE997', + 'Function76856677', + 'FunctionServiceRole675BB04A' + ] + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(haveResource('AWS::Lambda::Permission', { + Principal: 'config.amazonaws.com' + })); + + expect(stack).to(haveResource('AWS::IAM::Role', { + ManagedPolicyArns: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + ] + ] + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition' + }, + ':iam::aws:policy/service-role/AWSConfigRulesExecutionRole' + ] + ] + } + ] + })); + + test.done(); + }, + + 'scope to resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const rule = new config.ManagedRule(stack, 'Rule', { + identifier: 'AWS_SUPER_COOL' + }); + + // WHEN + rule.scopeToResource('AWS::EC2::Instance', 'i-1234'); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Scope: { + ComplianceResourceId: 'i-1234', + ComplianceResourceTypes: [ + 'AWS::EC2::Instance' + ] + } + })); + + test.done(); + }, + + 'scope to resources'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const rule = new config.ManagedRule(stack, 'Rule', { + identifier: 'AWS_SUPER_COOL' + }); + + // WHEN + rule.scopeToResources('AWS::S3::Bucket', 'AWS::CloudFormation::Stack'); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Scope: { + ComplianceResourceTypes: [ + 'AWS::S3::Bucket', + 'AWS::CloudFormation::Stack' + ] + } + })); + + test.done(); + }, + + 'scope to tag'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const rule = new config.ManagedRule(stack, 'Rule', { + identifier: 'RULE' + }); + + // WHEN + rule.scopeToTag('key', 'value'); + + // THEN + expect(stack).to(haveResource('AWS::Config::ConfigRule', { + Scope: { + TagKey: 'key', + TagValue: 'value' + } + })); + + test.done(); + }, + + 'throws when scoping a custom rule without configuration changes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.AssetCode.inline('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + // WHEN + const rule = new config.CustomRule(stack, 'Rule', { + lambdaFunction: fn, + periodic: true + }); + + // THEN + test.throws(() => rule.scopeToResource('resource'), /`configurationChanges`/); + + test.done(); + }, + + 'throws when both configurationChanges and periodic are falsy'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const fn = new lambda.Function(stack, 'Function', { + code: lambda.AssetCode.inline('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + // THEN + test.throws(() => new config.CustomRule(stack, 'Rule', { + lambdaFunction: fn + }), /`configurationChanges`.*`periodic`/); + + test.done(); + }, + + 'on compliance change event'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const rule = new config.ManagedRule(stack, 'Rule', { + identifier: 'RULE' + }); + + const fn = new lambda.Function(stack, 'Function', { + code: lambda.Code.inline('dummy'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS810 + }); + + // WHEN + rule.onComplianceChange('ComplianceChange', new targets.LambdaFunction(fn)); + + expect(stack).to(haveResource('AWS::Events::Rule', { + EventPattern: { + 'source': [ + 'aws.config' + ], + 'detail': { + configRuleName: [ + { + Ref: 'Rule4C995B7F' + } + ] + }, + 'detail-type': [ + 'Config Rules Compliance Change' + ] + } + })); + + test.done(); + } +};