@intentius/chant-lexicon-aws 0.18.11 → 0.18.13

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 (126) hide show
  1. package/dist/composites/agentcore-agent.d.ts +74 -0
  2. package/dist/composites/agentcore-agent.d.ts.map +1 -0
  3. package/dist/composites/agentcore-trust-policy.d.ts +11 -0
  4. package/dist/composites/agentcore-trust-policy.d.ts.map +1 -0
  5. package/dist/composites/index.d.ts +4 -0
  6. package/dist/composites/index.d.ts.map +1 -1
  7. package/dist/composites/microvm-app.d.ts +116 -0
  8. package/dist/composites/microvm-app.d.ts.map +1 -0
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/integrity.json +22 -4
  12. package/dist/lint/audit-catalog.d.ts.map +1 -1
  13. package/dist/lint/post-synth/cf-refs.d.ts +14 -0
  14. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  15. package/dist/lint/post-synth/index.d.ts.map +1 -1
  16. package/dist/lint/post-synth/waw038.d.ts +12 -0
  17. package/dist/lint/post-synth/waw038.d.ts.map +1 -0
  18. package/dist/lint/post-synth/waw039.d.ts +11 -0
  19. package/dist/lint/post-synth/waw039.d.ts.map +1 -0
  20. package/dist/lint/post-synth/waw040.d.ts +13 -0
  21. package/dist/lint/post-synth/waw040.d.ts.map +1 -0
  22. package/dist/lint/post-synth/waw041.d.ts +10 -0
  23. package/dist/lint/post-synth/waw041.d.ts.map +1 -0
  24. package/dist/lint/post-synth/waw042.d.ts +12 -0
  25. package/dist/lint/post-synth/waw042.d.ts.map +1 -0
  26. package/dist/lint/post-synth/waw043.d.ts +11 -0
  27. package/dist/lint/post-synth/waw043.d.ts.map +1 -0
  28. package/dist/lint/post-synth/waw044.d.ts +18 -0
  29. package/dist/lint/post-synth/waw044.d.ts.map +1 -0
  30. package/dist/lint/post-synth/waw045.d.ts +11 -0
  31. package/dist/lint/post-synth/waw045.d.ts.map +1 -0
  32. package/dist/lint/post-synth/waw046.d.ts +12 -0
  33. package/dist/lint/post-synth/waw046.d.ts.map +1 -0
  34. package/dist/lint/post-synth/waw047.d.ts +11 -0
  35. package/dist/lint/post-synth/waw047.d.ts.map +1 -0
  36. package/dist/lint/post-synth/waw048.d.ts +11 -0
  37. package/dist/lint/post-synth/waw048.d.ts.map +1 -0
  38. package/dist/lint/post-synth/waw049.d.ts +21 -0
  39. package/dist/lint/post-synth/waw049.d.ts.map +1 -0
  40. package/dist/lint/post-synth/waw050.d.ts +11 -0
  41. package/dist/lint/post-synth/waw050.d.ts.map +1 -0
  42. package/dist/lint/post-synth/waw051.d.ts +11 -0
  43. package/dist/lint/post-synth/waw051.d.ts.map +1 -0
  44. package/dist/lint/post-synth/waw052.d.ts +12 -0
  45. package/dist/lint/post-synth/waw052.d.ts.map +1 -0
  46. package/dist/lint/post-synth/waw053.d.ts +10 -0
  47. package/dist/lint/post-synth/waw053.d.ts.map +1 -0
  48. package/dist/lint/post-synth/waw054.d.ts +11 -0
  49. package/dist/lint/post-synth/waw054.d.ts.map +1 -0
  50. package/dist/lint/post-synth/waw055.d.ts +11 -0
  51. package/dist/lint/post-synth/waw055.d.ts.map +1 -0
  52. package/dist/manifest.json +1 -1
  53. package/dist/op/activities/floci.d.ts +6 -0
  54. package/dist/op/activities/floci.d.ts.map +1 -1
  55. package/dist/plugin.d.ts.map +1 -1
  56. package/dist/rules/cf-refs.ts +23 -0
  57. package/dist/rules/waw038.ts +50 -0
  58. package/dist/rules/waw039.ts +51 -0
  59. package/dist/rules/waw040.ts +54 -0
  60. package/dist/rules/waw041.ts +48 -0
  61. package/dist/rules/waw042.ts +87 -0
  62. package/dist/rules/waw043.ts +57 -0
  63. package/dist/rules/waw044.ts +68 -0
  64. package/dist/rules/waw045.ts +58 -0
  65. package/dist/rules/waw046.ts +59 -0
  66. package/dist/rules/waw047.ts +50 -0
  67. package/dist/rules/waw048.ts +47 -0
  68. package/dist/rules/waw049.ts +100 -0
  69. package/dist/rules/waw050.ts +54 -0
  70. package/dist/rules/waw051.ts +49 -0
  71. package/dist/rules/waw052.ts +51 -0
  72. package/dist/rules/waw053.ts +53 -0
  73. package/dist/rules/waw054.ts +49 -0
  74. package/dist/rules/waw055.ts +49 -0
  75. package/dist/types/index.d.ts +11 -9
  76. package/package.json +3 -2
  77. package/src/composites/agentcore-agent.test.ts +188 -0
  78. package/src/composites/agentcore-agent.ts +195 -0
  79. package/src/composites/agentcore-trust-policy.ts +13 -0
  80. package/src/composites/index.ts +4 -0
  81. package/src/composites/microvm-app.test.ts +234 -0
  82. package/src/composites/microvm-app.ts +436 -0
  83. package/src/generated/index.d.ts +11 -9
  84. package/src/index.ts +2 -0
  85. package/src/lint/audit-catalog.ts +20 -0
  86. package/src/lint/post-synth/cf-refs.ts +23 -0
  87. package/src/lint/post-synth/index.ts +36 -0
  88. package/src/lint/post-synth/waw038.test.ts +78 -0
  89. package/src/lint/post-synth/waw038.ts +50 -0
  90. package/src/lint/post-synth/waw039.test.ts +72 -0
  91. package/src/lint/post-synth/waw039.ts +51 -0
  92. package/src/lint/post-synth/waw040.test.ts +71 -0
  93. package/src/lint/post-synth/waw040.ts +54 -0
  94. package/src/lint/post-synth/waw041.test.ts +72 -0
  95. package/src/lint/post-synth/waw041.ts +48 -0
  96. package/src/lint/post-synth/waw042.test.ts +138 -0
  97. package/src/lint/post-synth/waw042.ts +87 -0
  98. package/src/lint/post-synth/waw043.test.ts +84 -0
  99. package/src/lint/post-synth/waw043.ts +57 -0
  100. package/src/lint/post-synth/waw044.test.ts +112 -0
  101. package/src/lint/post-synth/waw044.ts +68 -0
  102. package/src/lint/post-synth/waw045.test.ts +101 -0
  103. package/src/lint/post-synth/waw045.ts +58 -0
  104. package/src/lint/post-synth/waw046.test.ts +112 -0
  105. package/src/lint/post-synth/waw046.ts +59 -0
  106. package/src/lint/post-synth/waw047.test.ts +64 -0
  107. package/src/lint/post-synth/waw047.ts +50 -0
  108. package/src/lint/post-synth/waw048.test.ts +77 -0
  109. package/src/lint/post-synth/waw048.ts +47 -0
  110. package/src/lint/post-synth/waw049.test.ts +139 -0
  111. package/src/lint/post-synth/waw049.ts +100 -0
  112. package/src/lint/post-synth/waw050.test.ts +75 -0
  113. package/src/lint/post-synth/waw050.ts +54 -0
  114. package/src/lint/post-synth/waw051.test.ts +78 -0
  115. package/src/lint/post-synth/waw051.ts +49 -0
  116. package/src/lint/post-synth/waw052.test.ts +54 -0
  117. package/src/lint/post-synth/waw052.ts +51 -0
  118. package/src/lint/post-synth/waw053.test.ts +59 -0
  119. package/src/lint/post-synth/waw053.ts +53 -0
  120. package/src/lint/post-synth/waw054.test.ts +49 -0
  121. package/src/lint/post-synth/waw054.ts +49 -0
  122. package/src/lint/post-synth/waw055.test.ts +43 -0
  123. package/src/lint/post-synth/waw055.ts +49 -0
  124. package/src/op/activities/floci.test.ts +26 -0
  125. package/src/op/activities/floci.ts +25 -9
  126. package/src/plugin.ts +3 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * WAW042: S3 Bucket Missing TLS-Only Policy
