Files
prowler/ui/app/api/ingestions/route.ts

115 lines
3.4 KiB
TypeScript

import { NextResponse } from "next/server";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { parseIngestion } from "@/lib/ingestions";
import { isCloud } from "@/lib/shared/env";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
const INVALID_INGESTION_RESPONSE =
"Unable to start the import. Please try again.";
const INGESTION_REJECTION_BY_CODE = {
invalid: "The report is not a valid Prowler OCSF finding report.",
subscription_required:
"A Prowler Cloud subscription is required to import findings.",
permission_denied: "You do not have permission to import findings.",
file_too_large: "The selected file exceeds the allowed upload size.",
rate_limited: "Too many import requests. Please try again shortly.",
} as const;
const INGESTION_REJECTION_BY_STATUS = {
400: INGESTION_REJECTION_BY_CODE.invalid,
402: INGESTION_REJECTION_BY_CODE.subscription_required,
403: INGESTION_REJECTION_BY_CODE.permission_denied,
413: INGESTION_REJECTION_BY_CODE.file_too_large,
429: INGESTION_REJECTION_BY_CODE.rate_limited,
} as const;
const ingestionRejectionMessage = (
payload: unknown,
status: number,
fallback: string,
): string => {
if (typeof payload === "object" && payload !== null && "errors" in payload) {
const errors = payload.errors;
if (Array.isArray(errors) && typeof errors[0]?.code === "string") {
const message =
INGESTION_REJECTION_BY_CODE[
errors[0].code as keyof typeof INGESTION_REJECTION_BY_CODE
];
if (message) return message;
}
}
return (
INGESTION_REJECTION_BY_STATUS[
status as keyof typeof INGESTION_REJECTION_BY_STATUS
] ?? fallback
);
};
export async function POST(request: Request) {
if (!isCloud()) return new Response(null, { status: 404 });
const headers = await getAuthHeaders({ contentType: false });
const contentType = request.headers.get("content-type");
const contentLength = request.headers.get("content-length");
if (contentType) headers["Content-Type"] = contentType;
// Without the length the stream is forwarded chunked, and the ingestion API
// parses no file out of a chunked multipart body: refuse rather than spend
// the upload on a request that cannot succeed.
if (!contentLength) {
return NextResponse.json(
{ error: INVALID_INGESTION_RESPONSE },
{ status: 411 },
);
}
headers["Content-Length"] = contentLength;
const upstreamRequest: RequestInit & { duplex: "half" } = {
method: "POST",
headers,
body: request.body,
duplex: "half",
cache: "no-store",
};
let upstreamResponse: Response;
try {
upstreamResponse = await fetch(`${apiBaseUrl}/ingestions`, upstreamRequest);
} catch {
return NextResponse.json(
{ error: INVALID_INGESTION_RESPONSE },
{ status: 502 },
);
}
const payload = await upstreamResponse.json().catch(() => undefined);
if (!upstreamResponse.ok) {
return NextResponse.json(
{
error: ingestionRejectionMessage(
payload,
upstreamResponse.status,
INVALID_INGESTION_RESPONSE,
),
},
{ status: upstreamResponse.status },
);
}
const ingestion = parseIngestion(payload);
if (!ingestion) {
return NextResponse.json(
{ error: INVALID_INGESTION_RESPONSE },
{ status: 502 },
);
}
return NextResponse.json(
{ data: ingestion },
{ status: upstreamResponse.status },
);
}