Repository: awslabs/aws-ddk (language: python)

Issue:
(core) fail security check codebuild if cfnnag has at least 1 failure
### Describe the bug

[Cfn-nag action](https://github.com/awslabs/aws-ddk/blob/main/core/aws_ddk_core/cicd/actions.py#L149) runs against every file but due to that does not fail the CodeBuild if any failures found.

### How to Reproduce

```
*P.S. Please do not attach files as it's considered a security risk. Add code snippets directly in the message body as much as possible.*
```


### Expected behavior

- Fail the CodeBuild if scan results report failures
- Provide an option to suppress the failures

### Your project

_No response_

### Screenshots

_No response_

### Environment

```
Provide your `pip list` output, particularly the version of the AWS DDK library you used. Providing this information may significantly improve resolution times.
```


### OS

Mac

### Python version

3.9

### AWS DDK version

0.5.1

### Additional context

_No response_


Discussion / hints:
Marking this issue as stale due to inactivity. This helps our maintainers find and focus on the active issues. If this issue receives no comments in the next 7 days it will automatically be closed.

Fix patch:
diff --git a/API.md b/API.md
index 2a7bdccc..3b47ab89 100644
--- a/API.md
+++ b/API.md
@@ -5819,11 +5819,24 @@ const addSecurityLintStageProps: AddSecurityLintStageProps = { ... }
 
 | **Name** | **Type** | **Description** |
 | --- | --- | --- |
+| <code><a href="#aws-ddk-core.AddSecurityLintStageProps.property.cfnNagFailBuild">cfnNagFailBuild</a></code> | <code>boolean</code> | Fail Codepipeline Build Action on failed results from CfnNag scan. |
 | <code><a href="#aws-ddk-core.AddSecurityLintStageProps.property.cloudAssemblyFileSet">cloudAssemblyFileSet</a></code> | <code>aws-cdk-lib.pipelines.IFileSetProducer</code> | Cloud assembly file set producer. |
 | <code><a href="#aws-ddk-core.AddSecurityLintStageProps.property.stageName">stageName</a></code> | <code>string</code> | Name of the stage. |
 
 ---
 
+##### `cfnNagFailBuild`<sup>Optional</sup> <a name="cfnNagFailBuild" id="aws-ddk-core.AddSecurityLintStageProps.property.cfnNagFailBuild"></a>
+
+```typescript
+public readonly cfnNagFailBuild: boolean;
+```
+
+- *Type:* boolean
+
+Fail Codepipeline Build Action on failed results from CfnNag scan.
+
+---
+
 ##### `cloudAssemblyFileSet`<sup>Optional</sup> <a name="cloudAssemblyFileSet" id="aws-ddk-core.AddSecurityLintStageProps.property.cloudAssemblyFileSet"></a>
 
 ```typescript
@@ -10804,7 +10817,7 @@ CICDActions.getBanditAction(codePipelineSource: CodePipelineSource, stageName?:
 ```typescript
 import { CICDActions } from 'aws-ddk-core'
 
-CICDActions.getCfnNagAction(fileSetProducer: IFileSetProducer, stageName?: string)
+CICDActions.getCfnNagAction(fileSetProducer: IFileSetProducer, stageName?: string, failBuild?: boolean)
 ```
 
 ###### `fileSetProducer`<sup>Required</sup> <a name="fileSetProducer" id="aws-ddk-core.CICDActions.getCfnNagAction.parameter.fileSetProducer"></a>
@@ -10819,6 +10832,12 @@ CICDActions.getCfnNagAction(fileSetProducer: IFileSetProducer, stageName?: strin
 
 ---
 
+###### `failBuild`<sup>Optional</sup> <a name="failBuild" id="aws-ddk-core.CICDActions.getCfnNagAction.parameter.failBuild"></a>
+
+- *Type:* boolean
+
+---
+
 ##### `getCodeArtifactPublishAction` <a name="getCodeArtifactPublishAction" id="aws-ddk-core.CICDActions.getCodeArtifactPublishAction"></a>
 
 ```typescript
diff --git a/src/cicd/actions.ts b/src/cicd/actions.ts
index ba3db448..7d85f44c 100644
--- a/src/cicd/actions.ts
+++ b/src/cicd/actions.ts
@@ -71,13 +71,17 @@ export class CICDActions {
   public static getCfnNagAction(
     fileSetProducer: pipelines.IFileSetProducer,
     stageName: string = "CFNNag",
+    failBuild?: boolean,
   ): pipelines.ShellStep {
     return new pipelines.ShellStep(stageName, {
       input: fileSetProducer,
       installCommands: ["gem install cfn-nag"],
+      env: {
+        FAIL_BUILD: failBuild ? "true" : "false",
+      },
       commands: [
-        'fnames=$(find ./ -type f -name "*.template.json")',
-        "for f in $fnames; do cfn_nag_scan --input-path $f; done",
+        'cfn_nag_scan --input-path ./ --template-pattern ".*.template.json" && scan_result="SUCCESS" || echo scan_result="FAILED"',
+        'if [[ "$FAIL_BUILD" = "true" && "$scan_result" = "FAILED" ]]; then printf "\n\nFailing pipeline as possible insecure configurations were detected\n\n" && exit 1; fi',
       ],
     });
   }
diff --git a/src/cicd/pipelines.ts b/src/cicd/pipelines.ts
index 0e7b094d..a33d85bd 100644
--- a/src/cicd/pipelines.ts
+++ b/src/cicd/pipelines.ts
@@ -115,6 +115,10 @@ export interface AddSecurityLintStageProps {
    * Cloud assembly file set producer.
    */
   readonly cloudAssemblyFileSet?: pipelines.IFileSetProducer;
+  /**
+   * Fail Codepipeline Build Action on failed results from CfnNag scan.
+   */
+  readonly cfnNagFailBuild?: boolean;
 }
 
 /**
@@ -455,7 +459,10 @@ export class CICDPipelineStack extends BaseStack {
     var cloudAssemblyFileSet = props.cloudAssemblyFileSet ?? this.pipeline?.cloudAssemblyFileSet;
 
     this.pipeline?.addWave(stageName, {
-      post: [CICDActions.getCfnNagAction(cloudAssemblyFileSet), CICDActions.getBanditAction(this.sourceAction)],
+      post: [
+        CICDActions.getCfnNagAction(cloudAssemblyFileSet, "CFNNag", props.cfnNagFailBuild),
+        CICDActions.getBanditAction(this.sourceAction),
+      ],
     });
 
     return this;


Test patch:
diff --git a/test/cicd-pipeline-stack.test.ts b/test/cicd-pipeline-stack.test.ts
index 57af5471..c2ae1407 100644
--- a/test/cicd-pipeline-stack.test.ts
+++ b/test/cicd-pipeline-stack.test.ts
@@ -219,6 +219,60 @@ test("CICD Pipeline with Security Checks", () => {
   });
 });
 
+test("CICD Pipeline with Cfn Nag Action Set to Fail", () => {
+  const app = new cdk.App();
+  const stack = new CICDPipelineStack(app, "dummy-pipeline", { environmentId: "dev", pipelineName: "dummy-pipeline" })
+    .addSourceAction({ repositoryName: "dummy-repository" })
+    .addSynthAction()
+    .buildPipeline()
+    .addSecurityLintStage({ cfnNagFailBuild: true })
+    .synth();
+
+  const template = Template.fromStack(stack);
+  template.hasResourceProperties("AWS::CodeBuild::Project", {
+    Artifacts: {
+      Type: "CODEPIPELINE",
+    },
+    Environment: {
+      ComputeType: "BUILD_GENERAL1_SMALL",
+      Image: "aws/codebuild/standard:6.0",
+      ImagePullCredentialsType: "CODEBUILD",
+      PrivilegedMode: false,
+      Type: "LINUX_CONTAINER",
+      EnvironmentVariables: [
+        {
+          Name: "FAIL_BUILD",
+          Type: "PLAINTEXT",
+          Value: "true",
+        },
+      ],
+    },
+  });
+  template.hasResourceProperties("AWS::CodePipeline::Pipeline", {
+    Stages: Match.arrayWith([
+      Match.objectLike({
+        Name: "SecurityLint",
+        Actions: Match.arrayWith([
+          Match.objectLike({
+            Name: "Bandit",
+            ActionTypeId: {
+              Category: "Build",
+              Provider: "CodeBuild",
+            },
+          }),
+          Match.objectLike({
+            Name: "CFNNag",
+            ActionTypeId: {
+              Category: "Build",
+              Provider: "CodeBuild",
+            },
+          }),
+        ]),
+      }),
+    ]),
+  });
+});
+
 test("CICD Pipeline with Custom Stage", () => {
   const app = new cdk.App();
   const stack = new CICDPipelineStack(app, "dummy-pipeline", { environmentId: "dev", pipelineName: "dummy-pipeline" })