@intentius/chant-lexicon-aws 0.49.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.
Files changed (84) hide show
  1. package/dist/codegen/generate-lexicon.d.ts +5 -0
  2. package/dist/codegen/generate-lexicon.d.ts.map +1 -1
  3. package/dist/components/host-delivery.d.ts +4 -3
  4. package/dist/components/host-delivery.d.ts.map +1 -1
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/integrity.json +9 -4
  8. package/dist/lint/audit-catalog.d.ts.map +1 -1
  9. package/dist/lint/post-synth/index.d.ts.map +1 -1
  10. package/dist/lint/post-synth/waw016.d.ts +10 -2
  11. package/dist/lint/post-synth/waw016.d.ts.map +1 -1
  12. package/dist/lint/post-synth/waw064.d.ts +17 -0
  13. package/dist/lint/post-synth/waw064.d.ts.map +1 -0
  14. package/dist/lint/post-synth/waw065.d.ts +23 -0
  15. package/dist/lint/post-synth/waw065.d.ts.map +1 -0
  16. package/dist/lint/post-synth/waw066.d.ts +25 -0
  17. package/dist/lint/post-synth/waw066.d.ts.map +1 -0
  18. package/dist/lint/post-synth/waw067.d.ts +24 -0
  19. package/dist/lint/post-synth/waw067.d.ts.map +1 -0
  20. package/dist/lint/post-synth/waw068.d.ts +20 -0
  21. package/dist/lint/post-synth/waw068.d.ts.map +1 -0
  22. package/dist/manifest.json +1 -1
  23. package/dist/meta.json +34 -0
  24. package/dist/okf/index.md +5 -0
  25. package/dist/okf/rules/WAW064.md +18 -0
  26. package/dist/okf/rules/WAW065.md +25 -0
  27. package/dist/okf/rules/WAW066.md +22 -0
  28. package/dist/okf/rules/WAW067.md +20 -0
  29. package/dist/okf/rules/WAW068.md +19 -0
  30. package/dist/okf/types/EC2Route.md +2 -0
  31. package/dist/okf/types/InternetGateway.md +4 -0
  32. package/dist/okf/types/Map.md +4 -0
  33. package/dist/okf/types/NatGateway.md +4 -0
  34. package/dist/okf/types/RouteTable.md +4 -0
  35. package/dist/okf/types/Subnet.md +2 -0
  36. package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
  37. package/dist/okf/types/Table.md +3 -0
  38. package/dist/okf/types/TransitGateway.md +6 -0
  39. package/dist/okf/types/TransitGatewayAttachment.md +4 -0
  40. package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
  41. package/dist/okf/types/TransitGatewayRoute.md +5 -0
  42. package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
  43. package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
  44. package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
  45. package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
  46. package/dist/okf/types/Type.md +5 -0
  47. package/dist/okf/types/VPNConnection.md +4 -0
  48. package/dist/okf/types/VPNGateway.md +4 -0
  49. package/dist/op/builders.d.ts +47 -0
  50. package/dist/op/builders.d.ts.map +1 -0
  51. package/dist/rules/waw016.ts +37 -16
  52. package/dist/rules/waw064.ts +52 -0
  53. package/dist/rules/waw065.ts +110 -0
  54. package/dist/rules/waw066.ts +155 -0
  55. package/dist/rules/waw067.ts +114 -0
  56. package/dist/rules/waw068.ts +78 -0
  57. package/dist/spec/parse.d.ts +7 -0
  58. package/dist/spec/parse.d.ts.map +1 -1
  59. package/package.json +2 -2
  60. package/src/codegen/fallback.ts +1 -0
  61. package/src/codegen/generate-lexicon.ts +6 -0
  62. package/src/codegen/sam.ts +9 -0
  63. package/src/components/host-delivery.ts +4 -3
  64. package/src/generated/lexicon-aws.json +34 -0
  65. package/src/index.ts +8 -0
  66. package/src/lint/audit-catalog.ts +10 -0
  67. package/src/lint/post-synth/index.ts +10 -0
  68. package/src/lint/post-synth/waw016.test.ts +61 -4
  69. package/src/lint/post-synth/waw016.ts +37 -16
  70. package/src/lint/post-synth/waw064.test.ts +79 -0
  71. package/src/lint/post-synth/waw064.ts +52 -0
  72. package/src/lint/post-synth/waw065.test.ts +90 -0
  73. package/src/lint/post-synth/waw065.ts +110 -0
  74. package/src/lint/post-synth/waw066.test.ts +108 -0
  75. package/src/lint/post-synth/waw066.ts +155 -0
  76. package/src/lint/post-synth/waw067.test.ts +79 -0
  77. package/src/lint/post-synth/waw067.ts +114 -0
  78. package/src/lint/post-synth/waw068.test.ts +74 -0
  79. package/src/lint/post-synth/waw068.ts +78 -0
  80. package/src/lsp/hover.ts +17 -2
  81. package/src/op/builders.test.ts +61 -0
  82. package/src/op/builders.ts +72 -0
  83. package/src/spec/parse.test.ts +74 -0
  84. package/src/spec/parse.ts +26 -6
