-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ability to conditionally ignore suppressions (#1214)
Fixes #1010
- Loading branch information
Showing
14 changed files
with
1,316 additions
and
256 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
: ''; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.