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