1import * as FMS from "@/AWS/FMS";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12export class FMSTestFunction extends Lambda.Function<Lambda.Function>()(
13 "FMSTestFunction",
14) {}
15
16// The testing organization is deliberately NOT onboarded to Firewall Manager
17// (see AdminAccount.test.ts — onboarding blocks offboarding for >10 minutes),
18// so most routes below prove the binding + IAM grant by observing the exact
19// typed not-an-FMS-admin rejection instead of data:
20// - admin-scoped ops (ListPolicies, ListResourceSets, ListMemberAccounts,
21// GetNotificationChannel, ListAppsLists, ListProtocolsLists) reject with
22// `AccessDeniedException: No default admin could be found for account …`
23// - `ListAdminAccountsForOrganization` (management-account-only) rejects
24// with `InvalidOperationException: No default admin could be found for
25// organization …`
26// - `ListAdminsManagingAccount` rejects with `ResourceNotFoundException:
27// The referenced item does not exist.` when the org has no FMS admins
28// (all verified by live probe). A genuine IAM gap would surface as an
29// UNCAUGHT AccessDeniedException with an "not authorized to perform" message
30// on the admin-management routes, which never catch it — and on an FMS-
31// onboarded account every route returns Ok data unchanged.
32export default FMSTestFunction.make(
33 {
34 main,
35 functionUrl: true,
36 timeout: Duration.seconds(30),
37 },
38 Effect.gen(function* () {
39 // --- policies ---
40 const getPolicy = yield* FMS.GetPolicy();
41 const listPolicies = yield* FMS.ListPolicies();
42 const putPolicy = yield* FMS.PutPolicy();
43 const deletePolicy = yield* FMS.DeletePolicy();
44
45 // --- applications lists ---
46 const getAppsList = yield* FMS.GetAppsList();
47 const listAppsLists = yield* FMS.ListAppsLists();
48 const putAppsList = yield* FMS.PutAppsList();
49 const deleteAppsList = yield* FMS.DeleteAppsList();
50
51 // --- protocols lists ---
52 const getProtocolsList = yield* FMS.GetProtocolsList();
53 const listProtocolsLists = yield* FMS.ListProtocolsLists();
54 const putProtocolsList = yield* FMS.PutProtocolsList();
55 const deleteProtocolsList = yield* FMS.DeleteProtocolsList();
56
57 // --- resource sets ---
58 const getResourceSet = yield* FMS.GetResourceSet();
59 const listResourceSets = yield* FMS.ListResourceSets();
60 const putResourceSet = yield* FMS.PutResourceSet();
61 const deleteResourceSet = yield* FMS.DeleteResourceSet();
62 const listResourceSetResources = yield* FMS.ListResourceSetResources();
63 const batchAssociateResource = yield* FMS.BatchAssociateResource();
64 const batchDisassociateResource = yield* FMS.BatchDisassociateResource();
65 const listDiscoveredResources = yield* FMS.ListDiscoveredResources();
66
67 // --- compliance and protection status ---
68 const getComplianceDetail = yield* FMS.GetComplianceDetail();
69 const listComplianceStatus = yield* FMS.ListComplianceStatus();
70 const getProtectionStatus = yield* FMS.GetProtectionStatus();
71 const getViolationDetails = yield* FMS.GetViolationDetails();
72 const listMemberAccounts = yield* FMS.ListMemberAccounts();
73
74 // --- notification channel ---
75 const getNotificationChannel = yield* FMS.GetNotificationChannel();
76 const putNotificationChannel = yield* FMS.PutNotificationChannel();
77 const deleteNotificationChannel = yield* FMS.DeleteNotificationChannel();
78
79 // --- administrator management (pinned to us-east-1) ---
80 const getAdminScope = yield* FMS.GetAdminScope();
81 const listAdminAccountsForOrganization =
82 yield* FMS.ListAdminAccountsForOrganization();
83 const listAdminsManagingAccount = yield* FMS.ListAdminsManagingAccount();
84
85 // --- third-party firewalls (marketplace-subscription gated) ---
86 const associateThirdPartyFirewall =
87 yield* FMS.AssociateThirdPartyFirewall();
88 const disassociateThirdPartyFirewall =
89 yield* FMS.DisassociateThirdPartyFirewall();
90 const getThirdPartyFirewallAssociationStatus =
91 yield* FMS.GetThirdPartyFirewallAssociationStatus();
92 const listThirdPartyFirewallFirewallPolicies =
93 yield* FMS.ListThirdPartyFirewallFirewallPolicies();
94
95 const bound = {
96 getPolicy,
97 listPolicies,
98 putPolicy,
99 deletePolicy,
100 getAppsList,
101 listAppsLists,
102 putAppsList,
103 deleteAppsList,
104 getProtocolsList,
105 listProtocolsLists,
106 putProtocolsList,
107 deleteProtocolsList,
108 getResourceSet,
109 listResourceSets,
110 putResourceSet,
111 deleteResourceSet,
112 listResourceSetResources,
113 batchAssociateResource,
114 batchDisassociateResource,
115 listDiscoveredResources,
116 getComplianceDetail,
117 listComplianceStatus,
118 getProtectionStatus,
119 getViolationDetails,
120 listMemberAccounts,
121 getNotificationChannel,
122 putNotificationChannel,
123 deleteNotificationChannel,
124 getAdminScope,
125 listAdminAccountsForOrganization,
126 listAdminsManagingAccount,
127 associateThirdPartyFirewall,
128 disassociateThirdPartyFirewall,
129 getThirdPartyFirewallAssociationStatus,
130 listThirdPartyFirewallFirewallPolicies,
131 };
132
133 return {
134 fetch: Effect.gen(function* () {
135 const request = yield* HttpServerRequest;
136 const url = new URL(request.originalUrl);
137 const pathname = url.pathname;
138
139 if (request.method === "GET" && pathname === "/bindings") {
140 return yield* HttpServerResponse.json({
141 bound: Object.keys(bound),
142 });
143 }
144
145 if (
146 request.method === "GET" &&
147 pathname === "/admins-managing-account"
148 ) {
149 const result = yield* listAdminsManagingAccount().pipe(
150 Effect.map((r) => ({
151 tag: "Ok",
152 count: (r.AdminAccounts ?? []).length,
153 })),
154 Effect.catchTag("ResourceNotFoundException", (e) =>
155 Effect.succeed({ tag: e._tag, count: 0 }),
156 ),
157 );
158 return yield* HttpServerResponse.json(result);
159 }
160
161 if (
162 request.method === "GET" &&
163 pathname === "/admin-accounts-for-organization"
164 ) {
165 const result = yield* listAdminAccountsForOrganization().pipe(
166 Effect.map((r) => ({
167 tag: "Ok",
168 count: (r.AdminAccounts ?? []).length,
169 })),
170 Effect.catchTag(
171 ["InvalidOperationException", "ResourceNotFoundException"],
172 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
173 ),
174 );
175 return yield* HttpServerResponse.json(result);
176 }
177
178 if (request.method === "GET" && pathname === "/policies") {
179 const result = yield* listPolicies().pipe(
180 Effect.map((r) => ({
181 tag: "Ok",
182 count: (r.PolicyList ?? []).length,
183 })),
184 Effect.catchTag(
185 [
186 "AccessDeniedException",
187 "InvalidOperationException",
188 "ResourceNotFoundException",
189 ],
190 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
191 ),
192 );
193 return yield* HttpServerResponse.json(result);
194 }
195
196 if (request.method === "GET" && pathname === "/resource-sets") {
197 const result = yield* listResourceSets().pipe(
198 Effect.map((r) => ({
199 tag: "Ok",
200 count: (r.ResourceSets ?? []).length,
201 })),
202 Effect.catchTag(
203 ["AccessDeniedException", "InvalidOperationException"],
204 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
205 ),
206 );
207 return yield* HttpServerResponse.json(result);
208 }
209
210 if (request.method === "GET" && pathname === "/member-accounts") {
211 const result = yield* listMemberAccounts().pipe(
212 Effect.map((r) => ({
213 tag: "Ok",
214 count: (r.MemberAccounts ?? []).length,
215 })),
216 Effect.catchTag(
217 ["AccessDeniedException", "ResourceNotFoundException"],
218 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
219 ),
220 );
221 return yield* HttpServerResponse.json(result);
222 }
223
224 if (request.method === "GET" && pathname === "/notification-channel") {
225 const result = yield* getNotificationChannel().pipe(
226 Effect.map((r) => ({
227 tag: "Ok",
228 topicArn: r.SnsTopicArn ?? null,
229 })),
230 Effect.catchTag(
231 [
232 "AccessDeniedException",
233 "InvalidOperationException",
234 "ResourceNotFoundException",
235 ],
236 (e) => Effect.succeed({ tag: e._tag, topicArn: null }),
237 ),
238 );
239 return yield* HttpServerResponse.json(result);
240 }
241
242 if (request.method === "GET" && pathname === "/apps-lists") {
243 const result = yield* listAppsLists({ MaxResults: 10 }).pipe(
244 Effect.map((r) => ({
245 tag: "Ok",
246 count: (r.AppsLists ?? []).length,
247 })),
248 Effect.catchTag(
249 [
250 "AccessDeniedException",
251 "InvalidOperationException",
252 "ResourceNotFoundException",
253 ],
254 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
255 ),
256 );
257 return yield* HttpServerResponse.json(result);
258 }
259
260 if (request.method === "GET" && pathname === "/protocols-lists") {
261 const result = yield* listProtocolsLists({ MaxResults: 10 }).pipe(
262 Effect.map((r) => ({
263 tag: "Ok",
264 count: (r.ProtocolsLists ?? []).length,
265 })),
266 Effect.catchTag(
267 [
268 "AccessDeniedException",
269 "InvalidOperationException",
270 "ResourceNotFoundException",
271 ],
272 (e) => Effect.succeed({ tag: e._tag, count: 0 }),
273 ),
274 );
275 return yield* HttpServerResponse.json(result);
276 }
277
278 if (
279 request.method === "GET" &&
280 pathname === "/third-party-firewall-status"
281 ) {
282 const result = yield* getThirdPartyFirewallAssociationStatus({
283 ThirdPartyFirewall: "PALO_ALTO_NETWORKS_CLOUD_NGFW",
284 }).pipe(
285 Effect.map((r) => ({
286 tag: "Ok",
287 status: r.ThirdPartyFirewallStatus ?? null,
288 marketplaceStatus: r.MarketplaceOnboardingStatus ?? null,
289 })),
290 Effect.catchTag(
291 [
292 "AccessDeniedException",
293 "InvalidOperationException",
294 "InvalidInputException",
295 "ResourceNotFoundException",
296 ],
297 (e) =>
298 Effect.succeed({
299 tag: e._tag,
300 status: null,
301 marketplaceStatus: null,
302 }),
303 ),
304 );
305 return yield* HttpServerResponse.json(result);
306 }
307
308 return yield* HttpServerResponse.json(
309 { error: "Not found", method: request.method, pathname },
310 { status: 404 },
311 );
312 }).pipe(Effect.orDie),
313 };
314 }).pipe(
315 Effect.provide(
316 Layer.mergeAll(
317 Lambda.EventSource,
318 FMS.GetPolicyHttp,
319 FMS.ListPoliciesHttp,
320 FMS.PutPolicyHttp,
321 FMS.DeletePolicyHttp,
322 FMS.GetAppsListHttp,
323 FMS.ListAppsListsHttp,
324 FMS.PutAppsListHttp,
325 FMS.DeleteAppsListHttp,
326 FMS.GetProtocolsListHttp,
327 FMS.ListProtocolsListsHttp,
328 FMS.PutProtocolsListHttp,
329 FMS.DeleteProtocolsListHttp,
330 FMS.GetResourceSetHttp,
331 FMS.ListResourceSetsHttp,
332 FMS.PutResourceSetHttp,
333 FMS.DeleteResourceSetHttp,
334 FMS.ListResourceSetResourcesHttp,
335 FMS.BatchAssociateResourceHttp,
336 FMS.BatchDisassociateResourceHttp,
337 FMS.ListDiscoveredResourcesHttp,
338 FMS.GetComplianceDetailHttp,
339 FMS.ListComplianceStatusHttp,
340 FMS.GetProtectionStatusHttp,
341 FMS.GetViolationDetailsHttp,
342 FMS.ListMemberAccountsHttp,
343 FMS.GetNotificationChannelHttp,
344 FMS.PutNotificationChannelHttp,
345 FMS.DeleteNotificationChannelHttp,
346 FMS.GetAdminScopeHttp,
347 FMS.ListAdminAccountsForOrganizationHttp,
348 FMS.ListAdminsManagingAccountHttp,
349 FMS.AssociateThirdPartyFirewallHttp,
350 FMS.DisassociateThirdPartyFirewallHttp,
351 FMS.GetThirdPartyFirewallAssociationStatusHttp,
352 FMS.ListThirdPartyFirewallFirewallPoliciesHttp,
353 ),
354 ),
355 ),
356);