@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,78 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw051, checkCognitoImplicitGrant } from "./waw051";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW051: Cognito Implicit OAuth Grant Allowed", () => {
10
+ test("check metadata", () => {
11
+ expect(waw051.id).toBe("WAW051");
12
+ expect(waw051.description).toContain("implicit");
13
+ });
14
+
15
+ test("flags a client allowing the implicit grant", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyClient: {
19
+ Type: "AWS::Cognito::UserPoolClient",
20
+ Properties: { AllowedOAuthFlows: ["implicit"] },
21
+ },
22
+ },
23
+ });
24
+ const diags = checkCognitoImplicitGrant(ctx);
25
+ expect(diags).toHaveLength(1);
26
+ expect(diags[0].checkId).toBe("WAW051");
27
+ });
28
+
29
+ test("flags when implicit is one of several allowed flows", () => {
30
+ const ctx = makeCtx({
31
+ Resources: {
32
+ MyClient: {
33
+ Type: "AWS::Cognito::UserPoolClient",
34
+ Properties: { AllowedOAuthFlows: ["code", "implicit"] },
35
+ },
36
+ },
37
+ });
38
+ expect(checkCognitoImplicitGrant(ctx)).toHaveLength(1);
39
+ });
40
+
41
+ test("no diagnostic for the authorization code grant only", () => {
42
+ const ctx = makeCtx({
43
+ Resources: {
44
+ MyClient: {
45
+ Type: "AWS::Cognito::UserPoolClient",
46
+ Properties: { AllowedOAuthFlows: ["code"] },
47
+ },
48
+ },
49
+ });
50
+ expect(checkCognitoImplicitGrant(ctx)).toHaveLength(0);
51
+ });
52
+
53
+ test("no diagnostic when AllowedOAuthFlows is unset", () => {
54
+ const ctx = makeCtx({
55
+ Resources: { MyClient: { Type: "AWS::Cognito::UserPoolClient", Properties: {} } },
56
+ });
57
+ expect(checkCognitoImplicitGrant(ctx)).toHaveLength(0);
58
+ });
59
+
60
+ test("skips intrinsic AllowedOAuthFlows values", () => {
61
+ const ctx = makeCtx({
62
+ Resources: {
63
+ MyClient: {
64
+ Type: "AWS::Cognito::UserPoolClient",
65
+ Properties: { AllowedOAuthFlows: { Ref: "FlowsParam" } },
66
+ },
67
+ },
68
+ });
69
+ expect(checkCognitoImplicitGrant(ctx)).toHaveLength(0);
70
+ });
71
+
72
+ test("no diagnostic for non-UserPoolClient resources", () => {
73
+ const ctx = makeCtx({
74
+ Resources: { MyPool: { Type: "AWS::Cognito::UserPool", Properties: {} } },
75
+ });
76
+ expect(checkCognitoImplicitGrant(ctx)).toHaveLength(0);
77
+ });
78
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WAW051: Cognito Implicit OAuth Grant Allowed
3
+ *
4
+ * Flags UserPoolClients whose AllowedOAuthFlows include the deprecated
5
+ * "implicit" grant — it returns tokens directly in the redirect URL (exposed
6
+ * in browser history/referrer headers) instead of via a code exchange.
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 checkCognitoImplicitGrant(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::UserPoolClient") continue;
21
+
22
+ const props = resource.Properties ?? {};
23
+ const flows = props.AllowedOAuthFlows;
24
+
25
+ if (isIntrinsic(flows) || !Array.isArray(flows)) continue;
26
+
27
+ if (flows.some((f) => f === "implicit")) {
28
+ diagnostics.push({
29
+ checkId: "WAW051",
30
+ severity: "error",
31
+ message: `Cognito UserPoolClient "${logicalId}" allows the implicit OAuth grant — use the authorization code grant instead`,
32
+ entity: logicalId,
33
+ lexicon: "aws",
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ }
41
+
42
+ export const waw051: PostSynthCheck = {
43
+ id: "WAW051",
44
+ description: "Cognito UserPoolClient allows the deprecated implicit OAuth grant",
45
+
46
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
47
+ return checkCognitoImplicitGrant(ctx);
48
+ },
49
+ };
@@ -0,0 +1,54 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw052, checkCognitoMfaRequired } from "./waw052";
4
+
5
+ function makeCtx(template: object, env?: string) {
6
+ return { ...createPostSynthContext({ aws: template }), env };
7
+ }
8
+
9
+ const template = {
10
+ Resources: {
11
+ MyPool: { Type: "AWS::Cognito::UserPool", Properties: { MfaConfiguration: "OPTIONAL" } },
12
+ },
13
+ };
14
+
15
+ describe("WAW052: Cognito MFA Not Required (full tier)", () => {
16
+ test("check metadata", () => {
17
+ expect(waw052.id).toBe("WAW052");
18
+ expect(waw052.description).toContain("MFA");
19
+ });
20
+
21
+ test("warns (not errors) on the light tier — no env set", () => {
22
+ const diags = checkCognitoMfaRequired(makeCtx(template));
23
+ expect(diags).toHaveLength(1);
24
+ expect(diags[0].checkId).toBe("WAW052");
25
+ expect(diags[0].severity).toBe("warning");
26
+ });
27
+
28
+ test("warns on a non-production env", () => {
29
+ expect(checkCognitoMfaRequired(makeCtx(template, "dev"))[0].severity).toBe("warning");
30
+ });
31
+
32
+ test("errors on the full/production tier", () => {
33
+ const diags = checkCognitoMfaRequired(makeCtx(template, "production"));
34
+ expect(diags).toHaveLength(1);
35
+ expect(diags[0].severity).toBe("error");
36
+ });
37
+
38
+ test("flags MfaConfiguration: OFF too", () => {
39
+ const offTemplate = { Resources: { MyPool: { Type: "AWS::Cognito::UserPool", Properties: { MfaConfiguration: "OFF" } } } };
40
+ expect(checkCognitoMfaRequired(makeCtx(offTemplate, "prod"))).toHaveLength(1);
41
+ });
42
+
43
+ test("no diagnostic when MfaConfiguration: ON, even in prod", () => {
44
+ const onTemplate = { Resources: { MyPool: { Type: "AWS::Cognito::UserPool", Properties: { MfaConfiguration: "ON" } } } };
45
+ expect(checkCognitoMfaRequired(makeCtx(onTemplate, "prod"))).toHaveLength(0);
46
+ });
47
+
48
+ test("skips intrinsic MfaConfiguration values", () => {
49
+ const paramTemplate = {
50
+ Resources: { MyPool: { Type: "AWS::Cognito::UserPool", Properties: { MfaConfiguration: { Ref: "MfaParam" } } } },
51
+ };
52
+ expect(checkCognitoMfaRequired(makeCtx(paramTemplate, "prod"))).toHaveLength(0);
53
+ });
54
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * WAW052: Cognito MFA Not Required (full tier)
3
+ *
4
+ * Flags UserPools without MfaConfiguration: ON. Tier-aware (#894), same
5
+ * seam as WAW040: a relaxed "light" stack (local/Floci, or no --env/
6
+ * ownership.env set) only warns; the strict "full"/production tier
7
+ * (ctx.env === "prod"/"production"/"full") fails the build.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { parseCFTemplate, isIntrinsic, isFullTierEnv } from "./cf-refs";
12
+
13
+ export function checkCognitoMfaRequired(ctx: PostSynthContext): PostSynthDiagnostic[] {
14
+ const diagnostics: PostSynthDiagnostic[] = [];
15
+ const severity = isFullTierEnv(ctx.env) ? "error" : "warning";
16
+
17
+ for (const [_lexicon, output] of ctx.outputs) {
18
+ const template = parseCFTemplate(output);
19
+ if (!template?.Resources) continue;
20
+
21
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
22
+ if (resource.Type !== "AWS::Cognito::UserPool") continue;
23
+
24
+ const props = resource.Properties ?? {};
25
+ const mfaConfiguration = props.MfaConfiguration;
26
+
27
+ if (isIntrinsic(mfaConfiguration)) continue;
28
+
29
+ if (mfaConfiguration !== "ON") {
30
+ diagnostics.push({
31
+ checkId: "WAW052",
32
+ severity,
33
+ message: `Cognito UserPool "${logicalId}" does not require MFA (MfaConfiguration: ON) — require it on the full/production tier`,
34
+ entity: logicalId,
35
+ lexicon: "aws",
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ return diagnostics;
42
+ }
43
+
44
+ export const waw052: PostSynthCheck = {
45
+ id: "WAW052",
46
+ description: "Cognito UserPool does not require MFA (full tier)",
47
+
48
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
49
+ return checkCognitoMfaRequired(ctx);
50
+ },
51
+ };
@@ -0,0 +1,59 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw053, checkEcrScanOnPush } from "./waw053";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW053: ECR Image Scanning Disabled", () => {
10
+ test("check metadata", () => {
11
+ expect(waw053.id).toBe("WAW053");
12
+ expect(waw053.description).toContain("scan");
13
+ });
14
+
15
+ test("flags a repository with no ImageScanningConfiguration", () => {
16
+ const ctx = makeCtx({
17
+ Resources: { MyRepo: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "app" } } },
18
+ });
19
+ const diags = checkEcrScanOnPush(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].checkId).toBe("WAW053");
22
+ });
23
+
24
+ test("flags ScanOnPush: false", () => {
25
+ const ctx = makeCtx({
26
+ Resources: {
27
+ MyRepo: {
28
+ Type: "AWS::ECR::Repository",
29
+ Properties: { ImageScanningConfiguration: { ScanOnPush: false } },
30
+ },
31
+ },
32
+ });
33
+ expect(checkEcrScanOnPush(ctx)).toHaveLength(1);
34
+ });
35
+
36
+ test("no diagnostic when ScanOnPush: true", () => {
37
+ const ctx = makeCtx({
38
+ Resources: {
39
+ MyRepo: {
40
+ Type: "AWS::ECR::Repository",
41
+ Properties: { ImageScanningConfiguration: { ScanOnPush: true } },
42
+ },
43
+ },
44
+ });
45
+ expect(checkEcrScanOnPush(ctx)).toHaveLength(0);
46
+ });
47
+
48
+ test("skips intrinsic ImageScanningConfiguration/ScanOnPush values", () => {
49
+ const ctx = makeCtx({
50
+ Resources: { MyRepo: { Type: "AWS::ECR::Repository", Properties: { ImageScanningConfiguration: { Ref: "ScanParam" } } } },
51
+ });
52
+ expect(checkEcrScanOnPush(ctx)).toHaveLength(0);
53
+ });
54
+
55
+ test("no diagnostic for non-ECR resources", () => {
56
+ const ctx = makeCtx({ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } } });
57
+ expect(checkEcrScanOnPush(ctx)).toHaveLength(0);
58
+ });
59
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * WAW053: ECR Image Scanning Disabled
3
+ *
4
+ * Flags ECR repositories without ImageScanningConfiguration.ScanOnPush: true
5
+ * — pushed images aren't scanned for known CVEs.
6
+ */
7
+
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
9
+ import { parseCFTemplate, isIntrinsic } from "./cf-refs";
10
+
11
+ export function checkEcrScanOnPush(ctx: PostSynthContext): PostSynthDiagnostic[] {
12
+ const diagnostics: PostSynthDiagnostic[] = [];
13
+
14
+ for (const [_lexicon, output] of ctx.outputs) {
15
+ const template = parseCFTemplate(output);
16
+ if (!template?.Resources) continue;
17
+
18
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
19
+ if (resource.Type !== "AWS::ECR::Repository") continue;
20
+
21
+ const props = resource.Properties ?? {};
22
+ const scanConfig = props.ImageScanningConfiguration;
23
+
24
+ if (isIntrinsic(scanConfig)) continue;
25
+
26
+ const scanOnPush =
27
+ typeof scanConfig === "object" && scanConfig !== null ? (scanConfig as Record<string, unknown>).ScanOnPush : undefined;
28
+
29
+ if (isIntrinsic(scanOnPush)) continue;
30
+
31
+ if (scanOnPush !== true) {
32
+ diagnostics.push({
33
+ checkId: "WAW053",
34
+ severity: "error",
35
+ message: `ECR repository "${logicalId}" does not have ImageScanningConfiguration.ScanOnPush: true — pushed images aren't scanned for vulnerabilities`,
36
+ entity: logicalId,
37
+ lexicon: "aws",
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ return diagnostics;
44
+ }
45
+
46
+ export const waw053: PostSynthCheck = {
47
+ id: "WAW053",
48
+ description: "ECR repository does not scan images on push",
49
+
50
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
51
+ return checkEcrScanOnPush(ctx);
52
+ },
53
+ };
@@ -0,0 +1,49 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw054, checkEcrTagImmutability } from "./waw054";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW054: ECR Tag Mutability Not Immutable", () => {
10
+ test("check metadata", () => {
11
+ expect(waw054.id).toBe("WAW054");
12
+ expect(waw054.description).toContain("immutable");
13
+ });
14
+
15
+ test("flags a repository with no ImageTagMutability set (defaults to MUTABLE)", () => {
16
+ const ctx = makeCtx({
17
+ Resources: { MyRepo: { Type: "AWS::ECR::Repository", Properties: { RepositoryName: "app" } } },
18
+ });
19
+ const diags = checkEcrTagImmutability(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].checkId).toBe("WAW054");
22
+ });
23
+
24
+ test("flags ImageTagMutability: MUTABLE", () => {
25
+ const ctx = makeCtx({
26
+ Resources: { MyRepo: { Type: "AWS::ECR::Repository", Properties: { ImageTagMutability: "MUTABLE" } } },
27
+ });
28
+ expect(checkEcrTagImmutability(ctx)).toHaveLength(1);
29
+ });
30
+
31
+ test("no diagnostic when ImageTagMutability: IMMUTABLE", () => {
32
+ const ctx = makeCtx({
33
+ Resources: { MyRepo: { Type: "AWS::ECR::Repository", Properties: { ImageTagMutability: "IMMUTABLE" } } },
34
+ });
35
+ expect(checkEcrTagImmutability(ctx)).toHaveLength(0);
36
+ });
37
+
38
+ test("skips intrinsic ImageTagMutability values", () => {
39
+ const ctx = makeCtx({
40
+ Resources: { MyRepo: { Type: "AWS::ECR::Repository", Properties: { ImageTagMutability: { Ref: "MutabilityParam" } } } },
41
+ });
42
+ expect(checkEcrTagImmutability(ctx)).toHaveLength(0);
43
+ });
44
+
45
+ test("no diagnostic for non-ECR resources", () => {
46
+ const ctx = makeCtx({ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } } });
47
+ expect(checkEcrTagImmutability(ctx)).toHaveLength(0);
48
+ });
49
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WAW054: ECR Tag Mutability Not Immutable
3
+ *
4
+ * Flags ECR repositories without ImageTagMutability: IMMUTABLE — mutable tags
5
+ * let a previously deployed/scanned/signed tag (e.g. "prod") silently point at
6
+ * different image content later.
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 checkEcrTagImmutability(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::ECR::Repository") continue;
21
+
22
+ const props = resource.Properties ?? {};
23
+ const mutability = props.ImageTagMutability;
24
+
25
+ if (isIntrinsic(mutability)) continue;
26
+
27
+ if (mutability !== "IMMUTABLE") {
28
+ diagnostics.push({
29
+ checkId: "WAW054",
30
+ severity: "error",
31
+ message: `ECR repository "${logicalId}" does not have ImageTagMutability: IMMUTABLE — a tag can silently be repointed at different image content`,
32
+ entity: logicalId,
33
+ lexicon: "aws",
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ }
41
+
42
+ export const waw054: PostSynthCheck = {
43
+ id: "WAW054",
44
+ description: "ECR repository does not have immutable image tags",
45
+
46
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
47
+ return checkEcrTagImmutability(ctx);
48
+ },
49
+ };
@@ -0,0 +1,43 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw055, checkLogsRetention } from "./waw055";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW055: CloudWatch Logs Retention Not Set", () => {
10
+ test("check metadata", () => {
11
+ expect(waw055.id).toBe("WAW055");
12
+ expect(waw055.description).toContain("retention");
13
+ });
14
+
15
+ test("flags a log group with no RetentionInDays", () => {
16
+ const ctx = makeCtx({
17
+ Resources: { MyLogs: { Type: "AWS::Logs::LogGroup", Properties: { LogGroupName: "/app/logs" } } },
18
+ });
19
+ const diags = checkLogsRetention(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].checkId).toBe("WAW055");
22
+ expect(diags[0].severity).toBe("warning");
23
+ });
24
+
25
+ test("no diagnostic when RetentionInDays is set", () => {
26
+ const ctx = makeCtx({
27
+ Resources: { MyLogs: { Type: "AWS::Logs::LogGroup", Properties: { RetentionInDays: 90 } } },
28
+ });
29
+ expect(checkLogsRetention(ctx)).toHaveLength(0);
30
+ });
31
+
32
+ test("skips intrinsic RetentionInDays values", () => {
33
+ const ctx = makeCtx({
34
+ Resources: { MyLogs: { Type: "AWS::Logs::LogGroup", Properties: { RetentionInDays: { Ref: "RetentionParam" } } } },
35
+ });
36
+ expect(checkLogsRetention(ctx)).toHaveLength(0);
37
+ });
38
+
39
+ test("no diagnostic for non-LogGroup resources", () => {
40
+ const ctx = makeCtx({ Resources: { MyBucket: { Type: "AWS::S3::Bucket", Properties: {} } } });
41
+ expect(checkLogsRetention(ctx)).toHaveLength(0);
42
+ });
43
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * WAW055: CloudWatch Logs Retention Not Set
3
+ *
4
+ * Flags Logs::LogGroup resources without RetentionInDays — an unset
5
+ * retention means "Never expire," which is rarely the intended cost/
6
+ * compliance posture.
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 checkLogsRetention(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::Logs::LogGroup") continue;
21
+
22
+ const props = resource.Properties ?? {};
23
+ const retention = props.RetentionInDays;
24
+
25
+ if (isIntrinsic(retention)) continue;
26
+
27
+ if (retention === undefined) {
28
+ diagnostics.push({
29
+ checkId: "WAW055",
30
+ severity: "warning",
31
+ message: `Log group "${logicalId}" has no RetentionInDays set — logs are kept forever by default; set an explicit retention period`,
32
+ entity: logicalId,
33
+ lexicon: "aws",
34
+ });
35
+ }
36
+ }
37
+ }
38
+
39
+ return diagnostics;
40
+ }
41
+
42
+ export const waw055: PostSynthCheck = {
43
+ id: "WAW055",
44
+ description: "CloudWatch Logs log group has no retention period set",
45
+
46
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
47
+ return checkLogsRetention(ctx);
48
+ },
49
+ };
@@ -6,6 +6,8 @@ import {
6
6
  flociHealthUrl,
7
7
  flociEnv,
8
8
  isFlociReady,
9
+ FLOCI_SPEC,
10
+ FLOCI_EMULATOR,
9
11
  } from "./floci";
