mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
5b9824c379
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
184 lines
5.9 KiB
TypeScript
184 lines
5.9 KiB
TypeScript
import { Suspense } from "react";
|
|
|
|
import {
|
|
adaptFindingGroupsResponse,
|
|
getFindingGroups,
|
|
getLatestFindingGroups,
|
|
} from "@/actions/finding-groups";
|
|
import { getLatestMetadataInfo, getMetadataInfo } from "@/actions/findings";
|
|
import { getAllProviderGroups } from "@/actions/manage-groups/manage-groups";
|
|
import { getAllProviders } from "@/actions/providers";
|
|
import { getScan, getScans } from "@/actions/scans";
|
|
import { SeedFromFindingsButton } from "@/app/(prowler)/alerts/_components";
|
|
import { FindingsFilters } from "@/components/findings/findings-filters";
|
|
import {
|
|
FindingsGroupTable,
|
|
SkeletonTableFindings,
|
|
} from "@/components/findings/table";
|
|
import { ContentLayout } from "@/components/ui";
|
|
import { FilterTransitionWrapper } from "@/contexts";
|
|
import {
|
|
applyDefaultMutedFilter,
|
|
createScanDetailsMapping,
|
|
extractFiltersAndQuery,
|
|
extractSortAndKey,
|
|
hasDateOrScanFilter,
|
|
} from "@/lib";
|
|
import { resolveFindingScanDateFilters } from "@/lib/findings-scan-filters";
|
|
import { ScanEntity, ScanProps } from "@/types";
|
|
import { SearchParamsProps } from "@/types/components";
|
|
|
|
export default async function Findings({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<SearchParamsProps>;
|
|
}) {
|
|
const resolvedSearchParams = await searchParams;
|
|
const { encodedSort } = extractSortAndKey(resolvedSearchParams);
|
|
const { filters, query } = extractFiltersAndQuery(resolvedSearchParams);
|
|
|
|
const [providersData, providerGroupsData, scansData] = await Promise.all([
|
|
getAllProviders(),
|
|
getAllProviderGroups(),
|
|
getScans({ pageSize: 50 }),
|
|
]);
|
|
|
|
const filtersWithScanDates = await resolveFindingScanDateFilters({
|
|
filters,
|
|
scans: scansData?.data || [],
|
|
loadScan: async (scanId: string) => {
|
|
const response = await getScan(scanId);
|
|
return response?.data;
|
|
},
|
|
});
|
|
const resolvedFilters = applyDefaultMutedFilter(filtersWithScanDates);
|
|
const hasHistoricalData = hasDateOrScanFilter(filtersWithScanDates);
|
|
const metadataInfoData = await (
|
|
hasHistoricalData ? getMetadataInfo : getLatestMetadataInfo
|
|
)({
|
|
query,
|
|
sort: encodedSort,
|
|
filters: resolvedFilters,
|
|
});
|
|
|
|
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 || [];
|
|
const uniqueGroups = metadataInfoData?.data?.attributes?.groups || [];
|
|
|
|
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 onboardingAction =
|
|
completedScanIds.length > 0
|
|
? { flowId: "explore-findings" }
|
|
: {
|
|
flowId: "explore-findings",
|
|
fallbackFlowId: "view-first-scan",
|
|
useFallback: true,
|
|
};
|
|
|
|
const scanDetails = createScanDetailsMapping(
|
|
completedScans || [],
|
|
providersData,
|
|
) as { [uid: string]: ScanEntity }[];
|
|
const alertsEnabled = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
|
|
|
return (
|
|
<ContentLayout
|
|
title="Findings"
|
|
icon="lucide:tag"
|
|
onboardingAction={onboardingAction}
|
|
>
|
|
<FilterTransitionWrapper>
|
|
<div className="mb-6">
|
|
<FindingsFilters
|
|
providers={providersData?.data || []}
|
|
providerGroups={providerGroupsData?.data || []}
|
|
completedScanIds={completedScanIds}
|
|
scanDetails={scanDetails}
|
|
uniqueRegions={uniqueRegions}
|
|
uniqueServices={uniqueServices}
|
|
uniqueResourceTypes={uniqueResourceTypes}
|
|
uniqueCategories={uniqueCategories}
|
|
uniqueGroups={uniqueGroups}
|
|
trailingControls={
|
|
<SeedFromFindingsButton
|
|
filterBag={filters}
|
|
providers={providersData?.data || []}
|
|
scans={scanDetails}
|
|
uniqueRegions={uniqueRegions}
|
|
uniqueServices={uniqueServices}
|
|
uniqueResourceTypes={uniqueResourceTypes}
|
|
uniqueCategories={uniqueCategories}
|
|
uniqueGroups={uniqueGroups}
|
|
isCloudEnabled={alertsEnabled}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
<Suspense fallback={<SkeletonTableFindings />}>
|
|
<SSRDataTable
|
|
searchParams={resolvedSearchParams}
|
|
filters={resolvedFilters}
|
|
/>
|
|
</Suspense>
|
|
</FilterTransitionWrapper>
|
|
</ContentLayout>
|
|
);
|
|
}
|
|
|
|
const SSRDataTable = async ({
|
|
searchParams,
|
|
filters,
|
|
}: {
|
|
searchParams: SearchParamsProps;
|
|
filters: Record<string, string>;
|
|
}) => {
|
|
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
|
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10);
|
|
|
|
const { encodedSort } = extractSortAndKey(searchParams);
|
|
const hasHistoricalData = hasDateOrScanFilter(filters);
|
|
|
|
const fetchFindingGroups = hasHistoricalData
|
|
? getFindingGroups
|
|
: getLatestFindingGroups;
|
|
|
|
const findingGroupsData = await fetchFindingGroups({
|
|
page,
|
|
...(encodedSort && { sort: encodedSort }),
|
|
filters,
|
|
pageSize,
|
|
});
|
|
|
|
const groups = adaptFindingGroupsResponse(findingGroupsData);
|
|
// Key resets client state (selection, drill-down) when data changes.
|
|
const groupKey = groups.map((g) => g.id).join(",");
|
|
|
|
return (
|
|
<>
|
|
{findingGroupsData?.errors?.length > 0 && (
|
|
<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>{findingGroupsData.errors[0].detail}</p>
|
|
</div>
|
|
)}
|
|
<FindingsGroupTable
|
|
key={groupKey}
|
|
data={groups}
|
|
metadata={findingGroupsData?.meta}
|
|
resolvedFilters={filters}
|
|
hasHistoricalData={hasHistoricalData}
|
|
/>
|
|
</>
|
|
);
|
|
};
|