@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
package/src/index.ts CHANGED
@@ -7,6 +7,29 @@ export type { DefaultTags, TagEntry } from "./default-tags";
7
7
  export { templateTransform, isTemplateTransform, TEMPLATE_TRANSFORM_MARKER } from "./template-transform";
8
8
  export type { TemplateTransform } from "./template-transform";
9
9
 
10
+ // Effect receipts (#1835, epic #1703): the aws materialization row
11
+ // (`AWS::SSM::Parameter`, plain String, path derived from the ownership
12
+ // marker) and the SSM-backed receipt store behind the `effect()` op step.
13
+ export {
14
+ EffectReceipt,
15
+ receiptParameterName,
16
+ AWS_EFFECT_RECEIPT_ENTITY_TYPE,
17
+ EFFECT_RECEIPTS_METADATA_KEY,
18
+ RECEIPT_PATH_PREFIX,
19
+ RECEIPT_UNRESOLVED_VALUE_NOTE,
20
+ } from "./effect-receipt-row";
21
+ export type { AwsEffectReceiptDeclaration } from "./effect-receipt-row";
22
+ export { awsReceiptStore, observeReceiptRows, ssmGetParameter, ssmPutParameter } from "./receipt-store";
23
+ export type { AwsReceiptStoreOptions, ReceiptRowObservation } from "./receipt-store";
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
+
10
33
  // Serializer
11
34
  export { awsSerializer } from "./serializer";
12
35
 
@@ -122,25 +145,31 @@ export {
122
145
  LambdaFunction, LambdaNode, LambdaPython, NodeLambda, PythonLambda,
123
146
  LambdaApi,
124
147
  LambdaScheduled, ScheduledLambda,
125
- LambdaSqs, LambdaEventBridge, LambdaDynamoDB, LambdaS3, LambdaSns,
126
- VpcDefault, FargateAlb, AlbShared, FargateService, RdsInstance, RdsPostgres,
148
+ LambdaSqs, LambdaEventBridge, LambdaDynamoDB, DynamoDBTable, EcrRepository, LambdaS3, LambdaSns,
149
+ VpcDefault, FargateAlb, AlbShared, FargateService, FARGATE_SERVICE_LIMITS, NlbService, RdsInstance, RdsPostgres,
127
150
  EfsWithAccessPoint,
128
- Ec2InstanceRole, MinimalVpc, EksCluster,
151
+ Ec2InstanceRole, Ec2InstanceBundle, MinimalVpc, EksCluster,
129
152
  SolrFargateService,
130
153
  MicrovmApp, MICROVM_LIMITS,
131
- AgentCoreAgent, agentCoreDefaultEndpointArn,
154
+ AgentCoreAgent, agentCoreDefaultEndpointArn, AGENTCORE_LIMITS,
132
155
  OrganizationRoot, GovernanceFoundation, RegionRestriction, OrganizationTrail,
156
+ StepFunctionsWorkflow,
157
+ MonitoringStack,
158
+ BucketDeployment,
133
159
  } from "./composites/index";
134
160
  export type {
135
161
  LambdaFunctionProps, LambdaApiProps, ScheduledLambdaProps,
136
- LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, LambdaS3Props, LambdaSnsProps,
137
- VpcDefaultProps, FargateAlbProps, AlbSharedProps, FargateServiceProps, RdsInstanceProps, RdsPostgresProps,
162
+ LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, DynamoDBTableProps, DynamoDBKey, DynamoDBCapacity, DynamoDBGlobalSecondaryIndex, EcrRepositoryProps, EcrLifecycleRule, EcrEncryption, LambdaS3Props, LambdaSnsProps,
163
+ VpcDefaultProps, FargateAlbProps, AlbSharedProps, FargateServiceProps, NlbServiceProps, RdsInstanceProps, RdsPostgresProps,
138
164
  EfsWithAccessPointProps,
139
- Ec2InstanceRoleProps, MinimalVpcProps, EksClusterProps,
165
+ Ec2InstanceRoleProps, Ec2InstanceBundleProps, Ec2InstanceBundleIngressRule, MinimalVpcProps, EksClusterProps,
140
166
  SolrFargateServiceProps,
141
167
  MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB,
142
168
  AgentCoreAgentProps, AgentCoreAgentResult,
143
169
  OrganizationRootProps, GovernanceFoundationProps, RegionRestrictionProps, OrganizationTrailProps,
170
+ StepFunctionsWorkflowProps,
171
+ MonitoringStackProps, MonitoringMetricSpec,
172
+ BucketDeploymentProps, BucketDeploymentResult,
144
173
  } from "./composites/index";
