@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.
Files changed (115) hide show
  1. package/dist/integrity.json +25 -10
  2. package/dist/manifest.json +1 -1
  3. package/dist/meta.json +5743 -896
  4. package/dist/rules/cf-refs.ts +99 -0
  5. package/dist/rules/ext001.ts +30 -21
  6. package/dist/rules/hardcoded-region.ts +1 -0
  7. package/dist/rules/iam-wildcard.ts +1 -0
  8. package/dist/rules/s3-encryption.ts +1 -0
  9. package/dist/rules/waw016.ts +86 -0
  10. package/dist/rules/waw017.ts +53 -0
  11. package/dist/rules/waw018.ts +71 -0
  12. package/dist/rules/waw019.ts +82 -0
  13. package/dist/rules/waw020.ts +64 -0
  14. package/dist/rules/waw021.ts +53 -0
  15. package/dist/rules/waw022.ts +43 -0
  16. package/dist/rules/waw023.ts +47 -0
  17. package/dist/rules/waw024.ts +54 -0
  18. package/dist/rules/waw025.ts +43 -0
  19. package/dist/rules/waw026.ts +46 -0
  20. package/dist/rules/waw027.ts +50 -0
  21. package/dist/rules/waw028.ts +47 -0
  22. package/dist/rules/waw029.ts +62 -0
  23. package/dist/rules/waw030.ts +246 -0
  24. package/dist/skills/chant-aws.md +388 -30
  25. package/dist/types/index.d.ts +1552 -1528
  26. package/package.json +2 -2
  27. package/src/actions/actions.test.ts +75 -0
  28. package/src/actions/dynamodb.ts +36 -0
  29. package/src/actions/ecr.ts +9 -0
  30. package/src/actions/ecs.ts +5 -0
  31. package/src/actions/iam.ts +3 -0
  32. package/src/actions/index.ts +9 -0
  33. package/src/actions/lambda.ts +11 -0
  34. package/src/actions/logs.ts +4 -0
  35. package/src/actions/s3.ts +34 -0
  36. package/src/actions/sns.ts +5 -0
  37. package/src/actions/sqs.ts +15 -0
  38. package/src/codegen/__snapshots__/snapshot.test.ts.snap +2 -2
  39. package/src/codegen/docs-links.test.ts +143 -0
  40. package/src/codegen/docs.ts +247 -132
  41. package/src/codegen/generate-lexicon.ts +8 -0
  42. package/src/codegen/generate-typescript.ts +25 -1
  43. package/src/composites/composites.test.ts +442 -0
  44. package/src/composites/fargate-alb.ts +253 -0
  45. package/src/composites/index.ts +20 -0
  46. package/src/composites/lambda-api.ts +20 -0
  47. package/src/composites/lambda-dynamodb.ts +64 -0
  48. package/src/composites/lambda-eventbridge.ts +36 -0
  49. package/src/composites/lambda-function.ts +76 -0
  50. package/src/composites/lambda-s3.ts +72 -0
  51. package/src/composites/lambda-sns.ts +30 -0
  52. package/src/composites/lambda-sqs.ts +44 -0
  53. package/src/composites/scheduled-lambda.ts +37 -0
  54. package/src/composites/vpc-default.ts +148 -0
  55. package/src/default-tags.test.ts +38 -0
  56. package/src/default-tags.ts +77 -0
  57. package/src/generated/index.d.ts +1552 -1528
  58. package/src/generated/lexicon-aws.json +5743 -896
  59. package/src/import/roundtrip-fixtures.test.ts +1 -1
  60. package/src/index.ts +21 -0
  61. package/src/integration.test.ts +71 -0
  62. package/src/intrinsics.ts +24 -13
  63. package/src/lint/post-synth/cf-refs.ts +99 -0
  64. package/src/lint/post-synth/ext001.test.ts +214 -31
  65. package/src/lint/post-synth/ext001.ts +30 -21
  66. package/src/lint/post-synth/waw013.test.ts +120 -0
  67. package/src/lint/post-synth/waw014.test.ts +121 -0
  68. package/src/lint/post-synth/waw015.test.ts +147 -0
  69. package/src/lint/post-synth/waw016.test.ts +141 -0
  70. package/src/lint/post-synth/waw016.ts +86 -0
  71. package/src/lint/post-synth/waw017.test.ts +130 -0
  72. package/src/lint/post-synth/waw017.ts +53 -0
  73. package/src/lint/post-synth/waw018.test.ts +109 -0
  74. package/src/lint/post-synth/waw018.ts +71 -0
  75. package/src/lint/post-synth/waw019.test.ts +138 -0
  76. package/src/lint/post-synth/waw019.ts +82 -0
  77. package/src/lint/post-synth/waw020.test.ts +125 -0
  78. package/src/lint/post-synth/waw020.ts +64 -0
  79. package/src/lint/post-synth/waw021.test.ts +81 -0
  80. package/src/lint/post-synth/waw021.ts +53 -0
  81. package/src/lint/post-synth/waw022.test.ts +54 -0
  82. package/src/lint/post-synth/waw022.ts +43 -0
  83. package/src/lint/post-synth/waw023.test.ts +53 -0
  84. package/src/lint/post-synth/waw023.ts +47 -0
  85. package/src/lint/post-synth/waw024.test.ts +64 -0
  86. package/src/lint/post-synth/waw024.ts +54 -0
  87. package/src/lint/post-synth/waw025.test.ts +42 -0
  88. package/src/lint/post-synth/waw025.ts +43 -0
  89. package/src/lint/post-synth/waw026.test.ts +54 -0
  90. package/src/lint/post-synth/waw026.ts +46 -0
  91. package/src/lint/post-synth/waw027.test.ts +63 -0
  92. package/src/lint/post-synth/waw027.ts +50 -0
  93. package/src/lint/post-synth/waw028.test.ts +68 -0
  94. package/src/lint/post-synth/waw028.ts +47 -0
  95. package/src/lint/post-synth/waw029.test.ts +179 -0
  96. package/src/lint/post-synth/waw029.ts +62 -0
  97. package/src/lint/post-synth/waw030.test.ts +800 -0
  98. package/src/lint/post-synth/waw030.ts +246 -0
  99. package/src/lint/rules/hardcoded-region.ts +1 -0
  100. package/src/lint/rules/iam-wildcard.ts +1 -0
  101. package/src/lint/rules/s3-encryption.ts +1 -0
  102. package/src/lsp/hover.ts +15 -0
  103. package/src/nested-stack-integration.test.ts +100 -0
  104. package/src/nested-stack.ts +1 -1
  105. package/src/plugin.ts +468 -36
  106. package/src/serializer.test.ts +330 -2
  107. package/src/serializer.ts +62 -1
  108. package/src/spec/fetch.ts +10 -0
  109. package/src/spec/parse.test.ts +141 -0
  110. package/src/spec/parse.ts +40 -0
  111. package/src/taggable.ts +44 -0
  112. package/src/testdata/nested-stacks/app.ts +26 -0
  113. package/src/testdata/nested-stacks/network/outputs.ts +17 -0
  114. package/src/testdata/nested-stacks/network/security.ts +17 -0
  115. package/src/testdata/nested-stacks/network/vpc.ts +54 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * WAW025: SNS Topic Not Encrypted
