@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,59 @@
1
+ /**
2
+ * WAW046: ECS Secret-Looking Value In Plaintext Environment
3
+ *
4
+ * Flags ECS TaskDefinition container `Environment` entries whose name looks
5
+ * like a credential (password/secret/token/key/credential) — those values are
6
+ * visible in the task definition and console. They should be passed via
7
+ * `Secrets` (Secrets Manager / SSM Parameter Store) instead.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { parseCFTemplate, getContainerDefinitions, isIntrinsic } from "./cf-refs";
12
+
13
+ const SECRET_NAME_PATTERN = /password|secret|token|api[_-]?key|credential/i;
14
+
15
+ export function checkEcsPlaintextSecrets(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 (resource.Type !== "AWS::ECS::TaskDefinition") continue;
24
+
25
+ for (const container of getContainerDefinitions(resource)) {
26
+ const env = container.Environment;
27
+ if (!Array.isArray(env)) continue;
28
+
29
+ for (const entry of env) {
30
+ if (typeof entry !== "object" || entry === null) continue;
31
+ const name = (entry as Record<string, unknown>).Name;
32
+ if (isIntrinsic(name) || typeof name !== "string") continue;
33
+
34
+ if (SECRET_NAME_PATTERN.test(name)) {
35
+ const containerName = typeof container.Name === "string" ? container.Name : "<unnamed>";
36
+ diagnostics.push({
37
+ checkId: "WAW046",
38
+ severity: "error",
39
+ message: `TaskDefinition "${logicalId}" container "${containerName}" passes "${name}" via plaintext Environment — use Secrets (Secrets Manager/SSM) instead`,
40
+ entity: logicalId,
41
+ lexicon: "aws",
42
+ });
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ return diagnostics;
50
+ }
51
+
52
+ export const waw046: PostSynthCheck = {
53
+ id: "WAW046",
54
+ description: "ECS container passes a secret-looking value via plaintext Environment instead of Secrets",
55
+
56
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
57
+ return checkEcsPlaintextSecrets(ctx);
58
+ },
59
+ };
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw047, checkEcsPrivilegedContainer } from "./waw047";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW047: ECS Privileged Container", () => {
10
+ test("check metadata", () => {
11
+ expect(waw047.id).toBe("WAW047");
12
+ expect(waw047.description).toContain("privileged");
13
+ });
14
+
15
+ test("flags a container with Privileged: true", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyTask: {
19
+ Type: "AWS::ECS::TaskDefinition",
20
+ Properties: {
21
+ ContainerDefinitions: [{ Name: "app", Privileged: true }],
22
+ },
23
+ },
24
+ },
25
+ });
26
+ const diags = checkEcsPrivilegedContainer(ctx);
27
+ expect(diags).toHaveLength(1);
28
+ expect(diags[0].checkId).toBe("WAW047");
29
+ expect(diags[0].message).toContain("app");
30
+ });
31
+
32
+ test("no diagnostic when Privileged is false or unset", () => {
33
+ const ctx = makeCtx({
34
+ Resources: {
35
+ MyTask: {
36
+ Type: "AWS::ECS::TaskDefinition",
37
+ Properties: {
38
+ ContainerDefinitions: [{ Name: "app", Privileged: false }, { Name: "sidecar" }],
39
+ },
40
+ },
41
+ },
42
+ });
43
+ expect(checkEcsPrivilegedContainer(ctx)).toHaveLength(0);
44
+ });
45
+
46
+ test("skips intrinsic Privileged values", () => {
47
+ const ctx = makeCtx({
48
+ Resources: {
49
+ MyTask: {
50
+ Type: "AWS::ECS::TaskDefinition",
51
+ Properties: { ContainerDefinitions: [{ Name: "app", Privileged: { Ref: "PrivParam" } }] },
52
+ },
53
+ },
54
+ });
55
+ expect(checkEcsPrivilegedContainer(ctx)).toHaveLength(0);
56
+ });
57
+
58
+ test("no diagnostic for non-TaskDefinition resources", () => {
59
+ const ctx = makeCtx({
60
+ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } },
61
+ });
62
+ expect(checkEcsPrivilegedContainer(ctx)).toHaveLength(0);
63
+ });
64
+ });
@@ -0,0 +1,50 @@
1
+ /**
2
+ * WAW047: ECS Privileged Container
3
+ *
4
+ * Flags ECS TaskDefinition containers running with Privileged: true — the
5
+ * container gets elevated access to the host, equivalent to root on the EC2
6
+ * instance (and rejected outright on Fargate).
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate, getContainerDefinitions, isIntrinsic } from "./cf-refs";
11
+
12
+ export function checkEcsPrivilegedContainer(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
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
20
+ if (resource.Type !== "AWS::ECS::TaskDefinition") continue;
21
+
22
+ for (const container of getContainerDefinitions(resource)) {
23
+ const privileged = container.Privileged;
24
+ if (isIntrinsic(privileged)) continue;
25
+
26
+ if (privileged === true) {
27
+ const containerName = typeof container.Name === "string" ? container.Name : "<unnamed>";
28
+ diagnostics.push({
29
+ checkId: "WAW047",
30
+ severity: "error",
31
+ message: `TaskDefinition "${logicalId}" container "${containerName}" runs with Privileged: true — drop elevated host access`,
32
+ entity: logicalId,
33
+ lexicon: "aws",
34
+ });
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ return diagnostics;
41
+ }
42
+
43
+ export const waw047: PostSynthCheck = {
44
+ id: "WAW047",
45
+ description: "ECS container runs privileged — drop elevated host access",
46
+
47
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
48
+ return checkEcsPrivilegedContainer(ctx);
49
+ },
50
+ };
@@ -0,0 +1,77 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw048, checkEcsLogConfiguration } from "./waw048";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW048: ECS Missing Log Configuration", () => {
10
+ test("check metadata", () => {
11
+ expect(waw048.id).toBe("WAW048");
12
+ expect(waw048.description).toContain("LogConfiguration");
13
+ });
14
+
15
+ test("flags a container without LogConfiguration", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyTask: {
19
+ Type: "AWS::ECS::TaskDefinition",
20
+ Properties: { ContainerDefinitions: [{ Name: "app" }] },
21
+ },
22
+ },
23
+ });
24
+ const diags = checkEcsLogConfiguration(ctx);
25
+ expect(diags).toHaveLength(1);
26
+ expect(diags[0].checkId).toBe("WAW048");
27
+ expect(diags[0].severity).toBe("warning");
28
+ });
29
+
30
+ test("no diagnostic when LogConfiguration is present", () => {
31
+ const ctx = makeCtx({
32
+ Resources: {
33
+ MyTask: {
34
+ Type: "AWS::ECS::TaskDefinition",
35
+ Properties: {
36
+ ContainerDefinitions: [
37
+ {
38
+ Name: "app",
39
+ LogConfiguration: {
40
+ LogDriver: "awslogs",
41
+ Options: { "awslogs-group": "/ecs/app", "awslogs-region": "us-east-1", "awslogs-stream-prefix": "app" },
42
+ },
43
+ },
44
+ ],
45
+ },
46
+ },
47
+ },
48
+ });
49
+ expect(checkEcsLogConfiguration(ctx)).toHaveLength(0);
50
+ });
51
+
52
+ test("flags each container in a multi-container task independently", () => {
53
+ const ctx = makeCtx({
54
+ Resources: {
55
+ MyTask: {
56
+ Type: "AWS::ECS::TaskDefinition",
57
+ Properties: {
58
+ ContainerDefinitions: [
59
+ { Name: "app", LogConfiguration: { LogDriver: "awslogs" } },
60
+ { Name: "sidecar" },
61
+ ],
62
+ },
63
+ },
64
+ },
65
+ });
66
+ const diags = checkEcsLogConfiguration(ctx);
67
+ expect(diags).toHaveLength(1);
68
+ expect(diags[0].message).toContain("sidecar");
69
+ });
70
+
71
+ test("no diagnostic for non-TaskDefinition resources", () => {
72
+ const ctx = makeCtx({
73
+ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } },
74
+ });
75
+ expect(checkEcsLogConfiguration(ctx)).toHaveLength(0);
76
+ });
77
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * WAW048: ECS Missing Log Configuration
3
+ *
4
+ * Flags ECS TaskDefinition containers with no LogConfiguration — container
5
+ * stdout/stderr is discarded, leaving no audit trail. Mirrors WAW024 (ALB
6
+ * access logging): advisory, not a hard block.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate, getContainerDefinitions } from "./cf-refs";
11
+
12
+ export function checkEcsLogConfiguration(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
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
20
+ if (resource.Type !== "AWS::ECS::TaskDefinition") continue;
21
+
22
+ for (const container of getContainerDefinitions(resource)) {
23
+ if (container.LogConfiguration === undefined) {
24
+ const containerName = typeof container.Name === "string" ? container.Name : "<unnamed>";
25
+ diagnostics.push({
26
+ checkId: "WAW048",
27
+ severity: "warning",
28
+ message: `TaskDefinition "${logicalId}" container "${containerName}" has no LogConfiguration — container output is discarded`,
29
+ entity: logicalId,
30
+ lexicon: "aws",
31
+ });
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ return diagnostics;
38
+ }
39
+
40
+ export const waw048: PostSynthCheck = {
41
+ id: "WAW048",
42
+ description: "ECS container does not have a LogConfiguration — enable logging for audit trails",
43
+
44
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
45
+ return checkEcsLogConfiguration(ctx);
46
+ },
47
+ };
@@ -0,0 +1,139 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw049, checkBroadIngress } from "./waw049";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW049: Security Group Broad Ingress (any port, except ALB:80/443)", () => {
10
+ test("check metadata", () => {
11
+ expect(waw049.id).toBe("WAW049");
12
+ expect(waw049.description).toContain("443");
13
+ });
14
+
15
+ test("flags 0.0.0.0/0 ingress on an arbitrary app port (8080)", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MySg: {
19
+ Type: "AWS::EC2::SecurityGroup",
20
+ Properties: {
21
+ SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", IpProtocol: "tcp", FromPort: 8080, ToPort: 8080 }],
22
+ },
23
+ },
24
+ },
25
+ });
26
+ const diags = checkBroadIngress(ctx);
27
+ expect(diags).toHaveLength(1);
28
+ expect(diags[0].checkId).toBe("WAW049");
29
+ expect(diags[0].severity).toBe("error");
30
+ });
31
+
32
+ test("allows 0.0.0.0/0 ingress on exactly port 443 (ALB exception)", () => {
33
+ const ctx = makeCtx({
34
+ Resources: {
35
+ MySg: {
36
+ Type: "AWS::EC2::SecurityGroup",
37
+ Properties: {
38
+ SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", IpProtocol: "tcp", FromPort: 443, ToPort: 443 }],
39
+ },
40
+ },
41
+ },
42
+ });
43
+ expect(checkBroadIngress(ctx)).toHaveLength(0);
44
+ });
45
+
46
+ test("allows 0.0.0.0/0 ingress on exactly port 80 (light-tier public HTTP ALB, #917)", () => {
47
+ const ctx = makeCtx({
48
+ Resources: {
49
+ MySg: {
50
+ Type: "AWS::EC2::SecurityGroup",
51
+ Properties: {
52
+ SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", IpProtocol: "tcp", FromPort: 80, ToPort: 80 }],
53
+ },
54
+ },
55
+ },
56
+ });
57
+ expect(checkBroadIngress(ctx)).toHaveLength(0);
58
+ });
59
+
60
+ test("still flags 0.0.0.0/0 on SSH (22) — generalizes beyond WAW019's named ports too", () => {
61
+ const ctx = makeCtx({
62
+ Resources: {
63
+ MySg: {
64
+ Type: "AWS::EC2::SecurityGroup",
65
+ Properties: {
66
+ SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", IpProtocol: "tcp", FromPort: 22, ToPort: 22 }],
67
+ },
68
+ },
69
+ },
70
+ });
71
+ expect(checkBroadIngress(ctx)).toHaveLength(1);
72
+ });
73
+
74
+ test("flags a port range that includes but isn't exactly 443", () => {
75
+ const ctx = makeCtx({
76
+ Resources: {
77
+ MySg: {
78
+ Type: "AWS::EC2::SecurityGroup",
79
+ Properties: {
80
+ SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", IpProtocol: "tcp", FromPort: 0, ToPort: 65535 }],
81
+ },
82
+ },
83
+ },
84
+ });
85
+ expect(checkBroadIngress(ctx)).toHaveLength(1);
86
+ });
87
+
88
+ test("flags a rule with no port range (all ports) as unrestricted", () => {
89
+ const ctx = makeCtx({
90
+ Resources: {
91
+ MySg: {
92
+ Type: "AWS::EC2::SecurityGroup",
93
+ Properties: { SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", IpProtocol: "-1" }] },
94
+ },
95
+ },
96
+ });
97
+ expect(checkBroadIngress(ctx)).toHaveLength(1);
98
+ });
99
+
100
+ test("no diagnostic for a restricted CIDR", () => {
101
+ const ctx = makeCtx({
102
+ Resources: {
103
+ MySg: {
104
+ Type: "AWS::EC2::SecurityGroup",
105
+ Properties: {
106
+ SecurityGroupIngress: [{ CidrIp: "10.0.0.0/16", IpProtocol: "tcp", FromPort: 8080, ToPort: 8080 }],
107
+ },
108
+ },
109
+ },
110
+ });
111
+ expect(checkBroadIngress(ctx)).toHaveLength(0);
112
+ });
113
+
114
+ test("checks standalone SecurityGroupIngress resources too", () => {
115
+ const ctx = makeCtx({
116
+ Resources: {
117
+ MyIngress: {
118
+ Type: "AWS::EC2::SecurityGroupIngress",
119
+ Properties: { GroupId: "sg-123", CidrIp: "0.0.0.0/0", FromPort: 3000, ToPort: 3000 },
120
+ },
121
+ },
122
+ });
123
+ expect(checkBroadIngress(ctx)).toHaveLength(1);
124
+ });
125
+
126
+ test("skips intrinsic port values (can't verify it's the 443 exception)", () => {
127
+ const ctx = makeCtx({
128
+ Resources: {
129
+ MySg: {
130
+ Type: "AWS::EC2::SecurityGroup",
131
+ Properties: {
132
+ SecurityGroupIngress: [{ CidrIp: "0.0.0.0/0", FromPort: { Ref: "PortParam" }, ToPort: { Ref: "PortParam" } }],
133
+ },
134
+ },
135
+ },
136
+ });
137
+ expect(checkBroadIngress(ctx)).toHaveLength(0);
138
+ });
139
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * WAW049: Security Group Broad Ingress (any port, except ALB:80/443)
3
+ *
4
+ * Generalizes WAW019 beyond the four named sensitive ports (SSH/RDP/MySQL/
5
+ * Postgres): flags 0.0.0.0/0 or ::/0 ingress on *any* port, with a carve-out
6
+ * for the conventional public ALB listener ports (exactly 80 or 443).
7
+ *
8
+ * Port 80 is exempt unconditionally, not just on a relaxed tier (#917): a
9
+ * public ALB's HTTP listener is meant to be internet-facing on every tier —
10
+ * on the full/production tier it still needs an SG rule open to 0.0.0.0/0 so
11
+ * clients can reach the listener that redirects them to HTTPS (WAW044 is the
12
+ * rule that enforces the redirect actually happens; this rule only judges the
13
+ * SG's shape). The "light" tier serves plaintext HTTP straight off that same
14
+ * port instead of redirecting, which doesn't change the SG's exposure at all.
15
+ * A genuinely over-broad rule (all ports, SSH, etc.) still errors regardless
16
+ * of tier.
17
+ */
18
+
19
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
20
+ import { parseCFTemplate, getSecurityGroupIngress, isIntrinsic } from "./cf-refs";
21
+
22
+ const OPEN_CIDRS = new Set(["0.0.0.0/0", "::/0"]);
23
+ const ALB_LISTENER_PORTS = new Set([80, 443]);
24
+
25
+ function isAlbListenerException(rule: Record<string, unknown>): boolean {
26
+ return (
27
+ typeof rule.FromPort === "number" &&
28
+ rule.FromPort === rule.ToPort &&
29
+ ALB_LISTENER_PORTS.has(rule.FromPort)
30
+ );
31
+ }
32
+
33
+ function checkIngressRule(
34
+ rule: Record<string, unknown>,
35
+ logicalId: string,
36
+ diagnostics: PostSynthDiagnostic[],
37
+ ): void {
38
+ const cidrIp = rule.CidrIp;
39
+ const cidrIpv6 = rule.CidrIpv6;
40
+
41
+ const hasOpenCidr =
42
+ (typeof cidrIp === "string" && OPEN_CIDRS.has(cidrIp)) ||
43
+ (typeof cidrIpv6 === "string" && OPEN_CIDRS.has(cidrIpv6));
44
+
45
+ if (!hasOpenCidr) return;
46
+
47
+ // Can't statically verify a port pinned by an intrinsic — don't guess.
48
+ if (isIntrinsic(rule.FromPort) || isIntrinsic(rule.ToPort)) return;
49
+
50
+ if (isAlbListenerException(rule)) return;
51
+
52
+ const cidr = typeof cidrIp === "string" && OPEN_CIDRS.has(cidrIp) ? cidrIp : cidrIpv6;
53
+ const fromPort = rule.FromPort;
54
+ const toPort = rule.ToPort;
55
+ const portDesc =
56
+ fromPort !== undefined && toPort !== undefined ? ` on ports ${fromPort}-${toPort}` : " on all ports";
57
+
58
+ diagnostics.push({
59
+ checkId: "WAW049",
60
+ severity: "error",
61
+ message: `Security group "${logicalId}" allows unrestricted ingress from ${cidr}${portDesc} — restrict to specific CIDR ranges (only ALB:80/443 is exempt)`,
62
+ entity: logicalId,
63
+ lexicon: "aws",
64
+ });
65
+ }
66
+
67
+ export function checkBroadIngress(ctx: PostSynthContext): PostSynthDiagnostic[] {
68
+ const diagnostics: PostSynthDiagnostic[] = [];
69
+
70
+ for (const [_lexicon, output] of ctx.outputs) {
71
+ const template = parseCFTemplate(output);
72
+ if (!template?.Resources) continue;
73
+
74
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
75
+ if (resource.Type === "AWS::EC2::SecurityGroup") {
76
+ for (const rule of getSecurityGroupIngress(resource)) {
77
+ checkIngressRule(rule, logicalId, diagnostics);
78
+ }
79
+ }
80
+
81
+ if (resource.Type === "AWS::EC2::SecurityGroupIngress") {
82
+ const props = resource.Properties ?? {};
83
+ if (!isIntrinsic(props)) {
84
+ checkIngressRule(props as Record<string, unknown>, logicalId, diagnostics);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ return diagnostics;
91
+ }
92
+
93
+ export const waw049: PostSynthCheck = {
94
+ id: "WAW049",
95
+ description: "Security group allows unrestricted ingress on a port other than ALB:80/443",
96
+
97
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
98
+ return checkBroadIngress(ctx);
99
+ },
100
+ };
@@ -0,0 +1,75 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw050, checkCognitoAdvancedSecurity } from "./waw050";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW050: Cognito Advanced Security Disabled", () => {
10
+ test("check metadata", () => {
11
+ expect(waw050.id).toBe("WAW050");
12
+ expect(waw050.description).toContain("advanced security");
13
+ });
14
+
15
+ test("flags a pool with no UserPoolAddOns", () => {
16
+ const ctx = makeCtx({
17
+ Resources: { MyPool: { Type: "AWS::Cognito::UserPool", Properties: { UserPoolName: "app" } } },
18
+ });
19
+ const diags = checkCognitoAdvancedSecurity(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].checkId).toBe("WAW050");
22
+ });
23
+
24
+ test("flags AdvancedSecurityMode: OFF", () => {
25
+ const ctx = makeCtx({
26
+ Resources: {
27
+ MyPool: {
28
+ Type: "AWS::Cognito::UserPool",
29
+ Properties: { UserPoolAddOns: { AdvancedSecurityMode: "OFF" } },
30
+ },
31
+ },
32
+ });
33
+ expect(checkCognitoAdvancedSecurity(ctx)).toHaveLength(1);
34
+ });
35
+
36
+ test("no diagnostic for AUDIT mode", () => {
37
+ const ctx = makeCtx({
38
+ Resources: {
39
+ MyPool: {
40
+ Type: "AWS::Cognito::UserPool",
41
+ Properties: { UserPoolAddOns: { AdvancedSecurityMode: "AUDIT" } },
42
+ },
43
+ },
44
+ });
45
+ expect(checkCognitoAdvancedSecurity(ctx)).toHaveLength(0);
46
+ });
47
+
48
+ test("no diagnostic for ENFORCED mode", () => {
49
+ const ctx = makeCtx({
50
+ Resources: {
51
+ MyPool: {
52
+ Type: "AWS::Cognito::UserPool",
53
+ Properties: { UserPoolAddOns: { AdvancedSecurityMode: "ENFORCED" } },
54
+ },
55
+ },
56
+ });
57
+ expect(checkCognitoAdvancedSecurity(ctx)).toHaveLength(0);
58
+ });
59
+
60
+ test("skips intrinsic UserPoolAddOns/AdvancedSecurityMode values", () => {
61
+ const ctx = makeCtx({
62
+ Resources: {
63
+ MyPool: { Type: "AWS::Cognito::UserPool", Properties: { UserPoolAddOns: { Ref: "AddOnsParam" } } },
64
+ },
65
+ });
66
+ expect(checkCognitoAdvancedSecurity(ctx)).toHaveLength(0);
67
+ });
68
+
69
+ test("no diagnostic for non-UserPool resources", () => {
70
+ const ctx = makeCtx({
71
+ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } },
72
+ });
73
+ expect(checkCognitoAdvancedSecurity(ctx)).toHaveLength(0);
74
+ });
75
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * WAW050: Cognito Advanced Security Disabled
3
+ *
4
+ * Flags Cognito UserPools without UserPoolAddOns.AdvancedSecurityMode set to
5
+ * AUDIT or ENFORCED — without it, Cognito does not evaluate risk (compromised
6
+ * credentials, adaptive auth) on sign-in/sign-up.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate, isIntrinsic } from "./cf-refs";
11
+
12
+ export function checkCognitoAdvancedSecurity(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
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
20
+ if (resource.Type !== "AWS::Cognito::UserPool") continue;
21
+
22
+ const props = resource.Properties ?? {};
23
+ const addOns = props.UserPoolAddOns;
24
+
25
+ if (isIntrinsic(addOns)) continue;
26
+
27
+ const mode =
28
+ typeof addOns === "object" && addOns !== null ? (addOns as Record<string, unknown>).AdvancedSecurityMode : undefined;
29
+
30
+ if (isIntrinsic(mode)) continue;
31
+
32
+ if (mode !== "AUDIT" && mode !== "ENFORCED") {
33
+ diagnostics.push({
34
+ checkId: "WAW050",
35
+ severity: "error",
36
+ message: `Cognito UserPool "${logicalId}" does not have advanced security enabled — set UserPoolAddOns.AdvancedSecurityMode to AUDIT or ENFORCED`,
37
+ entity: logicalId,
38
+ lexicon: "aws",
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ return diagnostics;
45
+ }
46
+
47
+ export const waw050: PostSynthCheck = {
48
+ id: "WAW050",
49
+ description: "Cognito UserPool does not have advanced security (risk-based auth) enabled",
50
+
51
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
52
+ return checkCognitoAdvancedSecurity(ctx);
53
+ },
54
+ };