145
174
 
146
175
  // Code generation pipeline
@@ -72,4 +72,20 @@ export const awsAuditCatalog: Record<string, RuleMeta> = {
72
72
  // so no authority citation (those are reserved for merge-worthy entries).
73
73
  WAW059: auditRule("WAW059", "report-only", "guidance", "Wildcard Resource where the declared graph enumerates the touched set", "Tighten Resource from \"*\" to the Fn::GetAtt Arn list of the declared resources the role's consumers touch.", { category: "security" }),
74
74
  WAW060: auditRule("WAW060", "report-only", "guidance", "IAM policy attached to no principal", "Attach the policy via Roles/Users/Groups or reference it from a principal's ManagedPolicyArns — unattached it grants nothing.", { category: "security" }),
75
+
76
+ // #1140 — error-injection benchmark family (companion to epic #1139): static
77
+ // catches for the deploy-time/runtime failure class stock CDK can't see pre-synth.
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
+ 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
+ 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" }),
75
91
  };
@@ -169,6 +169,46 @@ export function isFullTierEnv(env: string | undefined): boolean {
169
169
  return env === "prod" || env === "production" || env === "full";
170
170
  }
171
171
 
172
+ /**
173
+ * Parse an IPv4 CIDR literal ("10.0.0.0/16") into its numeric base address
174
+ * and prefix length. Returns null for anything that isn't a plain IPv4 CIDR
175
+ * (IPv6, malformed octets, out-of-range prefix) — callers treat that as
176
+ * "can't prove statically" rather than an error.
177
+ */
178
+ export function parseIpv4Cidr(cidr: string): { base: number; prefix: number } | null {
179
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(cidr.trim());
180
+ if (!m) return null;
181
+ const octets = m.slice(1, 5).map(Number);
182
+ if (octets.some((o) => o < 0 || o > 255)) return null;
183
+ const prefix = Number(m[5]);
184
+ if (prefix < 0 || prefix > 32) return null;
185
+ const base = ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
186
+ return { base, prefix };
187
+ }
188
+
189
+ /** [networkAddress, broadcastAddress] for a parsed IPv4 CIDR block. */
190
+ function ipv4Range(cidr: { base: number; prefix: number }): [number, number] {
191
+ const maskBits = cidr.prefix === 0 ? 0 : (0xffffffff << (32 - cidr.prefix)) >>> 0;
192
+ const network = (cidr.base & maskBits) >>> 0;
193
+ const broadcast = (network | (~maskBits >>> 0)) >>> 0;
194
+ return [network, broadcast];
195
+ }
196
+
197
+ /**
198
+ * Whether `inner` (an IPv4 CIDR literal) falls entirely within `outer`.
199
+ * Returns null — not false — when either literal isn't a plain, parseable
200
+ * IPv4 CIDR (e.g. IPv6, or a CloudFormation intrinsic already stringified
201
+ * elsewhere); a null means "statically unprovable", not "violation".
202
+ */
203
+ export function ipv4CidrContains(outer: string, inner: string): boolean | null {
204
+ const outerCidr = parseIpv4Cidr(outer);
205
+ const innerCidr = parseIpv4Cidr(inner);
206
+ if (!outerCidr || !innerCidr) return null;
207
+ const [oStart, oEnd] = ipv4Range(outerCidr);
208
+ const [iStart, iEnd] = ipv4Range(innerCidr);
209
+ return iStart >= oStart && iEnd <= oEnd;
210
+ }
211
+
172
212
  /**
173
213
  * Check if a port range [fromPort, toPort] contains any of the sensitive ports.
174
214
  */
@@ -52,6 +52,14 @@ import { waw057 } from "./waw057";
52
52
  import { waw058 } from "./waw058";
