Skip to content

Commit

Permalink
feat(Nag Suppressions): granular suppressions (cdklabs#655)
Browse files Browse the repository at this point in the history
Closes cdklabs#634

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
dontirun authored Feb 24, 2022
1 parent bcf4aa6 commit 4c6579e
Show file tree
Hide file tree
Showing 16 changed files with 592 additions and 149 deletions.
15 changes: 9 additions & 6 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,31 +285,33 @@ protected createComplianceReportLine(params: IApplyRule, ruleId: string, complia
__Returns__:
* <code>string</code>

#### protected createMessage(ruleId, info, explanation) <a id="cdk-nag-nagpack-createmessage"></a>
#### protected createMessage(ruleId, findingId, info, explanation) <a id="cdk-nag-nagpack-createmessage"></a>

The message to output to the console when a rule is triggered.

```ts
protected createMessage(ruleId: string, info: string, explanation: string): string
protected createMessage(ruleId: string, findingId: string, info: string, explanation: string): string
```

* **ruleId** (<code>string</code>) The id of the rule.
* **findingId** (<code>string</code>) The id of the finding.
* **info** (<code>string</code>) Why the rule was triggered.
* **explanation** (<code>string</code>) Why the rule exists.

__Returns__:
* <code>string</code>

#### protected ignoreRule(ignores, ruleId) <a id="cdk-nag-nagpack-ignorerule"></a>
#### protected ignoreRule(ignores, ruleId, findingId) <a id="cdk-nag-nagpack-ignorerule"></a>

Check whether a specific rule should be ignored.

```ts
protected ignoreRule(ignores: Array<NagPackSuppression>, ruleId: string): string
protected ignoreRule(ignores: Array<NagPackSuppression>, ruleId: string, findingId: string): string
```

* **ignores** (<code>Array<[NagPackSuppression](#cdk-nag-nagpacksuppression)></code>) The ignores listed in cdk-nag metadata.
* **ruleId** (<code>string</code>) The id of the rule to ignore.
* **findingId** (<code>string</code>) The id of the finding that is being checked.

__Returns__:
* <code>string</code>
Expand Down Expand Up @@ -514,13 +516,13 @@ Name | Type | Description
The callback to the rule.

```ts
rule(node: CfnResource): NagRuleCompliance
rule(node: CfnResource): NagRuleCompliance &#124; Array<string>
```

* **node** (<code>[CfnResource](#aws-cdk-lib-cfnresource)</code>) The CfnResource to check.

__Returns__:
* <code>[NagRuleCompliance](#cdk-nag-nagrulecompliance)</code>
* <code>[NagRuleCompliance](#cdk-nag-nagrulecompliance) &#124; Array<string></code>



Expand Down Expand Up @@ -550,6 +552,7 @@ Name | Type | Description
-----|------|-------------
**id** | <code>string</code> | The id of the rule to ignore.
**reason** | <code>string</code> | The reason to ignore the rule (minimum 10 characters).
**appliesTo**? | <code>Array<string></code> | Rule specific granular suppressions.<br/>__*Optional*__



Expand Down
187 changes: 180 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ Aspects.of(app).add(new AwsSolutionsChecks());

```typescript
import { SecurityGroup, Vpc, Peer, Port } from 'aws-cdk-lib/aws-ec2';
import { Construct, Stack, StackProps } from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NagSuppressions } from 'cdk-nag';

export class CdkTestStack extends Stack {
Expand All @@ -119,7 +120,8 @@ export class CdkTestStack extends Stack {

```typescript
import { User, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Construct, Stack, StackProps } from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NagSuppressions } from 'cdk-nag';

export class CdkTestStack extends Stack {
Expand All @@ -135,7 +137,13 @@ export class CdkTestStack extends Stack {
// Enable adding suppressions to child constructs
NagSuppressions.addResourceSuppressions(
user,
[{ id: 'AwsSolutions-IAM5', reason: 'lorem ipsum' }],
[
{
id: 'AwsSolutions-IAM5',
reason: 'lorem ipsum',
appliesTo: ['Resource::arn:aws:s3:::bucket_name/*'], // optional
},
],
true
);
}
Expand Down Expand Up @@ -174,8 +182,9 @@ If you received the following error on synth/deploy
```typescript
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { BucketDeployment } from 'aws-cdk-lib/aws-s3-deployment';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NagSuppressions } from 'cdk-nag';
import { Construct, Stack, StackProps } from 'aws-cdk-lib';

export class CdkTestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
Expand All @@ -195,6 +204,77 @@ export class CdkTestStack extends Stack {

</details>

<details>
<summary>Example 5) Granular Suppressions of findings</summary>

Certain rules support granular suppressions of `findings`. If you received the following errors on synth/deploy

```bash
[Error at /StackName/rFirstUser/DefaultPolicy/Resource] AwsSolutions-IAM5[Action::s3:*]: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission.
[Error at /StackName/rFirstUser/DefaultPolicy/Resource] AwsSolutions-IAM5[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission.
[Error at /StackName/rSecondUser/DefaultPolicy/Resource] AwsSolutions-IAM5[Action::s3:*]: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission.
[Error at /StackName/rSecondUser/DefaultPolicy/Resource] AwsSolutions-IAM5[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission.
```

By applying the following suppressions

```typescript
import { User } from 'aws-cdk-lib/aws-iam';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NagSuppressions } from 'cdk-nag';

export class CdkTestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const firstUser = new User(this, 'rFirstUser');
firstUser.addToPolicy(
new PolicyStatement({
actions: ['s3:*'],
resources: ['*'],
})
);
const secondUser = new User(this, 'rSecondUser');
secondUser.addToPolicy(
new PolicyStatement({
actions: ['s3:*'],
resources: ['*'],
})
);
NagSuppressions.addResourceSuppressions(
firstUser,
[
{
id: 'AwsSolutions-IAM5',
reason:
"Only suppress AwsSolutions-IAM5 's3:*' finding on First User.",
appliesTo: ['Action::s3:*'],
},
],
true
);
NagSuppressions.addResourceSuppressions(
secondUser,
[
{
id: 'AwsSolutions-IAM5',
reason: 'Suppress all AwsSolutions-IAM5 findings on Second User.',
},
],
true
);
}
}
```

You would see the following error on synth/deploy

```bash
[Error at /StackName/rFirstUser/DefaultPolicy/Resource] AwsSolutions-IAM5[Resource::*]: The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission.
```

</details>

## Rules and Property Overrides

In some cases L2 Constructs do not have a native option to remediate an issue and must be fixed via [Raw Overrides](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html#cfn_layer_raw). Since raw overrides take place after template synthesis these fixes are not caught by the cdk_nag. In this case you should remediate the issue and suppress the issue like in the following example.
Expand All @@ -211,7 +291,8 @@ import {
Vpc,
CfnInstance,
} from 'aws-cdk-lib/aws-ec2';
import { Construct, Stack, StackProps } from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { NagSuppressions } from 'cdk-nag';

export class CdkTestStack extends Stack {
Expand Down Expand Up @@ -241,7 +322,7 @@ export class CdkTestStack extends Stack {
You can use cdk-nag on existing CloudFormation templates by using the [cloudformation-include](https://docs.aws.amazon.com/cdk/latest/guide/use_cfn_template.html#use_cfn_template_install) module.

<details>
<summary>Example) CloudFormation template with suppression</summary>
<summary>Example 1) CloudFormation template with suppression</summary>

Sample CloudFormation template with suppression

Expand Down Expand Up @@ -285,7 +366,8 @@ Sample Stack with imported template
```typescript
import { CfnInclude } from 'aws-cdk-lib/cloudformation-include';
import { NagSuppressions } from 'cdk-nag';
import { Construct, Stack, StackProps } from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkTestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
Expand All @@ -310,6 +392,97 @@ export class CdkTestStack extends Stack {

</details>

<details>
<summary>Example 2) CloudFormation template with granular suppressions</summary>

Sample CloudFormation template with suppression

```json
{
"Resources": {
"myPolicy": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"kms:Decrypt",
"kms:DescribeKey",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*"
],
"Effect": "Allow",
"Resource": ["some-key-arn"]
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"cdk_nag": {
"rules_to_suppress": [
{
"id": "AwsSolutions-IAM5",
"reason": "Allow key data access",
"applies_to": [
"Action::kms:ReEncrypt*",
"Action::kms:GenerateDataKey*"
]
}
]
}
}
}
}
}
```

Sample App

```typescript
import { App, Aspects } from 'aws-cdk-lib';
import { CdkTestStack } from '../lib/cdk-test-stack';
import { AwsSolutionsChecks } from 'cdk-nag';

const app = new App();
new CdkTestStack(app, 'CdkNagDemo');
Aspects.of(app).add(new AwsSolutionsChecks());
```

Sample Stack with imported template

```typescript
import { CfnInclude } from 'aws-cdk-lib/cloudformation-include';
import { NagSuppressions } from 'cdk-nag';
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkTestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
new CfnInclude(this, 'Template', {
templateFile: 'my-template.json',
});
// Add any additional suppressions
NagSuppressions.addResourceSuppressionsByPath(
this,
'/CdkNagDemo/Template/myPolicy',
[
{
id: 'AwsSolutions-IAM5',
reason: 'Allow key data access',
appliesTo: ['Action::kms:ReEncrypt*', 'Action::kms:GenerateDataKey*'],
},
]
);
}
}
```

</details>

## Contributing

See [CONTRIBUTING](./CONTRIBUTING.md) for more information.
Expand Down
2 changes: 1 addition & 1 deletion RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The [AWS Solutions Library](https://aws.amazon.com/solutions/) offers a collecti
| AwsSolutions-EMR6 | The EMR cluster does not implement authentication via an EC2 Key Pair or Kerberos. | SSH clients can use an EC2 key pair to authenticate to cluster instances. Alternatively, with EMR release version 5.10.0 or later, solutions can configure Kerberos to authenticate users and SSH connections to the master node. |
| AwsSolutions-EVB1 | The event bus policy allows for open access. | An open policy ("\*" principal without a condition) grants anonymous access to an event bus. Use a condition to limit the permission to accounts that fulfill a certain requirement, such as being a member of a certain AWS organization. |
| AwsSolutions-IAM4 | The IAM user, role, or group uses AWS managed policies. | An AWS managed policy is a standalone policy that is created and administered by AWS. Currently, many AWS managed policies do not restrict resource scope. Replace AWS managed policies with system specific (customer) managed policies. |
| AwsSolutions-IAM5 | The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. | Metadata explaining the evidence (e.g. via supporting links) for wildcard permissions allows for transparency to operators. |
| AwsSolutions-IAM5 | The IAM entity contains wildcard permissions and does not have a cdk_nag rule suppression with evidence for those permission. | Metadata explaining the evidence (e.g. via supporting links) for wildcard permissions allows for transparency to operators. This is a granular rule that returns individual findings that can be suppressed with `appliesTo`. The findings are in the format `Action::<action>` for policy actions and `Resource::<resource>` for resources. Example: `appliesTo: ['Action::s3:*']`. |
| AwsSolutions-KDF1 | The Kinesis Data Firehose delivery stream does have server-side encryption enabled. | This allows the system to meet strict regulatory requirements and enhance the security of system data. |
| AwsSolutions-KDS1 | The Kinesis Data Stream does not has server-side encryption enabled. | Data is encrypted before it's written to the Kinesis stream storage layer, and decrypted after it’s retrieved from storage. This allows the system to meet strict regulatory requirements and enhance the security of system data. |
| AwsSolutions-KMS5 | The KMS Symmetric key does not have automatic key rotation enabled. | KMS key rotation allow a system to set an yearly rotation schedule for a KMS key so when a AWS KMS key is required to encrypt new data, the KMS service can automatically use the latest version of the HSA backing key to perform the encryption. |
Expand Down
3 changes: 2 additions & 1 deletion docs/NagPack.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
NagPack,
NagPackProps,
NagRuleCompliance,
NagRuleResult,
NagRules,
rules,
} from 'cdk-nag';
Expand Down Expand Up @@ -75,7 +76,7 @@ export class ExampleChecks extends NagPack {
explanation:
'This rule does not prevent deployment unless level is set to NagMessageLevel.ERROR.',
level: NagMessageLevel.WARN,
rule: function (node2: CfnResource): NagRuleCompliance {
rule: function (node2: CfnResource): NagRuleResult {
if (node2 instanceof CfnReplicationInstance) {
const publicAccess = NagRules.resolveIfPrimitive(
node2,
Expand Down
19 changes: 11 additions & 8 deletions docs/RuleCreation.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,36 @@ A rule contains an assertion to make against each individual resource in your CD

## Anatomy of a Rule

A rule returns a `NagRuleCompliance` status.
A rule returns a `NagRuleResult` which is either a `NagRuleCompliance` status or a list of findings.

- `NON_COMPLIANT` - The resource that **does not meet** the requirements.
- `COMPLIANT` - The resource that **meets** the requirements.
- `NOT_APPLICABLE` - The rule **does not apply** to the given resource.
- `NagRuleCompliance.NON_COMPLIANT` - The resource that **does not meet** the requirements.
- `NagRuleCompliance.COMPLIANT` - The resource that **meets** the requirements.
- `NagRuleCompliance.NOT_APPLICABLE` - The rule **does not apply** to the given resource.
- Ex. The current resource is a S3 Bucket but the rule is for validating DMS Replication Instances.
- `NagRuleFindings` A a string array with a list of all findings.

```typescript
// CDK v2
import { CfnResource } from 'aws-cdk-lib';
// CDK v1
// import { CfnResource } from '@aws-cdk/core';
import { NagRuleCompliance, NagRules } from 'cdk-nag';
import { NagRuleCompliance, NagRuleResult, NagRules } from 'cdk-nag';
import { CfnReplicationInstance } from 'aws-cdk-lib/aws-dms';

/**
* DMS replication instances are not public
* @param node the CfnResource to check
*/
export function myRule(node: CfnResource): NagRuleCompliance {
export function myRule(node: CfnResource): NagRuleResult {
if (node instanceof CfnReplicationInstance) {
const publicAccess = NagRules.resolveIfPrimitive(
node,
node.publiclyAccessible
);
if (publicAccess !== false) {
return NagRuleCompliance.NON_COMPLIANT;
// or, if your rule returns multiple violations
// return ['publicAccess', ...]
}
return NagRuleCompliance.COMPLIANT;
} else {
Expand Down Expand Up @@ -70,10 +73,10 @@ You can use the [Object.defineProperty()](https://developer.mozilla.org/en-US/do
import { CfnResource } from 'aws-cdk-lib';
// CDK v1
// import { CfnResource } from '@aws-cdk/core';
import { NagRuleCompliance, NagRules } from 'cdk-nag';
import { NagRuleCompliance, NagRuleResult, NagRules } from 'cdk-nag';
import { CfnReplicationInstance } from 'aws-cdk-lib/aws-dms';

export function myRule(node: CfnResource): NagRuleCompliance {
export function myRule(node: CfnResource): NagRuleResult {
if (node instanceof CfnReplicationInstance) {
const publicAccess = NagRules.resolveIfPrimitive(
node,
Expand Down
Loading

0 comments on commit 4c6579e

Please sign in to comment.