@intentius/chant-lexicon-aws 0.49.0 → 0.51.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/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/disruption.d.ts +26 -0
- package/dist/disruption.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +9 -4
- package/dist/lint/audit-catalog.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/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 +5 -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/EC2Route.md +2 -0
- package/dist/okf/types/InternetGateway.md +4 -0
- package/dist/okf/types/Map.md +4 -0
- package/dist/okf/types/NatGateway.md +4 -0
- package/dist/okf/types/RouteTable.md +4 -0
- package/dist/okf/types/Subnet.md +2 -0
- package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
- package/dist/okf/types/Table.md +3 -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 +5 -0
- package/dist/okf/types/VPNConnection.md +4 -0
- package/dist/okf/types/VPNGateway.md +4 -0
- 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/rules/waw016.ts +37 -16
- 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/spec/parse.d.ts +7 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +2 -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/disruption.test.ts +198 -0
- package/src/disruption.ts +154 -0
- package/src/generated/lexicon-aws.json +34 -0
- package/src/index.ts +8 -0
- package/src/lint/audit-catalog.ts +10 -0
- package/src/lint/post-synth/index.ts +10 -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/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 +14 -2
- package/src/op/builders.test.ts +61 -0
- package/src/op/builders.ts +72 -0
- package/src/plugin.ts +16 -1
- package/src/spec/parse.test.ts +74 -0
- package/src/spec/parse.ts +26 -6
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replacement semantics for a pending update (#1665).
|
|
3
|
+
*
|
|
4
|
+
* CloudFormation already knows this and says so in the Registry schema:
|
|
5
|
+
* `createOnlyProperties` is the type's complete list of properties that cannot
|
|
6
|
+
* be updated without building a new resource, and `replacementStrategy` says
|
|
7
|
+
* whether the new one is created before the old is removed or after. The
|
|
8
|
+
* codegen compiles both into `lexicon-aws.json` alongside the types, which is
|
|
9
|
+
* why this is a lookup rather than a rule anyone maintains by hand.
|
|
10
|
+
*
|
|
11
|
+
* `conditionalCreateOnlyProperties` is the interesting case: the schema is
|
|
12
|
+
* saying "depends on the value", which is not an answer, so it degrades to
|
|
13
|
+
* `unknown` with the property named. That is the whole point of the contract
|
|
14
|
+
* having an `unknown` — a maybe reported as `in-place` is worse than no
|
|
15
|
+
* report.
|
|
16
|
+
*
|
|
17
|
+
* `rolling` is not produced here. Nothing in the Registry schema expresses a
|
|
18
|
+
* workload roll; that verdict belongs to a lexicon whose spec does.
|
|
19
|
+
*/
|
|
20
|
+
import { createRequire } from "module";
|
|
21
|
+
import type { DisruptionQuery, DisruptionVerdict } from "@intentius/chant/lexicon";
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
/** The slice of a `lexicon-aws.json` entry that carries replacement semantics. */
|
|
26
|
+
export interface DisruptionSpec {
|
|
27
|
+
resourceType: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
/** Read-only attributes (TS key → CF attr name). An output changing is a symptom, never a cause. */
|
|
30
|
+
attrs?: Record<string, string>;
|
|
31
|
+
createOnly?: string[];
|
|
32
|
+
conditionalCreateOnly?: string[];
|
|
33
|
+
replacementStrategy?: "delete_then_create" | "create_then_delete";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Classify one pending update against the type's compiled schema. Pure — the
|
|
38
|
+
* spec table is passed in, so this is testable without generated artifacts.
|
|
39
|
+
*/
|
|
40
|
+
export function classifyAwsChange(
|
|
41
|
+
query: DisruptionQuery,
|
|
42
|
+
specs: Map<string, DisruptionSpec>,
|
|
43
|
+
): DisruptionVerdict {
|
|
44
|
+
const spec = query.type ? specs.get(query.type) : undefined;
|
|
45
|
+
if (!spec || spec.kind !== "resource") {
|
|
46
|
+
return {
|
|
47
|
+
disruption: "unknown",
|
|
48
|
+
detail: query.type
|
|
49
|
+
? `no CloudFormation registry schema on record for ${query.type}`
|
|
50
|
+
: "the observation reported no resource type",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const readOnly = readOnlyNames(spec);
|
|
55
|
+
const considered: Array<{ path: string; property: string }> = [];
|
|
56
|
+
for (const delta of query.deltas) {
|
|
57
|
+
const property = propertyName(delta.path);
|
|
58
|
+
if (!property || readOnly.has(property)) continue;
|
|
59
|
+
considered.push({ path: delta.path, property });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const createOnly = new Set(spec.createOnly?.map(headSegment));
|
|
63
|
+
const forced = considered.filter((c) => createOnly.has(c.property));
|
|
64
|
+
if (forced.length > 0) {
|
|
65
|
+
const names = [...new Set(forced.map((c) => c.property))];
|
|
66
|
+
const destroys = spec.replacementStrategy === "delete_then_create";
|
|
67
|
+
return {
|
|
68
|
+
disruption: destroys ? "destroy" : "replace",
|
|
69
|
+
because: forced.map((c) => c.path),
|
|
70
|
+
detail: destroys
|
|
71
|
+
? `${names.join(", ")} is create-only and ${spec.resourceType} replaces by deleting first — the resource is gone before the new one exists`
|
|
72
|
+
: `${names.join(", ")} is create-only — CloudFormation builds a new ${spec.resourceType} and removes the old one`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const conditional = new Set(spec.conditionalCreateOnly?.map(headSegment));
|
|
77
|
+
const maybe = considered.filter((c) => conditional.has(c.property));
|
|
78
|
+
if (maybe.length > 0) {
|
|
79
|
+
const names = [...new Set(maybe.map((c) => c.property))];
|
|
80
|
+
return {
|
|
81
|
+
disruption: "unknown",
|
|
82
|
+
because: maybe.map((c) => c.path),
|
|
83
|
+
detail: `the schema marks ${names.join(", ")} conditionally create-only — whether this replaces depends on the value, and the schema does not say which way`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (considered.length === 0) {
|
|
88
|
+
return {
|
|
89
|
+
disruption: "in-place",
|
|
90
|
+
detail: "no configured property differs — only status, identity, or read-only attributes changed",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
disruption: "in-place",
|
|
96
|
+
detail: `no create-only property of ${spec.resourceType} changed`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* `LexiconPlugin.classifyDisruption` for aws. Loads the compiled registry once
|
|
102
|
+
* and answers every query from it. A registry that cannot be read leaves every
|
|
103
|
+
* change `unknown` — the honest answer, and the one the contract requires.
|
|
104
|
+
*/
|
|
105
|
+
export function awsDisruption(options: {
|
|
106
|
+
environment: string;
|
|
107
|
+
changes: DisruptionQuery[];
|
|
108
|
+
}): Record<string, DisruptionVerdict> {
|
|
109
|
+
const specs = registryByType();
|
|
110
|
+
const out: Record<string, DisruptionVerdict> = {};
|
|
111
|
+
for (const change of options.changes) {
|
|
112
|
+
out[change.name] = classifyAwsChange(change, specs);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let cached: Map<string, DisruptionSpec> | undefined;
|
|
118
|
+
|
|
119
|
+
function registryByType(): Map<string, DisruptionSpec> {
|
|
120
|
+
if (cached) return cached;
|
|
121
|
+
try {
|
|
122
|
+
const manifest = require("./generated/lexicon-aws.json") as Record<string, DisruptionSpec>;
|
|
123
|
+
cached = new Map(Object.values(manifest).map((e) => [e.resourceType, e]));
|
|
124
|
+
} catch {
|
|
125
|
+
cached = new Map();
|
|
126
|
+
}
|
|
127
|
+
return cached;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Every read-only attribute name the schema declares, top segment only. */
|
|
131
|
+
function readOnlyNames(spec: DisruptionSpec): Set<string> {
|
|
132
|
+
const names = new Set<string>();
|
|
133
|
+
for (const attr of Object.values(spec.attrs ?? {})) names.add(attr.split(".")[0]);
|
|
134
|
+
return names;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The property a core delta path names, or undefined when the path is not
|
|
139
|
+
* about a property at all. Core's own observation fields (`status`,
|
|
140
|
+
* `physicalId`, `lastUpdated`) sit at the root of the change and say nothing
|
|
141
|
+
* about configuration; only what is under `attributes.` was read off the
|
|
142
|
+
* resource.
|
|
143
|
+
*/
|
|
144
|
+
function propertyName(path: string): string | undefined {
|
|
145
|
+
const prefix = "attributes.";
|
|
146
|
+
if (!path.startsWith(prefix)) return undefined;
|
|
147
|
+
const rest = path.slice(prefix.length);
|
|
148
|
+
return rest ? headSegment(rest) : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The first segment of a JSON-pointer-ish property path (`Config/Engine` → `Config`). */
|
|
152
|
+
function headSegment(path: string): string {
|
|
153
|
+
return path.split(/[/.]/)[0];
|
|
154
|
+
}
|
|
@@ -6222,6 +6222,9 @@
|
|
|
6222
6222
|
"deprecatedProperties": [
|
|
6223
6223
|
"DeleteStorageConnectors"
|
|
6224
6224
|
],
|
|
6225
|
+
"inferredDeprecations": [
|
|
6226
|
+
"DeleteStorageConnectors"
|
|
6227
|
+
],
|
|
6225
6228
|
"tagging": {
|
|
6226
6229
|
"taggable": true,
|
|
6227
6230
|
"tagOnCreate": true,
|
|
@@ -11669,6 +11672,9 @@
|
|
|
11669
11672
|
"deprecatedProperties": [
|
|
11670
11673
|
"AccessControl"
|
|
11671
11674
|
],
|
|
11675
|
+
"inferredDeprecations": [
|
|
11676
|
+
"AccessControl"
|
|
11677
|
+
],
|
|
11672
11678
|
"tagging": {
|
|
11673
11679
|
"taggable": true,
|
|
11674
11680
|
"tagOnCreate": true,
|
|
@@ -14989,6 +14995,9 @@
|
|
|
14989
14995
|
"deprecatedProperties": [
|
|
14990
14996
|
"ComputeModel"
|
|
14991
14997
|
],
|
|
14998
|
+
"inferredDeprecations": [
|
|
14999
|
+
"ComputeModel"
|
|
15000
|
+
],
|
|
14992
15001
|
"replacementStrategy": "delete_then_create",
|
|
14993
15002
|
"tagging": {
|
|
14994
15003
|
"taggable": true,
|
|
@@ -15385,6 +15394,9 @@
|
|
|
15385
15394
|
"deprecatedProperties": [
|
|
15386
15395
|
"ComputeModel"
|
|
15387
15396
|
],
|
|
15397
|
+
"inferredDeprecations": [
|
|
15398
|
+
"ComputeModel"
|
|
15399
|
+
],
|
|
15388
15400
|
"conditionalCreateOnly": [
|
|
15389
15401
|
"DbNodes"
|
|
15390
15402
|
],
|
|
@@ -17085,6 +17097,9 @@
|
|
|
17085
17097
|
"deprecatedProperties": [
|
|
17086
17098
|
"Deprecated"
|
|
17087
17099
|
],
|
|
17100
|
+
"inferredDeprecations": [
|
|
17101
|
+
"Deprecated"
|
|
17102
|
+
],
|
|
17088
17103
|
"tagging": {
|
|
17089
17104
|
"taggable": true,
|
|
17090
17105
|
"tagOnCreate": true,
|
|
@@ -29032,6 +29047,9 @@
|
|
|
29032
29047
|
"TdeCredentialPassword",
|
|
29033
29048
|
"DBSecurityGroups"
|
|
29034
29049
|
],
|
|
29050
|
+
"inferredDeprecations": [
|
|
29051
|
+
"DBSecurityGroups"
|
|
29052
|
+
],
|
|
29035
29053
|
"conditionalCreateOnly": [
|
|
29036
29054
|
"AutoMinorVersionUpgrade",
|
|
29037
29055
|
"AvailabilityZone",
|
|
@@ -33246,6 +33264,9 @@
|
|
|
33246
33264
|
"deprecatedProperties": [
|
|
33247
33265
|
"ImageScanningConfiguration"
|
|
33248
33266
|
],
|
|
33267
|
+
"inferredDeprecations": [
|
|
33268
|
+
"ImageScanningConfiguration"
|
|
33269
|
+
],
|
|
33249
33270
|
"tagging": {
|
|
33250
33271
|
"taggable": true,
|
|
33251
33272
|
"tagOnCreate": true,
|
|
@@ -41749,6 +41770,9 @@
|
|
|
41749
41770
|
"deprecatedProperties": [
|
|
41750
41771
|
"Runtime"
|
|
41751
41772
|
],
|
|
41773
|
+
"inferredDeprecations": [
|
|
41774
|
+
"Runtime"
|
|
41775
|
+
],
|
|
41752
41776
|
"conditionalCreateOnly": [
|
|
41753
41777
|
"DurableConfig"
|
|
41754
41778
|
],
|
|
@@ -49621,6 +49645,10 @@
|
|
|
49621
49645
|
"ElasticGpuSpecifications",
|
|
49622
49646
|
"ElasticInferenceAccelerators"
|
|
49623
49647
|
],
|
|
49648
|
+
"inferredDeprecations": [
|
|
49649
|
+
"ElasticGpuSpecifications",
|
|
49650
|
+
"ElasticInferenceAccelerators"
|
|
49651
|
+
],
|
|
49624
49652
|
"conditionalCreateOnly": [
|
|
49625
49653
|
"AdditionalInfo",
|
|
49626
49654
|
"Affinity",
|
|
@@ -64878,6 +64906,9 @@
|
|
|
64878
64906
|
"deprecatedProperties": [
|
|
64879
64907
|
"DBPort"
|
|
64880
64908
|
],
|
|
64909
|
+
"inferredDeprecations": [
|
|
64910
|
+
"DBPort"
|
|
64911
|
+
],
|
|
64881
64912
|
"tagging": {
|
|
64882
64913
|
"taggable": true,
|
|
64883
64914
|
"tagOnCreate": true,
|
|
@@ -100101,6 +100132,9 @@
|
|
|
100101
100132
|
"deprecatedProperties": [
|
|
100102
100133
|
"State"
|
|
100103
100134
|
],
|
|
100135
|
+
"inferredDeprecations": [
|
|
100136
|
+
"State"
|
|
100137
|
+
],
|
|
100104
100138
|
"tagging": {
|
|
100105
100139
|
"taggable": true,
|
|
100106
100140
|
"tagOnCreate": true,
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,14 @@ export type { AwsEffectReceiptDeclaration } from "./effect-receipt-row";
|
|
|
22
22
|
export { awsReceiptStore, observeReceiptRows, ssmGetParameter, ssmPutParameter } from "./receipt-store";
|
|
23
23
|
export type { AwsReceiptStoreOptions, ReceiptRowObservation } from "./receipt-store";
|
|
24
24
|
|
|
25
|
+
// Typed Op step-builder wrappers (chant #1288 Stage 2) — awsApply/awsDelete/
|
|
26
|
+
// flociUp/flociDown with authoring-time types derived from this lexicon's
|
|
27
|
+
// own *Args interfaces (see ./op/builders.ts's module doc for why these live
|
|
28
|
+
// here rather than in core or the temporal barrel). Opt-in:
|
|
29
|
+
// `@intentius/chant-lexicon-temporal`'s same-named exports are core's
|
|
30
|
+
// original untyped builders, unchanged, for cloud-agnostic authoring.
|
|
31
|
+
export { awsApply, awsDelete, flociUp, flociDown } from "./op/builders";
|
|
32
|
+
|
|
25
33
|
// Serializer
|
|
26
34
|
export { awsSerializer } from "./serializer";
|
|
27
35
|
|
|
@@ -78,4 +78,14 @@ export const awsAuditCatalog: Record<string, RuleMeta> = {
|
|
|
78
78
|
WAW061: auditRule("WAW061", "merge-worthy", "guidance", "Subnet CidrBlock not contained in its VPC's CidrBlock", "Fix the subnet's CidrBlock so it falls within the VPC's CidrBlock range.", { category: "correctness" }),
|
|
79
79
|
WAW062: auditRule("WAW062", "merge-worthy", "guidance", "Duplicate export name or explicit resource name within a template", "Give each duplicated Export/name a distinct literal value.", { category: "correctness" }),
|
|
80
80
|
WAW063: auditRule("WAW063", "merge-worthy", "guidance", "IAM policy denies an action another attached policy on the same role allows", "Remove or narrow the Deny, or move the Allow off this role — the explicit Deny always wins.", { category: "correctness" }),
|
|
81
|
+
|
|
82
|
+
// #826 — network-topology resiliency (Well-Architected Reliability Pillar):
|
|
83
|
+
// TGW route hygiene, private-subnet and NAT-gateway cross-resource joins,
|
|
84
|
+
// VPN redundancy. No live state needed — all determinable from the
|
|
85
|
+
// declared template.
|
|
86
|
+
WAW064: auditRule("WAW064", "report-only", "guidance", "Transit Gateway route table declares a Blackhole route", "Confirm the traffic drop is intentional; remove the route if it isn't.", { category: "best-practice" }),
|
|
87
|
+
WAW065: auditRule("WAW065", "report-only", "guidance", "Transit Gateway route table wiring is incomplete", "Add the missing TransitGatewayRouteTablePropagation, or wire the orphaned attachment into a route table.", { category: "best-practice" }),
|
|
88
|
+
WAW066: auditRule("WAW066", "merge-worthy", "guidance", "Private subnet's route table has no working default route", "Add a 0.0.0.0/0 route to a NAT gateway/Transit Gateway that exists in the template.", { category: "correctness" }),
|
|
89
|
+
WAW067: auditRule("WAW067", "report-only", "guidance", "Single-AZ NAT gateway serves multi-AZ private subnets", "Add one NAT gateway per Availability Zone and point each AZ's subnets at its own.", { category: "best-practice" }),
|
|
90
|
+
WAW068: auditRule("WAW068", "report-only", "guidance", "VPN Gateway or Transit Gateway has only one attached VPN Connection", "Attach a second VPNConnection (ideally to a separate Customer Gateway) for redundancy.", { category: "best-practice" }),
|
|
81
91
|
};
|
|
@@ -55,6 +55,11 @@ import { waw060 } from "./waw060";
|
|
|
55
55
|
import { waw061 } from "./waw061";
|
|
56
56
|
import { waw062 } from "./waw062";
|
|
57
57
|
import { waw063 } from "./waw063";
|
|
58
|
+
import { waw064 } from "./waw064";
|
|
59
|
+
import { waw065 } from "./waw065";
|
|
60
|
+
import { waw066 } from "./waw066";
|
|
61
|
+
import { waw067 } from "./waw067";
|
|
62
|
+
import { waw068 } from "./waw068";
|
|
58
63
|
|
|
59
64
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
60
65
|
cor020,
|
|
@@ -112,4 +117,9 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
112
117
|
waw061,
|
|
113
118
|
waw062,
|
|
114
119
|
waw063,
|
|
120
|
+
waw064,
|
|
121
|
+
waw065,
|
|
122
|
+
waw066,
|
|
123
|
+
waw067,
|
|
124
|
+
waw068,
|
|
115
125
|
];
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
-
import { waw016, checkDeprecatedProperties } from "./waw016";
|
|
3
|
+
import { waw016, checkDeprecatedProperties, type DeprecationBasis } from "./waw016";
|
|
4
4
|
|
|
5
5
|
function makeCtx(template: object) {
|
|
6
6
|
return createPostSynthContext({ aws: template });
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function basisMap(entries: Array<[string, DeprecationBasis]>): Map<string, DeprecationBasis> {
|
|
10
|
+
return new Map(entries);
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
/** Synthetic deprecated-property map — no disk dependency. */
|
|
10
|
-
function fakeDeprecated(): Map<string,
|
|
14
|
+
function fakeDeprecated(): Map<string, Map<string, DeprecationBasis>> {
|
|
11
15
|
return new Map([
|
|
12
|
-
[
|
|
13
|
-
|
|
16
|
+
[
|
|
17
|
+
"AWS::S3::Bucket",
|
|
18
|
+
basisMap([
|
|
19
|
+
["AccessControl", "declared"],
|
|
20
|
+
["ObjectLockConfiguration", "declared"],
|
|
21
|
+
]),
|
|
22
|
+
],
|
|
23
|
+
["AWS::Lambda::Function", basisMap([["Code", "declared"]])],
|
|
14
24
|
]);
|
|
15
25
|
}
|
|
16
26
|
|
|
@@ -138,4 +148,51 @@ describe("WAW016: Deprecated Property Usage", () => {
|
|
|
138
148
|
expect(diags[0].entity).toBe("Bucket");
|
|
139
149
|
expect(diags[1].entity).toBe("Func");
|
|
140
150
|
});
|
|
151
|
+
|
|
152
|
+
// --- Deprecation basis (#1701) ---
|
|
153
|
+
|
|
154
|
+
test("an inferred deprecation reports at info and names its basis", () => {
|
|
155
|
+
const ctx = makeCtx({
|
|
156
|
+
Resources: {
|
|
157
|
+
Func: {
|
|
158
|
+
Type: "AWS::Lambda::Function",
|
|
159
|
+
Properties: { Runtime: "nodejs20.x" },
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
const deprecated = new Map([
|
|
164
|
+
["AWS::Lambda::Function", basisMap([["Runtime", "inferred"]])],
|
|
165
|
+
]);
|
|
166
|
+
const diags = checkDeprecatedProperties(ctx, deprecated);
|
|
167
|
+
expect(diags).toHaveLength(1);
|
|
168
|
+
expect(diags[0].checkId).toBe("WAW016");
|
|
169
|
+
expect(diags[0].severity).toBe("info");
|
|
170
|
+
expect(diags[0].message).toContain("Runtime");
|
|
171
|
+
expect(diags[0].message).toContain("does not declare it deprecated");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("declared and inferred deprecations on one resource keep separate severities", () => {
|
|
175
|
+
const ctx = makeCtx({
|
|
176
|
+
Resources: {
|
|
177
|
+
MyBucket: {
|
|
178
|
+
Type: "AWS::S3::Bucket",
|
|
179
|
+
Properties: { AccessControl: "Private", ObjectLockConfiguration: {} },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const deprecated = new Map([
|
|
184
|
+
[
|
|
185
|
+
"AWS::S3::Bucket",
|
|
186
|
+
basisMap([
|
|
187
|
+
["AccessControl", "declared"],
|
|
188
|
+
["ObjectLockConfiguration", "inferred"],
|
|
189
|
+
]),
|
|
190
|
+
],
|
|
191
|
+
]);
|
|
192
|
+
const diags = checkDeprecatedProperties(ctx, deprecated);
|
|
193
|
+
expect(diags).toHaveLength(2);
|
|
194
|
+
const bySeverity = new Map(diags.map((d) => [d.severity, d.message]));
|
|
195
|
+
expect(bySeverity.get("warning")).toContain("AccessControl");
|
|
196
|
+
expect(bySeverity.get("info")).toContain("ObjectLockConfiguration");
|
|
197
|
+
});
|
|
141
198
|
});
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* WAW016: Deprecated Property Usage
|
|
3
3
|
*
|
|
4
4
|
* Flags properties marked as deprecated in the CloudFormation Registry.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Two bases feed this, and the finding says which one it stands on (#1701).
|
|
7
|
+
* A `declared` name comes from the Registry schema's own `deprecatedProperties`
|
|
8
|
+
* array. An `inferred` name comes from a regex over the property description,
|
|
9
|
+
* which also matches descriptions that mention the deprecation of a sibling
|
|
10
|
+
* property, an enum value, or the thing the property configures. Declared is a
|
|
11
|
+
* warning; inferred is reported at info and worded as a guess.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import { readFileSync } from "fs";
|
|
@@ -10,18 +16,23 @@ import { join } from "path";
|
|
|
10
16
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
17
|
import { parseCFTemplate } from "./cf-refs";
|
|
12
18
|
|
|
19
|
+
/** What a deprecation classification rests on. */
|
|
20
|
+
export type DeprecationBasis = "declared" | "inferred";
|
|
21
|
+
|
|
13
22
|
interface LexiconEntry {
|
|
14
23
|
kind: string;
|
|
15
24
|
resourceType: string;
|
|
16
25
|
deprecatedProperties?: string[];
|
|
26
|
+
inferredDeprecations?: string[];
|
|
17
27
|
[key: string]: unknown;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
|
-
* Load deprecated properties per resource type from the lexicon JSON
|
|
31
|
+
* Load deprecated properties per resource type from the lexicon JSON, each
|
|
32
|
+
* tagged with the basis it was classified on.
|
|
22
33
|
*/
|
|
23
|
-
function loadDeprecatedProperties(): Map<string,
|
|
24
|
-
const map = new Map<string,
|
|
34
|
+
function loadDeprecatedProperties(): Map<string, Map<string, DeprecationBasis>> {
|
|
35
|
+
const map = new Map<string, Map<string, DeprecationBasis>>();
|
|
25
36
|
try {
|
|
26
37
|
const pkgDir = join(__dirname, "..", "..", "..");
|
|
27
38
|
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
@@ -29,9 +40,15 @@ function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
|
29
40
|
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
30
41
|
|
|
31
42
|
for (const [_name, entry] of Object.entries(data)) {
|
|
32
|
-
if (entry.kind
|
|
33
|
-
|
|
43
|
+
if (entry.kind !== "resource" || !entry.resourceType) continue;
|
|
44
|
+
if (!entry.deprecatedProperties?.length) continue;
|
|
45
|
+
|
|
46
|
+
const inferred = new Set(entry.inferredDeprecations ?? []);
|
|
47
|
+
const byProp = new Map<string, DeprecationBasis>();
|
|
48
|
+
for (const propName of entry.deprecatedProperties) {
|
|
49
|
+
byProp.set(propName, inferred.has(propName) ? "inferred" : "declared");
|
|
34
50
|
}
|
|
51
|
+
map.set(entry.resourceType, byProp);
|
|
35
52
|
}
|
|
36
53
|
} catch {
|
|
37
54
|
// Lexicon not available — skip
|
|
@@ -44,7 +61,7 @@ function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
|
44
61
|
*/
|
|
45
62
|
export function checkDeprecatedProperties(
|
|
46
63
|
ctx: PostSynthContext,
|
|
47
|
-
deprecated: Map<string,
|
|
64
|
+
deprecated: Map<string, Map<string, DeprecationBasis>>,
|
|
48
65
|
): PostSynthDiagnostic[] {
|
|
49
66
|
if (deprecated.size === 0) return [];
|
|
50
67
|
|
|
@@ -60,15 +77,19 @@ export function checkDeprecatedProperties(
|
|
|
60
77
|
|
|
61
78
|
const props = resource.Properties ?? {};
|
|
62
79
|
for (const propName of Object.keys(props)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const basis = deprProps.get(propName);
|
|
81
|
+
if (!basis) continue;
|
|
82
|
+
|
|
83
|
+
diagnostics.push({
|
|
84
|
+
checkId: "WAW016",
|
|
85
|
+
severity: basis === "declared" ? "warning" : "info",
|
|
86
|
+
message:
|
|
87
|
+
basis === "declared"
|
|
88
|
+
? `Resource "${logicalId}" (${resource.Type}) uses deprecated property "${propName}" — consider alternatives`
|
|
89
|
+
: `Resource "${logicalId}" (${resource.Type}) uses property "${propName}", whose description reads as deprecated — the CloudFormation Registry does not declare it deprecated, so confirm before changing it`,
|
|
90
|
+
entity: logicalId,
|
|
91
|
+
lexicon: "aws",
|
|
92
|
+
});
|
|
72
93
|
}
|
|
73
94
|
}
|
|
74
95
|
}
|
|
@@ -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
|
+
};
|