Skip to content

Commit 7aac5f8

Browse files
committed
add tests
1 parent 10bf44f commit 7aac5f8

6 files changed

Lines changed: 177 additions & 11 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ these licenses. In particular this means that this project may not depend on
3838
GPL-licensed or AGPL-licensed libraries, as these would violate the terms of those
3939
libraries' licenses.
4040

41-
`packages/cdkConstructs/src/nag/rules/APIGWStructuredLogging.ts` derives from https://github.com/cdklabs/cdk-nag and is remains under Apache 2.0 licence
42-
41+
These files derive from https://github.com/cdklabs/cdk-nag and remain under Apache 2.0 licence
42+
`packages/cdkConstructs/src/nag/rules/APIGWStructuredLogging.ts`
43+
`packages/cdkConstructs/tests/nag/ApiGWStructuredLogging.test.ts`
4344

4445
The contents of this repository are protected by Crown Copyright (C).
4546

THIRD_PARTY_NOTICES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
The following files are licenced under Apache 2.0 and have been copied and modified with minor fixes from https://github.com/cdklabs/cdk-nag.
22

33
`packages/cdkConstructs/src/nag/rules/APIGWStructuredLogging.ts`
4+
`packages/cdkConstructs/tests/nag/ApiGWStructuredLogging.test.ts`
45

56
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
67
SPDX-License-Identifier: Apache-2.0

packages/cdkConstructs/src/nag/rules/ApiGatewayMutualTls.ts renamed to packages/cdkConstructs/src/nag/rules/ApiGWMutualTls.ts

File renamed without changes.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
export {default as ApiGatewayMutualTls} from "./ApiGatewayMutualTls"
1+
export {default as ApiGWMutualTls} from "./ApiGWMutualTls"
22
export {default as APIGWStructuredLogging} from "./APIGWStructuredLogging"
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
Modifications copyright (c) 2026 NHS Digital – see THIRD_PARTY_NOTICES.md
5+
*/
6+
import {Aspects, Stack} from "aws-cdk-lib"
7+
import {beforeEach, test} from "vitest"
8+
import {TestPack, TestType, validateStack} from "./utils"
9+
import {APIGWStructuredLogging} from "../../src/nag/rules"
10+
import {describe} from "node:test"
11+
import {CfnDeployment, CfnStage} from "aws-cdk-lib/aws-apigateway"
12+
import {CfnApi, CfnHttpApi} from "aws-cdk-lib/aws-sam"
13+
import {CfnStage as CfnV2Stage} from "aws-cdk-lib/aws-apigatewayv2"
14+
15+
// Copied from https://github.com/cdklabs/cdk-nag/blob/main/test/rules/APIGW.test.ts
16+
// with minor adjustments to handle CfnDeployment access log settings possibly being undefined
17+
// only copied relevant tests for structured logging
18+
// see https://github.com/cdklabs/cdk-nag/issues/2267
19+
// and https://github.com/cdklabs/cdk-nag/pull/2268
20+
21+
const testPack = new TestPack([
22+
APIGWStructuredLogging
23+
])
24+
let stack: Stack
25+
beforeEach(() => {
26+
stack = new Stack()
27+
Aspects.of(stack).add(testPack)
28+
})
29+
30+
describe("APIGWStructuredLogging: API Gateway stages use JSON-formatted structured logging", () => {
31+
const ruleId = "APIGWStructuredLogging"
32+
33+
test("Noncompliance 1: Non-JSON format (CfnStage)", () => {
34+
new CfnStage(stack, "RestApiStageNonJsonFormat", {
35+
restApiId: "foo",
36+
stageName: "prod",
37+
accessLogSetting: {
38+
destinationArn:
39+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod",
40+
format:
41+
// eslint-disable-next-line max-len
42+
'$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.responseLength $context.requestId'
43+
}
44+
})
45+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
46+
})
47+
48+
test("Noncompliance 2: No access log settings (CfnDeployment)", () => {
49+
new CfnDeployment(stack, "RestApiDeploymentNoLogs", {
50+
restApiId: "foo",
51+
stageDescription: {
52+
accessLogSetting: {
53+
destinationArn:
54+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod"
55+
}
56+
}
57+
})
58+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
59+
})
60+
61+
test("Noncompliance 3: No access log settings (CfnApi)", () => {
62+
new CfnApi(stack, "SamApiNoLogs", {
63+
stageName: "MyApi",
64+
accessLogSetting: {
65+
destinationArn:
66+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod"
67+
}
68+
})
69+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
70+
})
71+
72+
test("Noncompliance 4: No access log settings (CfnHttpApi)", () => {
73+
new CfnHttpApi(stack, "SamHttpApiNoLogs", {
74+
stageName: "MyApi",
75+
accessLogSetting: {
76+
destinationArn:
77+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod"
78+
}
79+
})
80+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
81+
})
82+
83+
test("Compliance 1: JSON-formatted log (CfnStage)", () => {
84+
new CfnStage(stack, "RestApiStageJsonFormat", {
85+
restApiId: "foo",
86+
stageName: "prod",
87+
accessLogSetting: {
88+
destinationArn:
89+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod",
90+
format:
91+
// eslint-disable-next-line max-len
92+
'{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}'
93+
}
94+
})
95+
validateStack(stack, ruleId, TestType.COMPLIANCE)
96+
})
97+
98+
test("Compliance 2: HTTP API with JSON-formatted log (CfnStageV2)", () => {
99+
new CfnV2Stage(stack, "HttpApiStageJsonFormat", {
100+
apiId: "bar",
101+
stageName: "prod",
102+
accessLogSettings: {
103+
destinationArn:
104+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod",
105+
format:
106+
// eslint-disable-next-line max-len
107+
'{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}'
108+
}
109+
})
110+
validateStack(stack, ruleId, TestType.COMPLIANCE)
111+
})
112+
113+
test("Compliance 3: JSON-formatted log (CfnDeployment)", () => {
114+
new CfnDeployment(stack, "RestApiDeploymentJsonFormat", {
115+
restApiId: "foo",
116+
stageDescription: {
117+
accessLogSetting: {
118+
destinationArn:
119+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod",
120+
format:
121+
// eslint-disable-next-line max-len
122+
'{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}'
123+
}
124+
}
125+
})
126+
validateStack(stack, ruleId, TestType.COMPLIANCE)
127+
})
128+
129+
test("Compliance 4: JSON-formatted log (CfnApi)", () => {
130+
new CfnApi(stack, "SamApiJsonFormat", {
131+
stageName: "MyApi",
132+
accessLogSetting: {
133+
destinationArn:
134+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod",
135+
format:
136+
// eslint-disable-next-line max-len
137+
'{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "caller":"$context.identity.caller", "user":"$context.identity.user","requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}'
138+
}
139+
})
140+
validateStack(stack, ruleId, TestType.COMPLIANCE)
141+
})
142+
143+
test("Compliance 5: JSON-formatted log (CfnHttpApi)", () => {
144+
new CfnHttpApi(stack, "SamHttpApiJsonFormat", {
145+
stageName: "MyApi",
146+
accessLogSetting: {
147+
destinationArn:
148+
"arn:aws:logs:us-east-1:123456789012:log-group:API-Gateway-Execution-Logs_abc123/prod",
149+
format:
150+
// eslint-disable-next-line max-len
151+
'{"requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength"}'
152+
}
153+
})
154+
validateStack(stack, ruleId, TestType.COMPLIANCE)
155+
})
156+
157+
test("Compliance 6: No stageDescription (CfnDeployment)", () => {
158+
new CfnDeployment(stack, "RestApiDeploymentNoStageDescription", {
159+
restApiId: "foo"
160+
})
161+
validateStack(stack, ruleId, TestType.COMPLIANCE)
162+
})
163+
})

