@intentius/chant-lexicon-aws 0.1.0 → 0.1.4
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/integrity.json +25 -19
- package/dist/manifest.json +1 -1
- package/dist/meta.json +734 -435
- package/dist/rules/waw032.ts +52 -0
- package/dist/rules/waw033.ts +86 -0
- package/dist/rules/waw034.ts +63 -0
- package/dist/rules/waw035.ts +71 -0
- package/dist/rules/waw036.ts +88 -0
- package/dist/rules/waw037.ts +81 -0
- package/dist/types/index.d.ts +991 -59
- package/package.json +2 -2
- package/src/codegen/docs.ts +9 -1
- package/src/composites/composites.test.ts +65 -0
- package/src/composites/ec2-instance-role.ts +39 -0
- package/src/composites/efs-with-access-point.ts +90 -0
- package/src/composites/fargate-service.ts +102 -2
- package/src/composites/index.ts +8 -0
- package/src/composites/lambda-dynamodb.ts +66 -17
- package/src/composites/lambda-function.ts +2 -1
- package/src/composites/lambda-s3.ts +66 -20
- package/src/composites/minimal-vpc.ts +71 -0
- package/src/composites/solr-fargate-service.ts +42 -0
- package/src/generated/index.d.ts +991 -59
- package/src/generated/index.ts +34 -5
- package/src/generated/lexicon-aws.json +734 -435
- package/src/index.ts +4 -0
- package/src/lint/post-synth/waw032.test.ts +83 -0
- package/src/lint/post-synth/waw032.ts +52 -0
- package/src/lint/post-synth/waw033.test.ts +68 -0
- package/src/lint/post-synth/waw033.ts +86 -0
- package/src/lint/post-synth/waw034.test.ts +54 -0
- package/src/lint/post-synth/waw034.ts +63 -0
- package/src/lint/post-synth/waw035.test.ts +74 -0
- package/src/lint/post-synth/waw035.ts +71 -0
- package/src/lint/post-synth/waw036.test.ts +217 -0
- package/src/lint/post-synth/waw036.ts +88 -0
- package/src/lint/post-synth/waw037.test.ts +155 -0
- package/src/lint/post-synth/waw037.ts +81 -0
- package/src/serializer.ts +1 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw036, checkNonAsciiProps } from "./waw036";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW036: Non-ASCII Characters in String Properties", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw036.id).toBe("WAW036");
|
|
12
|
+
expect(waw036.description.toLowerCase()).toContain("non-ascii");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ── SecurityGroup.GroupDescription ───────────────────────────────
|
|
16
|
+
|
|
17
|
+
test("GroupDescription with em-dash → error", () => {
|
|
18
|
+
const ctx = makeCtx({
|
|
19
|
+
Resources: {
|
|
20
|
+
MySg: {
|
|
21
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
22
|
+
Properties: {
|
|
23
|
+
GroupDescription: "cluster nodes \u2014 Slurm",
|
|
24
|
+
VpcId: "vpc-123",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const diags = checkNonAsciiProps(ctx);
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
expect(diags[0].checkId).toBe("WAW036");
|
|
32
|
+
expect(diags[0].severity).toBe("error");
|
|
33
|
+
expect(diags[0].entity).toBe("MySg");
|
|
34
|
+
expect(diags[0].message).toContain("GroupDescription");
|
|
35
|
+
expect(diags[0].message).toContain("U+2014");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("GroupDescription with ASCII only → no diagnostic", () => {
|
|
39
|
+
const ctx = makeCtx({
|
|
40
|
+
Resources: {
|
|
41
|
+
MySg: {
|
|
42
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
43
|
+
Properties: {
|
|
44
|
+
GroupDescription: "cluster nodes - Slurm",
|
|
45
|
+
VpcId: "vpc-123",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const diags = checkNonAsciiProps(ctx);
|
|
51
|
+
expect(diags).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ── CloudWatch.AlarmDescription ──────────────────────────────────
|
|
55
|
+
|
|
56
|
+
test("AlarmDescription with curly quote → error", () => {
|
|
57
|
+
const ctx = makeCtx({
|
|
58
|
+
Resources: {
|
|
59
|
+
MyAlarm: {
|
|
60
|
+
Type: "AWS::CloudWatch::Alarm",
|
|
61
|
+
Properties: {
|
|
62
|
+
AlarmName: "my-alarm",
|
|
63
|
+
AlarmDescription: "FSx throughput \u201chigh\u201d",
|
|
64
|
+
MetricName: "BytesReadFromDisk",
|
|
65
|
+
Namespace: "AWS/FSx",
|
|
66
|
+
Period: 300,
|
|
67
|
+
EvaluationPeriods: 1,
|
|
68
|
+
Threshold: 1000,
|
|
69
|
+
ComparisonOperator: "GreaterThanThreshold",
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const diags = checkNonAsciiProps(ctx);
|
|
75
|
+
expect(diags).toHaveLength(1);
|
|
76
|
+
expect(diags[0].entity).toBe("MyAlarm");
|
|
77
|
+
expect(diags[0].message).toContain("AlarmDescription");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("AlarmName with accented char → error", () => {
|
|
81
|
+
const ctx = makeCtx({
|
|
82
|
+
Resources: {
|
|
83
|
+
MyAlarm: {
|
|
84
|
+
Type: "AWS::CloudWatch::Alarm",
|
|
85
|
+
Properties: {
|
|
86
|
+
AlarmName: "m\u00e9trique-alarm",
|
|
87
|
+
AlarmDescription: "normal",
|
|
88
|
+
MetricName: "CPUUtilization",
|
|
89
|
+
Namespace: "AWS/EC2",
|
|
90
|
+
Period: 60,
|
|
91
|
+
EvaluationPeriods: 1,
|
|
92
|
+
Threshold: 80,
|
|
93
|
+
ComparisonOperator: "GreaterThanThreshold",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const diags = checkNonAsciiProps(ctx);
|
|
99
|
+
expect(diags).toHaveLength(1);
|
|
100
|
+
expect(diags[0].message).toContain("AlarmName");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── IAM::Role.RoleName ───────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
test("RoleName with non-ASCII → error", () => {
|
|
106
|
+
const ctx = makeCtx({
|
|
107
|
+
Resources: {
|
|
108
|
+
MyRole: {
|
|
109
|
+
Type: "AWS::IAM::Role",
|
|
110
|
+
Properties: {
|
|
111
|
+
RoleName: "role\u2013name",
|
|
112
|
+
AssumeRolePolicyDocument: {},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const diags = checkNonAsciiProps(ctx);
|
|
118
|
+
expect(diags).toHaveLength(1);
|
|
119
|
+
expect(diags[0].message).toContain("RoleName");
|
|
120
|
+
expect(diags[0].message).toContain("U+2013");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── AutoScalingGroup.AutoScalingGroupName ────────────────────────
|
|
124
|
+
|
|
125
|
+
test("AutoScalingGroupName with non-ASCII → error", () => {
|
|
126
|
+
const ctx = makeCtx({
|
|
127
|
+
Resources: {
|
|
128
|
+
MyAsg: {
|
|
129
|
+
Type: "AWS::AutoScaling::AutoScalingGroup",
|
|
130
|
+
Properties: {
|
|
131
|
+
AutoScalingGroupName: "asg\u2014prod",
|
|
132
|
+
MinSize: "0",
|
|
133
|
+
MaxSize: "10",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const diags = checkNonAsciiProps(ctx);
|
|
139
|
+
expect(diags).toHaveLength(1);
|
|
140
|
+
expect(diags[0].entity).toBe("MyAsg");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── Non-string (intrinsic) values are skipped ────────────────────
|
|
144
|
+
|
|
145
|
+
test("GroupDescription as Ref intrinsic → no diagnostic", () => {
|
|
146
|
+
const ctx = makeCtx({
|
|
147
|
+
Resources: {
|
|
148
|
+
MySg: {
|
|
149
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
150
|
+
Properties: {
|
|
151
|
+
GroupDescription: { Ref: "SomeParam" },
|
|
152
|
+
VpcId: "vpc-123",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const diags = checkNonAsciiProps(ctx);
|
|
158
|
+
expect(diags).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── Multiple violations ──────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
test("multiple resources with non-ASCII → multiple diagnostics", () => {
|
|
164
|
+
const ctx = makeCtx({
|
|
165
|
+
Resources: {
|
|
166
|
+
Sg1: {
|
|
167
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
168
|
+
Properties: {
|
|
169
|
+
GroupDescription: "sg \u2014 one",
|
|
170
|
+
VpcId: "vpc-123",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
Sg2: {
|
|
174
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
175
|
+
Properties: {
|
|
176
|
+
GroupDescription: "sg \u2014 two",
|
|
177
|
+
VpcId: "vpc-456",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
CleanSg: {
|
|
181
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
182
|
+
Properties: {
|
|
183
|
+
GroupDescription: "clean sg",
|
|
184
|
+
VpcId: "vpc-789",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const diags = checkNonAsciiProps(ctx);
|
|
190
|
+
expect(diags).toHaveLength(2);
|
|
191
|
+
const entities = diags.map((d) => d.entity).sort();
|
|
192
|
+
expect(entities).toEqual(["Sg1", "Sg2"]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Resource types not in the list ──────────────────────────────
|
|
196
|
+
|
|
197
|
+
test("unmonitored resource type with non-ASCII → no diagnostic", () => {
|
|
198
|
+
const ctx = makeCtx({
|
|
199
|
+
Resources: {
|
|
200
|
+
MyTable: {
|
|
201
|
+
Type: "AWS::DynamoDB::Table",
|
|
202
|
+
Properties: {
|
|
203
|
+
TableName: "table\u2014name",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
const diags = checkNonAsciiProps(ctx);
|
|
209
|
+
expect(diags).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("empty Resources → no diagnostic", () => {
|
|
213
|
+
const ctx = makeCtx({ Resources: {} });
|
|
214
|
+
const diags = checkNonAsciiProps(ctx);
|
|
215
|
+
expect(diags).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW036: Non-ASCII Characters in EC2/IAM String Properties
|
|
3
|
+
*
|
|
4
|
+
* EC2, IAM, CloudWatch, and other AWS services only accept ASCII 0x20–0x7E in
|
|
5
|
+
* description, name, and label fields. Non-ASCII characters (em-dashes, curly
|
|
6
|
+
* quotes, accented letters, etc.) cause the changeset to fail at EarlyValidation
|
|
7
|
+
* with an opaque "Invalid parameter" error that doesn't name the offending property.
|
|
8
|
+
*
|
|
9
|
+
* Properties checked (all must be plain ASCII strings):
|
|
10
|
+
* - AWS::EC2::SecurityGroup GroupDescription
|
|
11
|
+
* - AWS::EC2::LaunchTemplate LaunchTemplateName
|
|
12
|
+
* - AWS::IAM::Role RoleName
|
|
13
|
+
* - AWS::Lambda::Function FunctionName
|
|
14
|
+
* - AWS::RDS::DBSubnetGroup DBSubnetGroupDescription
|
|
15
|
+
* - AWS::CloudWatch::Alarm AlarmDescription, AlarmName
|
|
16
|
+
* - AWS::AutoScaling::AutoScalingGroup AutoScalingGroupName
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
20
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
21
|
+
|
|
22
|
+
/** Map of CFN resource type → list of property names that must be ASCII-only. */
|
|
23
|
+
const ASCII_REQUIRED: Record<string, string[]> = {
|
|
24
|
+
"AWS::EC2::SecurityGroup": ["GroupDescription"],
|
|
25
|
+
"AWS::EC2::LaunchTemplate": ["LaunchTemplateName"],
|
|
26
|
+
"AWS::IAM::Role": ["RoleName"],
|
|
27
|
+
"AWS::Lambda::Function": ["FunctionName"],
|
|
28
|
+
"AWS::RDS::DBSubnetGroup": ["DBSubnetGroupDescription"],
|
|
29
|
+
"AWS::CloudWatch::Alarm": ["AlarmDescription", "AlarmName"],
|
|
30
|
+
"AWS::AutoScaling::AutoScalingGroup": ["AutoScalingGroupName"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Return true if the string contains any character outside ASCII printable range (0x20–0x7E). */
|
|
34
|
+
function hasNonAscii(s: string): boolean {
|
|
35
|
+
for (let i = 0; i < s.length; i++) {
|
|
36
|
+
const code = s.charCodeAt(i);
|
|
37
|
+
if (code < 0x20 || code > 0x7e) return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function checkNonAsciiProps(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
43
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
44
|
+
|
|
45
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
46
|
+
const template = parseCFTemplate(output);
|
|
47
|
+
if (!template?.Resources) continue;
|
|
48
|
+
|
|
49
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
50
|
+
const propsToCheck = ASCII_REQUIRED[resource.Type];
|
|
51
|
+
if (!propsToCheck) continue;
|
|
52
|
+
|
|
53
|
+
const props = resource.Properties ?? {};
|
|
54
|
+
|
|
55
|
+
for (const propName of propsToCheck) {
|
|
56
|
+
const value = props[propName];
|
|
57
|
+
if (typeof value !== "string") continue;
|
|
58
|
+
if (!hasNonAscii(value)) continue;
|
|
59
|
+
|
|
60
|
+
// Find the specific offending characters for the message.
|
|
61
|
+
const badChars = [...new Set([...value].filter((c) => {
|
|
62
|
+
const code = c.charCodeAt(0);
|
|
63
|
+
return code < 0x20 || code > 0x7e;
|
|
64
|
+
}))];
|
|
65
|
+
const charList = badChars.map((c) => `U+${c.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`).join(", ");
|
|
66
|
+
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
checkId: "WAW036",
|
|
69
|
+
severity: "error",
|
|
70
|
+
message: `${resource.Type} "${logicalId}" property "${propName}" contains non-ASCII characters (${charList}) — AWS rejects these at changeset validation time`,
|
|
71
|
+
entity: logicalId,
|
|
72
|
+
lexicon: "aws",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return diagnostics;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const waw036: PostSynthCheck = {
|
|
82
|
+
id: "WAW036",
|
|
83
|
+
description: "Non-ASCII characters in EC2/IAM/CW string properties — rejected at changeset time",
|
|
84
|
+
|
|
85
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
86
|
+
return checkNonAsciiProps(ctx);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw037, checkNullProperties } from "./waw037";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW037: Null Values in CloudFormation Resource Properties", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw037.id).toBe("WAW037");
|
|
12
|
+
expect(waw037.description.toLowerCase()).toContain("null");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ── Top-level null property ──────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
test("top-level null property → error with path", () => {
|
|
18
|
+
const ctx = makeCtx({
|
|
19
|
+
Resources: {
|
|
20
|
+
MyRole: {
|
|
21
|
+
Type: "AWS::IAM::InstanceProfile",
|
|
22
|
+
Properties: {
|
|
23
|
+
Roles: [null],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const diags = checkNullProperties(ctx);
|
|
29
|
+
expect(diags).toHaveLength(1);
|
|
30
|
+
expect(diags[0].checkId).toBe("WAW037");
|
|
31
|
+
expect(diags[0].severity).toBe("error");
|
|
32
|
+
expect(diags[0].entity).toBe("MyRole");
|
|
33
|
+
expect(diags[0].message).toContain("MyRole");
|
|
34
|
+
expect(diags[0].message).toContain("Roles[0]");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("nested null property → error with dotted path", () => {
|
|
38
|
+
const ctx = makeCtx({
|
|
39
|
+
Resources: {
|
|
40
|
+
MyHook: {
|
|
41
|
+
Type: "AWS::AutoScaling::LifecycleHook",
|
|
42
|
+
Properties: {
|
|
43
|
+
AutoScalingGroupName: null,
|
|
44
|
+
LifecycleTransition: "autoscaling:EC2_INSTANCE_TERMINATING",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const diags = checkNullProperties(ctx);
|
|
50
|
+
expect(diags).toHaveLength(1);
|
|
51
|
+
expect(diags[0].entity).toBe("MyHook");
|
|
52
|
+
expect(diags[0].message).toContain("AutoScalingGroupName");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("deeply nested null in array → correct path reported", () => {
|
|
56
|
+
const ctx = makeCtx({
|
|
57
|
+
Resources: {
|
|
58
|
+
MyLt: {
|
|
59
|
+
Type: "AWS::EC2::LaunchTemplate",
|
|
60
|
+
Properties: {
|
|
61
|
+
LaunchTemplateData: {
|
|
62
|
+
TagSpecifications: [
|
|
63
|
+
{
|
|
64
|
+
ResourceType: "instance",
|
|
65
|
+
Tags: [{ Key: "cluster", Value: null }],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const diags = checkNullProperties(ctx);
|
|
74
|
+
expect(diags).toHaveLength(1);
|
|
75
|
+
expect(diags[0].message).toContain("LaunchTemplateData.TagSpecifications[0].Tags[0].Value");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("IamInstanceProfile null from wrong attr → error", () => {
|
|
79
|
+
const ctx = makeCtx({
|
|
80
|
+
Resources: {
|
|
81
|
+
MyInstance: {
|
|
82
|
+
Type: "AWS::EC2::Instance",
|
|
83
|
+
Properties: {
|
|
84
|
+
InstanceType: "c5.2xlarge",
|
|
85
|
+
IamInstanceProfile: null,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const diags = checkNullProperties(ctx);
|
|
91
|
+
expect(diags).toHaveLength(1);
|
|
92
|
+
expect(diags[0].entity).toBe("MyInstance");
|
|
93
|
+
expect(diags[0].message).toContain("IamInstanceProfile");
|
|
94
|
+
expect(diags[0].message).toContain("Ref(resource)");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── Clean template ───────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
test("all non-null properties → no diagnostic", () => {
|
|
100
|
+
const ctx = makeCtx({
|
|
101
|
+
Resources: {
|
|
102
|
+
MySg: {
|
|
103
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
104
|
+
Properties: {
|
|
105
|
+
GroupDescription: "my sg",
|
|
106
|
+
VpcId: { Ref: "MyVpc" },
|
|
107
|
+
SecurityGroupIngress: [
|
|
108
|
+
{ IpProtocol: "tcp", FromPort: 443, ToPort: 443, CidrIp: "10.0.0.0/8" },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const diags = checkNullProperties(ctx);
|
|
115
|
+
expect(diags).toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("resource with no Properties → no diagnostic", () => {
|
|
119
|
+
const ctx = makeCtx({
|
|
120
|
+
Resources: {
|
|
121
|
+
MyWaitHandle: {
|
|
122
|
+
Type: "AWS::CloudFormation::WaitConditionHandle",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const diags = checkNullProperties(ctx);
|
|
127
|
+
expect(diags).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── Multiple nulls in same resource ─────────────────────────────
|
|
131
|
+
|
|
132
|
+
test("multiple null props in same resource → one diagnostic per null", () => {
|
|
133
|
+
const ctx = makeCtx({
|
|
134
|
+
Resources: {
|
|
135
|
+
MyHook: {
|
|
136
|
+
Type: "AWS::AutoScaling::LifecycleHook",
|
|
137
|
+
Properties: {
|
|
138
|
+
AutoScalingGroupName: null,
|
|
139
|
+
LifecycleHookName: null,
|
|
140
|
+
LifecycleTransition: "autoscaling:EC2_INSTANCE_TERMINATING",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
const diags = checkNullProperties(ctx);
|
|
146
|
+
expect(diags).toHaveLength(2);
|
|
147
|
+
expect(diags.every((d) => d.entity === "MyHook")).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("empty Resources → no diagnostic", () => {
|
|
151
|
+
const ctx = makeCtx({ Resources: {} });
|
|
152
|
+
const diags = checkNullProperties(ctx);
|
|
153
|
+
expect(diags).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW037: Null Values in CloudFormation Resource Properties
|
|
3
|
+
*
|
|
4
|
+
* `resource.PropName` where PropName is not a real GetAtt attribute returns null
|
|
5
|
+
* silently in chant's AttrRef system — the TypeScript types say `string` but the
|
|
6
|
+
* runtime value is null. This produces a template with literal null values that
|
|
7
|
+
* CloudFormation rejects at changeset time with an unhelpful "Invalid template"
|
|
8
|
+
* error.
|
|
9
|
+
*
|
|
10
|
+
* Common causes:
|
|
11
|
+
* - `resource.SomeId` instead of `Ref(resource)` (use Ref for the primary identifier)
|
|
12
|
+
* - `resource.SomeProp` where SomeProp is not listed in AWS CloudFormation GetAtt docs
|
|
13
|
+
* - Typo in an attribute name (e.g. `resource.GroupName` vs `resource.GroupId`)
|
|
14
|
+
*
|
|
15
|
+
* This check scans every resource's Properties for null values at any depth and
|
|
16
|
+
* reports the logical resource ID and dotted property path.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
20
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
21
|
+
|
|
22
|
+
interface NullLocation {
|
|
23
|
+
logicalId: string;
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Recursively collect all paths where the value is null. */
|
|
28
|
+
function collectNullPaths(value: unknown, path: string, results: NullLocation[], logicalId: string): void {
|
|
29
|
+
if (value === null) {
|
|
30
|
+
results.push({ logicalId, path });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (let i = 0; i < value.length; i++) {
|
|
35
|
+
collectNullPaths(value[i], `${path}[${i}]`, results, logicalId);
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "object") {
|
|
40
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
41
|
+
collectNullPaths(v, path ? `${path}.${k}` : k, results, logicalId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkNullProperties(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
47
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
48
|
+
|
|
49
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
50
|
+
const template = parseCFTemplate(output);
|
|
51
|
+
if (!template?.Resources) continue;
|
|
52
|
+
|
|
53
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
54
|
+
if (!resource.Properties) continue;
|
|
55
|
+
|
|
56
|
+
const nullLocations: NullLocation[] = [];
|
|
57
|
+
collectNullPaths(resource.Properties, "", nullLocations, logicalId);
|
|
58
|
+
|
|
59
|
+
for (const loc of nullLocations) {
|
|
60
|
+
diagnostics.push({
|
|
61
|
+
checkId: "WAW037",
|
|
62
|
+
severity: "error",
|
|
63
|
+
message: `${resource.Type} "${logicalId}" has a null value at Properties.${loc.path} — likely a .PropName AttrRef on a non-existent GetAtt attribute. Use Ref(resource) for the primary identifier, or check the attribute name.`,
|
|
64
|
+
entity: logicalId,
|
|
65
|
+
lexicon: "aws",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return diagnostics;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const waw037: PostSynthCheck = {
|
|
75
|
+
id: "WAW037",
|
|
76
|
+
description: "Null values in CFN resource properties — caused by invalid AttrRef (.PropName) usage",
|
|
77
|
+
|
|
78
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
79
|
+
return checkNullProperties(ctx);
|
|
80
|
+
},
|
|
81
|
+
};
|
package/src/serializer.ts
CHANGED
|
@@ -303,9 +303,7 @@ function serializeToTemplate(
|
|
|
303
303
|
template.Outputs = template.Outputs ?? {};
|
|
304
304
|
for (const output of outputs) {
|
|
305
305
|
template.Outputs[output.outputName] = {
|
|
306
|
-
Value:
|
|
307
|
-
"Fn::GetAtt": [output.sourceEntity, output.sourceAttribute],
|
|
308
|
-
},
|
|
306
|
+
Value: output.getOutputValue(),
|
|
309
307
|
};
|
|
310
308
|
}
|
|
311
309
|
}
|