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 customConfigV1 = {
17 predicate: { expression: 'resource.name == "alchemy-nonexistent"' },
18 resourceSelector: {
19 resourceTypes: ["compute.googleapis.com/Instance"],
20 },
21 severity: "LOW" as const,
22 description: "unused detector",
23 recommendation: "n/a",
24};
25
26const customConfigV2 = {
27 ...customConfigV1,
28 severity: "MEDIUM" as const,
29 description: "updated unused detector",
30};
31
32const waitUntilGone = (name: string) =>
33 scc.getFoldersSecurityHealthAnalyticsSettingsCustomModules({ name }).pipe(
34 Effect.as("found" as const),
35 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
36 Effect.repeat({
37 schedule: Schedule.spaced("1 second"),
38 until: (status) => status === "gone",
39 times: 10,
40 }),
41 );
42
43// Folder-level Security Command Center needs a folder under an organization
44// with SCC activated and folder-level SCC roles for the test identity. Set
45// GCP_TEST_SECURITY_CENTER=1 and GOOGLE_FOLDER_ID when both hold.
46const folderId = process.env.GOOGLE_FOLDER_ID;
47const runLifecycle = !!process.env.GCP_TEST_SECURITY_CENTER && !!folderId;
48
49const folderOf = () =>
50 Effect.succeed(
51 folderId === undefined
52 ? ""
53 : folderId.startsWith("folders/")
54 ? folderId
55 : `folders/${folderId}`,
56 );
57
58test.provider(
59 "getFoldersSecurityHealthAnalyticsSettingsCustomModules on a missing module fails with a typed tag",
60 (stack) =>
61 Effect.gen(function* () {
62 yield* stack.destroy();
63
64 const folder = (yield* folderOf()) || "folders/0";
65 const error = yield* Effect.flip(
66 scc.getFoldersSecurityHealthAnalyticsSettingsCustomModules({
67 name: `${folder}/securityHealthAnalyticsSettings/customModules/alchemy-missing`,
68 }),
69 );
70 expect(error._tag).toEqual("ServiceDisabled");
71
72 yield* stack.destroy();
73 }).pipe(logLevel),
74 {
75 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
76 timeout: 90_000,
77 },
78);
79
80test.provider.skipIf(!runLifecycle)(
81 "create, update, and delete a folder Security Health Analytics custom module",
82 (stack) =>
83 Effect.gen(function* () {
84 yield* stack.destroy();
85
86 const folder = yield* folderOf();
87 const parent = folder
88 ? `${folder}/securityHealthAnalyticsSettings`
89 : "folders/0/securityHealthAnalyticsSettings";
90
91 const created = yield* stack.deploy(
92 Effect.gen(function* () {
93 return yield* GCP.SecurityCenter.FolderSecurityHealthAnalyticsSettingsCustomModule(
94 "Unused",
95 {
96 folder,
97 customConfig: customConfigV1,
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}/securityHealthAnalyticsSettings/customModules/`,
107 );
108 expect(created.customConfig?.description).toEqual("unused detector");
109
110 const fetched =
111 yield* scc.getFoldersSecurityHealthAnalyticsSettingsCustomModules({
112 name: created.name,
113 });
114 expect(fetched.name).toEqual(created.name);
115 expect(fetched.customConfig?.description).toContain("alchemy-id=");
116
117 const updated = yield* stack.deploy(
118 Effect.gen(function* () {
119 return yield* GCP.SecurityCenter.FolderSecurityHealthAnalyticsSettingsCustomModule(
120 "Unused",
121 {
122 folder,
123 displayName: created.displayName,
124 customConfig: customConfigV2,
125 },
126 );
127 }),
128 );
129
130 expect(updated.name).toEqual(created.name);
131 expect(updated.customConfig?.description).toEqual(
132 "updated unused detector",
133 );
134 expect(updated.customConfig?.severity).toEqual("MEDIUM");
135
136 yield* stack.destroy();
137
138 const gone = yield* waitUntilGone(created.name);
139 expect(gone).toEqual("gone");
140 }).pipe(logLevel),
141 {
142 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
143 timeout: 90_000,
144 },
145);