3
+ *
4
+ * Flags SNS topics without KmsMasterKeyId.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { parseCFTemplate } from "./cf-refs";
9
+
10
+ export function checkSnsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
11
+ const diagnostics: PostSynthDiagnostic[] = [];
12
+
13
+ for (const [_lexicon, output] of ctx.outputs) {
14
+ const template = parseCFTemplate(output);
15
+ if (!template?.Resources) continue;
16
+
17
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
18
+ if (resource.Type !== "AWS::SNS::Topic") continue;
19
+
20
+ const props = resource.Properties ?? {};
21
+ if (!("KmsMasterKeyId" in props)) {
22
+ diagnostics.push({
23
+ checkId: "WAW025",
24
+ severity: "warning",
25
+ message: `SNS topic "${logicalId}" is not encrypted — add KmsMasterKeyId for encryption at rest`,
26
+ entity: logicalId,
27
+ lexicon: "aws",
28
+ });
29
+ }
30
+ }
31
+ }
32
+
33
+ return diagnostics;
34
+ }
35
+
36
+ export const waw025: PostSynthCheck = {
37
+ id: "WAW025",
38
+ description: "SNS topic is not encrypted — add KmsMasterKeyId for encryption at rest",
39
+
40
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
41
+ return checkSnsEncryption(ctx);
42
+ },
43
+ };
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw026, checkSqsEncryption } from "./waw026";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW026: SQS Queue Not Encrypted", () => {
10
+ test("check metadata", () => {
11
+ expect(waw026.id).toBe("WAW026");
12
+ expect(waw026.description).toContain("encrypted");
13
+ });
14
+
15
+ test("flags queue without encryption", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyQueue: {
19
+ Type: "AWS::SQS::Queue",
20
+ Properties: { QueueName: "my-queue" },
21
+ },
22
+ },
23
+ });
24
+ const diags = checkSqsEncryption(ctx);
25
+ expect(diags).toHaveLength(1);
26
+ expect(diags[0].checkId).toBe("WAW026");
27
+ });
28
+
29
+ test("no diagnostic with SqsManagedSseEnabled", () => {
30
+ const ctx = makeCtx({
31
+ Resources: {
32
+ MyQueue: {
33
+ Type: "AWS::SQS::Queue",
34
+ Properties: { SqsManagedSseEnabled: true },
35
+ },
36
+ },
37
+ });
38
+ const diags = checkSqsEncryption(ctx);
39
+ expect(diags).toHaveLength(0);
40
+ });
41
+
42
+ test("no diagnostic with KmsMasterKeyId", () => {
43
+ const ctx = makeCtx({
44
+ Resources: {
45
+ MyQueue: {
46
+ Type: "AWS::SQS::Queue",
47
+ Properties: { KmsMasterKeyId: "alias/my-key" },
48
+ },
49
+ },
50
+ });
51
+ const diags = checkSqsEncryption(ctx);
52
+ expect(diags).toHaveLength(0);
53
+ });
54
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * WAW026: SQS Queue Not Encrypted
3
+ *
4
+ * Flags SQS queues without SqsManagedSseEnabled or KmsMasterKeyId.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { parseCFTemplate } from "./cf-refs";
9
+
10
+ export function checkSqsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
11
+ const diagnostics: PostSynthDiagnostic[] = [];
12
+
13
+ for (const [_lexicon, output] of ctx.outputs) {
14
+ const template = parseCFTemplate(output);
15
+ if (!template?.Resources) continue;
16
+
17
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
18
+ if (resource.Type !== "AWS::SQS::Queue") continue;
19
+
20
+ const props = resource.Properties ?? {};
21
+ const hasSseSqs = props.SqsManagedSseEnabled === true;
22
+ const hasKms = "KmsMasterKeyId" in props;
23
+
24
+ if (!hasSseSqs && !hasKms) {
25
+ diagnostics.push({
26
+ checkId: "WAW026",
27
+ severity: "warning",
28
+ message: `SQS queue "${logicalId}" is not encrypted — enable SqsManagedSseEnabled or set KmsMasterKeyId`,
29
+ entity: logicalId,
30
+ lexicon: "aws",
31
+ });
32
+ }
33
+ }
34
+ }
35
+
36
+ return diagnostics;
37
+ }
38
+
39
+ export const waw026: PostSynthCheck = {
40
+ id: "WAW026",
41
+ description: "SQS queue is not encrypted — enable SqsManagedSseEnabled or set KmsMasterKeyId",
42
+
43
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
44
+ return checkSqsEncryption(ctx);
45
+ },
46
+ };
@@ -0,0 +1,63 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw027, checkDynamoDbPitr } from "./waw027";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW027: DynamoDB Missing PITR", () => {
10
+ test("check metadata", () => {
11
+ expect(waw027.id).toBe("WAW027");
12
+ expect(waw027.description).toContain("point-in-time");
13
+ });
14
+
15
+ test("flags table without PITR", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyTable: {
19
+ Type: "AWS::DynamoDB::Table",
20
+ Properties: {
21
+ TableName: "my-table",
22
+ KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
23
+ AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
24
+ },
25
+ },
26
+ },
27
+ });
28
+ const diags = checkDynamoDbPitr(ctx);
29
+ expect(diags).toHaveLength(1);
30
+ expect(diags[0].checkId).toBe("WAW027");
31
+ expect(diags[0].severity).toBe("info");
32
+ });
33
+
34
+ test("no diagnostic when PITR is enabled", () => {
35
+ const ctx = makeCtx({
36
+ Resources: {
37
+ MyTable: {
38
+ Type: "AWS::DynamoDB::Table",
39
+ Properties: {
40
+ PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: true },
41
+ },
42
+ },
43
+ },
44
+ });
45
+ const diags = checkDynamoDbPitr(ctx);
46
+ expect(diags).toHaveLength(0);
47
+ });
48
+
49
+ test("flags when PITR spec present but not enabled", () => {
50
+ const ctx = makeCtx({
51
+ Resources: {
52
+ MyTable: {
53
+ Type: "AWS::DynamoDB::Table",
54
+ Properties: {
55
+ PointInTimeRecoverySpecification: { PointInTimeRecoveryEnabled: false },
56
+ },
57
+ },
58
+ },
59
+ });
60
+ const diags = checkDynamoDbPitr(ctx);
61
+ expect(diags).toHaveLength(1);
62
+ });
63
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WAW027: DynamoDB Missing Point-in-Time Recovery
3
+ *
4
+ * Flags DynamoDB tables without PointInTimeRecoverySpecification.PointInTimeRecoveryEnabled.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { parseCFTemplate } from "./cf-refs";
9
+
10
+ export function checkDynamoDbPitr(ctx: PostSynthContext): PostSynthDiagnostic[] {
11
+ const diagnostics: PostSynthDiagnostic[] = [];
12
+
13
+ for (const [_lexicon, output] of ctx.outputs) {
14
+ const template = parseCFTemplate(output);
15
+ if (!template?.Resources) continue;
16
+
17
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
18
+ if (resource.Type !== "AWS::DynamoDB::Table") continue;
19
+
20
+ const props = resource.Properties ?? {};
21
+ const pitrSpec = props.PointInTimeRecoverySpecification;
22
+
23
+ let pitrEnabled = false;
24
+ if (typeof pitrSpec === "object" && pitrSpec !== null) {
25
+ pitrEnabled = (pitrSpec as Record<string, unknown>).PointInTimeRecoveryEnabled === true;
26
+ }
27
+
28
+ if (!pitrEnabled) {
29
+ diagnostics.push({
30
+ checkId: "WAW027",
31
+ severity: "info",
32
+ message: `DynamoDB table "${logicalId}" does not have point-in-time recovery enabled — consider enabling for data protection`,
33
+ entity: logicalId,
34
+ lexicon: "aws",
35
+ });
36
+ }
37
+ }
38
+ }
39
+
40
+ return diagnostics;
41
+ }
42
+
43
+ export const waw027: PostSynthCheck = {
44
+ id: "WAW027",
45
+ description: "DynamoDB table does not have point-in-time recovery enabled",
46
+
47
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
48
+ return checkDynamoDbPitr(ctx);
49
+ },
50
+ };
@@ -0,0 +1,68 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw028, checkEbsEncryption } from "./waw028";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW028: EBS Volume Not Encrypted", () => {
10
+ test("check metadata", () => {
11
+ expect(waw028.id).toBe("WAW028");
12
+ expect(waw028.description).toContain("encrypted");
13
+ });
14
+
15
+ test("flags volume without Encrypted", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyVol: {
19
+ Type: "AWS::EC2::Volume",
20
+ Properties: { AvailabilityZone: "us-east-1a", Size: 100 },
21
+ },
22
+ },
23
+ });
24
+ const diags = checkEbsEncryption(ctx);
25
+ expect(diags).toHaveLength(1);
26
+ expect(diags[0].checkId).toBe("WAW028");
27
+ expect(diags[0].severity).toBe("warning");
28
+ });
29
+
30
+ test("no diagnostic when Encrypted: true", () => {
31
+ const ctx = makeCtx({
32
+ Resources: {
33
+ MyVol: {
34
+ Type: "AWS::EC2::Volume",
35
+ Properties: { AvailabilityZone: "us-east-1a", Encrypted: true },
36
+ },
37
+ },
38
+ });
39
+ const diags = checkEbsEncryption(ctx);
40
+ expect(diags).toHaveLength(0);
41
+ });
42
+
43
+ test("flags Encrypted: false", () => {
44
+ const ctx = makeCtx({
45
+ Resources: {
46
+ MyVol: {
47
+ Type: "AWS::EC2::Volume",
48
+ Properties: { AvailabilityZone: "us-east-1a", Encrypted: false },
49
+ },
50
+ },
51
+ });
52
+ const diags = checkEbsEncryption(ctx);
53
+ expect(diags).toHaveLength(1);
54
+ });
55
+
56
+ test("skips intrinsic value for Encrypted", () => {
57
+ const ctx = makeCtx({
58
+ Resources: {
59
+ MyVol: {
60
+ Type: "AWS::EC2::Volume",
61
+ Properties: { AvailabilityZone: "us-east-1a", Encrypted: { Ref: "EncryptParam" } },
62
+ },
63
+ },
64
+ });
65
+ const diags = checkEbsEncryption(ctx);
66
+ expect(diags).toHaveLength(0);
67
+ });
68
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * WAW028: EBS Volume Not Encrypted
3
+ *
4
+ * Flags EBS volumes without Encrypted: true.
5
+ */
6
+
7
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
8
+ import { parseCFTemplate, isIntrinsic } from "./cf-refs";
9
+
10
+ export function checkEbsEncryption(ctx: PostSynthContext): PostSynthDiagnostic[] {
11
+ const diagnostics: PostSynthDiagnostic[] = [];
12
+
13
+ for (const [_lexicon, output] of ctx.outputs) {
14
+ const template = parseCFTemplate(output);
15
+ if (!template?.Resources) continue;
16
+
17
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
18
+ if (resource.Type !== "AWS::EC2::Volume") continue;
19
+
20
+ const props = resource.Properties ?? {};
21
+ const encrypted = props.Encrypted;
22
+
23
+ if (isIntrinsic(encrypted)) continue;
24
+
25
+ if (encrypted !== true) {
26
+ diagnostics.push({
27
+ checkId: "WAW028",
28
+ severity: "warning",
29
+ message: `EBS volume "${logicalId}" does not have Encrypted: true — enable encryption at rest`,
30
+ entity: logicalId,
31
+ lexicon: "aws",
32
+ });
33
+ }
34
+ }
35
+ }
36
+
37
+ return diagnostics;
38
+ }
39
+
40
+ export const waw028: PostSynthCheck = {
41
+ id: "WAW028",
42
+ description: "EBS volume is not encrypted — enable encryption at rest",
43
+
44
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
45
+ return checkEbsEncryption(ctx);
46
+ },
47
+ };
@@ -0,0 +1,179 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw029, checkInvalidDependsOn } from "./waw029";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW029: Invalid DependsOn Target", () => {
10
+ test("check metadata", () => {
11
+ expect(waw029.id).toBe("WAW029");
12
+ expect(waw029.description).toContain("DependsOn");
13
+ });
14
+
15
+ test("dangling DependsOn target → error", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyBucket: {
19
+ Type: "AWS::S3::Bucket",
20
+ DependsOn: "MyBukcet",
21
+ Properties: {},
22
+ },
23
+ },
24
+ });
25
+ const diags = checkInvalidDependsOn(ctx);
26
+ expect(diags).toHaveLength(1);
27
+ expect(diags[0].checkId).toBe("WAW029");
28
+ expect(diags[0].severity).toBe("error");
29
+ expect(diags[0].message).toContain("MyBukcet");
30
+ expect(diags[0].message).toContain("does not exist");
31
+ expect(diags[0].entity).toBe("MyBucket");
32
+ expect(diags[0].lexicon).toBe("aws");
33
+ });
34
+
35
+ test("self-referencing DependsOn → error", () => {
36
+ const ctx = makeCtx({
37
+ Resources: {
38
+ MyBucket: {
39
+ Type: "AWS::S3::Bucket",
40
+ DependsOn: "MyBucket",
41
+ Properties: {},
42
+ },
43
+ },
44
+ });
45
+ const diags = checkInvalidDependsOn(ctx);
46
+ expect(diags).toHaveLength(1);
47
+ expect(diags[0].checkId).toBe("WAW029");
48
+ expect(diags[0].severity).toBe("error");
49
+ expect(diags[0].message).toContain("itself");
50
+ expect(diags[0].entity).toBe("MyBucket");
51
+ });
52
+
53
+ test("valid DependsOn → no diagnostic", () => {
54
+ const ctx = makeCtx({
55
+ Resources: {
56
+ MyBucket: {
57
+ Type: "AWS::S3::Bucket",
58
+ Properties: {},
59
+ },
60
+ MyFunction: {
61
+ Type: "AWS::Lambda::Function",
62
+ DependsOn: "MyBucket",
63
+ Properties: {},
64
+ },
65
+ },
66
+ });
67
+ const diags = checkInvalidDependsOn(ctx);
68
+ expect(diags).toHaveLength(0);
69
+ });
70
+
71
+ test("string DependsOn (not array) → works", () => {
72
+ const ctx = makeCtx({
73
+ Resources: {
74
+ MyBucket: {
75
+ Type: "AWS::S3::Bucket",
76
+ DependsOn: "NonExistent",
77
+ Properties: {},
78
+ },
79
+ },
80
+ });
81
+ const diags = checkInvalidDependsOn(ctx);
82
+ expect(diags).toHaveLength(1);
83
+ expect(diags[0].message).toContain("NonExistent");
84
+ });
85
+
86
+ test("multiple invalid targets → multiple diagnostics", () => {
87
+ const ctx = makeCtx({
88
+ Resources: {
89
+ MyBucket: {
90
+ Type: "AWS::S3::Bucket",
91
+ DependsOn: ["Typo1", "Typo2"],
92
+ Properties: {},
93
+ },
94
+ },
95
+ });
96
+ const diags = checkInvalidDependsOn(ctx);
97
+ expect(diags).toHaveLength(2);
98
+ expect(diags[0].message).toContain("Typo1");
99
+ expect(diags[1].message).toContain("Typo2");
100
+ });
101
+
102
+ test("no DependsOn → no diagnostic", () => {
103
+ const ctx = makeCtx({
104
+ Resources: {
105
+ MyBucket: {
106
+ Type: "AWS::S3::Bucket",
107
+ Properties: {},
108
+ },
109
+ },
110
+ });
111
+ const diags = checkInvalidDependsOn(ctx);
112
+ expect(diags).toHaveLength(0);
113
+ });
114
+
115
+ test("empty Resources → no diagnostic", () => {
116
+ const ctx = makeCtx({ Resources: {} });
117
+ const diags = checkInvalidDependsOn(ctx);
118
+ expect(diags).toHaveLength(0);
119
+ });
120
+
121
+ test("empty DependsOn array → no diagnostic", () => {
122
+ const ctx = makeCtx({
123
+ Resources: {
124
+ MyBucket: {
125
+ Type: "AWS::S3::Bucket",
126
+ DependsOn: [],
127
+ Properties: {},
128
+ },
129
+ },
130
+ });
131
+ const diags = checkInvalidDependsOn(ctx);
132
+ expect(diags).toHaveLength(0);
133
+ });
134
+
135
+ test("mixed valid and invalid in same array", () => {
136
+ const ctx = makeCtx({
137
+ Resources: {
138
+ A: { Type: "AWS::S3::Bucket", Properties: {} },
139
+ B: {
140
+ Type: "AWS::Lambda::Function",
141
+ DependsOn: ["A", "NonExistent", "B"],
142
+ Properties: {},
143
+ },
144
+ },
145
+ });
146
+ const diags = checkInvalidDependsOn(ctx);
147
+ expect(diags).toHaveLength(2);
148
+ expect(diags[0].message).toContain("NonExistent");
149
+ expect(diags[0].message).toContain("does not exist");
150
+ expect(diags[1].message).toContain("itself");
151
+ });
152
+
153
+ test("no Resources key → no diagnostic", () => {
154
+ const ctx = makeCtx({ AWSTemplateFormatVersion: "2010-09-09" });
155
+ const diags = checkInvalidDependsOn(ctx);
156
+ expect(diags).toHaveLength(0);
157
+ });
158
+
159
+ test("multiple resources each with invalid DependsOn", () => {
160
+ const ctx = makeCtx({
161
+ Resources: {
162
+ A: {
163
+ Type: "AWS::S3::Bucket",
164
+ DependsOn: "Ghost1",
165
+ Properties: {},
166
+ },
167
+ B: {
168
+ Type: "AWS::Lambda::Function",
169
+ DependsOn: "Ghost2",
170
+ Properties: {},
171
+ },
172
+ },
173
+ });
174
+ const diags = checkInvalidDependsOn(ctx);
175
+ expect(diags).toHaveLength(2);
176
+ expect(diags[0].entity).toBe("A");
177
+ expect(diags[1].entity).toBe("B");
178
+ });
179
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * WAW029: Invalid DependsOn Target
3
+ *
4
+ * Detects two cases in the serialized template:
5
+ * - Dangling reference: DependsOn target not in template.Resources
6
+ * - Self-reference: Resource depends on itself
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate } from "./cf-refs";
11
+
12
+ export function checkInvalidDependsOn(ctx: PostSynthContext): PostSynthDiagnostic[] {
13
+ const diagnostics: PostSynthDiagnostic[] = [];
14
+
15
+ for (const [_lexicon, output] of ctx.outputs) {
16
+ const template = parseCFTemplate(output);
17
+ if (!template?.Resources) continue;
18
+
19
+ const resourceIds = new Set(Object.keys(template.Resources));
20
+
21
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
22
+ if (!resource.DependsOn) continue;
23
+
24
+ const deps = Array.isArray(resource.DependsOn)
25
+ ? resource.DependsOn
26
+ : [resource.DependsOn];
27
+
28
+ for (const dep of deps) {
29
+ if (typeof dep !== "string") continue;
30
+
31
+ if (dep === logicalId) {
32
+ diagnostics.push({
33
+ checkId: "WAW029",
34
+ severity: "error",
35
+ message: `Resource "${logicalId}" has a DependsOn on itself — self-references are invalid`,
36
+ entity: logicalId,
37
+ lexicon: "aws",
38
+ });
39
+ } else if (!resourceIds.has(dep)) {
40
+ diagnostics.push({
41
+ checkId: "WAW029",
42
+ severity: "error",
43
+ message: `Resource "${logicalId}" has DependsOn "${dep}" which does not exist in the template`,
44
+ entity: logicalId,
45
+ lexicon: "aws",
46
+ });
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ return diagnostics;
53
+ }
54
+
55
+ export const waw029: PostSynthCheck = {
56
+ id: "WAW029",
57
+ description: "Invalid DependsOn target — dangling reference or self-reference",
58
+
59
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
60
+ return checkInvalidDependsOn(ctx);
61
+ },
62
+ };