@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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW022: Lambda Not in VPC
|
|
3
|
+
*
|
|
4
|
+
* Flags Lambda functions without VpcConfig.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkLambdaVpc(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::Lambda::Function") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
if (!("VpcConfig" in props)) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
checkId: "WAW022",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `Lambda function "${logicalId}" is not configured with a VPC — consider adding VpcConfig for network isolation`,
|
|
26
|
+
entity: logicalId,
|
|
27
|
+
lexicon: "aws",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return diagnostics;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const waw022: PostSynthCheck = {
|
|
37
|
+
id: "WAW022",
|
|
38
|
+
description: "Lambda function is not configured with a VPC — consider adding VpcConfig for network isolation",
|
|
39
|
+
|
|
40
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
41
|
+
return checkLambdaVpc(ctx);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw023, checkCloudFrontWaf } from "./waw023";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW023: CloudFront Without WAF", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw023.id).toBe("WAW023");
|
|
12
|
+
expect(waw023.description).toContain("WAF");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags distribution without WebACLId", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyCF: {
|
|
19
|
+
Type: "AWS::CloudFront::Distribution",
|
|
20
|
+
Properties: {
|
|
21
|
+
DistributionConfig: {
|
|
22
|
+
Origins: [{ Id: "origin1", DomainName: "example.com" }],
|
|
23
|
+
DefaultCacheBehavior: { TargetOriginId: "origin1", ViewerProtocolPolicy: "redirect-to-https" },
|
|
24
|
+
Enabled: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const diags = checkCloudFrontWaf(ctx);
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("WAW023");
|
|
33
|
+
expect(diags[0].severity).toBe("warning");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("no diagnostic when WebACLId is present", () => {
|
|
37
|
+
const ctx = makeCtx({
|
|
38
|
+
Resources: {
|
|
39
|
+
MyCF: {
|
|
40
|
+
Type: "AWS::CloudFront::Distribution",
|
|
41
|
+
Properties: {
|
|
42
|
+
DistributionConfig: {
|
|
43
|
+
WebACLId: "arn:aws:wafv2:us-east-1:123456789012:global/webacl/my-acl/abc",
|
|
44
|
+
Enabled: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const diags = checkCloudFrontWaf(ctx);
|
|
51
|
+
expect(diags).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW023: CloudFront Without WAF
|
|
3
|
+
*
|
|
4
|
+
* Flags CloudFront distributions without WebACLId.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkCloudFrontWaf(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::CloudFront::Distribution") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
const distConfig = props.DistributionConfig;
|
|
22
|
+
if (typeof distConfig !== "object" || distConfig === null) continue;
|
|
23
|
+
|
|
24
|
+
const config = distConfig as Record<string, unknown>;
|
|
25
|
+
if (!("WebACLId" in config)) {
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
checkId: "WAW023",
|
|
28
|
+
severity: "warning",
|
|
29
|
+
message: `CloudFront distribution "${logicalId}" has no WebACLId — consider attaching a WAF web ACL for protection`,
|
|
30
|
+
entity: logicalId,
|
|
31
|
+
lexicon: "aws",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return diagnostics;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const waw023: PostSynthCheck = {
|
|
41
|
+
id: "WAW023",
|
|
42
|
+
description: "CloudFront distribution has no WAF web ACL — consider attaching one for protection",
|
|
43
|
+
|
|
44
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
45
|
+
return checkCloudFrontWaf(ctx);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw024, checkAlbAccessLogs } from "./waw024";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW024: ALB Without Access Logging", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw024.id).toBe("WAW024");
|
|
12
|
+
expect(waw024.description).toContain("access logging");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags ALB without access logging", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyALB: {
|
|
19
|
+
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
20
|
+
Properties: { Type: "application" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkAlbAccessLogs(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW024");
|
|
27
|
+
expect(diags[0].severity).toBe("warning");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("no diagnostic when access logging is enabled", () => {
|
|
31
|
+
const ctx = makeCtx({
|
|
32
|
+
Resources: {
|
|
33
|
+
MyALB: {
|
|
34
|
+
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
35
|
+
Properties: {
|
|
36
|
+
LoadBalancerAttributes: [
|
|
37
|
+
{ Key: "access_logs.s3.enabled", Value: "true" },
|
|
38
|
+
{ Key: "access_logs.s3.bucket", Value: "my-logs-bucket" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const diags = checkAlbAccessLogs(ctx);
|
|
45
|
+
expect(diags).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("flags when access_logs.s3.enabled is false", () => {
|
|
49
|
+
const ctx = makeCtx({
|
|
50
|
+
Resources: {
|
|
51
|
+
MyALB: {
|
|
52
|
+
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer",
|
|
53
|
+
Properties: {
|
|
54
|
+
LoadBalancerAttributes: [
|
|
55
|
+
{ Key: "access_logs.s3.enabled", Value: "false" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const diags = checkAlbAccessLogs(ctx);
|
|
62
|
+
expect(diags).toHaveLength(1);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW024: ALB Without Access Logging
|
|
3
|
+
*
|
|
4
|
+
* Flags Application Load Balancers without access logging enabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkAlbAccessLogs(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::ElasticLoadBalancingV2::LoadBalancer") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
const attrs = props.LoadBalancerAttributes;
|
|
22
|
+
|
|
23
|
+
let hasAccessLogs = false;
|
|
24
|
+
if (Array.isArray(attrs)) {
|
|
25
|
+
hasAccessLogs = attrs.some((attr) => {
|
|
26
|
+
if (typeof attr !== "object" || attr === null) return false;
|
|
27
|
+
const a = attr as Record<string, unknown>;
|
|
28
|
+
return a.Key === "access_logs.s3.enabled" && (a.Value === "true" || a.Value === true);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!hasAccessLogs) {
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
checkId: "WAW024",
|
|
35
|
+
severity: "warning",
|
|
36
|
+
message: `Load balancer "${logicalId}" does not have access logging enabled — enable access_logs.s3.enabled for audit trails`,
|
|
37
|
+
entity: logicalId,
|
|
38
|
+
lexicon: "aws",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return diagnostics;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const waw024: PostSynthCheck = {
|
|
48
|
+
id: "WAW024",
|
|
49
|
+
description: "Application Load Balancer does not have access logging enabled",
|
|
50
|
+
|
|
51
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
52
|
+
return checkAlbAccessLogs(ctx);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw025, checkSnsEncryption } from "./waw025";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW025: SNS Topic Not Encrypted", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw025.id).toBe("WAW025");
|
|
12
|
+
expect(waw025.description).toContain("encrypted");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags topic without KmsMasterKeyId", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyTopic: {
|
|
19
|
+
Type: "AWS::SNS::Topic",
|
|
20
|
+
Properties: { TopicName: "my-topic" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkSnsEncryption(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW025");
|
|
27
|
+
expect(diags[0].severity).toBe("warning");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("no diagnostic when KmsMasterKeyId is set", () => {
|
|
31
|
+
const ctx = makeCtx({
|
|
32
|
+
Resources: {
|
|
33
|
+
MyTopic: {
|
|
34
|
+
Type: "AWS::SNS::Topic",
|
|
35
|
+
Properties: { KmsMasterKeyId: "alias/my-key" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const diags = checkSnsEncryption(ctx);
|
|
40
|
+
expect(diags).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW025: SNS Topic Not Encrypted
|
|
3
|
+
*
|
|
4
|
+
* Flags SNS topics without KmsMasterKeyId.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkSnsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::SNS::Topic") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
if (!("KmsMasterKeyId" in props)) {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
checkId: "WAW025",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `SNS topic "${logicalId}" is not encrypted — add KmsMasterKeyId for encryption at rest`,
|
|
26
|
+
entity: logicalId,
|
|
27
|
+
lexicon: "aws",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return diagnostics;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const waw025: PostSynthCheck = {
|
|
37
|
+
id: "WAW025",
|
|
38
|
+
description: "SNS topic is not encrypted — add KmsMasterKeyId for encryption at rest",
|
|
39
|
+
|
|
40
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
41
|
+
return checkSnsEncryption(ctx);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw026, checkSqsEncryption } from "./waw026";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW026: SQS Queue Not Encrypted", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw026.id).toBe("WAW026");
|
|
12
|
+
expect(waw026.description).toContain("encrypted");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags queue without encryption", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyQueue: {
|
|
19
|
+
Type: "AWS::SQS::Queue",
|
|
20
|
+
Properties: { QueueName: "my-queue" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkSqsEncryption(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW026");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("no diagnostic with SqsManagedSseEnabled", () => {
|
|
30
|
+
const ctx = makeCtx({
|
|
31
|
+
Resources: {
|
|
32
|
+
MyQueue: {
|
|
33
|
+
Type: "AWS::SQS::Queue",
|
|
34
|
+
Properties: { SqsManagedSseEnabled: true },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const diags = checkSqsEncryption(ctx);
|
|
39
|
+
expect(diags).toHaveLength(0);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("no diagnostic with KmsMasterKeyId", () => {
|
|
43
|
+
const ctx = makeCtx({
|
|
44
|
+
Resources: {
|
|
45
|
+
MyQueue: {
|
|
46
|
+
Type: "AWS::SQS::Queue",
|
|
47
|
+
Properties: { KmsMasterKeyId: "alias/my-key" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const diags = checkSqsEncryption(ctx);
|
|
52
|
+
expect(diags).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW026: SQS Queue Not Encrypted
|
|
3
|
+
*
|
|
4
|
+
* Flags SQS queues without SqsManagedSseEnabled or KmsMasterKeyId.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkSqsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::SQS::Queue") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
const hasSseSqs = props.SqsManagedSseEnabled === true;
|
|
22
|
+
const hasKms = "KmsMasterKeyId" in props;
|
|
23
|
+
|
|
24
|
+
if (!hasSseSqs && !hasKms) {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
checkId: "WAW026",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: `SQS queue "${logicalId}" is not encrypted — enable SqsManagedSseEnabled or set KmsMasterKeyId`,
|
|
29
|
+
entity: logicalId,
|
|
30
|
+
lexicon: "aws",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return diagnostics;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const waw026: PostSynthCheck = {
|
|
40
|
+
id: "WAW026",
|
|
41
|
+
description: "SQS queue is not encrypted — enable SqsManagedSseEnabled or set KmsMasterKeyId",
|
|
42
|
+
|
|
43
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
44
|
+
return checkSqsEncryption(ctx);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw027, checkDynamoDbPitr } from "./waw027";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW027: DynamoDB Missing PITR", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw027.id).toBe("WAW027");
|
|
12
|
+
expect(waw027.description).toContain("point-in-time");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags table without PITR", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyTable: {
|
|
19
|
+
Type: "AWS::DynamoDB::Table",
|
|
20
|
+
Properties: {
|
|
21
|
+
TableName: "my-table",
|
|
22
|
+
KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
|
|
23
|
+
AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const diags = checkDynamoDbPitr(ctx);
|
|
29
|
+
expect(diags).toHaveLength(1);
|
|
30
|
+
expect(diags[0].checkId).toBe("WAW027");
|
|
31
|
+
expect(diags[0].severity).toBe("info");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("no diagnostic when PITR is enabled", () => {
|
|
35
|
+
const ctx = makeCtx({
|
|
36
|
+
Resources: {
|
|
37
|
+
MyTable: {
|
|
38
|
+
Type: "AWS::DynamoDB::Table",
|
|
39
|
+
Properties: {
|
|
40
|
+
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const diags = checkDynamoDbPitr(ctx);
|
|
46
|
+
expect(diags).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("flags when PITR spec present but not enabled", () => {
|
|
50
|
+
const ctx = makeCtx({
|
|
51
|
+
Resources: {
|
|
52
|
+
MyTable: {
|
|
53
|
+
Type: "AWS::DynamoDB::Table",
|
|
54
|
+
Properties: {
|
|
55
|
+
PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: false },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const diags = checkDynamoDbPitr(ctx);
|
|
61
|
+
expect(diags).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW027: DynamoDB Missing Point-in-Time Recovery
|
|
3
|
+
*
|
|
4
|
+
* Flags DynamoDB tables without PointInTimeRecoverySpecification.PointInTimeRecoveryEnabled.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkDynamoDbPitr(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::DynamoDB::Table") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
const pitrSpec = props.PointInTimeRecoverySpecification;
|
|
22
|
+
|
|
23
|
+
let pitrEnabled = false;
|
|
24
|
+
if (typeof pitrSpec === "object" && pitrSpec !== null) {
|
|
25
|
+
pitrEnabled = (pitrSpec as Record<string, unknown>).PointInTimeRecoveryEnabled === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!pitrEnabled) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
checkId: "WAW027",
|
|
31
|
+
severity: "info",
|
|
32
|
+
message: `DynamoDB table "${logicalId}" does not have point-in-time recovery enabled — consider enabling for data protection`,
|
|
33
|
+
entity: logicalId,
|
|
34
|
+
lexicon: "aws",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return diagnostics;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const waw027: PostSynthCheck = {
|
|
44
|
+
id: "WAW027",
|
|
45
|
+
description: "DynamoDB table does not have point-in-time recovery enabled",
|
|
46
|
+
|
|
47
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
48
|
+
return checkDynamoDbPitr(ctx);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw028, checkEbsEncryption } from "./waw028";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW028: EBS Volume Not Encrypted", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw028.id).toBe("WAW028");
|
|
12
|
+
expect(waw028.description).toContain("encrypted");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags volume without Encrypted", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyVol: {
|
|
19
|
+
Type: "AWS::EC2::Volume",
|
|
20
|
+
Properties: { AvailabilityZone: "us-east-1a", Size: 100 },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkEbsEncryption(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW028");
|
|
27
|
+
expect(diags[0].severity).toBe("warning");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("no diagnostic when Encrypted: true", () => {
|
|
31
|
+
const ctx = makeCtx({
|
|
32
|
+
Resources: {
|
|
33
|
+
MyVol: {
|
|
34
|
+
Type: "AWS::EC2::Volume",
|
|
35
|
+
Properties: { AvailabilityZone: "us-east-1a", Encrypted: true },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const diags = checkEbsEncryption(ctx);
|
|
40
|
+
expect(diags).toHaveLength(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("flags Encrypted: false", () => {
|
|
44
|
+
const ctx = makeCtx({
|
|
45
|
+
Resources: {
|
|
46
|
+
MyVol: {
|
|
47
|
+
Type: "AWS::EC2::Volume",
|
|
48
|
+
Properties: { AvailabilityZone: "us-east-1a", Encrypted: false },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const diags = checkEbsEncryption(ctx);
|
|
53
|
+
expect(diags).toHaveLength(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("skips intrinsic value for Encrypted", () => {
|
|
57
|
+
const ctx = makeCtx({
|
|
58
|
+
Resources: {
|
|
59
|
+
MyVol: {
|
|
60
|
+
Type: "AWS::EC2::Volume",
|
|
61
|
+
Properties: { AvailabilityZone: "us-east-1a", Encrypted: { Ref: "EncryptParam" } },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const diags = checkEbsEncryption(ctx);
|
|
66
|
+
expect(diags).toHaveLength(0);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW028: EBS Volume Not Encrypted
|
|
3
|
+
*
|
|
4
|
+
* Flags EBS volumes without Encrypted: true.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate, isIntrinsic } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
export function checkEbsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
11
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
12
|
+
|
|
13
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
14
|
+
const template = parseCFTemplate(output);
|
|
15
|
+
if (!template?.Resources) continue;
|
|
16
|
+
|
|
17
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
18
|
+
if (resource.Type !== "AWS::EC2::Volume") continue;
|
|
19
|
+
|
|
20
|
+
const props = resource.Properties ?? {};
|
|
21
|
+
const encrypted = props.Encrypted;
|
|
22
|
+
|
|
23
|
+
if (isIntrinsic(encrypted)) continue;
|
|
24
|
+
|
|
25
|
+
if (encrypted !== true) {
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
checkId: "WAW028",
|
|
28
|
+
severity: "warning",
|
|
29
|
+
message: `EBS volume "${logicalId}" does not have Encrypted: true — enable encryption at rest`,
|
|
30
|
+
entity: logicalId,
|
|
31
|
+
lexicon: "aws",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return diagnostics;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const waw028: PostSynthCheck = {
|
|
41
|
+
id: "WAW028",
|
|
42
|
+
description: "EBS volume is not encrypted — enable encryption at rest",
|
|
43
|
+
|
|
44
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
45
|
+
return checkEbsEncryption(ctx);
|
|
46
|
+
},
|
|
47
|
+
};
|