@intentius/chant-lexicon-aws 0.46.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/read-client.d.ts +10 -2
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/codegen/generate-lexicon.d.ts +5 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/components/host-delivery.d.ts +4 -3
- package/dist/components/host-delivery.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +26 -1
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/bucket-deployment.d.ts +82 -0
- package/dist/composites/bucket-deployment.d.ts.map +1 -0
- package/dist/composites/dynamodb-table.d.ts +45 -0
- package/dist/composites/dynamodb-table.d.ts.map +1 -0
- package/dist/composites/ec2-instance-bundle.d.ts +61 -0
- package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
- package/dist/composites/ecr-repository.d.ts +49 -0
- package/dist/composites/ecr-repository.d.ts.map +1 -0
- package/dist/composites/fargate-service.d.ts +16 -0
- package/dist/composites/fargate-service.d.ts.map +1 -1
- package/dist/composites/index.d.ts +16 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/monitoring-stack.d.ts +99 -0
- package/dist/composites/monitoring-stack.d.ts.map +1 -0
- package/dist/composites/nlb-service.d.ts +48 -0
- package/dist/composites/nlb-service.d.ts.map +1 -0
- package/dist/composites/step-functions-workflow.d.ts +36 -0
- package/dist/composites/step-functions-workflow.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +45 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-topology.d.ts +104 -0
- package/dist/deep-topology.d.ts.map +1 -0
- package/dist/effect-receipt-row.d.ts +92 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +14 -6
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +17 -0
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw016.d.ts +10 -2
- package/dist/lint/post-synth/waw016.d.ts.map +1 -1
- package/dist/lint/post-synth/waw061.d.ts +19 -0
- package/dist/lint/post-synth/waw061.d.ts.map +1 -0
- package/dist/lint/post-synth/waw062.d.ts +19 -0
- package/dist/lint/post-synth/waw062.d.ts.map +1 -0
- package/dist/lint/post-synth/waw063.d.ts +35 -0
- package/dist/lint/post-synth/waw063.d.ts.map +1 -0
- package/dist/lint/post-synth/waw064.d.ts +17 -0
- package/dist/lint/post-synth/waw064.d.ts.map +1 -0
- package/dist/lint/post-synth/waw065.d.ts +23 -0
- package/dist/lint/post-synth/waw065.d.ts.map +1 -0
- package/dist/lint/post-synth/waw066.d.ts +25 -0
- package/dist/lint/post-synth/waw066.d.ts.map +1 -0
- package/dist/lint/post-synth/waw067.d.ts +24 -0
- package/dist/lint/post-synth/waw067.d.ts.map +1 -0
- package/dist/lint/post-synth/waw068.d.ts +20 -0
- package/dist/lint/post-synth/waw068.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +34 -0
- package/dist/okf/index.md +8 -0
- package/dist/okf/rules/WAW061.md +17 -0
- package/dist/okf/rules/WAW062.md +27 -0
- package/dist/okf/rules/WAW063.md +19 -0
- package/dist/okf/rules/WAW064.md +18 -0
- package/dist/okf/rules/WAW065.md +25 -0
- package/dist/okf/rules/WAW066.md +22 -0
- package/dist/okf/rules/WAW067.md +20 -0
- package/dist/okf/rules/WAW068.md +19 -0
- package/dist/okf/types/Action.md +1 -0
- package/dist/okf/types/AutoScalingGroup.md +1 -0
- package/dist/okf/types/Bucket.md +1 -0
- package/dist/okf/types/EC2Route.md +2 -0
- package/dist/okf/types/ECRRepository.md +1 -0
- package/dist/okf/types/Function.md +1 -0
- package/dist/okf/types/IamPolicy.md +1 -0
- package/dist/okf/types/InternetGateway.md +4 -0
- package/dist/okf/types/LaunchTemplate.md +1 -0
- package/dist/okf/types/LoadBalancer.md +1 -0
- package/dist/okf/types/ManagedPolicy.md +2 -0
- package/dist/okf/types/Map.md +5 -0
- package/dist/okf/types/NatGateway.md +4 -0
- package/dist/okf/types/Role.md +2 -0
- package/dist/okf/types/RouteTable.md +4 -0
- package/dist/okf/types/SecurityGroup.md +1 -0
- package/dist/okf/types/StateMachine.md +4 -0
- package/dist/okf/types/Subnet.md +6 -0
- package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
- package/dist/okf/types/Table.md +4 -0
- package/dist/okf/types/TransitGateway.md +6 -0
- package/dist/okf/types/TransitGatewayAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayRoute.md +5 -0
- package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
- package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
- package/dist/okf/types/Type.md +8 -0
- package/dist/okf/types/VPNConnection.md +4 -0
- package/dist/okf/types/VPNGateway.md +4 -0
- package/dist/okf/types/Vpc.md +1 -0
- package/dist/op/activities/index.d.ts +6 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/builders.d.ts +47 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +109 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/cf-refs.ts +40 -0
- package/dist/rules/waw016.ts +37 -16
- package/dist/rules/waw061.ts +67 -0
- package/dist/rules/waw062.ts +0 -0
- package/dist/rules/waw063.ts +0 -0
- package/dist/rules/waw064.ts +52 -0
- package/dist/rules/waw065.ts +110 -0
- package/dist/rules/waw066.ts +155 -0
- package/dist/rules/waw067.ts +114 -0
- package/dist/rules/waw068.ts +78 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-aws-carve-terraform.md +26 -23
- package/dist/spec/parse.d.ts +7 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/api/read-client.test.ts +9 -0
- package/src/api/read-client.ts +15 -2
- package/src/codegen/fallback.ts +1 -0
- package/src/codegen/generate-lexicon.ts +6 -0
- package/src/codegen/sam.ts +9 -0
- package/src/components/host-delivery.ts +4 -3
- package/src/composites/agentcore-agent.ts +35 -13
- package/src/composites/bucket-deployment.test.ts +84 -0
- package/src/composites/bucket-deployment.ts +153 -0
- package/src/composites/composites.test.ts +325 -0
- package/src/composites/dynamodb-table.test.ts +154 -0
- package/src/composites/dynamodb-table.ts +124 -0
- package/src/composites/ec2-instance-bundle.ts +134 -0
- package/src/composites/ecr-repository.test.ts +118 -0
- package/src/composites/ecr-repository.ts +117 -0
- package/src/composites/fargate-service.ts +19 -3
- package/src/composites/index.ts +21 -2
- package/src/composites/monitoring-stack.test.ts +130 -0
- package/src/composites/monitoring-stack.ts +164 -0
- package/src/composites/nlb-service.ts +117 -0
- package/src/composites/step-functions-workflow.ts +127 -0
- package/src/deep-observe.test.ts +201 -1
- package/src/deep-observe.ts +204 -0
- package/src/deep-topology.test.ts +392 -0
- package/src/deep-topology.ts +237 -0
- package/src/effect-receipt-row.test.ts +164 -0
- package/src/effect-receipt-row.ts +147 -0
- package/src/generated/lexicon-aws.json +34 -0
- package/src/index.ts +36 -7
- package/src/lint/audit-catalog.ts +16 -0
- package/src/lint/post-synth/cf-refs.ts +40 -0
- package/src/lint/post-synth/index.ts +16 -0
- package/src/lint/post-synth/waw016.test.ts +61 -4
- package/src/lint/post-synth/waw016.ts +37 -16
- package/src/lint/post-synth/waw061.test.ts +99 -0
- package/src/lint/post-synth/waw061.ts +67 -0
- package/src/lint/post-synth/waw062.test.ts +109 -0
- package/src/lint/post-synth/waw062.ts +0 -0
- package/src/lint/post-synth/waw063.test.ts +220 -0
- package/src/lint/post-synth/waw063.ts +0 -0
- package/src/lint/post-synth/waw064.test.ts +79 -0
- package/src/lint/post-synth/waw064.ts +52 -0
- package/src/lint/post-synth/waw065.test.ts +90 -0
- package/src/lint/post-synth/waw065.ts +110 -0
- package/src/lint/post-synth/waw066.test.ts +108 -0
- package/src/lint/post-synth/waw066.ts +155 -0
- package/src/lint/post-synth/waw067.test.ts +79 -0
- package/src/lint/post-synth/waw067.ts +114 -0
- package/src/lint/post-synth/waw068.test.ts +74 -0
- package/src/lint/post-synth/waw068.ts +78 -0
- package/src/lsp/hover.ts +17 -2
- package/src/op/activities/index.ts +15 -0
- package/src/op/builders.test.ts +61 -0
- package/src/op/builders.ts +72 -0
- package/src/plugin.ts +41 -10
- package/src/receipt-store.test.ts +204 -0
- package/src/receipt-store.ts +315 -0
- package/src/serializer.ts +105 -2
- package/src/skills/chant-aws-carve-terraform.md +26 -23
- package/src/spec/parse.test.ts +74 -0
- package/src/spec/parse.ts +26 -6
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw063, checkDenyAllowContradiction } from "./waw063";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW063: IAM Policy Denies An Action It Also Allows", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw063.id).toBe("WAW063");
|
|
12
|
+
expect(waw063.description).toContain("Deny");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("guardrail policy denies exactly what a workflow policy allows on the same role → error", () => {
|
|
16
|
+
// Mirrors the issue's own example: a role used by a StepFunctions workflow
|
|
17
|
+
// needs ec2:ModifyInstanceAttribute; a separately-attached guardrail policy
|
|
18
|
+
// denies it. Deploy succeeds; the workflow 403s at runtime.
|
|
19
|
+
const ctx = makeCtx({
|
|
20
|
+
Resources: {
|
|
21
|
+
WorkflowRole: { Type: "AWS::IAM::Role", Properties: { AssumeRolePolicyDocument: { Statement: [] } } },
|
|
22
|
+
WorkflowPolicy: {
|
|
23
|
+
Type: "AWS::IAM::Policy",
|
|
24
|
+
Properties: {
|
|
25
|
+
PolicyName: "workflow-needs",
|
|
26
|
+
Roles: [{ Ref: "WorkflowRole" }],
|
|
27
|
+
PolicyDocument: {
|
|
28
|
+
Statement: [{ Effect: "Allow", Action: "ec2:ModifyInstanceAttribute", Resource: "*" }],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
SecurityGuardrailPolicy: {
|
|
33
|
+
Type: "AWS::IAM::Policy",
|
|
34
|
+
Properties: {
|
|
35
|
+
PolicyName: "guardrail",
|
|
36
|
+
Roles: [{ Ref: "WorkflowRole" }],
|
|
37
|
+
PolicyDocument: {
|
|
38
|
+
Statement: [{ Effect: "Deny", Action: "ec2:ModifyInstanceAttribute", Resource: "*" }],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const diags = checkDenyAllowContradiction(ctx);
|
|
45
|
+
expect(diags).toHaveLength(1);
|
|
46
|
+
expect(diags[0].checkId).toBe("WAW063");
|
|
47
|
+
expect(diags[0].severity).toBe("error");
|
|
48
|
+
expect(diags[0].entity).toBe("WorkflowRole");
|
|
49
|
+
expect(diags[0].message).toContain("ec2:ModifyInstanceAttribute");
|
|
50
|
+
expect(diags[0].message).toContain("SecurityGuardrailPolicy");
|
|
51
|
+
expect(diags[0].message).toContain("WorkflowPolicy");
|
|
52
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("wildcarded Deny nullifies a literal Allow on the same role's inline policy → error", () => {
|
|
56
|
+
const ctx = makeCtx({
|
|
57
|
+
Resources: {
|
|
58
|
+
AppRole: {
|
|
59
|
+
Type: "AWS::IAM::Role",
|
|
60
|
+
Properties: {
|
|
61
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
62
|
+
Policies: [
|
|
63
|
+
{
|
|
64
|
+
PolicyName: "inline",
|
|
65
|
+
PolicyDocument: {
|
|
66
|
+
Statement: [
|
|
67
|
+
{ Effect: "Allow", Action: "s3:GetObject", Resource: "*" },
|
|
68
|
+
{ Effect: "Deny", Action: "s3:*", Resource: "*" },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
expect(checkDenyAllowContradiction(ctx)).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("broad Allow with a narrow safety Deny (intentional guardrail) → no diagnostic", () => {
|
|
81
|
+
const ctx = makeCtx({
|
|
82
|
+
Resources: {
|
|
83
|
+
AppRole: {
|
|
84
|
+
Type: "AWS::IAM::Role",
|
|
85
|
+
Properties: {
|
|
86
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
87
|
+
Policies: [
|
|
88
|
+
{
|
|
89
|
+
PolicyName: "inline",
|
|
90
|
+
PolicyDocument: {
|
|
91
|
+
Statement: [
|
|
92
|
+
{ Effect: "Allow", Action: "ec2:*", Resource: "*" },
|
|
93
|
+
{ Effect: "Deny", Action: "ec2:TerminateInstances", Resource: "*" },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
expect(checkDenyAllowContradiction(ctx)).toHaveLength(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("Deny on a different role than the Allow → no diagnostic", () => {
|
|
106
|
+
const ctx = makeCtx({
|
|
107
|
+
Resources: {
|
|
108
|
+
RoleA: {
|
|
109
|
+
Type: "AWS::IAM::Role",
|
|
110
|
+
Properties: {
|
|
111
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
112
|
+
Policies: [{ PolicyName: "p", PolicyDocument: { Statement: [{ Effect: "Allow", Action: "ec2:ModifyInstanceAttribute", Resource: "*" }] } }],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
RoleB: {
|
|
116
|
+
Type: "AWS::IAM::Role",
|
|
117
|
+
Properties: {
|
|
118
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
119
|
+
Policies: [{ PolicyName: "p", PolicyDocument: { Statement: [{ Effect: "Deny", Action: "ec2:ModifyInstanceAttribute", Resource: "*" }] } }],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
expect(checkDenyAllowContradiction(ctx)).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("Deny scoped to a disjoint literal resource → no diagnostic", () => {
|
|
128
|
+
const ctx = makeCtx({
|
|
129
|
+
Resources: {
|
|
130
|
+
AppRole: {
|
|
131
|
+
Type: "AWS::IAM::Role",
|
|
132
|
+
Properties: {
|
|
133
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
134
|
+
Policies: [
|
|
135
|
+
{
|
|
136
|
+
PolicyName: "inline",
|
|
137
|
+
PolicyDocument: {
|
|
138
|
+
Statement: [
|
|
139
|
+
{ Effect: "Allow", Action: "s3:GetObject", Resource: "arn:aws:s3:::bucket-a/*" },
|
|
140
|
+
{ Effect: "Deny", Action: "s3:GetObject", Resource: "arn:aws:s3:::bucket-b/*" },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
expect(checkDenyAllowContradiction(ctx)).toHaveLength(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("conditional Deny is skipped (unprovable statically) → no diagnostic", () => {
|
|
153
|
+
const ctx = makeCtx({
|
|
154
|
+
Resources: {
|
|
155
|
+
AppRole: {
|
|
156
|
+
Type: "AWS::IAM::Role",
|
|
157
|
+
Properties: {
|
|
158
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
159
|
+
Policies: [
|
|
160
|
+
{
|
|
161
|
+
PolicyName: "inline",
|
|
162
|
+
PolicyDocument: {
|
|
163
|
+
Statement: [
|
|
164
|
+
{ Effect: "Allow", Action: "s3:GetObject", Resource: "*" },
|
|
165
|
+
{
|
|
166
|
+
Effect: "Deny",
|
|
167
|
+
Action: "s3:GetObject",
|
|
168
|
+
Resource: "*",
|
|
169
|
+
Condition: { StringNotEquals: { "aws:SourceVpc": "vpc-123" } },
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
expect(checkDenyAllowContradiction(ctx)).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("Deny from a managed policy attached via ManagedPolicyArns collides with an inline Allow → error", () => {
|
|
183
|
+
const ctx = makeCtx({
|
|
184
|
+
Resources: {
|
|
185
|
+
AppRole: {
|
|
186
|
+
Type: "AWS::IAM::Role",
|
|
187
|
+
Properties: {
|
|
188
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
189
|
+
ManagedPolicyArns: [{ Ref: "GuardrailManagedPolicy" }],
|
|
190
|
+
Policies: [{ PolicyName: "inline", PolicyDocument: { Statement: [{ Effect: "Allow", Action: "ec2:ModifyInstanceAttribute", Resource: "*" }] } }],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
GuardrailManagedPolicy: {
|
|
194
|
+
Type: "AWS::IAM::ManagedPolicy",
|
|
195
|
+
Properties: {
|
|
196
|
+
PolicyDocument: { Statement: [{ Effect: "Deny", Action: "ec2:ModifyInstanceAttribute", Resource: "*" }] },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const diags = checkDenyAllowContradiction(ctx);
|
|
202
|
+
expect(diags).toHaveLength(1);
|
|
203
|
+
expect(diags[0].message).toContain("GuardrailManagedPolicy");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("role with only Allow statements → no diagnostic", () => {
|
|
207
|
+
const ctx = makeCtx({
|
|
208
|
+
Resources: {
|
|
209
|
+
AppRole: {
|
|
210
|
+
Type: "AWS::IAM::Role",
|
|
211
|
+
Properties: {
|
|
212
|
+
AssumeRolePolicyDocument: { Statement: [] },
|
|
213
|
+
Policies: [{ PolicyName: "p", PolicyDocument: { Statement: [{ Effect: "Allow", Action: "s3:GetObject", Resource: "*" }] } }],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
expect(checkDenyAllowContradiction(ctx)).toHaveLength(0);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
Binary file
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw064, checkTgwBlackholeRoute } from "./waw064";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW064: Transit Gateway Route Table Declares A Blackhole Route", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw064.id).toBe("WAW064");
|
|
12
|
+
expect(waw064.description).toContain("Blackhole");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("TransitGatewayRoute with Blackhole: true → warning", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
QuarantineRoute: {
|
|
19
|
+
Type: "AWS::EC2::TransitGatewayRoute",
|
|
20
|
+
Properties: {
|
|
21
|
+
TransitGatewayRouteTableId: { Ref: "TgwRouteTable" },
|
|
22
|
+
DestinationCidrBlock: "10.99.0.0/16",
|
|
23
|
+
Blackhole: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
TgwRouteTable: { Type: "AWS::EC2::TransitGatewayRouteTable", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
27
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const diags = checkTgwBlackholeRoute(ctx);
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("WAW064");
|
|
33
|
+
expect(diags[0].severity).toBe("warning");
|
|
34
|
+
expect(diags[0].entity).toBe("QuarantineRoute");
|
|
35
|
+
expect(diags[0].message).toContain("10.99.0.0/16");
|
|
36
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("normal TransitGatewayRoute forwarding to an attachment → no diagnostic", () => {
|
|
40
|
+
const ctx = makeCtx({
|
|
41
|
+
Resources: {
|
|
42
|
+
SpokeRoute: {
|
|
43
|
+
Type: "AWS::EC2::TransitGatewayRoute",
|
|
44
|
+
Properties: {
|
|
45
|
+
TransitGatewayRouteTableId: { Ref: "TgwRouteTable" },
|
|
46
|
+
DestinationCidrBlock: "10.0.1.0/24",
|
|
47
|
+
TransitGatewayAttachmentId: { Ref: "SpokeAttachment" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
TgwRouteTable: { Type: "AWS::EC2::TransitGatewayRouteTable", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
51
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
52
|
+
SpokeAttachment: { Type: "AWS::EC2::TransitGatewayVpcAttachment", Properties: {} },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
expect(checkTgwBlackholeRoute(ctx)).toHaveLength(0);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("Blackhole: false is not flagged", () => {
|
|
59
|
+
const ctx = makeCtx({
|
|
60
|
+
Resources: {
|
|
61
|
+
SpokeRoute: {
|
|
62
|
+
Type: "AWS::EC2::TransitGatewayRoute",
|
|
63
|
+
Properties: {
|
|
64
|
+
TransitGatewayRouteTableId: { Ref: "TgwRouteTable" },
|
|
65
|
+
DestinationCidrBlock: "10.0.1.0/24",
|
|
66
|
+
TransitGatewayAttachmentId: { Ref: "SpokeAttachment" },
|
|
67
|
+
Blackhole: false,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
expect(checkTgwBlackholeRoute(ctx)).toHaveLength(0);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("no TransitGatewayRoute resources → no diagnostic", () => {
|
|
76
|
+
const ctx = makeCtx({ Resources: { Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } } } });
|
|
77
|
+
expect(checkTgwBlackholeRoute(ctx)).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW064: Transit Gateway Route Table Declares A Blackhole Route
|
|
3
|
+
*
|
|
4
|
+
* `AWS::EC2::TransitGatewayRoute` with `Blackhole: true` silently drops any
|
|
5
|
+
* traffic matching its destination CIDR instead of forwarding it —
|
|
6
|
+
* CloudFormation deploys this without complaint, and the packet loss only
|
|
7
|
+
* surfaces downstream as unreachable hosts or a timeout nobody can explain
|
|
8
|
+
* from the template alone. A blackhole route is sometimes deliberate — an
|
|
9
|
+
* explicit deny for a CIDR range, or a placeholder pending real
|
|
10
|
+
* infrastructure — but it is also exactly what a copy-pasted route table or
|
|
11
|
+
* a forgotten cleanup step leaves behind. Warn so it stays a conscious
|
|
12
|
+
* choice rather than a silent accident.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
17
|
+
|
|
18
|
+
export function checkTgwBlackholeRoute(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
22
|
+
const template = parseCFTemplate(output);
|
|
23
|
+
if (!template?.Resources) continue;
|
|
24
|
+
|
|
25
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
26
|
+
if (resource.Type !== "AWS::EC2::TransitGatewayRoute") continue;
|
|
27
|
+
|
|
28
|
+
const props = resource.Properties ?? {};
|
|
29
|
+
if (props.Blackhole !== true) continue;
|
|
30
|
+
|
|
31
|
+
const cidr = typeof props.DestinationCidrBlock === "string" ? props.DestinationCidrBlock : "(unknown destination)";
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "WAW064",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `TransitGatewayRoute "${logicalId}" declares Blackhole: true for destination ${cidr} — traffic matching this route is silently dropped; confirm this is intentional`,
|
|
36
|
+
entity: logicalId,
|
|
37
|
+
lexicon: "aws",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return diagnostics;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const waw064: PostSynthCheck = {
|
|
46
|
+
id: "WAW064",
|
|
47
|
+
description: "Transit Gateway route table declares a Blackhole route — confirm the traffic drop is intentional, not an accident",
|
|
48
|
+
|
|
49
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
50
|
+
return checkTgwBlackholeRoute(ctx);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw065, checkTgwRouteTableWiring } from "./waw065";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW065: Transit Gateway Route Table Wiring Is Incomplete", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw065.id).toBe("WAW065");
|
|
12
|
+
expect(waw065.description).toContain("propagation");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("route table with an association but no propagation → warning", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
19
|
+
SpokeRouteTable: { Type: "AWS::EC2::TransitGatewayRouteTable", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
20
|
+
SpokeAttachment: { Type: "AWS::EC2::TransitGatewayVpcAttachment", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
21
|
+
SpokeAssociation: {
|
|
22
|
+
Type: "AWS::EC2::TransitGatewayRouteTableAssociation",
|
|
23
|
+
Properties: { TransitGatewayAttachmentId: { Ref: "SpokeAttachment" }, TransitGatewayRouteTableId: { Ref: "SpokeRouteTable" } },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const diags = checkTgwRouteTableWiring(ctx);
|
|
28
|
+
expect(diags).toHaveLength(1);
|
|
29
|
+
expect(diags[0].checkId).toBe("WAW065");
|
|
30
|
+
expect(diags[0].entity).toBe("SpokeRouteTable");
|
|
31
|
+
expect(diags[0].message).toContain("no route propagations");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("attachment referenced by neither an association nor a propagation → warning", () => {
|
|
35
|
+
const ctx = makeCtx({
|
|
36
|
+
Resources: {
|
|
37
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
38
|
+
SpokeRouteTable: { Type: "AWS::EC2::TransitGatewayRouteTable", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
39
|
+
OrphanAttachment: { Type: "AWS::EC2::TransitGatewayVpcAttachment", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const diags = checkTgwRouteTableWiring(ctx);
|
|
43
|
+
expect(diags).toHaveLength(1);
|
|
44
|
+
expect(diags[0].entity).toBe("OrphanAttachment");
|
|
45
|
+
expect(diags[0].message).toContain("no route table wiring");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("both patterns fire independently in the same template", () => {
|
|
49
|
+
const ctx = makeCtx({
|
|
50
|
+
Resources: {
|
|
51
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
52
|
+
SpokeRouteTable: { Type: "AWS::EC2::TransitGatewayRouteTable", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
53
|
+
SpokeAttachment: { Type: "AWS::EC2::TransitGatewayVpcAttachment", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
54
|
+
SpokeAssociation: {
|
|
55
|
+
Type: "AWS::EC2::TransitGatewayRouteTableAssociation",
|
|
56
|
+
Properties: { TransitGatewayAttachmentId: { Ref: "SpokeAttachment" }, TransitGatewayRouteTableId: { Ref: "SpokeRouteTable" } },
|
|
57
|
+
},
|
|
58
|
+
OrphanAttachment: { Type: "AWS::EC2::TransitGatewayVpnAttachment", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const diags = checkTgwRouteTableWiring(ctx);
|
|
62
|
+
expect(diags).toHaveLength(2);
|
|
63
|
+
const entities = diags.map((d) => d.entity).sort();
|
|
64
|
+
expect(entities).toEqual(["OrphanAttachment", "SpokeRouteTable"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("route table with both an association and a propagation, attachment referenced by both → no diagnostic", () => {
|
|
68
|
+
const ctx = makeCtx({
|
|
69
|
+
Resources: {
|
|
70
|
+
Tgw: { Type: "AWS::EC2::TransitGateway", Properties: {} },
|
|
71
|
+
SpokeRouteTable: { Type: "AWS::EC2::TransitGatewayRouteTable", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
72
|
+
SpokeAttachment: { Type: "AWS::EC2::TransitGatewayVpcAttachment", Properties: { TransitGatewayId: { Ref: "Tgw" } } },
|
|
73
|
+
SpokeAssociation: {
|
|
74
|
+
Type: "AWS::EC2::TransitGatewayRouteTableAssociation",
|
|
75
|
+
Properties: { TransitGatewayAttachmentId: { Ref: "SpokeAttachment" }, TransitGatewayRouteTableId: { Ref: "SpokeRouteTable" } },
|
|
76
|
+
},
|
|
77
|
+
SpokePropagation: {
|
|
78
|
+
Type: "AWS::EC2::TransitGatewayRouteTablePropagation",
|
|
79
|
+
Properties: { TransitGatewayAttachmentId: { Ref: "SpokeAttachment" }, TransitGatewayRouteTableId: { Ref: "SpokeRouteTable" } },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
expect(checkTgwRouteTableWiring(ctx)).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("no transit gateway resources at all → no diagnostic", () => {
|
|
87
|
+
const ctx = makeCtx({ Resources: { Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } } } });
|
|
88
|
+
expect(checkTgwRouteTableWiring(ctx)).toHaveLength(0);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW065: Transit Gateway Route Table Wiring Is Incomplete
|
|
3
|
+
*
|
|
4
|
+
* A Transit Gateway route table only does anything once two independent
|
|
5
|
+
* pieces are both wired up: an `AWS::EC2::TransitGatewayRouteTableAssociation`
|
|
6
|
+
* (which attachment's traffic is evaluated against this table) and an
|
|
7
|
+
* `AWS::EC2::TransitGatewayRouteTablePropagation` (which attachments' routes
|
|
8
|
+
* populate it). CloudFormation is happy to deploy a route table with one but
|
|
9
|
+
* not the other — the stack succeeds, and the gap only shows up as traffic
|
|
10
|
+
* that can't find a route. Two patterns, both "declared but half-wired":
|
|
11
|
+
*
|
|
12
|
+
* - a route table has one or more associations but zero propagations — the
|
|
13
|
+
* associated attachment's traffic is evaluated against this table, but
|
|
14
|
+
* nothing ever populates it with routes;
|
|
15
|
+
* - a declared attachment (VPC/VPN/peering, or the generic
|
|
16
|
+
* TransitGatewayAttachment) is referenced by neither an association nor
|
|
17
|
+
* a propagation anywhere in the template — it hangs off the transit
|
|
18
|
+
* gateway with no route table wiring at all.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
22
|
+
import { parseCFTemplate, findResourceRefs } from "./cf-refs";
|
|
23
|
+
|
|
24
|
+
const ATTACHMENT_TYPES = new Set([
|
|
25
|
+
"AWS::EC2::TransitGatewayAttachment",
|
|
26
|
+
"AWS::EC2::TransitGatewayVpcAttachment",
|
|
27
|
+
"AWS::EC2::TransitGatewayVpnAttachment",
|
|
28
|
+
"AWS::EC2::TransitGatewayPeeringAttachment",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function checkTgwRouteTableWiring(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
32
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
33
|
+
|
|
34
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
35
|
+
const template = parseCFTemplate(output);
|
|
36
|
+
if (!template?.Resources) continue;
|
|
37
|
+
const resources = template.Resources;
|
|
38
|
+
|
|
39
|
+
const routeTableIds = new Set<string>();
|
|
40
|
+
const attachmentIds = new Set<string>();
|
|
41
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
42
|
+
if (resource.Type === "AWS::EC2::TransitGatewayRouteTable") routeTableIds.add(logicalId);
|
|
43
|
+
if (ATTACHMENT_TYPES.has(resource.Type)) attachmentIds.add(logicalId);
|
|
44
|
+
}
|
|
45
|
+
if (routeTableIds.size === 0 && attachmentIds.size === 0) continue;
|
|
46
|
+
|
|
47
|
+
const associationsByTable = new Map<string, Set<string>>();
|
|
48
|
+
const propagationsByTable = new Map<string, Set<string>>();
|
|
49
|
+
const referencedAttachments = new Set<string>();
|
|
50
|
+
|
|
51
|
+
for (const resource of Object.values(resources)) {
|
|
52
|
+
let bucket: Map<string, Set<string>> | null = null;
|
|
53
|
+
if (resource.Type === "AWS::EC2::TransitGatewayRouteTableAssociation") bucket = associationsByTable;
|
|
54
|
+
if (resource.Type === "AWS::EC2::TransitGatewayRouteTablePropagation") bucket = propagationsByTable;
|
|
55
|
+
if (!bucket) continue;
|
|
56
|
+
|
|
57
|
+
const props = resource.Properties ?? {};
|
|
58
|
+
const attachmentRefs = findResourceRefs(props.TransitGatewayAttachmentId);
|
|
59
|
+
for (const attId of attachmentRefs) referencedAttachments.add(attId);
|
|
60
|
+
|
|
61
|
+
for (const rtId of findResourceRefs(props.TransitGatewayRouteTableId)) {
|
|
62
|
+
let attSet = bucket.get(rtId);
|
|
63
|
+
if (!attSet) {
|
|
64
|
+
attSet = new Set();
|
|
65
|
+
bucket.set(rtId, attSet);
|
|
66
|
+
}
|
|
67
|
+
for (const attId of attachmentRefs) attSet.add(attId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Pattern A: associated but never propagated.
|
|
72
|
+
for (const rtId of routeTableIds) {
|
|
73
|
+
const associations = associationsByTable.get(rtId);
|
|
74
|
+
const propagations = propagationsByTable.get(rtId);
|
|
75
|
+
if (associations && associations.size > 0 && (!propagations || propagations.size === 0)) {
|
|
76
|
+
diagnostics.push({
|
|
77
|
+
checkId: "WAW065",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
message: `TransitGatewayRouteTable "${rtId}" has ${associations.size} association(s) but no route propagations — the associated attachment's routes never populate this table, likely incomplete wiring`,
|
|
80
|
+
entity: rtId,
|
|
81
|
+
lexicon: "aws",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pattern B: a declared attachment wired into no route table at all.
|
|
87
|
+
for (const attId of attachmentIds) {
|
|
88
|
+
if (!referencedAttachments.has(attId)) {
|
|
89
|
+
diagnostics.push({
|
|
90
|
+
checkId: "WAW065",
|
|
91
|
+
severity: "warning",
|
|
92
|
+
message: `Transit Gateway attachment "${attId}" is referenced by no TransitGatewayRouteTableAssociation or TransitGatewayRouteTablePropagation — it has no route table wiring, so nothing can route to or from it`,
|
|
93
|
+
entity: attId,
|
|
94
|
+
lexicon: "aws",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return diagnostics;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const waw065: PostSynthCheck = {
|
|
104
|
+
id: "WAW065",
|
|
105
|
+
description: "Transit Gateway route table has associations but no propagations, or an attachment wired into no route table",
|
|
106
|
+
|
|
107
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
108
|
+
return checkTgwRouteTableWiring(ctx);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw066, checkPrivateSubnetDefaultRoute } from "./waw066";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW066: Private Subnet Route Table Has No Working Default Route", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw066.id).toBe("WAW066");
|
|
12
|
+
expect(waw066.description).toContain("default route");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("subnet's route table has no default route at all → warning", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
PrivateSubnet: { Type: "AWS::EC2::Subnet", Properties: { VpcId: { Ref: "Vpc" }, CidrBlock: "10.0.1.0/24" } },
|
|
19
|
+
PrivateRouteTable: { Type: "AWS::EC2::RouteTable", Properties: { VpcId: { Ref: "Vpc" } } },
|
|
20
|
+
PrivateAssoc: {
|
|
21
|
+
Type: "AWS::EC2::SubnetRouteTableAssociation",
|
|
22
|
+
Properties: { SubnetId: { Ref: "PrivateSubnet" }, RouteTableId: { Ref: "PrivateRouteTable" } },
|
|
23
|
+
},
|
|
24
|
+
Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const diags = checkPrivateSubnetDefaultRoute(ctx);
|
|
28
|
+
expect(diags).toHaveLength(1);
|
|
29
|
+
expect(diags[0].checkId).toBe("WAW066");
|
|
30
|
+
expect(diags[0].severity).toBe("warning");
|
|
31
|
+
expect(diags[0].entity).toBe("PrivateSubnet");
|
|
32
|
+
expect(diags[0].message).toContain("no default route");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("default route targets a NAT gateway that does not exist in the template → error", () => {
|
|
36
|
+
const ctx = makeCtx({
|
|
37
|
+
Resources: {
|
|
38
|
+
PrivateSubnet: { Type: "AWS::EC2::Subnet", Properties: { VpcId: { Ref: "Vpc" }, CidrBlock: "10.0.1.0/24" } },
|
|
39
|
+
PrivateRouteTable: { Type: "AWS::EC2::RouteTable", Properties: { VpcId: { Ref: "Vpc" } } },
|
|
40
|
+
PrivateAssoc: {
|
|
41
|
+
Type: "AWS::EC2::SubnetRouteTableAssociation",
|
|
42
|
+
Properties: { SubnetId: { Ref: "PrivateSubnet" }, RouteTableId: { Ref: "PrivateRouteTable" } },
|
|
43
|
+
},
|
|
44
|
+
DefaultRoute: {
|
|
45
|
+
Type: "AWS::EC2::Route",
|
|
46
|
+
Properties: { RouteTableId: { Ref: "PrivateRouteTable" }, DestinationCidrBlock: "0.0.0.0/0", NatGatewayId: { Ref: "RemovedNatGateway" } },
|
|
47
|
+
},
|
|
48
|
+
Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const diags = checkPrivateSubnetDefaultRoute(ctx);
|
|
52
|
+
expect(diags).toHaveLength(1);
|
|
53
|
+
expect(diags[0].severity).toBe("error");
|
|
54
|
+
expect(diags[0].entity).toBe("PrivateSubnet");
|
|
55
|
+
expect(diags[0].message).toContain("does not exist in this template");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("default route to a NAT gateway that exists in the template → no diagnostic", () => {
|
|
59
|
+
const ctx = makeCtx({
|
|
60
|
+
Resources: {
|
|
61
|
+
PrivateSubnet: { Type: "AWS::EC2::Subnet", Properties: { VpcId: { Ref: "Vpc" }, CidrBlock: "10.0.1.0/24" } },
|
|
62
|
+
PrivateRouteTable: { Type: "AWS::EC2::RouteTable", Properties: { VpcId: { Ref: "Vpc" } } },
|
|
63
|
+
PrivateAssoc: {
|
|
64
|
+
Type: "AWS::EC2::SubnetRouteTableAssociation",
|
|
65
|
+
Properties: { SubnetId: { Ref: "PrivateSubnet" }, RouteTableId: { Ref: "PrivateRouteTable" } },
|
|
66
|
+
},
|
|
67
|
+
DefaultRoute: {
|
|
68
|
+
Type: "AWS::EC2::Route",
|
|
69
|
+
Properties: { RouteTableId: { Ref: "PrivateRouteTable" }, DestinationCidrBlock: "0.0.0.0/0", NatGatewayId: { Ref: "NatGateway" } },
|
|
70
|
+
},
|
|
71
|
+
NatGateway: { Type: "AWS::EC2::NatGateway", Properties: { SubnetId: { Ref: "PublicSubnet" } } },
|
|
72
|
+
PublicSubnet: { Type: "AWS::EC2::Subnet", Properties: { VpcId: { Ref: "Vpc" }, CidrBlock: "10.0.0.0/24" } },
|
|
73
|
+
Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
expect(checkPrivateSubnetDefaultRoute(ctx)).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("public subnet with a default route to an existing Internet Gateway → no diagnostic (out of scope)", () => {
|
|
80
|
+
const ctx = makeCtx({
|
|
81
|
+
Resources: {
|
|
82
|
+
PublicSubnet: { Type: "AWS::EC2::Subnet", Properties: { VpcId: { Ref: "Vpc" }, CidrBlock: "10.0.0.0/24" } },
|
|
83
|
+
PublicRouteTable: { Type: "AWS::EC2::RouteTable", Properties: { VpcId: { Ref: "Vpc" } } },
|
|
84
|
+
PublicAssoc: {
|
|
85
|
+
Type: "AWS::EC2::SubnetRouteTableAssociation",
|
|
86
|
+
Properties: { SubnetId: { Ref: "PublicSubnet" }, RouteTableId: { Ref: "PublicRouteTable" } },
|
|
87
|
+
},
|
|
88
|
+
DefaultRoute: {
|
|
89
|
+
Type: "AWS::EC2::Route",
|
|
90
|
+
Properties: { RouteTableId: { Ref: "PublicRouteTable" }, DestinationCidrBlock: "0.0.0.0/0", GatewayId: { Ref: "Igw" } },
|
|
91
|
+
},
|
|
92
|
+
Igw: { Type: "AWS::EC2::InternetGateway", Properties: {} },
|
|
93
|
+
Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
expect(checkPrivateSubnetDefaultRoute(ctx)).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("subnet with no explicit route table association → no diagnostic (out of scope, can't prove)", () => {
|
|
100
|
+
const ctx = makeCtx({
|
|
101
|
+
Resources: {
|
|
102
|
+
Subnet: { Type: "AWS::EC2::Subnet", Properties: { VpcId: { Ref: "Vpc" }, CidrBlock: "10.0.1.0/24" } },
|
|
103
|
+
Vpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
expect(checkPrivateSubnetDefaultRoute(ctx)).toHaveLength(0);
|
|
107
|
+
});
|
|
108
|
+
});
|