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