@intentius/chant-lexicon-aws 0.45.0 → 0.49.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 (147) 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/components/cloud-executor.d.ts.map +1 -1
  4. package/dist/composites/agentcore-agent.d.ts +26 -1
  5. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  6. package/dist/composites/bucket-deployment.d.ts +82 -0
  7. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  8. package/dist/composites/dynamodb-table.d.ts +45 -0
  9. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  10. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  11. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  12. package/dist/composites/ecr-repository.d.ts +49 -0
  13. package/dist/composites/ecr-repository.d.ts.map +1 -0
  14. package/dist/composites/fargate-service.d.ts +16 -0
  15. package/dist/composites/fargate-service.d.ts.map +1 -1
  16. package/dist/composites/index.d.ts +16 -2
  17. package/dist/composites/index.d.ts.map +1 -1
  18. package/dist/composites/lambda-function.d.ts +4 -4
  19. package/dist/composites/lambda-function.d.ts.map +1 -1
  20. package/dist/composites/monitoring-stack.d.ts +99 -0
  21. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  22. package/dist/composites/nlb-service.d.ts +48 -0
  23. package/dist/composites/nlb-service.d.ts.map +1 -0
  24. package/dist/composites/step-functions-workflow.d.ts +36 -0
  25. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  26. package/dist/deep-observe.d.ts +45 -0
  27. package/dist/deep-observe.d.ts.map +1 -1
  28. package/dist/deep-topology.d.ts +104 -0
  29. package/dist/deep-topology.d.ts.map +1 -0
  30. package/dist/effect-receipt-row.d.ts +92 -0
  31. package/dist/effect-receipt-row.d.ts.map +1 -0
  32. package/dist/index.d.ts +6 -2
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/integrity.json +9 -4
  35. package/dist/lint/audit-catalog.d.ts.map +1 -1
  36. package/dist/lint/post-synth/cf-refs.d.ts +24 -0
  37. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  38. package/dist/lint/post-synth/index.d.ts.map +1 -1
  39. package/dist/lint/post-synth/waw059.d.ts +36 -0
  40. package/dist/lint/post-synth/waw059.d.ts.map +1 -0
  41. package/dist/lint/post-synth/waw060.d.ts +16 -0
  42. package/dist/lint/post-synth/waw060.d.ts.map +1 -0
  43. package/dist/lint/post-synth/waw061.d.ts +19 -0
  44. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  45. package/dist/lint/post-synth/waw062.d.ts +19 -0
  46. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  47. package/dist/lint/post-synth/waw063.d.ts +35 -0
  48. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  49. package/dist/manifest.json +1 -1
  50. package/dist/okf/index.md +5 -0
  51. package/dist/okf/rules/WAW059.md +25 -0
  52. package/dist/okf/rules/WAW060.md +17 -0
  53. package/dist/okf/rules/WAW061.md +17 -0
  54. package/dist/okf/rules/WAW062.md +27 -0
  55. package/dist/okf/rules/WAW063.md +19 -0
  56. package/dist/okf/types/Action.md +2 -0
  57. package/dist/okf/types/AutoScalingGroup.md +1 -0
  58. package/dist/okf/types/Bucket.md +2 -0
  59. package/dist/okf/types/ECRRepository.md +1 -0
  60. package/dist/okf/types/Function.md +1 -0
  61. package/dist/okf/types/GlobalTable.md +4 -0
  62. package/dist/okf/types/IamPolicy.md +3 -0
  63. package/dist/okf/types/InstanceProfile.md +4 -0
  64. package/dist/okf/types/LaunchTemplate.md +1 -0
  65. package/dist/okf/types/LoadBalancer.md +1 -0
  66. package/dist/okf/types/ManagedPolicy.md +4 -0
  67. package/dist/okf/types/Map.md +2 -0
  68. package/dist/okf/types/Queue.md +1 -0
  69. package/dist/okf/types/Role.md +3 -0
  70. package/dist/okf/types/SecurityGroup.md +1 -0
  71. package/dist/okf/types/StateMachine.md +4 -0
  72. package/dist/okf/types/Subnet.md +4 -0
  73. package/dist/okf/types/Table.md +2 -0
  74. package/dist/okf/types/Type.md +5 -0
  75. package/dist/okf/types/Vpc.md +1 -0
  76. package/dist/op/activities/aws-apply.d.ts +48 -5
  77. package/dist/op/activities/aws-apply.d.ts.map +1 -1
  78. package/dist/op/activities/index.d.ts +11 -4
  79. package/dist/op/activities/index.d.ts.map +1 -1
  80. package/dist/ownership.d.ts +18 -0
  81. package/dist/ownership.d.ts.map +1 -1
  82. package/dist/plugin.d.ts.map +1 -1
  83. package/dist/receipt-store.d.ts +109 -0
  84. package/dist/receipt-store.d.ts.map +1 -0
  85. package/dist/rules/cf-refs.ts +62 -0
  86. package/dist/rules/waw059.ts +353 -0
  87. package/dist/rules/waw060.ts +91 -0
  88. package/dist/rules/waw061.ts +67 -0
  89. package/dist/rules/waw062.ts +0 -0
  90. package/dist/rules/waw063.ts +0 -0
  91. package/dist/serializer.d.ts.map +1 -1
  92. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  93. package/dist/teardown.d.ts +85 -0
  94. package/dist/teardown.d.ts.map +1 -0
  95. package/package.json +3 -3
  96. package/src/api/read-client.test.ts +9 -0
  97. package/src/api/read-client.ts +15 -2
  98. package/src/components/cloud-executor.ts +10 -1
  99. package/src/composites/agentcore-agent.ts +35 -13
  100. package/src/composites/bucket-deployment.test.ts +84 -0
  101. package/src/composites/bucket-deployment.ts +153 -0
  102. package/src/composites/composites.test.ts +325 -0
  103. package/src/composites/dynamodb-table.test.ts +154 -0
  104. package/src/composites/dynamodb-table.ts +124 -0
  105. package/src/composites/ec2-instance-bundle.ts +134 -0
  106. package/src/composites/ecr-repository.test.ts +118 -0
  107. package/src/composites/ecr-repository.ts +117 -0
  108. package/src/composites/fargate-service.ts +19 -3
  109. package/src/composites/index.ts +21 -2
  110. package/src/composites/monitoring-stack.test.ts +130 -0
  111. package/src/composites/monitoring-stack.ts +164 -0
  112. package/src/composites/nlb-service.ts +117 -0
  113. package/src/composites/step-functions-workflow.ts +127 -0
  114. package/src/deep-observe.test.ts +201 -1
  115. package/src/deep-observe.ts +204 -0
  116. package/src/deep-topology.test.ts +392 -0
  117. package/src/deep-topology.ts +237 -0
  118. package/src/effect-receipt-row.test.ts +164 -0
  119. package/src/effect-receipt-row.ts +147 -0
  120. package/src/index.ts +28 -7
  121. package/src/lifecycle-integration.test.ts +4 -0
  122. package/src/lint/audit-catalog.ts +11 -0
  123. package/src/lint/post-synth/cf-refs.ts +62 -0
  124. package/src/lint/post-synth/index.ts +10 -0
  125. package/src/lint/post-synth/waw059.test.ts +309 -0
  126. package/src/lint/post-synth/waw059.ts +353 -0
  127. package/src/lint/post-synth/waw060.test.ts +131 -0
  128. package/src/lint/post-synth/waw060.ts +91 -0
  129. package/src/lint/post-synth/waw061.test.ts +99 -0
  130. package/src/lint/post-synth/waw061.ts +67 -0
  131. package/src/lint/post-synth/waw062.test.ts +109 -0
  132. package/src/lint/post-synth/waw062.ts +0 -0
  133. package/src/lint/post-synth/waw063.test.ts +220 -0
  134. package/src/lint/post-synth/waw063.ts +0 -0
  135. package/src/op/activities/aws-apply.test.ts +111 -0
  136. package/src/op/activities/aws-apply.ts +100 -6
  137. package/src/op/activities/index.ts +20 -3
  138. package/src/ownership.test.ts +24 -1
  139. package/src/ownership.ts +37 -0
  140. package/src/plugin.ts +59 -10
  141. package/src/receipt-store.test.ts +204 -0
  142. package/src/receipt-store.ts +315 -0
  143. package/src/serializer-ownership.test.ts +18 -0
  144. package/src/serializer.ts +114 -3
  145. package/src/skills/chant-aws-carve-terraform.md +26 -23
  146. package/src/teardown.test.ts +258 -0
  147. package/src/teardown.ts +276 -0
