1import type * as securityhub from "@distilled.cloud/aws/securityhub";
2import type * as Effect from "effect/Effect";
3import type * as Stream from "effect/Stream";
4import {
5 consumeBusEvents,
6 type EventRecord,
7 type EventRouteProps,
8} from "../EventBridge/EventSource.ts";
9
10/**
11 * The `detail` payload Security Hub delivers to EventBridge — a batch of
12 * findings in AWS Security Finding Format (ASFF).
13 */
14export interface FindingEventDetail {
15 /** The findings the event carries (ASFF documents, up to 25 per event). */
16 findings?: securityhub.AwsSecurityFinding[];
17 /** Additional detail fields (the schema grows over time). */
18 [key: string]: unknown;
19}
20
21/** A Security Hub findings EventBridge event delivered to the handler. */
22export type FindingEvent = EventRecord<FindingEventDetail>;
23
24/**
25 * The `detail` payload of a `Security Hub Findings - Custom Action` event —
26 * the action that was invoked plus the findings it was invoked on.
27 */
28export interface CustomActionEventDetail extends FindingEventDetail {
29 /** Name of the custom action that was invoked. */
30 actionName?: string;
31 /** Description of the custom action that was invoked. */
32 actionDescription?: string;
33}
34
35/** A Security Hub custom-action EventBridge event delivered to the handler. */
36export type CustomActionEvent = EventRecord<CustomActionEventDetail>;
37
38export interface FindingEventSourceProps extends EventRouteProps {
39 /**
40 * Logical id for the backing EventBridge rule.
41 * @default "SecurityHubFindings"
42 */
43 id?: string;
44 /**
45 * Restrict to findings with specific severity labels (matched against
46 * `detail.findings.Severity.Label`), e.g. `["HIGH", "CRITICAL"]`.
47 */
48 severityLabels?: readonly string[];
49 /**
50 * Restrict to findings with specific workflow statuses (matched against
51 * `detail.findings.Workflow.Status`), e.g. `["NEW"]`.
52 */
53 workflowStatuses?: readonly string[];
54}
55
56/**
57 * Event source connecting Security Hub findings to the hosting compute.
58 * Security Hub publishes every new finding and finding update to the
59 * account's default EventBridge bus (source `aws.securityhub`, detail-type
60 * `Security Hub Findings - Imported`); this subscribes the host Function to
61 * those events so it can triage, notify, or auto-remediate.
62 *
63 * Security Hub publishes to EventBridge automatically — no additional
64 * resource is created besides the EventBridge rule targeting the host.
65 * Provide the host-specific implementation layer (e.g.
66 * `AWS.Lambda.EventSource`) on the Function effect.
67 *
68 * ### Consuming Findings
69 * **Example:** Alert on Critical Findings
70 * ```typescript
71 * import * as AWS from "alchemy/AWS";
72 *
73 * export default AlertFunction.make(
74 * { main: import.meta.url },
75 * Effect.gen(function* () {
76 * yield* AWS.SecurityHub.consumeFindings(
77 * { severityLabels: ["CRITICAL"] },
78 * (events) =>
79 * Stream.runForEach(events, (event) =>
80 * Effect.forEach(event.detail.findings ?? [], (finding) =>
81 * Effect.logError(`Security Hub: ${finding.Title}`),
82 * ),
83 * ),
84 * );
85 * return {};
86 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
87 * );
88 * ```
89 */
90export const consumeFindings = <StreamReq = never, Req = never>(
91 props: FindingEventSourceProps,
92 process: (
93 events: Stream.Stream<FindingEvent, never, StreamReq>,
94 ) => Effect.Effect<void, never, Req>,
95) =>
96 consumeBusEvents(
97 props.id ?? "SecurityHubFindings",
98 {
99 source: ["aws.securityhub"],
100 "detail-type": ["Security Hub Findings - Imported"],
101 ...(props.severityLabels !== undefined ||
102 props.workflowStatuses !== undefined
103 ? {
104 detail: {
105 findings: {
106 ...(props.severityLabels !== undefined
107 ? { Severity: { Label: [...props.severityLabels] } }
108 : {}),
109 ...(props.workflowStatuses !== undefined
110 ? { Workflow: { Status: [...props.workflowStatuses] } }
111 : {}),
112 },
113 },
114 }
115 : {}),
116 },
117 { description: props.description, state: props.state },
118 process,
119 );
120
121export interface CustomActionEventSourceProps extends EventRouteProps {
122 /**
123 * Logical id for the backing EventBridge rule.
124 * @default "SecurityHubCustomActions"
125 */
126 id?: string;
127 /**
128 * Restrict to specific custom actions (matched against the event's
129 * `resources`, which carry the action target ARN), e.g.
130 * `[actionTarget.actionTargetArn]`.
131 */
132 actionArns?: readonly string[];
133}
134
135/**
136 * Event source connecting Security Hub custom actions to the hosting
137 * compute. Selecting a custom action on findings or insights in the console
138 * publishes a `Security Hub Findings - Custom Action` event carrying the
139 * selected findings; this subscribes the host Function to those events.
140 * Define the action with {@link ActionTarget}.
141 *
142 * ### Consuming Custom Actions
143 * **Example:** Handle an Escalation Action
144 * ```typescript
145 * yield* AWS.SecurityHub.consumeCustomActions(
146 * { actionArns: [escalate.actionTargetArn] },
147 * (events) =>
148 * Stream.runForEach(events, (event) =>
149 * Effect.log(`${event.detail.actionName}:`, event.detail.findings),
150 * ),
151 * );
152 * ```
153 */
154export const consumeCustomActions = <StreamReq = never, Req = never>(
155 props: CustomActionEventSourceProps,
156 process: (
157 events: Stream.Stream<CustomActionEvent, never, StreamReq>,
158 ) => Effect.Effect<void, never, Req>,
159) =>
160 consumeBusEvents(
161 props.id ?? "SecurityHubCustomActions",
162 {
163 source: ["aws.securityhub"],
164 "detail-type": ["Security Hub Findings - Custom Action"],
165 ...(props.actionArns !== undefined
166 ? { resources: [...props.actionArns] }
167 : {}),
168 },
169 { description: props.description, state: props.state },
170 process,
171 );