1import * as containeranalysis from "@distilled.cloud/gcp/containeranalysis_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import { hasAlchemyLabels } from "../Labels.ts";
9import type { Providers } from "../Providers.ts";
10import type { OccurrenceProps } from "./Occurrence.ts";
11import {
12 findOwnedOccurrence,
13 hasOwnershipMarker,
14 ignoreGone,
15 listLocationOccurrences,
16 locationParent,
17 missingGet,
18 normalizeLocation,
19 occurrenceAttrs,
20 occurrenceKind,
21 parseDescription,
22 reconcileOccurrence,
23 replaceOnIdentity,
24 retryTransient,
25} from "./internal.ts";
26
27export type LocationsOccurrenceProps = OccurrenceProps & {
28 /**
29 * Location (`us-central1`, `us-east1`, …). Immutable — changing it
30 * replaces the occurrence. `US-CENTRAL1` is accepted and normalized to
31 * `us-central1`.
32 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
33 */
34 location?: string;
35};
36
37export type LocationsOccurrence = Resource<
38 "GCP.ContainerAnalysis.LocationsOccurrence",
39 LocationsOccurrenceProps,
40 {
41 /** Full resource name `projects/{project}/locations/{location}/occurrences/{occurrence}`. */
42 name: string;
43 /** Occurrence id (last path segment, server-assigned). */
44 occurrenceId: string;
45 /** Project id. */
46 project: string;
47 /** Location id. */
48 location: string;
49 /** Note resource name. */
50 noteName: string;
51 /** Analyzed resource URI. */
52 resourceUri: string;
53 /** User remediation with the Alchemy ownership prefix stripped. */
54 remediation: string | undefined;
55 /** Server-reported analysis kind. */
56 kind: string | undefined;
57 /** DSSE envelope, if any. */
58 envelope: containeranalysis.Envelope | undefined;
59 /** Attestation details. */
60 attestation: containeranalysis.AttestationOccurrence | undefined;
61 /** Build provenance details. */
62 build: containeranalysis.BuildOccurrence | undefined;
63 /** Discovery details. */
64 discovery: containeranalysis.DiscoveryOccurrence | undefined;
65 /** Image basis details. */
66 image: containeranalysis.ImageOccurrence | undefined;
67 /** Package installation details. */
68 package: containeranalysis.PackageOccurrence | undefined;
69 /** Vulnerability details. */
70 vulnerability: containeranalysis.VulnerabilityOccurrence | undefined;
71 /** Compliance details. */
72 compliance: containeranalysis.ComplianceOccurrence | undefined;
73 /** Deployment details. */
74 deployment: containeranalysis.DeploymentOccurrence | undefined;
75 /** Upgrade details. */
76 upgrade: containeranalysis.UpgradeOccurrence | undefined;
77 /** DSSE attestation details. */
78 dsseAttestation: containeranalysis.DSSEAttestationOccurrence | undefined;
79 /** SBOM reference details. */
80 sbomReference: containeranalysis.SBOMReferenceOccurrence | undefined;
81 /** Secret details. */
82 secret: containeranalysis.SecretOccurrence | undefined;
83 /** AI skill analysis details. */
84 aiSkillAnalysis: containeranalysis.AISkillAnalysisOccurrence | undefined;
85 /** RFC3339 creation timestamp. */
86 createTime: string | undefined;
87 /** RFC3339 last-update timestamp. */
88 updateTime: string | undefined;
89 },
90 never,
91 Providers
92>;
93
94/**
95 * A location-scoped Container Analysis (Grafeas) occurrence. Same shape as
96 * {@link Occurrence}, stored at
97 * `projects/{project}/locations/{location}/occurrences/{occurrence}`.
98 *
99 * Location, `noteName`, and `resourceUri` are identity. Alchemy stamps
100 * ownership into `remediation` for `list` / nuke.
101 *
102 * ### Creating a Locations Occurrence
103 * **Example:** Regional attestation
104 * ```typescript
105 * const note = yield* GCP.ContainerAnalysis.LocationsNote("Authority", {
106 * location: "us-central1",
107 * attestation: { hint: { humanReadableName: "QA" } },
108 * });
109 * const occurrence = yield* GCP.ContainerAnalysis.LocationsOccurrence(
110 * "Signed",
111 * {
112 * location: "us-central1",
113 * noteName: note.name,
114 * resourceUri: "https://example.com/image@sha256:abc",
115 * attestation: {
116 * serializedPayload: btoa("payload"),
117 * signatures: [
118 * {
119 * publicKeyId: "https://example.com/keys/qa",
120 * signature: btoa("sig"),
121 * },
122 * ],
123 * },
124 * },
125 * );
126 * ```
127 *
128 * @resource
129 * @category ContainerAnalysis
130 */
131export const LocationsOccurrence = Resource<LocationsOccurrence>(
132 "GCP.ContainerAnalysis.LocationsOccurrence",
133);
134
135const getByName = missingGet(containeranalysis.getProjectsLocationsOccurrences);
136
137const toPublicAttrs = (
138 occurrence: containeranalysis.Occurrence,
139 project: string,
140 region: string,
141): LocationsOccurrence["Attributes"] => {
142 const attrs = occurrenceAttrs(occurrence, project);
143 return {
144 name: attrs.name,
145 occurrenceId: attrs.occurrenceId,
146 project: attrs.project,
147 location: attrs.location ?? region,
148 noteName: attrs.noteName,
149 resourceUri: attrs.resourceUri,
150 remediation: attrs.remediation,
151 kind: attrs.kind,
152 envelope: attrs.envelope,
153 attestation: attrs.attestation,
154 build: attrs.build,
155 discovery: attrs.discovery,
156 image: attrs.image,
157 package: attrs.package,
158 vulnerability: attrs.vulnerability,
159 compliance: attrs.compliance,
160 deployment: attrs.deployment,
161 upgrade: attrs.upgrade,
162 dsseAttestation: attrs.dsseAttestation,
163 sbomReference: attrs.sbomReference,
164 secret: attrs.secret,
165 aiSkillAnalysis: attrs.aiSkillAnalysis,
166 createTime: attrs.createTime,
167 updateTime: attrs.updateTime,
168 };
169};
170
171export const LocationsOccurrenceProvider = () =>
172 Provider.succeed(LocationsOccurrence, {
173 stables: [
174 "name",
175 "occurrenceId",
176 "project",
177 "location",
178 "noteName",
179 "resourceUri",
180 "kind",
181 "createTime",
182 ],
183
184 diff: Effect.fn(function* ({ news, olds, output }) {
185 if (!isResolved(news)) return undefined;
186 const env = yield* GcpEnvironment.current;
187 const previousKind =
188 occurrenceKind(olds ?? { noteName: "", resourceUri: "" }) ??
189 output?.kind;
190 const nextKind = occurrenceKind(news) ?? previousKind;
191 return replaceOnIdentity({
192 previousParent: locationParent(
193 "x",
194 normalizeLocation(olds?.location ?? output?.location, env.region),
195 ),
196 nextParent: locationParent(
197 "x",
198 normalizeLocation(
199 news.location ?? olds?.location ?? output?.location,
200 env.region,
201 ),
202 ),
203 extra:
204 ((olds?.noteName ?? output?.noteName) !== undefined &&
205 news.noteName !== (olds?.noteName ?? output?.noteName)) ||
206 ((olds?.resourceUri ?? output?.resourceUri) !== undefined &&
207 news.resourceUri !== (olds?.resourceUri ?? output?.resourceUri)) ||
208 (previousKind !== undefined &&
209 nextKind !== undefined &&
210 previousKind !== nextKind),
211 });
212 }),
213
214 read: Effect.fn(function* ({ id, olds, output }) {
215 const env = yield* GcpEnvironment.current;
216 const name = output?.name;
217 if (name !== undefined && name.length > 0) {
218 const existing = yield* getByName(name);
219 if (existing === undefined) return undefined;
220 const attrs = toPublicAttrs(existing, env.project, env.region);
221 const { labels } = parseDescription(existing.remediation);
222 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
223 }
224 const location = normalizeLocation(
225 olds?.location ?? output?.location,
226 env.region,
227 );
228 const items = yield* listLocationOccurrences(env.project, location);
229 const owned = yield* findOwnedOccurrence(id, items, env.project);
230 if (owned === undefined) return undefined;
231 return {
232 name: owned.name,
233 occurrenceId: owned.occurrenceId,
234 project: owned.project,
235 location: owned.location ?? location,
236 noteName: owned.noteName,
237 resourceUri: owned.resourceUri,
238 remediation: owned.remediation,
239 kind: owned.kind,
240 envelope: owned.envelope,
241 attestation: owned.attestation,
242 build: owned.build,
243 discovery: owned.discovery,
244 image: owned.image,
245 package: owned.package,
246 vulnerability: owned.vulnerability,
247 compliance: owned.compliance,
248 deployment: owned.deployment,
249 upgrade: owned.upgrade,
250 dsseAttestation: owned.dsseAttestation,
251 sbomReference: owned.sbomReference,
252 secret: owned.secret,
253 aiSkillAnalysis: owned.aiSkillAnalysis,
254 createTime: owned.createTime,
255 updateTime: owned.updateTime,
256 };
257 }),
258
259 list: () =>
260 Effect.gen(function* () {
261 const env = yield* GcpEnvironment.current;
262 const items = yield* listLocationOccurrences(env.project, env.region);
263 return items
264 .filter((item) => hasOwnershipMarker(item.remediation))
265 .map((item) => toPublicAttrs(item, env.project, env.region));
266 }),
267
268 reconcile: Effect.fn(function* ({ id, news, output }) {
269 const env = yield* GcpEnvironment.current;
270 const location = normalizeLocation(
271 news.location ?? output?.location,
272 env.region,
273 );
274 const parent = locationParent(env.project, location);
275 const current = yield* reconcileOccurrence({
276 id,
277 name: output?.name,
278 parent,
279 project: env.project,
280 location,
281 news,
282 ops: {
283 get: getByName,
284 create: (request) =>
285 retryTransient(
286 containeranalysis.createProjectsLocationsOccurrences({
287 parent: request.parent,
288 body: request.body,
289 }),
290 ),
291 patch: (request) =>
292 retryTransient(
293 containeranalysis.patchProjectsLocationsOccurrences({
294 name: request.name,
295 updateMask: request.updateMask,
296 body: request.body,
297 }),
298 ),
299 },
300 });
301 return toPublicAttrs(current, env.project, env.region);
302 }),
303
304 delete: Effect.fn(function* ({ output }) {
305 yield* ignoreGone(
306 containeranalysis.deleteProjectsLocationsOccurrences({
307 name: output.name,
308 }),
309 );
310 }),
311 });