mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
178 lines
5.6 KiB
TypeScript
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}
|
|
/>
|
|
</>
|
|
);
|
|
};
|