1import * as AWS from "@/AWS";
2import { LogGroup } from "@/AWS/Logs";
3import { LoggingConfiguration, WebACL } from "@/AWS/WAFv2";
4import * as Test from "@/Test/Alchemy";
5import * as wafv2 from "@distilled.cloud/aws/wafv2";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13class LoggingConfigStillExists extends Data.TaggedError(
14 "LoggingConfigStillExists",
15)<{
16 readonly resourceArn: string;
17}> {}
18
19const assertLoggingDeleted = (resourceArn: string) =>
20 wafv2.getLoggingConfiguration({ ResourceArn: resourceArn }).pipe(
21 Effect.flatMap(() =>
22 Effect.fail(new LoggingConfigStillExists({ resourceArn })),
23 ),
24 Effect.catchTag("WAFNonexistentItemException", () => Effect.void),
25 Effect.retry({
26 while: (e) => e._tag === "LoggingConfigStillExists",
27 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
28 }),
29 );
30
31test.provider(
32 "enable logging to CloudWatch Logs, add a filter, delete",
33 (stack) =>
34 Effect.gen(function* () {
35 // reconcile away any prior partial/crashed deployment
36 yield* stack.destroy();
37
38 // Destinations must be named aws-waf-logs-*.
39 const deployed = yield* stack.deploy(
40 Effect.gen(function* () {
41 const acl = yield* WebACL("LoggedAcl", {});
42 const logGroup = yield* LogGroup("WafLogGroup", {
43 logGroupName: "aws-waf-logs-alchemy-wafv2-test",
44 });
45 const logging = yield* LoggingConfiguration("Logging", {
46 resourceArn: acl.webAclArn,
47 logDestinationConfigs: [logGroup.logGroupArn],
48 });
49 return { acl, logging };
50 }),
51 );
52
53 expect(deployed.logging.scope).toBe("REGIONAL");
54 expect(deployed.logging.logDestinationConfigs).toHaveLength(1);
55
56 // out-of-band verification via distilled
57 const created = yield* wafv2.getLoggingConfiguration({
58 ResourceArn: deployed.acl.webAclArn,
59 });
60 expect(
61 created.LoggingConfiguration?.LogDestinationConfigs?.[0],
62 ).toContain("aws-waf-logs-alchemy-wafv2-test");
63 expect(created.LoggingConfiguration?.LoggingFilter).toBeUndefined();
64
65 // update in place: add a redacted field + logging filter
66 yield* stack.deploy(
67 Effect.gen(function* () {
68 const acl = yield* WebACL("LoggedAcl", {});
69 const logGroup = yield* LogGroup("WafLogGroup", {
70 logGroupName: "aws-waf-logs-alchemy-wafv2-test",
71 });
72 const logging = yield* LoggingConfiguration("Logging", {
73 resourceArn: acl.webAclArn,
74 logDestinationConfigs: [logGroup.logGroupArn],
75 redactedFields: [{ SingleHeader: { Name: "authorization" } }],
76 loggingFilter: {
77 DefaultBehavior: "KEEP",
78 Filters: [
79 {
80 Behavior: "DROP",
81 Requirement: "MEETS_ANY",
82 Conditions: [{ ActionCondition: { Action: "ALLOW" } }],
83 },
84 ],
85 },
86 });
87 return { acl, logging };
88 }),
89 );
90
91 const updated = yield* wafv2.getLoggingConfiguration({
92 ResourceArn: deployed.acl.webAclArn,
93 });
94 expect(updated.LoggingConfiguration?.LoggingFilter?.DefaultBehavior).toBe(
95 "KEEP",
96 );
97 expect(
98 updated.LoggingConfiguration?.RedactedFields?.[0]?.SingleHeader?.Name,
99 ).toBe("authorization");
100
101 yield* stack.destroy();
102 yield* assertLoggingDeleted(deployed.acl.webAclArn);
103 }),
104 {
105 tags: ["provider:aws", "provider:aws:logs", "provider:aws:wafv2", "live"],
106 timeout: 300_000,
107 },
108);