@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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW018: S3 Public Access Not Blocked
|
|
3
|
+
*
|
|
4
|
+
* Flags S3 buckets missing PublicAccessBlockConfiguration or with any flag set to false.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate, isIntrinsic } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
const REQUIRED_FLAGS = [
|
|
11
|
+
"BlockPublicAcls",
|
|
12
|
+
"BlockPublicPolicy",
|
|
13
|
+
"IgnorePublicAcls",
|
|
14
|
+
"RestrictPublicBuckets",
|
|
15
|
+
] as const;
|
|
16
|
+
|
|
17
|
+
export function checkS3PublicAccess(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
18
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
21
|
+
const template = parseCFTemplate(output);
|
|
22
|
+
if (!template?.Resources) continue;
|
|
23
|
+
|
|
24
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
25
|
+
if (resource.Type !== "AWS::S3::Bucket") continue;
|
|
26
|
+
|
|
27
|
+
const props = resource.Properties ?? {};
|
|
28
|
+
const pab = props.PublicAccessBlockConfiguration;
|
|
29
|
+
|
|
30
|
+
if (!pab) {
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
checkId: "WAW018",
|
|
33
|
+
severity: "error",
|
|
34
|
+
message: `S3 bucket "${logicalId}" is missing PublicAccessBlockConfiguration — all public access should be blocked`,
|
|
35
|
+
entity: logicalId,
|
|
36
|
+
lexicon: "aws",
|
|
37
|
+
});
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isIntrinsic(pab)) continue;
|
|
42
|
+
|
|
43
|
+
if (typeof pab === "object" && pab !== null) {
|
|
44
|
+
const config = pab as Record<string, unknown>;
|
|
45
|
+
for (const flag of REQUIRED_FLAGS) {
|
|
46
|
+
const value = config[flag];
|
|
47
|
+
if (value === false) {
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
checkId: "WAW018",
|
|
50
|
+
severity: "error",
|
|
51
|
+
message: `S3 bucket "${logicalId}" has ${flag} set to false — all public access should be blocked`,
|
|
52
|
+
entity: logicalId,
|
|
53
|
+
lexicon: "aws",
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return diagnostics;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const waw018: PostSynthCheck = {
|
|
65
|
+
id: "WAW018",
|
|
66
|
+
description: "S3 bucket missing public access block — all public access should be blocked",
|
|
67
|
+
|
|
68
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
69
|
+
return checkS3PublicAccess(ctx);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw019, checkUnrestrictedIngress } from "./waw019";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW019: Security Group Unrestricted Ingress", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw019.id).toBe("WAW019");
|
|
12
|
+
expect(waw019.description).toContain("ingress");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags SG with 0.0.0.0/0 on port 22", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MySG: {
|
|
19
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
20
|
+
Properties: {
|
|
21
|
+
SecurityGroupIngress: [
|
|
22
|
+
{ IpProtocol: "tcp", FromPort: 22, ToPort: 22, CidrIp: "0.0.0.0/0" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
29
|
+
expect(diags).toHaveLength(1);
|
|
30
|
+
expect(diags[0].checkId).toBe("WAW019");
|
|
31
|
+
expect(diags[0].severity).toBe("error");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("flags SG with ::/0 on port 3389", () => {
|
|
35
|
+
const ctx = makeCtx({
|
|
36
|
+
Resources: {
|
|
37
|
+
MySG: {
|
|
38
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
39
|
+
Properties: {
|
|
40
|
+
SecurityGroupIngress: [
|
|
41
|
+
{ IpProtocol: "tcp", FromPort: 3389, ToPort: 3389, CidrIpv6: "::/0" },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
48
|
+
expect(diags).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("flags wide port range containing sensitive port", () => {
|
|
52
|
+
const ctx = makeCtx({
|
|
53
|
+
Resources: {
|
|
54
|
+
MySG: {
|
|
55
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
56
|
+
Properties: {
|
|
57
|
+
SecurityGroupIngress: [
|
|
58
|
+
{ IpProtocol: "tcp", FromPort: 0, ToPort: 65535, CidrIp: "0.0.0.0/0" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
65
|
+
expect(diags).toHaveLength(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("no diagnostic for restricted CIDR", () => {
|
|
69
|
+
const ctx = makeCtx({
|
|
70
|
+
Resources: {
|
|
71
|
+
MySG: {
|
|
72
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
73
|
+
Properties: {
|
|
74
|
+
SecurityGroupIngress: [
|
|
75
|
+
{ IpProtocol: "tcp", FromPort: 22, ToPort: 22, CidrIp: "10.0.0.0/8" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
82
|
+
expect(diags).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("no diagnostic for non-sensitive port with open CIDR", () => {
|
|
86
|
+
const ctx = makeCtx({
|
|
87
|
+
Resources: {
|
|
88
|
+
MySG: {
|
|
89
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
90
|
+
Properties: {
|
|
91
|
+
SecurityGroupIngress: [
|
|
92
|
+
{ IpProtocol: "tcp", FromPort: 443, ToPort: 443, CidrIp: "0.0.0.0/0" },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
99
|
+
expect(diags).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("checks standalone SecurityGroupIngress resources", () => {
|
|
103
|
+
const ctx = makeCtx({
|
|
104
|
+
Resources: {
|
|
105
|
+
MyIngress: {
|
|
106
|
+
Type: "AWS::EC2::SecurityGroupIngress",
|
|
107
|
+
Properties: {
|
|
108
|
+
GroupId: { Ref: "MySG" },
|
|
109
|
+
IpProtocol: "tcp",
|
|
110
|
+
FromPort: 5432,
|
|
111
|
+
ToPort: 5432,
|
|
112
|
+
CidrIp: "0.0.0.0/0",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
118
|
+
expect(diags).toHaveLength(1);
|
|
119
|
+
expect(diags[0].entity).toBe("MyIngress");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("handles missing port range (all ports)", () => {
|
|
123
|
+
const ctx = makeCtx({
|
|
124
|
+
Resources: {
|
|
125
|
+
MySG: {
|
|
126
|
+
Type: "AWS::EC2::SecurityGroup",
|
|
127
|
+
Properties: {
|
|
128
|
+
SecurityGroupIngress: [
|
|
129
|
+
{ IpProtocol: "-1", CidrIp: "0.0.0.0/0" },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const diags = checkUnrestrictedIngress(ctx);
|
|
136
|
+
expect(diags).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW019: Security Group Unrestricted Ingress
|
|
3
|
+
*
|
|
4
|
+
* Flags security groups with 0.0.0.0/0 or ::/0 ingress on sensitive ports
|
|
5
|
+
* (SSH 22, RDP 3389, MySQL 3306, PostgreSQL 5432).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { parseCFTemplate, getSecurityGroupIngress, portRangeContainsSensitive, isIntrinsic } from "./cf-refs";
|
|
10
|
+
|
|
11
|
+
const SENSITIVE_PORTS = [22, 3389, 3306, 5432];
|
|
12
|
+
const OPEN_CIDRS = new Set(["0.0.0.0/0", "::/0"]);
|
|
13
|
+
|
|
14
|
+
function checkIngressRule(
|
|
15
|
+
rule: Record<string, unknown>,
|
|
16
|
+
logicalId: string,
|
|
17
|
+
diagnostics: PostSynthDiagnostic[],
|
|
18
|
+
): void {
|
|
19
|
+
const cidrIp = rule.CidrIp;
|
|
20
|
+
const cidrIpv6 = rule.CidrIpv6;
|
|
21
|
+
|
|
22
|
+
const hasOpenCidr =
|
|
23
|
+
(typeof cidrIp === "string" && OPEN_CIDRS.has(cidrIp)) ||
|
|
24
|
+
(typeof cidrIpv6 === "string" && OPEN_CIDRS.has(cidrIpv6));
|
|
25
|
+
|
|
26
|
+
if (!hasOpenCidr) return;
|
|
27
|
+
|
|
28
|
+
if (portRangeContainsSensitive(rule.FromPort, rule.ToPort, SENSITIVE_PORTS)) {
|
|
29
|
+
const cidr = typeof cidrIp === "string" && OPEN_CIDRS.has(cidrIp) ? cidrIp : cidrIpv6;
|
|
30
|
+
const fromPort = rule.FromPort;
|
|
31
|
+
const toPort = rule.ToPort;
|
|
32
|
+
const portDesc = fromPort !== undefined && toPort !== undefined
|
|
33
|
+
? ` on ports ${fromPort}-${toPort}`
|
|
34
|
+
: " on all ports";
|
|
35
|
+
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
checkId: "WAW019",
|
|
38
|
+
severity: "error",
|
|
39
|
+
message: `Security group "${logicalId}" allows unrestricted ingress from ${cidr}${portDesc} — restrict to specific CIDR ranges`,
|
|
40
|
+
entity: logicalId,
|
|
41
|
+
lexicon: "aws",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function checkUnrestrictedIngress(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
47
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
48
|
+
|
|
49
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
50
|
+
const template = parseCFTemplate(output);
|
|
51
|
+
if (!template?.Resources) continue;
|
|
52
|
+
|
|
53
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
54
|
+
// Check inline SecurityGroupIngress on EC2::SecurityGroup
|
|
55
|
+
if (resource.Type === "AWS::EC2::SecurityGroup") {
|
|
56
|
+
const rules = getSecurityGroupIngress(resource);
|
|
57
|
+
for (const rule of rules) {
|
|
58
|
+
checkIngressRule(rule, logicalId, diagnostics);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check standalone SecurityGroupIngress resources
|
|
63
|
+
if (resource.Type === "AWS::EC2::SecurityGroupIngress") {
|
|
64
|
+
const props = resource.Properties ?? {};
|
|
65
|
+
if (!isIntrinsic(props)) {
|
|
66
|
+
checkIngressRule(props as Record<string, unknown>, logicalId, diagnostics);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return diagnostics;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const waw019: PostSynthCheck = {
|
|
76
|
+
id: "WAW019",
|
|
77
|
+
description: "Security group allows unrestricted ingress on sensitive ports (SSH, RDP, database)",
|
|
78
|
+
|
|
79
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
80
|
+
return checkUnrestrictedIngress(ctx);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw020, checkIamWildcardAction } from "./waw020";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW020: IAM Wildcard Action", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw020.id).toBe("WAW020");
|
|
12
|
+
expect(waw020.description).toContain("wildcard");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags IAM::Policy with Action: '*'", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyPolicy: {
|
|
19
|
+
Type: "AWS::IAM::Policy",
|
|
20
|
+
Properties: {
|
|
21
|
+
PolicyDocument: {
|
|
22
|
+
Statement: [{ Effect: "Allow", Action: "*", Resource: "*" }],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const diags = checkIamWildcardAction(ctx);
|
|
29
|
+
expect(diags).toHaveLength(1);
|
|
30
|
+
expect(diags[0].checkId).toBe("WAW020");
|
|
31
|
+
expect(diags[0].severity).toBe("warning");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("flags IAM::Role with wildcard in inline Policies", () => {
|
|
35
|
+
const ctx = makeCtx({
|
|
36
|
+
Resources: {
|
|
37
|
+
MyRole: {
|
|
38
|
+
Type: "AWS::IAM::Role",
|
|
39
|
+
Properties: {
|
|
40
|
+
AssumeRolePolicyDocument: {
|
|
41
|
+
Statement: [{ Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, Action: "sts:AssumeRole" }],
|
|
42
|
+
},
|
|
43
|
+
Policies: [
|
|
44
|
+
{
|
|
45
|
+
PolicyName: "admin",
|
|
46
|
+
PolicyDocument: {
|
|
47
|
+
Statement: [{ Effect: "Allow", Action: "*", Resource: "*" }],
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const diags = checkIamWildcardAction(ctx);
|
|
56
|
+
expect(diags).toHaveLength(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("flags wildcard in Action array", () => {
|
|
60
|
+
const ctx = makeCtx({
|
|
61
|
+
Resources: {
|
|
62
|
+
MyPolicy: {
|
|
63
|
+
Type: "AWS::IAM::ManagedPolicy",
|
|
64
|
+
Properties: {
|
|
65
|
+
PolicyDocument: {
|
|
66
|
+
Statement: [{ Effect: "Allow", Action: ["s3:GetObject", "*"], Resource: "*" }],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const diags = checkIamWildcardAction(ctx);
|
|
73
|
+
expect(diags).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("no diagnostic for specific actions", () => {
|
|
77
|
+
const ctx = makeCtx({
|
|
78
|
+
Resources: {
|
|
79
|
+
MyPolicy: {
|
|
80
|
+
Type: "AWS::IAM::Policy",
|
|
81
|
+
Properties: {
|
|
82
|
+
PolicyDocument: {
|
|
83
|
+
Statement: [{ Effect: "Allow", Action: ["s3:GetObject", "s3:PutObject"], Resource: "arn:aws:s3:::my-bucket/*" }],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const diags = checkIamWildcardAction(ctx);
|
|
90
|
+
expect(diags).toHaveLength(0);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("no diagnostic for non-IAM resources", () => {
|
|
94
|
+
const ctx = makeCtx({
|
|
95
|
+
Resources: {
|
|
96
|
+
MyBucket: {
|
|
97
|
+
Type: "AWS::S3::Bucket",
|
|
98
|
+
Properties: { BucketName: "test" },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const diags = checkIamWildcardAction(ctx);
|
|
103
|
+
expect(diags).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("emits one diagnostic per resource even with multiple wildcard statements", () => {
|
|
107
|
+
const ctx = makeCtx({
|
|
108
|
+
Resources: {
|
|
109
|
+
MyPolicy: {
|
|
110
|
+
Type: "AWS::IAM::Policy",
|
|
111
|
+
Properties: {
|
|
112
|
+
PolicyDocument: {
|
|
113
|
+
Statement: [
|
|
114
|
+
{ Effect: "Allow", Action: "*", Resource: "*" },
|
|
115
|
+
{ Effect: "Allow", Action: "*", Resource: "arn:aws:s3:::*" },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const diags = checkIamWildcardAction(ctx);
|
|
123
|
+
expect(diags).toHaveLength(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW020: IAM Wildcard Action
|
|
3
|
+
*
|
|
4
|
+
* Flags IAM policies with Action: "*" in any statement.
|
|
5
|
+
* Checks IAM::Policy, IAM::Role, and IAM::ManagedPolicy resource types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { parseCFTemplate, walkPolicyStatements, isIntrinsic } from "./cf-refs";
|
|
10
|
+
|
|
11
|
+
const IAM_TYPES = new Set([
|
|
12
|
+
"AWS::IAM::Policy",
|
|
13
|
+
"AWS::IAM::Role",
|
|
14
|
+
"AWS::IAM::ManagedPolicy",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function hasWildcardAction(statement: Record<string, unknown>): boolean {
|
|
18
|
+
const action = statement.Action;
|
|
19
|
+
if (action === "*") return true;
|
|
20
|
+
if (Array.isArray(action)) {
|
|
21
|
+
return action.some((a) => a === "*");
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function checkIamWildcardAction(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
30
|
+
const template = parseCFTemplate(output);
|
|
31
|
+
if (!template?.Resources) continue;
|
|
32
|
+
|
|
33
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
34
|
+
if (!IAM_TYPES.has(resource.Type)) continue;
|
|
35
|
+
|
|
36
|
+
const statements = walkPolicyStatements(resource);
|
|
37
|
+
for (const stmt of statements) {
|
|
38
|
+
if (isIntrinsic(stmt.Action)) continue;
|
|
39
|
+
|
|
40
|
+
if (hasWildcardAction(stmt)) {
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
checkId: "WAW020",
|
|
43
|
+
severity: "warning",
|
|
44
|
+
message: `IAM resource "${logicalId}" has a policy statement with Action: "*" — use specific actions following least privilege`,
|
|
45
|
+
entity: logicalId,
|
|
46
|
+
lexicon: "aws",
|
|
47
|
+
});
|
|
48
|
+
break; // One diagnostic per resource
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return diagnostics;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const waw020: PostSynthCheck = {
|
|
58
|
+
id: "WAW020",
|
|
59
|
+
description: "IAM policy uses wildcard Action — use specific actions following least privilege",
|
|
60
|
+
|
|
61
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
62
|
+
return checkIamWildcardAction(ctx);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw021, checkRdsEncryption } from "./waw021";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW021: RDS Storage Not Encrypted", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw021.id).toBe("WAW021");
|
|
12
|
+
expect(waw021.description).toContain("encrypted");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags DBInstance without StorageEncrypted", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyDB: {
|
|
19
|
+
Type: "AWS::RDS::DBInstance",
|
|
20
|
+
Properties: { DBInstanceClass: "db.t3.micro", Engine: "mysql" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkRdsEncryption(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW021");
|
|
27
|
+
expect(diags[0].severity).toBe("error");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("flags DBCluster without StorageEncrypted", () => {
|
|
31
|
+
const ctx = makeCtx({
|
|
32
|
+
Resources: {
|
|
33
|
+
MyCluster: {
|
|
34
|
+
Type: "AWS::RDS::DBCluster",
|
|
35
|
+
Properties: { Engine: "aurora-mysql" },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const diags = checkRdsEncryption(ctx);
|
|
40
|
+
expect(diags).toHaveLength(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("no diagnostic when StorageEncrypted: true", () => {
|
|
44
|
+
const ctx = makeCtx({
|
|
45
|
+
Resources: {
|
|
46
|
+
MyDB: {
|
|
47
|
+
Type: "AWS::RDS::DBInstance",
|
|
48
|
+
Properties: { StorageEncrypted: true, DBInstanceClass: "db.t3.micro" },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const diags = checkRdsEncryption(ctx);
|
|
53
|
+
expect(diags).toHaveLength(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("flags StorageEncrypted: false", () => {
|
|
57
|
+
const ctx = makeCtx({
|
|
58
|
+
Resources: {
|
|
59
|
+
MyDB: {
|
|
60
|
+
Type: "AWS::RDS::DBInstance",
|
|
61
|
+
Properties: { StorageEncrypted: false, DBInstanceClass: "db.t3.micro" },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const diags = checkRdsEncryption(ctx);
|
|
66
|
+
expect(diags).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("skips intrinsic value for StorageEncrypted", () => {
|
|
70
|
+
const ctx = makeCtx({
|
|
71
|
+
Resources: {
|
|
72
|
+
MyDB: {
|
|
73
|
+
Type: "AWS::RDS::DBInstance",
|
|
74
|
+
Properties: { StorageEncrypted: { Ref: "EncryptParam" } },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const diags = checkRdsEncryption(ctx);
|
|
79
|
+
expect(diags).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW021: RDS Storage Not Encrypted
|
|
3
|
+
*
|
|
4
|
+
* Flags RDS instances and clusters without StorageEncrypted: true.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
8
|
+
import { parseCFTemplate, isIntrinsic } from "./cf-refs";
|
|
9
|
+
|
|
10
|
+
const RDS_TYPES = new Set([
|
|
11
|
+
"AWS::RDS::DBInstance",
|
|
12
|
+
"AWS::RDS::DBCluster",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export function checkRdsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
16
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
17
|
+
|
|
18
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
19
|
+
const template = parseCFTemplate(output);
|
|
20
|
+
if (!template?.Resources) continue;
|
|
21
|
+
|
|
22
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
23
|
+
if (!RDS_TYPES.has(resource.Type)) continue;
|
|
24
|
+
|
|
25
|
+
const props = resource.Properties ?? {};
|
|
26
|
+
const encrypted = props.StorageEncrypted;
|
|
27
|
+
|
|
28
|
+
// Skip if it's an intrinsic (can't statically verify)
|
|
29
|
+
if (isIntrinsic(encrypted)) continue;
|
|
30
|
+
|
|
31
|
+
if (encrypted !== true) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "WAW021",
|
|
34
|
+
severity: "error",
|
|
35
|
+
message: `RDS resource "${logicalId}" (${resource.Type}) does not have StorageEncrypted: true — enable encryption at rest`,
|
|
36
|
+
entity: logicalId,
|
|
37
|
+
lexicon: "aws",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const waw021: PostSynthCheck = {
|
|
47
|
+
id: "WAW021",
|
|
48
|
+
description: "RDS instance or cluster storage is not encrypted — enable encryption at rest",
|
|
49
|
+
|
|
50
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
51
|
+
return checkRdsEncryption(ctx);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { createPostSynthContext } from "@intentius/chant-test-utils";
|
|
3
|
+
import { waw022, checkLambdaVpc } from "./waw022";
|
|
4
|
+
|
|
5
|
+
function makeCtx(template: object) {
|
|
6
|
+
return createPostSynthContext({ aws: template });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe("WAW022: Lambda Not in VPC", () => {
|
|
10
|
+
test("check metadata", () => {
|
|
11
|
+
expect(waw022.id).toBe("WAW022");
|
|
12
|
+
expect(waw022.description).toContain("VPC");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("flags Lambda without VpcConfig", () => {
|
|
16
|
+
const ctx = makeCtx({
|
|
17
|
+
Resources: {
|
|
18
|
+
MyFunc: {
|
|
19
|
+
Type: "AWS::Lambda::Function",
|
|
20
|
+
Properties: { FunctionName: "test", Runtime: "nodejs20.x", Handler: "index.handler" },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
const diags = checkLambdaVpc(ctx);
|
|
25
|
+
expect(diags).toHaveLength(1);
|
|
26
|
+
expect(diags[0].checkId).toBe("WAW022");
|
|
27
|
+
expect(diags[0].severity).toBe("warning");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("no diagnostic when VpcConfig is present", () => {
|
|
31
|
+
const ctx = makeCtx({
|
|
32
|
+
Resources: {
|
|
33
|
+
MyFunc: {
|
|
34
|
+
Type: "AWS::Lambda::Function",
|
|
35
|
+
Properties: {
|
|
36
|
+
VpcConfig: { SubnetIds: ["subnet-123"], SecurityGroupIds: ["sg-123"] },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
const diags = checkLambdaVpc(ctx);
|
|
42
|
+
expect(diags).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("no diagnostic for non-Lambda resources", () => {
|
|
46
|
+
const ctx = makeCtx({
|
|
47
|
+
Resources: {
|
|
48
|
+
MyBucket: { Type: "AWS::S3::Bucket", Properties: {} },
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const diags = checkLambdaVpc(ctx);
|
|
52
|
+
expect(diags).toHaveLength(0);
|
|
53
|
+
});
|
|
54
|
+
});
|