3
+ *
4
+ * Flags S3 buckets without a companion BucketPolicy that denies requests made
5
+ * over plaintext (a Deny statement keyed on `aws:SecureTransport` being
6
+ * false). Encryption at rest is WAW006; the public-access block is WAW018;
7
+ * this covers encryption in transit.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { parseCFTemplate, findResourceRefs, type CFResource } from "./cf-refs";
12
+
13
+ function statementDeniesInsecureTransport(stmt: Record<string, unknown>): boolean {
14
+ if (stmt.Effect !== "Deny") return false;
15
+
16
+ const condition = stmt.Condition;
17
+ if (typeof condition !== "object" || condition === null) return false;
18
+
19
+ const cond = condition as Record<string, unknown>;
20
+ for (const operator of ["Bool", "NumericLessThan"]) {
21
+ const clause = cond[operator];
22
+ if (typeof clause !== "object" || clause === null) continue;
23
+ const value = (clause as Record<string, unknown>)["aws:SecureTransport"];
24
+ if (value === "false" || value === false) return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ /** Logical ids of every bucket a BucketPolicy's `Bucket` property resolves to. */
30
+ function bucketPolicyTargets(resource: CFResource): Set<string> {
31
+ const props = resource.Properties ?? {};
32
+ return findResourceRefs(props.Bucket);
33
+ }
34
+
35
+ export function checkS3TlsOnlyPolicy(ctx: PostSynthContext): PostSynthDiagnostic[] {
36
+ const diagnostics: PostSynthDiagnostic[] = [];
37
+
38
+ for (const [_lexicon, output] of ctx.outputs) {
39
+ const template = parseCFTemplate(output);
40
+ if (!template?.Resources) continue;
41
+
42
+ const bucketsWithTlsPolicy = new Set<string>();
43
+
44
+ for (const resource of Object.values(template.Resources)) {
45
+ if (resource.Type !== "AWS::S3::BucketPolicy") continue;
46
+
47
+ const props = resource.Properties ?? {};
48
+ const policyDoc = props.PolicyDocument;
49
+ if (typeof policyDoc !== "object" || policyDoc === null) continue;
50
+ const statements = (policyDoc as Record<string, unknown>).Statement;
51
+ if (!Array.isArray(statements)) continue;
52
+
53
+ const hasTlsDeny = statements.some(
54
+ (stmt) => typeof stmt === "object" && stmt !== null && statementDeniesInsecureTransport(stmt as Record<string, unknown>),
55
+ );
56
+ if (!hasTlsDeny) continue;
57
+
58
+ for (const bucketId of bucketPolicyTargets(resource)) {
59
+ bucketsWithTlsPolicy.add(bucketId);
60
+ }
61
+ }
62
+
63
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
64
+ if (resource.Type !== "AWS::S3::Bucket") continue;
65
+ if (bucketsWithTlsPolicy.has(logicalId)) continue;
66
+
67
+ diagnostics.push({
68
+ checkId: "WAW042",
69
+ severity: "error",
70
+ message: `S3 bucket "${logicalId}" has no bucket policy denying non-TLS requests — add a Deny statement on aws:SecureTransport = false`,
71
+ entity: logicalId,
72
+ lexicon: "aws",
73
+ });
74
+ }
75
+ }
76
+
77
+ return diagnostics;
78
+ }
79
+
80
+ export const waw042: PostSynthCheck = {
81
+ id: "WAW042",
82
+ description: "S3 bucket missing a TLS-only bucket policy — deny requests over plaintext",
83
+
84
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
85
+ return checkS3TlsOnlyPolicy(ctx);
86
+ },
87
+ };
@@ -0,0 +1,84 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw043, checkKmsKeyRotation } from "./waw043";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW043: KMS Key Rotation Disabled", () => {
10
+ test("check metadata", () => {
11
+ expect(waw043.id).toBe("WAW043");
12
+ expect(waw043.description).toContain("rotation");
13
+ });
14
+
15
+ test("flags a key missing EnableKeyRotation", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyKey: { Type: "AWS::KMS::Key", Properties: { Description: "app key" } },
19
+ },
20
+ });
21
+ const diags = checkKmsKeyRotation(ctx);
22
+ expect(diags).toHaveLength(1);
23
+ expect(diags[0].checkId).toBe("WAW043");
24
+ expect(diags[0].severity).toBe("warning");
25
+ });
26
+
27
+ test("flags a key with EnableKeyRotation: false", () => {
28
+ const ctx = makeCtx({
29
+ Resources: {
30
+ MyKey: { Type: "AWS::KMS::Key", Properties: { EnableKeyRotation: false } },
31
+ },
32
+ });
33
+ expect(checkKmsKeyRotation(ctx)).toHaveLength(1);
34
+ });
35
+
36
+ test("no diagnostic when EnableKeyRotation: true", () => {
37
+ const ctx = makeCtx({
38
+ Resources: {
39
+ MyKey: { Type: "AWS::KMS::Key", Properties: { EnableKeyRotation: true } },
40
+ },
41
+ });
42
+ expect(checkKmsKeyRotation(ctx)).toHaveLength(0);
43
+ });
44
+
45
+ test("skips asymmetric keys (non-default KeySpec) even without rotation", () => {
46
+ const ctx = makeCtx({
47
+ Resources: {
48
+ MyKey: {
49
+ Type: "AWS::KMS::Key",
50
+ Properties: { KeySpec: "RSA_2048", KeyUsage: "SIGN_VERIFY" },
51
+ },
52
+ },
53
+ });
54
+ expect(checkKmsKeyRotation(ctx)).toHaveLength(0);
55
+ });
56
+
57
+ test("still flags an explicit SYMMETRIC_DEFAULT KeySpec without rotation", () => {
58
+ const ctx = makeCtx({
59
+ Resources: {
60
+ MyKey: {
61
+ Type: "AWS::KMS::Key",
62
+ Properties: { KeySpec: "SYMMETRIC_DEFAULT" },
63
+ },
64
+ },
65
+ });
66
+ expect(checkKmsKeyRotation(ctx)).toHaveLength(1);
67
+ });
68
+
69
+ test("skips intrinsic value for EnableKeyRotation", () => {
70
+ const ctx = makeCtx({
71
+ Resources: {
72
+ MyKey: { Type: "AWS::KMS::Key", Properties: { EnableKeyRotation: { Ref: "RotateParam" } } },
73
+ },
74
+ });
75
+ expect(checkKmsKeyRotation(ctx)).toHaveLength(0);
76
+ });
77
+
78
+ test("no diagnostic for non-KMS resources", () => {
79
+ const ctx = makeCtx({
80
+ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } },
81
+ });
82
+ expect(checkKmsKeyRotation(ctx)).toHaveLength(0);
83
+ });
84
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * WAW043: KMS Key Rotation Disabled
3
+ *
4
+ * Flags customer-managed KMS keys without EnableKeyRotation: true. Skips
5
+ * asymmetric/HMAC keys (a non-default KeySpec) — AWS does not support
6
+ * automatic rotation for those.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate, isIntrinsic } from "./cf-refs";
11
+
12
+ function supportsRotation(props: Record<string, unknown>): boolean {
13
+ const keySpec = props.KeySpec;
14
+ if (keySpec === undefined) return true; // default is SYMMETRIC_DEFAULT
15
+ if (isIntrinsic(keySpec)) return false; // can't statically verify — don't guess
16
+ return keySpec === "SYMMETRIC_DEFAULT";
17
+ }
18
+
19
+ export function checkKmsKeyRotation(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ for (const [_lexicon, output] of ctx.outputs) {
23
+ const template = parseCFTemplate(output);
24
+ if (!template?.Resources) continue;
25
+
26
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
27
+ if (resource.Type !== "AWS::KMS::Key") continue;
28
+
29
+ const props = resource.Properties ?? {};
30
+ if (!supportsRotation(props)) continue;
31
+
32
+ const rotation = props.EnableKeyRotation;
33
+ if (isIntrinsic(rotation)) continue;
34
+
35
+ if (rotation !== true) {
36
+ diagnostics.push({
37
+ checkId: "WAW043",
38
+ severity: "warning",
39
+ message: `KMS key "${logicalId}" does not have EnableKeyRotation: true — enable automatic annual key rotation`,
40
+ entity: logicalId,
41
+ lexicon: "aws",
42
+ });
43
+ }
44
+ }
45
+ }
46
+
47
+ return diagnostics;
48
+ }
49
+
50
+ export const waw043: PostSynthCheck = {
51
+ id: "WAW043",
52
+ description: "KMS customer-managed key does not have automatic key rotation enabled",
53
+
54
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
55
+ return checkKmsKeyRotation(ctx);
56
+ },
57
+ };
@@ -0,0 +1,112 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw044, checkAlbHttpRedirect } from "./waw044";
4
+
5
+ function makeCtx(template: object, env?: string) {
6
+ return { ...createPostSynthContext({ aws: template }), env };
7
+ }
8
+
9
+ describe("WAW044: ALB HTTP Listener Not Redirecting To HTTPS (full tier)", () => {
10
+ test("check metadata", () => {
11
+ expect(waw044.id).toBe("WAW044");
12
+ expect(waw044.description).toContain("HTTPS");
13
+ });
14
+
15
+ const template = {
16
+ Resources: {
17
+ HttpListener: {
18
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
19
+ Properties: {
20
+ Protocol: "HTTP",
21
+ Port: 80,
22
+ DefaultActions: [{ Type: "forward", TargetGroupArn: "arn:aws:...:tg" }],
23
+ },
24
+ },
25
+ },
26
+ };
27
+
28
+ test("warns (not errors) on the light tier — no env set", () => {
29
+ const diags = checkAlbHttpRedirect(makeCtx(template));
30
+ expect(diags).toHaveLength(1);
31
+ expect(diags[0].checkId).toBe("WAW044");
32
+ expect(diags[0].severity).toBe("warning");
33
+ });
34
+
35
+ test("warns on a non-production env", () => {
36
+ const diags = checkAlbHttpRedirect(makeCtx(template, "dev"));
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].severity).toBe("warning");
39
+ });
40
+
41
+ test("errors on the full/production tier", () => {
42
+ const diags = checkAlbHttpRedirect(makeCtx(template, "prod"));
43
+ expect(diags).toHaveLength(1);
44
+ expect(diags[0].severity).toBe("error");
45
+ });
46
+
47
+ test("errors on env: production", () => {
48
+ const diags = checkAlbHttpRedirect(makeCtx(template, "production"));
49
+ expect(diags[0].severity).toBe("error");
50
+ });
51
+
52
+ test("errors on env: full", () => {
53
+ const diags = checkAlbHttpRedirect(makeCtx(template, "full"));
54
+ expect(diags[0].severity).toBe("error");
55
+ });
56
+
57
+ test("no diagnostic when the HTTP listener redirects to HTTPS, even in prod", () => {
58
+ const ctx = makeCtx(
59
+ {
60
+ Resources: {
61
+ HttpListener: {
62
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
63
+ Properties: {
64
+ Protocol: "HTTP",
65
+ Port: 80,
66
+ DefaultActions: [
67
+ { Type: "redirect", RedirectConfig: { Protocol: "HTTPS", Port: "443", StatusCode: "HTTP_301" } },
68
+ ],
69
+ },
70
+ },
71
+ },
72
+ },
73
+ "prod",
74
+ );
75
+ expect(checkAlbHttpRedirect(ctx)).toHaveLength(0);
76
+ });
77
+
78
+ test("no diagnostic for an HTTPS listener", () => {
79
+ const ctx = makeCtx({
80
+ Resources: {
81
+ HttpsListener: {
82
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
83
+ Properties: {
84
+ Protocol: "HTTPS",
85
+ Port: 443,
86
+ DefaultActions: [{ Type: "forward", TargetGroupArn: "arn:aws:...:tg" }],
87
+ },
88
+ },
89
+ },
90
+ });
91
+ expect(checkAlbHttpRedirect(ctx)).toHaveLength(0);
92
+ });
93
+
94
+ test("skips intrinsic Protocol values", () => {
95
+ const ctx = makeCtx({
96
+ Resources: {
97
+ Listener: {
98
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
99
+ Properties: { Protocol: { Ref: "ProtocolParam" }, DefaultActions: [] },
100
+ },
101
+ },
102
+ });
103
+ expect(checkAlbHttpRedirect(ctx)).toHaveLength(0);
104
+ });
105
+
106
+ test("no diagnostic for non-Listener resources", () => {
107
+ const ctx = makeCtx({
108
+ Resources: { MyLb: { Type: "AWS::ElasticLoadBalancingV2::LoadBalancer", Properties: {} } },
109
+ });
110
+ expect(checkAlbHttpRedirect(ctx)).toHaveLength(0);
111
+ });
112
+ });
@@ -0,0 +1,68 @@
1
+ /**
2
+ * WAW044: ALB HTTP Listener Not Redirecting To HTTPS (full tier)
3
+ *
4
+ * Flags load balancer Listener resources on Protocol: HTTP whose
5
+ * DefaultActions don't redirect to HTTPS — the listener serves plaintext
6
+ * traffic instead of upgrading it. Access logging is WAW024; this covers
7
+ * transport security on the listener itself.
8
+ *
9
+ * Tier-aware (#917), same seam as WAW040/WAW052 (#894): a relaxed "light"
10
+ * stack (local/Floci, or no --env/ownership.env set) only warns, since the
11
+ * light tier is intentionally HTTP-only with no ACM cert to redirect to; the
12
+ * strict "full"/production tier (ctx.env === "prod"/"production"/"full", the
13
+ * existing #201 env seam — see isFullTierEnv) fails the build.
14
+ */
15
+
16
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
17
+ import { parseCFTemplate, isIntrinsic, isFullTierEnv } from "./cf-refs";
18
+
19
+ function redirectsToHttps(actions: unknown): boolean {
20
+ if (!Array.isArray(actions)) return false;
21
+ return actions.some((action) => {
22
+ if (typeof action !== "object" || action === null) return false;
23
+ const a = action as Record<string, unknown>;
24
+ if (a.Type !== "redirect") return false;
25
+ const redirectConfig = a.RedirectConfig;
26
+ if (typeof redirectConfig !== "object" || redirectConfig === null) return false;
27
+ return (redirectConfig as Record<string, unknown>).Protocol === "HTTPS";
28
+ });
29
+ }
30
+
31
+ export function checkAlbHttpRedirect(ctx: PostSynthContext): PostSynthDiagnostic[] {
32
+ const diagnostics: PostSynthDiagnostic[] = [];
33
+ const severity = isFullTierEnv(ctx.env) ? "error" : "warning";
34
+
35
+ for (const [_lexicon, output] of ctx.outputs) {
36
+ const template = parseCFTemplate(output);
37
+ if (!template?.Resources) continue;
38
+
39
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
40
+ if (resource.Type !== "AWS::ElasticLoadBalancingV2::Listener") continue;
41
+
42
+ const props = resource.Properties ?? {};
43
+ const protocol = props.Protocol;
44
+ if (isIntrinsic(protocol) || protocol !== "HTTP") continue;
45
+
46
+ if (!redirectsToHttps(props.DefaultActions)) {
47
+ diagnostics.push({
48
+ checkId: "WAW044",
49
+ severity,
50
+ message: `Listener "${logicalId}" is HTTP and does not redirect to HTTPS — add a redirect DefaultAction (Protocol: HTTPS) on the full/production tier`,
51
+ entity: logicalId,
52
+ lexicon: "aws",
53
+ });
54
+ }
55
+ }
56
+ }
57
+
58
+ return diagnostics;
59
+ }
60
+
61
+ export const waw044: PostSynthCheck = {
62
+ id: "WAW044",
63
+ description: "ALB HTTP listener does not redirect to HTTPS (full tier)",
64
+
65
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
66
+ return checkAlbHttpRedirect(ctx);
67
+ },
68
+ };
@@ -0,0 +1,101 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw045, checkAlbTlsPolicy } from "./waw045";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW045: ALB Weak Or Missing TLS Policy", () => {
10
+ test("check metadata", () => {
11
+ expect(waw045.id).toBe("WAW045");
12
+ expect(waw045.description).toContain("TLS");
13
+ });
14
+
15
+ test("flags an HTTPS listener with no SslPolicy", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ HttpsListener: {
19
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
20
+ Properties: { Protocol: "HTTPS", Port: 443 },
21
+ },
22
+ },
23
+ });
24
+ const diags = checkAlbTlsPolicy(ctx);
25
+ expect(diags).toHaveLength(1);
26
+ expect(diags[0].checkId).toBe("WAW045");
27
+ expect(diags[0].severity).toBe("error");
28
+ });
29
+
30
+ test("flags a legacy SslPolicy (TLS 1.0 default)", () => {
31
+ const ctx = makeCtx({
32
+ Resources: {
33
+ HttpsListener: {
34
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
35
+ Properties: { Protocol: "HTTPS", SslPolicy: "ELBSecurityPolicy-2016-08" },
36
+ },
37
+ },
38
+ });
39
+ expect(checkAlbTlsPolicy(ctx)).toHaveLength(1);
40
+ });
41
+
42
+ test("flags an explicit TLS 1.1 policy", () => {
43
+ const ctx = makeCtx({
44
+ Resources: {
45
+ HttpsListener: {
46
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
47
+ Properties: { Protocol: "HTTPS", SslPolicy: "ELBSecurityPolicy-TLS-1-1-2017-01" },
48
+ },
49
+ },
50
+ });
51
+ expect(checkAlbTlsPolicy(ctx)).toHaveLength(1);
52
+ });
53
+
54
+ test("no diagnostic for a modern TLS 1.2 policy", () => {
55
+ const ctx = makeCtx({
56
+ Resources: {
57
+ HttpsListener: {
58
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
59
+ Properties: { Protocol: "HTTPS", SslPolicy: "ELBSecurityPolicy-TLS-1-2-2017-01" },
60
+ },
61
+ },
62
+ });
63
+ expect(checkAlbTlsPolicy(ctx)).toHaveLength(0);
64
+ });
65
+
66
+ test("no diagnostic for a modern TLS13 policy on a TLS (NLB) listener", () => {
67
+ const ctx = makeCtx({
68
+ Resources: {
69
+ TlsListener: {
70
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
71
+ Properties: { Protocol: "TLS", SslPolicy: "ELBSecurityPolicy-TLS13-1-2-2021-06" },
72
+ },
73
+ },
74
+ });
75
+ expect(checkAlbTlsPolicy(ctx)).toHaveLength(0);
76
+ });
77
+
78
+ test("no diagnostic for a plain HTTP listener (not in scope)", () => {
79
+ const ctx = makeCtx({
80
+ Resources: {
81
+ HttpListener: {
82
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
83
+ Properties: { Protocol: "HTTP" },
84
+ },
85
+ },
86
+ });
87
+ expect(checkAlbTlsPolicy(ctx)).toHaveLength(0);
88
+ });
89
+
90
+ test("skips intrinsic SslPolicy values", () => {
91
+ const ctx = makeCtx({
92
+ Resources: {
93
+ HttpsListener: {
94
+ Type: "AWS::ElasticLoadBalancingV2::Listener",
95
+ Properties: { Protocol: "HTTPS", SslPolicy: { Ref: "PolicyParam" } },
96
+ },
97
+ },
98
+ });
99
+ expect(checkAlbTlsPolicy(ctx)).toHaveLength(0);
100
+ });
101
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * WAW045: ALB Weak Or Missing TLS Policy
3
+ *
4
+ * Flags HTTPS/TLS listeners without a modern SslPolicy (TLS 1.2+). The
5
+ * account-wide default (`ELBSecurityPolicy-2016-08`) still allows TLS 1.0, so
6
+ * a missing SslPolicy is flagged the same as an explicit legacy one.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate, isIntrinsic } from "./cf-refs";
11
+
12
+ const SECURE_PROTOCOLS = new Set(["HTTPS", "TLS"]);
13
+
14
+ /** AWS's predefined TLS 1.2+ policies all carry a "-1-2-" or "-1-3-" segment (FS/TLS13 variants included). */
15
+ function isModernTlsPolicy(policy: string): boolean {
16
+ return /-1-2-|-1-3-/.test(policy);
17
+ }
18
+
19
+ export function checkAlbTlsPolicy(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+
22
+ for (const [_lexicon, output] of ctx.outputs) {
23
+ const template = parseCFTemplate(output);
24
+ if (!template?.Resources) continue;
25
+
26
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
27
+ if (resource.Type !== "AWS::ElasticLoadBalancingV2::Listener") continue;
28
+
29
+ const props = resource.Properties ?? {};
30
+ const protocol = props.Protocol;
31
+ if (isIntrinsic(protocol) || typeof protocol !== "string" || !SECURE_PROTOCOLS.has(protocol)) continue;
32
+
33
+ const sslPolicy = props.SslPolicy;
34
+ if (isIntrinsic(sslPolicy)) continue;
35
+
36
+ if (typeof sslPolicy !== "string" || !isModernTlsPolicy(sslPolicy)) {
37
+ diagnostics.push({
38
+ checkId: "WAW045",
39
+ severity: "error",
40
+ message: `Listener "${logicalId}" (${protocol}) ${sslPolicy ? `uses SslPolicy "${sslPolicy}"` : "has no SslPolicy set"} — use a TLS 1.2+ predefined policy (e.g. ELBSecurityPolicy-TLS13-1-2-2021-06)`,
41
+ entity: logicalId,
42
+ lexicon: "aws",
43
+ });
44
+ }
45
+ }
46
+ }
47
+
48
+ return diagnostics;
49
+ }
50
+
51
+ export const waw045: PostSynthCheck = {
52
+ id: "WAW045",
53
+ description: "ALB HTTPS/TLS listener does not use a modern (TLS 1.2+) SslPolicy",
54
+
55
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
56
+ return checkAlbTlsPolicy(ctx);
57
+ },
58
+ };
@@ -0,0 +1,112 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw046, checkEcsPlaintextSecrets } from "./waw046";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW046: ECS Secret-Looking Value In Plaintext Environment", () => {
10
+ test("check metadata", () => {
11
+ expect(waw046.id).toBe("WAW046");
12
+ expect(waw046.description).toContain("Secrets");
13
+ });
14
+
15
+ test("flags a DB_PASSWORD environment variable", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyTask: {
19
+ Type: "AWS::ECS::TaskDefinition",
20
+ Properties: {
21
+ ContainerDefinitions: [
22
+ { Name: "app", Environment: [{ Name: "DB_PASSWORD", Value: "hunter2" }] },
23
+ ],
24
+ },
25
+ },
26
+ },
27
+ });
28
+ const diags = checkEcsPlaintextSecrets(ctx);
29
+ expect(diags).toHaveLength(1);
30
+ expect(diags[0].checkId).toBe("WAW046");
31
+ expect(diags[0].message).toContain("DB_PASSWORD");
32
+ });
33
+
34
+ test("flags API_KEY, TOKEN, and CREDENTIAL variants", () => {
35
+ const ctx = makeCtx({
36
+ Resources: {
37
+ MyTask: {
38
+ Type: "AWS::ECS::TaskDefinition",
39
+ Properties: {
40
+ ContainerDefinitions: [
41
+ {
42
+ Name: "app",
43
+ Environment: [
44
+ { Name: "STRIPE_API_KEY", Value: "sk_live_x" },
45
+ { Name: "AUTH_TOKEN", Value: "x" },
46
+ { Name: "AWS_CREDENTIAL_BLOB", Value: "x" },
47
+ ],
48
+ },
49
+ ],
50
+ },
51
+ },
52
+ },
53
+ });
54
+ expect(checkEcsPlaintextSecrets(ctx)).toHaveLength(3);
55
+ });
56
+
57
+ test("no diagnostic for non-secret-looking environment names", () => {
58
+ const ctx = makeCtx({
59
+ Resources: {
60
+ MyTask: {
61
+ Type: "AWS::ECS::TaskDefinition",
62
+ Properties: {
63
+ ContainerDefinitions: [
64
+ { Name: "app", Environment: [{ Name: "LOG_LEVEL", Value: "info" }] },
65
+ ],
66
+ },
67
+ },
68
+ },
69
+ });
70
+ expect(checkEcsPlaintextSecrets(ctx)).toHaveLength(0);
71
+ });
72
+
73
+ test("no diagnostic when the secret is passed via Secrets instead", () => {
74
+ const ctx = makeCtx({
75
+ Resources: {
76
+ MyTask: {
77
+ Type: "AWS::ECS::TaskDefinition",
78
+ Properties: {
79
+ ContainerDefinitions: [
80
+ {
81
+ Name: "app",
82
+ Secrets: [{ Name: "DB_PASSWORD", ValueFrom: "arn:aws:secretsmanager:...:secret:db-pw" }],
83
+ },
84
+ ],
85
+ },
86
+ },
87
+ },
88
+ });
89
+ expect(checkEcsPlaintextSecrets(ctx)).toHaveLength(0);
90
+ });
91
+
92
+ test("skips intrinsic Name values", () => {
93
+ const ctx = makeCtx({
94
+ Resources: {
95
+ MyTask: {
96
+ Type: "AWS::ECS::TaskDefinition",
97
+ Properties: {
98
+ ContainerDefinitions: [{ Name: "app", Environment: [{ Name: { Ref: "NameParam" }, Value: "x" }] }],
99
+ },
100
+ },
101
+ },
102
+ });
103
+ expect(checkEcsPlaintextSecrets(ctx)).toHaveLength(0);
104
+ });
105
+
106
+ test("no diagnostic for non-TaskDefinition resources", () => {
107
+ const ctx = makeCtx({
108
+ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } },
109
+ });
110
+ expect(checkEcsPlaintextSecrets(ctx)).toHaveLength(0);
111
+ });
112
+ });