1import * as Inspector2 from "@/AWS/Inspector2";
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 Inspector2TestFunction extends Lambda.Function<Lambda.Function>()(
13 "Inspector2TestFunction",
14) {}
15
16/**
17 * Routes answer `{ …fields }` on success or `{ errorTag }` when the
18 * operation fails with a TYPED error — the test asserts the tag is in a
19 * route-specific allowlist, which proves both the binding wiring and the
20 * IAM grant. An untyped error crashes into a 500 instead.
21 */
22const errorTagged = <A, E extends { _tag: string }, R>(
23 effect: Effect.Effect<A, E, R>,
24): Effect.Effect<A | { errorTag: string; errorMessage?: string }, never, R> =>
25 effect.pipe(
26 Effect.map((a): A | { errorTag: string; errorMessage?: string } => a),
27 Effect.catch((e) =>
28 Effect.succeed({
29 errorTag: e._tag,
30 errorMessage:
31 (e as { message?: string }).message ??
32 (e as { Message?: string }).Message,
33 }),
34 ),
35 );
36
37// One binding per Inspector2 capability. Write/org-admin operations with
38// irreversible or cross-account side effects (UpdateConfiguration changes
39// account scan settings; AssociateMember / Enable-DisableDelegatedAdmin
40// require an organization; UpdateEncryptionKey re-keys scan results) are
41// bound — proving init + IAM deploy — but not exercised against live state.
42export default Inspector2TestFunction.make(
43 {
44 main,
45 functionUrl: true,
46 timeout: Duration.seconds(30),
47 },
48 Effect.gen(function* () {
49 // Findings & code snippets
50 const listFindings = yield* Inspector2.ListFindings();
51 const listFindingAggregations = yield* Inspector2.ListFindingAggregations();
52 const batchGetFindingDetails = yield* Inspector2.BatchGetFindingDetails();
53 const batchGetCodeSnippet = yield* Inspector2.BatchGetCodeSnippet();
54
55 // Coverage & vulnerability intel
56 const listCoverage = yield* Inspector2.ListCoverage();
57 const listCoverageStatistics = yield* Inspector2.ListCoverageStatistics();
58 const searchVulnerabilities = yield* Inspector2.SearchVulnerabilities();
59 const getClustersForImage = yield* Inspector2.GetClustersForImage();
60
61 // Findings reports & SBOM exports
62 const createFindingsReport = yield* Inspector2.CreateFindingsReport();
63 const getFindingsReportStatus = yield* Inspector2.GetFindingsReportStatus();
64 const cancelFindingsReport = yield* Inspector2.CancelFindingsReport();
65 const createSbomExport = yield* Inspector2.CreateSbomExport();
66 const getSbomExport = yield* Inspector2.GetSbomExport();
67 const cancelSbomExport = yield* Inspector2.CancelSbomExport();
68
69 // CIS scan results
70 const listCisScans = yield* Inspector2.ListCisScans();
71 const listCisScanResultsAggregatedByChecks =
72 yield* Inspector2.ListCisScanResultsAggregatedByChecks();
73 const listCisScanResultsAggregatedByTargetResource =
74 yield* Inspector2.ListCisScanResultsAggregatedByTargetResource();
75 const getCisScanResultDetails = yield* Inspector2.GetCisScanResultDetails();
76 const getCisScanReport = yield* Inspector2.GetCisScanReport();
77
78 // Code security scans
79 const startCodeSecurityScan = yield* Inspector2.StartCodeSecurityScan();
80 const getCodeSecurityScan = yield* Inspector2.GetCodeSecurityScan();
81
82 // Account settings & usage
83 const listUsageTotals = yield* Inspector2.ListUsageTotals();
84 const batchGetFreeTrialInfo = yield* Inspector2.BatchGetFreeTrialInfo();
85 const listAccountPermissions = yield* Inspector2.ListAccountPermissions();
86 const getConfiguration = yield* Inspector2.GetConfiguration();
87 const updateConfiguration = yield* Inspector2.UpdateConfiguration();
88 const getEc2DeepInspectionConfiguration =
89 yield* Inspector2.GetEc2DeepInspectionConfiguration();
90 const updateEc2DeepInspectionConfiguration =
91 yield* Inspector2.UpdateEc2DeepInspectionConfiguration();
92 const getEncryptionKey = yield* Inspector2.GetEncryptionKey();
93 const updateEncryptionKey = yield* Inspector2.UpdateEncryptionKey();
94 const resetEncryptionKey = yield* Inspector2.ResetEncryptionKey();
95
96 // Organization & members
97 const getMember = yield* Inspector2.GetMember();
98 const listMembers = yield* Inspector2.ListMembers();
99 const associateMember = yield* Inspector2.AssociateMember();
100 const disassociateMember = yield* Inspector2.DisassociateMember();
101 const getDelegatedAdminAccount =
102 yield* Inspector2.GetDelegatedAdminAccount();
103 const listDelegatedAdminAccounts =
104 yield* Inspector2.ListDelegatedAdminAccounts();
105 const enableDelegatedAdminAccount =
106 yield* Inspector2.EnableDelegatedAdminAccount();
107 const disableDelegatedAdminAccount =
108 yield* Inspector2.DisableDelegatedAdminAccount();
109 const describeOrganizationConfiguration =
110 yield* Inspector2.DescribeOrganizationConfiguration();
111 const updateOrganizationConfiguration =
112 yield* Inspector2.UpdateOrganizationConfiguration();
113 const updateOrgEc2DeepInspectionConfiguration =
114 yield* Inspector2.UpdateOrgEc2DeepInspectionConfiguration();
115 const batchGetMemberEc2DeepInspectionStatus =
116 yield* Inspector2.BatchGetMemberEc2DeepInspectionStatus();
117 const batchUpdateMemberEc2DeepInspectionStatus =
118 yield* Inspector2.BatchUpdateMemberEc2DeepInspectionStatus();
119
120 const bound = {
121 listFindings,
122 listFindingAggregations,
123 batchGetFindingDetails,
124 batchGetCodeSnippet,
125 listCoverage,
126 listCoverageStatistics,
127 searchVulnerabilities,
128 getClustersForImage,
129 createFindingsReport,
130 getFindingsReportStatus,
131 cancelFindingsReport,
132 createSbomExport,
133 getSbomExport,
134 cancelSbomExport,
135 listCisScans,
136 listCisScanResultsAggregatedByChecks,
137 listCisScanResultsAggregatedByTargetResource,
138 getCisScanResultDetails,
139 getCisScanReport,
140 startCodeSecurityScan,
141 getCodeSecurityScan,
142 listUsageTotals,
143 batchGetFreeTrialInfo,
144 listAccountPermissions,
145 getConfiguration,
146 updateConfiguration,
147 getEc2DeepInspectionConfiguration,
148 updateEc2DeepInspectionConfiguration,
149 getEncryptionKey,
150 updateEncryptionKey,
151 resetEncryptionKey,
152 getMember,
153 listMembers,
154 associateMember,
155 disassociateMember,
156 getDelegatedAdminAccount,
157 listDelegatedAdminAccounts,
158 enableDelegatedAdminAccount,
159 disableDelegatedAdminAccount,
160 describeOrganizationConfiguration,
161 updateOrganizationConfiguration,
162 updateOrgEc2DeepInspectionConfiguration,
163 batchGetMemberEc2DeepInspectionStatus,
164 batchUpdateMemberEc2DeepInspectionStatus,
165 };
166
167 return {
168 fetch: Effect.gen(function* () {
169 const request = yield* HttpServerRequest;
170 const url = new URL(request.originalUrl);
171 const pathname = url.pathname;
172
173 if (request.method === "GET" && pathname === "/bindings") {
174 return yield* HttpServerResponse.json({
175 bound: Object.keys(bound),
176 });
177 }
178
179 // Findings — works whether or not Inspector scanning is enabled
180 // (an un-enabled account just has none).
181 if (request.method === "GET" && pathname === "/findings") {
182 const result = yield* errorTagged(
183 listFindings({ maxResults: 10 }).pipe(
184 Effect.map((r) => ({ count: (r.findings ?? []).length })),
185 ),
186 );
187 return yield* HttpServerResponse.json(result);
188 }
189
190 if (request.method === "GET" && pathname === "/coverage") {
191 const result = yield* errorTagged(
192 listCoverage({ maxResults: 10 }).pipe(
193 Effect.map((r) => ({
194 count: (r.coveredResources ?? []).length,
195 })),
196 ),
197 );
198 return yield* HttpServerResponse.json(result);
199 }
200
201 // Vulnerability intel — the public vulnerability database.
202 if (request.method === "GET" && pathname === "/vulnerability") {
203 const id = url.searchParams.get("id") ?? "CVE-2021-44228";
204 const result = yield* errorTagged(
205 searchVulnerabilities({
206 filterCriteria: { vulnerabilityIds: [id] },
207 }).pipe(
208 Effect.map((r) => ({
209 ids: (r.vulnerabilities ?? []).map((v) => v.id),
210 })),
211 ),
212 );
213 return yield* HttpServerResponse.json(result);
214 }
215
216 if (request.method === "GET" && pathname === "/usage") {
217 const result = yield* errorTagged(
218 listUsageTotals({}).pipe(
219 Effect.map((r) => ({ totals: (r.totals ?? []).length })),
220 ),
221 );
222 return yield* HttpServerResponse.json(result);
223 }
224
225 if (request.method === "GET" && pathname === "/account-permissions") {
226 const result = yield* errorTagged(
227 listAccountPermissions({}).pipe(
228 Effect.map((r) => ({
229 permissions: (r.permissions ?? []).length,
230 })),
231 ),
232 );
233 return yield* HttpServerResponse.json(result);
234 }
235
236 if (request.method === "GET" && pathname === "/free-trial") {
237 const account = url.searchParams.get("account") ?? "";
238 const result = yield* errorTagged(
239 batchGetFreeTrialInfo({ accountIds: [account] }).pipe(
240 Effect.map((r) => ({
241 accounts: (r.accounts ?? []).length,
242 failed: (r.failedAccounts ?? []).length,
243 })),
244 ),
245 );
246 return yield* HttpServerResponse.json(result);
247 }
248
249 if (request.method === "GET" && pathname === "/config") {
250 const result = yield* errorTagged(
251 getConfiguration({}).pipe(
252 Effect.map((r) => ({
253 hasEcrConfiguration: r.ecrConfiguration !== undefined,
254 hasEc2Configuration: r.ec2Configuration !== undefined,
255 })),
256 ),
257 );
258 return yield* HttpServerResponse.json(result);
259 }
260
261 if (request.method === "GET" && pathname === "/deep-inspection") {
262 const result = yield* errorTagged(
263 getEc2DeepInspectionConfiguration({}).pipe(
264 Effect.map((r) => ({ status: r.status ?? "UNSET" })),
265 ),
266 );
267 return yield* HttpServerResponse.json(result);
268 }
269
270 if (request.method === "GET" && pathname === "/encryption-key") {
271 const result = yield* errorTagged(
272 getEncryptionKey({
273 resourceType: "AWS_ECR_CONTAINER_IMAGE",
274 scanType: "PACKAGE",
275 }).pipe(Effect.map((r) => ({ kmsKeyId: r.kmsKeyId }))),
276 );
277 return yield* HttpServerResponse.json(result);
278 }
279
280 if (request.method === "GET" && pathname === "/cis-scans") {
281 const result = yield* errorTagged(
282 listCisScans({}).pipe(
283 Effect.map((r) => ({ scans: (r.scans ?? []).length })),
284 ),
285 );
286 return yield* HttpServerResponse.json(result);
287 }
288
289 if (request.method === "GET" && pathname === "/members") {
290 const result = yield* errorTagged(
291 listMembers({}).pipe(
292 Effect.map((r) => ({ members: (r.members ?? []).length })),
293 ),
294 );
295 return yield* HttpServerResponse.json(result);
296 }
297
298 // Org administration: a standalone (non-organization) account gets a
299 // typed rejection, which still proves the binding + IAM wiring.
300 if (request.method === "GET" && pathname === "/org-config") {
301 const result = yield* errorTagged(
302 describeOrganizationConfiguration({}).pipe(
303 Effect.map((r) => ({
304 maxAccountLimitReached: r.maxAccountLimitReached ?? false,
305 })),
306 ),
307 );
308 return yield* HttpServerResponse.json(result);
309 }
310
311 if (request.method === "GET" && pathname === "/delegated-admin") {
312 const result = yield* errorTagged(
313 getDelegatedAdminAccount({}).pipe(
314 Effect.map((r) => ({
315 accountId: r.delegatedAdmin?.accountId,
316 })),
317 ),
318 );
319 return yield* HttpServerResponse.json(result);
320 }
321
322 // Report status for a well-formed but nonexistent report id — a
323 // typed ResourceNotFoundException proves the binding end-to-end.
324 if (request.method === "GET" && pathname === "/report-status") {
325 const result = yield* errorTagged(
326 getFindingsReportStatus({
327 reportId:
328 url.searchParams.get("id") ??
329 "00000000-0000-0000-0000-000000000000",
330 }).pipe(Effect.map((r) => ({ status: r.status }))),
331 );
332 return yield* HttpServerResponse.json(result);
333 }
334
335 return yield* HttpServerResponse.json(
336 { error: "Not found", method: request.method, pathname },
337 { status: 404 },
338 );
339 }).pipe(Effect.orDie),
340 };
341 }).pipe(
342 Effect.provide(
343 Layer.mergeAll(
344 Inspector2.AssociateMemberHttp,
345 Inspector2.BatchGetCodeSnippetHttp,
346 Inspector2.BatchGetFindingDetailsHttp,
347 Inspector2.BatchGetFreeTrialInfoHttp,
348 Inspector2.BatchGetMemberEc2DeepInspectionStatusHttp,
349 Inspector2.BatchUpdateMemberEc2DeepInspectionStatusHttp,
350 Inspector2.CancelFindingsReportHttp,
351 Inspector2.CancelSbomExportHttp,
352 Inspector2.CreateFindingsReportHttp,
353 Inspector2.CreateSbomExportHttp,
354 Inspector2.DescribeOrganizationConfigurationHttp,
355 Inspector2.DisableDelegatedAdminAccountHttp,
356 Inspector2.DisassociateMemberHttp,
357 Inspector2.EnableDelegatedAdminAccountHttp,
358 Inspector2.GetCisScanReportHttp,
359 Inspector2.GetCisScanResultDetailsHttp,
360 Inspector2.GetClustersForImageHttp,
361 Inspector2.GetCodeSecurityScanHttp,
362 Inspector2.GetConfigurationHttp,
363 Inspector2.GetDelegatedAdminAccountHttp,
364 Inspector2.GetEc2DeepInspectionConfigurationHttp,
365 Inspector2.GetEncryptionKeyHttp,
366 Inspector2.GetFindingsReportStatusHttp,
367 Inspector2.GetMemberHttp,
368 Inspector2.GetSbomExportHttp,
369 Inspector2.ListAccountPermissionsHttp,
370 Inspector2.ListCisScanResultsAggregatedByChecksHttp,
371 Inspector2.ListCisScanResultsAggregatedByTargetResourceHttp,
372 Inspector2.ListCisScansHttp,
373 Inspector2.ListCoverageHttp,
374 Inspector2.ListCoverageStatisticsHttp,
375 Inspector2.ListDelegatedAdminAccountsHttp,
376 Inspector2.ListFindingAggregationsHttp,
377 Inspector2.ListFindingsHttp,
378 Inspector2.ListMembersHttp,
379 Inspector2.ListUsageTotalsHttp,
380 Inspector2.ResetEncryptionKeyHttp,
381 Inspector2.SearchVulnerabilitiesHttp,
382 Inspector2.StartCodeSecurityScanHttp,
383 Inspector2.UpdateConfigurationHttp,
384 Inspector2.UpdateEc2DeepInspectionConfigurationHttp,
385 Inspector2.UpdateEncryptionKeyHttp,
386 Inspector2.UpdateOrgEc2DeepInspectionConfigurationHttp,
387 Inspector2.UpdateOrganizationConfigurationHttp,
388 ),
389 ),
390 ),
391);