diff --git a/api/src/backend/api/fixtures/dev/2_dev_providers.json b/api/src/backend/api/fixtures/dev/2_dev_providers.json index a4d73950cc..72e0602055 100644 --- a/api/src/backend/api/fixtures/dev/2_dev_providers.json +++ b/api/src/backend/api/fixtures/dev/2_dev_providers.json @@ -122,6 +122,22 @@ "scanner_args": {} } }, + { + "model": "api.provider", + "pk": "7791914f-d646-4fe2-b2ed-73f2c6499a36", + "fields": { + "tenant": "12646005-9067-4d2a-a098-8bb378604362", + "inserted_at": "2024-10-18T10:45:26.352Z", + "updated_at": "2024-10-18T11:16:23.533Z", + "provider": "kubernetes", + "uid": "gke_lucky-coast-419309_us-central1_autopilot-cluster-2", + "alias": "k8s_testing_2", + "connected": true, + "connection_last_checked_at": "2024-10-18T11:16:23.503Z", + "metadata": {}, + "scanner_args": {} + } + }, { "model": "api.providersecret", "pk": "11491b47-75ae-4f71-ad8d-3e630a72182e", diff --git a/api/src/backend/api/fixtures/dev/3_dev_scans.json b/api/src/backend/api/fixtures/dev/3_dev_scans.json index d03f4ac07b..4f6d284b99 100644 --- a/api/src/backend/api/fixtures/dev/3_dev_scans.json +++ b/api/src/backend/api/fixtures/dev/3_dev_scans.json @@ -11,9 +11,7 @@ "unique_resource_count": 1, "duration": 5, "scanner_args": { - "checks_to_execute": [ - "accessanalyzer_enabled" - ] + "checks_to_execute": ["accessanalyzer_enabled"] }, "inserted_at": "2024-09-01T17:25:27.050Z", "started_at": "2024-09-01T17:25:27.050Z", @@ -33,9 +31,7 @@ "unique_resource_count": 1, "duration": 20, "scanner_args": { - "checks_to_execute": [ - "accessanalyzer_enabled" - ] + "checks_to_execute": ["accessanalyzer_enabled"] }, "inserted_at": "2024-09-02T17:24:27.050Z", "started_at": "2024-09-02T17:24:27.050Z", @@ -55,9 +51,7 @@ "unique_resource_count": 10, "duration": 10, "scanner_args": { - "checks_to_execute": [ - "cloudsql_instance_automated_backups" - ] + "checks_to_execute": ["cloudsql_instance_automated_backups"] }, "inserted_at": "2024-09-02T19:26:27.050Z", "started_at": "2024-09-02T19:26:27.050Z", @@ -77,9 +71,7 @@ "unique_resource_count": 1, "duration": 35, "scanner_args": { - "checks_to_execute": [ - "accessanalyzer_enabled" - ] + "checks_to_execute": ["accessanalyzer_enabled"] }, "inserted_at": "2024-09-02T19:27:27.050Z", "started_at": "2024-09-02T19:27:27.050Z", @@ -97,9 +89,7 @@ "name": "test scheduled aws scan", "state": "available", "scanner_args": { - "checks_to_execute": [ - "cloudformation_stack_outputs_find_secrets" - ] + "checks_to_execute": ["cloudformation_stack_outputs_find_secrets"] }, "scheduled_at": "2030-09-02T19:20:27.050Z", "inserted_at": "2024-09-02T19:24:27.050Z", @@ -178,9 +168,7 @@ "unique_resource_count": 19, "progress": 100, "scanner_args": { - "checks_to_execute": [ - "accessanalyzer_enabled" - ] + "checks_to_execute": ["accessanalyzer_enabled"] }, "duration": 7, "scheduled_at": null, @@ -190,6 +178,56 @@ "completed_at": "2024-10-18T10:46:05.127Z" } }, + { + "model": "api.scan", + "pk": "6dd8925f-a52d-48de-a546-d2d90db30ab1", + "fields": { + "tenant": "12646005-9067-4d2a-a098-8bb378604362", + "name": "real scan azure", + "provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2", + "trigger": "manual", + "state": "completed", + "unique_resource_count": 20, + "progress": 100, + "scanner_args": { + "checks_to_execute": [ + "accessanalyzer_enabled", + "account_security_contact_information_is_registered" + ] + }, + "duration": 4, + "scheduled_at": null, + "inserted_at": "2024-10-18T11:16:21.358Z", + "updated_at": "2024-10-18T11:16:26.060Z", + "started_at": "2024-10-18T11:16:21.593Z", + "completed_at": "2024-10-18T11:16:26.060Z" + } + }, + { + "model": "api.scan", + "pk": "4ca7ce89-3236-41a8-a369-8937bc152af5", + "fields": { + "tenant": "12646005-9067-4d2a-a098-8bb378604362", + "name": "real scan k8s", + "provider": "7791914f-d646-4fe2-b2ed-73f2c6499a36", + "trigger": "manual", + "state": "completed", + "unique_resource_count": 20, + "progress": 100, + "scanner_args": { + "checks_to_execute": [ + "accessanalyzer_enabled", + "account_security_contact_information_is_registered" + ] + }, + "duration": 4, + "scheduled_at": null, + "inserted_at": "2024-10-18T11:16:21.358Z", + "updated_at": "2024-10-18T11:16:26.060Z", + "started_at": "2024-10-18T11:16:21.593Z", + "completed_at": "2024-10-18T11:16:26.060Z" + } + }, { "model": "api.scan", "pk": "01929f57-c0ee-7553-be0b-cbde006fb6f7", diff --git a/ui/actions/compliances/compliances.ts b/ui/actions/compliances/compliances.ts index e9c8814c31..5eb94cde98 100644 --- a/ui/actions/compliances/compliances.ts +++ b/ui/actions/compliances/compliances.ts @@ -7,9 +7,11 @@ import { parseStringify } from "@/lib"; export const getCompliancesOverview = async ({ scanId, region, + query, }: { scanId: string; region?: string | string[]; + query?: string; }) => { const session = await auth(); @@ -17,6 +19,7 @@ export const getCompliancesOverview = async ({ const url = new URL(`${keyServer}/compliance-overviews`); if (scanId) url.searchParams.append("filter[scan_id]", scanId); + if (query) url.searchParams.append("filter[search]", query); if (region) { const regionValue = Array.isArray(region) ? region.join(",") : region; diff --git a/ui/actions/findings/findings.ts b/ui/actions/findings/findings.ts index 859dc86b7e..901228a91a 100644 --- a/ui/actions/findings/findings.ts +++ b/ui/actions/findings/findings.ts @@ -82,17 +82,15 @@ export const getMetadataInfo = async ({ }); if (!response.ok) { - throw new Error( - `Failed to fetch services regions: ${response.statusText}`, - ); + throw new Error(`Failed to fetch metadata info: ${response.statusText}`); } - const data = await response.json(); - const parsedData = parseStringify(data); + const parsedData = parseStringify(await response.json()); + return parsedData; } catch (error) { // eslint-disable-next-line no-console - console.error("Error fetching services regions:", error); + console.error("Error fetching metadata info:", error); return undefined; } }; diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx index 22164bbcc3..68789e6900 100644 --- a/ui/app/(prowler)/compliance/page.tsx +++ b/ui/app/(prowler)/compliance/page.tsx @@ -4,88 +4,107 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getCompliancesOverview } from "@/actions/compliances"; +import { getMetadataInfo } from "@/actions/findings"; +import { getProvider } from "@/actions/providers"; import { getScans } from "@/actions/scans"; import { ComplianceCard, ComplianceSkeletonGrid, + NoScansAvailable, } from "@/components/compliance"; import { DataCompliance } from "@/components/compliance/data-compliance"; +import { FilterControls } from "@/components/filters"; import { ContentLayout } from "@/components/ui"; -import { ComplianceOverviewData, SearchParamsProps } from "@/types"; +import { DataTableFilterCustom } from "@/components/ui/table/data-table-filter-custom"; +import { ComplianceOverviewData, ScanProps, SearchParamsProps } from "@/types"; export default async function Compliance({ searchParams, }: { searchParams: SearchParamsProps; }) { - let scansData; - let scanList: { - id: string; - name: string; - state: string; - progress: number; - }[] = []; + const searchParamsKey = JSON.stringify(searchParams || {}); - try { - scansData = await getScans({}); - scanList = - scansData?.data - ?.filter( - (scan: any) => - scan.attributes.state === "completed" && - scan.attributes.progress === 100, - ) - .map((scan: any) => ({ - id: scan.id, - name: scan.attributes.name || "Unnamed Scan", - state: scan.attributes.state, - progress: scan.attributes.progress, - })) || []; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error fetching scans data:", error); + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + const scansData = await getScans({ + filters: { + "filter[state]": "completed", + }, + }); + + if (!scansData?.data) { + return ; } - const selectedScanId = searchParams.scanId || scanList[0]?.id || null; + // Expand scans with provider information + const expandedScansData = await Promise.all( + scansData.data.map(async (scan: ScanProps) => { + const providerId = scan.relationships?.provider?.data?.id; - // If there are no scans available, return a message - if (!selectedScanId) { - return ( -
-
No scans available to select.
-
- ); - } + if (!providerId) { + return { ...scan, providerInfo: null }; + } - // Fetch compliance data for regions - let compliancesData; - let regions: string[] = []; - try { - compliancesData = await getCompliancesOverview({ - scanId: selectedScanId as string, - }); - regions = compliancesData?.data - ? Array.from( - new Set( - compliancesData.data.map( - (compliance: ComplianceOverviewData) => - compliance.attributes.region as string, - ), - ), - ) - : []; - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error fetching compliance data:", error); - } + const formData = new FormData(); + formData.append("id", providerId); + + const providerData = await getProvider(formData); + + return { + ...scan, + providerInfo: providerData?.data + ? { + provider: providerData.data.attributes.provider, + uid: providerData.data.attributes.uid, + alias: providerData.data.attributes.alias, + } + : null, + }; + }), + ); + + const selectedScanId = + searchParams.scanId || expandedScansData[0]?.id || null; + const query = (filters["filter[search]"] as string) || ""; + + const metadataInfoData = await getMetadataInfo({ + query, + filters: { + "filter[scan]": selectedScanId, + }, + }); + + const uniqueRegions = metadataInfoData?.data?.attributes?.regions || []; return ( - - - }> - - + {selectedScanId ? ( + <> + + + + + + + }> + + + + ) : ( + + )} ); } @@ -98,30 +117,25 @@ const SSRComplianceGrid = async ({ const scanId = searchParams.scanId?.toString() || ""; const regionFilter = searchParams["filter[region__in]"]?.toString() || ""; - // Fetch compliance data - let compliancesData; - try { - compliancesData = await getCompliancesOverview({ - scanId, - region: regionFilter, - }); - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error fetching compliances overview:", error); - return ( -
-
- Failed to load compliance data. Please try again later. -
-
- ); - } + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; + + const compliancesData = await getCompliancesOverview({ + scanId, + region: regionFilter, + query, + }); // Check if the response contains no data if (!compliancesData || compliancesData?.data?.length === 0) { return ( -
-
+
+
No compliance data available for the selected scan.
@@ -131,8 +145,8 @@ const SSRComplianceGrid = async ({ // Handle errors returned by the API if (compliancesData?.errors?.length > 0) { return ( -
-
Provide a valid scan ID.
+
+
Provide a valid scan ID.
); } diff --git a/ui/components/compliance/compliance-scan-info.tsx b/ui/components/compliance/compliance-scan-info.tsx new file mode 100644 index 0000000000..9cd062259a --- /dev/null +++ b/ui/components/compliance/compliance-scan-info.tsx @@ -0,0 +1,40 @@ +import { Divider } from "@nextui-org/react"; +import React from "react"; + +import { DateWithTime, EntityInfoShort } from "@/components/ui/entities"; + +interface ComplianceScanInfoProps { + scan: { + providerInfo: { + provider: "aws" | "azure" | "gcp" | "kubernetes"; + alias?: string; + uid?: string; + }; + attributes: { + name?: string; + completed_at: string; + }; + }; +} + +export const ComplianceScanInfo: React.FC = ({ + scan, +}) => { + return ( +
+ + +
+

+ {scan.attributes.name || "- -"} +

+ +
+
+ ); +}; diff --git a/ui/components/compliance/data-compliance/data-compliance.tsx b/ui/components/compliance/data-compliance/data-compliance.tsx index edc4b2396d..9b57033bc1 100644 --- a/ui/components/compliance/data-compliance/data-compliance.tsx +++ b/ui/components/compliance/data-compliance/data-compliance.tsx @@ -1,22 +1,20 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect } from "react"; import { SelectScanComplianceData } from "@/components/compliance/data-compliance"; -import { CrossIcon } from "@/components/icons"; -import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom"; - +import { SelectScanComplianceDataProps } from "@/types"; interface DataComplianceProps { - scans: { id: string; name: string; state: string; progress: number }[]; - regions: string[]; + scans: SelectScanComplianceDataProps["scans"]; } -export const DataCompliance = ({ scans, regions }: DataComplianceProps) => { +export const DataCompliance = ({ scans }: DataComplianceProps) => { const router = useRouter(); const searchParams = useSearchParams(); - const [showClearButton, setShowClearButton] = useState(false); + const scanIdParam = searchParams.get("scanId"); + const selectedScanId = scanIdParam || (scans.length > 0 ? scans[0].id : ""); useEffect(() => { @@ -27,71 +25,20 @@ export const DataCompliance = ({ scans, regions }: DataComplianceProps) => { } }, [scans, scanIdParam, searchParams, router]); - useEffect(() => { - const hasFilters = Array.from(searchParams.keys()).some( - (key) => key.startsWith("filter[") || key === "sort", - ); - setShowClearButton(hasFilters); - }, [searchParams]); const handleScanChange = (selectedKey: string) => { const params = new URLSearchParams(searchParams); params.set("scanId", selectedKey); router.push(`?${params.toString()}`); }; - const pushDropdownFilter = useCallback( - (key: string, values: string[]) => { - const params = new URLSearchParams(searchParams); - const filterKey = `filter[${key}]`; - - if (values.length === 0) { - params.delete(filterKey); - } else { - params.set(filterKey, values.join(",")); - } - - router.push(`?${params.toString()}`); - }, - [router, searchParams], - ); - const clearAllFilters = useCallback(() => { - const params = new URLSearchParams(searchParams.toString()); - Array.from(params.keys()).forEach((key) => { - if (key.startsWith("filter[") || key === "sort") { - params.delete(key); - } - }); - router.push(`?${params.toString()}`, { scroll: false }); - }, [router, searchParams]); return (
-
+
- - {showClearButton && ( - } - radius="sm" - > - Reset - - )}
); diff --git a/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx b/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx index 6bbfd3d4e0..77e4b12198 100644 --- a/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx +++ b/ui/components/compliance/data-compliance/select-scan-compliance-data.tsx @@ -1,10 +1,8 @@ import { Select, SelectItem } from "@nextui-org/react"; -interface SelectScanComplianceDataProps { - scans: { id: string; name: string; state: string; progress: number }[]; - selectedScanId: string; - onSelectionChange: (selectedKey: string) => void; -} +import { SelectScanComplianceDataProps } from "@/types"; + +import { ComplianceScanInfo } from "../compliance-scan-info"; export const SelectScanComplianceData = ({ scans, @@ -18,8 +16,8 @@ export const SelectScanComplianceData = ({ classNames={{ selectorIcon: "right-2", }} + size="lg" labelPlacement="outside" - size="md" selectedKeys={new Set([selectedScanId])} onSelectionChange={(keys) => onSelectionChange(Array.from(keys)[0] as string) @@ -27,25 +25,15 @@ export const SelectScanComplianceData = ({ renderValue={() => { const selectedItem = scans.find((item) => item.id === selectedScanId); return selectedItem ? ( -
- {selectedItem.name} - - State: {selectedItem.state}, Progress: {selectedItem.progress}% - -
+ ) : ( "Select a scan" ); }} > {scans.map((scan) => ( - -
- {scan.name} - - State: {scan.state}, Progress: {scan.progress}% - -
+ + ))} diff --git a/ui/components/compliance/index.ts b/ui/components/compliance/index.ts index bb1b06d995..d0ba1eb027 100644 --- a/ui/components/compliance/index.ts +++ b/ui/components/compliance/index.ts @@ -1,2 +1,4 @@ export * from "./compliance-card"; +export * from "./compliance-scan-info"; export * from "./compliance-skeleton-grid"; +export * from "./no-scans-available"; diff --git a/ui/components/compliance/no-scans-available.tsx b/ui/components/compliance/no-scans-available.tsx new file mode 100644 index 0000000000..6089bfd846 --- /dev/null +++ b/ui/components/compliance/no-scans-available.tsx @@ -0,0 +1,41 @@ +"use client"; + +import React from "react"; + +import { InfoIcon } from "../icons/Icons"; +import { CustomButton } from "../ui/custom"; + +export const NoScansAvailable = () => { + return ( +
+
+
+
+
+ +
+

+ No Scans available +

+

+ A scan must be completed before generating a compliance + report. +

+
+
+ + Go to Scans + +
+
+
+
+ ); +}; diff --git a/ui/components/ui/entities/entity-info-short.tsx b/ui/components/ui/entities/entity-info-short.tsx index e984772f39..fcfbe84ae2 100644 --- a/ui/components/ui/entities/entity-info-short.tsx +++ b/ui/components/ui/entities/entity-info-short.tsx @@ -7,22 +7,27 @@ interface EntityInfoProps { cloudProvider: "aws" | "azure" | "gcp" | "kubernetes"; entityAlias?: string; entityId?: string; + hideCopyButton?: boolean; } export const EntityInfoShort: React.FC = ({ cloudProvider, entityAlias, entityId, + hideCopyButton = false, }) => { return (
{getProviderLogo(cloudProvider)}
-
+
{entityAlias && ( - {entityAlias} + {entityAlias} )} - +
diff --git a/ui/components/ui/entities/snippet-id.tsx b/ui/components/ui/entities/snippet-id.tsx index c485763070..cb4b1a7e87 100644 --- a/ui/components/ui/entities/snippet-id.tsx +++ b/ui/components/ui/entities/snippet-id.tsx @@ -5,9 +5,14 @@ import { CopyIcon, DoneIcon, IdIcon } from "@/components/icons"; interface SnippetIdProps { entityId: string; + hideCopyButton?: boolean; [key: string]: any; } -export const SnippetId: React.FC = ({ entityId, ...props }) => { +export const SnippetId: React.FC = ({ + entityId, + hideCopyButton = false, + ...props +}) => { return ( = ({ entityId, ...props }) => { hideSymbol copyIcon={} checkIcon={} + hideCopyButton={hideCopyButton} {...props} >

diff --git a/ui/types/components.ts b/ui/types/components.ts index 312cb51a18..342e4d401f 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -42,6 +42,18 @@ export interface CollapseMenuButtonProps { isOpen: boolean | undefined; } +export interface SelectScanComplianceDataProps { + scans: (ScanProps & { + providerInfo: { + provider: "aws" | "azure" | "gcp" | "kubernetes"; + uid: string; + alias: string; + }; + })[]; + selectedScanId: string; + onSelectionChange: (selectedKey: string) => void; +} + export type NextUIVariants = | "solid" | "faded"