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.getOrganizationsBigQueryExports({ 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// Organization-level Security Command Center needs SCC activated on the
29// organization and org-level SCC roles for the test identity. Set
30// GCP_TEST_SECURITY_CENTER=1 and GOOGLE_ORGANIZATION_ID when both hold.
31const organizationId = process.env.GOOGLE_ORGANIZATION_ID;
32const runLifecycle = !!process.env.GCP_TEST_SECURITY_CENTER && !!organizationId;
33
34const organizationOf = () =>
35 Effect.succeed(
36 organizationId === undefined
37 ? ""
38 : organizationId.startsWith("organizations/")
39 ? organizationId
40 : `organizations/${organizationId}`,
41 );
42
43test.provider(
44 "getOrganizationsBigQueryExports on a missing export fails with a typed tag",
45 (stack) =>
46 Effect.gen(function* () {
47 yield* stack.destroy();
48
49 const organization = (yield* organizationOf()) || "organizations/0";
50 const error = yield* Effect.flip(
51 scc.getOrganizationsBigQueryExports({
52 name: `${organization}/bigQueryExports/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 an organization BigQuery export",
67 (stack) =>
68 Effect.gen(function* () {
69 const { project } = yield* GcpEnvironment.current;
70
71 yield* stack.destroy();
72
73 const organization = yield* organizationOf();
74
75 const created = yield* stack.deploy(
76 Effect.gen(function* () {
77 const dataset = yield* GCP.BigQuery.Dataset("SccOrgExport", {
78 location: "US",
79 forceDestroy: true,
80 });
81 const exp = yield* GCP.SecurityCenter.OrganizationBigQueryExport(
82 "Findings",
83 {
84 organization,
85 dataset: `projects/${project}/datasets/${dataset.datasetId}`,
86 filter: 'state="ACTIVE"',
87 description: "active findings",
88 },
89 );
90 return { exp, datasetId: dataset.datasetId };
91 }),
92 );
93
94 expect(created.exp.exportId).toEqual(expect.any(String));
95 expect(created.exp.organization).toEqual(organization);
96 expect(created.exp.name).toEqual(
97 `${organization}/bigQueryExports/${created.exp.exportId}`,
98 );
99 expect(created.exp.filter).toEqual('state="ACTIVE"');
100 expect(created.exp.description).toEqual("active findings");
101
102 const fetched = yield* scc.getOrganizationsBigQueryExports({
103 name: created.exp.name,
104 });
105 expect(fetched.name).toEqual(created.exp.name);
106 expect(fetched.description).toContain("alchemy-id=");
107
108 const updated = yield* stack.deploy(
109 Effect.gen(function* () {
110 const dataset = yield* GCP.BigQuery.Dataset("SccOrgExport", {
111 datasetId: created.datasetId,
112 location: "US",
113 forceDestroy: true,
114 });
115 return yield* GCP.SecurityCenter.OrganizationBigQueryExport(
116 "Findings",
117 {
118 organization,
119 exportId: created.exp.exportId,
120 dataset: `projects/${project}/datasets/${dataset.datasetId}`,
121 filter: 'state="INACTIVE"',
122 description: "inactive findings",
123 },
124 );
125 }),
126 );
127
128 expect(updated.name).toEqual(created.exp.name);
129 expect(updated.filter).toEqual('state="INACTIVE"');
130 expect(updated.description).toEqual("inactive findings");
131
132 yield* stack.destroy();
133
134 const gone = yield* waitUntilGone(created.exp.name);
135 expect(gone).toEqual("gone");
136 }).pipe(logLevel),
137 {
138 tags: ["provider:gcp", "provider:gcp:securitycenter", "live"],
139 timeout: 90_000,
140 },
141);