@intentius/chant-lexicon-aws 0.1.1 → 0.1.4

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/src/index.ts CHANGED
@@ -79,12 +79,14 @@ export {
79
79
  LambdaSqs, LambdaEventBridge, LambdaDynamoDB, LambdaS3, LambdaSns,
80
80
  VpcDefault, FargateAlb, AlbShared, FargateService, RdsInstance, RdsPostgres,
81
81
  EfsWithAccessPoint,
82
+ Ec2InstanceRole, MinimalVpc,
82
83
  } from "./composites/index";
83
84
  export type {
84
85
  LambdaFunctionProps, LambdaApiProps, ScheduledLambdaProps,
85
86
  LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, LambdaS3Props, LambdaSnsProps,
86
87
  VpcDefaultProps, FargateAlbProps, AlbSharedProps, FargateServiceProps, RdsInstanceProps, RdsPostgresProps,
87
88
  EfsWithAccessPointProps,
89
+ Ec2InstanceRoleProps, MinimalVpcProps,
88
90
  } from "./composites/index";
89
91
 
90
92
  // Code generation pipeline
@@ -0,0 +1,83 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw032, checkEfsTransitEncryption } from "./waw032";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW032: EFS Transit Encryption Disabled", () => {
10
+ test("check metadata", () => {
11
+ expect(waw032.id).toBe("WAW032");
12
+ expect(waw032.description).toContain("transit encryption");
13
+ });
14
+
15
+ test("flags task with DISABLED transit encryption", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyTask: {
19
+ Type: "AWS::ECS::TaskDefinition",
20
+ Properties: {
21
+ Volumes: [
22
+ {
23
+ Name: "solr-data",
24
+ EFSVolumeConfiguration: {
25
+ FileSystemId: "fs-abc123",
26
+ TransitEncryption: "DISABLED",
27
+ },
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ },
33
+ });
34
+ const diags = checkEfsTransitEncryption(ctx);
35
+ expect(diags).toHaveLength(1);
36
+ expect(diags[0].checkId).toBe("WAW032");
37
+ expect(diags[0].severity).toBe("warning");
38
+ expect(diags[0].entity).toBe("MyTask");
39
+ });
40
+
41
+ test("no diagnostic when transit encryption is ENABLED", () => {
42
+ const ctx = makeCtx({
43
+ Resources: {
44
+ MyTask: {
45
+ Type: "AWS::ECS::TaskDefinition",
46
+ Properties: {
47
+ Volumes: [
48
+ {
49
+ Name: "solr-data",
50
+ EFSVolumeConfiguration: {
51
+ FileSystemId: "fs-abc123",
52
+ TransitEncryption: "ENABLED",
53
+ },
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ },
59
+ });
60
+ expect(checkEfsTransitEncryption(ctx)).toHaveLength(0);
61
+ });
62
+
63
+ test("no diagnostic when no EFS volumes", () => {
64
+ const ctx = makeCtx({
65
+ Resources: {
66
+ MyTask: {
67
+ Type: "AWS::ECS::TaskDefinition",
68
+ Properties: { Volumes: [] },
69
+ },
70
+ },
71
+ });
72
+ expect(checkEfsTransitEncryption(ctx)).toHaveLength(0);
73
+ });
74
+
75
+ test("ignores non-task resources", () => {
76
+ const ctx = makeCtx({
77
+ Resources: {
78
+ MyBucket: { Type: "AWS::S3::Bucket", Properties: {} },
79
+ },
80
+ });
81
+ expect(checkEfsTransitEncryption(ctx)).toHaveLength(0);
82
+ });
83
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * WAW032: EFS Transit Encryption Disabled
3
+ *
4
+ * Flags EFS volume configurations on Fargate task definitions where transit
5
+ * encryption has been explicitly disabled. FargateService defaults to ENABLED;
6
+ * this rule catches intentional opt-outs.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate } from "./cf-refs";
11
+
12
+ export function checkEfsTransitEncryption(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
+ const volumes = resource.Properties?.Volumes;
23
+ if (!Array.isArray(volumes)) continue;
24
+
25
+ for (const volume of volumes) {
26
+ const efsConfig = volume?.EFSVolumeConfiguration;
27
+ if (!efsConfig) continue;
28
+
29
+ if (efsConfig.TransitEncryption === "DISABLED") {
30
+ diagnostics.push({
31
+ checkId: "WAW032",
32
+ severity: "warning",
33
+ message: `EFS volume "${volume.Name ?? "unnamed"}" in task "${logicalId}" has transit encryption disabled — set TransitEncryption: ENABLED to protect data in transit`,
34
+ entity: logicalId,
35
+ lexicon: "aws",
36
+ });
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ return diagnostics;
43
+ }
44
+
45
+ export const waw032: PostSynthCheck = {
46
+ id: "WAW032",
47
+ description: "EFS volume on Fargate task has transit encryption disabled",
48
+
49
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
50
+ return checkEfsTransitEncryption(ctx);
51
+ },
52
+ };
@@ -0,0 +1,68 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw033, checkSolrHeapRatio } from "./waw033";
4
+
5
+ function makeTask(memory: string, image: string, solrHeap?: string) {
6
+ return createPostSynthContext({
7
+ aws: {
8
+ Resources: {
9
+ SolrTask: {
10
+ Type: "AWS::ECS::TaskDefinition",
11
+ Properties: {
12
+ Memory: memory,
13
+ ContainerDefinitions: [
14
+ {
15
+ Name: "app",
16
+ Image: image,
17
+ ...(solrHeap && {
18
+ Environment: [{ Name: "SOLR_HEAP", Value: solrHeap }],
19
+ }),
20
+ },
21
+ ],
22
+ },
23
+ },
24
+ },
25
+ },
26
+ });
27
+ }
28
+
29
+ describe("WAW033: Solr Heap Ratio", () => {
30
+ test("check metadata", () => {
31
+ expect(waw033.id).toBe("WAW033");
32
+ expect(waw033.description).toContain("SOLR_HEAP");
33
+ });
34
+
35
+ test("flags heap > 50% of task memory (string MB)", () => {
36
+ const diags = checkSolrHeapRatio(makeTask("2048", "solr:9", "1500m"));
37
+ expect(diags).toHaveLength(1);
38
+ expect(diags[0].checkId).toBe("WAW033");
39
+ expect(diags[0].severity).toBe("error");
40
+ });
41
+
42
+ test("flags heap > 50% specified in gigabytes", () => {
43
+ // 2g = 2048MB, task is 2048MB — 2048 > 1024 (50%)
44
+ const diags = checkSolrHeapRatio(makeTask("2048", "solr:9", "2g"));
45
+ expect(diags).toHaveLength(1);
46
+ });
47
+
48
+ test("no diagnostic when heap is within limit", () => {
49
+ // 900m < 50% of 2048 (1024)
50
+ const diags = checkSolrHeapRatio(makeTask("2048", "solr:9", "900m"));
51
+ expect(diags).toHaveLength(0);
52
+ });
53
+
54
+ test("no diagnostic when SOLR_HEAP not set", () => {
55
+ const diags = checkSolrHeapRatio(makeTask("2048", "solr:9"));
56
+ expect(diags).toHaveLength(0);
57
+ });
58
+
59
+ test("no diagnostic for non-solr image", () => {
60
+ const diags = checkSolrHeapRatio(makeTask("2048", "nginx:latest", "1500m"));
61
+ expect(diags).toHaveLength(0);
62
+ });
63
+
64
+ test("detects solr image case-insensitively", () => {
65
+ const diags = checkSolrHeapRatio(makeTask("2048", "myrepo/Solr-Custom:9.7", "1500m"));
66
+ expect(diags).toHaveLength(1);
67
+ });
68
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * WAW033: Solr Heap Exceeds 50% of Container Memory
3
+ *
4
+ * When SOLR_HEAP is set on a Fargate task running a Solr image, validates that
5
+ * the heap does not exceed 50% of the task's allocated memory. Exceeding this
6
+ * leaves insufficient headroom for the OS file cache that Lucene MMap relies on,
7
+ * causing the OOM killer to terminate the container.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { parseCFTemplate } from "./cf-refs";
12
+
13
+ /** Parse heap strings like "4g", "2048m", "2048" → megabytes */
14
+ function parseHeapMb(value: string): number | null {
15
+ const lower = value.trim().toLowerCase();
16
+ const gMatch = lower.match(/^(\d+(?:\.\d+)?)g$/);
17
+ if (gMatch) return Math.round(parseFloat(gMatch[1]) * 1024);
18
+ const mMatch = lower.match(/^(\d+(?:\.\d+)?)m?$/);
19
+ if (mMatch) return Math.round(parseFloat(mMatch[1]));
20
+ return null;
21
+ }
22
+
23
+ function isSolrImage(image: unknown): boolean {
24
+ return typeof image === "string" && image.toLowerCase().includes("solr");
25
+ }
26
+
27
+ export function checkSolrHeapRatio(ctx: PostSynthContext): PostSynthDiagnostic[] {
28
+ const diagnostics: PostSynthDiagnostic[] = [];
29
+
30
+ for (const [_lexicon, output] of ctx.outputs) {
31
+ const template = parseCFTemplate(output);
32
+ if (!template?.Resources) continue;
33
+
34
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
35
+ if (resource.Type !== "AWS::ECS::TaskDefinition") continue;
36
+
37
+ const props = resource.Properties ?? {};
38
+ if (typeof props.Memory !== "string") continue;
39
+ const taskMemoryMb = parseInt(props.Memory);
40
+ if (!taskMemoryMb) continue;
41
+
42
+ const containers: unknown[] = Array.isArray(props.ContainerDefinitions)
43
+ ? props.ContainerDefinitions
44
+ : [];
45
+
46
+ for (const container of containers) {
47
+ if (typeof container !== "object" || container === null) continue;
48
+ const c = container as Record<string, unknown>;
49
+
50
+ if (!isSolrImage(c.Image)) continue;
51
+
52
+ const envVars: unknown[] = Array.isArray(c.Environment) ? c.Environment : [];
53
+ const heapEntry = envVars.find(
54
+ (e): e is Record<string, unknown> =>
55
+ typeof e === "object" && e !== null && (e as Record<string, unknown>).Name === "SOLR_HEAP",
56
+ );
57
+
58
+ if (!heapEntry) continue; // WAW034 covers missing heap
59
+
60
+ const heapMb = parseHeapMb(String(heapEntry.Value ?? ""));
61
+ if (heapMb === null) continue;
62
+
63
+ if (heapMb > taskMemoryMb * 0.5) {
64
+ diagnostics.push({
65
+ checkId: "WAW033",
66
+ severity: "error",
67
+ message: `Solr container "${c.Name ?? "app"}" SOLR_HEAP (${heapMb}MB) exceeds 50% of task memory (${taskMemoryMb}MB) — risk of OOM kill; set SOLR_HEAP <= ${Math.floor(taskMemoryMb * 0.45)}m`,
68
+ entity: logicalId,
69
+ lexicon: "aws",
70
+ });
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ return diagnostics;
77
+ }
78
+
79
+ export const waw033: PostSynthCheck = {
80
+ id: "WAW033",
81
+ description: "Solr SOLR_HEAP exceeds 50% of Fargate task memory",
82
+
83
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
84
+ return checkSolrHeapRatio(ctx);
85
+ },
86
+ };
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw034, checkSolrMemoryMinimum } from "./waw034";
4
+
5
+ function makeTask(memory: string, image: string) {
6
+ return createPostSynthContext({
7
+ aws: {
8
+ Resources: {
9
+ SolrTask: {
10
+ Type: "AWS::ECS::TaskDefinition",
11
+ Properties: {
12
+ Memory: memory,
13
+ ContainerDefinitions: [{ Name: "app", Image: image }],
14
+ },
15
+ },
16
+ },
17
+ },
18
+ });
19
+ }
20
+
21
+ describe("WAW034: Solr Memory Minimum", () => {
22
+ test("check metadata", () => {
23
+ expect(waw034.id).toBe("WAW034");
24
+ expect(waw034.description).toContain("2048");
25
+ });
26
+
27
+ test("flags task with 512MB", () => {
28
+ const diags = checkSolrMemoryMinimum(makeTask("512", "solr:9"));
29
+ expect(diags).toHaveLength(1);
30
+ expect(diags[0].checkId).toBe("WAW034");
31
+ expect(diags[0].severity).toBe("warning");
32
+ expect(diags[0].message).toContain("512MB");
33
+ });
34
+
35
+ test("flags task with 1024MB", () => {
36
+ const diags = checkSolrMemoryMinimum(makeTask("1024", "solr:9"));
37
+ expect(diags).toHaveLength(1);
38
+ });
39
+
40
+ test("no diagnostic at exactly 2048MB", () => {
41
+ const diags = checkSolrMemoryMinimum(makeTask("2048", "solr:9"));
42
+ expect(diags).toHaveLength(0);
43
+ });
44
+
45
+ test("no diagnostic at 4096MB", () => {
46
+ const diags = checkSolrMemoryMinimum(makeTask("4096", "solr:9"));
47
+ expect(diags).toHaveLength(0);
48
+ });
49
+
50
+ test("no diagnostic for non-solr image", () => {
51
+ const diags = checkSolrMemoryMinimum(makeTask("512", "postgres:16"));
52
+ expect(diags).toHaveLength(0);
53
+ });
54
+ });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * WAW034: Solr Container Undersized
3
+ *
4
+ * Fargate tasks running a Solr image with less than 2048MB of memory will
5
+ * fail under any real load — the JVM alone needs headroom for the heap plus
6
+ * OS file cache for Lucene MMap. 4096MB is the practical production minimum.
7
+ */
8
+
9
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
10
+ import { parseCFTemplate } from "./cf-refs";
11
+
12
+ function isSolrImage(image: unknown): boolean {
13
+ return typeof image === "string" && image.toLowerCase().includes("solr");
14
+ }
15
+
16
+ export function checkSolrMemoryMinimum(ctx: PostSynthContext): PostSynthDiagnostic[] {
17
+ const diagnostics: PostSynthDiagnostic[] = [];
18
+
19
+ for (const [_lexicon, output] of ctx.outputs) {
20
+ const template = parseCFTemplate(output);
21
+ if (!template?.Resources) continue;
22
+
23
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
24
+ if (resource.Type !== "AWS::ECS::TaskDefinition") continue;
25
+
26
+ const props = resource.Properties ?? {};
27
+ if (typeof props.Memory !== "string") continue;
28
+ const taskMemoryMb = parseInt(props.Memory);
29
+ if (!taskMemoryMb) continue;
30
+
31
+ const containers: unknown[] = Array.isArray(props.ContainerDefinitions)
32
+ ? props.ContainerDefinitions
33
+ : [];
34
+
35
+ const hasSolr = containers.some(
36
+ (c) => typeof c === "object" && c !== null && isSolrImage((c as Record<string, unknown>).Image),
37
+ );
38
+
39
+ if (!hasSolr) continue;
40
+
41
+ if (taskMemoryMb < 2048) {
42
+ diagnostics.push({
43
+ checkId: "WAW034",
44
+ severity: "warning",
45
+ message: `Solr task "${logicalId}" has only ${taskMemoryMb}MB memory — Solr requires at least 2048MB; 4096MB recommended for production`,
46
+ entity: logicalId,
47
+ lexicon: "aws",
48
+ });
49
+ }
50
+ }
51
+ }
52
+
53
+ return diagnostics;
54
+ }
55
+
56
+ export const waw034: PostSynthCheck = {
57
+ id: "WAW034",
58
+ description: "Fargate task running Solr has insufficient memory (< 2048MB)",
59
+
60
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
61
+ return checkSolrMemoryMinimum(ctx);
62
+ },
63
+ };
@@ -0,0 +1,74 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw035, checkSolrUlimits } from "./waw035";
4
+
5
+ function makeTask(image: string, ulimits?: unknown[]) {
6
+ return createPostSynthContext({
7
+ aws: {
8
+ Resources: {
9
+ SolrTask: {
10
+ Type: "AWS::ECS::TaskDefinition",
11
+ Properties: {
12
+ Memory: "4096",
13
+ ContainerDefinitions: [
14
+ {
15
+ Name: "app",
16
+ Image: image,
17
+ ...(ulimits && { Ulimits: ulimits }),
18
+ },
19
+ ],
20
+ },
21
+ },
22
+ },
23
+ },
24
+ });
25
+ }
26
+
27
+ describe("WAW035: Solr nofile Ulimit", () => {
28
+ test("check metadata", () => {
29
+ expect(waw035.id).toBe("WAW035");
30
+ expect(waw035.description).toContain("nofile");
31
+ });
32
+
33
+ test("flags Solr container with no ulimits", () => {
34
+ const diags = checkSolrUlimits(makeTask("solr:9"));
35
+ expect(diags).toHaveLength(1);
36
+ expect(diags[0].checkId).toBe("WAW035");
37
+ expect(diags[0].severity).toBe("warning");
38
+ expect(diags[0].message).toContain("not set");
39
+ });
40
+
41
+ test("flags Solr container with nofile below minimum", () => {
42
+ const diags = checkSolrUlimits(makeTask("solr:9", [
43
+ { Name: "nofile", SoftLimit: 1024, HardLimit: 1024 },
44
+ ]));
45
+ expect(diags).toHaveLength(1);
46
+ expect(diags[0].message).toContain("1024");
47
+ });
48
+
49
+ test("no diagnostic when nofile >= 65535", () => {
50
+ const diags = checkSolrUlimits(makeTask("solr:9", [
51
+ { Name: "nofile", SoftLimit: 65535, HardLimit: 65535 },
52
+ ]));
53
+ expect(diags).toHaveLength(0);
54
+ });
55
+
56
+ test("no diagnostic when nofile HardLimit > 65535", () => {
57
+ const diags = checkSolrUlimits(makeTask("solr:9", [
58
+ { Name: "nofile", SoftLimit: 65535, HardLimit: 131072 },
59
+ ]));
60
+ expect(diags).toHaveLength(0);
61
+ });
62
+
63
+ test("no diagnostic for non-solr image", () => {
64
+ const diags = checkSolrUlimits(makeTask("nginx:latest"));
65
+ expect(diags).toHaveLength(0);
66
+ });
67
+
68
+ test("flags when only nproc ulimit set but nofile missing", () => {
69
+ const diags = checkSolrUlimits(makeTask("solr:9", [
70
+ { Name: "nproc", SoftLimit: 65535, HardLimit: 65535 },
71
+ ]));
72
+ expect(diags).toHaveLength(1);
73
+ });
74
+ });
@@ -0,0 +1,71 @@
1
+ /**
2
+ * WAW035: Solr Container Missing nofile Ulimit
3
+ *
4
+ * Solr opens a file descriptor for every shard replica, index file, log, and
5
+ * connection. Without a raised nofile limit the process hits the default kernel
6
+ * limit (~1024) under moderate load, causing "Too many open files" errors that
7
+ * bring the node down. The production minimum is 65535.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { parseCFTemplate } from "./cf-refs";
12
+
13
+ const NOFILE_MIN = 65535;
14
+
15
+ function isSolrImage(image: unknown): boolean {
16
+ return typeof image === "string" && image.toLowerCase().includes("solr");
17
+ }
18
+
19
+ export function checkSolrUlimits(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::ECS::TaskDefinition") continue;
28
+
29
+ const containers: unknown[] = Array.isArray(resource.Properties?.ContainerDefinitions)
30
+ ? resource.Properties.ContainerDefinitions
31
+ : [];
32
+
33
+ for (const container of containers) {
34
+ if (typeof container !== "object" || container === null) continue;
35
+ const c = container as Record<string, unknown>;
36
+
37
+ if (!isSolrImage(c.Image)) continue;
38
+
39
+ const ulimits: unknown[] = Array.isArray(c.Ulimits) ? c.Ulimits : [];
40
+ const nofile = ulimits.find(
41
+ (u): u is Record<string, unknown> =>
42
+ typeof u === "object" && u !== null && (u as Record<string, unknown>).Name === "nofile",
43
+ );
44
+
45
+ const hardLimit = nofile ? Number(nofile.HardLimit ?? 0) : 0;
46
+
47
+ if (!nofile || hardLimit < NOFILE_MIN) {
48
+ const current = nofile ? ` (current HardLimit: ${hardLimit})` : " (not set)";
49
+ diagnostics.push({
50
+ checkId: "WAW035",
51
+ severity: "warning",
52
+ message: `Solr container "${c.Name ?? "app"}" in task "${logicalId}" nofile ulimit${current} — set HardLimit >= ${NOFILE_MIN} to prevent "Too many open files" under load`,
53
+ entity: logicalId,
54
+ lexicon: "aws",
55
+ });
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ return diagnostics;
62
+ }
63
+
64
+ export const waw035: PostSynthCheck = {
65
+ id: "WAW035",
66
+ description: "Solr container missing nofile ulimit >= 65535",
67
+
68
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
69
+ return checkSolrUlimits(ctx);
70
+ },
71
+ };