Files
prowler/ui/actions/attack-paths/scans.ts
Pablo Fernandez Guerra (PFE) 3554859a5c fix(ui): load every Attack Paths scan before displaying the selector (#10864)
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com>
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
2026-04-24 09:41:47 +02:00

116 lines
3.1 KiB
TypeScript

"use server";
import { z } from "zod";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { AttackPathScan, AttackPathScansResponse } from "@/types/attack-paths";
import { adaptAttackPathScansResponse } from "./scans.adapter";
const UUIDSchema = z.uuid();
const ATTACK_PATH_SCANS_PAGE_SIZE = 100;
const ATTACK_PATH_SCANS_MAX_PAGES = 50;
/**
* Fetch list of attack path scans (latest scan for each provider).
*
* Iterates through every backend page so callers receive the complete
* dedup'd dataset along with an accurate total count. The underlying
* endpoint is paginated server-side (default page_size=10), so fetching
* only the first page would silently hide providers beyond that window.
*/
export const getAttackPathScans = async (): Promise<
{ data: AttackPathScan[] } | undefined
> => {
const headers = await getAuthHeaders({ contentType: false });
const allScans: AttackPathScan[] = [];
let currentPage = 1;
let lastResponse: AttackPathScansResponse | undefined;
let hasMorePages = true;
while (hasMorePages && currentPage <= ATTACK_PATH_SCANS_MAX_PAGES) {
try {
const url = new URL(`${apiBaseUrl}/attack-paths-scans`);
url.searchParams.append("page[number]", currentPage.toString());
url.searchParams.append(
"page[size]",
ATTACK_PATH_SCANS_PAGE_SIZE.toString(),
);
const response = await fetch(url.toString(), {
headers,
method: "GET",
});
const data = (await handleApiResponse(response)) as
| AttackPathScansResponse
| undefined;
if (!data?.data || data.data.length === 0) {
hasMorePages = false;
continue;
}
allScans.push(...data.data);
lastResponse = data;
const totalPages = data.meta?.pagination?.pages ?? 1;
if (currentPage >= totalPages) {
hasMorePages = false;
} else {
currentPage++;
}
} catch (error) {
console.error("Error fetching attack path scans:", error);
if (allScans.length === 0) {
return undefined;
}
break;
}
}
if (!lastResponse) {
return { data: [] };
}
const adapted = adaptAttackPathScansResponse({
...lastResponse,
data: allScans,
});
return { data: adapted.data };
};
/**
* Fetch detail of a specific attack path scan
*/
export const getAttackPathScanDetail = async (
scanId: string,
): Promise<{ data: AttackPathScan } | undefined> => {
// Validate scanId is a valid UUID format to prevent request forgery
const validatedScanId = UUIDSchema.safeParse(scanId);
if (!validatedScanId.success) {
console.error("Invalid scan ID format");
return undefined;
}
const headers = await getAuthHeaders({ contentType: false });
try {
const response = await fetch(
`${apiBaseUrl}/attack-paths-scans/${validatedScanId.data}`,
{
headers,
method: "GET",
},
);
return handleApiResponse(response);
} catch (error) {
console.error("Error fetching attack path scan detail:", error);
return undefined;
}
};