Files
prowler/ui/app/(prowler)/findings/page.tsx
2025-12-17 13:49:40 +01:00

178 lines
5.6 KiB
TypeScript

import { Spacer } from "@heroui/spacer";
import { Suspense } from "react";
import {
getFindings,
getLatestFindings,
getLatestMetadataInfo,
getMetadataInfo,
} from "@/actions/findings";
import { getProviders } from "@/actions/providers";
import { getScans } from "@/actions/scans";
import { FindingsFilters } from "@/components/findings/findings-filters";
import {
ColumnFindings,
SkeletonTableFindings,
} from "@/components/findings/table";
import { ContentLayout } from "@/components/ui";
import { DataTable } from "@/components/ui/table";
import {
createDict,
createScanDetailsMapping,
extractFiltersAndQuery,
extractSortAndKey,
hasDateOrScanFilter,
} from "@/lib";
import {
createProviderDetailsMappingById,
extractProviderIds,
} from "@/lib/provider-helpers";
import { FilterEntity, ScanEntity, ScanProps } from "@/types";
import { FindingProps, SearchParamsProps } from "@/types/components";
export default async function Findings({
searchParams,
}: {
searchParams: Promise<SearchParamsProps>;
}) {
const resolvedSearchParams = await searchParams;
const { searchParamsKey, encodedSort } =
extractSortAndKey(resolvedSearchParams);
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);
// Check if the searchParams contain any date or scan filter
const hasDateOrScan = hasDateOrScanFilter(resolvedSearchParams);
const [metadataInfoData, providersData, scansData] = await Promise.all([
(hasDateOrScan ? getMetadataInfo : getLatestMetadataInfo)({
query,
sort: encodedSort,
filters,
}),
getProviders({ pageSize: 50 }),
getScans({ pageSize: 50 }),
]);
// Extract unique regions, services, categories from the new endpoint
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
const uniqueServices = metadataInfoData?.data?.attributes?.services || [];
const uniqueResourceTypes =
metadataInfoData?.data?.attributes?.resource_types || [];
const uniqueCategories = metadataInfoData?.data?.attributes?.categories || [];
// Extract provider IDs and details using helper functions
const providerIds = providersData ? extractProviderIds(providersData) : [];
const providerDetails = providersData
? (createProviderDetailsMappingById(providerIds, providersData) as {
[id: string]: FilterEntity;
}[])
: [];
// Extract scan UUIDs with "completed" state and more than one resource
const completedScans = scansData?.data?.filter(
(scan: ScanProps) =>
scan.attributes.state === "completed" &&
scan.attributes.unique_resource_count > 1,
);
const completedScanIds =
completedScans?.map((scan: ScanProps) => scan.id) || [];
const scanDetails = createScanDetailsMapping(
completedScans || [],
providersData,
) as { [uid: string]: ScanEntity }[];
return (
<ContentLayout title="Findings" icon="lucide:tag">
<FindingsFilters
providerIds={providerIds}
providerDetails={providerDetails}
completedScans={completedScans || []}
completedScanIds={completedScanIds}
scanDetails={scanDetails}
uniqueRegions={uniqueRegions}
uniqueServices={uniqueServices}
uniqueResourceTypes={uniqueResourceTypes}
uniqueCategories={uniqueCategories}
/>
<Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={resolvedSearchParams} />
</Suspense>
</ContentLayout>
);
}
const SSRDataTable = async ({
searchParams,
}: {
searchParams: SearchParamsProps;
}) => {
const page = parseInt(searchParams.page?.toString() || "1", 10);
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10);
const defaultSort = "severity,status,-inserted_at";
const { encodedSort } = extractSortAndKey({
...searchParams,
sort: searchParams.sort ?? defaultSort,
});
const { filters, query } = extractFiltersAndQuery(searchParams);
// Check if the searchParams contain any date or scan filter
const hasDateOrScan = hasDateOrScanFilter(searchParams);
const fetchFindings = hasDateOrScan ? getFindings : getLatestFindings;
const findingsData = await fetchFindings({
query,
page,
sort: encodedSort,
filters,
pageSize,
});
// Create dictionaries for resources, scans, and providers
const resourceDict = createDict("resources", findingsData);
const scanDict = createDict("scans", findingsData);
const providerDict = createDict("providers", findingsData);
// Expand each finding with its corresponding resource, scan, and provider
const expandedFindings = findingsData?.data
? findingsData.data.map((finding: FindingProps) => {
const scan = scanDict[finding.relationships?.scan?.data?.id];
const resource =
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
const provider = providerDict[scan?.relationships?.provider?.data?.id];
return {
...finding,
relationships: { scan, resource, provider },
};
})
: [];
// Create the new object while maintaining the original structure
const expandedResponse = {
...findingsData,
data: expandedFindings,
};
return (
<>
{findingsData?.errors && (
<div className="text-small mb-4 flex rounded-lg border border-red-500 bg-red-100 p-2 text-red-700">
<p className="mr-2 font-semibold">Error:</p>
<p>{findingsData.errors[0].detail}</p>
</div>
)}
<DataTable
key={Date.now()}
columns={ColumnFindings}
data={expandedResponse?.data || []}
metadata={findingsData?.meta}
/>
</>
);
};