@intentius/chant-lexicon-aws 0.0.6 → 0.0.9
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 -10
- package/dist/manifest.json +1 -1
- package/dist/meta.json +9444 -4597
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +32 -25
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +3 -3
- package/dist/rules/waw016.ts +86 -0
- package/dist/rules/waw017.ts +53 -0
- package/dist/rules/waw018.ts +71 -0
- package/dist/rules/waw019.ts +82 -0
- package/dist/rules/waw020.ts +64 -0
- package/dist/rules/waw021.ts +53 -0
- package/dist/rules/waw022.ts +43 -0
- package/dist/rules/waw023.ts +47 -0
- package/dist/rules/waw024.ts +54 -0
- package/dist/rules/waw025.ts +43 -0
- package/dist/rules/waw026.ts +46 -0
- package/dist/rules/waw027.ts +50 -0
- package/dist/rules/waw028.ts +47 -0
- package/dist/rules/waw029.ts +62 -0
- package/dist/rules/waw030.ts +246 -0
- package/dist/skills/chant-aws.md +430 -0
- package/dist/types/index.d.ts +58525 -58501
- package/package.json +2 -2
- package/src/actions/actions.test.ts +75 -0
- package/src/actions/dynamodb.ts +36 -0
- package/src/actions/ecr.ts +9 -0
- package/src/actions/ecs.ts +5 -0
- package/src/actions/iam.ts +3 -0
- package/src/actions/index.ts +9 -0
- package/src/actions/lambda.ts +11 -0
- package/src/actions/logs.ts +4 -0
- package/src/actions/s3.ts +34 -0
- package/src/actions/sns.ts +5 -0
- package/src/actions/sqs.ts +15 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +20 -20
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +294 -124
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -1
- package/src/codegen/generate.ts +1 -13
- package/src/codegen/package.ts +2 -0
- package/src/codegen/typecheck.test.ts +1 -1
- package/src/composites/composites.test.ts +442 -0
- package/src/composites/fargate-alb.ts +253 -0
- package/src/composites/index.ts +20 -0
- package/src/composites/lambda-api.ts +20 -0
- package/src/composites/lambda-dynamodb.ts +64 -0
- package/src/composites/lambda-eventbridge.ts +36 -0
- package/src/composites/lambda-function.ts +76 -0
- package/src/composites/lambda-s3.ts +72 -0
- package/src/composites/lambda-sns.ts +30 -0
- package/src/composites/lambda-sqs.ts +44 -0
- package/src/composites/scheduled-lambda.ts +37 -0
- package/src/composites/vpc-default.ts +148 -0
- package/src/default-tags.test.ts +38 -0
- package/src/default-tags.ts +77 -0
- package/src/generated/index.d.ts +58525 -58501
- package/src/generated/index.ts +1351 -1351
- package/src/generated/lexicon-aws.json +9444 -4597
- package/src/import/generator.test.ts +5 -5
- package/src/import/generator.ts +4 -4
- package/src/import/roundtrip-fixtures.test.ts +2 -1
- package/src/import/roundtrip.test.ts +5 -5
- package/src/index.ts +21 -0
- package/src/integration.test.ts +92 -21
- package/src/intrinsics.ts +24 -13
- package/src/lint/post-synth/cf-refs.ts +99 -0
- package/src/lint/post-synth/ext001.test.ts +214 -31
- package/src/lint/post-synth/ext001.ts +32 -25
- package/src/lint/post-synth/waw013.test.ts +120 -0
- package/src/lint/post-synth/waw014.test.ts +121 -0
- package/src/lint/post-synth/waw015.test.ts +147 -0
- package/src/lint/post-synth/waw016.test.ts +141 -0
- package/src/lint/post-synth/waw016.ts +86 -0
- package/src/lint/post-synth/waw017.test.ts +130 -0
- package/src/lint/post-synth/waw017.ts +53 -0
- package/src/lint/post-synth/waw018.test.ts +109 -0
- package/src/lint/post-synth/waw018.ts +71 -0
- package/src/lint/post-synth/waw019.test.ts +138 -0
- package/src/lint/post-synth/waw019.ts +82 -0
- package/src/lint/post-synth/waw020.test.ts +125 -0
- package/src/lint/post-synth/waw020.ts +64 -0
- package/src/lint/post-synth/waw021.test.ts +81 -0
- package/src/lint/post-synth/waw021.ts +53 -0
- package/src/lint/post-synth/waw022.test.ts +54 -0
- package/src/lint/post-synth/waw022.ts +43 -0
- package/src/lint/post-synth/waw023.test.ts +53 -0
- package/src/lint/post-synth/waw023.ts +47 -0
- package/src/lint/post-synth/waw024.test.ts +64 -0
- package/src/lint/post-synth/waw024.ts +54 -0
- package/src/lint/post-synth/waw025.test.ts +42 -0
- package/src/lint/post-synth/waw025.ts +43 -0
- package/src/lint/post-synth/waw026.test.ts +54 -0
- package/src/lint/post-synth/waw026.ts +46 -0
- package/src/lint/post-synth/waw027.test.ts +63 -0
- package/src/lint/post-synth/waw027.ts +50 -0
- package/src/lint/post-synth/waw028.test.ts +68 -0
- package/src/lint/post-synth/waw028.ts +47 -0
- package/src/lint/post-synth/waw029.test.ts +179 -0
- package/src/lint/post-synth/waw029.ts +62 -0
- package/src/lint/post-synth/waw030.test.ts +800 -0
- package/src/lint/post-synth/waw030.ts +246 -0
- package/src/lint/rules/hardcoded-region.ts +1 -0
- package/src/lint/rules/iam-wildcard.ts +1 -0
- package/src/lint/rules/rules.test.ts +8 -8
- package/src/lint/rules/s3-encryption.ts +3 -3
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +17 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +2 -2
- package/src/plugin.test.ts +13 -15
- package/src/plugin.ts +552 -114
- package/src/serializer.test.ts +370 -43
- package/src/serializer.ts +69 -17
- package/src/spec/fetch.ts +10 -0
- package/src/spec/parse.test.ts +141 -0
- package/src/spec/parse.ts +40 -0
- package/src/taggable.ts +44 -0
- package/src/testdata/nested-stacks/app.ts +26 -0
- package/src/testdata/nested-stacks/network/outputs.ts +17 -0
- package/src/testdata/nested-stacks/network/security.ts +17 -0
- package/src/testdata/nested-stacks/network/vpc.ts +54 -0
- package/dist/skills/aws-cloudformation.md +0 -41
- package/src/codegen/rollback.test.ts +0 -80
- package/src/codegen/rollback.ts +0 -20
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
|
+
import { CHILD_PROJECT_MARKER, type ChildProjectInstance } from "@intentius/chant/child-project";
|
|
5
|
+
import { DECLARABLE_MARKER } from "@intentius/chant/declarable";
|
|
6
|
+
import { waw015 } from "./waw015";
|
|
7
|
+
|
|
8
|
+
function makeChildProject(
|
|
9
|
+
projectPath: string,
|
|
10
|
+
childEntities?: Map<string, Declarable>,
|
|
11
|
+
): ChildProjectInstance {
|
|
12
|
+
return {
|
|
13
|
+
[CHILD_PROJECT_MARKER]: true,
|
|
14
|
+
[DECLARABLE_MARKER]: true,
|
|
15
|
+
lexicon: "aws",
|
|
16
|
+
entityType: "AWS::CloudFormation::Stack",
|
|
17
|
+
kind: "resource",
|
|
18
|
+
projectPath,
|
|
19
|
+
logicalName: "Child",
|
|
20
|
+
outputs: {},
|
|
21
|
+
options: {},
|
|
22
|
+
...(childEntities && {
|
|
23
|
+
buildResult: {
|
|
24
|
+
outputs: new Map(),
|
|
25
|
+
entities: childEntities,
|
|
26
|
+
warnings: [],
|
|
27
|
+
errors: [],
|
|
28
|
+
sourceFileCount: 1,
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function makeCtx(entities: Map<string, Declarable>): PostSynthContext {
|
|
35
|
+
return {
|
|
36
|
+
outputs: new Map(),
|
|
37
|
+
entities,
|
|
38
|
+
buildResult: {
|
|
39
|
+
outputs: new Map(),
|
|
40
|
+
entities,
|
|
41
|
+
warnings: [],
|
|
42
|
+
errors: [],
|
|
43
|
+
sourceFileCount: 1,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("WAW015: Circular Project References", () => {
|
|
49
|
+
test("check metadata", () => {
|
|
50
|
+
expect(waw015.id).toBe("WAW015");
|
|
51
|
+
expect(waw015.description).toContain("Circular");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("detects simple two-node cycle (A → B → A)", () => {
|
|
55
|
+
// A's child entities include a child project pointing to B's path
|
|
56
|
+
// B's child entities include a child project pointing to A's path
|
|
57
|
+
const bInA = makeChildProject("/tmp/B");
|
|
58
|
+
const aInB = makeChildProject("/tmp/A");
|
|
59
|
+
|
|
60
|
+
const childA = makeChildProject("/tmp/A", new Map<string, Declarable>([
|
|
61
|
+
["B", bInA],
|
|
62
|
+
]));
|
|
63
|
+
const childB = makeChildProject("/tmp/B", new Map<string, Declarable>([
|
|
64
|
+
["A", aInB],
|
|
65
|
+
]));
|
|
66
|
+
|
|
67
|
+
const entities = new Map<string, Declarable>([
|
|
68
|
+
["A", childA],
|
|
69
|
+
["B", childB],
|
|
70
|
+
]);
|
|
71
|
+
const ctx = makeCtx(entities);
|
|
72
|
+
const diags = waw015.check(ctx);
|
|
73
|
+
expect(diags).toHaveLength(1);
|
|
74
|
+
expect(diags[0].checkId).toBe("WAW015");
|
|
75
|
+
expect(diags[0].severity).toBe("error");
|
|
76
|
+
expect(diags[0].message).toContain("Circular");
|
|
77
|
+
expect(diags[0].message).toContain("A");
|
|
78
|
+
expect(diags[0].message).toContain("B");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("detects three-node cycle (A → B → C → A)", () => {
|
|
82
|
+
const childA = makeChildProject("/tmp/A", new Map<string, Declarable>([
|
|
83
|
+
["B", makeChildProject("/tmp/B")],
|
|
84
|
+
]));
|
|
85
|
+
const childB = makeChildProject("/tmp/B", new Map<string, Declarable>([
|
|
86
|
+
["C", makeChildProject("/tmp/C")],
|
|
87
|
+
]));
|
|
88
|
+
const childC = makeChildProject("/tmp/C", new Map<string, Declarable>([
|
|
89
|
+
["A", makeChildProject("/tmp/A")],
|
|
90
|
+
]));
|
|
91
|
+
|
|
92
|
+
const entities = new Map<string, Declarable>([
|
|
93
|
+
["A", childA],
|
|
94
|
+
["B", childB],
|
|
95
|
+
["C", childC],
|
|
96
|
+
]);
|
|
97
|
+
const ctx = makeCtx(entities);
|
|
98
|
+
const diags = waw015.check(ctx);
|
|
99
|
+
expect(diags).toHaveLength(1);
|
|
100
|
+
expect(diags[0].message).toContain("A");
|
|
101
|
+
expect(diags[0].message).toContain("B");
|
|
102
|
+
expect(diags[0].message).toContain("C");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("no diagnostic for acyclic graph (A → B, no back-edge)", () => {
|
|
106
|
+
const childA = makeChildProject("/tmp/A", new Map<string, Declarable>([
|
|
107
|
+
["B", makeChildProject("/tmp/B")],
|
|
108
|
+
]));
|
|
109
|
+
const childB = makeChildProject("/tmp/B", new Map<string, Declarable>());
|
|
110
|
+
|
|
111
|
+
const entities = new Map<string, Declarable>([
|
|
112
|
+
["A", childA],
|
|
113
|
+
["B", childB],
|
|
114
|
+
]);
|
|
115
|
+
const ctx = makeCtx(entities);
|
|
116
|
+
const diags = waw015.check(ctx);
|
|
117
|
+
expect(diags).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("no diagnostic with fewer than 2 stacks", () => {
|
|
121
|
+
const childA = makeChildProject("/tmp/A", new Map());
|
|
122
|
+
const entities = new Map<string, Declarable>([["A", childA]]);
|
|
123
|
+
const ctx = makeCtx(entities);
|
|
124
|
+
const diags = waw015.check(ctx);
|
|
125
|
+
expect(diags).toHaveLength(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("no diagnostic with no child projects", () => {
|
|
129
|
+
const ctx = makeCtx(new Map());
|
|
130
|
+
const diags = waw015.check(ctx);
|
|
131
|
+
expect(diags).toHaveLength(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("handles child without buildResult (not yet built)", () => {
|
|
135
|
+
// No buildResult means no edges — no cycle possible
|
|
136
|
+
const childA = makeChildProject("/tmp/A");
|
|
137
|
+
const childB = makeChildProject("/tmp/B");
|
|
138
|
+
// These have no buildResult, so deps map will be empty
|
|
139
|
+
const entities = new Map<string, Declarable>([
|
|
140
|
+
["A", childA],
|
|
141
|
+
["B", childB],
|
|
142
|
+
]);
|
|
143
|
+
const ctx = makeCtx(entities);
|
|
144
|
+
const diags = waw015.check(ctx);
|
|
145
|
+
expect(diags).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw016, checkDeprecatedProperties } from "./waw016";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Synthetic deprecated-property map — no disk dependency. */
|
|
10
|
+
function fakeDeprecated(): Map<string, Set<string>> {
|
|
11
|
+
return new Map([
|
|
12
|
+
["AWS::S3::Bucket", new Set(["AccessControl", "ObjectLockConfiguration"])],
|
|
13
|
+
["AWS::Lambda::Function", new Set(["Code"])],
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("WAW016: Deprecated Property Usage", () => {
|
|
18
|
+
test("check metadata", () => {
|
|
19
|
+
expect(waw016.id).toBe("WAW016");
|
|
20
|
+
expect(waw016.description).toContain("Deprecated");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("emits warning for deprecated property", () => {
|
|
24
|
+
const ctx = makeCtx({
|
|
25
|
+
Resources: {
|
|
26
|
+
MyBucket: {
|
|
27
|
+
Type: "AWS::S3::Bucket",
|
|
28
|
+
Properties: {
|
|
29
|
+
AccessControl: "LogDeliveryWrite",
|
|
30
|
+
BucketName: "my-bucket",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
36
|
+
expect(diags).toHaveLength(1);
|
|
37
|
+
expect(diags[0].checkId).toBe("WAW016");
|
|
38
|
+
expect(diags[0].severity).toBe("warning");
|
|
39
|
+
expect(diags[0].message).toContain("AccessControl");
|
|
40
|
+
expect(diags[0].message).toContain("MyBucket");
|
|
41
|
+
expect(diags[0].message).toContain("deprecated");
|
|
42
|
+
expect(diags[0].entity).toBe("MyBucket");
|
|
43
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("emits one warning per deprecated property", () => {
|
|
47
|
+
const ctx = makeCtx({
|
|
48
|
+
Resources: {
|
|
49
|
+
MyBucket: {
|
|
50
|
+
Type: "AWS::S3::Bucket",
|
|
51
|
+
Properties: {
|
|
52
|
+
AccessControl: "Private",
|
|
53
|
+
ObjectLockConfiguration: {},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
59
|
+
expect(diags).toHaveLength(2);
|
|
60
|
+
const props = diags.map((d) => d.message);
|
|
61
|
+
expect(props.some((m) => m.includes("AccessControl"))).toBe(true);
|
|
62
|
+
expect(props.some((m) => m.includes("ObjectLockConfiguration"))).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("no diagnostic for non-deprecated properties", () => {
|
|
66
|
+
const ctx = makeCtx({
|
|
67
|
+
Resources: {
|
|
68
|
+
MyBucket: {
|
|
69
|
+
Type: "AWS::S3::Bucket",
|
|
70
|
+
Properties: {
|
|
71
|
+
BucketName: "clean-bucket",
|
|
72
|
+
VersioningConfiguration: { Status: "Enabled" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
78
|
+
expect(diags).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("no diagnostic for resource type not in map", () => {
|
|
82
|
+
const ctx = makeCtx({
|
|
83
|
+
Resources: {
|
|
84
|
+
MyRole: {
|
|
85
|
+
Type: "AWS::IAM::Role",
|
|
86
|
+
Properties: { RoleName: "test" },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
91
|
+
expect(diags).toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("no diagnostic on empty template", () => {
|
|
95
|
+
const ctx = makeCtx({ Resources: {} });
|
|
96
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
97
|
+
expect(diags).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("handles resource with no Properties", () => {
|
|
101
|
+
const ctx = makeCtx({
|
|
102
|
+
Resources: {
|
|
103
|
+
MyBucket: { Type: "AWS::S3::Bucket" },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
107
|
+
expect(diags).toHaveLength(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns empty when deprecated map is empty", () => {
|
|
111
|
+
const ctx = makeCtx({
|
|
112
|
+
Resources: {
|
|
113
|
+
MyBucket: {
|
|
114
|
+
Type: "AWS::S3::Bucket",
|
|
115
|
+
Properties: { AccessControl: "Private" },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const diags = checkDeprecatedProperties(ctx, new Map());
|
|
120
|
+
expect(diags).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("flags deprecated properties across multiple resources", () => {
|
|
124
|
+
const ctx = makeCtx({
|
|
125
|
+
Resources: {
|
|
126
|
+
Bucket: {
|
|
127
|
+
Type: "AWS::S3::Bucket",
|
|
128
|
+
Properties: { AccessControl: "Private" },
|
|
129
|
+
},
|
|
130
|
+
Func: {
|
|
131
|
+
Type: "AWS::Lambda::Function",
|
|
132
|
+
Properties: { Code: {} },
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const diags = checkDeprecatedProperties(ctx, fakeDeprecated());
|
|
137
|
+
expect(diags).toHaveLength(2);
|
|
138
|
+
expect(diags[0].entity).toBe("Bucket");
|
|
139
|
+
expect(diags[1].entity).toBe("Func");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW016: Deprecated Property Usage
|
|
3
|
+
*
|
|
4
|
+
* Flags properties marked as deprecated in the CloudFormation Registry.
|
|
5
|
+
* Sources: explicit `deprecatedProperties` array + description text mining.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
12
|
+
|
|
13
|
+
interface LexiconEntry {
|
|
14
|
+
kind: string;
|
|
15
|
+
resourceType: string;
|
|
16
|
+
deprecatedProperties?: string[];
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load deprecated properties per resource type from the lexicon JSON.
|
|
22
|
+
*/
|
|
23
|
+
function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
24
|
+
const map = new Map<string, Set<string>>();
|
|
25
|
+
try {
|
|
26
|
+
const pkgDir = join(__dirname, "..", "..", "..");
|
|
27
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
28
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
29
|
+
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
30
|
+
|
|
31
|
+
for (const [_name, entry] of Object.entries(data)) {
|
|
32
|
+
if (entry.kind === "resource" && entry.resourceType && entry.deprecatedProperties && entry.deprecatedProperties.length > 0) {
|
|
33
|
+
map.set(entry.resourceType, new Set(entry.deprecatedProperties));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Lexicon not available — skip
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
44
|
+
*/
|
|
45
|
+
export function checkDeprecatedProperties(
|
|
46
|
+
ctx: PostSynthContext,
|
|
47
|
+
deprecated: Map<string, Set<string>>,
|
|
48
|
+
): PostSynthDiagnostic[] {
|
|
49
|
+
if (deprecated.size === 0) return [];
|
|
50
|
+
|
|
51
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
52
|
+
|
|
53
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
54
|
+
const template = parseCFTemplate(output);
|
|
55
|
+
if (!template?.Resources) continue;
|
|
56
|
+
|
|
57
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
58
|
+
const deprProps = deprecated.get(resource.Type);
|
|
59
|
+
if (!deprProps) continue;
|
|
60
|
+
|
|
61
|
+
const props = resource.Properties ?? {};
|
|
62
|
+
for (const propName of Object.keys(props)) {
|
|
63
|
+
if (deprProps.has(propName)) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
checkId: "WAW016",
|
|
66
|
+
severity: "warning",
|
|
67
|
+
message: `Resource "${logicalId}" (${resource.Type}) uses deprecated property "${propName}" — consider alternatives`,
|
|
68
|
+
entity: logicalId,
|
|
69
|
+
lexicon: "aws",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return diagnostics;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const waw016: PostSynthCheck = {
|
|
80
|
+
id: "WAW016",
|
|
81
|
+
description: "Deprecated property usage — flags properties marked as deprecated in the CloudFormation Registry",
|
|
82
|
+
|
|
83
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
84
|
+
return checkDeprecatedProperties(ctx, loadDeprecatedProperties());
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw017, checkMissingTags } from "./waw017";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Synthetic taggable set — no disk dependency. */
|
|
10
|
+
const taggable = new Set(["AWS::S3::Bucket", "AWS::Lambda::Function"]);
|
|
11
|
+
|
|
12
|
+
describe("WAW017: Missing Tags on Taggable Resource", () => {
|
|
13
|
+
test("check metadata", () => {
|
|
14
|
+
expect(waw017.id).toBe("WAW017");
|
|
15
|
+
expect(waw017.description).toContain("tags");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("emits warning for taggable resource without Tags", () => {
|
|
19
|
+
const ctx = makeCtx({
|
|
20
|
+
Resources: {
|
|
21
|
+
MyBucket: {
|
|
22
|
+
Type: "AWS::S3::Bucket",
|
|
23
|
+
Properties: { BucketName: "my-bucket" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
28
|
+
expect(diags).toHaveLength(1);
|
|
29
|
+
expect(diags[0].checkId).toBe("WAW017");
|
|
30
|
+
expect(diags[0].severity).toBe("warning");
|
|
31
|
+
expect(diags[0].message).toContain("tagging");
|
|
32
|
+
expect(diags[0].message).toContain("MyBucket");
|
|
33
|
+
expect(diags[0].entity).toBe("MyBucket");
|
|
34
|
+
expect(diags[0].lexicon).toBe("aws");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("no diagnostic when Tags are present", () => {
|
|
38
|
+
const ctx = makeCtx({
|
|
39
|
+
Resources: {
|
|
40
|
+
MyBucket: {
|
|
41
|
+
Type: "AWS::S3::Bucket",
|
|
42
|
+
Properties: {
|
|
43
|
+
BucketName: "my-bucket",
|
|
44
|
+
Tags: [{ Key: "Env", Value: "prod" }],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
50
|
+
expect(diags).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("no diagnostic for non-taggable resource type", () => {
|
|
54
|
+
const ctx = makeCtx({
|
|
55
|
+
Resources: {
|
|
56
|
+
MyCustom: {
|
|
57
|
+
Type: "Custom::MyResource",
|
|
58
|
+
Properties: { Foo: "bar" },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
63
|
+
expect(diags).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("no diagnostic on empty template", () => {
|
|
67
|
+
const ctx = makeCtx({ Resources: {} });
|
|
68
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
69
|
+
expect(diags).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("handles resource with no Properties (still flags missing Tags)", () => {
|
|
73
|
+
const ctx = makeCtx({
|
|
74
|
+
Resources: {
|
|
75
|
+
MyBucket: { Type: "AWS::S3::Bucket" },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
79
|
+
expect(diags).toHaveLength(1);
|
|
80
|
+
expect(diags[0].message).toContain("MyBucket");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("returns empty when taggable set is empty", () => {
|
|
84
|
+
const ctx = makeCtx({
|
|
85
|
+
Resources: {
|
|
86
|
+
MyBucket: {
|
|
87
|
+
Type: "AWS::S3::Bucket",
|
|
88
|
+
Properties: { BucketName: "test" },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const diags = checkMissingTags(ctx, new Set());
|
|
93
|
+
expect(diags).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("flags multiple taggable resources missing Tags", () => {
|
|
97
|
+
const ctx = makeCtx({
|
|
98
|
+
Resources: {
|
|
99
|
+
Bucket: {
|
|
100
|
+
Type: "AWS::S3::Bucket",
|
|
101
|
+
Properties: { BucketName: "b" },
|
|
102
|
+
},
|
|
103
|
+
Func: {
|
|
104
|
+
Type: "AWS::Lambda::Function",
|
|
105
|
+
Properties: { FunctionName: "f" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
110
|
+
expect(diags).toHaveLength(2);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("only flags resources without Tags, not those with", () => {
|
|
114
|
+
const ctx = makeCtx({
|
|
115
|
+
Resources: {
|
|
116
|
+
Tagged: {
|
|
117
|
+
Type: "AWS::S3::Bucket",
|
|
118
|
+
Properties: { Tags: [{ Key: "k", Value: "v" }] },
|
|
119
|
+
},
|
|
120
|
+
Untagged: {
|
|
121
|
+
Type: "AWS::S3::Bucket",
|
|
122
|
+
Properties: { BucketName: "b" },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const diags = checkMissingTags(ctx, taggable);
|
|
127
|
+
expect(diags).toHaveLength(1);
|
|
128
|
+
expect(diags[0].entity).toBe("Untagged");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW017: Missing Tags on Taggable Resource
|
|
3
|
+
*
|
|
4
|
+
* Flags taggable resources that have no Tags property set.
|
|
5
|
+
* Encourages adding tags for cost allocation and compliance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
10
|
+
import { loadTaggableResources } from "../../taggable";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
14
|
+
*/
|
|
15
|
+
export function checkMissingTags(
|
|
16
|
+
ctx: PostSynthContext,
|
|
17
|
+
taggable: Set<string>,
|
|
18
|
+
): PostSynthDiagnostic[] {
|
|
19
|
+
if (taggable.size === 0) return [];
|
|
20
|
+
|
|
21
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
22
|
+
|
|
23
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
24
|
+
const template = parseCFTemplate(output);
|
|
25
|
+
if (!template?.Resources) continue;
|
|
26
|
+
|
|
27
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
28
|
+
if (!taggable.has(resource.Type)) continue;
|
|
29
|
+
|
|
30
|
+
const props = resource.Properties ?? {};
|
|
31
|
+
if (!("Tags" in props)) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "WAW017",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `Resource "${logicalId}" (${resource.Type}) supports tagging but has no Tags — consider adding tags for cost allocation and compliance`,
|
|
36
|
+
entity: logicalId,
|
|
37
|
+
lexicon: "aws",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const waw017: PostSynthCheck = {
|
|
47
|
+
id: "WAW017",
|
|
48
|
+
description: "Missing tags on taggable resource — suggests adding tags for cost allocation and compliance",
|
|
49
|
+
|
|
50
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
51
|
+
return checkMissingTags(ctx, loadTaggableResources());
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw018, checkS3PublicAccess } from "./waw018";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW018: S3 Public Access Not Blocked", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw018.id).toBe("WAW018");
|
|
12
|
+
expect(waw018.description).toContain("public access");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags bucket missing PublicAccessBlockConfiguration", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyBucket: {
|
|
19
|
+
Type: "AWS::S3::Bucket",
|
|
20
|
+
Properties: { BucketName: "test" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkS3PublicAccess(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW018");
|
|
27
|
+
expect(diags[0].severity).toBe("error");
|
|
28
|
+
expect(diags[0].entity).toBe("MyBucket");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("flags bucket with a flag set to false", () => {
|
|
32
|
+
const ctx = makeCtx({
|
|
33
|
+
Resources: {
|
|
34
|
+
MyBucket: {
|
|
35
|
+
Type: "AWS::S3::Bucket",
|
|
36
|
+
Properties: {
|
|
37
|
+
PublicAccessBlockConfiguration: {
|
|
38
|
+
BlockPublicAcls: true,
|
|
39
|
+
BlockPublicPolicy: false,
|
|
40
|
+
IgnorePublicAcls: true,
|
|
41
|
+
RestrictPublicBuckets: true,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const diags = checkS3PublicAccess(ctx);
|
|
48
|
+
expect(diags).toHaveLength(1);
|
|
49
|
+
expect(diags[0].message).toContain("BlockPublicPolicy");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("no diagnostic when all flags are true", () => {
|
|
53
|
+
const ctx = makeCtx({
|
|
54
|
+
Resources: {
|
|
55
|
+
MyBucket: {
|
|
56
|
+
Type: "AWS::S3::Bucket",
|
|
57
|
+
Properties: {
|
|
58
|
+
PublicAccessBlockConfiguration: {
|
|
59
|
+
BlockPublicAcls: true,
|
|
60
|
+
BlockPublicPolicy: true,
|
|
61
|
+
IgnorePublicAcls: true,
|
|
62
|
+
RestrictPublicBuckets: true,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
const diags = checkS3PublicAccess(ctx);
|
|
69
|
+
expect(diags).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("no diagnostic for non-S3 resources", () => {
|
|
73
|
+
const ctx = makeCtx({
|
|
74
|
+
Resources: {
|
|
75
|
+
MyFunc: {
|
|
76
|
+
Type: "AWS::Lambda::Function",
|
|
77
|
+
Properties: { FunctionName: "test" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const diags = checkS3PublicAccess(ctx);
|
|
82
|
+
expect(diags).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("skips intrinsic value in PublicAccessBlockConfiguration", () => {
|
|
86
|
+
const ctx = makeCtx({
|
|
87
|
+
Resources: {
|
|
88
|
+
MyBucket: {
|
|
89
|
+
Type: "AWS::S3::Bucket",
|
|
90
|
+
Properties: {
|
|
91
|
+
PublicAccessBlockConfiguration: { Ref: "PublicAccessParam" },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const diags = checkS3PublicAccess(ctx);
|
|
97
|
+
expect(diags).toHaveLength(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("handles missing Properties", () => {
|
|
101
|
+
const ctx = makeCtx({
|
|
102
|
+
Resources: {
|
|
103
|
+
MyBucket: { Type: "AWS::S3::Bucket" },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const diags = checkS3PublicAccess(ctx);
|
|
107
|
+
expect(diags).toHaveLength(1);
|
|
108
|
+
});
|
|
109
|
+
});
|