@intentius/chant-lexicon-aws 0.46.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/api/read-client.d.ts +10 -2
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/codegen/generate-lexicon.d.ts +5 -0
  4. package/dist/codegen/generate-lexicon.d.ts.map +1 -1
  5. package/dist/components/host-delivery.d.ts +4 -3
  6. package/dist/components/host-delivery.d.ts.map +1 -1
  7. package/dist/composites/agentcore-agent.d.ts +26 -1
  8. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  9. package/dist/composites/bucket-deployment.d.ts +82 -0
  10. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  11. package/dist/composites/dynamodb-table.d.ts +45 -0
  12. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  13. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  14. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  15. package/dist/composites/ecr-repository.d.ts +49 -0
  16. package/dist/composites/ecr-repository.d.ts.map +1 -0
  17. package/dist/composites/fargate-service.d.ts +16 -0
  18. package/dist/composites/fargate-service.d.ts.map +1 -1
  19. package/dist/composites/index.d.ts +16 -2
  20. package/dist/composites/index.d.ts.map +1 -1
  21. package/dist/composites/monitoring-stack.d.ts +99 -0
  22. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  23. package/dist/composites/nlb-service.d.ts +48 -0
  24. package/dist/composites/nlb-service.d.ts.map +1 -0
  25. package/dist/composites/step-functions-workflow.d.ts +36 -0
  26. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  27. package/dist/deep-observe.d.ts +45 -0
  28. package/dist/deep-observe.d.ts.map +1 -1
  29. package/dist/deep-topology.d.ts +104 -0
  30. package/dist/deep-topology.d.ts.map +1 -0
  31. package/dist/effect-receipt-row.d.ts +92 -0
  32. package/dist/effect-receipt-row.d.ts.map +1 -0
  33. package/dist/index.d.ts +7 -2
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/integrity.json +14 -6
  36. package/dist/lint/audit-catalog.d.ts.map +1 -1
  37. package/dist/lint/post-synth/cf-refs.d.ts +17 -0
  38. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  39. package/dist/lint/post-synth/index.d.ts.map +1 -1
  40. package/dist/lint/post-synth/waw016.d.ts +10 -2
  41. package/dist/lint/post-synth/waw016.d.ts.map +1 -1
  42. package/dist/lint/post-synth/waw061.d.ts +19 -0
  43. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  44. package/dist/lint/post-synth/waw062.d.ts +19 -0
  45. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  46. package/dist/lint/post-synth/waw063.d.ts +35 -0
  47. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  48. package/dist/lint/post-synth/waw064.d.ts +17 -0
  49. package/dist/lint/post-synth/waw064.d.ts.map +1 -0
  50. package/dist/lint/post-synth/waw065.d.ts +23 -0
  51. package/dist/lint/post-synth/waw065.d.ts.map +1 -0
  52. package/dist/lint/post-synth/waw066.d.ts +25 -0
  53. package/dist/lint/post-synth/waw066.d.ts.map +1 -0
  54. package/dist/lint/post-synth/waw067.d.ts +24 -0
  55. package/dist/lint/post-synth/waw067.d.ts.map +1 -0
  56. package/dist/lint/post-synth/waw068.d.ts +20 -0
  57. package/dist/lint/post-synth/waw068.d.ts.map +1 -0
  58. package/dist/manifest.json +1 -1
  59. package/dist/meta.json +34 -0
  60. package/dist/okf/index.md +8 -0
  61. package/dist/okf/rules/WAW061.md +17 -0
  62. package/dist/okf/rules/WAW062.md +27 -0
  63. package/dist/okf/rules/WAW063.md +19 -0
  64. package/dist/okf/rules/WAW064.md +18 -0
  65. package/dist/okf/rules/WAW065.md +25 -0
  66. package/dist/okf/rules/WAW066.md +22 -0
  67. package/dist/okf/rules/WAW067.md +20 -0
  68. package/dist/okf/rules/WAW068.md +19 -0
  69. package/dist/okf/types/Action.md +1 -0
  70. package/dist/okf/types/AutoScalingGroup.md +1 -0
  71. package/dist/okf/types/Bucket.md +1 -0
  72. package/dist/okf/types/EC2Route.md +2 -0
  73. package/dist/okf/types/ECRRepository.md +1 -0
  74. package/dist/okf/types/Function.md +1 -0
  75. package/dist/okf/types/IamPolicy.md +1 -0
  76. package/dist/okf/types/InternetGateway.md +4 -0
  77. package/dist/okf/types/LaunchTemplate.md +1 -0
  78. package/dist/okf/types/LoadBalancer.md +1 -0
  79. package/dist/okf/types/ManagedPolicy.md +2 -0
  80. package/dist/okf/types/Map.md +5 -0
  81. package/dist/okf/types/NatGateway.md +4 -0
  82. package/dist/okf/types/Role.md +2 -0
  83. package/dist/okf/types/RouteTable.md +4 -0
  84. package/dist/okf/types/SecurityGroup.md +1 -0
  85. package/dist/okf/types/StateMachine.md +4 -0
  86. package/dist/okf/types/Subnet.md +6 -0
  87. package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
  88. package/dist/okf/types/Table.md +4 -0
  89. package/dist/okf/types/TransitGateway.md +6 -0
  90. package/dist/okf/types/TransitGatewayAttachment.md +4 -0
  91. package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
  92. package/dist/okf/types/TransitGatewayRoute.md +5 -0
  93. package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
  94. package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
  95. package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
  96. package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
  97. package/dist/okf/types/Type.md +8 -0
  98. package/dist/okf/types/VPNConnection.md +4 -0
  99. package/dist/okf/types/VPNGateway.md +4 -0
  100. package/dist/okf/types/Vpc.md +1 -0
  101. package/dist/op/activities/index.d.ts +6 -0
  102. package/dist/op/activities/index.d.ts.map +1 -1
  103. package/dist/op/builders.d.ts +47 -0
  104. package/dist/op/builders.d.ts.map +1 -0
  105. package/dist/plugin.d.ts.map +1 -1
  106. package/dist/receipt-store.d.ts +109 -0
  107. package/dist/receipt-store.d.ts.map +1 -0
  108. package/dist/rules/cf-refs.ts +40 -0
  109. package/dist/rules/waw016.ts +37 -16
  110. package/dist/rules/waw061.ts +67 -0
  111. package/dist/rules/waw062.ts +0 -0
  112. package/dist/rules/waw063.ts +0 -0
  113. package/dist/rules/waw064.ts +52 -0
  114. package/dist/rules/waw065.ts +110 -0
  115. package/dist/rules/waw066.ts +155 -0
  116. package/dist/rules/waw067.ts +114 -0
  117. package/dist/rules/waw068.ts +78 -0
  118. package/dist/serializer.d.ts.map +1 -1
  119. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  120. package/dist/spec/parse.d.ts +7 -0
  121. package/dist/spec/parse.d.ts.map +1 -1
  122. package/package.json +3 -3
  123. package/src/api/read-client.test.ts +9 -0
  124. package/src/api/read-client.ts +15 -2
  125. package/src/codegen/fallback.ts +1 -0
  126. package/src/codegen/generate-lexicon.ts +6 -0
  127. package/src/codegen/sam.ts +9 -0
  128. package/src/components/host-delivery.ts +4 -3
  129. package/src/composites/agentcore-agent.ts +35 -13
  130. package/src/composites/bucket-deployment.test.ts +84 -0
  131. package/src/composites/bucket-deployment.ts +153 -0
  132. package/src/composites/composites.test.ts +325 -0
  133. package/src/composites/dynamodb-table.test.ts +154 -0
  134. package/src/composites/dynamodb-table.ts +124 -0
  135. package/src/composites/ec2-instance-bundle.ts +134 -0
  136. package/src/composites/ecr-repository.test.ts +118 -0
  137. package/src/composites/ecr-repository.ts +117 -0
  138. package/src/composites/fargate-service.ts +19 -3
  139. package/src/composites/index.ts +21 -2
  140. package/src/composites/monitoring-stack.test.ts +130 -0
  141. package/src/composites/monitoring-stack.ts +164 -0
  142. package/src/composites/nlb-service.ts +117 -0
  143. package/src/composites/step-functions-workflow.ts +127 -0
  144. package/src/deep-observe.test.ts +201 -1
  145. package/src/deep-observe.ts +204 -0
  146. package/src/deep-topology.test.ts +392 -0
  147. package/src/deep-topology.ts +237 -0
  148. package/src/effect-receipt-row.test.ts +164 -0
  149. package/src/effect-receipt-row.ts +147 -0
  150. package/src/generated/lexicon-aws.json +34 -0
  151. package/src/index.ts +36 -7
  152. package/src/lint/audit-catalog.ts +16 -0
  153. package/src/lint/post-synth/cf-refs.ts +40 -0
  154. package/src/lint/post-synth/index.ts +16 -0
  155. package/src/lint/post-synth/waw016.test.ts +61 -4
  156. package/src/lint/post-synth/waw016.ts +37 -16
  157. package/src/lint/post-synth/waw061.test.ts +99 -0
  158. package/src/lint/post-synth/waw061.ts +67 -0
  159. package/src/lint/post-synth/waw062.test.ts +109 -0
  160. package/src/lint/post-synth/waw062.ts +0 -0
  161. package/src/lint/post-synth/waw063.test.ts +220 -0
  162. package/src/lint/post-synth/waw063.ts +0 -0
  163. package/src/lint/post-synth/waw064.test.ts +79 -0
  164. package/src/lint/post-synth/waw064.ts +52 -0
  165. package/src/lint/post-synth/waw065.test.ts +90 -0
  166. package/src/lint/post-synth/waw065.ts +110 -0
  167. package/src/lint/post-synth/waw066.test.ts +108 -0
  168. package/src/lint/post-synth/waw066.ts +155 -0
  169. package/src/lint/post-synth/waw067.test.ts +79 -0
  170. package/src/lint/post-synth/waw067.ts +114 -0
  171. package/src/lint/post-synth/waw068.test.ts +74 -0
  172. package/src/lint/post-synth/waw068.ts +78 -0
  173. package/src/lsp/hover.ts +17 -2
  174. package/src/op/activities/index.ts +15 -0
  175. package/src/op/builders.test.ts +61 -0
  176. package/src/op/builders.ts +72 -0
  177. package/src/plugin.ts +41 -10
  178. package/src/receipt-store.test.ts +204 -0
  179. package/src/receipt-store.ts +315 -0
  180. package/src/serializer.ts +105 -2
  181. package/src/skills/chant-aws-carve-terraform.md +26 -23
  182. package/src/spec/parse.test.ts +74 -0
  183. package/src/spec/parse.ts +26 -6
