@intentius/chant-lexicon-aws 0.0.6 → 0.0.9

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 (128) hide show
  1. package/dist/integrity.json +25 -10
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +9444 -4597
  4. package/dist/rules/cf-refs.ts +99 -0
  5. package/dist/rules/ext001.ts +32 -25
  6. package/dist/rules/hardcoded-region.ts +1 -0
  7. package/dist/rules/iam-wildcard.ts +1 -0
  8. package/dist/rules/s3-encryption.ts +3 -3
  9. package/dist/rules/waw016.ts +86 -0
  10. package/dist/rules/waw017.ts +53 -0
  11. package/dist/rules/waw018.ts +71 -0
  12. package/dist/rules/waw019.ts +82 -0
  13. package/dist/rules/waw020.ts +64 -0
  14. package/dist/rules/waw021.ts +53 -0
  15. package/dist/rules/waw022.ts +43 -0
  16. package/dist/rules/waw023.ts +47 -0
  17. package/dist/rules/waw024.ts +54 -0
  18. package/dist/rules/waw025.ts +43 -0
  19. package/dist/rules/waw026.ts +46 -0
  20. package/dist/rules/waw027.ts +50 -0
  21. package/dist/rules/waw028.ts +47 -0
  22. package/dist/rules/waw029.ts +62 -0
  23. package/dist/rules/waw030.ts +246 -0
  24. package/dist/skills/chant-aws.md +430 -0
  25. package/dist/types/index.d.ts +58525 -58501
  26. package/package.json +2 -2
  27. package/src/actions/actions.test.ts +75 -0
  28. package/src/actions/dynamodb.ts +36 -0
  29. package/src/actions/ecr.ts +9 -0
  30. package/src/actions/ecs.ts +5 -0
  31. package/src/actions/iam.ts +3 -0
  32. package/src/actions/index.ts +9 -0
  33. package/src/actions/lambda.ts +11 -0
  34. package/src/actions/logs.ts +4 -0
  35. package/src/actions/s3.ts +34 -0
  36. package/src/actions/sns.ts +5 -0
  37. package/src/actions/sqs.ts +15 -0
  38. package/src/codegen/__snapshots__/snapshot.test.ts.snap +20 -20
  39. package/src/codegen/docs-links.test.ts +143 -0
  40. package/src/codegen/docs.ts +294 -124
  41. package/src/codegen/generate-lexicon.ts +8 -0
  42. package/src/codegen/generate-typescript.ts +25 -1
  43. package/src/codegen/generate.ts +1 -13
  44. package/src/codegen/package.ts +2 -0
  45. package/src/codegen/typecheck.test.ts +1 -1
  46. package/src/composites/composites.test.ts +442 -0
  47. package/src/composites/fargate-alb.ts +253 -0
  48. package/src/composites/index.ts +20 -0
  49. package/src/composites/lambda-api.ts +20 -0
  50. package/src/composites/lambda-dynamodb.ts +64 -0
  51. package/src/composites/lambda-eventbridge.ts +36 -0
  52. package/src/composites/lambda-function.ts +76 -0
  53. package/src/composites/lambda-s3.ts +72 -0
  54. package/src/composites/lambda-sns.ts +30 -0
  55. package/src/composites/lambda-sqs.ts +44 -0
  56. package/src/composites/scheduled-lambda.ts +37 -0
  57. package/src/composites/vpc-default.ts +148 -0
  58. package/src/default-tags.test.ts +38 -0
  59. package/src/default-tags.ts +77 -0
  60. package/src/generated/index.d.ts +58525 -58501
  61. package/src/generated/index.ts +1351 -1351
  62. package/src/generated/lexicon-aws.json +9444 -4597
  63. package/src/import/generator.test.ts +5 -5
  64. package/src/import/generator.ts +4 -4
  65. package/src/import/roundtrip-fixtures.test.ts +2 -1
  66. package/src/import/roundtrip.test.ts +5 -5
  67. package/src/index.ts +21 -0
  68. package/src/integration.test.ts +92 -21
  69. package/src/intrinsics.ts +24 -13
  70. package/src/lint/post-synth/cf-refs.ts +99 -0
  71. package/src/lint/post-synth/ext001.test.ts +214 -31
  72. package/src/lint/post-synth/ext001.ts +32 -25
  73. package/src/lint/post-synth/waw013.test.ts +120 -0
  74. package/src/lint/post-synth/waw014.test.ts +121 -0
  75. package/src/lint/post-synth/waw015.test.ts +147 -0
  76. package/src/lint/post-synth/waw016.test.ts +141 -0
  77. package/src/lint/post-synth/waw016.ts +86 -0
  78. package/src/lint/post-synth/waw017.test.ts +130 -0
  79. package/src/lint/post-synth/waw017.ts +53 -0
  80. package/src/lint/post-synth/waw018.test.ts +109 -0
  81. package/src/lint/post-synth/waw018.ts +71 -0
  82. package/src/lint/post-synth/waw019.test.ts +138 -0
  83. package/src/lint/post-synth/waw019.ts +82 -0
  84. package/src/lint/post-synth/waw020.test.ts +125 -0
  85. package/src/lint/post-synth/waw020.ts +64 -0
  86. package/src/lint/post-synth/waw021.test.ts +81 -0
  87. package/src/lint/post-synth/waw021.ts +53 -0
  88. package/src/lint/post-synth/waw022.test.ts +54 -0
  89. package/src/lint/post-synth/waw022.ts +43 -0
  90. package/src/lint/post-synth/waw023.test.ts +53 -0
  91. package/src/lint/post-synth/waw023.ts +47 -0
  92. package/src/lint/post-synth/waw024.test.ts +64 -0
  93. package/src/lint/post-synth/waw024.ts +54 -0
  94. package/src/lint/post-synth/waw025.test.ts +42 -0
  95. package/src/lint/post-synth/waw025.ts +43 -0
  96. package/src/lint/post-synth/waw026.test.ts +54 -0
  97. package/src/lint/post-synth/waw026.ts +46 -0
  98. package/src/lint/post-synth/waw027.test.ts +63 -0
  99. package/src/lint/post-synth/waw027.ts +50 -0
  100. package/src/lint/post-synth/waw028.test.ts +68 -0
  101. package/src/lint/post-synth/waw028.ts +47 -0
  102. package/src/lint/post-synth/waw029.test.ts +179 -0
  103. package/src/lint/post-synth/waw029.ts +62 -0
  104. package/src/lint/post-synth/waw030.test.ts +800 -0
  105. package/src/lint/post-synth/waw030.ts +246 -0
  106. package/src/lint/rules/hardcoded-region.ts +1 -0
  107. package/src/lint/rules/iam-wildcard.ts +1 -0
  108. package/src/lint/rules/rules.test.ts +8 -8
  109. package/src/lint/rules/s3-encryption.ts +3 -3
  110. package/src/lsp/completions.ts +2 -0
  111. package/src/lsp/hover.ts +17 -0
  112. package/src/nested-stack-integration.test.ts +100 -0
  113. package/src/nested-stack.ts +2 -2
  114. package/src/plugin.test.ts +13 -15
  115. package/src/plugin.ts +552 -114
  116. package/src/serializer.test.ts +370 -43
  117. package/src/serializer.ts +69 -17
  118. package/src/spec/fetch.ts +10 -0
  119. package/src/spec/parse.test.ts +141 -0
  120. package/src/spec/parse.ts +40 -0
  121. package/src/taggable.ts +44 -0
  122. package/src/testdata/nested-stacks/app.ts +26 -0
  123. package/src/testdata/nested-stacks/network/outputs.ts +17 -0
  124. package/src/testdata/nested-stacks/network/security.ts +17 -0
  125. package/src/testdata/nested-stacks/network/vpc.ts +54 -0
  126. package/dist/skills/aws-cloudformation.md +0 -41
  127. package/src/codegen/rollback.test.ts +0 -80
  128. package/src/codegen/rollback.ts +0 -20
