@intentius/chant-lexicon-aws 0.49.0 → 0.51.0
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/codegen/generate-lexicon.d.ts +5 -0
- package/dist/codegen/generate-lexicon.d.ts.map +1 -1
- package/dist/components/host-delivery.d.ts +4 -3
- package/dist/components/host-delivery.d.ts.map +1 -1
- package/dist/disruption.d.ts +26 -0
- package/dist/disruption.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +9 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw016.d.ts +10 -2
- package/dist/lint/post-synth/waw016.d.ts.map +1 -1
- package/dist/lint/post-synth/waw064.d.ts +17 -0
- package/dist/lint/post-synth/waw064.d.ts.map +1 -0
- package/dist/lint/post-synth/waw065.d.ts +23 -0
- package/dist/lint/post-synth/waw065.d.ts.map +1 -0
- package/dist/lint/post-synth/waw066.d.ts +25 -0
- package/dist/lint/post-synth/waw066.d.ts.map +1 -0
- package/dist/lint/post-synth/waw067.d.ts +24 -0
- package/dist/lint/post-synth/waw067.d.ts.map +1 -0
- package/dist/lint/post-synth/waw068.d.ts +20 -0
- package/dist/lint/post-synth/waw068.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/meta.json +34 -0
- package/dist/okf/index.md +5 -0
- package/dist/okf/rules/WAW064.md +18 -0
- package/dist/okf/rules/WAW065.md +25 -0
- package/dist/okf/rules/WAW066.md +22 -0
- package/dist/okf/rules/WAW067.md +20 -0
- package/dist/okf/rules/WAW068.md +19 -0
- package/dist/okf/types/EC2Route.md +2 -0
- package/dist/okf/types/InternetGateway.md +4 -0
- package/dist/okf/types/Map.md +4 -0
- package/dist/okf/types/NatGateway.md +4 -0
- package/dist/okf/types/RouteTable.md +4 -0
- package/dist/okf/types/Subnet.md +2 -0
- package/dist/okf/types/SubnetRouteTableAssociation.md +5 -0
- package/dist/okf/types/Table.md +3 -0
- package/dist/okf/types/TransitGateway.md +6 -0
- package/dist/okf/types/TransitGatewayAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayPeeringAttachment.md +4 -0
- package/dist/okf/types/TransitGatewayRoute.md +5 -0
- package/dist/okf/types/TransitGatewayRouteTable.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTableAssociation.md +4 -0
- package/dist/okf/types/TransitGatewayRouteTablePropagation.md +4 -0
- package/dist/okf/types/TransitGatewayVpcAttachment.md +4 -0
- package/dist/okf/types/Type.md +5 -0
- package/dist/okf/types/VPNConnection.md +4 -0
- package/dist/okf/types/VPNGateway.md +4 -0
- package/dist/op/builders.d.ts +47 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/waw016.ts +37 -16
- package/dist/rules/waw064.ts +52 -0
- package/dist/rules/waw065.ts +110 -0
- package/dist/rules/waw066.ts +155 -0
- package/dist/rules/waw067.ts +114 -0
- package/dist/rules/waw068.ts +78 -0
- package/dist/spec/parse.d.ts +7 -0
- package/dist/spec/parse.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/fallback.ts +1 -0
- package/src/codegen/generate-lexicon.ts +6 -0
- package/src/codegen/sam.ts +9 -0
- package/src/components/host-delivery.ts +4 -3
- package/src/disruption.test.ts +198 -0
- package/src/disruption.ts +154 -0
- package/src/generated/lexicon-aws.json +34 -0
- package/src/index.ts +8 -0
- package/src/lint/audit-catalog.ts +10 -0
- package/src/lint/post-synth/index.ts +10 -0
- package/src/lint/post-synth/waw016.test.ts +61 -4
- package/src/lint/post-synth/waw016.ts +37 -16
- package/src/lint/post-synth/waw064.test.ts +79 -0
- package/src/lint/post-synth/waw064.ts +52 -0
- package/src/lint/post-synth/waw065.test.ts +90 -0
- package/src/lint/post-synth/waw065.ts +110 -0
- package/src/lint/post-synth/waw066.test.ts +108 -0
- package/src/lint/post-synth/waw066.ts +155 -0
- package/src/lint/post-synth/waw067.test.ts +79 -0
- package/src/lint/post-synth/waw067.ts +114 -0
- package/src/lint/post-synth/waw068.test.ts +74 -0
- package/src/lint/post-synth/waw068.ts +78 -0
- package/src/lsp/hover.ts +14 -2
- package/src/op/builders.test.ts +61 -0
- package/src/op/builders.ts +72 -0
- package/src/plugin.ts +16 -1
- package/src/spec/parse.test.ts +74 -0
- package/src/spec/parse.ts +26 -6
package/dist/okf/types/Table.md
CHANGED
|
@@ -44,3 +44,6 @@ resource_type: AWS::DynamoDB::Table
|
|
|
44
44
|
- [WAW030](/rules/WAW030.md): Missing DependsOn for known CloudFormation ordering patterns
|
|
45
45
|
- [WAW059](/rules/WAW059.md): Wildcard Resource where the declared graph enumerates the touched set — tighten to the consumers' declared Arns
|
|
46
46
|
- [WAW062](/rules/WAW062.md): Duplicate export name or explicit resource name within a template — fails at deploy time
|
|
47
|
+
- [WAW064](/rules/WAW064.md): Transit Gateway route table declares a Blackhole route — confirm the traffic drop is intentional, not an accident
|
|
48
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
49
|
+
- [WAW066](/rules/WAW066.md): Private subnet's route table has no default route, or its default route targets a gateway that does not exist in the template
|
|
@@ -33,3 +33,9 @@ resource_type: AWS::EC2::TransitGateway
|
|
|
33
33
|
- `EncryptionSupportState`
|
|
34
34
|
- `Id`
|
|
35
35
|
- `TransitGatewayArn`
|
|
36
|
+
|
|
37
|
+
## Governed by
|
|
38
|
+
|
|
39
|
+
- [WAW064](/rules/WAW064.md): Transit Gateway route table declares a Blackhole route — confirm the traffic drop is intentional, not an accident
|
|
40
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
41
|
+
- [WAW068](/rules/WAW068.md): VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity
|
|
@@ -26,3 +26,7 @@ resource_type: AWS::EC2::TransitGatewayPeeringAttachment
|
|
|
26
26
|
- `State`
|
|
27
27
|
- `Status`
|
|
28
28
|
- `TransitGatewayAttachmentId`
|
|
29
|
+
|
|
30
|
+
## Governed by
|
|
31
|
+
|
|
32
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
@@ -14,3 +14,8 @@ resource_type: AWS::EC2::TransitGatewayRoute
|
|
|
14
14
|
- `TransitGatewayRouteTableId` (`string`, required): The ID of transit gateway route table.
|
|
15
15
|
- `Blackhole` (`boolean`, optional): Indicates whether to drop traffic that matches this route.
|
|
16
16
|
- `TransitGatewayAttachmentId` (`string`, optional): The ID of transit gateway attachment.
|
|
17
|
+
|
|
18
|
+
## Governed by
|
|
19
|
+
|
|
20
|
+
- [WAW064](/rules/WAW064.md): Transit Gateway route table declares a Blackhole route — confirm the traffic drop is intentional, not an accident
|
|
21
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
@@ -17,3 +17,7 @@ resource_type: AWS::EC2::TransitGatewayRouteTable
|
|
|
17
17
|
## Attributes
|
|
18
18
|
|
|
19
19
|
- `TransitGatewayRouteTableId`
|
|
20
|
+
|
|
21
|
+
## Governed by
|
|
22
|
+
|
|
23
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
@@ -12,3 +12,7 @@ resource_type: AWS::EC2::TransitGatewayRouteTableAssociation
|
|
|
12
12
|
|
|
13
13
|
- `TransitGatewayAttachmentId` (`string`, required): The ID of transit gateway attachment.
|
|
14
14
|
- `TransitGatewayRouteTableId` (`string`, required): The ID of transit gateway route table.
|
|
15
|
+
|
|
16
|
+
## Governed by
|
|
17
|
+
|
|
18
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
@@ -12,3 +12,7 @@ resource_type: AWS::EC2::TransitGatewayRouteTablePropagation
|
|
|
12
12
|
|
|
13
13
|
- `TransitGatewayAttachmentId` (`string`, required): The ID of transit gateway attachment.
|
|
14
14
|
- `TransitGatewayRouteTableId` (`string`, required): The ID of transit gateway route table.
|
|
15
|
+
|
|
16
|
+
## Governed by
|
|
17
|
+
|
|
18
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
package/dist/okf/types/Type.md
CHANGED
|
@@ -78,3 +78,8 @@ resource_type: AWS::Cassandra::Type
|
|
|
78
78
|
- [WAW061](/rules/WAW061.md): Subnet CidrBlock falls outside its VPC's CidrBlock — fails at deploy time
|
|
79
79
|
- [WAW062](/rules/WAW062.md): Duplicate export name or explicit resource name within a template — fails at deploy time
|
|
80
80
|
- [WAW063](/rules/WAW063.md): IAM policy denies an action another attached policy on the same role allows — explicit Deny wins, runtime 403
|
|
81
|
+
- [WAW064](/rules/WAW064.md): Transit Gateway route table declares a Blackhole route — confirm the traffic drop is intentional, not an accident
|
|
82
|
+
- [WAW065](/rules/WAW065.md): Transit Gateway route table has associations but no propagations, or an attachment wired into no route table
|
|
83
|
+
- [WAW066](/rules/WAW066.md): Private subnet's route table has no default route, or its default route targets a gateway that does not exist in the template
|
|
84
|
+
- [WAW067](/rules/WAW067.md): A single-AZ NAT gateway serves private subnets across multiple Availability Zones — single point of failure for cross-AZ egress
|
|
85
|
+
- [WAW068](/rules/WAW068.md): VPN Gateway or Transit Gateway has only one attached VPN Connection — no redundant path for hybrid connectivity
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities (chant #1288
|
|
3
|
+
* Stage 2). See `lexicons/k8s/src/op/builders.ts`'s module doc for why these
|
|
4
|
+
* live beside their `*Args` interfaces rather than in core or the temporal
|
|
5
|
+
* barrel: core cannot import a lexicon's types (lexicons depend on core, not
|
|
6
|
+
* the reverse), and wiring them into the cloud-agnostic temporal barrel
|
|
7
|
+
* would make it depend on every cloud lexicon at runtime. `opts`'s type in
|
|
8
|
+
* each wrapper below IS the activity's own `*Args` interface (via
|
|
9
|
+
* `Omit`/`WithStepRefs`) — never restated. `core`'s own
|
|
10
|
+
* `awsApply`/`awsDelete`/`flociUp`/`flociDown` are unchanged and produce
|
|
11
|
+
* byte-identical `ActivityStep` output; these are purely additive.
|
|
12
|
+
*/
|
|
13
|
+
import { type ActivityStep, type NamedActivityStep, type WithStepRefs } from "@intentius/chant/op";
|
|
14
|
+
import type { AwsApplyArgs, AwsDeleteArgs } from "./activities/aws-apply.js";
|
|
15
|
+
import type { FlociUpArgs, FlociDownArgs } from "./activities/floci.js";
|
|
16
|
+
/** Extra opts every wrapper below accepts alongside its activity's own fields. */
|
|
17
|
+
type StepOpts = {
|
|
18
|
+
profile?: ActivityStep["profile"];
|
|
19
|
+
id?: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Deploy a built CloudFormation template directly against the CFN API — the
|
|
23
|
+
* fully typed twin of core's `awsApply`. `opts` is {@link AwsApplyArgs}
|
|
24
|
+
* itself, minus the positional `templatePath`. Defaults to the `longInfra`
|
|
25
|
+
* profile.
|
|
26
|
+
*/
|
|
27
|
+
export declare const awsApply: (templatePath: string, opts: WithStepRefs<Omit<AwsApplyArgs, "templatePath">> & StepOpts) => NamedActivityStep;
|
|
28
|
+
/**
|
|
29
|
+
* Delete a CloudFormation stack — the inverse of {@link awsApply}, fully
|
|
30
|
+
* typed twin of core's `awsDelete`. `opts` is {@link AwsDeleteArgs} itself,
|
|
31
|
+
* minus the positional `templatePath`. Defaults to the `longInfra` profile.
|
|
32
|
+
*/
|
|
33
|
+
export declare const awsDelete: (templatePath: string, opts: WithStepRefs<Omit<AwsDeleteArgs, "templatePath">> & StepOpts) => NamedActivityStep;
|
|
34
|
+
/**
|
|
35
|
+
* Boot the local Floci AWS emulator — the fully typed twin of core's
|
|
36
|
+
* `flociUp`. `opts` is {@link FlociUpArgs} itself. Defaults to the
|
|
37
|
+
* `longInfra` profile.
|
|
38
|
+
*/
|
|
39
|
+
export declare const flociUp: (opts?: WithStepRefs<FlociUpArgs> & StepOpts) => NamedActivityStep;
|
|
40
|
+
/**
|
|
41
|
+
* Stop and remove the local Floci emulator container — the fully typed twin
|
|
42
|
+
* of core's `flociDown`. `opts` is {@link FlociDownArgs} itself. Defaults to
|
|
43
|
+
* the `fastIdempotent` profile.
|
|
44
|
+
*/
|
|
45
|
+
export declare const flociDown: (opts?: WithStepRefs<FlociDownArgs> & StepOpts) => NamedActivityStep;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=builders.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"builders.d.ts","sourceRoot":"","sources":["../../src/op/builders.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAErE,kFAAkF;AAClF,KAAK,QAAQ,GAAG;IAAE,OAAO,CAAC,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GACnB,cAAc,MAAM,EACpB,MAAM,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC,GAAG,QAAQ,KAChE,iBAGF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,GACpB,cAAc,MAAM,EACpB,MAAM,YAAY,CAAC,IAAI,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,GAAG,QAAQ,KACjE,iBAGF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,YAAY,CAAC,WAAW,CAAC,GAAG,QAAQ,KAAG,iBAGrE,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,YAAY,CAAC,aAAa,CAAC,GAAG,QAAQ,KAAG,iBAGzE,CAAC"}
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAoN,MAAM,0BAA0B,CAAC;AAsChR;yEACyE;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAiBnD,eAAO,MAAM,SAAS,EAAE,aAg+BvB,CAAC"}
|
package/dist/rules/waw016.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
* WAW016: Deprecated Property Usage
|
|
3
3
|
*
|
|
4
4
|
* Flags properties marked as deprecated in the CloudFormation Registry.
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Two bases feed this, and the finding says which one it stands on (#1701).
|
|
7
|
+
* A `declared` name comes from the Registry schema's own `deprecatedProperties`
|
|
8
|
+
* array. An `inferred` name comes from a regex over the property description,
|
|
9
|
+
* which also matches descriptions that mention the deprecation of a sibling
|
|
10
|
+
* property, an enum value, or the thing the property configures. Declared is a
|
|
11
|
+
* warning; inferred is reported at info and worded as a guess.
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import { readFileSync } from "fs";
|
|
@@ -10,18 +16,23 @@ import { join } from "path";
|
|
|
10
16
|
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
17
|
import { parseCFTemplate } from "./cf-refs";
|
|
12
18
|
|
|
19
|
+
/** What a deprecation classification rests on. */
|
|
20
|
+
export type DeprecationBasis = "declared" | "inferred";
|
|
21
|
+
|
|
13
22
|
interface LexiconEntry {
|
|
14
23
|
kind: string;
|
|
15
24
|
resourceType: string;
|
|
16
25
|
deprecatedProperties?: string[];
|
|
26
|
+
inferredDeprecations?: string[];
|
|
17
27
|
[key: string]: unknown;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
/**
|
|
21
|
-
* Load deprecated properties per resource type from the lexicon JSON
|
|
31
|
+
* Load deprecated properties per resource type from the lexicon JSON, each
|
|
32
|
+
* tagged with the basis it was classified on.
|
|
22
33
|
*/
|
|
23
|
-
function loadDeprecatedProperties(): Map<string,
|
|
24
|
-
const map = new Map<string,
|
|
34
|
+
function loadDeprecatedProperties(): Map<string, Map<string, DeprecationBasis>> {
|
|
35
|
+
const map = new Map<string, Map<string, DeprecationBasis>>();
|
|
25
36
|
try {
|
|
26
37
|
const pkgDir = join(__dirname, "..", "..", "..");
|
|
27
38
|
const lexiconPath = join(pkgDir, "src", "generated", "lexicon-aws.json");
|
|
@@ -29,9 +40,15 @@ function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
|
29
40
|
const data = JSON.parse(content) as Record<string, LexiconEntry>;
|
|
30
41
|
|
|
31
42
|
for (const [_name, entry] of Object.entries(data)) {
|
|
32
|
-
if (entry.kind
|
|
33
|
-
|
|
43
|
+
if (entry.kind !== "resource" || !entry.resourceType) continue;
|
|
44
|
+
if (!entry.deprecatedProperties?.length) continue;
|
|
45
|
+
|
|
46
|
+
const inferred = new Set(entry.inferredDeprecations ?? []);
|
|
47
|
+
const byProp = new Map<string, DeprecationBasis>();
|
|
48
|
+
for (const propName of entry.deprecatedProperties) {
|
|
49
|
+
byProp.set(propName, inferred.has(propName) ? "inferred" : "declared");
|
|
34
50
|
}
|
|
51
|
+
map.set(entry.resourceType, byProp);
|
|
35
52
|
}
|
|
36
53
|
} catch {
|
|
37
54
|
// Lexicon not available — skip
|
|
@@ -44,7 +61,7 @@ function loadDeprecatedProperties(): Map<string, Set<string>> {
|
|
|
44
61
|
*/
|
|
45
62
|
export function checkDeprecatedProperties(
|
|
46
63
|
ctx: PostSynthContext,
|
|
47
|
-
deprecated: Map<string,
|
|
64
|
+
deprecated: Map<string, Map<string, DeprecationBasis>>,
|
|
48
65
|
): PostSynthDiagnostic[] {
|
|
49
66
|
if (deprecated.size === 0) return [];
|
|
50
67
|
|
|
@@ -60,15 +77,19 @@ export function checkDeprecatedProperties(
|
|
|
60
77
|
|
|
61
78
|
const props = resource.Properties ?? {};
|
|
62
79
|
for (const propName of Object.keys(props)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
const basis = deprProps.get(propName);
|
|
81
|
+
if (!basis) continue;
|
|
82
|
+
|
|
83
|
+
diagnostics.push({
|
|
84
|
+
checkId: "WAW016",
|
|
85
|
+
severity: basis === "declared" ? "warning" : "info",
|
|
86
|
+
message:
|
|
87
|
+
basis === "declared"
|
|
88
|
+
? `Resource "${logicalId}" (${resource.Type}) uses deprecated property "${propName}" — consider alternatives`
|
|
89
|
+
: `Resource "${logicalId}" (${resource.Type}) uses property "${propName}", whose description reads as deprecated — the CloudFormation Registry does not declare it deprecated, so confirm before changing it`,
|
|
90
|
+
entity: logicalId,
|
|
91
|
+
lexicon: "aws",
|
|
92
|
+
});
|
|
72
93
|
}
|
|
73
94
|
}
|
|
74
95
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW064: Transit Gateway Route Table Declares A Blackhole Route
|
|
3
|
+
*
|
|
4
|
+
* `AWS::EC2::TransitGatewayRoute` with `Blackhole: true` silently drops any
|
|
5
|
+
* traffic matching its destination CIDR instead of forwarding it —
|
|
6
|
+
* CloudFormation deploys this without complaint, and the packet loss only
|
|
7
|
+
* surfaces downstream as unreachable hosts or a timeout nobody can explain
|
|
8
|
+
* from the template alone. A blackhole route is sometimes deliberate — an
|
|
9
|
+
* explicit deny for a CIDR range, or a placeholder pending real
|
|
10
|
+
* infrastructure — but it is also exactly what a copy-pasted route table or
|
|
11
|
+
* a forgotten cleanup step leaves behind. Warn so it stays a conscious
|
|
12
|
+
* choice rather than a silent accident.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
16
|
+
import { parseCFTemplate } from "./cf-refs";
|
|
17
|
+
|
|
18
|
+
export function checkTgwBlackholeRoute(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
22
|
+
const template = parseCFTemplate(output);
|
|
23
|
+
if (!template?.Resources) continue;
|
|
24
|
+
|
|
25
|
+
for (const [logicalId, resource] of Object.entries(template.Resources)) {
|
|
26
|
+
if (resource.Type !== "AWS::EC2::TransitGatewayRoute") continue;
|
|
27
|
+
|
|
28
|
+
const props = resource.Properties ?? {};
|
|
29
|
+
if (props.Blackhole !== true) continue;
|
|
30
|
+
|
|
31
|
+
const cidr = typeof props.DestinationCidrBlock === "string" ? props.DestinationCidrBlock : "(unknown destination)";
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "WAW064",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `TransitGatewayRoute "${logicalId}" declares Blackhole: true for destination ${cidr} — traffic matching this route is silently dropped; confirm this is intentional`,
|
|
36
|
+
entity: logicalId,
|
|
37
|
+
lexicon: "aws",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return diagnostics;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const waw064: PostSynthCheck = {
|
|
46
|
+
id: "WAW064",
|
|
47
|
+
description: "Transit Gateway route table declares a Blackhole route — confirm the traffic drop is intentional, not an accident",
|
|
48
|
+
|
|
49
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
50
|
+
return checkTgwBlackholeRoute(ctx);
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW065: Transit Gateway Route Table Wiring Is Incomplete
|
|
3
|
+
*
|
|
4
|
+
* A Transit Gateway route table only does anything once two independent
|
|
5
|
+
* pieces are both wired up: an `AWS::EC2::TransitGatewayRouteTableAssociation`
|
|
6
|
+
* (which attachment's traffic is evaluated against this table) and an
|
|
7
|
+
* `AWS::EC2::TransitGatewayRouteTablePropagation` (which attachments' routes
|
|
8
|
+
* populate it). CloudFormation is happy to deploy a route table with one but
|
|
9
|
+
* not the other — the stack succeeds, and the gap only shows up as traffic
|
|
10
|
+
* that can't find a route. Two patterns, both "declared but half-wired":
|
|
11
|
+
*
|
|
12
|
+
* - a route table has one or more associations but zero propagations — the
|
|
13
|
+
* associated attachment's traffic is evaluated against this table, but
|
|
14
|
+
* nothing ever populates it with routes;
|
|
15
|
+
* - a declared attachment (VPC/VPN/peering, or the generic
|
|
16
|
+
* TransitGatewayAttachment) is referenced by neither an association nor
|
|
17
|
+
* a propagation anywhere in the template — it hangs off the transit
|
|
18
|
+
* gateway with no route table wiring at all.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
22
|
+
import { parseCFTemplate, findResourceRefs } from "./cf-refs";
|
|
23
|
+
|
|
24
|
+
const ATTACHMENT_TYPES = new Set([
|
|
25
|
+
"AWS::EC2::TransitGatewayAttachment",
|
|
26
|
+
"AWS::EC2::TransitGatewayVpcAttachment",
|
|
27
|
+
"AWS::EC2::TransitGatewayVpnAttachment",
|
|
28
|
+
"AWS::EC2::TransitGatewayPeeringAttachment",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function checkTgwRouteTableWiring(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
32
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
33
|
+
|
|
34
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
35
|
+
const template = parseCFTemplate(output);
|
|
36
|
+
if (!template?.Resources) continue;
|
|
37
|
+
const resources = template.Resources;
|
|
38
|
+
|
|
39
|
+
const routeTableIds = new Set<string>();
|
|
40
|
+
const attachmentIds = new Set<string>();
|
|
41
|
+
for (const [logicalId, resource] of Object.entries(resources)) {
|
|
42
|
+
if (resource.Type === "AWS::EC2::TransitGatewayRouteTable") routeTableIds.add(logicalId);
|
|
43
|
+
if (ATTACHMENT_TYPES.has(resource.Type)) attachmentIds.add(logicalId);
|
|
44
|
+
}
|
|
45
|
+
if (routeTableIds.size === 0 && attachmentIds.size === 0) continue;
|
|
46
|
+
|
|
47
|
+
const associationsByTable = new Map<string, Set<string>>();
|
|
48
|
+
const propagationsByTable = new Map<string, Set<string>>();
|
|
49
|
+
const referencedAttachments = new Set<string>();
|
|
50
|
+
|
|
51
|
+
for (const resource of Object.values(resources)) {
|
|
52
|
+
let bucket: Map<string, Set<string>> | null = null;
|
|
53
|
+
if (resource.Type === "AWS::EC2::TransitGatewayRouteTableAssociation") bucket = associationsByTable;
|
|
54
|
+
if (resource.Type === "AWS::EC2::TransitGatewayRouteTablePropagation") bucket = propagationsByTable;
|
|
55
|
+
if (!bucket) continue;
|
|
56
|
+
|
|
57
|
+
const props = resource.Properties ?? {};
|
|
58
|
+
const attachmentRefs = findResourceRefs(props.TransitGatewayAttachmentId);
|
|
59
|
+
for (const attId of attachmentRefs) referencedAttachments.add(attId);
|
|
60
|
+
|
|
61
|
+
for (const rtId of findResourceRefs(props.TransitGatewayRouteTableId)) {
|
|
62
|
+
let attSet = bucket.get(rtId);
|
|
63
|
+
if (!attSet) {
|
|
64
|
+
attSet = new Set();
|
|
65
|
+
bucket.set(rtId, attSet);
|
|
66
|
+
}
|
|
67
|
+
for (const attId of attachmentRefs) attSet.add(attId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Pattern A: associated but never propagated.
|
|
72
|
+
for (const rtId of routeTableIds) {
|
|
73
|
+
const associations = associationsByTable.get(rtId);
|
|
74
|
+
const propagations = propagationsByTable.get(rtId);
|
|
75
|
+
if (associations && associations.size > 0 && (!propagations || propagations.size === 0)) {
|
|
76
|
+
diagnostics.push({
|
|
77
|
+
checkId: "WAW065",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
message: `TransitGatewayRouteTable "${rtId}" has ${associations.size} association(s) but no route propagations — the associated attachment's routes never populate this table, likely incomplete wiring`,
|
|
80
|
+
entity: rtId,
|
|
81
|
+
lexicon: "aws",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pattern B: a declared attachment wired into no route table at all.
|
|
87
|
+
for (const attId of attachmentIds) {
|
|
88
|
+
if (!referencedAttachments.has(attId)) {
|
|
89
|
+
diagnostics.push({
|
|
90
|
+
checkId: "WAW065",
|
|
91
|
+
severity: "warning",
|
|
92
|
+
message: `Transit Gateway attachment "${attId}" is referenced by no TransitGatewayRouteTableAssociation or TransitGatewayRouteTablePropagation — it has no route table wiring, so nothing can route to or from it`,
|
|
93
|
+
entity: attId,
|
|
94
|
+
lexicon: "aws",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return diagnostics;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const waw065: PostSynthCheck = {
|
|
104
|
+
id: "WAW065",
|
|
105
|
+
description: "Transit Gateway route table has associations but no propagations, or an attachment wired into no route table",
|
|
106
|
+
|
|
107
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
108
|
+
return checkTgwRouteTableWiring(ctx);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WAW066: Private Subnet Route Table Has No Working Default Route
|
|
3
|
+
*
|
|
4
|
+
* Cross-resource join: a subnet's outbound path depends on a chain of three
|
|
5
|
+
* independently-declared resources — the `AWS::EC2::SubnetRouteTableAssociation`
|
|
6
|
+
* that pins it to a route table, the `AWS::EC2::Route` on that table for
|
|
7
|
+
* `0.0.0.0/0`, and the gateway that route names. CloudFormation validates
|
|
8
|
+
* none of this end-to-end: a subnet can be wired to a route table with no
|
|
9
|
+
* default route at all, or with a default route whose NAT gateway / Transit
|
|
10
|
+
* Gateway target was renamed or removed elsewhere in the template, and the
|
|
11
|
+
* stack still deploys clean. The subnet is black-holed — every deploy
|
|
12
|
+
* succeeds, and the only symptom is a workload with no outbound connectivity.
|
|
13
|
+
*
|
|
14
|
+
* A route table with a default route to an `AWS::EC2::InternetGateway` is
|
|
15
|
+
* treated as a public subnet and is out of scope for this check — this rule
|
|
16
|
+
* only flags the private case: no default route, or one whose target
|
|
17
|
+
* (`NatGatewayId` / `TransitGatewayId` / etc.) does not resolve to a
|
|
18
|
+
* resource declared in the same template. A route naming a target that
|
|
19
|
+
* exists in the template resolves cleanly and is never flagged, per the
|
|
20
|
+
* cross-resource contract.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
24
|
+
import { parseCFTemplate, findResourceRefs, type CFResource, type CFTemplate } from "./cf-refs";
|
|
25
|
+
|
|
26
|
+
type RouteClass = "public" | "resolved" | "dangling";
|
|
27
|
+
|
|
28
|
+
const OTHER_TARGET_KEYS = [
|
|
29
|
+
"NatGatewayId",
|
|
30
|
+
"TransitGatewayId",
|
|
31
|
+
"VpcPeeringConnectionId",
|
|
32
|
+
"EgressOnlyInternetGatewayId",
|
|
33
|
+
"LocalGatewayId",
|
|
34
|
+
"CarrierGatewayId",
|
|
35
|
+
"NetworkInterfaceId",
|
|
36
|
+
"InstanceId",
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
/** Classify a `0.0.0.0/0` Route's target. A target Ref that exists always resolves. */
|
|
40
|
+
function classifyDefaultRoute(route: CFResource, resources: Record<string, CFResource>): RouteClass {
|
|
41
|
+
const props = route.Properties ?? {};
|
|
42
|
+
|
|
43
|
+
if (props.GatewayId !== undefined) {
|
|
44
|
+
if (typeof props.GatewayId === "string") {
|
|
45
|
+
return props.GatewayId.startsWith("igw-") ? "public" : "resolved";
|
|
46
|
+
}
|
|
47
|
+
const refs = findResourceRefs(props.GatewayId);
|
|
48
|
+
if (refs.size !== 1) return "resolved"; // unprovable intrinsic — stay quiet
|
|
49
|
+
const [id] = refs;
|
|
50
|
+
if (!resources[id]) return "dangling";
|
|
51
|
+
return resources[id].Type === "AWS::EC2::InternetGateway" ? "public" : "resolved";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const key of OTHER_TARGET_KEYS) {
|
|
55
|
+
const value = (props as Record<string, unknown>)[key];
|
|
56
|
+
if (value === undefined) continue;
|
|
57
|
+
if (typeof value === "string") return "resolved"; // literal id from outside the template
|
|
58
|
+
const refs = findResourceRefs(value);
|
|
59
|
+
if (refs.size !== 1) return "resolved";
|
|
60
|
+
const [id] = refs;
|
|
61
|
+
return resources[id] ? "resolved" : "dangling";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return "resolved"; // no recognized target key — can't classify, stay quiet
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isDefaultRoute(route: CFResource): boolean {
|
|
68
|
+
return route.Properties?.DestinationCidrBlock === "0.0.0.0/0";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function checkPrivateSubnetDefaultRoute(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
72
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
73
|
+
|
|
74
|
+
for (const [_lexicon, output] of ctx.outputs) {
|
|
75
|
+
const template: CFTemplate | null = parseCFTemplate(output);
|
|
76
|
+
if (!template?.Resources) continue;
|
|
77
|
+
const resources = template.Resources;
|
|
78
|
+
|
|
79
|
+
// subnetId -> Set<routeTableId>, from explicit associations only.
|
|
80
|
+
const subnetRouteTables = new Map<string, Set<string>>();
|
|
81
|
+
for (const resource of Object.values(resources)) {
|
|
82
|
+
if (resource.Type !== "AWS::EC2::SubnetRouteTableAssociation") continue;
|
|
83
|
+
const props = resource.Properties ?? {};
|
|
84
|
+
const subnetRefs = findResourceRefs(props.SubnetId);
|
|
85
|
+
const rtRefs = findResourceRefs(props.RouteTableId);
|
|
86
|
+
for (const subnetId of subnetRefs) {
|
|
87
|
+
let set = subnetRouteTables.get(subnetId);
|
|
88
|
+
if (!set) {
|
|
89
|
+
set = new Set();
|
|
90
|
+
subnetRouteTables.set(subnetId, set);
|
|
91
|
+
}
|
|
92
|
+
for (const rtId of rtRefs) set.add(rtId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// routeTableId -> Route resources targeting it.
|
|
97
|
+
const routesByTable = new Map<string, CFResource[]>();
|
|
98
|
+
for (const resource of Object.values(resources)) {
|
|
99
|
+
if (resource.Type !== "AWS::EC2::Route") continue;
|
|
100
|
+
for (const rtId of findResourceRefs(resource.Properties?.RouteTableId)) {
|
|
101
|
+
let list = routesByTable.get(rtId);
|
|
102
|
+
if (!list) {
|
|
103
|
+
list = [];
|
|
104
|
+
routesByTable.set(rtId, list);
|
|
105
|
+
}
|
|
106
|
+
list.push(resource);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [subnetId, rtIds] of subnetRouteTables) {
|
|
111
|
+
if (resources[subnetId]?.Type !== "AWS::EC2::Subnet") continue;
|
|
112
|
+
|
|
113
|
+
for (const rtId of rtIds) {
|
|
114
|
+
if (resources[rtId]?.Type !== "AWS::EC2::RouteTable") continue;
|
|
115
|
+
|
|
116
|
+
const routes = routesByTable.get(rtId) ?? [];
|
|
117
|
+
const defaultRoutes = routes.filter(isDefaultRoute);
|
|
118
|
+
|
|
119
|
+
if (defaultRoutes.length === 0) {
|
|
120
|
+
diagnostics.push({
|
|
121
|
+
checkId: "WAW066",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
message: `Subnet "${subnetId}" is associated with route table "${rtId}", which has no default route (0.0.0.0/0) — outbound traffic from this subnet has no path out, black-holed by omission`,
|
|
124
|
+
entity: subnetId,
|
|
125
|
+
lexicon: "aws",
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const classes = defaultRoutes.map((route) => classifyDefaultRoute(route, resources));
|
|
131
|
+
if (classes.includes("public")) continue; // has a real IGW default route — public subnet, out of scope
|
|
132
|
+
if (classes.every((c) => c === "dangling")) {
|
|
133
|
+
diagnostics.push({
|
|
134
|
+
checkId: "WAW066",
|
|
135
|
+
severity: "error",
|
|
136
|
+
message: `Subnet "${subnetId}"'s route table "${rtId}" has a default route (0.0.0.0/0) whose target does not exist in this template — outbound traffic from this subnet is black-holed`,
|
|
137
|
+
entity: subnetId,
|
|
138
|
+
lexicon: "aws",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return diagnostics;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const waw066: PostSynthCheck = {
|
|
149
|
+
id: "WAW066",
|
|
150
|
+
description: "Private subnet's route table has no default route, or its default route targets a gateway that does not exist in the template",
|
|
151
|
+
|
|
152
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
153
|
+
return checkPrivateSubnetDefaultRoute(ctx);
|
|
154
|
+
},
|
|
155
|
+
};
|