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 alwaysTrue = {
18 predicate: { expression: 'resource.name == "alchemy-nonexistent"' },
19 resourceSelector: {
20 resourceTypes: ["compute.googleapis.com/Instance"],
21 },
22 severity: "LOW" as const,
23 description: "always true",
24 recommendation: "n/a",
25};
26
27const waitUntilGone = (name: string) =>
28 scc.getProjectsSecurityHealthAnalyticsSettingsCustomModules({ name }).pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("1 second"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38// Security Command Center is not activated on the testing project (every
39// call fails with ServiceDisabled). Set GCP_TEST_SECURITY_CENTER=1 on a
40// project with SCC activated.
41const runLifecycle = !!process.env.GCP_TEST_SECURITY_CENTER;
42
43test.provider(
44 "getProjectsSecurityHealthAnalyticsSettingsCustomModules on a missing module fails with a typed tag",
45 (stack) =>
46 Effect.gen(function* () {
47 const { project } = yield* GcpEnvironment.current;
48 const parent = `projects/${project}/securityHealthAnalyticsSettings`;
49
50 yield* stack.destroy();
51
52 const error = yield* Effect.flip(
53 scc.getProjectsSecurityHealthAnalyticsSettingsCustomModules({
54 name: `${parent}/customModules/alchemy-missing`,
55 }),
56 );
57 expect(error._tag).toEqual("ServiceDisabled");
58
59 yield* stack.destroy();
60 }).pipe(logLevel),
61 {
62 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
63 timeout: 90_000,
64 },
65);
66
67test.provider.skipIf(!runLifecycle)(
68 "create, update, and delete a security health analytics custom module",
69 (stack) =>
70 Effect.gen(function* () {
71 const { project } = yield* GcpEnvironment.current;
72 const parent = `projects/${project}/securityHealthAnalyticsSettings`;
73
74 yield* stack.destroy();
75
76 const created = yield* stack.deploy(
77 Effect.gen(function* () {
78 return yield* GCP.SecurityCenter.SecurityHealthAnalyticsSettingsCustomModule(
79 "AlwaysTrue",
80 {
81 displayName: "AlchemyAlwaysTrue",
82 customConfig: alwaysTrue,
83 },
84 );
85 }),
86 );
87
88 expect(created.moduleId).toEqual(expect.any(String));
89 expect(created.name).toEqual(
90 `${parent}/customModules/${created.moduleId}`,
91 );
92 expect(created.displayName).toEqual("AlchemyAlwaysTrue");
93 expect(created.customConfig?.description).toEqual("always true");
94 expect(created.enablementState).toEqual("ENABLED");
95
96 const fetched =
97 yield* scc.getProjectsSecurityHealthAnalyticsSettingsCustomModules({
98 name: created.name,
99 });
100 expect(fetched.name).toEqual(created.name);
101 expect(fetched.customConfig?.description).toContain("alchemy-id=");
102
103 const updated = yield* stack.deploy(
104 Effect.gen(function* () {
105 return yield* GCP.SecurityCenter.SecurityHealthAnalyticsSettingsCustomModule(
106 "AlwaysTrue",
107 {
108 moduleId: created.moduleId,
109 displayName: "AlchemyAlwaysTrue",
110 enablementState: "DISABLED",
111 customConfig: {
112 ...alwaysTrue,
113 description: "always true disabled",
114 severity: "MEDIUM",
115 },
116 },
117 );
118 }),
119 );
120
121 expect(updated.name).toEqual(created.name);
122 expect(updated.enablementState).toEqual("DISABLED");
123 expect(updated.customConfig?.description).toEqual("always true disabled");
124 expect(updated.customConfig?.severity).toEqual("MEDIUM");
125
126 yield* stack.destroy();
127
128 const gone = yield* waitUntilGone(created.name);
129 expect(gone).toEqual("gone");
130 }).pipe(logLevel),
131 {
132 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
133 timeout: 90_000,
134 },
135);