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 GuardDuty delivers to EventBridge for a finding —
11 * the full finding document. Only the commonly-matched fields are typed;
12 * the schema grows over time.
13 */
14export interface FindingEventDetail {
15 /** The finding id. */
16 id?: string;
17 /** The finding type, e.g. `Recon:EC2/PortProbeUnprotectedPort`. */
18 type?: string;
19 /** The finding severity (0.1–8+; 7+ is high). */
20 severity?: number;
21 /** The account the finding was generated in. */
22 accountId?: string;
23 /** The region the finding was generated in. */
24 region?: string;
25 /** The finding's ARN. */
26 arn?: string;
27 /** The finding title. */
28 title?: string;
29 /** The finding description. */
30 description?: string;
31 /** The affected resource document. */
32 resource?: Record<string, unknown>;
33 /** The detection service document (counts, first/last seen, …). */
34 service?: Record<string, unknown>;
35 /** Additional finding fields (the schema grows over time). */
36 [key: string]: unknown;
37}
38
39/** A GuardDuty finding EventBridge event delivered to the handler. */
40export type FindingEvent = EventRecord<FindingEventDetail>;
41
42export interface FindingEventSourceProps extends EventRouteProps {
43 /**
44 * Logical id for the backing EventBridge rule.
45 * @default "GuardDutyFindings"
46 */
47 id?: string;
48 /**
49 * Restrict to specific finding types (matched against `detail.type`),
50 * e.g. `["Recon:EC2/PortProbeUnprotectedPort"]`.
51 */
52 types?: readonly string[];
53 /**
54 * Restrict to specific numeric severities (matched against
55 * `detail.severity`). GuardDuty publishes updated findings at the
56 * detector's `findingPublishingFrequency`, but new findings arrive
57 * within about five minutes.
58 */
59 severities?: readonly number[];
60}
61
62/**
63 * Event source connecting GuardDuty findings to the hosting compute.
64 * GuardDuty publishes every new finding (and periodic updates for
65 * recurring ones) to the account's default EventBridge bus (source
66 * `aws.guardduty`, detail-type `GuardDuty Finding`); this subscribes the
67 * host Function to those events so it can triage, notify, or auto-remediate.
68 *
69 * GuardDuty publishes to EventBridge automatically — no additional
70 * resource is created besides the EventBridge rule targeting the host.
71 * Provide the host-specific implementation layer (e.g.
72 * `AWS.Lambda.EventSource`) on the Function effect.
73 *
74 * ### Consuming Findings
75 * **Example:** Alert on High-Severity Findings
76 * ```typescript
77 * import * as AWS from "alchemy/AWS";
78 *
79 * export default AlertFunction.make(
80 * { main: import.meta.url },
81 * Effect.gen(function* () {
82 * yield* AWS.GuardDuty.consumeFindings({}, (events) =>
83 * Stream.runForEach(events, (event) =>
84 * (event.detail.severity ?? 0) >= 7
85 * ? Effect.logError(
86 * `GuardDuty: ${event.detail.type} on ${event.detail.accountId}`,
87 * )
88 * : Effect.void,
89 * ),
90 * );
91 * return {};
92 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
93 * );
94 * ```
95 */
96export const consumeFindings = <StreamReq = never, Req = never>(
97 props: FindingEventSourceProps,
98 process: (
99 events: Stream.Stream<FindingEvent, never, StreamReq>,
100 ) => Effect.Effect<void, never, Req>,
101) =>
102 consumeBusEvents(
103 props.id ?? "GuardDutyFindings",
104 {
105 source: ["aws.guardduty"],
106 "detail-type": ["GuardDuty Finding"],
107 ...(props.types !== undefined || props.severities !== undefined
108 ? {
109 detail: {
110 ...(props.types !== undefined ? { type: [...props.types] } : {}),
111 ...(props.severities !== undefined
112 ? { severity: [...props.severities] }
113 : {}),
114 },
115 }
116 : {}),
117 },
118 { description: props.description, state: props.state },
119 process,
120 );