|
| 1 | +import { |
| 2 | + afterEach, |
| 3 | + describe, |
| 4 | + expect, |
| 5 | + test, |
| 6 | + vi |
| 7 | +} from "vitest" |
| 8 | +import {Stack, CfnResource} from "aws-cdk-lib" |
| 9 | +import {Code, Function as LambdaFunction, Runtime} from "aws-cdk-lib/aws-lambda" |
| 10 | +import {NagPackSuppression, NagSuppressions} from "cdk-nag" |
| 11 | + |
| 12 | +import * as helpers from "../../src/utils/helpers" |
| 13 | + |
| 14 | +const defaultSuppressionRules = ["LAMBDA_DLQ_CHECK", "LAMBDA_INSIDE_VPC", "LAMBDA_CONCURRENCY_CHECK"] |
| 15 | + |
| 16 | +const createResource = (stack: Stack, id: string, type = "Custom::Test", path?: string): CfnResource => { |
| 17 | + const resource = new CfnResource(stack, id, {type, properties: {}}) |
| 18 | + resource.cfnOptions.metadata = { |
| 19 | + ...(resource.cfnOptions.metadata ?? {}), |
| 20 | + "aws:cdk:path": path ?? `${stack.stackName}/${id}` |
| 21 | + } |
| 22 | + return resource |
| 23 | +} |
| 24 | + |
| 25 | +afterEach(() => { |
| 26 | + vi.restoreAllMocks() |
| 27 | +}) |
| 28 | + |
| 29 | +describe("findCloudFormationResourcesByPath", () => { |
| 30 | + test("returns unique matches for the provided metadata paths", () => { |
| 31 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 32 | + const first = createResource(stack, "First", "Custom::Foo", "match/one") |
| 33 | + const second = createResource(stack, "Second", "Custom::Foo", "match/two") |
| 34 | + createResource(stack, "Third", "Custom::Foo", "nope") |
| 35 | + |
| 36 | + const matches = helpers.findCloudFormationResourcesByPath(stack, ["match/one", "match/one", "match/two"]) |
| 37 | + |
| 38 | + expect(matches).toEqual([first, second]) |
| 39 | + }) |
| 40 | +}) |
| 41 | + |
| 42 | +describe("findCloudFormationResourcesByType", () => { |
| 43 | + test("returns every resource whose CloudFormation type matches", () => { |
| 44 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 45 | + const fooOne = createResource(stack, "FooOne", "Custom::Foo") |
| 46 | + const fooTwo = createResource(stack, "FooTwo", "Custom::Foo") |
| 47 | + createResource(stack, "Bar", "Custom::Bar") |
| 48 | + |
| 49 | + const matches = helpers.findCloudFormationResourcesByType(stack, "Custom::Foo") |
| 50 | + |
| 51 | + expect(matches).toEqual([fooOne, fooTwo]) |
| 52 | + }) |
| 53 | +}) |
| 54 | + |
| 55 | +describe("addSuppressions", () => { |
| 56 | + test("merges new rules, deduplicates them, and creates metadata when missing", () => { |
| 57 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 58 | + const existing = createResource(stack, "Existing") |
| 59 | + existing.cfnOptions.metadata = { |
| 60 | + ...existing.cfnOptions.metadata, |
| 61 | + guard: {SuppressedRules: ["EXISTING", "SHARED"]} |
| 62 | + } |
| 63 | + const empty = createResource(stack, "Empty") |
| 64 | + |
| 65 | + helpers.addSuppressions([existing, empty], ["SHARED", "NEW"]) |
| 66 | + |
| 67 | + expect(existing.cfnOptions.metadata?.guard?.SuppressedRules).toEqual(["EXISTING", "SHARED", "NEW"]) |
| 68 | + expect(empty.cfnOptions.metadata?.guard?.SuppressedRules).toEqual(["SHARED", "NEW"]) |
| 69 | + }) |
| 70 | +}) |
| 71 | + |
| 72 | +describe("addLambdaCfnGuardSuppressions", () => { |
| 73 | + test("applies the default lambda suppressions to every lambda in the stack", () => { |
| 74 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 75 | + const lambdaOne = new LambdaFunction(stack, "LambdaOne", { |
| 76 | + runtime: Runtime.NODEJS_18_X, |
| 77 | + handler: "index.handler", |
| 78 | + code: Code.fromInline("exports.handler = async () => {};") |
| 79 | + }) |
| 80 | + const lambdaTwo = new LambdaFunction(stack, "LambdaTwo", { |
| 81 | + runtime: Runtime.NODEJS_18_X, |
| 82 | + handler: "index.handler", |
| 83 | + code: Code.fromInline("exports.handler = async () => {};") |
| 84 | + }) |
| 85 | + |
| 86 | + helpers.addLambdaCfnGuardSuppressions(stack) |
| 87 | + |
| 88 | + const firstCfn = lambdaOne.node.defaultChild as CfnResource |
| 89 | + const secondCfn = lambdaTwo.node.defaultChild as CfnResource |
| 90 | + expect(firstCfn.cfnOptions.metadata?.guard?.SuppressedRules).toEqual(defaultSuppressionRules) |
| 91 | + expect(secondCfn.cfnOptions.metadata?.guard?.SuppressedRules).toEqual(defaultSuppressionRules) |
| 92 | + }) |
| 93 | +}) |
| 94 | + |
| 95 | +describe("safeAddNagSuppressionGroup", () => { |
| 96 | + test("invokes cdk-nag for every provided path", () => { |
| 97 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 98 | + const suppressions: Array<NagPackSuppression> = [{id: "RULE", reason: "already covered"}] |
| 99 | + const spy = vi.spyOn(NagSuppressions, "addResourceSuppressionsByPath").mockImplementation(() => {}) |
| 100 | + |
| 101 | + helpers.safeAddNagSuppressionGroup(stack, ["one", "two"], suppressions) |
| 102 | + |
| 103 | + expect(spy).toHaveBeenCalledTimes(2) |
| 104 | + expect(spy).toHaveBeenNthCalledWith(1, stack, "one", suppressions) |
| 105 | + expect(spy).toHaveBeenNthCalledWith(2, stack, "two", suppressions) |
| 106 | + }) |
| 107 | +}) |
| 108 | + |
| 109 | +describe("safeAddNagSuppression", () => { |
| 110 | + const sampleSuppressions: Array<NagPackSuppression> = [{id: "RULE", reason: "covered elsewhere"}] |
| 111 | + |
| 112 | + test("routes suppressions to cdk-nag", () => { |
| 113 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 114 | + const spy = vi.spyOn(NagSuppressions, "addResourceSuppressionsByPath").mockImplementation(() => {}) |
| 115 | + |
| 116 | + helpers.safeAddNagSuppression(stack, "path/to/resource", sampleSuppressions) |
| 117 | + |
| 118 | + expect(spy).toHaveBeenCalledWith(stack, "path/to/resource", sampleSuppressions) |
| 119 | + }) |
| 120 | + |
| 121 | + test("logs and swallows errors when the target path cannot be resolved", () => { |
| 122 | + const stack = new Stack(undefined, "HelpersTestStack") |
| 123 | + vi.spyOn(NagSuppressions, "addResourceSuppressionsByPath").mockImplementation(() => { |
| 124 | + throw new Error("missing") |
| 125 | + }) |
| 126 | + const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {}) |
| 127 | + |
| 128 | + expect(() => helpers.safeAddNagSuppression(stack, "missing/path", sampleSuppressions)).not.toThrow() |
| 129 | + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("missing/path")) |
| 130 | + }) |
| 131 | +}) |
0 commit comments