DEV Community

Cover image for How to publish custom cdk-nag rules and rule packs with Projen

How to publish custom cdk-nag rules and rule packs with Projen

cdk-nag is a small tool for checking AWS CDK applications for (security) best practices. It provides rules and rule packs that can be applied to a CDK application. The rules are evaluated during cdk synth, which has the benefit of providing feedback to developers early in the development cycle. Similar capabilities can be implemented using the AWS Security Hub. However, cdk-nag has the advantage of finding issues before deployment.

cdk-nag provides five rules packs that can be used right out of the box:

  • AWS Solutions
  • HIPAA Security
  • NIST 800-53 rev 4
  • NIST 800-53 rev 5
  • PCI DSS 3.2.1

If one of these rule packs meets your organization's needs, you can directly use it. If you have custom rules, you can define your own rules and rule packs to make them available to all your projects. The documentation shows how to create them, but there is no information on how to publish them. So this blog post shows how to publish custom cdk-nag rules and rule packs.

Preconditions

You must have access to GitHub in order to create and use GitHub repositories. The Projen features used in this blog post are only available on GitHub.

To publish the cdk-nag ruleset, you must use an npm registry such as npmjs.com. In this blog post, the rule pack is published as a private npm package, which requires a paid npmjs account. If you are using a self-hosted npm registry, this is not a strict requirement.

Create a new Projen project

Projen is another open source project - it manages project configurations. Similar to AWS CDK, which creates CloudFormation templates, a Projen project is synthesized into project configurations, including GitHub actions, linter configuration, and so on.

cdk-nag rules and rule packs are published as a software library, so Projen provides everything needed to publish them. Also, cdk-nag itself uses Projen to publish new releases. The Projen type awscdk-construct contains everything needed to manage the project, including the GitHub workflows to publish new releases. With jsii it is also possible to publish releases in other programming languages like Java or Python. To create the new project, run this command:

npx projen new awscdk-construct
Enter fullscreen mode Exit fullscreen mode

Add cdk-nag dependency

First, add cdk-nag as new dependency in file .projenrc.ts. Insert the following line:

peerDeps: ['cdk-nag'],
Enter fullscreen mode Exit fullscreen mode

Then run npx projen to install it.

Implement a custom rule and rule pack

Now add one or more custom rules and a rule pack. See the documentation and the cdk-nag repository on GitHub for more information. As an example, use the following rule and rule pack.

Implement a rule that checks if SSE is enabled in S3 buckets. This is just an example - it probably makes more sense to use KMS encryption instead. Add a new file src/rules/S3DefaultEncryptionSSE.ts and paste the content:

import { parse } from 'path';
import { CfnResource, Stack } from 'aws-cdk-lib';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import { NagRuleCompliance, NagRules } from 'cdk-nag/lib';

/**
 * S3 Buckets are encrypted S3 SSE encryption
 * @param node the CfnResource to check
 */
export default Object.defineProperty(
  (node: CfnResource): NagRuleCompliance => {
    if (node instanceof CfnBucket) {
      if (node.bucketEncryption == undefined) {
        return NagRuleCompliance.NON_COMPLIANT;
      }
      const encryption = Stack.of(node).resolve(node.bucketEncryption);
      if (encryption.serverSideEncryptionConfiguration == undefined) {
        return NagRuleCompliance.NON_COMPLIANT;
      }
      const sse = Stack.of(node).resolve(
        encryption.serverSideEncryptionConfiguration,
      );
      for (const rule of sse) {
        const defaultEncryption = Stack.of(node).resolve(
          rule.serverSideEncryptionByDefault,
        );
        if (defaultEncryption == undefined) {
          return NagRuleCompliance.NON_COMPLIANT;
        }
        const sseAlgorithm = NagRules.resolveIfPrimitive(
          node,
          defaultEncryption.sseAlgorithm,
        );
        if (sseAlgorithm != 'AES256') {
          return NagRuleCompliance.NON_COMPLIANT;
        }
      }
      return NagRuleCompliance.COMPLIANT;
    } else {
      return NagRuleCompliance.NOT_APPLICABLE;
    }
  },
  'name',
  { value: parse(__filename).name },
);
Enter fullscreen mode Exit fullscreen mode

Also add a custom rule pack. This one contains a reference to the new rule and an existing rule from cdk-nag. Paste the contents into file src/MyCustomChecks.ts:

import { CfnResource } from 'aws-cdk-lib';
import { NagMessageLevel, NagPack, NagPackProps, rules } from 'cdk-nag';
import { IConstruct } from 'constructs';
import S3DefaultEncryptionSSE from './rules/S3DefaultEncryptionSSE';