@@ -0,0 +1,110 @@
1
+ /**
2
+ * WAW065: Transit Gateway Route Table Wiring Is Incomplete
3
+ *
4
+ * A Transit Gateway route table only does anything once two independent
5
+ * pieces are both wired up: an `AWS::EC2::TransitGatewayRouteTableAssociation`
6
+ * (which attachment's traffic is evaluated against this table) and an
7
+ * `AWS::EC2::TransitGatewayRouteTablePropagation` (which attachments' routes
8
+ * populate it). CloudFormation is happy to deploy a route table with one but
9
+ * not the other — the stack succeeds, and the gap only shows up as traffic
10
+ * that can't find a route. Two patterns, both "declared but half-wired":
11
+ *
12
+ * - a route table has one or more associations but zero propagations — the
13
+ * associated attachment's traffic is evaluated against this table, but
14
+ * nothing ever populates it with routes;
15
+ * - a declared attachment (VPC/VPN/peering, or the generic
16
+ * TransitGatewayAttachment) is referenced by neither an association nor
17
+ * a propagation anywhere in the template — it hangs off the transit
18
+ * gateway with no route table wiring at all.
19
+ */
20
+
21
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
22
+ import { parseCFTemplate, findResourceRefs } from "./cf-refs";
23
+
24
+ const ATTACHMENT_TYPES = new Set([
25
+ "AWS::EC2::TransitGatewayAttachment",
26
+ "AWS::EC2::TransitGatewayVpcAttachment",
27
+ "AWS::EC2::TransitGatewayVpnAttachment",
28
+ "AWS::EC2::TransitGatewayPeeringAttachment",
29
+ ]);
30
+
31
+ export function checkTgwRouteTableWiring(ctx: PostSynthContext): PostSynthDiagnostic[] {
32
+ const diagnostics: PostSynthDiagnostic[] = [];
33
+
34
+ for (const [_lexicon, output] of ctx.outputs) {
35
+ const template = parseCFTemplate(output);
36
+ if (!template?.Resources) continue;
37
+ const resources = template.Resources;
38
+
39
+ const routeTableIds = new Set<string>();
40
+ const attachmentIds = new Set<string>();
41
+ for (const [logicalId, resource] of Object.entries(resources)) {
42
+ if (resource.Type === "AWS::EC2::TransitGatewayRouteTable") routeTableIds.add(logicalId);
43
+ if (ATTACHMENT_TYPES.has(resource.Type)) attachmentIds.add(logicalId);
44
+ }
45
+ if (routeTableIds.size === 0 && attachmentIds.size === 0) continue;
46
+
47
+ const associationsByTable = new Map<string, Set<string>>();
48
+ const propagationsByTable = new Map<string, Set<string>>();
49
+ const referencedAttachments = new Set<string>();
50
+
51
+ for (const resource of Object.values(resources)) {
52
+ let bucket: Map<string, Set<string>> | null = null;
53
+ if (resource.Type === "AWS::EC2::TransitGatewayRouteTableAssociation") bucket = associationsByTable;
54
+ if (resource.Type === "AWS::EC2::TransitGatewayRouteTablePropagation") bucket = propagationsByTable;
55
+ if (!bucket) continue;
56
+
57
+ const props = resource.Properties ?? {};
58
+ const attachmentRefs = findResourceRefs(props.TransitGatewayAttachmentId);
59
+ for (const attId of attachmentRefs) referencedAttachments.add(attId);
60
+
61
+ for (const rtId of findResourceRefs(props.TransitGatewayRouteTableId)) {
62
+ let attSet = bucket.get(rtId);
63
+ if (!attSet) {
64
+ attSet = new Set();
65
+ bucket.set(rtId, attSet);
66
+ }
67
+ for (const attId of attachmentRefs) attSet.add(attId);
68
+ }
69
+ }
70
+
71
+ // Pattern A: associated but never propagated.
72
+ for (const rtId of routeTableIds) {
73
+ const associations = associationsByTable.get(rtId);
74
+ const propagations = propagationsByTable.get(rtId);
75
+ if (associations && associations.size > 0 && (!propagations || propagations.size === 0)) {
76
+ diagnostics.push({
77
+ checkId: "WAW065",
78
+ severity: "warning",
79
+ message: `TransitGatewayRouteTable "${rtId}" has ${associations.size} association(s) but no route propagations — the associated attachment's routes never populate this table, likely incomplete wiring`,
80
+ entity: rtId,
81
+ lexicon: "aws",
82
+ });
83
+ }
84
+ }
85
+
86
+ // Pattern B: a declared attachment wired into no route table at all.
87
+ for (const attId of attachmentIds) {
88
+ if (!referencedAttachments.has(attId)) {
89
+ diagnostics.push({
90
+ checkId: "WAW065",
91
+ severity: "warning",
92
+ message: `Transit Gateway attachment "${attId}" is referenced by no TransitGatewayRouteTableAssociation or TransitGatewayRouteTablePropagation — it has no route table wiring, so nothing can route to or from it`,
93
+ entity: attId,
94
+ lexicon: "aws",
95
+ });
96
+ }
97
+ }
98
+ }
99
+
100
+ return diagnostics;
101
+ }
102
+
103
+ export const waw065: PostSynthCheck = {
104
+ id: "WAW065",
105
+ description: "Transit Gateway route table has associations but no propagations, or an attachment wired into no route table",
106
+
107
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
108
+ return checkTgwRouteTableWiring(ctx);
109
+ },
110
+ };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * WAW066: Private Subnet Route Table Has No Working Default Route
3
+ *
4
+ * Cross-resource join: a subnet's outbound path depends on a chain of three
5
+ * independently-declared resources — the `AWS::EC2::SubnetRouteTableAssociation`
6
+ * that pins it to a route table, the `AWS::EC2::Route` on that table for
7
+ * `0.0.0.0/0`, and the gateway that route names. CloudFormation validates
8
+ * none of this end-to-end: a subnet can be wired to a route table with no
9
+ * default route at all, or with a default route whose NAT gateway / Transit
10
+ * Gateway target was renamed or removed elsewhere in the template, and the
11
+ * stack still deploys clean. The subnet is black-holed — every deploy
12
+ * succeeds, and the only symptom is a workload with no outbound connectivity.
13
+ *
14
+ * A route table with a default route to an `AWS::EC2::InternetGateway` is
15
+ * treated as a public subnet and is out of scope for this check — this rule
16
+ * only flags the private case: no default route, or one whose target
17
+ * (`NatGatewayId` / `TransitGatewayId` / etc.) does not resolve to a
18
+ * resource declared in the same template. A route naming a target that
19
+ * exists in the template resolves cleanly and is never flagged, per the
20
+ * cross-resource contract.
21
+ */
22
+
23
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
24
+ import { parseCFTemplate, findResourceRefs, type CFResource, type CFTemplate } from "./cf-refs";
25
+
26
+ type RouteClass = "public" | "resolved" | "dangling";
27
+
28
+ const OTHER_TARGET_KEYS = [
29
+ "NatGatewayId",
30
+ "TransitGatewayId",
31
+ "VpcPeeringConnectionId",
32
+ "EgressOnlyInternetGatewayId",
33
+ "LocalGatewayId",
34
+ "CarrierGatewayId",
35
+ "NetworkInterfaceId",
36
+ "InstanceId",
37
+ ] as const;
38
+
39
+ /** Classify a `0.0.0.0/0` Route's target. A target Ref that exists always resolves. */
40
+ function classifyDefaultRoute(route: CFResource, resources: Record<string, CFResource>): RouteClass {
41
+ const props = route.Properties ?? {};
42
+
43
+ if (props.GatewayId !== undefined) {
44
+ if (typeof props.GatewayId === "string") {
45
+ return props.GatewayId.startsWith("igw-") ? "public" : "resolved";
46
+ }
47
+ const refs = findResourceRefs(props.GatewayId);
48
+ if (refs.size !== 1) return "resolved"; // unprovable intrinsic — stay quiet
49
+ const [id] = refs;
50
+ if (!resources[id]) return "dangling";
51
+ return resources[id].Type === "AWS::EC2::InternetGateway" ? "public" : "resolved";
52
+ }
53
+
54
+ for (const key of OTHER_TARGET_KEYS) {
55
+ const value = (props as Record<string, unknown>)[key];
56
+ if (value === undefined) continue;
57
+ if (typeof value === "string") return "resolved"; // literal id from outside the template
58
+ const refs = findResourceRefs(value);
59
+ if (refs.size !== 1) return "resolved";
60
+ const [id] = refs;
61
+ return resources[id] ? "resolved" : "dangling";
62
+ }
63
+
64
+ return "resolved"; // no recognized target key — can't classify, stay quiet
65
+ }
66
+
67
+ function isDefaultRoute(route: CFResource): boolean {
68
+ return route.Properties?.DestinationCidrBlock === "0.0.0.0/0";
69
+ }
70
+
71
+ export function checkPrivateSubnetDefaultRoute(ctx: PostSynthContext): PostSynthDiagnostic[] {
72
+ const diagnostics: PostSynthDiagnostic[] = [];
73
+
74
+ for (const [_lexicon, output] of ctx.outputs) {
75
+ const template: CFTemplate | null = parseCFTemplate(output);
76
+ if (!template?.Resources) continue;
77
+ const resources = template.Resources;
78
+
79
+ // subnetId -> Set<routeTableId>, from explicit associations only.
80
+ const subnetRouteTables = new Map<string, Set<string>>();
81
+ for (const resource of Object.values(resources)) {
82
+ if (resource.Type !== "AWS::EC2::SubnetRouteTableAssociation") continue;
83
+ const props = resource.Properties ?? {};
84
+ const subnetRefs = findResourceRefs(props.SubnetId);
85
+ const rtRefs = findResourceRefs(props.RouteTableId);
86
+ for (const subnetId of subnetRefs) {
87
+ let set = subnetRouteTables.get(subnetId);
88
+ if (!set) {
89
+ set = new Set();
90
+ subnetRouteTables.set(subnetId, set);
91
+ }
92
+ for (const rtId of rtRefs) set.add(rtId);
93
+ }
94
+ }
95
+
96
+ // routeTableId -> Route resources targeting it.
97
+ const routesByTable = new Map<string, CFResource[]>();
98
+ for (const resource of Object.values(resources)) {
99
+ if (resource.Type !== "AWS::EC2::Route") continue;
100
+ for (const rtId of findResourceRefs(resource.Properties?.RouteTableId)) {
101
+ let list = routesByTable.get(rtId);
102
+ if (!list) {
103
+ list = [];
104
+ routesByTable.set(rtId, list);
105
+ }
106
+ list.push(resource);
107
+ }
108
+ }
109
+
110
+ for (const [subnetId, rtIds] of subnetRouteTables) {
111
+ if (resources[subnetId]?.Type !== "AWS::EC2::Subnet") continue;
112
+
113
+ for (const rtId of rtIds) {
114
+ if (resources[rtId]?.Type !== "AWS::EC2::RouteTable") continue;
115
+
116
+ const routes = routesByTable.get(rtId) ?? [];
117
+ const defaultRoutes = routes.filter(isDefaultRoute);
118
+
119
+ if (defaultRoutes.length === 0) {
120
+ diagnostics.push({
121
+ checkId: "WAW066",
122
+ severity: "warning",
123
+ message: `Subnet "${subnetId}" is associated with route table "${rtId}", which has no default route (0.0.0.0/0) — outbound traffic from this subnet has no path out, black-holed by omission`,
124
+ entity: subnetId,
125
+ lexicon: "aws",
126
+ });
127
+ continue;
128
+ }
129
+
130
+ const classes = defaultRoutes.map((route) => classifyDefaultRoute(route, resources));
131
+ if (classes.includes("public")) continue; // has a real IGW default route — public subnet, out of scope
132
+ if (classes.every((c) => c === "dangling")) {
133
+ diagnostics.push({
134
+ checkId: "WAW066",
135
+ severity: "error",
136
+ message: `Subnet "${subnetId}"'s route table "${rtId}" has a default route (0.0.0.0/0) whose target does not exist in this template — outbound traffic from this subnet is black-holed`,
137
+ entity: subnetId,
138
+ lexicon: "aws",
139
+ });
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ return diagnostics;
146
+ }
147
+
148
+ export const waw066: PostSynthCheck = {
149
+ id: "WAW066",
150
+ description: "Private subnet's route table has no default route, or its default route targets a gateway that does not exist in the template",
151
+
152
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
153
+ return checkPrivateSubnetDefaultRoute(ctx);
154
+ },
155
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * WAW067: Single-AZ NAT Gateway Serves Multi-AZ Private Subnets
3
+ *
4
+ * Cross-resource join across four resource kinds: a NAT gateway lives in one
5
+ * subnet, which lives in one Availability Zone; a route table's default
6
+ * route names the NAT gateway it forwards to; a subnet is pinned to a route
7
+ * table by an `AWS::EC2::SubnetRouteTableAssociation`. Follow that chain and
8
+ * a common reliability gap falls out — private subnets in two or more AZs
9
+ * all defaulting to the *same* NAT gateway. CloudFormation has no opinion on
10
+ * this; it deploys cleanly. But a NAT gateway lives entirely inside one AZ,
11
+ * so if that AZ has an outage, every subnet depending on it loses egress —
12
+ * including the subnets in AZs that were otherwise healthy. The
13
+ * Well-Architected fix is one NAT gateway per AZ, each serving only its own
14
+ * AZ's subnets.
15
+ *
16
+ * Only fires when the Availability Zones involved are literal strings —
17
+ * an `Fn::GetAZs`/`Fn::Select`-derived AZ can't be compared statically, so a
18
+ * subnet without a literal `AvailabilityZone` is silently excluded rather
19
+ * than guessed at.
20
+ */
21
+
22
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
23
+ import { parseCFTemplate, findResourceRefs, type CFTemplate } from "./cf-refs";
24
+
25
+ export function checkNatGatewaySingleAz(ctx: PostSynthContext): PostSynthDiagnostic[] {
26
+ const diagnostics: PostSynthDiagnostic[] = [];
27
+
28
+ for (const [_lexicon, output] of ctx.outputs) {
29
+ const template: CFTemplate | null = parseCFTemplate(output);
30
+ if (!template?.Resources) continue;
31
+ const resources = template.Resources;
32
+
33
+ // NAT gateway id -> its own subnet's literal AZ.
34
+ const natGatewayAz = new Map<string, string>();
35
+ for (const [logicalId, resource] of Object.entries(resources)) {
36
+ if (resource.Type !== "AWS::EC2::NatGateway") continue;
37
+ const subnetRefs = findResourceRefs(resource.Properties?.SubnetId);
38
+ if (subnetRefs.size !== 1) continue;
39
+ const [subnetId] = subnetRefs;
40
+ const az = resources[subnetId]?.Properties?.AvailabilityZone;
41
+ if (typeof az === "string") natGatewayAz.set(logicalId, az);
42
+ }
43
+ if (natGatewayAz.size === 0) continue;
44
+
45
+ // route table id -> the NAT gateway its default route forwards to.
46
+ const routeTableNatTarget = new Map<string, string>();
47
+ for (const resource of Object.values(resources)) {
48
+ if (resource.Type !== "AWS::EC2::Route") continue;
49
+ const props = resource.Properties ?? {};
50
+ if (props.DestinationCidrBlock !== "0.0.0.0/0") continue;
51
+ const natRefs = findResourceRefs(props.NatGatewayId);
52
+ if (natRefs.size !== 1) continue;
53
+ const [natId] = natRefs;
54
+ if (!natGatewayAz.has(natId)) continue;
55
+ for (const rtId of findResourceRefs(props.RouteTableId)) {
56
+ routeTableNatTarget.set(rtId, natId);
57
+ }
58
+ }
59
+
60
+ // subnet id -> route table id, from explicit associations only.
61
+ const subnetRouteTable = new Map<string, string>();
62
+ for (const resource of Object.values(resources)) {
63
+ if (resource.Type !== "AWS::EC2::SubnetRouteTableAssociation") continue;
64
+ const props = resource.Properties ?? {};
65
+ const subnetRefs = findResourceRefs(props.SubnetId);
66
+ const rtRefs = findResourceRefs(props.RouteTableId);
67
+ if (subnetRefs.size !== 1 || rtRefs.size !== 1) continue;
68
+ const [subnetId] = subnetRefs;
69
+ const [rtId] = rtRefs;
70
+ subnetRouteTable.set(subnetId, rtId);
71
+ }
72
+
73
+ // NAT gateway id -> the set of subnets that default-route through it.
74
+ const servedSubnets = new Map<string, Set<string>>();
75
+ for (const [subnetId, rtId] of subnetRouteTable) {
76
+ const natId = routeTableNatTarget.get(rtId);
77
+ if (!natId) continue;
78
+ let set = servedSubnets.get(natId);
79
+ if (!set) {
80
+ set = new Set();
81
+ servedSubnets.set(natId, set);
82
+ }
83
+ set.add(subnetId);
84
+ }
85
+
86
+ for (const [natId, subnetIds] of servedSubnets) {
87
+ const azs = new Set<string>();
88
+ for (const subnetId of subnetIds) {
89
+ const az = resources[subnetId]?.Properties?.AvailabilityZone;
90
+ if (typeof az === "string") azs.add(az);
91
+ }
92
+ if (azs.size > 1) {
93
+ diagnostics.push({
94
+ checkId: "WAW067",
95
+ severity: "warning",
96
+ message: `NAT Gateway "${natId}" (in ${natGatewayAz.get(natId)}) is the default-route egress for private subnets across ${azs.size} Availability Zones (${[...azs].sort().join(", ")}) — a single-AZ NAT gateway is a single point of failure for cross-AZ egress; add one NAT gateway per AZ`,
97
+ entity: natId,
98
+ lexicon: "aws",
99
+ });
100
+ }
101
+ }
102
+ }
103
+
104
+ return diagnostics;
105
+ }
106
+
107
+ export const waw067: PostSynthCheck = {
108
+ id: "WAW067",
109
+ description: "A single-AZ NAT gateway serves private subnets across multiple Availability Zones — single point of failure for cross-AZ egress",
110
+
111
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
112
+ return checkNatGatewaySingleAz(ctx);
113
+ },
114
+ };
@@ -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
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AAkZlG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UAoE3B,CAAC"}
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAsC,MAAM,6BAA6B,CAAC;AAiflG;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,UA4E3B,CAAC"}
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  skill: chant-aws-carve-terraform
3
- description: Demo carving a resource out of Terraform into native chant — advise, emit, bridge, apply — fully offline
3
+ description: Demo carving a resource out of Terraform into native chant — advise, emit, audit, bridge, apply — fully offline
4
4
  user-invocable: true
5
5
  ---
6
6
 
@@ -15,7 +15,7 @@ graduation.
15
15
  ## When to use
16
16
 
17
17
  - Someone asks "how does chant move things off Terraform?"
18
- - You want to show the advise → emit → bridge → apply loop end to end.
18
+ - You want to show the advise → emit → audit → bridge → apply loop end to end.
19
19
  - You are evaluating whether a Terraform estate is worth carving.
20
20
 
21
21
  ## Preconditions
@@ -27,7 +27,7 @@ graduation.
27
27
  ## The fastest path: run the bundled demo
28
28
 
29
29
  The `examples/terraform-carve-out` example ships a runnable estate + state and a
30
- script that runs all four steps with commentary:
30
+ script that runs all five steps with commentary:
31
31
 
32
32
  ```bash
33
33
  cd examples/terraform-carve-out
@@ -54,28 +54,30 @@ From `examples/terraform-carve-out`, with `TF=./terraform`:
54
54
  --state ./terraform/terraform.tfstate --output ./carveout
55
55
  ```
56
56
  Show `./carveout/src/assets.ts` — a real `new Bucket({ BucketName, Tags })` with
57
- CloudFormation-style properties mapped from the Terraform state attributes.
58
- Emit also scaffolds `./carveout` into a buildable chant project
59
- (`chant.config.ts`, `package.json`), so `npm install && npm run build` works
60
- there as-is. Explain: a Terraform-managed resource is not in any
61
- CloudFormation stack, so the correct source of its live shape is the state
62
- file, not a cloud read.
63
-
64
- 3. **Lint the inherited resource — optional, offer it.**
57
+ CloudFormation-style properties mapped from the Terraform state attributes,
58
+ and the `aws_s3_bucket_versioning` sub-resource folded into
59
+ `VersioningConfiguration`. Emit also scaffolds `./carveout` into a buildable
60
+ chant project (`chant.config.ts`, `package.json`) and persists a carve
61
+ manifest (`*.carve.json`) bridge and apply read the target from it, so
62
+ `--select` is only needed once. Explain: a Terraform-managed resource is not
63
+ in any CloudFormation stack, so the correct source of its live shape is the
64
+ state file, not a cloud read.
65
+
66
+ 3. **Audit the inherited resource.**
65
67
  ```bash
66
- chant lint ./carveout/src --lexicon aws
68
+ chant build ./carveout/src --lexicon aws
67
69
  ```
68
- After emit, offer to lint the carved source. chant audits the resource you
69
- inherited from Terraform against the AWS lexicon's rules. Findings have a
70
- severity: `error` (must fix before `chant build` will emit e.g. an S3
71
- bucket with no public-access block) and `warning`/`info` (advisory — e.g.
72
- DynamoDB point-in-time recovery). This is a feature of carving: chant
73
- immediately tells the person what is wrong with what they adopted. Whether to
74
- fix an advisory finding is their call; errors block the build until resolved.
70
+ The first build fails, deliberately: the post-synth audit refuses the
71
+ adopted bucket because Terraform managed it without a public-access block or
72
+ a TLS-only policy. This is a feature of carving: chant immediately tells the
73
+ person what is wrong with what they adopted. Show the fix (add
74
+ `PublicAccessBlockConfiguration` and a companion `S3BucketPolicy` with an
75
+ `aws:SecureTransport = false` Deny the tutorial page has the exact code),
76
+ then re-run the build and show the valid CloudFormation template.
75
77
 
76
78
  4. **Bridge — patch the surviving Terraform.**
77
79
  ```bash
78
- chant carve bridge --from ./terraform --select aws_s3_bucket.assets --output ./carveout
80
+ chant carve bridge --from ./terraform --output ./carveout
79
81
  ```
80
82
  Show the generated `data "aws_s3_bucket" "assets"` and the rewired survivor.
81
83
  Emphasize it is dry-run: nothing in `./terraform` changed. `--apply-rewrites`
@@ -83,11 +85,12 @@ From `examples/terraform-carve-out`, with `TF=./terraform`:
83
85
 
84
86
  5. **Apply — graduation plan.**
85
87
  ```bash
86
- chant carve apply --from ./terraform --select aws_s3_bucket.assets --env prod --stack assets
88
+ chant carve apply --from ./terraform --output ./carveout --env prod --stack assets
87
89
  ```
88
90
  Show the ownership marker (`chant:managed-by/stack/env`) and the finalized
89
- runbook. Stress this makes no cloud call the apply is the person's own
90
- lifecycle; chant just plans it.
91
+ runbook. `--write-source` additionally stamps the marker tags into the
92
+ emitted source. Stress this makes no cloud call — the apply is the person's
93
+ own lifecycle; chant just plans it.
91
94
 
92
95
  ## Key points to land
93
96
 
@@ -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.46.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",
@@ -73,14 +73,14 @@
73
73
  },
74
74
  "dependencies": {
75
75
  "fflate": "^0.8.2",
76
- "js-yaml": "^4.1.0"
76
+ "js-yaml": "^4.3.1"
77
77
  },
78
78
  "devDependencies": {
79
79
  "@intentius/chant": "*",
80
80
  "typescript": "^5.9.3"
81
81
  },
82
82
  "peerDependencies": {
83
- "@intentius/chant": "^0.46.0",
83
+ "@intentius/chant": "^0.50.0",
84
84
  "typescript": "^5.9.3"
85
85
  }
86
86
  }
@@ -198,6 +198,15 @@ describe("Cloud Control", () => {
198
198
  expect(await listResources("AWS::S3::Bucket", { http })).toEqual([]);
199
199
  });
200
200
 
201
+ test("listResources scopes a child listing with ResourceModel, JSON-encoded as the API takes it", async () => {
202
+ const { http, calls } = recording(() => respond(JSON.stringify({ ResourceDescriptions: [] })));
203
+ await listResources("AWS::IAM::RolePolicy", { http }, { RoleName: "app-role" });
204
+ expect(JSON.parse(calls[0].body)).toEqual({
205
+ TypeName: "AWS::IAM::RolePolicy",
206
+ ResourceModel: JSON.stringify({ RoleName: "app-role" }),
207
+ });
208
+ });
209
+
201
210
  test("parseResourceDescription refuses anything that is not a model object", () => {
202
211
  expect(parseResourceDescription(null)).toBeNull();
203
212
  expect(parseResourceDescription({ Identifier: "a" })).toBeNull();
@@ -434,17 +434,30 @@ export async function getResource(
434
434
  return parseResourceDescription(body.ResourceDescription);
435
435
  }
436
436
 
437
- /** `ListResources` — every live resource of one type, paginated to exhaustion. */
437
+ /**
438
+ * `ListResources` — every live resource of one type, paginated to exhaustion.
439
+ *
440
+ * `resourceModel` is Cloud Control's `ResourceModel`: the scope a child-typed
441
+ * listing requires (`{ RoleName }` for `AWS::IAM::RolePolicy`,
442
+ * `{ TopicArn }` for `AWS::SNS::Subscription`). The API takes it as a JSON
443
+ * *string*, the same double encoding `GetResource` answers with; this takes
444
+ * the object and encodes it so no caller repeats that detail.
445
+ */
438
446
  export async function listResources(
439
447
  typeName: string,
440
448
  options: AwsReadClientOptions = {},
449
+ resourceModel?: Record<string, unknown>,
441
450
  ): Promise<CloudControlDescription[]> {
442
451
  const out: CloudControlDescription[] = [];
443
452
  let nextToken: string | undefined;
444
453
  do {
445
454
  const body = await cloudControl(
446
455
  "ListResources",
447
- { TypeName: typeName, ...(nextToken ? { NextToken: nextToken } : {}) },
456
+ {
457
+ TypeName: typeName,
458
+ ...(resourceModel ? { ResourceModel: JSON.stringify(resourceModel) } : {}),
459
+ ...(nextToken ? { NextToken: nextToken } : {}),
460
+ },
448
461
  options,
449
462
  );
450
463
  const descriptions = Array.isArray(body.ResourceDescriptions) ? body.ResourceDescriptions : [];
@@ -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: [],