10
12
 
11
13
  describe("flociRunCommand (#704)", () => {
@@ -55,6 +57,30 @@ describe("flociHealthUrl / flociEnv (#704)", () => {
55
57
  });
56
58
  });
57
59
 
60
+ describe("FLOCI_SPEC / FLOCI_EMULATOR — the aws plugin's emulator capability (#920)", () => {
61
+ test("spec pins the Floci container identity the shared lifecycle boots", () => {
62
+ expect(FLOCI_SPEC.name).toBe("chant-floci");
63
+ expect(FLOCI_SPEC.image).toBe("floci/floci:latest");
64
+ expect(FLOCI_SPEC.containerPort).toBe(4566);
65
+ expect(FLOCI_SPEC.healthPath).toBe("/_localstack/health");
66
+ });
67
+
68
+ test("ready predicate gates on cloudformation appearing in the health body", () => {
69
+ expect(FLOCI_SPEC.ready?.('{"services": {"cloudformation": "available"}}')).toBe(true);
70
+ expect(FLOCI_SPEC.ready?.('{"services": {"s3": "available"}}')).toBe(false);
71
+ });
72
+
73
+ test("capability env redirects the aws SDK at the running endpoint with test creds", () => {
74
+ expect(FLOCI_EMULATOR.spec).toBe(FLOCI_SPEC);
75
+ expect(FLOCI_EMULATOR.env("http://localhost:4566")).toEqual({
76
+ AWS_ENDPOINT_URL: "http://localhost:4566",
77
+ AWS_ACCESS_KEY_ID: "test",
78
+ AWS_SECRET_ACCESS_KEY: "test",
79
+ AWS_REGION: "us-east-1",
80
+ });
81
+ });
82
+ });
83
+
58
84
  describe("isFlociReady (#704)", () => {
59
85
  test("true when the health body names the required service", () => {
60
86
  const body = '{"services": {"s3": "available", "cloudformation": "available"}}';
@@ -1,4 +1,4 @@
1
- import { emulatorLifecycle } from "@intentius/chant/op";
1
+ import { emulatorLifecycle, type EmulatorSpec, type EmulatorCapability } from "@intentius/chant/op";
2
2
 
3
3
  const DEFAULT_NAME = "chant-floci";
4
4
  const DEFAULT_PORT = 4566;
@@ -49,14 +49,30 @@ export function flociEnv(port: number, region: string): Record<string, string> {
49
49
  // AWS runs the LocalStack-compatible floci/floci image, so readiness is gated on
50
50
  // a service (`cloudformation`) appearing in `/_localstack/health` — unlike the
51
51
  // bespoke az/gcp fakes. Shared lifecycle: emulatorLifecycle (#746).
52
- const flociFor = (readyService: string) =>
53
- emulatorLifecycle({
54
- name: DEFAULT_NAME,
55
- image: DEFAULT_IMAGE,
56
- containerPort: DEFAULT_PORT,
57
- healthPath: "/_localstack/health",
58
- ready: (body) => isFlociReady(body, readyService),
59
- });
52
+ const flociSpecFor = (readyService: string): EmulatorSpec => ({
53
+ name: DEFAULT_NAME,
54
+ image: DEFAULT_IMAGE,
55
+ containerPort: DEFAULT_PORT,
56
+ healthPath: "/_localstack/health",
57
+ ready: (body) => isFlociReady(body, readyService),
58
+ });
59
+
60
+ /** The Floci emulator spec — the aws plugin's `emulator` capability (#920). */
61
+ export const FLOCI_SPEC: EmulatorSpec = flociSpecFor(DEFAULT_READY_SERVICE);
62
+
63
+ /** The aws plugin's emulator capability (#920): Floci + the AWS env that points
64
+ * the SDK / `chant graph --live` / a triggered Op at it. */
65
+ export const FLOCI_EMULATOR: EmulatorCapability = {
66
+ spec: FLOCI_SPEC,
67
+ env: (endpoint) => ({
68
+ AWS_ENDPOINT_URL: endpoint,
69
+ AWS_ACCESS_KEY_ID: "test",
70
+ AWS_SECRET_ACCESS_KEY: "test",
71
+ AWS_REGION: DEFAULT_REGION,
72
+ }),
73
+ };
74
+
75
+ const flociFor = (readyService: string) => emulatorLifecycle(flociSpecFor(readyService));
60
76
 
61
77
  const builders = flociFor(DEFAULT_READY_SERVICE);
62
78
 
package/src/plugin.ts CHANGED
@@ -14,6 +14,7 @@ import { readFileSync } from "fs";
14
14
  import { join, dirname } from "path";
15
15
  import { fileURLToPath } from "url";
16
16
  import { awsSerializer } from "./serializer";
17
+ import { FLOCI_EMULATOR } from "./op/activities/floci";
17
18
  import { awsReferenceCatalog } from "./reference-catalog";
18
19
  import { resolveTemplateAttrs } from "./live-attrs";
19
20
  import { CFParser } from "./import/parser";
@@ -31,6 +32,8 @@ import { awsHover } from "./lsp/hover";
31
32
  export const awsPlugin: LexiconPlugin = {
32
33
  name: "aws",
33
34
  serializer: awsSerializer,
35
+ // Local emulator (#920): Floci + the AWS env that redirects the SDK / observe.
36
+ emulator: FLOCI_EMULATOR,
34
37
  // Audit rule metadata for this lexicon's WAW* checks (#687).
35
38
  auditCatalog: () => awsAuditCatalog,
36
39
  // Live edge reconstruction for `chant graph --live` (#778).