export class MyCustomChecks extends NagPack {
  constructor(props?: NagPackProps) {
    super(props);
    this.packName = 'MyCustom';
  }
  public visit(node: IConstruct): void {
    if (node instanceof CfnResource) {

      this.applyRule({
        info: 'SSL not enabled in this bucket.',
        explanation: 'SSL must be enabled to encrypt traffic.',
        level: NagMessageLevel.ERROR,
        rule: rules.s3.S3BucketSSLRequestsOnly,
        node: node,
      });

      this.applyRule({
        info: 'SSE encryption not set for this bucket.',
        explanation: 'SSE encryption must be enabled to encrypt files in this bucket.',
        level: NagMessageLevel.ERROR,
        rule: S3DefaultEncryptionSSE,
        node: node,
      });

    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Replace the existing content in src/index.ts and export the two files created before.

export * from './MyCustomChecks';
export * from './rules/S3DefaultEncryptionSSE';
Enter fullscreen mode Exit fullscreen mode

It is also recommended to add some test cases. First remove the generated default example test case by deleting file test/hello.test.ts. For the rule, add a new test case in file test/S3DefaultEncryptionSSE.test.ts

import { App, Stack } from 'aws-cdk-lib';
import { CfnBucket } from 'aws-cdk-lib/aws-s3';
import S3DefaultEncryptionSSE from '../src/rules/S3DefaultEncryptionSSE';

test('Non-Complient Bucket', () => {
  const app = new App();
  const stack = new Stack(app, 'Stack');
  const bucket = new CfnBucket(stack, 'Bucket', {
  });
  expect(S3DefaultEncryptionSSE(bucket)).toBe('Non-Compliant');
});

test('Complient Bucket', () => {
  const app = new App();
  const stack = new Stack(app, 'Stack');
  const bucket = new CfnBucket(stack, 'Bucket', {
    bucketEncryption: {
      serverSideEncryptionConfiguration: [
        {
          serverSideEncryptionByDefault: {
            sseAlgorithm: 'AES256',
          },
        },
      ],
    },
  });
  expect(S3DefaultEncryptionSSE(bucket)).toBe('Compliant');
});
Enter fullscreen mode Exit fullscreen mode

To complete this step, run npx projen build to build and test the code. If successful, proceed to the next step.

Release to npmjs

To import the rule pack into another project, it must be published to npmjs. This will be done by Github actions generated by Projen. To release an internal library, add a scope to property name in .projenrc.ts. In my case, I added @jumic.

name: '@jumic/cdk-nag-custom-rules',
Enter fullscreen mode Exit fullscreen mode

Also, enable npm release and set access level to RESTRICTED.

releaseToNpm: true,
npmAccess: NpmAccess.RESTRICTED,
Enter fullscreen mode Exit fullscreen mode

At npmjs.com, open Access Tokens in the navigation and generate a new access token.
Generate access token

Enter a token name such as GitHub.
Token name

Define packages and scope.
Packages and scope

As a result, npmjs will display the generated access token. This needs to be copied to GitHub.

On GitHub, go to the repository settings for your repository. Create a new secret.
New repository secret

Enter the name NPM_TOKEN, which is the default name Projen expects. Then paste the access token.
Add secret

Once you have completed these configuration steps, commit your source code to the repository. The GitHub actions will start to build the project and publish it to npmjs.

Check the status on GitHub and make sure that the workflow is running successfully.

Check GitHub

Also, go to the npmjs packages and check if the release is available.
Check npmjs

If both checks pass, the custom cdk-nag rule package has been successfully released.

Adding your rule pack to a new CDK project

It's time to test the new rule pack. Create a new CDK project or use an existing CDK project. As this library was published as a private package, login to npmjs to access private packages:

npm login
Enter fullscreen mode Exit fullscreen mode

Now you can install the dependency to your project:

npm install @jumic/cdk-nag-custom-rules
Enter fullscreen mode Exit fullscreen mode

In the bin folder, add the new rule pack to the CDK application.

cdk.Aspects.of(app).add(new MyCustomChecks())
Enter fullscreen mode Exit fullscreen mode

Then execute cdk synth and it will show all invalid resources.
cdk synth

In case that all checks are passed, you will see the standard CDK output.

Summary

Publishing cdk-nag rule packs with Projen works great. It is a very useful combination of tools for publishing custom cdk-nag rule packs. Once configured, changes are continuously published to the registry.

Using CDK in multiple programming languages? Just add more jsii targets to .projenrc.ts to publish packages for Python, Java, ... (see Projen documentation).

Full source code on GitHub

Top comments (0)