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 {
11 findOwnedOccurrence,
12 hasOwnershipMarker,
13 ignoreGone,
14 listProjectOccurrences,
15 missingGet,
16 occurrenceAttrs,
17 occurrenceKind,
18 parseDescription,
19 projectParent,
20 reconcileOccurrence,
21 replaceOnIdentity,
22 retryTransient,
23} from "./internal.ts";
24
25export type OccurrenceProps = {
26 /**
27 * Analysis note this occurrence attaches to, as
28 * `projects/{project}/notes/{note}` or a bare note id. Immutable —
29 * changing it replaces the occurrence.
30 */
31 noteName: string;
32 /**
33 * URI of the analyzed resource, for example
34 * `https://gcr.io/project/image@sha256:abc`. Immutable — changing it
35 * replaces the occurrence.
36 */
37 resourceUri: string;
38 /**
39 * Remediation notes. Alchemy stamps ownership into this field for
40 * `list` / nuke because occurrences have no labels API.
41 */
42 remediation?: string;
43 /**
44 * DSSE envelope stored on the occurrence.
45 */
46 envelope?: containeranalysis.Envelope;
47 /**
48 * Attestation details. Used when no other kind is set.
49 */
50 attestation?: containeranalysis.AttestationOccurrence;
51 /**
52 * Build provenance details.
53 */
54 build?: containeranalysis.BuildOccurrence;
55 /**
56 * Discovery details.
57 */
58 discovery?: containeranalysis.DiscoveryOccurrence;
59 /**
60 * Image basis details.
61 */
62 image?: containeranalysis.ImageOccurrence;
63 /**
64 * Package installation details.
65 */
66 package?: containeranalysis.PackageOccurrence;
67 /**
68 * Vulnerability details.
69 */
70 vulnerability?: containeranalysis.VulnerabilityOccurrence;
71 /**
72 * Compliance details.
73 */
74 compliance?: containeranalysis.ComplianceOccurrence;
75 /**
76 * Deployment details.
77 */
78 deployment?: containeranalysis.DeploymentOccurrence;
79 /**
80 * Upgrade details.
81 */
82 upgrade?: containeranalysis.UpgradeOccurrence;
83 /**
84 * DSSE attestation details.
85 */
86 dsseAttestation?: containeranalysis.DSSEAttestationOccurrence;
87 /**
88 * SBOM reference details.
89 */
90 sbomReference?: containeranalysis.SBOMReferenceOccurrence;
91 /**
92 * Secret details.
93 */
94 secret?: containeranalysis.SecretOccurrence;
95 /**
96 * AI skill analysis details.
97 */
98 aiSkillAnalysis?: containeranalysis.AISkillAnalysisOccurrence;
99};
100
101export type Occurrence = Resource<
102 "GCP.ContainerAnalysis.Occurrence",
103 OccurrenceProps,
104 {
105 /** Full resource name `projects/{project}/occurrences/{occurrence}`. */
106 name: string;
107 /** Occurrence id (last path segment, server-assigned). */
108 occurrenceId: string;
109 /** Project id. */
110 project: string;
111 /** Note resource name. */
112 noteName: string;
113 /** Analyzed resource URI. */
114 resourceUri: string;
115 /** User remediation with the Alchemy ownership prefix stripped. */
116 remediation: string | undefined;
117 /** Server-reported analysis kind. */
118 kind: string | undefined;
119 /** DSSE envelope, if any. */
120 envelope: containeranalysis.Envelope | undefined;
121 /** Attestation details. */
122 attestation: containeranalysis.AttestationOccurrence | undefined;
123 /** Build provenance details. */
124 build: containeranalysis.BuildOccurrence | undefined;
125 /** Discovery details. */
126 discovery: containeranalysis.DiscoveryOccurrence | undefined;
127 /** Image basis details. */
128 image: containeranalysis.ImageOccurrence | undefined;
129 /** Package installation details. */
130 package: containeranalysis.PackageOccurrence | undefined;
131 /** Vulnerability details. */
132 vulnerability: containeranalysis.VulnerabilityOccurrence | undefined;
133 /** Compliance details. */
134 compliance: containeranalysis.ComplianceOccurrence | undefined;
135 /** Deployment details. */
136 deployment: containeranalysis.DeploymentOccurrence | undefined;
137 /** Upgrade details. */
138 upgrade: containeranalysis.UpgradeOccurrence | undefined;
139 /** DSSE attestation details. */
140 dsseAttestation: containeranalysis.DSSEAttestationOccurrence | undefined;
141 /** SBOM reference details. */
142 sbomReference: containeranalysis.SBOMReferenceOccurrence | undefined;
143 /** Secret details. */
144 secret: containeranalysis.SecretOccurrence | undefined;
145 /** AI skill analysis details. */
146 aiSkillAnalysis: containeranalysis.AISkillAnalysisOccurrence | undefined;
147 /** RFC3339 creation timestamp. */
148 createTime: string | undefined;
149 /** RFC3339 last-update timestamp. */
150 updateTime: string | undefined;
151 },
152 never,
153 Providers
154>;
155
156/**
157 * A project-scoped Container Analysis (Grafeas) occurrence. Occurrences
158 * attach a note to a concrete resource URI such as a container image
159 * digest.
160 *
161 * The occurrence id is assigned by the API. `noteName` and `resourceUri`
162 * are identity — changing either replaces the occurrence. Occurrences have
163 * no labels field, so Alchemy stamps ownership into `remediation` for
164 * `list` / nuke.
165 *
166 * ### Creating an Occurrence
167 * **Example:** Attest an image
168 * ```typescript
169 * const note = yield* GCP.ContainerAnalysis.Note("Authority", {
170 * attestation: { hint: { humanReadableName: "QA" } },
171 * });
172 * const occurrence = yield* GCP.ContainerAnalysis.Occurrence("Signed", {
173 * noteName: note.name,
174 * resourceUri: "https://example.com/image@sha256:abc",
175 * attestation: {
176 * serializedPayload: btoa("payload"),
177 * signatures: [
178 * { publicKeyId: "https://example.com/keys/qa", signature: btoa("sig") },
179 * ],
180 * },
181 * });
182 * ```
183 *
184 * ### Updating an Occurrence
185 * Re-declare the same logical id with changed props; the engine keeps the
186 * physical resource and updates it in place.
187 *
188 * **Example:** Change remediation text
189 * ```typescript
190 * const occurrence = yield* GCP.ContainerAnalysis.Occurrence("Signed", {
191 * noteName: note.name,
192 * resourceUri: "https://example.com/image@sha256:abc",
193 * remediation: "rebuild from a patched base",
194 * attestation: { hint: { humanReadableName: "QA" } },
195 * });
196 * ```
197 *
198 * @resource
199 * @category ContainerAnalysis
200 */
201export const Occurrence = Resource<Occurrence>(
202 "GCP.ContainerAnalysis.Occurrence",
203);
204
205const getByName = missingGet(containeranalysis.getProjectsOccurrences);
206
207const toPublicAttrs = (
208 occurrence: containeranalysis.Occurrence,
209 project: string,
210): Occurrence["Attributes"] => {
211 const attrs = occurrenceAttrs(occurrence, project);
212 return {
213 name: attrs.name,
214 occurrenceId: attrs.occurrenceId,
215 project: attrs.project,
216 noteName: attrs.noteName,
217 resourceUri: attrs.resourceUri,
218 remediation: attrs.remediation,
219 kind: attrs.kind,
220 envelope: attrs.envelope,
221 attestation: attrs.attestation,
222 build: attrs.build,
223 discovery: attrs.discovery,
224 image: attrs.image,
225 package: attrs.package,
226 vulnerability: attrs.vulnerability,
227 compliance: attrs.compliance,
228 deployment: attrs.deployment,
229 upgrade: attrs.upgrade,
230 dsseAttestation: attrs.dsseAttestation,
231 sbomReference: attrs.sbomReference,
232 secret: attrs.secret,
233 aiSkillAnalysis: attrs.aiSkillAnalysis,
234 createTime: attrs.createTime,
235 updateTime: attrs.updateTime,
236 };
237};
238
239export const OccurrenceProvider = () =>
240 Provider.succeed(Occurrence, {
241 stables: [
242 "name",
243 "occurrenceId",
244 "project",
245 "noteName",
246 "resourceUri",
247 "kind",
248 "createTime",
249 ],
250
251 diff: Effect.fn(function* ({ news, olds, output }) {
252 if (!isResolved(news)) return undefined;
253 const previousKind =
254 occurrenceKind(olds ?? { noteName: "", resourceUri: "" }) ??
255 output?.kind;
256 const nextKind = occurrenceKind(news) ?? previousKind;
257 return replaceOnIdentity({
258 previousParent: olds?.noteName ?? output?.noteName,
259 nextParent: news.noteName,
260 extra:
261 ((olds?.resourceUri ?? output?.resourceUri) !== undefined &&
262 news.resourceUri !== (olds?.resourceUri ?? output?.resourceUri)) ||
263 (previousKind !== undefined &&
264 nextKind !== undefined &&
265 previousKind !== nextKind),
266 });
267 }),
268
269 read: Effect.fn(function* ({ id, olds, output }) {
270 const env = yield* GcpEnvironment.current;
271 const name = output?.name;
272 if (name !== undefined && name.length > 0) {
273 const existing = yield* getByName(name);
274 if (existing === undefined) return undefined;
275 const attrs = toPublicAttrs(existing, env.project);
276 const { labels } = parseDescription(existing.remediation);
277 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
278 }
279 const items = yield* listProjectOccurrences(env.project);
280 const owned = yield* findOwnedOccurrence(id, items, env.project);
281 if (owned === undefined) return undefined;
282 return {
283 name: owned.name,
284 occurrenceId: owned.occurrenceId,
285 project: owned.project,
286 noteName: owned.noteName,
287 resourceUri: owned.resourceUri,
288 remediation: owned.remediation,
289 kind: owned.kind,
290 envelope: owned.envelope,
291 attestation: owned.attestation,
292 build: owned.build,
293 discovery: owned.discovery,
294 image: owned.image,
295 package: owned.package,
296 vulnerability: owned.vulnerability,
297 compliance: owned.compliance,
298 deployment: owned.deployment,
299 upgrade: owned.upgrade,
300 dsseAttestation: owned.dsseAttestation,
301 sbomReference: owned.sbomReference,
302 secret: owned.secret,
303 aiSkillAnalysis: owned.aiSkillAnalysis,
304 createTime: owned.createTime,
305 updateTime: owned.updateTime,
306 };
307 }),
308
309 list: () =>
310 Effect.gen(function* () {
311 const env = yield* GcpEnvironment.current;
312 const items = yield* listProjectOccurrences(env.project);
313 return items
314 .filter((item) => hasOwnershipMarker(item.remediation))
315 .map((item) => toPublicAttrs(item, env.project));
316 }),
317
318 reconcile: Effect.fn(function* ({ id, news, output }) {
319 const env = yield* GcpEnvironment.current;
320 const parent = projectParent(env.project);
321 const current = yield* reconcileOccurrence({
322 id,
323 name: output?.name,
324 parent,
325 project: env.project,
326 location: undefined,
327 news,
328 ops: {
329 get: getByName,
330 create: (request) =>
331 retryTransient(
332 containeranalysis.createProjectsOccurrences({
333 parent: request.parent,
334 body: request.body,
335 }),
336 ),
337 patch: (request) =>
338 retryTransient(
339 containeranalysis.patchProjectsOccurrences({
340 name: request.name,
341 updateMask: request.updateMask,
342 body: request.body,
343 }),
344 ),
345 },
346 });
347 return toPublicAttrs(current, env.project);
348 }),
349
350 delete: Effect.fn(function* ({ output }) {
351 yield* ignoreGone(
352 containeranalysis.deleteProjectsOccurrences({ name: output.name }),
353 );
354 }),
355 });