packages/cdkConstructs/tests/nag/ApiGatewayMutualTls.test.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import {Aspects, Stack} from "aws-cdk-lib"
22
import {beforeEach, test} from "vitest"
33
import {TestPack, TestType, validateStack} from "./utils"
4-
import {ApiGatewayMutualTls} from "../../src/nag/rules"
4+
import {ApiGWMutualTls} from "../../src/nag/rules"
55
import {describe} from "node:test"
66
import {CfnDomainName} from "aws-cdk-lib/aws-apigateway"
77

88
const testPack = new TestPack([
9-
ApiGatewayMutualTls
9+
ApiGWMutualTls
1010
])
1111
let stack: Stack
1212
beforeEach(() => {
1313
stack = new Stack()
1414
Aspects.of(stack).add(testPack)
1515
})
1616

17-
describe("ApiGatewayMutualTls", () => {
17+
describe("ApiGWMutualTls", () => {
18+
const ruleId = "ApiGWMutualTls"
1819
test("Non-compliant when mutual TLS is not enabled", () => {
1920
new CfnDomainName(stack, "TestDomain", {
2021
domainName: "test.example.com"
2122
})
2223

2324
// Validate
24-
validateStack(stack, "ApiGatewayMutualTls", TestType.NON_COMPLIANCE)
25+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
2526
})
2627
test("Compliant when mutual TLS is enabled", () => {
2728
new CfnDomainName(stack, "TestDomain", {
@@ -33,7 +34,7 @@ describe("ApiGatewayMutualTls", () => {
3334
})
3435

3536
// Validate
36-
validateStack(stack, "ApiGatewayMutualTls", TestType.COMPLIANCE)
37+
validateStack(stack, ruleId, TestType.COMPLIANCE)
3738
})
3839

3940
test("Non-compliant when mutual TLS is missing trustStoreVersion", () => {
@@ -45,7 +46,7 @@ describe("ApiGatewayMutualTls", () => {
4546
})
4647

4748
// Validate
48-
validateStack(stack, "ApiGatewayMutualTls", TestType.NON_COMPLIANCE)
49+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
4950
})
5051
test("Compliant when mutual TLS is not enabled in a pull request", () => {
5152
stack.node.setContext("isPullRequest", true)
@@ -54,7 +55,7 @@ describe("ApiGatewayMutualTls", () => {
5455
})
5556

5657
// Validate
57-
validateStack(stack, "ApiGatewayMutualTls", TestType.COMPLIANCE)
58+
validateStack(stack, ruleId, TestType.COMPLIANCE)
5859
})
5960
test("Compliant when mutual TLS is not enabled in not a pull request", () => {
6061
stack.node.setContext("isPullRequest", false)
@@ -63,7 +64,7 @@ describe("ApiGatewayMutualTls", () => {
6364
})
6465

6566
// Validate
66-
validateStack(stack, "ApiGatewayMutualTls", TestType.NON_COMPLIANCE)
67+
validateStack(stack, ruleId, TestType.NON_COMPLIANCE)
6768
})
6869

6970
})

0 commit comments

Comments
 (0)