1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import * as scc from "@distilled.cloud/gcp/securitycenter_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const badIpConfig = {
18 metadata: {
19 severity: "LOW",
20 description: "test",
21 recommendation: "investigate",
22 },
23 ips: ["192.0.2.1"],
24};
25
26const waitUntilGone = (name: string) =>
27 scc.getProjectsEventThreatDetectionSettingsCustomModules({ name }).pipe(
28 Effect.as("found" as const),
29 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
30 Effect.repeat({
31 schedule: Schedule.spaced("1 second"),
32 until: (status) => status === "gone",
33 times: 10,
34 }),
35 );
36
37// Security Command Center is not activated on the testing project (every
38// call fails with ServiceDisabled). Set GCP_TEST_SECURITY_CENTER=1 on a
39// project with SCC activated.
40const runLifecycle = !!process.env.GCP_TEST_SECURITY_CENTER;
41
42test.provider(
43 "getProjectsEventThreatDetectionSettingsCustomModules on a missing module fails with a typed tag",
44 (stack) =>
45 Effect.gen(function* () {
46 const { project } = yield* GcpEnvironment.current;
47 const parent = `projects/${project}/eventThreatDetectionSettings`;
48
49 yield* stack.destroy();
50
51 const error = yield* Effect.flip(
52 scc.getProjectsEventThreatDetectionSettingsCustomModules({
53 name: `${parent}/customModules/alchemy-missing`,
54 }),
55 );
56 expect(error._tag).toEqual("ServiceDisabled");
57
58 yield* stack.destroy();
59 }).pipe(logLevel),
60 {
61 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
62 timeout: 90_000,
63 },
64);
65
66test.provider.skipIf(!runLifecycle)(
67 "create, update, and delete an event threat detection custom module",
68 (stack) =>
69 Effect.gen(function* () {
70 const { project } = yield* GcpEnvironment.current;
71 const parent = `projects/${project}/eventThreatDetectionSettings`;
72
73 yield* stack.destroy();
74
75 const created = yield* stack.deploy(
76 Effect.gen(function* () {
77 return yield* GCP.SecurityCenter.EventThreatDetectionSettingsCustomModule(
78 "BadIp",
79 {
80 type: "CONFIGURABLE_BAD_IP",
81 displayName: "alchemy_bad_ip",
82 description: "test bad ip",
83 config: badIpConfig,
84 },
85 );
86 }),
87 );
88
89 expect(created.moduleId).toEqual(expect.any(String));
90 expect(created.name).toEqual(
91 `${parent}/customModules/${created.moduleId}`,
92 );
93 expect(created.type).toEqual("CONFIGURABLE_BAD_IP");
94 expect(created.description).toEqual("test bad ip");
95 expect(created.enablementState).toEqual("ENABLED");
96
97 const fetched =
98 yield* scc.getProjectsEventThreatDetectionSettingsCustomModules({
99 name: created.name,
100 });
101 expect(fetched.name).toEqual(created.name);
102 expect(fetched.description).toContain("alchemy-id=");
103
104 const updated = yield* stack.deploy(
105 Effect.gen(function* () {
106 return yield* GCP.SecurityCenter.EventThreatDetectionSettingsCustomModule(
107 "BadIp",
108 {
109 moduleId: created.moduleId,
110 type: "CONFIGURABLE_BAD_IP",
111 displayName: "alchemy_bad_ip",
112 description: "updated bad ip",
113 enablementState: "DISABLED",
114 config: {
115 ...badIpConfig,
116 ips: ["192.0.2.1", "192.0.2.0/24"],
117 },
118 },
119 );
120 }),
121 );
122
123 expect(updated.name).toEqual(created.name);
124 expect(updated.description).toEqual("updated bad ip");
125 expect(updated.enablementState).toEqual("DISABLED");
126
127 yield* stack.destroy();
128
129 const gone = yield* waitUntilGone(created.name);
130 expect(gone).toEqual("gone");
131 }).pipe(logLevel),
132 {
133 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
134 timeout: 90_000,
135 },
136);