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 IAM Access Analyzer delivers to EventBridge when a
11 * finding is generated, updated, or deleted. External-access events carry the
12 * `resource`/`isPublic`/`principal` fields; unused-access events carry the
13 * `findingType`/`resource` (the IAM entity) fields. Fields not shared by both
14 * kinds are optional.
15 */
16export interface FindingDetail {
17 /** Version of the finding event schema. */
18 version?: string;
19 /** The finding id. */
20 id: string;
21 /** Finding status: `ACTIVE`, `ARCHIVED`, or `RESOLVED`. */
22 status?: string;
23 /** ARN of the resource the finding is for. */
24 resource?: string;
25 /** Type of the resource, e.g. `AWS::S3::Bucket` or `AWS::IAM::Role`. */
26 resourceType?: string;
27 /** Account that owns the analyzed resource. */
28 resourceOwnerAccount?: string;
29 /** External-access: whether the policy grants public access. */
30 isPublic?: boolean;
31 /** External-access: the external principal granted access. */
32 principal?: Record<string, string>;
33 /** External-access: the actions granted to the external principal. */
34 action?: string[];
35 /** External-access: condition keys constraining the grant. */
36 condition?: Record<string, string>;
37 /** Unused-access: the finding type, e.g. `UnusedIAMRole`. */
38 findingType?: string;
39 /** When the finding was created. */
40 createdAt?: string;
41 /** When the resource was last analyzed. */
42 analyzedAt?: string;
43 /** When the finding was last updated. */
44 updatedAt?: string;
45 /** Additional event fields (the schema grows over time). */
46 [key: string]: unknown;
47}
48
49/** An Access Analyzer finding EventBridge event delivered to the handler. */
50export type FindingEvent = EventRecord<FindingDetail>;
51
52export interface FindingsEventSourceProps extends EventRouteProps {
53 /**
54 * Logical id for the backing EventBridge rule.
55 * @default "AccessAnalyzerFindings"
56 */
57 id?: string;
58 /**
59 * Which finding events to subscribe to:
60 * - `"external"` — external-access findings (`Access Analyzer Finding`)
61 * - `"unused"` — unused-access findings (`Unused Access Finding for IAM entities`)
62 * - `"all"` — both detail types
63 * @default "all"
64 */
65 kind?: "external" | "unused" | "all";
66}
67
68const DETAIL_TYPES = {
69 external: ["Access Analyzer Finding"],
70 unused: ["Unused Access Finding for IAM entities"],
71 all: ["Access Analyzer Finding", "Unused Access Finding for IAM entities"],
72} as const;
73
74/**
75 * Event source connecting IAM Access Analyzer findings to the hosting
76 * compute. Access Analyzer publishes every finding create/update/delete to
77 * the account's default EventBridge bus (source `aws.access-analyzer`); this
78 * subscribes the host Function to those events so it can alert on or
79 * auto-remediate new findings.
80 *
81 * Analyzers publish to EventBridge automatically — no additional resource is
82 * created besides the EventBridge rule targeting the host. Provide the
83 * host-specific implementation layer (e.g. `AWS.Lambda.EventSource`) on the
84 * Function effect.
85 *
86 * ### Consuming Finding Events
87 * **Example:** Alert on New External-Access Findings
88 * ```typescript
89 * import * as AWS from "alchemy/AWS";
90 *
91 * export default AlertFunction.make(
92 * { main: import.meta.url },
93 * Effect.gen(function* () {
94 * yield* AWS.AccessAnalyzer.consumeFindings(
95 * { kind: "external" },
96 * (events) =>
97 * Stream.runForEach(events, (event) =>
98 * Effect.log(
99 * `finding ${event.detail.id} on ${event.detail.resource}`,
100 * ),
101 * ),
102 * );
103 * return {};
104 * }).pipe(Effect.provide(AWS.Lambda.EventSource)),
105 * );
106 * ```
107 */
108export const consumeFindings = <StreamReq = never, Req = never>(
109 props: FindingsEventSourceProps,
110 process: (
111 events: Stream.Stream<FindingEvent, never, StreamReq>,
112 ) => Effect.Effect<void, never, Req>,
113) =>
114 consumeBusEvents(
115 props.id ?? "AccessAnalyzerFindings",
116 {
117 source: ["aws.access-analyzer"],
118 "detail-type": [...DETAIL_TYPES[props.kind ?? "all"]],
119 },
120 { description: props.description, state: props.state },
121 process,
122 );