1import type * as NFW from "@distilled.cloud/aws/network-firewall";
2import * as nfw from "@distilled.cloud/aws/network-firewall";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
5import { Unowned } from "../../AdoptPolicy.ts";
6import { deepEqual, isResolved } from "../../Diff.ts";
7import { createPhysicalName } from "../../PhysicalName.ts";
8import * as Provider from "../../Provider.ts";
9import { Resource } from "../../Resource.ts";
10import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts";
11import type { Providers } from "../Providers.ts";
12import {
13 nfwTagsToRecord,
14 recordToNfwTags,
15 retryWhileNfwInUse,
16 retryWhileNfwNotFound,
17} from "./internal.ts";
18
19export interface FirewallPolicyProps {
20 /**
21 * Name of the firewall policy. Must be 1-128 alphanumeric characters or
22 * hyphens. If omitted, a deterministic physical name is generated.
23 * Changing the name replaces the policy.
24 */
25 firewallPolicyName?: string;
26 /**
27 * The policy definition: stateless/stateful rule group references,
28 * default actions, and engine options. Uses raw Network Firewall API
29 * structures. `StatelessDefaultActions` and
30 * `StatelessFragmentDefaultActions` are required.
31 */
32 firewallPolicy: NFW.FirewallPolicy;
33 /**
34 * Human-readable description of the firewall policy.
35 */
36 description?: string;
37 /**
38 * Tags to apply to the policy. Merged with internal Alchemy tags.
39 */
40 tags?: Record<string, string>;
41}
42
43export interface FirewallPolicy extends Resource<
44 "AWS.NetworkFirewall.FirewallPolicy",
45 FirewallPolicyProps,
46 {
47 /** Name of the firewall policy. */
48 firewallPolicyName: string;
49 /** ARN of the firewall policy. */
50 firewallPolicyArn: string;
51 /** Server-assigned unique id of the firewall policy. */
52 firewallPolicyId: string;
53 },
54 never,
55 Providers
56> {}
57
58/**
59 * An AWS Network Firewall policy — defines a firewall's traffic inspection
60 * behavior as a collection of stateless and stateful
61 * {@link RuleGroup | rule group} references plus default actions. One policy
62 * can be shared by multiple {@link Firewall | firewalls}.
63 * ### Creating Policies
64 * **Example:** Pass-everything Policy
65 * ```typescript
66 * import * as NetworkFirewall from "alchemy/AWS/NetworkFirewall";
67 *
68 * const policy = yield* NetworkFirewall.FirewallPolicy("Policy", {
69 * firewallPolicy: {
70 * StatelessDefaultActions: ["aws:pass"],
71 * StatelessFragmentDefaultActions: ["aws:pass"],
72 * },
73 * });
74 * ```
75 *
76 * **Example:** Policy referencing Rule Groups
77 * ```typescript
78 * const stateless = yield* NetworkFirewall.RuleGroup("Stateless", {
79 * type: "STATELESS",
80 * capacity: 10,
81 * ruleGroup: { ... },
82 * });
83 *
84 * const policy = yield* NetworkFirewall.FirewallPolicy("Policy", {
85 * firewallPolicy: {
86 * StatelessDefaultActions: ["aws:forward_to_sfe"],
87 * StatelessFragmentDefaultActions: ["aws:forward_to_sfe"],
88 * StatelessRuleGroupReferences: [
89 * { ResourceArn: stateless.ruleGroupArn, Priority: 1 },
90 * ],
91 * },
92 * });
93 * ```
94 *
95 * @resource
96 */
97export const FirewallPolicy = Resource<FirewallPolicy>(
98 "AWS.NetworkFirewall.FirewallPolicy",
99);
100
101export const FirewallPolicyProvider = () =>
102 Provider.effect(
103 FirewallPolicy,
104 Effect.gen(function* () {
105 const createName = Effect.fn(function* (
106 id: string,
107 props: { firewallPolicyName?: string },
108 ) {
109 return (
110 props.firewallPolicyName ??
111 (yield* createPhysicalName({ id, maxLength: 128 }))
112 );
113 });
114
115 const toAttrs = (
116 response: NFW.FirewallPolicyResponse,
117 ): FirewallPolicy["Attributes"] => ({
118 firewallPolicyName: response.FirewallPolicyName,
119 firewallPolicyArn: response.FirewallPolicyArn,
120 firewallPolicyId: response.FirewallPolicyId,
121 });
122
123 return FirewallPolicy.Provider.of({
124 stables: [
125 "firewallPolicyName",
126 "firewallPolicyArn",
127 "firewallPolicyId",
128 ],
129
130 list: () =>
131 Effect.gen(function* () {
132 const pages = yield* nfw.listFirewallPolicies
133 .pages({})
134 .pipe(Stream.runCollect);
135 const metas = Array.from(pages).flatMap(
136 (page) => page.FirewallPolicies ?? [],
137 );
138 const items = yield* Effect.forEach(
139 metas,
140 (meta) =>
141 nfw
142 .describeFirewallPolicy({ FirewallPolicyArn: meta.Arn })
143 .pipe(
144 Effect.map((r) => toAttrs(r.FirewallPolicyResponse)),
145 Effect.catchTag("ResourceNotFoundException", () =>
146 Effect.succeed(undefined),
147 ),
148 ),
149 { concurrency: 5 },
150 );
151 return items.filter(
152 (item): item is FirewallPolicy["Attributes"] =>
153 item !== undefined,
154 );
155 }),
156
157 read: Effect.fn(function* ({ id, olds, output }) {
158 const name =
159 output?.firewallPolicyName ?? (yield* createName(id, olds ?? {}));
160 const found = yield* nfw
161 .describeFirewallPolicy({ FirewallPolicyName: name })
162 .pipe(
163 Effect.catchTag("ResourceNotFoundException", () =>
164 Effect.succeed(undefined),
165 ),
166 );
167 if (found === undefined) return undefined;
168 const attrs = toAttrs(found.FirewallPolicyResponse);
169 const tags = nfwTagsToRecord(found.FirewallPolicyResponse.Tags);
170 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
171 }),
172
173 diff: Effect.fn(function* ({ id, news, olds }) {
174 if (!isResolved(news)) return undefined;
175 const oldName = yield* createName(id, olds);
176 const newName = yield* createName(id, news);
177 if (oldName !== newName) {
178 return { action: "replace" } as const;
179 }
180 }),
181
182 reconcile: Effect.fn(function* ({ id, news, output, session }) {
183 const name =
184 output?.firewallPolicyName ?? (yield* createName(id, news));
185 const internalTags = yield* createInternalTags(id);
186 const desiredTags: Record<string, string> = {
187 ...news.tags,
188 ...internalTags,
189 };
190
191 // 1. Observe — cloud state is authoritative.
192 let observed = yield* nfw
193 .describeFirewallPolicy({ FirewallPolicyName: name })
194 .pipe(
195 Effect.catchTag("ResourceNotFoundException", () =>
196 Effect.succeed(undefined),
197 ),
198 );
199
200 // 2. Ensure — create if missing.
201 if (observed === undefined) {
202 yield* nfw.createFirewallPolicy({
203 FirewallPolicyName: name,
204 FirewallPolicy: news.firewallPolicy,
205 Description: news.description,
206 Tags: recordToNfwTags(desiredTags),
207 });
208 observed = yield* nfw
209 .describeFirewallPolicy({ FirewallPolicyName: name })
210 .pipe(retryWhileNfwNotFound);
211 }
212
213 // 3. Sync definition — compare desired against OBSERVED state.
214 const observedDescription =
215 observed.FirewallPolicyResponse.Description;
216 const definitionDiffers =
217 !deepEqual(news.firewallPolicy, observed.FirewallPolicy) ||
218 (news.description ?? undefined) !== observedDescription;
219 if (definitionDiffers) {
220 yield* nfw.updateFirewallPolicy({
221 UpdateToken: observed.UpdateToken,
222 FirewallPolicyArn:
223 observed.FirewallPolicyResponse.FirewallPolicyArn,
224 FirewallPolicy: news.firewallPolicy,
225 Description: news.description,
226 });
227 }
228
229 // 3b. Sync tags — diff against OBSERVED cloud tags.
230 const observedTags = nfwTagsToRecord(
231 observed.FirewallPolicyResponse.Tags,
232 );
233 const { upsert, removed } = diffTags(observedTags, desiredTags);
234 const arn = observed.FirewallPolicyResponse.FirewallPolicyArn;
235 if (upsert.length > 0) {
236 yield* nfw.tagResource({ ResourceArn: arn, Tags: upsert });
237 }
238 if (removed.length > 0) {
239 yield* nfw.untagResource({ ResourceArn: arn, TagKeys: removed });
240 }
241
242 yield* session.note(name);
243 return toAttrs(observed.FirewallPolicyResponse);
244 }),
245
246 delete: Effect.fn(function* ({ output }) {
247 // A policy still associated to a firewall (deleted moments ago by
248 // the engine) rejects deletion with InvalidOperationException —
249 // retry through the release window.
250 yield* nfw
251 .deleteFirewallPolicy({
252 FirewallPolicyArn: output.firewallPolicyArn,
253 })
254 .pipe(
255 retryWhileNfwInUse,
256 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
257 );
258 }),
259 });
260 }),
261 );