Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): add custom policy rule constructs #21794

Merged
merged 6 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions packages/@aws-cdk/aws-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Comment on lines +152 to +162
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we model these rules in a way that they can be implemented via code instead of just being blocks of code? I get that they're custom rules so there may be a lot of complexity there, so if it doesn't make sense, let me know.

Copy link
Contributor Author

@watany-dev watany-dev Sep 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry if my understanding is wrong. You need to attempt to parse the GuardDSL in the link below into a grammar in CDK.
https://github.com/aws-cloudformation/cloudformation-guard/blob/main/docs/CLAUSES.md

I am aware that this is a complex effort at this stage, and while it is great as a loadmap, would it be acceptable at this stage to work on it without a detailed parsing of the string as we do with other CustomRules?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only problem with that approach is that, if we're changing the contract in the future, that's a breaking change. I suppose we can leave the string option and/or deprecate it so that it's not technically breaking. Considering that this is how we implement else where, though, I suppose I'm fine with this.

`;

new config.CustomPolicy(stack, "Custom", {
policyText: samplePolicyText,
enableDebugLog: true,
ruleScope: config.RuleScope.fromResources([
config.ResourceType.DYNAMODB_TABLE,
]),
});
```

### Triggers

Expand Down
154 changes: 146 additions & 8 deletions packages/@aws-cdk/aws-config/lib/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,63 @@ export class ManagedRule extends RuleNew {
}
}

/**
* The source of the event, such as an AWS service,
* that triggers AWS Config to evaluate your AWS resources.
*/
enum EventSource {

/* from aws.config */
AWS_CONFIG = 'aws.config',

}

/**
* 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: EventSource;
/**
* 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.
*/
Expand Down Expand Up @@ -331,25 +388,24 @@ 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',
eventSource: EventSource.AWS_CONFIG,
messageType: MessageType.CONFIGURATION_ITEM_CHANGE_NOTIFICATION,
});
sourceDetails.push({
eventSource: 'aws.config',
messageType: 'OversizedConfigurationItemChangeNotification',
eventSource: EventSource.AWS_CONFIG,
messageType: MessageType.OVERSIZED_CONFIGURATION_ITEM_CHANGE_NOTIFICATION,
});
}

if (props.periodic) {
sourceDetails.push({
eventSource: 'aws.config',
eventSource: EventSource.AWS_CONFIG,
maximumExecutionFrequency: props.maximumExecutionFrequency,
messageType: 'ScheduledNotification',
messageType: MessageType.SCHEDULED_NOTIFICATION,
});
}

Expand Down Expand Up @@ -391,6 +447,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: EventSource.AWS_CONFIG,
messageType: MessageType.CONFIGURATION_ITEM_CHANGE_NOTIFICATION,
});
sourceDetails.push({
eventSource: 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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add an additional integration test that uses something like stackId where it needs to be produced late.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheRealAmazonKendra
This comment is actually still copied from CustomRule.

I am reading the documentation that might be relevant, but there doesn't appear to be an i/f where I can enter a stackid.
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_config.RuleScope.html
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_config.CloudFormationStackDriftDetectionCheck.html

As a suggestion, can we remove this misleading comment from CustomRule and CustomPolicy.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment looks accurately written. Render scope does the following

function renderScope(ruleScope?: RuleScope): CfnConfigRule.ScopeProperty | undefined {
  return ruleScope ? {
    complianceResourceId: ruleScope.resourceId,
    complianceResourceTypes: ruleScope.resourceTypes?.map(resource => resource.complianceResourceType),
    tagKey: ruleScope.key,
    tagValue: ruleScope.value,
  } : undefined;
}

So, if the resource that this scope is being assigned to doesn't yet have its resource id created at the time of this policy rule being declared, this needs to produce that information after that resource has been created. Basically, if this is declared.

This test can be written by creating a resource of some sort and then making the rule scope specific to that resource by adding the id as the second input for fromResource. It will have to be produced via Lazy.

source: {
owner: 'CUSTOM_POLICY',
sourceDetails,
customPolicyDetails: {
enableDebugLogDelivery: props.enableDebugLog,
policyRuntime: 'guard-2.x.x',
watany-dev marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"version": "21.0.0",
"files": {
"51dce3b1479f4a685a2f5a815b141fdf3e07e49181ce9da06750e820f5b92859": {
"source": {
"path": "aws-cdk-config-custompolicy.template.json",
"packaging": "file"
},
"destinations": {
"current_account-current_region": {
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
"objectKey": "51dce3b1479f4a685a2f5a815b141fdf3e07e49181ce9da06750e820f5b92859.json",
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
}
}
}
},
"dockerImages": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"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"
]
}
}
},
"sampleuser2D3A0B43": {
"Type": "AWS::IAM::User"
},
"Customlazy5E6C8AE4": {
"Type": "AWS::Config::ConfigRule",
"Properties": {
"Source": {
"CustomPolicyDetails": {
"EnableDebugLogDelivery": true,
"PolicyRuntime": "guard-2.x.x",
"PolicyText": "lazy-create-test"
},
"Owner": "CUSTOM_POLICY",
"SourceDetails": [
{
"EventSource": "aws.config",
"MessageType": "ConfigurationItemChangeNotification"
},
{
"EventSource": "aws.config",
"MessageType": "OversizedConfigurationItemChangeNotification"
}
]
},
"Scope": {
"ComplianceResourceId": {
"Ref": "sampleuser2D3A0B43"
},
"ComplianceResourceTypes": [
"AWS::IAM::User"
]
}
}
}
},
"Parameters": {
"BootstrapVersion": {
"Type": "AWS::SSM::Parameter::Value<String>",
"Default": "/cdk-bootstrap/hnb659fds/version",
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
}
},
"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."
}
]
}
}
}
Loading