53
53
  import { waw059 } from "./waw059";
54
54
  import { waw060 } from "./waw060";
55
+ import { waw061 } from "./waw061";
56
+ import { waw062 } from "./waw062";
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";
55
63
 
56
64
  export const postSynthChecks: PostSynthCheck[] = [
57
65
  cor020,
@@ -106,4 +114,12 @@ export const postSynthChecks: PostSynthCheck[] = [
106
114
  waw058,
107
115
  waw059,
108
116
  waw060,
117
+ waw061,
118
+ waw062,
119
+ waw063,
120
+ waw064,
121
+ waw065,
122
+ waw066,
123
+ waw067,
124
+ waw068,
109
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,99 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw061, checkSubnetCidrContainment } from "./waw061";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW061: Subnet CIDR Not Contained In VPC CIDR", () => {
10
+ test("check metadata", () => {
11
+ expect(waw061.id).toBe("WAW061");
12
+ expect(waw061.description).toContain("CidrBlock");
13
+ });
14
+
15
+ test("subnet CIDR outside the VPC's CIDR range → error", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
19
+ MySubnet: {
20
+ Type: "AWS::EC2::Subnet",
21
+ Properties: { CidrBlock: "10.1.0.0/24", VpcId: { Ref: "MyVpc" } },
22
+ },
23
+ },
24
+ });
25
+ const diags = checkSubnetCidrContainment(ctx);
26
+ expect(diags).toHaveLength(1);
27
+ expect(diags[0].checkId).toBe("WAW061");
28
+ expect(diags[0].severity).toBe("error");
29
+ expect(diags[0].entity).toBe("MySubnet");
30
+ expect(diags[0].message).toContain("10.1.0.0/24");
31
+ expect(diags[0].message).toContain("10.0.0.0/16");
32
+ expect(diags[0].lexicon).toBe("aws");
33
+ });
34
+
35
+ test("subnet wider than its VPC → error", () => {
36
+ const ctx = makeCtx({
37
+ Resources: {
38
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.1.0/24" } },
39
+ MySubnet: {
40
+ Type: "AWS::EC2::Subnet",
41
+ Properties: { CidrBlock: "10.0.0.0/16", VpcId: { Ref: "MyVpc" } },
42
+ },
43
+ },
44
+ });
45
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(1);
46
+ });
47
+
48
+ test("subnet CIDR contained in the VPC's CIDR → no diagnostic", () => {
49
+ const ctx = makeCtx({
50
+ Resources: {
51
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
52
+ MySubnet: {
53
+ Type: "AWS::EC2::Subnet",
54
+ Properties: { CidrBlock: "10.0.1.0/24", VpcId: { Ref: "MyVpc" } },
55
+ },
56
+ },
57
+ });
58
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
59
+ });
60
+
61
+ test("intrinsic CidrBlock → no diagnostic (unprovable)", () => {
62
+ const ctx = makeCtx({
63
+ Resources: {
64
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
65
+ MySubnet: {
66
+ Type: "AWS::EC2::Subnet",
67
+ Properties: {
68
+ CidrBlock: { "Fn::Select": [0, { "Fn::Cidr": [{ "Fn::GetAtt": ["MyVpc", "CidrBlock"] }, 4, 8] }] },
69
+ VpcId: { Ref: "MyVpc" },
70
+ },
71
+ },
72
+ },
73
+ });
74
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
75
+ });
76
+
77
+ test("VpcId not resolvable to a declared VPC (imported) → no diagnostic", () => {
78
+ const ctx = makeCtx({
79
+ Resources: {
80
+ MySubnet: {
81
+ Type: "AWS::EC2::Subnet",
82
+ Properties: { CidrBlock: "10.1.0.0/24", VpcId: { "Fn::ImportValue": "shared-vpc-id" } },
83
+ },
84
+ },
85
+ });
86
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
87
+ });
88
+
89
+ test("multiple subnets in a VPC, all contained → no diagnostic", () => {
90
+ const ctx = makeCtx({
91
+ Resources: {
92
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
93
+ SubnetA: { Type: "AWS::EC2::Subnet", Properties: { CidrBlock: "10.0.0.0/24", VpcId: { Ref: "MyVpc" } } },
94
+ SubnetB: { Type: "AWS::EC2::Subnet", Properties: { CidrBlock: "10.0.1.0/24", VpcId: { Ref: "MyVpc" } } },
95
+ },
96
+ });
97
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
98
+ });
99
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * WAW061: Subnet CIDR Not Contained In VPC CIDR
3
+ *
4
+ * A subnet's CidrBlock must fall entirely inside its VPC's CidrBlock.
5
+ * Stock CDK does not check this — CloudFormation accepts the template and
6
+ * only fails the `AWS::EC2::Subnet` resource at deploy time
7
+ * ("The CIDR ... is invalid" / "does not fall within the CIDR range of the
8
+ * VPC"), after the rest of the stack may have already started rolling out.
9
+ * This is a cheap static graph check: follow the subnet's VpcId Ref to a
10
+ * declared VPC in the same template and do IPv4 CIDR-range math.
11
+ *
12
+ * Stays quiet whenever the graph can't prove it: intrinsic CIDR blocks,
13
+ * IPv6, a VpcId that isn't a simple Ref to a declared AWS::EC2::VPC, or a
14
+ * VPC whose own CidrBlock isn't a literal string.
15
+ */
16
+
17
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
18
+ import { parseCFTemplate, findResourceRefs, ipv4CidrContains } from "./cf-refs";
19
+
20
+ export function checkSubnetCidrContainment(ctx: PostSynthContext): PostSynthDiagnostic[] {
21
+ const diagnostics: PostSynthDiagnostic[] = [];
22
+
23
+ for (const [_lexicon, output] of ctx.outputs) {
24
+ const template = parseCFTemplate(output);
25
+ if (!template?.Resources) continue;
26
+
27
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
28
+ if (resource.Type !== "AWS::EC2::Subnet") continue;
29
+
30
+ const props = resource.Properties ?? {};
31
+ const subnetCidr = props.CidrBlock;
32
+ if (typeof subnetCidr !== "string") continue;
33
+
34
+ const vpcRefs = findResourceRefs(props.VpcId);
35
+ if (vpcRefs.size !== 1) continue;
36
+ const [vpcId] = vpcRefs;
37
+
38
+ const vpc = template.Resources[vpcId];
39
+ if (!vpc || vpc.Type !== "AWS::EC2::VPC") continue;
40
+
41
+ const vpcCidr = vpc.Properties?.CidrBlock;
42
+ if (typeof vpcCidr !== "string") continue;
43
+
44
+ const contained = ipv4CidrContains(vpcCidr, subnetCidr);
45
+ if (contained === false) {
46
+ diagnostics.push({
47
+ checkId: "WAW061",
48
+ severity: "error",
49
+ message: `Subnet "${logicalId}" CidrBlock ${subnetCidr} is not contained within VPC "${vpcId}"'s CidrBlock ${vpcCidr} — CloudFormation rejects this at deploy time`,
50
+ entity: logicalId,
51
+ lexicon: "aws",
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return diagnostics;
58
+ }
59
+
60
+ export const waw061: PostSynthCheck = {
61
+ id: "WAW061",
62
+ description: "Subnet CidrBlock falls outside its VPC's CidrBlock — fails at deploy time",
63
+
64
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
65
+ return checkSubnetCidrContainment(ctx);
66
+ },
67
+ };
@@ -0,0 +1,109 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw062, checkDuplicateNamesAndExports } from "./waw062";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW062: Duplicate Name Or Export Within A Template", () => {
10
+ test("check metadata", () => {
11
+ expect(waw062.id).toBe("WAW062");
12
+ expect(waw062.description.toLowerCase()).toContain("duplicate");
13
+ });
14
+
15
+ test("duplicate Export Name → error", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyBucket: { Type: "AWS::S3::Bucket", Properties: {} },
19
+ },
20
+ Outputs: {
21
+ BucketArnA: { Value: { "Fn::GetAtt": ["MyBucket", "Arn"] }, Export: { Name: "shared-bucket-arn" } },
22
+ BucketArnB: { Value: { "Fn::GetAtt": ["MyBucket", "Arn"] }, Export: { Name: "shared-bucket-arn" } },
23
+ },
24
+ });
25
+ const diags = checkDuplicateNamesAndExports(ctx);
26
+ expect(diags).toHaveLength(1);
27
+ expect(diags[0].checkId).toBe("WAW062");
28
+ expect(diags[0].severity).toBe("error");
29
+ expect(diags[0].message).toContain("shared-bucket-arn");
30
+ expect(diags[0].message).toContain("BucketArnA");
31
+ expect(diags[0].message).toContain("BucketArnB");
32
+ expect(diags[0].lexicon).toBe("aws");
33
+ });
34
+
35
+ test("unique Export Names → no diagnostic", () => {
36
+ const ctx = makeCtx({
37
+ Resources: {},
38
+ Outputs: {
39
+ A: { Value: "a", Export: { Name: "export-a" } },
40
+ B: { Value: "b", Export: { Name: "export-b" } },
41
+ },
42
+ });
43
+ expect(checkDuplicateNamesAndExports(ctx)).toHaveLength(0);
44
+ });
45
+
46
+ test("duplicate ECR RepositoryName across two composites → error", () => {
47
+ const ctx = makeCtx({
48
+ Resources: {
49
+ RepoOne: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "svc-images" } },
50
+ RepoTwo: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "svc-images" } },
51
+ },
52
+ });
53
+ const diags = checkDuplicateNamesAndExports(ctx);
54
+ expect(diags).toHaveLength(1);
55
+ expect(diags[0].message).toContain("svc-images");
56
+ expect(diags[0].message).toContain("RepoOne");
57
+ expect(diags[0].message).toContain("RepoTwo");
58
+ });
59
+
60
+ test("duplicate LaunchTemplateName → error", () => {
61
+ const ctx = makeCtx({
62
+ Resources: {
63
+ LtA: { Type: "AWS::EC2::LaunchTemplate", Properties: { LaunchTemplateName: "worker-lt" } },
64
+ LtB: { Type: "AWS::EC2::LaunchTemplate", Properties: { LaunchTemplateName: "worker-lt" } },
65
+ },
66
+ });
67
+ expect(checkDuplicateNamesAndExports(ctx)).toHaveLength(1);
68
+ });
69
+
70
+ test("duplicate SecurityGroup GroupName → error", () => {
71
+ const ctx = makeCtx({
72
+ Resources: {
73
+ SgA: { Type: "AWS::EC2::SecurityGroup", Properties: { GroupName: "web-sg", GroupDescription: "a" } },
74
+ SgB: { Type: "AWS::EC2::SecurityGroup", Properties: { GroupName: "web-sg", GroupDescription: "b" } },
75
+ },
76
+ });
77
+ expect(checkDuplicateNamesAndExports(ctx)).toHaveLength(1);
78
+ });
79
+
80
+ test("same literal name across different resource types → no diagnostic (not comparable)", () => {
81
+ const ctx = makeCtx({
82
+ Resources: {
83
+ RepoOne: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "shared" } },
84
+ TableOne: { Type: "AWS::DynamoDB::Table", Properties: { TableName: "shared" } },
85
+ },
86
+ });
87
+ expect(checkDuplicateNamesAndExports(ctx)).toHaveLength(0);
88
+ });
89
+
90
+ test("intrinsic explicit names → no diagnostic (unprovable)", () => {
91
+ const ctx = makeCtx({
92
+ Resources: {
93
+ RepoOne: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: { "Fn::Sub": "svc-${AWS::StackName}" } } },
94
+ RepoTwo: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: { "Fn::Sub": "svc-${AWS::StackName}" } } },
95
+ },
96
+ });
97
+ expect(checkDuplicateNamesAndExports(ctx)).toHaveLength(0);
98
+ });
99
+
100
+ test("distinct explicit names → no diagnostic", () => {
101
+ const ctx = makeCtx({
102
+ Resources: {
103
+ RepoOne: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "svc-images-a" } },
104
+ RepoTwo: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "svc-images-b" } },
105
+ },
106
+ });
107
+ expect(checkDuplicateNamesAndExports(ctx)).toHaveLength(0);
108
+ });
109
+ });
Binary file