t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/AWS/SecurityHub/handler.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

409 lines
16 KiB
TypeScript

import * as Lambda from "@/AWS/Lambda";
import * as SecurityHub from "@/AWS/SecurityHub";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { HttpServerRequest } from "effect/http/HttpServerRequest";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import path from "pathe";
const main = path.resolve(import.meta.dirname, "handler.ts");
export class SecurityHubTestFunction extends Lambda.Function<Lambda.Function>()(
"SecurityHubTestFunction",
) {}
/**
* Routes answer `{ …fields }` on success or `{ errorTag }` when the operation
* fails with a TYPED error — the test asserts the tag is in a route-specific
* allowlist, which proves both the binding wiring and the IAM grant. An
* untyped error crashes into a 500 instead.
*/
const errorTagged = <A, E extends { _tag: string }, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A | { errorTag: string; errorMessage?: string }, never, R> =>
effect.pipe(
Effect.map((a): A | { errorTag: string; errorMessage?: string } => a),
Effect.catch((e) =>
Effect.succeed({
errorTag: e._tag,
errorMessage:
(e as { Message?: string }).Message ??
(e as { message?: string }).message,
}),
),
);
// One binding per exercised Security Hub capability. Every SecurityHub
// binding shares the single account-level scaffolding shape (the request
// passes through as-is), so a representative spread across the thematic
// groups proves the scaffold; org/member writes with cross-account side
// effects (CreateMembers, EnableOrganizationAdminAccount, …) use the same
// builder and are not re-proven here.
export default SecurityHubTestFunction.make(
{
main,
functionUrl: true,
timeout: Duration.seconds(30),
},
Effect.gen(function* () {
// Security Hub must be enabled for every runtime call — the Hub is the
// account/region singleton the whole fixture leans on. The Bindings test
// only runs when the account had no Hub (capture-and-restore safety).
yield* SecurityHub.Hub("BindingsHub", { enableDefaultStandards: false });
// Findings triage loop
const getFindings = yield* SecurityHub.GetFindings();
const getFindingHistory = yield* SecurityHub.GetFindingHistory();
const batchImportFindings = yield* SecurityHub.BatchImportFindings();
const batchUpdateFindings = yield* SecurityHub.BatchUpdateFindings();
// Insights
const getInsights = yield* SecurityHub.GetInsights();
// Standards & controls
const describeStandards = yield* SecurityHub.DescribeStandards();
const getEnabledStandards = yield* SecurityHub.GetEnabledStandards();
const listSecurityControlDefinitions =
yield* SecurityHub.ListSecurityControlDefinitions();
const getSecurityControlDefinition =
yield* SecurityHub.GetSecurityControlDefinition();
// Product integrations
const describeProducts = yield* SecurityHub.DescribeProducts();
const listEnabledProductsForImport =
yield* SecurityHub.ListEnabledProductsForImport();
// Custom actions, automation rules & aggregation reads
const describeActionTargets = yield* SecurityHub.DescribeActionTargets();
const listAutomationRules = yield* SecurityHub.ListAutomationRules();
const listFindingAggregators = yield* SecurityHub.ListFindingAggregators();
// Members & organization
const listMembers = yield* SecurityHub.ListMembers();
const listInvitations = yield* SecurityHub.ListInvitations();
const getInvitationsCount = yield* SecurityHub.GetInvitationsCount();
const getAdministratorAccount =
yield* SecurityHub.GetAdministratorAccount();
const listOrganizationAdminAccounts =
yield* SecurityHub.ListOrganizationAdminAccounts();
const describeOrganizationConfiguration =
yield* SecurityHub.DescribeOrganizationConfiguration();
const bound = {
getFindings,
getFindingHistory,
batchImportFindings,
batchUpdateFindings,
getInsights,
describeStandards,
getEnabledStandards,
listSecurityControlDefinitions,
getSecurityControlDefinition,
describeProducts,
listEnabledProductsForImport,
describeActionTargets,
listAutomationRules,
listFindingAggregators,
listMembers,
listInvitations,
getInvitationsCount,
getAdministratorAccount,
listOrganizationAdminAccounts,
describeOrganizationConfiguration,
};
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.originalUrl);
const pathname = url.pathname;
if (request.method === "GET" && pathname === "/bindings") {
return yield* HttpServerResponse.json({ bound: Object.keys(bound) });
}
// Imports a synthetic ASFF finding through the account's default
// product — the documented way to exercise a findings pipeline. The
// test computes id/account/region out-of-band and passes them in.
if (request.method === "POST" && pathname === "/import") {
const id = url.searchParams.get("id");
const accountId = url.searchParams.get("account");
const region = url.searchParams.get("region");
if (!id || !accountId || !region) {
return yield* HttpServerResponse.json(
{ error: "missing id/account/region" },
{ status: 400 },
);
}
const productArn = `arn:aws:securityhub:${region}:${accountId}:product/${accountId}/default`;
const now = new Date().toISOString();
const result = yield* batchImportFindings({
Findings: [
{
SchemaVersion: "2018-10-08",
Id: id,
ProductArn: productArn,
GeneratorId: "alchemy-bindings-test",
AwsAccountId: accountId,
Types: ["Software and Configuration Checks"],
CreatedAt: now,
UpdatedAt: now,
Severity: { Label: "INFORMATIONAL" },
Title: "Alchemy SecurityHub bindings test finding",
Description:
"Synthetic finding imported by the alchemy SecurityHub bindings test.",
Resources: [
{
Type: "Other",
Id: "alchemy-bindings-test-resource",
Partition: "aws",
Region: region,
},
],
},
],
});
return yield* HttpServerResponse.json({
success: Number(result.SuccessCount ?? 0),
failed: Number(result.FailedCount ?? 0),
});
}
if (request.method === "GET" && pathname === "/findings") {
const id = url.searchParams.get("id");
const { Findings } = yield* getFindings({
...(id
? { Filters: { Id: [{ Value: id, Comparison: "EQUALS" }] } }
: { MaxResults: 10 }),
});
return yield* HttpServerResponse.json({
count: (Findings ?? []).length,
workflow: Findings?.[0]?.Workflow?.Status,
title: Findings?.[0]?.Title,
});
}
// Updates customer-editable fields on the imported finding.
if (request.method === "POST" && pathname === "/resolve") {
const id = url.searchParams.get("id");
const productArn = url.searchParams.get("productArn");
if (!id || !productArn) {
return yield* HttpServerResponse.json(
{ error: "missing id/productArn" },
{ status: 400 },
);
}
const result = yield* batchUpdateFindings({
FindingIdentifiers: [{ Id: id, ProductArn: productArn }],
Workflow: { Status: "NOTIFIED" },
Note: {
Text: "acknowledged by the alchemy bindings test",
UpdatedBy: "alchemy",
},
});
return yield* HttpServerResponse.json({
processed: (result.ProcessedFindings ?? []).length,
unprocessed: (result.UnprocessedFindings ?? []).length,
});
}
// Finding history may lag the import — a typed ResourceNotFound is an
// acceptable outcome that still proves the binding + IAM wiring.
if (request.method === "GET" && pathname === "/history") {
const id = url.searchParams.get("id");
const productArn = url.searchParams.get("productArn");
if (!id || !productArn) {
return yield* HttpServerResponse.json(
{ error: "missing id/productArn" },
{ status: 400 },
);
}
const result = yield* errorTagged(
getFindingHistory({
FindingIdentifier: { Id: id, ProductArn: productArn },
}).pipe(Effect.map((r) => ({ records: (r.Records ?? []).length }))),
);
return yield* HttpServerResponse.json(result);
}
if (request.method === "GET" && pathname === "/insights") {
const { Insights } = yield* getInsights();
return yield* HttpServerResponse.json({
count: (Insights ?? []).length,
});
}
if (request.method === "GET" && pathname === "/standards") {
const { Standards } = yield* describeStandards();
return yield* HttpServerResponse.json({
count: (Standards ?? []).length,
});
}
if (request.method === "GET" && pathname === "/enabled-standards") {
const { StandardsSubscriptions } = yield* getEnabledStandards();
return yield* HttpServerResponse.json({
count: (StandardsSubscriptions ?? []).length,
});
}
if (request.method === "GET" && pathname === "/control-definitions") {
const { SecurityControlDefinitions } =
yield* listSecurityControlDefinitions({ MaxResults: 10 });
return yield* HttpServerResponse.json({
count: (SecurityControlDefinitions ?? []).length,
});
}
if (request.method === "GET" && pathname === "/control") {
const { SecurityControlDefinition } =
yield* getSecurityControlDefinition({
SecurityControlId: "IAM.1",
});
return yield* HttpServerResponse.json({
id: SecurityControlDefinition?.SecurityControlId ?? null,
severity: SecurityControlDefinition?.SeverityRating ?? null,
});
}
if (request.method === "GET" && pathname === "/products") {
const { Products } = yield* describeProducts({ MaxResults: 10 });
return yield* HttpServerResponse.json({
count: (Products ?? []).length,
});
}
if (request.method === "GET" && pathname === "/enabled-products") {
const { ProductSubscriptions } =
yield* listEnabledProductsForImport();
return yield* HttpServerResponse.json({
count: (ProductSubscriptions ?? []).length,
});
}
if (request.method === "GET" && pathname === "/action-targets") {
const { ActionTargets } = yield* describeActionTargets();
return yield* HttpServerResponse.json({
count: (ActionTargets ?? []).length,
});
}
if (request.method === "GET" && pathname === "/automation-rules") {
const { AutomationRulesMetadata } = yield* listAutomationRules();
return yield* HttpServerResponse.json({
count: (AutomationRulesMetadata ?? []).length,
});
}
if (request.method === "GET" && pathname === "/aggregators") {
const { FindingAggregators } = yield* listFindingAggregators();
return yield* HttpServerResponse.json({
count: (FindingAggregators ?? []).length,
});
}
if (request.method === "GET" && pathname === "/members") {
const { Members } = yield* listMembers();
return yield* HttpServerResponse.json({
count: (Members ?? []).length,
});
}
if (request.method === "GET" && pathname === "/invitations") {
const { Invitations } = yield* listInvitations();
return yield* HttpServerResponse.json({
count: (Invitations ?? []).length,
});
}
if (request.method === "GET" && pathname === "/invitations-count") {
const { InvitationsCount } = yield* getInvitationsCount();
return yield* HttpServerResponse.json({
count: Number(InvitationsCount ?? 0),
});
}
// A standalone account has no administrator — Security Hub answers
// with an empty document or a typed error.
if (request.method === "GET" && pathname === "/admin") {
const result = yield* errorTagged(
getAdministratorAccount().pipe(
Effect.map((r) => ({
administrator: r.Administrator?.AccountId ?? null,
})),
),
);
return yield* HttpServerResponse.json(result);
}
// Organization administration: for a standalone (non-management)
// account Security Hub answers with a typed error — asserting the tag
// proves the binding + IAM wiring end-to-end.
if (request.method === "GET" && pathname === "/org-admins") {
const result = yield* errorTagged(
listOrganizationAdminAccounts().pipe(
Effect.map((r) => ({ admins: (r.AdminAccounts ?? []).length })),
),
);
return yield* HttpServerResponse.json(result);
}
if (request.method === "GET" && pathname === "/org-config") {
const result = yield* errorTagged(
describeOrganizationConfiguration().pipe(
Effect.map((r) => ({ autoEnable: r.AutoEnable ?? false })),
),
);
return yield* HttpServerResponse.json(result);
}
return yield* HttpServerResponse.json(
{ error: "Not found", method: request.method, pathname },
{ status: 404 },
);
}).pipe(
// Surface typed operation errors as JSON so live-test failures carry
// the tag + message instead of an opaque 500 (routes that expect a
// typed error use `errorTagged` above and never reach this).
Effect.catch((e) =>
HttpServerResponse.json(
{
errorTag: (e as { _tag?: string })._tag ?? "UnknownError",
errorMessage:
(e as { message?: string }).message ??
(e as { Message?: string }).Message,
},
{ status: 500 },
),
),
Effect.orDie,
),
};
}).pipe(
Effect.provide(
Layer.mergeAll(
SecurityHub.BatchImportFindingsHttp,
SecurityHub.BatchUpdateFindingsHttp,
SecurityHub.DescribeActionTargetsHttp,
SecurityHub.DescribeOrganizationConfigurationHttp,
SecurityHub.DescribeProductsHttp,
SecurityHub.DescribeStandardsHttp,
SecurityHub.GetAdministratorAccountHttp,
SecurityHub.GetEnabledStandardsHttp,
SecurityHub.GetFindingHistoryHttp,
SecurityHub.GetFindingsHttp,
SecurityHub.GetInsightsHttp,
SecurityHub.GetInvitationsCountHttp,
SecurityHub.GetSecurityControlDefinitionHttp,
SecurityHub.ListAutomationRulesHttp,
SecurityHub.ListEnabledProductsForImportHttp,
SecurityHub.ListFindingAggregatorsHttp,
SecurityHub.ListInvitationsHttp,
SecurityHub.ListMembersHttp,
SecurityHub.ListOrganizationAdminAccountsHttp,
SecurityHub.ListSecurityControlDefinitionsHttp,
),
),
),
);