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 waitUntilGone = (name: string) =>
17 scc.getFoldersMuteConfigs({ name }).pipe(
18 Effect.as("found" as const),
19 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
20 Effect.repeat({
21 schedule: Schedule.spaced("1 second"),
22 until: (status) => status === "gone",
23 times: 10,
24 }),
25 );
26
27// Folder-level Security Command Center needs a folder under an organization
28// with SCC activated and folder-level SCC roles for the test identity. Set
29// GCP_TEST_SECURITY_CENTER=1 and GOOGLE_FOLDER_ID when both hold.
30const folderId = process.env.GOOGLE_FOLDER_ID;
31const runLifecycle = !!process.env.GCP_TEST_SECURITY_CENTER && !!folderId;
32
33const folderOf = () =>
34 Effect.succeed(
35 folderId === undefined
36 ? ""
37 : folderId.startsWith("folders/")
38 ? folderId
39 : `folders/${folderId}`,
40 );
41
42test.provider(
43 "getFoldersMuteConfigs on a missing config fails with a typed tag",
44 (stack) =>
45 Effect.gen(function* () {
46 yield* stack.destroy();
47
48 const folder = (yield* folderOf()) || "folders/0";
49 const error = yield* Effect.flip(
50 scc.getFoldersMuteConfigs({
51 name: `${folder}/muteConfigs/alchemy-missing`,
52 }),
53 );
54 expect(error._tag).toEqual("ServiceDisabled");
55
56 yield* stack.destroy();
57 }).pipe(logLevel),
58 {
59 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
60 timeout: 90_000,
61 },
62);
63
64test.provider.skipIf(!runLifecycle)(
65 "create, update, and delete a folder mute config",
66 (stack) =>
67 Effect.gen(function* () {
68 yield* stack.destroy();
69
70 const folder = yield* folderOf();
71
72 const created = yield* stack.deploy(
73 Effect.gen(function* () {
74 return yield* GCP.SecurityCenter.FolderMuteConfig("Low", {
75 folder,
76 filter: 'severity="LOW"',
77 description: "mute low findings",
78 });
79 }),
80 );
81
82 expect(created.muteConfigId).toEqual(expect.any(String));
83 expect(created.folder).toEqual(folder);
84 expect(created.name).toEqual(
85 `${folder}/muteConfigs/${created.muteConfigId}`,
86 );
87 expect(created.filter).toEqual('severity="LOW"');
88 expect(created.description).toEqual("mute low findings");
89
90 const fetched = yield* scc.getFoldersMuteConfigs({ name: created.name });
91 expect(fetched.name).toEqual(created.name);
92 expect(fetched.description).toContain("alchemy-id=");
93
94 const updated = yield* stack.deploy(
95 Effect.gen(function* () {
96 return yield* GCP.SecurityCenter.FolderMuteConfig("Low", {
97 folder,
98 muteConfigId: created.muteConfigId,
99 filter: 'severity="MEDIUM"',
100 description: "mute medium findings",
101 });
102 }),
103 );
104
105 expect(updated.name).toEqual(created.name);
106 expect(updated.filter).toEqual('severity="MEDIUM"');
107 expect(updated.description).toEqual("mute medium findings");
108
109 yield* stack.destroy();
110
111 const gone = yield* waitUntilGone(created.name);
112 expect(gone).toEqual("gone");
113 }).pipe(logLevel),
114 {
115 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
116 timeout: 90_000,
117 },
118);