@intentius/chant-lexicon-aws 0.0.8 → 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 +5743 -896
- package/dist/rules/cf-refs.ts +99 -0
- package/dist/rules/ext001.ts +30 -21
- package/dist/rules/hardcoded-region.ts +1 -0
- package/dist/rules/iam-wildcard.ts +1 -0
- package/dist/rules/s3-encryption.ts +1 -0
- 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 +388 -30
- package/dist/types/index.d.ts +1552 -1528
- 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 +2 -2
- package/src/codegen/docs-links.test.ts +143 -0
- package/src/codegen/docs.ts +247 -132
- package/src/codegen/generate-lexicon.ts +8 -0
- package/src/codegen/generate-typescript.ts +25 -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 +1552 -1528
- package/src/generated/lexicon-aws.json +5743 -896
- package/src/import/roundtrip-fixtures.test.ts +1 -1
- package/src/index.ts +21 -0
- package/src/integration.test.ts +71 -0
- 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 +30 -21
- 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/s3-encryption.ts +1 -0
- package/src/lsp/hover.ts +15 -0
- package/src/nested-stack-integration.test.ts +100 -0
- package/src/nested-stack.ts +1 -1
- package/src/plugin.ts +468 -36
- package/src/serializer.test.ts +330 -2
- package/src/serializer.ts +62 -1
- 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/rules/cf-refs.ts
CHANGED
|
@@ -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;
|
package/dist/rules/ext001.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { join } from "path";
|
|
|
16
16
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
17
17
|
import { parseCFTemplate, type CFResource } from "./cf-refs";
|
|
18
18
|
|
|
19
|
-
interface ExtensionConstraint {
|
|
19
|
+
export interface ExtensionConstraint {
|
|
20
20
|
name: string;
|
|
21
21
|
type: "if_then" | "dependent_excluded" | "required_or" | "required_xor";
|
|
22
22
|
condition?: unknown;
|
|
@@ -25,7 +25,7 @@ interface ExtensionConstraint {
|
|
|
25
25
|
|
|
26
26
|
interface LexiconEntry {
|
|
27
27
|
kind: string;
|
|
28
|
-
|
|
28
|
+
resourceType: string;
|
|
29
29
|
constraints?: ExtensionConstraint[];
|
|
30
30
|
[key: string]: unknown;
|
|
31
31
|
}
|
|
@@ -43,8 +43,8 @@ function loadLexiconConstraints(): Map<string, ExtensionConstraint[]> {
|
|
|
43
43
|
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
44
44
|
|
|
45
45
|
for (const [_name, entry] of Object.entries(data)) {
|
|
46
|
-
if (entry.kind === "resource" && entry.
|
|
47
|
-
map.set(entry.
|
|
46
|
+
if (entry.kind === "resource" && entry.resourceType && entry.constraints && entry.constraints.length > 0) {
|
|
47
|
+
map.set(entry.resourceType, entry.constraints);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
} catch {
|
|
@@ -193,28 +193,37 @@ function validateResource(
|
|
|
193
193
|
return diagnostics;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
198
|
+
*/
|
|
199
|
+
export function checkExtensionConstraints(
|
|
200
|
+
ctx: PostSynthContext,
|
|
201
|
+
constraintMap: Map<string, ExtensionConstraint[]>,
|
|
202
|
+
): PostSynthDiagnostic[] {
|
|
203
|
+
if (constraintMap.size === 0) return [];
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
205
206
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
208
|
+
const template = parseCFTemplate(output);
|
|
209
|
+
if (!template?.Resources) continue;
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
212
|
+
const constraints = constraintMap.get(resource.Type);
|
|
213
|
+
if (!constraints) continue;
|
|
213
214
|
|
|
214
|
-
|
|
215
|
-
}
|
|
215
|
+
diagnostics.push(...validateResource(logicalId, resource, constraints));
|
|
216
216
|
}
|
|
217
|
+
}
|
|
217
218
|
|
|
218
|
-
|
|
219
|
+
return diagnostics;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const ext001: PostSynthCheck = {
|
|
223
|
+
id: "EXT001",
|
|
224
|
+
description: "Extension constraint violation — cross-property validation from cfn-lint extension schemas",
|
|
225
|
+
|
|
226
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
227
|
+
return checkExtensionConstraints(ctx, loadLexiconConstraints());
|
|
219
228
|
},
|
|
220
229
|
};
|
|
@@ -11,6 +11,7 @@ export const hardcodedRegionRule: LintRule = {
|
|
|
11
11
|
id: "WAW001",
|
|
12
12
|
severity: "warning",
|
|
13
13
|
category: "security",
|
|
14
|
+
description: "Detects hardcoded AWS region strings — use AWS.Region pseudo-parameter instead",
|
|
14
15
|
|
|
15
16
|
check(context: LintContext): LintDiagnostic[] {
|
|
16
17
|
const { sourceFile } = context;
|
|
@@ -11,6 +11,7 @@ export const iamWildcardRule: LintRule = {
|
|
|
11
11
|
id: "WAW009",
|
|
12
12
|
severity: "warning",
|
|
13
13
|
category: "security",
|
|
14
|
+
description: "Detects IAM policies with wildcard (*) resources — specify explicit resource ARNs for better security",
|
|
14
15
|
|
|
15
16
|
check(context: LintContext): LintDiagnostic[] {
|
|
16
17
|
const { sourceFile } = context;
|
|
@@ -11,6 +11,7 @@ export const s3EncryptionRule: LintRule = {
|
|
|
11
11
|
id: "WAW006",
|
|
12
12
|
severity: "warning",
|
|
13
13
|
category: "security",
|
|
14
|
+
description: "Detects S3 Bucket creation without encryption configuration — all buckets should have server-side encryption enabled",
|
|
14
15
|
|
|
15
16
|
check(context: LintContext): LintDiagnostic[] {
|
|
16
17
|
const { sourceFile } = context;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW016: Deprecated Property Usage
|
|
3
|
+
*
|
|
4
|
+
* Flags properties marked as deprecated in the CloudFormation Registry.
|
|
5
|
+
* Sources: explicit `deprecatedProperties` array + description text mining.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
12
|
+
|
|
13
|
+
interface LexiconEntry {
|
|
14
|
+
kind: string;
|
|
15
|
+
resourceType: string;
|
|
16
|
+
deprecatedProperties?: string[];
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load deprecated properties per resource type from the lexicon JSON.
|
|
22
|
+
*/
|
|
23
|
+
function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
24
|
+
const map = new Map<string, Set<string>>();
|
|
25
|
+
try {
|
|
26
|
+
const pkgDir = join(__dirname, "..", "..", "..");
|
|
27
|
+
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
28
|
+
const content = readFileSync(lexiconPath, "utf-8");
|
|
29
|
+
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
30
|
+
|
|
31
|
+
for (const [_name, entry] of Object.entries(data)) {
|
|
32
|
+
if (entry.kind === "resource" && entry.resourceType && entry.deprecatedProperties && entry.deprecatedProperties.length > 0) {
|
|
33
|
+
map.set(entry.resourceType, new Set(entry.deprecatedProperties));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Lexicon not available — skip
|
|
38
|
+
}
|
|
39
|
+
return map;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
44
|
+
*/
|
|
45
|
+
export function checkDeprecatedProperties(
|
|
46
|
+
ctx: PostSynthContext,
|
|
47
|
+
deprecated: Map<string, Set<string>>,
|
|
48
|
+
): PostSynthDiagnostic[] {
|
|
49
|
+
if (deprecated.size === 0) return [];
|
|
50
|
+
|
|
51
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
52
|
+
|
|
53
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
54
|
+
const template = parseCFTemplate(output);
|
|
55
|
+
if (!template?.Resources) continue;
|
|
56
|
+
|
|
57
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
58
|
+
const deprProps = deprecated.get(resource.Type);
|
|
59
|
+
if (!deprProps) continue;
|
|
60
|
+
|
|
61
|
+
const props = resource.Properties ?? {};
|
|
62
|
+
for (const propName of Object.keys(props)) {
|
|
63
|
+
if (deprProps.has(propName)) {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
checkId: "WAW016",
|
|
66
|
+
severity: "warning",
|
|
67
|
+
message: `Resource "${logicalId}" (${resource.Type}) uses deprecated property "${propName}" — consider alternatives`,
|
|
68
|
+
entity: logicalId,
|
|
69
|
+
lexicon: "aws",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return diagnostics;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const waw016: PostSynthCheck = {
|
|
80
|
+
id: "WAW016",
|
|
81
|
+
description: "Deprecated property usage — flags properties marked as deprecated in the CloudFormation Registry",
|
|
82
|
+
|
|
83
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
84
|
+
return checkDeprecatedProperties(ctx, loadDeprecatedProperties());
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW017: Missing Tags on Taggable Resource
|
|
3
|
+
*
|
|
4
|
+
* Flags taggable resources that have no Tags property set.
|
|
5
|
+
* Encourages adding tags for cost allocation and compliance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
10
|
+
import { loadTaggableResources } from "../../taggable";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Core detection logic — exported for direct testing with synthetic data.
|
|
14
|
+
*/
|
|
15
|
+
export function checkMissingTags(
|
|
16
|
+
ctx: PostSynthContext,
|
|
17
|
+
taggable: Set<string>,
|
|
18
|
+
): PostSynthDiagnostic[] {
|
|
19
|
+
if (taggable.size === 0) return [];
|
|
20
|
+
|
|
21
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
22
|
+
|
|
23
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
24
|
+
const template = parseCFTemplate(output);
|
|
25
|
+
if (!template?.Resources) continue;
|
|
26
|
+
|
|
27
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
28
|
+
if (!taggable.has(resource.Type)) continue;
|
|
29
|
+
|
|
30
|
+
const props = resource.Properties ?? {};
|
|
31
|
+
if (!("Tags" in props)) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "WAW017",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `Resource "${logicalId}" (${resource.Type}) supports tagging but has no Tags — consider adding tags for cost allocation and compliance`,
|
|
36
|
+
entity: logicalId,
|
|
37
|
+
lexicon: "aws",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return diagnostics;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const waw017: PostSynthCheck = {
|
|
47
|
+
id: "WAW017",
|
|
48
|
+
description: "Missing tags on taggable resource — suggests adding tags for cost allocation and compliance",
|
|
49
|
+
|
|
50
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
51
|
+
return checkMissingTags(ctx, loadTaggableResources());
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -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,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,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,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,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
|
+
};
|