@@ -0,0 +1,78 @@
1
+ /**
2
+ * WAW068: VPN Gateway Has A Single VPN Connection
3
+ *
4
+ * An `AWS::EC2::VPNConnection` always gets two tunnels from AWS, but that
5
+ * redundancy only covers the tunnel pair inside one connection — it does
6
+ * nothing if the connection's Customer Gateway, on-prem device, or the
7
+ * single site it terminates at goes down. The reliability boundary that
8
+ * matters is the connection itself: a `AWS::EC2::VPNGateway` (or a Transit
9
+ * Gateway used for VPN attachment) with only one `VPNConnection` attached
10
+ * has no fallback path for hybrid connectivity if that connection's
11
+ * Customer Gateway or on-prem side fails.
12
+ *
13
+ * Cross-resource join: group every declared `VPNConnection` by the gateway
14
+ * (`VpnGatewayId` or `TransitGatewayId`) it attaches to, and flag any
15
+ * gateway left with exactly one.
16
+ */
17
+
18
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
19
+ import { parseCFTemplate, findResourceRefs, type CFTemplate } from "./cf-refs";
20
+
21
+ export function checkVpnGatewayRedundancy(ctx: PostSynthContext): PostSynthDiagnostic[] {
22
+ const diagnostics: PostSynthDiagnostic[] = [];
23
+
24
+ for (const [_lexicon, output] of ctx.outputs) {
25
+ const template: CFTemplate | null = parseCFTemplate(output);
26
+ if (!template?.Resources) continue;
27
+ const resources = template.Resources;
28
+
29
+ // gateway logical id -> the VPNConnection logical ids attached to it.
30
+ const connectionsByGateway = new Map<string, string[]>();
31
+
32
+ for (const [logicalId, resource] of Object.entries(resources)) {
33
+ if (resource.Type !== "AWS::EC2::VPNConnection") continue;
34
+ const props = resource.Properties ?? {};
35
+
36
+ const gatewayProp = props.VpnGatewayId !== undefined ? props.VpnGatewayId : props.TransitGatewayId;
37
+ if (gatewayProp === undefined) continue; // no declared gateway target — can't group, stay quiet
38
+
39
+ const gwRefs = findResourceRefs(gatewayProp);
40
+ if (gwRefs.size !== 1) continue;
41
+ const [gwId] = gwRefs;
42
+
43
+ const gw = resources[gwId];
44
+ if (!gw || (gw.Type !== "AWS::EC2::VPNGateway" && gw.Type !== "AWS::EC2::TransitGateway")) continue;
45
+
46
+ let list = connectionsByGateway.get(gwId);
47
+ if (!list) {
48
+ list = [];
49
+ connectionsByGateway.set(gwId, list);
50
+ }
51
+ list.push(logicalId);
52
+ }
53
+
54
+ for (const [gwId, connectionIds] of connectionsByGateway) {
55
+ if (connectionIds.length !== 1) continue;
56
+
57
+ const kind = resources[gwId].Type === "AWS::EC2::VPNGateway" ? "VPN Gateway" : "Transit Gateway";
58
+ diagnostics.push({
59
+ checkId: "WAW068",
60
+ severity: "warning",
61
+ message: `${kind} "${gwId}" has only one VPN connection ("${connectionIds[0]}") attached — no redundant path if this connection, its tunnels, or its Customer Gateway fail; attach a second VPNConnection (ideally to a separate Customer Gateway) for hybrid-connectivity redundancy`,
62
+ entity: gwId,
63
+ lexicon: "aws",
64
+ });
65
+ }
66
+ }
67
+
68
+ return diagnostics;
69
+ }
70
+
71
+ export const waw068: PostSynthCheck = {
72
+ id: "WAW068",
73
+ description: "VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity",
74
+
75
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
76
+ return checkVpnGatewayRedundancy(ctx);
77
+ },
78
+ };
@@ -36,6 +36,13 @@ export interface ParsedResource {
36
36
  writeOnly: string[];
37
37
  primaryIdentifier: string[];
38
38
  deprecatedProperties: string[];
39
+ /**
40
+ * The subset of `deprecatedProperties` that no upstream declaration backs.
41
+ * These names come from {@link DEPRECATION_RE} matching the property
42
+ * description, so they are an inference rather than a reading. Kept apart so
43
+ * a consumer can tell the two apart and calibrate what it says (#1701).
44
+ */
45
+ inferredDeprecations: string[];
39
46
  conditionalCreateOnly: string[];
40
47
  replacementStrategy?: "delete_then_create" | "create_then_delete";
41
48
  tagging?: {
@@ -1 +1 @@
1
- {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAGL,kBAAkB,IAAI,sBAAsB,EAE5C,KAAK,mBAAmB,EAIzB,MAAM,sCAAsC,CAAC;AAG9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAWhF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,CA6IvE;AAiBD,eAAO,MAAM,kBAAkB,+BAAyB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvD"}
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/spec/parse.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAGL,kBAAkB,IAAI,sBAAsB,EAE5C,KAAK,mBAAmB,EAIzB,MAAM,sCAAsC,CAAC;AAG9C,YAAY,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAkBhF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,mBAAmB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,mBAAmB,CAAC,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IAClE,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,WAAW,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,OAAO,CAAA;KAAE,CAAC;CAC9E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,EAAE,kBAAkB,EAAE,CAAC;IACpC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,iBAAiB,CAmJvE;AAiBD,eAAO,MAAM,kBAAkB,+BAAyB,CAAC;AAMzD;;GAEG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGvD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-aws",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "AWS CloudFormation lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -80,7 +80,7 @@
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.49.0",
83
+ "@intentius/chant": "^0.50.0",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -27,6 +27,7 @@ function fallbackLogGroup(): SchemaParseResult {
27
27
  writeOnly: [],
28
28
  primaryIdentifier: [],
29
29
  deprecatedProperties: [],
30
+ inferredDeprecations: [],
30
31
  conditionalCreateOnly: [],
31
32
  },
32
33
  propertyTypes: [],
@@ -26,6 +26,11 @@ export interface LexiconEntry {
26
26
  writeOnly?: string[];
27
27
  primaryIdentifier?: string[];
28
28
  deprecatedProperties?: string[];
29
+ /**
30
+ * Subset of `deprecatedProperties` that only a regex over the property
31
+ * description supports; the rest is declared by the Registry schema (#1701).
32
+ */
33
+ inferredDeprecations?: string[];
29
34
  conditionalCreateOnly?: string[];
30
35
  replacementStrategy?: "delete_then_create" | "create_then_delete";
31
36
  tagging?: { taggable: boolean; tagOnCreate: boolean; tagUpdatable: boolean };
@@ -72,6 +77,7 @@ export function generateLexiconJSON(
72
77
  ...(r.resource.writeOnly.length > 0 && { writeOnly: r.resource.writeOnly }),
73
78
  ...(r.resource.primaryIdentifier.length > 0 && { primaryIdentifier: r.resource.primaryIdentifier }),
74
79
  ...(r.resource.deprecatedProperties?.length && { deprecatedProperties: r.resource.deprecatedProperties }),
80
+ ...(r.resource.inferredDeprecations?.length && { inferredDeprecations: r.resource.inferredDeprecations }),
75
81
  ...(r.resource.conditionalCreateOnly?.length && { conditionalCreateOnly: r.resource.conditionalCreateOnly }),
76
82
  ...(r.resource.replacementStrategy && { replacementStrategy: r.resource.replacementStrategy }),
77
83
  ...(r.resource.tagging && { tagging: r.resource.tagging }),
@@ -59,6 +59,7 @@ function samFunction(): SchemaParseResult {
59
59
  writeOnly: [],
60
60
  primaryIdentifier: [],
61
61
  deprecatedProperties: [],
62
+ inferredDeprecations: [],
62
63
  conditionalCreateOnly: [],
63
64
  },
64
65
  propertyTypes: [
@@ -147,6 +148,7 @@ function samApi(): SchemaParseResult {
147
148
  writeOnly: [],
148
149
  primaryIdentifier: [],
149
150
  deprecatedProperties: [],
151
+ inferredDeprecations: [],
150
152
  conditionalCreateOnly: [],
151
153
  },
152
154
  propertyTypes: [
@@ -200,6 +202,7 @@ function samHttpApi(): SchemaParseResult {
200
202
  writeOnly: [],
201
203
  primaryIdentifier: [],
202
204
  deprecatedProperties: [],
205
+ inferredDeprecations: [],
203
206
  conditionalCreateOnly: [],
204
207
  },
205
208
  propertyTypes: [
@@ -236,6 +239,7 @@ function samSimpleTable(): SchemaParseResult {
236
239
  writeOnly: [],
237
240
  primaryIdentifier: [],
238
241
  deprecatedProperties: [],
242
+ inferredDeprecations: [],
239
243
  conditionalCreateOnly: [],
240
244
  },
241
245
  propertyTypes: [
@@ -273,6 +277,7 @@ function samLayerVersion(): SchemaParseResult {
273
277
  writeOnly: [],
274
278
  primaryIdentifier: [],
275
279
  deprecatedProperties: [],
280
+ inferredDeprecations: [],
276
281
  conditionalCreateOnly: [],
277
282
  },
278
283
  propertyTypes: [],
@@ -306,6 +311,7 @@ function samStateMachine(): SchemaParseResult {
306
311
  writeOnly: [],
307
312
  primaryIdentifier: [],
308
313
  deprecatedProperties: [],
314
+ inferredDeprecations: [],
309
315
  conditionalCreateOnly: [],
310
316
  },
311
317
  propertyTypes: [
@@ -339,6 +345,7 @@ function samApplication(): SchemaParseResult {
339
345
  writeOnly: [],
340
346
  primaryIdentifier: [],
341
347
  deprecatedProperties: [],
348
+ inferredDeprecations: [],
342
349
  conditionalCreateOnly: [],
343
350
  },
344
351
  propertyTypes: [],
@@ -360,6 +367,7 @@ function samConnector(): SchemaParseResult {
360
367
  writeOnly: [],
361
368
  primaryIdentifier: [],
362
369
  deprecatedProperties: [],
370
+ inferredDeprecations: [],
363
371
  conditionalCreateOnly: [],
364
372
  },
365
373
  propertyTypes: [],
@@ -397,6 +405,7 @@ function samGraphQLApi(): SchemaParseResult {
397
405
  writeOnly: [],
398
406
  primaryIdentifier: [],
399
407
  deprecatedProperties: [],
408
+ inferredDeprecations: [],
400
409
  conditionalCreateOnly: [],
401
410
  },
402
411
  propertyTypes: [],
@@ -73,9 +73,10 @@ function resolveTargets(
73
73
  * Deploy a revision to a host (fleet) via AWS CodeDeploy: create the
74
74
  * deployment, then wait for a terminal status. Rollback re-invokes
75
75
  * `stopAndRollback` against the deployment id this step created (captured in
76
- * a closure-scoped map keyed by application/deploymentGroup, since the
77
- * driver's saga rollback calls `rollback(ctx, sameInput)` rather than passing
78
- * `run`'s output) riding CodeDeploy's own native automatic rollback to the
76
+ * a closure-scoped map keyed by application/deploymentGroup; the driver now
77
+ * also passes `run`'s output as `rollback`'s optional third arg (#1944), but
78
+ * this capability's input-derived key needs neither channel to resolve
79
+ * identity) — riding CodeDeploy's own native automatic rollback to the
79
80
  * last known-good revision rather than a chant-scripted compensation, the
80
81
  * "auto" rollback style the Neo4j pilot's docs comment contrasts with the
81
82
  * ALB/ECS pilot's component-declared one.
@@ -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, Set<string>> {
14
+ function fakeDeprecated(): Map<string, Map<string, DeprecationBasis>> {
11
15
  return new Map([
12
- ["AWS::S3::Bucket", new Set(["AccessControl", "ObjectLockConfiguration"])],
13
- ["AWS::Lambda::Function", new Set(["Code"])],
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
- * Sources: explicit `deprecatedProperties` array + description text mining.
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, Set<string>> {
24
- const map = new Map<string, Set<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 === "resource" && entry.resourceType && entry.deprecatedProperties && entry.deprecatedProperties.length > 0) {
33
- map.set(entry.resourceType, new Set(entry.deprecatedProperties));
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, Set<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
- 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
- }
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
+ });