Skip to content

Commit

Permalink
feat: ability to conditionally ignore suppressions (#1214)
Browse files Browse the repository at this point in the history
Fixes #1010
  • Loading branch information
dontirun authored Mar 16, 2023
1 parent 4089cdb commit 6760c1b
Show file tree
Hide file tree
Showing 14 changed files with 1,316 additions and 256 deletions.
280 changes: 274 additions & 6 deletions API.md

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,30 @@ export class CdkTestStack extends Stack {

</details>

## Conditionally Ignoring Suppressions

You can optionally create a condition that prevents certain rules from being suppressed. You can create conditions for any variety of reasons. Examples include a condition that always ignores a suppression, a condition that ignores a suppression based on the date, a condition that ignores a suppression based on the reason. You can read [the developer docs](./docs/IgnoreSuppressionConditions.md) for more information on creating your own conditions.

<details>
<summary>Example) Using the pre-built `SuppressionIgnoreErrors` class to ignore suppressions on any `Error` level rules.</summary>

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

const app = new App();
new CdkTestStack(app, 'CdkNagDemo');
// Ignore Suppressions on any errors
Aspects.of(app).add(
new AwsSolutionsChecks({
suppressionIgnoreCondition: new SuppressionIgnoreErrors(),
})
);
```

</details>

## Using on CloudFormation templates

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.
Expand Down
138 changes: 138 additions & 0 deletions docs/IgnoreSuppressionConditions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<!--
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Conditionally Ignoring Suppressions

As a [NagPack](./NagPack.md) author or user, you can optionally create a condition that prevents certain rules from being suppressed. You can create conditions for any variety of reasons. Examples include a condition that always ignores a suppression, a condition that ignores a suppression based on the date, a condition that ignores a suppression based on the reason.

## Creating A Condition

Conditions implement the `INagSuppressionIgnore` interface. They return a message string when the `createMessage()` method is called. If the method returns a non-empty string the suppression is ignored. Conversely if the method returns an empty string the suppression is allowed.

Here is an example of a re-usable condition class that ignores a suppression if the suppression reason doesn't contain the word `Arun`

```ts
import { INagSuppressionIgnore, SuppressionIgnoreInput } from 'cdk-nag';
class ArunCondition implements INagSuppressionIgnore {
createMessage(input: SuppressionIgnoreInput) {
if (!input.reason.includes('Arun')) {
return 'The reason must contain the word Arun!';
}
return '';
}
}
```

You could also create the same condition without a class and by just implementing the interface

```ts
({
createMessage(input: SuppressionIgnoreInput) {
return !input.reason.includes('Arun')
? 'The reason must contain the word Arun!'
: '';
},
});
```

### Applying Conditions

There are 3 ways of applying conditions to rules. Users can have 1 way, they can supply an additional global condition that gets applied to all rules. `NagPack` authors have 2 ways, they can individually apply conditions to rules and/or apply a global condition to all rules. All present conditions are evaluated together using a `SuppressionIgnoreOr`(review [this section](#creating-complex-conditions) for more details on complex conditions).

Here is an example of a `NagPack` author applying both a global condition and an individual condition to the prebuilt `S3BucketSSLRequestsOnly` S3 Rule.

```ts
import { CfnResource } from 'aws-cdk-lib';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import {
NagMessageLevel,
NagPack,
NagPackProps,
NagSuppressions,
SuppressionIgnoreInput,
rules,
} from 'cdk-nag';
import { IConstruct } from 'constructs';

export class ExampleChecks extends NagPack {
constructor(props?: NagPackProps) {
super(props);
this.packName = 'Example';
this.packGlobalSuppressionIgnore = {
createMessage(input: SuppressionIgnoreInput) {
return !input.reason.includes('Arun')
? 'The reason must contain the word Arun!'
: '';
},
};
}
public visit(node: IConstruct): void {
if (node instanceof CfnResource) {
this.applyRule({
info: 'My brief info.',
explanation: 'My detailed explanation.',
level: NagMessageLevel.ERROR,
rule: rules.s3.S3BucketSSLRequestsOnly,
ignoreSuppressionCondition: {
createMessage(input: SuppressionIgnoreInput) {
return !input.reason.includes('Donti')
? 'The reason must contain the word Donti!'
: '';
},
},
node: node,
});
}
}
}
```

A user would see the following output when attempting to synthesize an application using a non-compliant suppression on a S3 Bucket

```bash
[Info at /Test/bucket/Resource] The suppression for Example-S3BucketSSLRequestsOnly was ignored for the following reason(s).
The reason must contain the word Arun!
The reason must contain the word Donti!
[Error at /Test/bucket/Resource] Example-S3BucketSSLRequestsOnly: My brief info.
```

## Creating Complex Conditions

`cdk-nag` exposes both a `SuppressionIgnoreAnd` class and a `SuppressionIgnoreOr` to help developers create more complicated conditions

- `SuppressionIgnoreAnd`: Ignores the suppression if **ALL** of the given INagSuppressionIgnore return a non-empty message (logical and)
- `SuppressionIgnoreOr`: Ignores the suppression if **ANY** of the given INagSuppressionIgnore return a non-empty message (logical or)

Here is an example `SuppressionIgnoreAnd` that ignores a suppression if both a 'ticket' CloudFormation metadata entry does not exist on the resource and the current year is after 2022.

```ts
import { SuppressionIgnoreAnd, SuppressionIgnoreInput } from 'cdk-nag';

new SuppressionIgnoreAnd(
{
createMessage(input: SuppressionIgnoreInput) {
return !input.resource.getMetadata('ticket')
? 'Must provide a ticket for an exception!'
: '';
},
},
{
createMessage(_input: SuppressionIgnoreInput) {
return Date.now() > Date.parse('31 Dec 2022 23:59 UTC')
? 'Suppressions are only allowed before the year 2023'
: '';
},
}
);
```

A user would see the following output when attempting to synthesize an application using a non-compliant suppression on a rule evaluating a S3 Bucket.

```bash
[Info at /Test/bucket/Resource] The suppression for Example-S3BucketSSLRequestsOnly was ignored for the following reason(s).
Must provide a ticket for an exception!
Suppressions are only allowed before the year 2023
[Error at /Test/bucket/Resource] Example-S3BucketSSLRequestsOnly: My brief info.
```
43 changes: 43 additions & 0 deletions docs/NagPack.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,49 @@ export class ExampleChecks extends NagPack {
}
```

### Ignoring Suppressions

You can optionally add a prebuilt or custom condition that prevents a rule from being suppressed. Below is an example of a condition that always prevents suppressions.
The documentation on [rules](./IgnoreSuppressionConditions.md) walks through the process of creating your own conditions.

```typescript
import { CfnResource } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
import {
NagMessageLevel,
NagPack,
NagPackProps,
NagRuleCompliance,
NagRuleResult,
NagRules,
SuppressionIgnoreAlways,
rules,
} from 'cdk-nag';

const ALWAYS_IGNORE = new SuppressionIgnoreAlways(
'Here is a reason for ignoring the suppression.'
);

export class ExampleChecks extends NagPack {
constructor(props?: NagPackProps) {
super(props);
this.packName = 'Example';
}
public visit(node: IConstruct): void {
if (node instanceof CfnResource) {
this.applyRule({
info: 'My brief info.',
explanation: 'My detailed explanation.',
level: NagMessageLevel.ERROR,
rule: rules.s3.S3BucketSSLRequestsOnly,
ignoreSuppressionCondition: ALWAYS_IGNORE,
node: node,
});
}
}
}
```

## Using a NagPack

You can apply as many `NagPacks` to a CDK Stack or Application via Aspects
Expand Down
122 changes: 122 additions & 0 deletions src/ignore-suppression-conditions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
import { CfnResource } from 'aws-cdk-lib';
import { NagMessageLevel } from './nag-pack';

/**
* Information about the NagRule and the relevant NagSuppression for the INagSuppressionIgnore
* @param resource The resource the suppression is applied to.
* @param reason The reason given for the suppression.
* @param ruleId The id of the rule to ignore.
* @param findingId The id of the finding that is being checked.
* @param ruleLevel The severity level of the rule.
*/
export interface SuppressionIgnoreInput {
readonly resource: CfnResource;
readonly reason: string;
readonly ruleId: string;
readonly findingId: string;
readonly ruleLevel: NagMessageLevel;
}

/**
* Interface for creating NagSuppression Ignores
*/
export interface INagSuppressionIgnore {
createMessage(input: SuppressionIgnoreInput): string;
}

/**
* Ignore the suppression if all of the given INagSuppressionIgnore return a non-empty message
*/
export class SuppressionIgnoreAnd implements INagSuppressionIgnore {
private andSuppressionIgnores: INagSuppressionIgnore[];

constructor(...SuppressionIgnoreAnds: INagSuppressionIgnore[]) {
if (SuppressionIgnoreAnds.length === 0) {
throw new Error(
'SuppressionIgnoreAnd needs at least one INagSuppressionIgnore'
);
}
this.andSuppressionIgnores = SuppressionIgnoreAnds;
}

createMessage(input: SuppressionIgnoreInput): string {
let messages = [];
for (const i of this.andSuppressionIgnores) {
const m = i.createMessage(input);
messages.push(m);
if (!m) {
return '';
}
}
return messages.join('\n\t');
}
}

/**
* Ignore the suppression if any of the given INagSuppressionIgnore return a non-empty message
*/
export class SuppressionIgnoreOr implements INagSuppressionIgnore {
private SuppressionIgnoreOrs: INagSuppressionIgnore[];

constructor(...orSuppressionIgnores: INagSuppressionIgnore[]) {
if (orSuppressionIgnores.length === 0) {
throw new Error(
'SuppressionIgnoreOr needs at least one INagSuppressionIgnore'
);
}
this.SuppressionIgnoreOrs = orSuppressionIgnores;
}

createMessage(input: SuppressionIgnoreInput): string {
let messages = [];
for (const i of this.SuppressionIgnoreOrs) {
const m = i.createMessage(input);
if (m) {
messages.push(m);
}
}
return messages ? messages.join('\n\t') : '';
}
}

/**
* Always ignore the suppression
*/
export class SuppressionIgnoreAlways implements INagSuppressionIgnore {
private triggerMessage: string;
constructor(triggerMessage: string) {
if (triggerMessage.length === 0) {
throw new Error(
'provide a triggerMessage for the SuppressionIgnoreAlways'
);
}
this.triggerMessage = triggerMessage;
}
createMessage(_input: SuppressionIgnoreInput): string {
return this.triggerMessage;
}
}

/**
* Don't ignore the suppression
*/
export class SuppressionIgnoreNever implements INagSuppressionIgnore {
createMessage(_input: SuppressionIgnoreInput): string {
return '';
}
}

/**
* Ignore Suppressions for Rules with a NagMessageLevel.ERROR
*/
export class SuppressionIgnoreErrors implements INagSuppressionIgnore {
createMessage(input: SuppressionIgnoreInput): string {
return input.ruleLevel == NagMessageLevel.ERROR
? `${input.ruleId} is categorized as an ERROR and may not be suppressed`
: '';
}
}
9 changes: 5 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
export * from './ignore-suppression-conditions';
export * from './models/nag-suppression';
export * from './nag-pack';
export * from './nag-rules';
export * from './nag-suppressions';
export * from './packs/aws-solutions';
export * from './packs/hipaa-security';
export * from './packs/nist-800-53-r4';
export * from './packs/nist-800-53-r5';
export * from './packs/pci-dss-321';
export * from './nag-pack';
export * from './nag-suppressions';
export * from './nag-rules';
export * from './models/nag-suppression';
export * as rules from './rules';
Loading

0 comments on commit 6760c1b

Please sign in to comment.