@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.
- package/dist/integrity.json +25 -10
- package/dist/manifest.json +1 -1
- package/dist/meta.json +9444 -4597
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +32 -25
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +3 -3
- package/dist/rules/waw016.ts +86 -0
- package/dist/rules/waw017.ts +53 -0
- package/dist/rules/waw018.ts +71 -0
- package/dist/rules/waw019.ts +82 -0
- package/dist/rules/waw020.ts +64 -0
- package/dist/rules/waw021.ts +53 -0
- package/dist/rules/waw022.ts +43 -0
- package/dist/rules/waw023.ts +47 -0
- package/dist/rules/waw024.ts +54 -0
- package/dist/rules/waw025.ts +43 -0
- package/dist/rules/waw026.ts +46 -0
- package/dist/rules/waw027.ts +50 -0
- package/dist/rules/waw028.ts +47 -0
- package/dist/rules/waw029.ts +62 -0
- package/dist/rules/waw030.ts +246 -0
- package/dist/skills/chant-aws.md +430 -0
- package/dist/types/index.d.ts +58525 -58501
- package/package.json +2 -2
- package/src/actions/actions.test.ts +75 -0
- package/src/actions/dynamodb.ts +36 -0
- package/src/actions/ecr.ts +9 -0
- package/src/actions/ecs.ts +5 -0
- package/src/actions/iam.ts +3 -0
- package/src/actions/index.ts +9 -0
- package/src/actions/lambda.ts +11 -0
- package/src/actions/logs.ts +4 -0
- package/src/actions/s3.ts +34 -0
- package/src/actions/sns.ts +5 -0
- package/src/actions/sqs.ts +15 -0
- package/src/codegen/__snapshots__/snapshot.test.ts.snap +20 -20
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +294 -124
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -1
- package/src/codegen/generate.ts +1 -13
- package/src/codegen/package.ts +2 -0
- package/src/codegen/typecheck.test.ts +1 -1
- package/src/composites/composites.test.ts +442 -0
- package/src/composites/fargate-alb.ts +253 -0
- package/src/composites/index.ts +20 -0
- package/src/composites/lambda-api.ts +20 -0
- package/src/composites/lambda-dynamodb.ts +64 -0
- package/src/composites/lambda-eventbridge.ts +36 -0
- package/src/composites/lambda-function.ts +76 -0
- package/src/composites/lambda-s3.ts +72 -0
- package/src/composites/lambda-sns.ts +30 -0
- package/src/composites/lambda-sqs.ts +44 -0
- package/src/composites/scheduled-lambda.ts +37 -0
- package/src/composites/vpc-default.ts +148 -0
- package/src/default-tags.test.ts +38 -0
- package/src/default-tags.ts +77 -0
- package/src/generated/index.d.ts +58525 -58501
- package/src/generated/index.ts +1351 -1351
- package/src/generated/lexicon-aws.json +9444 -4597
- package/src/import/generator.test.ts +5 -5
- package/src/import/generator.ts +4 -4
- package/src/import/roundtrip-fixtures.test.ts +2 -1
- package/src/import/roundtrip.test.ts +5 -5
- package/src/index.ts +21 -0
- package/src/integration.test.ts +92 -21
- package/src/intrinsics.ts +24 -13
- package/src/lint/post-synth/cf-refs.ts +99 -0
- package/src/lint/post-synth/ext001.test.ts +214 -31
- package/src/lint/post-synth/ext001.ts +32 -25
- package/src/lint/post-synth/waw013.test.ts +120 -0
- package/src/lint/post-synth/waw014.test.ts +121 -0
- package/src/lint/post-synth/waw015.test.ts +147 -0
- package/src/lint/post-synth/waw016.test.ts +141 -0
- package/src/lint/post-synth/waw016.ts +86 -0
- package/src/lint/post-synth/waw017.test.ts +130 -0
- package/src/lint/post-synth/waw017.ts +53 -0
- package/src/lint/post-synth/waw018.test.ts +109 -0
- package/src/lint/post-synth/waw018.ts +71 -0
- package/src/lint/post-synth/waw019.test.ts +138 -0
- package/src/lint/post-synth/waw019.ts +82 -0
- package/src/lint/post-synth/waw020.test.ts +125 -0
- package/src/lint/post-synth/waw020.ts +64 -0
- package/src/lint/post-synth/waw021.test.ts +81 -0
- package/src/lint/post-synth/waw021.ts +53 -0
- package/src/lint/post-synth/waw022.test.ts +54 -0
- package/src/lint/post-synth/waw022.ts +43 -0
- package/src/lint/post-synth/waw023.test.ts +53 -0
- package/src/lint/post-synth/waw023.ts +47 -0
- package/src/lint/post-synth/waw024.test.ts +64 -0
- package/src/lint/post-synth/waw024.ts +54 -0
- package/src/lint/post-synth/waw025.test.ts +42 -0
- package/src/lint/post-synth/waw025.ts +43 -0
- package/src/lint/post-synth/waw026.test.ts +54 -0
- package/src/lint/post-synth/waw026.ts +46 -0
- package/src/lint/post-synth/waw027.test.ts +63 -0
- package/src/lint/post-synth/waw027.ts +50 -0
- package/src/lint/post-synth/waw028.test.ts +68 -0
- package/src/lint/post-synth/waw028.ts +47 -0
- package/src/lint/post-synth/waw029.test.ts +179 -0
- package/src/lint/post-synth/waw029.ts +62 -0
- package/src/lint/post-synth/waw030.test.ts +800 -0
- package/src/lint/post-synth/waw030.ts +246 -0
- package/src/lint/rules/hardcoded-region.ts +1 -0
- package/src/lint/rules/iam-wildcard.ts +1 -0
- package/src/lint/rules/rules.test.ts +8 -8
- package/src/lint/rules/s3-encryption.ts +3 -3
- package/src/lsp/completions.ts +2 -0
- package/src/lsp/hover.ts +17 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +2 -2
- package/src/plugin.test.ts +13 -15
- package/src/plugin.ts +552 -114
- package/src/serializer.test.ts +370 -43
- package/src/serializer.ts +69 -17
- package/src/spec/fetch.ts +10 -0
- package/src/spec/parse.test.ts +141 -0
- package/src/spec/parse.ts +40 -0
- package/src/taggable.ts +44 -0
- package/src/testdata/nested-stacks/app.ts +26 -0
- package/src/testdata/nested-stacks/network/outputs.ts +17 -0
- package/src/testdata/nested-stacks/network/security.ts +17 -0
- package/src/testdata/nested-stacks/network/vpc.ts +54 -0
- package/dist/skills/aws-cloudformation.md +0 -41
- package/src/codegen/rollback.test.ts +0 -80
- 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('
|
|
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('
|
|
62
|
-
expect(files[0].content).toContain('
|
|
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("
|
|
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("
|
|
105
|
+
expect(files[0].content).toContain("SourceArn: SourceBucket.Arn");
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test("generates Sub as tagged template", () => {
|
package/src/import/generator.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
|
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('
|
|
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("
|
|
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.
|
|
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("
|
|
152
|
-
expect(files[0].content).toContain("
|
|
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";
|
package/src/integration.test.ts
CHANGED
|
@@ -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
|
-
|
|
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)({
|
|
64
|
+
const bucket = new (Bucket as any)({ BucketName: "source" });
|
|
65
65
|
// Set logical name for the AttrRef
|
|
66
|
-
(bucket.
|
|
66
|
+
(bucket.Arn as Record<string, unknown>)._setLogicalName("SourceBucket");
|
|
67
67
|
|
|
68
|
-
expect(bucket.
|
|
69
|
-
expect(bucket.
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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.
|
|
107
|
-
expect(bucket.
|
|
108
|
-
expect(bucket.
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
184
|
+
Runtime: "nodejs18.x",
|
|
185
|
+
Handler: "index.handler",
|
|
186
|
+
Code: { S3Bucket: "bucket", S3Key: "key" },
|
|
187
|
+
Role: "role-arn",
|
|
117
188
|
});
|
|
118
|
-
expect(fn.
|
|
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
|
-
|
|
194
|
+
AssumeRolePolicyDocument: {},
|
|
124
195
|
});
|
|
125
|
-
expect(role.
|
|
126
|
-
expect(role.
|
|
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
|
|
48
|
+
private target: string | Declarable;
|
|
45
49
|
|
|
46
|
-
constructor(
|
|
47
|
-
this.
|
|
50
|
+
constructor(target: string | Declarable) {
|
|
51
|
+
this.target = target;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
toJSON(): { Ref: string } {
|
|
51
|
-
|
|
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(
|
|
59
|
-
return new RefIntrinsic(
|
|
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
|
-
|
|
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;
|