1import type * as Effect from "effect/Effect";
2import type * as Stream from "effect/Stream";
3import {
4 consumeBusEvents,
5 type EventRecord,
6 type EventRouteProps,
7} from "../EventBridge/EventSource.ts";
8
9/**
10 * The `detail` payload ECR delivers to EventBridge on every completed image
11 * push or delete (`ECR Image Action`).
12 */
13export interface ImageActionDetail {
14 /** Whether the action was a `PUSH` or a `DELETE`. */
15 "action-type": "PUSH" | "DELETE";
16 /** Outcome of the action (`SUCCESS` or `FAILURE`). */
17 result: "SUCCESS" | "FAILURE";
18 /** Name of the repository the image was pushed to / deleted from. */
19 "repository-name": string;
20 /** Manifest digest of the affected image, e.g. `sha256:…`. */
21 "image-digest": string;
22 /** Tag of the affected image, when the action targeted a tag. */
23 "image-tag"?: string;
24 /** Media type of the manifest, e.g. `application/vnd.docker.distribution.manifest.v2+json`. */
25 "manifest-media-type"?: string;
26 /** Artifact media type, for OCI artifacts. */
27 "artifact-media-type"?: string;
28 /** Additional event fields (the schema grows over time). */
29 [key: string]: unknown;
30}
31
32/** An ECR image action EventBridge event delivered to the handler. */
33export type ImageActionEvent = EventRecord<ImageActionDetail>;
34
35/**
36 * The `detail` payload ECR delivers to EventBridge when an image scan
37 * completes (`ECR Image Scan`).
38 */
39export interface ImageScanDetail {
40 /** Status of the completed scan, e.g. `COMPLETE`. */
41 "scan-status": string;
42 /** Name of the repository containing the scanned image. */
43 "repository-name": string;
44 /** Manifest digest of the scanned image. */
45 "image-digest": string;
46 /** Tags of the scanned image. */
47 "image-tags"?: string[];
48 /** Vulnerability counts keyed by severity, e.g. `{ HIGH: 2 }`. */
49 "finding-severity-counts"?: Record<string, number>;
50 /** Additional event fields (the schema grows over time). */
51 [key: string]: unknown;
52}
53
54/** An ECR image scan EventBridge event delivered to the handler. */
55export type ImageScanEvent = EventRecord<ImageScanDetail>;
56
57export interface ImageActionsProps extends EventRouteProps {
58 /**
59 * Logical id for the backing EventBridge rule.
60 * @default "EcrImageActions"
61 */
62 id?: string;
63 /**
64 * Only deliver events for these repositories (by name).
65 * @default all repositories
66 */
67 repositories?: string[];
68 /**
69 * Only deliver `PUSH` or `DELETE` actions.
70 * @default both
71 */
72 actionTypes?: ("PUSH" | "DELETE")[];
73 /**
74 * Only deliver actions with these results (e.g. `["SUCCESS"]`).
75 * @default both
76 */
77 results?: ("SUCCESS" | "FAILURE")[];
78}
79
80/**
81 * Event source connecting ECR image pushes and deletes to the hosting
82 * compute. ECR publishes an event to the account's default EventBridge bus
83 * (source `aws.ecr`, detail-type `ECR Image Action`) every time an image
84 * push or delete completes; this subscribes the host Function to those
85 * events so it can react without polling.
86 *
87 * ECR publishes to EventBridge automatically — no additional resource is
88 * created besides the EventBridge rule targeting the host. Provide the
89 * host-specific implementation layer (e.g. `AWS.Lambda.EventSource`) on the
90 * Function effect.
91 *
92 * ### Consuming Image Events
93 * **Example:** React to Successful Pushes
94 * ```typescript
95 * import * as AWS from "alchemy/AWS";
96 *
97 * export default DeployBot.make(
98 * { main: import.meta.url },
99 * Effect.gen(function* () {
100 * yield* AWS.ECR.consumeImageActions(
101 * { actionTypes: ["PUSH"], results: ["SUCCESS"] },
102 * (events) =>
103 * Stream.runForEach(events, (event) =>
104 * Effect.log(
105 * `${event.detail["repository-name"]}:${event.detail["image-tag"]} pushed`,
106 * ),
107 * ),
108 * );
109 * return {};
110 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
111 * );
112 * ```
113 */
114export const consumeImageActions = <StreamReq = never, Req = never>(
115 props: ImageActionsProps,
116 process: (
117 events: Stream.Stream<ImageActionEvent, never, StreamReq>,
118 ) => Effect.Effect<void, never, Req>,
119) =>
120 consumeBusEvents(
121 props.id ?? "EcrImageActions",
122 {
123 source: ["aws.ecr"],
124 "detail-type": ["ECR Image Action"],
125 ...(props.repositories || props.actionTypes || props.results
126 ? {
127 detail: {
128 ...(props.repositories
129 ? { "repository-name": [...props.repositories] }
130 : {}),
131 ...(props.actionTypes
132 ? { "action-type": [...props.actionTypes] }
133 : {}),
134 ...(props.results ? { result: [...props.results] } : {}),
135 },
136 }
137 : {}),
138 },
139 { description: props.description, state: props.state },
140 process,
141 );
142
143export interface ImageScansProps extends EventRouteProps {
144 /**
145 * Logical id for the backing EventBridge rule.
146 * @default "EcrImageScans"
147 */
148 id?: string;
149 /**
150 * Only deliver events for these repositories (by name).
151 * @default all repositories
152 */
153 repositories?: string[];
154}
155
156/**
157 * Event source connecting completed ECR image scans to the hosting compute.
158 * ECR publishes an event to the account's default EventBridge bus (source
159 * `aws.ecr`, detail-type `ECR Image Scan`) when a vulnerability scan
160 * finishes — including scan-on-push scans; this subscribes the host Function
161 * to those events so it can react to new findings without polling.
162 *
163 * ### Consuming Image Events
164 * **Example:** Alert on High-Severity Findings
165 * ```typescript
166 * yield* AWS.ECR.consumeImageScans({}, (events) =>
167 * Stream.runForEach(events, (event) =>
168 * Effect.log(
169 * event.detail["repository-name"],
170 * event.detail["finding-severity-counts"],
171 * ),
172 * ),
173 * );
174 * ```
175 */
176export const consumeImageScans = <StreamReq = never, Req = never>(
177 props: ImageScansProps,
178 process: (
179 events: Stream.Stream<ImageScanEvent, never, StreamReq>,
180 ) => Effect.Effect<void, never, Req>,
181) =>
182 consumeBusEvents(
183 props.id ?? "EcrImageScans",
184 {
185 source: ["aws.ecr"],
186 "detail-type": ["ECR Image Scan"],
187 ...(props.repositories
188 ? { detail: { "repository-name": [...props.repositories] } }
189 : {}),
190 },
191 { description: props.description, state: props.state },
192 process,
193 );