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.getOrganizationsEventThreatDetectionSettingsCustomModules({ 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// Organization-level Security Command Center needs SCC activated on the
42// organization and org-level SCC roles for the test identity. Set
43// GCP_TEST_SECURITY_CENTER=1 and GOOGLE_ORGANIZATION_ID when both hold.
44const organizationId = process.env.GOOGLE_ORGANIZATION_ID;
45const runLifecycle = !!process.env.GCP_TEST_SECURITY_CENTER && !!organizationId;
46
47const organizationOf = () =>
48 Effect.succeed(
49 organizationId === undefined
50 ? ""
51 : organizationId.startsWith("organizations/")
52 ? organizationId
53 : `organizations/${organizationId}`,
54 );
55
56test.provider(
57 "getOrganizationsEventThreatDetectionSettingsCustomModules on a missing module fails with a typed tag",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 const organization = (yield* organizationOf()) || "organizations/0";
63 const error = yield* Effect.flip(
64 scc.getOrganizationsEventThreatDetectionSettingsCustomModules({
65 name: `${organization}/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 an organization Event Threat Detection custom module",
80 (stack) =>
81 Effect.gen(function* () {
82 yield* stack.destroy();
83
84 const organization = yield* organizationOf();
85 const parent = organization
86 ? `${organization}/eventThreatDetectionSettings`
87 : "organizations/0/eventThreatDetectionSettings";
88
89 const created = yield* stack.deploy(
90 Effect.gen(function* () {
91 return yield* GCP.SecurityCenter.OrganizationEventThreatDetectionSettingsCustomModule(
92 "BadIp",
93 {
94 organization,
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.organization).toEqual(organization);
105 expect(created.name).toContain(
106 `${organization}/eventThreatDetectionSettings/customModules/`,
107 );
108 expect(created.description).toEqual("test bad ip");
109
110 const fetched =
111 yield* scc.getOrganizationsEventThreatDetectionSettingsCustomModules({
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.OrganizationEventThreatDetectionSettingsCustomModule(
120 "BadIp",
121 {
122 organization,
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);