@@ -0,0 +1,131 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw060, checkPolicyUnattached } from "./waw060";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ const ALLOW_S3_READ = {
10
+ Version: "2012-10-17",
11
+ Statement: [{ Effect: "Allow", Action: ["s3:GetObject"], Resource: "arn:aws:s3:::data/*" }],
12
+ };
13
+
14
+ function managedPolicy(extra?: Record<string, unknown>) {
15
+ return {
16
+ Type: "AWS::IAM::ManagedPolicy",
17
+ Properties: { PolicyDocument: ALLOW_S3_READ, ...extra },
18
+ };
19
+ }
20
+
21
+ function inlinePolicy(extra?: Record<string, unknown>) {
22
+ return {
23
+ Type: "AWS::IAM::Policy",
24
+ Properties: { PolicyName: "read", PolicyDocument: ALLOW_S3_READ, ...extra },
25
+ };
26
+ }
27
+
28
+ function role(extra?: Record<string, unknown>) {
29
+ return {
30
+ Type: "AWS::IAM::Role",
31
+ Properties: {
32
+ AssumeRolePolicyDocument: {
33
+ Version: "2012-10-17",
34
+ Statement: [{ Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, Action: "sts:AssumeRole" }],
35
+ },
36
+ ...extra,
37
+ },
38
+ };
39
+ }
40
+
41
+ describe("WAW060: IAM policy attached to no principal", () => {
42
+ test("check metadata", () => {
43
+ expect(waw060.id).toBe("WAW060");
44
+ expect(waw060.description).toContain("principal");
45
+ });
46
+
47
+ test("flags an unattached IAM::Policy", () => {
48
+ const ctx = makeCtx({ Resources: { Orphan: inlinePolicy() } });
49
+ const diags = checkPolicyUnattached(ctx);
50
+ expect(diags).toHaveLength(1);
51
+ expect(diags[0].checkId).toBe("WAW060");
52
+ expect(diags[0].severity).toBe("warning");
53
+ expect(diags[0].entity).toBe("Orphan");
54
+ expect(diags[0].message).toContain("grants nothing");
55
+ });
56
+
57
+ test("flags an unattached ManagedPolicy and one with empty principal lists", () => {
58
+ const ctx = makeCtx({
59
+ Resources: {
60
+ Orphan: managedPolicy(),
61
+ EmptyLists: managedPolicy({ Roles: [], Users: [], Groups: [] }),
62
+ },
63
+ });
64
+ const diags = checkPolicyUnattached(ctx);
65
+ expect(diags.map((d) => d.entity).sort()).toEqual(["EmptyLists", "Orphan"]);
66
+ });
67
+
68
+ test("quiet when a role's ManagedPolicyArns references the policy", () => {
69
+ const ctx = makeCtx({
70
+ Resources: {
71
+ ReadPolicy: managedPolicy(),
72
+ AppRole: role({ ManagedPolicyArns: [{ Ref: "ReadPolicy" }] }),
73
+ },
74
+ });
75
+ expect(checkPolicyUnattached(ctx)).toHaveLength(0);
76
+ });
77
+
78
+ test("quiet when the policy declares a Roles list", () => {
79
+ const ctx = makeCtx({
80
+ Resources: {
81
+ AppRole: role(),
82
+ ReadPolicy: managedPolicy({ Roles: [{ Ref: "AppRole" }] }),
83
+ InlineRead: inlinePolicy({ Roles: [{ Ref: "AppRole" }] }),
84
+ },
85
+ });
86
+ expect(checkPolicyUnattached(ctx)).toHaveLength(0);
87
+ });
88
+
89
+ test("AWS-managed ARN strings elsewhere do not attach the template's own policy", () => {
90
+ const ctx = makeCtx({
91
+ Resources: {
92
+ Orphan: managedPolicy(),
93
+ AppRole: role({ ManagedPolicyArns: ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"] }),
94
+ },
95
+ });
96
+ const diags = checkPolicyUnattached(ctx);
97
+ expect(diags).toHaveLength(1);
98
+ expect(diags[0].entity).toBe("Orphan");
99
+ });
100
+
101
+ test("quiet on attachment-style resources and GetAtt edges", () => {
102
+ const ctx = makeCtx({
103
+ Resources: {
104
+ ReadPolicy: managedPolicy(),
105
+ Attachment: {
106
+ Type: "AWS::SSO::PermissionSet",
107
+ Properties: { ManagedPolicies: [{ "Fn::GetAtt": ["ReadPolicy", "PolicyArn"] }] },
108
+ },
109
+ },
110
+ });
111
+ expect(checkPolicyUnattached(ctx)).toHaveLength(0);
112
+ });
113
+
114
+ test("an Output exporting the policy counts as a reference", () => {
115
+ const ctx = makeCtx({
116
+ Resources: { SharedPolicy: managedPolicy() },
117
+ Outputs: { SharedPolicyArn: { Value: { Ref: "SharedPolicy" }, Export: { Name: "shared-read" } } },
118
+ });
119
+ expect(checkPolicyUnattached(ctx)).toHaveLength(0);
120
+ });
121
+
122
+ test("intrinsic principal lists are skipped, and non-policy IAM types are ignored", () => {
123
+ const ctx = makeCtx({
124
+ Resources: {
125
+ Conditional: managedPolicy({ Roles: { "Fn::If": ["UseRole", [{ Ref: "AppRole" }], []] } }),
126
+ AppRole: role(),
127
+ },
128
+ });
129
+ expect(checkPolicyUnattached(ctx)).toHaveLength(0);
130
+ });
131
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * WAW060: IAM Policy Attached to No Principal
3
+ *
4
+ * Flags AWS::IAM::Policy / AWS::IAM::ManagedPolicy resources with empty or
5
+ * absent Roles, Users, and Groups that no other part of the template
6
+ * references. A policy attached to no principal grants nothing — the same
7
+ * detached-guardrail shape WAW057 catches for SCPs. Any Ref/GetAtt edge to
8
+ * the policy (a role's ManagedPolicyArns, an attachment-style resource, a
9
+ * stack Output exporting it for another stack to attach) keeps it quiet;
10
+ * plain AWS-managed ARN strings elsewhere are not references to the
11
+ * template's own policy.
12
+ */
13
+
14
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
15
+ import { parseCFTemplate, findResourceRefs, isIntrinsic, type CFTemplate } from "./cf-refs";
16
+
17
+ const POLICY_TYPES = new Set(["AWS::IAM::Policy", "AWS::IAM::ManagedPolicy"]);
18
+ const PRINCIPAL_PROPS = ["Roles", "Users", "Groups"] as const;
19
+
20
+ /**
21
+ * Collect every logical id referenced via Ref/Fn::GetAtt from anywhere in the
22
+ * template except the policy resources themselves — other resources'
23
+ * properties and the Outputs section. An Output edge counts as attachment
24
+ * evidence: the policy may be exported for a consuming stack to attach.
25
+ */
26
+ function collectExternalRefs(template: CFTemplate, excludeIds: Set<string>): Set<string> {
27
+ const refs = new Set<string>();
28
+ for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) {
29
+ if (excludeIds.has(logicalId)) continue;
30
+ for (const ref of findResourceRefs(resource)) refs.add(ref);
31
+ }
32
+ for (const ref of findResourceRefs(template.Outputs)) refs.add(ref);
33
+ return refs;
34
+ }
35
+
36
+ export function checkPolicyUnattached(ctx: PostSynthContext): PostSynthDiagnostic[] {
37
+ const diagnostics: PostSynthDiagnostic[] = [];
38
+
39
+ for (const [_lexicon, output] of ctx.outputs) {
40
+ const template = parseCFTemplate(output);
41
+ if (!template?.Resources) continue;
42
+
43
+ const policyIds = new Set(
44
+ Object.entries(template.Resources)
45
+ .filter(([, resource]) => POLICY_TYPES.has(resource.Type))
46
+ .map(([logicalId]) => logicalId),
47
+ );
48
+ if (policyIds.size === 0) continue;
49
+
50
+ const externalRefs = collectExternalRefs(template, policyIds);
51
+
52
+ for (const logicalId of policyIds) {
53
+ const resource = template.Resources[logicalId];
54
+ const props = resource.Properties ?? {};
55
+
56
+ // Any declared principal list keeps the policy quiet. An intrinsic
57
+ // can't be statically evaluated, so it counts as attached.
58
+ let attached = false;
59
+ for (const prop of PRINCIPAL_PROPS) {
60
+ const value = props[prop];
61
+ if (isIntrinsic(value)) attached = true;
62
+ else if (Array.isArray(value) && value.length > 0) attached = true;
63
+ }
64
+ if (attached) continue;
65
+
66
+ // Reverse lookup: anything else in the template holding a Ref/GetAtt
67
+ // edge to this policy attaches (or exports) it.
68
+ if (externalRefs.has(logicalId)) continue;
69
+
70
+ const kind = resource.Type === "AWS::IAM::ManagedPolicy" ? "Managed policy" : "Policy";
71
+ diagnostics.push({
72
+ checkId: "WAW060",
73
+ severity: "warning",
74
+ message: `${kind} "${logicalId}" is attached to no principal — no Roles/Users/Groups and nothing in the template references it; an unattached policy grants nothing`,
75
+ entity: logicalId,
76
+ lexicon: "aws",
77
+ });
78
+ }
79
+ }
80
+
81
+ return diagnostics;
82
+ }
83
+
84
+ export const waw060: PostSynthCheck = {
85
+ id: "WAW060",
86
+ description: "IAM policy attached to no principal — it grants nothing",
87
+
88
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
89
+ return checkPolicyUnattached(ctx);
90
+ },
91
+ };
@@ -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