1import * as macie2 from "@distilled.cloud/aws/macie2";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import { AWSEnvironment } from "../Environment.ts";
7import type { Providers } from "../Providers.ts";
8
9/**
10 * How frequently Amazon Macie publishes updated policy findings.
11 */
12export type FindingPublishingFrequency =
13 | "FIFTEEN_MINUTES"
14 | "ONE_HOUR"
15 | "SIX_HOURS";
16
17/**
18 * The Macie enablement status. `ENABLED` runs Macie; `PAUSED` suspends it
19 * without disabling the account entirely.
20 */
21export type MacieStatus = "PAUSED" | "ENABLED";
22
23export interface SessionProps {
24 /**
25 * Whether Macie is enabled (`ENABLED`) or suspended (`PAUSED`) for the
26 * account. A paused session stops all Macie activity but keeps the account
27 * enrolled.
28 * @default "ENABLED"
29 */
30 status?: MacieStatus;
31
32 /**
33 * How frequently Macie publishes updated policy findings to EventBridge and
34 * Security Hub.
35 * @default "SIX_HOURS"
36 */
37 findingPublishingFrequency?: FindingPublishingFrequency;
38}
39
40/** @resource */
41export interface Session extends Resource<
42 "AWS.Macie2.Session",
43 SessionProps,
44 {
45 /** The account the Macie session belongs to. */
46 accountId: string;
47 /** Current Macie status (`ENABLED` / `PAUSED`). */
48 status: string | undefined;
49 /** The effective finding-publishing frequency. */
50 findingPublishingFrequency: string | undefined;
51 /** ARN of the Macie service-linked role. */
52 serviceRole: string | undefined;
53 /** ISO timestamp of when the Macie session was created. */
54 createdAt: string | undefined;
55 },
56 never,
57 Providers
58> {}
59
60/**
61 * The Amazon Macie session — the account/region singleton that enables Macie
62 * data-security scanning. Only one session can exist per region, so this is a
63 * capture-and-restore singleton: Macie exposes no session tags, so ownership is
64 * tracked by Alchemy state. Adopting a session that Alchemy did not create
65 * requires `--adopt`, and destroy disables Macie for the account.
66 *
67 * ### Enabling Macie
68 * **Example:** Enable Macie
69 * ```typescript
70 * const macie = yield* Macie2.Session("Macie", {});
71 * ```
72 *
73 * **Example:** Enable with frequent findings and pause later
74 * ```typescript
75 * const macie = yield* Macie2.Session("Macie", {
76 * status: "ENABLED",
77 * findingPublishingFrequency: "FIFTEEN_MINUTES",
78 * });
79 * ```
80 */
81const SessionResource = Resource<Session>("AWS.Macie2.Session");
82
83export { SessionResource as Session };
84
85// `getMacieSession` throws `AccessDeniedException` ("Macie is not enabled")
86// when the account has no session and `ResourceNotFoundException` transiently
87// right after enablement — both mean "no session", so collapse to `undefined`.
88const getSession = macie2.getMacieSession({}).pipe(
89 Effect.map((s) => s as macie2.GetMacieSessionResponse | undefined),
90 Effect.catchTag("AccessDeniedException", () => Effect.succeed(undefined)),
91 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(undefined)),
92);
93
94const buildAttrs = (accountId: string, s: macie2.GetMacieSessionResponse) => ({
95 accountId,
96 status: s.status,
97 findingPublishingFrequency: s.findingPublishingFrequency,
98 serviceRole: s.serviceRole,
99 createdAt: s.createdAt?.toISOString(),
100});
101
102export const SessionProvider = () =>
103 Provider.effect(
104 SessionResource,
105 Effect.gen(function* () {
106 return {
107 read: Effect.fn(function* ({ output }) {
108 const session = yield* getSession;
109 if (!session) return undefined;
110 const accountId =
111 output?.accountId ?? (yield* AWSEnvironment.current).accountId;
112 const attrs = buildAttrs(accountId, session);
113 // Macie has no session-level tags, so ownership cannot be verified
114 // from the cloud. If we have no prior state (`output` absent) but a
115 // session already exists, treat it as foreign and gate adoption.
116 return output ? attrs : Unowned(attrs);
117 }),
118
119 // Account/region singleton — report the single session, if any.
120 list: () =>
121 Effect.gen(function* () {
122 const session = yield* getSession;
123 if (!session) return [];
124 const { accountId } = yield* AWSEnvironment.current;
125 return [buildAttrs(accountId, session)];
126 }),
127
128 reconcile: Effect.fn(function* ({ news = {}, session }) {
129 const { accountId } = yield* AWSEnvironment.current;
130 const desiredStatus = news.status ?? "ENABLED";
131
132 // 1. OBSERVE — cloud state is authoritative.
133 let live = yield* getSession;
134
135 // 2. ENSURE — enable Macie if there is no session.
136 if (!live) {
137 yield* macie2.enableMacie({
138 status: desiredStatus,
139 findingPublishingFrequency: news.findingPublishingFrequency,
140 });
141 live = yield* macie2.getMacieSession({});
142 } else {
143 // 3. SYNC — observed ↔ desired status + finding frequency.
144 const statusChanged = live.status !== desiredStatus;
145 const freqChanged =
146 news.findingPublishingFrequency !== undefined &&
147 news.findingPublishingFrequency !==
148 live.findingPublishingFrequency;
149 if (statusChanged || freqChanged) {
150 yield* macie2.updateMacieSession({
151 status: desiredStatus,
152 findingPublishingFrequency: news.findingPublishingFrequency,
153 });
154 }
155 }
156
157 // 4. RETURN fresh attributes.
158 const final = yield* macie2.getMacieSession({});
159 yield* session.note(accountId);
160 return buildAttrs(accountId, final);
161 }),
162
163 delete: Effect.fn(function* () {
164 yield* macie2.disableMacie({}).pipe(
165 Effect.catchTag("AccessDeniedException", () => Effect.void),
166 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
167 );
168 }),
169 };
170 }),
171 );