@@ -35,7 +35,7 @@ describe("CFGenerator", () => {
35
35
 
36
36
  expect(files[0].content).toContain("import { Bucket }");
37
37
  expect(files[0].content).toContain("export const MyBucket = new Bucket({");
38
- expect(files[0].content).toContain('bucketName: "my-bucket"');
38
+ expect(files[0].content).toContain('BucketName: "my-bucket"');
39
39
  });
40
40
 
41
41
  test("generates Lambda Function", () => {
@@ -58,8 +58,8 @@ describe("CFGenerator", () => {
58
58
 
59
59
  expect(files[0].content).toContain("import { Function }");
60
60
  expect(files[0].content).toContain("export const MyFunction = new Function({");
61
- expect(files[0].content).toContain('functionName: "my-function"');
62
- expect(files[0].content).toContain('runtime: "nodejs18.x"');
61
+ expect(files[0].content).toContain('FunctionName: "my-function"');
62
+ expect(files[0].content).toContain('Runtime: "nodejs18.x"');
63
63
  });
64
64
 
65
65
  test("generates Ref as variable reference", () => {
@@ -78,7 +78,7 @@ describe("CFGenerator", () => {
78
78
 
79
79
  const files = generator.generate(ir);
80
80
 
81
- expect(files[0].content).toContain("bucketName: Ref(BucketName)");
81
+ expect(files[0].content).toContain("BucketName: Ref(BucketName)");
82
82
  });
83
83
 
84
84
  test("generates GetAtt as property access", () => {
@@ -102,7 +102,7 @@ describe("CFGenerator", () => {
102
102
 
103
103
  const files = generator.generate(ir);
104
104
 
105
- expect(files[0].content).toContain("sourceArn: SourceBucket.arn");
105
+ expect(files[0].content).toContain("SourceArn: SourceBucket.Arn");
106
106
  });
107
107
 
108
108
  test("generates Sub as tagged template", () => {
@@ -1,4 +1,6 @@
1
+ import { createRequire } from "module";
1
2
  import type { TemplateIR, ResourceIR, ParameterIR } from "@intentius/chant/import/parser";
3
+ const require = createRequire(import.meta.url);
2
4
  import type { TypeScriptGenerator, GeneratedFile } from "@intentius/chant/import/generator";
3
5
  import { topoSort } from "@intentius/chant/codegen/topo-sort";
4
6
  import { hasIntrinsicInValue, irUsesIntrinsic, collectDependencies } from "@intentius/chant/import/ir-utils";
@@ -455,11 +457,9 @@ export class CFGenerator implements TypeScriptGenerator {
455
457
  }
456
458
 
457
459
  /**
458
- * Convert a property name to camelCase (lowercase first char).
459
- * Used for resource property access (e.g., GetAtt attribute names)
460
- * which matches chant's camelCase property convention.
460
+ * Property names use spec-native casing (PascalCase for CloudFormation).
461
461
  */
462
462
  private toPropName(name: string): string {
463
- return name.charAt(0).toLowerCase() + name.slice(1);
463
+ return name;
464
464
  }
465
465
  }
@@ -5,7 +5,8 @@ import { CFParser } from "./parser";
5
5
  import { CFGenerator } from "./generator";
6
6
  import { build } from "@intentius/chant/build";
7
7
  import { awsSerializer } from "../serializer";
8
- import * as awsLexicon from "../index";
8
+ // Dynamic import to get all export keys for validation (not a runtime re-export)
9
+ const awsLexicon = await import("../index");
9
10
 
10
11
  const parser = new CFParser();
11
12
  const generator = new CFGenerator();
@@ -24,7 +24,7 @@ describe("CloudFormation round-trip", () => {
24
24
  const files = generator.generate(ir);
25
25
 
26
26
  expect(files[0].content).toContain("Bucket");
27
- expect(files[0].content).toContain('bucketName: "my-bucket"');
27
+ expect(files[0].content).toContain('BucketName: "my-bucket"');
28
28
  expect(files[0].content).toContain("export const MyBucket");
29
29
  });
30
30
 
@@ -53,7 +53,7 @@ describe("CloudFormation round-trip", () => {
53
53
 
54
54
  expect(files[0].content).toContain("Parameter");
55
55
  expect(files[0].content).toContain("Environment");
56
- expect(files[0].content).toContain("bucketName: Ref(Environment)");
56
+ expect(files[0].content).toContain("BucketName: Ref(Environment)");
57
57
  });
58
58
 
59
59
  test("round-trips template with Fn::Sub", () => {
@@ -115,7 +115,7 @@ describe("CloudFormation round-trip", () => {
115
115
 
116
116
  expect(files[0].content).toContain("Role");
117
117
  expect(files[0].content).toContain("Function");
118
- expect(files[0].content).toContain("LambdaRole.arn");
118
+ expect(files[0].content).toContain("LambdaRole.Arn");
119
119
  });
120
120
 
121
121
  test("round-trips complex nested properties", () => {
@@ -148,8 +148,8 @@ describe("CloudFormation round-trip", () => {
148
148
  const files = generator.generate(ir);
149
149
 
150
150
  expect(files[0].content).toContain("Function");
151
- expect(files[0].content).toContain("environment:");
152
- expect(files[0].content).toContain("vpcConfig:");
151
+ expect(files[0].content).toContain("Environment:");
152
+ expect(files[0].content).toContain("VpcConfig:");
153
153
  });
154
154
 
155
155
  test("round-trips empty template", () => {
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  // Parameter
2
2
  export { Parameter } from "./parameter";
3
3
 
4
+ // Default Tags
5
+ export { defaultTags, isDefaultTags, DEFAULT_TAGS_MARKER } from "./default-tags";
6
+ export type { DefaultTags, TagEntry } from "./default-tags";
7
+
4
8
  // Serializer
5
9
  export { awsSerializer } from "./serializer";
6
10
 
@@ -64,6 +68,23 @@ export type { CFNSchema, SchemaProperty, SchemaDefinition } from "./spec/fetch";
64
68
  export { parseCFNSchema, cfnShortName, cfnServiceName } from "./spec/parse";
65
69
  export type { SchemaParseResult, ParsedResource, ParsedProperty, ParsedAttribute, ParsedPropertyType, ParsedEnum, PropertyConstraints } from "./spec/parse";
66
70
 
71
+ // Action constants
72
+ export { S3Actions, LambdaActions, DynamoDBActions, SQSActions, SNSActions, IAMActions, ECRActions, LogsActions, ECSActions } from "./actions/index";
73
+
74
+ // Built-in composites
75
+ export {
76
+ LambdaFunction, LambdaNode, LambdaPython, NodeLambda, PythonLambda,
77
+ LambdaApi,
78
+ LambdaScheduled, ScheduledLambda,
79
+ LambdaSqs, LambdaEventBridge, LambdaDynamoDB, LambdaS3, LambdaSns,
80
+ VpcDefault, FargateAlb,
81
+ } from "./composites/index";
82
+ export type {
83
+ LambdaFunctionProps, LambdaApiProps, ScheduledLambdaProps,
84
+ LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, LambdaS3Props, LambdaSnsProps,
85
+ VpcDefaultProps, FargateAlbProps,
86
+ } from "./composites/index";
87
+
67
88
  // Code generation pipeline
68
89
  export { generate, writeGeneratedFiles } from "./codegen/generate";
69
90
  export { packageLexicon } from "./codegen/package";
@@ -17,7 +17,7 @@ describe("AWS Integration", () => {
17
17
 
18
18
  test("serializes S3 bucket with properties", () => {
19
19
  const bucket = new (Bucket as any)({
20
- bucketName: "my-bucket",
20
+ BucketName: "my-bucket",
21
21
  });
22
22
 
23
23
  const entities = new Map<string, Declarable>();
@@ -61,12 +61,12 @@ describe("AWS Integration", () => {
61
61
 
62
62
  describe("Cross-resource references", () => {
63
63
  test("GetAtt for bucket ARN", () => {
64
- const bucket = new (Bucket as any)({ bucketName: "source" });
64
+ const bucket = new (Bucket as any)({ BucketName: "source" });
65
65
  // Set logical name for the AttrRef
66
- (bucket.arn as Record<string, unknown>)._setLogicalName("SourceBucket");
66
+ (bucket.Arn as Record<string, unknown>)._setLogicalName("SourceBucket");
67
67
 
68
- expect(bucket.arn.getLogicalName()).toBe("SourceBucket");
69
- expect(bucket.arn.attribute).toBe("Arn");
68
+ expect(bucket.Arn.getLogicalName()).toBe("SourceBucket");
69
+ expect(bucket.Arn.attribute).toBe("Arn");
70
70
  });
71
71
  });
72
72
 
@@ -79,10 +79,10 @@ describe("AWS Integration", () => {
79
79
 
80
80
  test("Lambda Function has correct entity type", () => {
81
81
  const fn = new (Function as any)({
82
- runtime: "nodejs18.x",
83
- handler: "index.handler",
84
- code: { s3Bucket: "my-bucket", s3Key: "code.zip" },
85
- role: "arn:aws:iam::123456789012:role/lambda-role",
82
+ Runtime: "nodejs18.x",
83
+ Handler: "index.handler",
84
+ Code: { S3Bucket: "my-bucket", S3Key: "code.zip" },
85
+ Role: "arn:aws:iam::123456789012:role/lambda-role",
86
86
  });
87
87
  expect(fn.entityType).toBe("AWS::Lambda::Function");
88
88
  expect(fn[DECLARABLE_MARKER]).toBe(true);
@@ -90,7 +90,7 @@ describe("AWS Integration", () => {
90
90
 
91
91
  test("IAM Role has correct entity type", () => {
92
92
  const role = new (Role as any)({
93
- assumeRolePolicyDocument: {
93
+ AssumeRolePolicyDocument: {
94
94
  Version: "2012-10-17",
95
95
  Statement: [],
96
96
  },
@@ -100,30 +100,101 @@ describe("AWS Integration", () => {
100
100
  });
101
101
  });
102
102
 
103
+ describe("Resource-level attributes (second constructor arg)", () => {
104
+ test("DependsOn with Declarable reference in real generated class", () => {
105
+ const bucket = new (Bucket as any)({ BucketName: "data" });
106
+ const fn = new (Function as any)(
107
+ {
108
+ Runtime: "nodejs20.x",
109
+ Handler: "index.handler",
110
+ Code: { S3Bucket: "my-bucket", S3Key: "code.zip" },
111
+ Role: "arn:aws:iam::123456789012:role/role",
112
+ },
113
+ { DependsOn: [bucket] },
114
+ );
115
+
116
+ expect(fn.attributes).toBeDefined();
117
+ expect(fn.attributes.DependsOn).toEqual([bucket]);
118
+
119
+ const entities = new Map<string, Declarable>();
120
+ entities.set("DataBucket", bucket);
121
+ entities.set("Handler", fn);
122
+
123
+ const output = awsSerializer.serialize(entities);
124
+ const template = JSON.parse(output);
125
+ expect(template.Resources.Handler.DependsOn).toBe("DataBucket");
126
+ });
127
+
128
+ test("DeletionPolicy and Condition on real generated class", () => {
129
+ const bucket = new (Bucket as any)(
130
+ { BucketName: "important-data" },
131
+ { DeletionPolicy: "Retain", Condition: "CreateBucket" },
132
+ );
133
+
134
+ const entities = new Map<string, Declarable>();
135
+ entities.set("DataBucket", bucket);
136
+
137
+ const output = awsSerializer.serialize(entities);
138
+ const template = JSON.parse(output);
139
+
140
+ expect(template.Resources.DataBucket.DeletionPolicy).toBe("Retain");
141
+ expect(template.Resources.DataBucket.Condition).toBe("CreateBucket");
142
+ });
143
+
144
+ test("resource without attributes works unchanged", () => {
145
+ const bucket = new (Bucket as any)({ BucketName: "simple" });
146
+ expect(bucket.attributes).toEqual({});
147
+
148
+ const entities = new Map<string, Declarable>();
149
+ entities.set("SimpleBucket", bucket);
150
+
151
+ const output = awsSerializer.serialize(entities);
152
+ const template = JSON.parse(output);
153
+ expect(template.Resources.SimpleBucket.DeletionPolicy).toBeUndefined();
154
+ expect(template.Resources.SimpleBucket.Condition).toBeUndefined();
155
+ });
156
+
157
+ test("Metadata with intrinsics resolves correctly", () => {
158
+ const bucket = new (Bucket as any)(
159
+ { BucketName: "meta" },
160
+ { Metadata: { DeployedWith: Sub`${AWS.StackName}-chant` } },
161
+ );
162
+
163
+ const entities = new Map<string, Declarable>();
164
+ entities.set("MetaBucket", bucket);
165
+
166
+ const output = awsSerializer.serialize(entities);
167
+ const template = JSON.parse(output);
168
+ expect(template.Resources.MetaBucket.Metadata.DeployedWith).toEqual({
169
+ "Fn::Sub": "${AWS::StackName}-chant",
170
+ });
171
+ });
172
+ });
173
+
103
174
  describe("AttrRefs", () => {
104
175
  test("Bucket has expected AttrRefs", () => {
105
176
  const bucket = new (Bucket as any)({});
106
- expect(bucket.arn).toBeDefined();
107
- expect(bucket.domainName).toBeDefined();
108
- expect(bucket.websiteURL).toBeDefined();
177
+ expect(bucket.Arn).toBeDefined();
178
+ expect(bucket.DomainName).toBeDefined();
179
+ expect(bucket.WebsiteURL).toBeDefined();
109
180
  });
110
181
 
111
182
  test("Lambda Function has expected AttrRefs", () => {
112
183
  const fn = new (Function as any)({
113
- runtime: "nodejs18.x",
114
- handler: "index.handler",
115
- code: { s3Bucket: "bucket", s3Key: "key" },
116
- role: "role-arn",
184
+ Runtime: "nodejs18.x",
185
+ Handler: "index.handler",
186
+ Code: { S3Bucket: "bucket", S3Key: "key" },
187
+ Role: "role-arn",
117
188
  });
118
- expect(fn.arn).toBeDefined();
189
+ expect(fn.Arn).toBeDefined();
119
190
  });
120
191
 
121
192
  test("IAM Role has expected AttrRefs", () => {
122
193
  const role = new (Role as any)({
123
- assumeRolePolicyDocument: {},
194
+ AssumeRolePolicyDocument: {},
124
195
  });
125
- expect(role.arn).toBeDefined();
126
- expect(role.roleId).toBeDefined();
196
+ expect(role.Arn).toBeDefined();
197
+ expect(role.RoleId).toBeDefined();
127
198
  });
128
199
  });
129
200
  });
package/src/intrinsics.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { INTRINSIC_MARKER, resolveIntrinsicValue, type Intrinsic } from "@intentius/chant/intrinsic";
2
2
  import { buildInterpolatedString, defaultInterpolationSerializer } from "@intentius/chant/intrinsic-interpolation";
3
+ import { type Declarable } from "@intentius/chant/declarable";
4
+ import { getLogicalName } from "@intentius/chant/utils";
3
5
 
4
6
  /**
5
7
  * Fn::Sub intrinsic function implementation
@@ -37,26 +39,32 @@ export function Sub(
37
39
 
38
40
  /**
39
41
  * Ref intrinsic function
40
- * References a parameter or resource by logical name
42
+ * References a parameter or resource by logical name.
43
+ * Accepts either a string name or a Declarable entity (e.g. Parameter).
44
+ * When given a Declarable, the logical name is resolved at serialization time.
41
45
  */
42
46
  export class RefIntrinsic implements Intrinsic {
43
47
  readonly [INTRINSIC_MARKER] = true as const;
44
- private name: string;
48
+ private target: string | Declarable;
45
49
 
46
- constructor(name: string) {
47
- this.name = name;
50
+ constructor(target: string | Declarable) {
51
+ this.target = target;
48
52
  }
49
53
 
50
54
  toJSON(): { Ref: string } {
51
- return { Ref: this.name };
55
+ if (typeof this.target === "string") {
56
+ return { Ref: this.target };
57
+ }
58
+ return { Ref: getLogicalName(this.target) };
52
59
  }
53
60
  }
54
61
 
55
62
  /**
56
- * Create a Ref intrinsic
63
+ * Create a Ref intrinsic.
64
+ * Pass a string for direct parameter/resource names, or a Declarable (e.g. Parameter) for type-safe references.
57
65
  */
58
- export function Ref(name: string): RefIntrinsic {
59
- return new RefIntrinsic(name);
66
+ export function Ref(target: string | Declarable): RefIntrinsic {
67
+ return new RefIntrinsic(target);
60
68
  }
61
69
 
62
70
  /**
@@ -147,22 +155,25 @@ export function Join(delimiter: string, values: unknown[]): JoinIntrinsic {
147
155
  export class SelectIntrinsic implements Intrinsic {
148
156
  readonly [INTRINSIC_MARKER] = true as const;
149
157
  private index: number;
150
- private values: unknown[];
158
+ private values: unknown[] | Intrinsic;
151
159
 
152
- constructor(index: number, values: unknown[]) {
160
+ constructor(index: number, values: unknown[] | Intrinsic) {
153
161
  this.index = index;
154
162
  this.values = values;
155
163
  }
156
164
 
157
- toJSON(): { "Fn::Select": [string, unknown[]] } {
158
- return { "Fn::Select": [String(this.index), this.values.map(resolveIntrinsicValue)] };
165
+ toJSON(): { "Fn::Select": [string, unknown] } {
166
+ const resolvedValues = Array.isArray(this.values)
167
+ ? this.values.map(resolveIntrinsicValue)
168
+ : (this.values as Intrinsic & { toJSON(): unknown }).toJSON();
169
+ return { "Fn::Select": [String(this.index), resolvedValues] };
159
170
  }
160
171
  }
161
172
 
162
173
  /**
163
174
  * Create a Select intrinsic
164
175
  */
165
- export function Select(index: number, values: unknown[]): SelectIntrinsic {
176
+ export function Select(index: number, values: unknown[] | Intrinsic): SelectIntrinsic {
166
177
  return new SelectIntrinsic(index, values);
167
178
  }
168
179
 
@@ -50,6 +50,105 @@ export function findResourceRefs(value: unknown): Set<string> {
50
50
  return refs;
51
51
  }
52
52
 
53
+ /**
54
+ * Check if a value is a CloudFormation intrinsic function (Ref, Fn::*, etc.)
55
+ * that cannot be statically evaluated.
56
+ */
57
+ export function isIntrinsic(value: unknown): boolean {
58
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
59
+ const obj = value as Record<string, unknown>;
60
+ return "Ref" in obj || Object.keys(obj).some((k) => k.startsWith("Fn::"));
61
+ }
62
+
63
+ /**
64
+ * Walk IAM policy statements from a resource's properties.
65
+ * Handles IAM::Policy, IAM::Role, and IAM::ManagedPolicy layouts.
66
+ */
67
+ export function walkPolicyStatements(
68
+ resource: CFResource,
69
+ ): Array<Record<string, unknown>> {
70
+ const statements: Array<Record<string, unknown>> = [];
71
+ const props = resource.Properties ?? {};
72
+
73
+ // PolicyDocument.Statement (IAM::Policy, IAM::ManagedPolicy)
74
+ collectStatements(props.PolicyDocument, statements);
75
+
76
+ // AssumeRolePolicyDocument.Statement (IAM::Role)
77
+ collectStatements(props.AssumeRolePolicyDocument, statements);
78
+
79
+ // Policies[].PolicyDocument.Statement (IAM::Role inline policies)
80
+ if (Array.isArray(props.Policies)) {
81
+ for (const policy of props.Policies) {
82
+ if (typeof policy === "object" && policy !== null) {
83
+ collectStatements((policy as Record<string, unknown>).PolicyDocument, statements);
84
+ }
85
+ }
86
+ }
87
+
88
+ return statements;
89
+ }
90
+
91
+ function collectStatements(
92
+ policyDoc: unknown,
93
+ out: Array<Record<string, unknown>>,
94
+ ): void {
95
+ if (typeof policyDoc !== "object" || policyDoc === null) return;
96
+ const doc = policyDoc as Record<string, unknown>;
97
+ if (Array.isArray(doc.Statement)) {
98
+ for (const stmt of doc.Statement) {
99
+ if (typeof stmt === "object" && stmt !== null) {
100
+ out.push(stmt as Record<string, unknown>);
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Normalize security group ingress rules from inline SecurityGroupIngress
108
+ * property and standalone SecurityGroupIngress resources.
109
+ */
110
+ export function getSecurityGroupIngress(
111
+ resource: CFResource,
112
+ ): Array<Record<string, unknown>> {
113
+ const rules: Array<Record<string, unknown>> = [];
114
+ const props = resource.Properties ?? {};
115
+
116
+ if (Array.isArray(props.SecurityGroupIngress)) {
117
+ for (const rule of props.SecurityGroupIngress) {
118
+ if (typeof rule === "object" && rule !== null) {
119
+ rules.push(rule as Record<string, unknown>);
120
+ }
121
+ }
122
+ }
123
+
124
+ return rules;
125
+ }
126
+
127
+ /**
128
+ * Check if a port range [fromPort, toPort] contains any of the sensitive ports.
129
+ */
130
+ export function portRangeContainsSensitive(
131
+ fromPort: unknown,
132
+ toPort: unknown,
133
+ sensitivePorts: number[],
134
+ ): boolean {
135
+ // Missing ports means all ports
136
+ if (fromPort === undefined && toPort === undefined) return true;
137
+
138
+ const from = typeof fromPort === "number" ? fromPort : -1;
139
+ const to = typeof toPort === "number" ? toPort : -1;
140
+
141
+ // If either is an intrinsic, we can't statically verify
142
+ if (isIntrinsic(fromPort) || isIntrinsic(toPort)) return false;
143
+
144
+ if (from === -1 && to === -1) return true;
145
+
146
+ for (const port of sensitivePorts) {
147
+ if (from <= port && port <= to) return true;
148
+ }
149
+ return false;
150
+ }
151
+
53
152
  function walkValue(value: unknown, refs: Set<string>): void {
54
153
  if (value === null || value === undefined) return;
55
154
  if (typeof value !== "object") return;