From f8af960909ea172a7b472d6cff9fc571a7284b7c Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sun, 24 Nov 2024 15:22:12 +0100 Subject: [PATCH 1/2] feat: add graph in overview page with providers overview --- actions/overview/overview.ts | 49 ++++++++ app/(prowler)/page.tsx | 52 ++++---- components/overview/index.ts | 2 + .../provider-overview/provider-overview.tsx | 111 ++++++++++++++++++ .../skeleton-provider-overview.tsx | 64 ++++++++++ types/components.ts | 21 ++++ 6 files changed, 278 insertions(+), 21 deletions(-) create mode 100644 actions/overview/overview.ts create mode 100644 components/overview/provider-overview/provider-overview.tsx create mode 100644 components/overview/provider-overview/skeleton-provider-overview.tsx diff --git a/actions/overview/overview.ts b/actions/overview/overview.ts new file mode 100644 index 0000000000..8d83ab7088 --- /dev/null +++ b/actions/overview/overview.ts @@ -0,0 +1,49 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { auth } from "@/auth.config"; +import { parseStringify } from "@/lib"; + +export const getProvidersOverview = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/providers-overview"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL(`${keyServer}/overviews/providers`); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + + const data = await response.json(); + const parsedData = parseStringify(data); + revalidatePath("/providers-overview"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching providers overview:", error); + return undefined; + } +}; diff --git a/app/(prowler)/page.tsx b/app/(prowler)/page.tsx index 335b53b4f4..bae0d835c4 100644 --- a/app/(prowler)/page.tsx +++ b/app/(prowler)/page.tsx @@ -1,32 +1,42 @@ import { Spacer } from "@nextui-org/react"; +import { Suspense } from "react"; +import { getProvidersOverview } from "@/actions/overview/overview"; +import { + ProvidersOverview, + SkeletonProvidersOverview, +} from "@/components/overview"; import { Header } from "@/components/ui"; export default function Home() { return ( <>
- - {/*
- - - - - - - - - -
*/} + +
+
+ {/* Providers Overview */} +
+ }> + + +
+ +
+ +
+
+
); } + +const SSRProvidersOverview = async () => { + const providersOverview = await getProvidersOverview({}); + + if (!providersOverview) { + return

There is no providers overview info available

; + } + + return ; +}; diff --git a/components/overview/index.ts b/components/overview/index.ts index 452749b068..eb393767f1 100644 --- a/components/overview/index.ts +++ b/components/overview/index.ts @@ -1 +1,3 @@ export * from "./AttackSurface"; +export * from "./provider-overview/provider-overview"; +export * from "./provider-overview/skeleton-provider-overview"; diff --git a/components/overview/provider-overview/provider-overview.tsx b/components/overview/provider-overview/provider-overview.tsx new file mode 100644 index 0000000000..6c5edfb102 --- /dev/null +++ b/components/overview/provider-overview/provider-overview.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { Card, CardBody, CardHeader } from "@nextui-org/react"; + +import { AddIcon } from "@/components/icons/Icons"; +import { + AWSProviderBadge, + AzureProviderBadge, + GCPProviderBadge, + KS8ProviderBadge, +} from "@/components/icons/providers-badge"; +import { CustomButton } from "@/components/ui/custom/custom-button"; +import { ProviderOverviewProps } from "@/types"; + +export const ProvidersOverview = ({ + providersOverview, +}: { + providersOverview: ProviderOverviewProps; +}) => { + console.log(providersOverview); + if (!providersOverview || !Array.isArray(providersOverview.data)) { + return

No provider data available

; + } + + const calculatePassingPercentage = (pass: number, total: number) => + total > 0 ? ((pass / total) * 100).toFixed(2) : "0.00"; + + const renderProviderBadge = (providerId: string) => { + switch (providerId) { + case "aws": + return ; + case "azure": + return ; + case "gcp": + return ; + case "kubernetes": + return ; + default: + return null; + } + }; + + return ( + + +

Providers Overview

+
+ +
+
+ Provider + + Percent + Passing + + + Failing + Checks + + + Total + Resources + +
+ + {providersOverview.data.length === 0 ? ( +
+ - + - + - + - +
+ ) : ( + providersOverview.data.map((provider) => { + const { pass, fail, total } = provider.attributes.findings; + const resourcesTotal = provider.attributes.resources.total; + + return ( +
+ + {renderProviderBadge(provider.id)} + + + {calculatePassingPercentage(pass, total)}% + + {fail} + {resourcesTotal} +
+ ); + }) + )} +
+
+ } + > + Go to Providers + +
+
+
+ ); +}; diff --git a/components/overview/provider-overview/skeleton-provider-overview.tsx b/components/overview/provider-overview/skeleton-provider-overview.tsx new file mode 100644 index 0000000000..47febd1700 --- /dev/null +++ b/components/overview/provider-overview/skeleton-provider-overview.tsx @@ -0,0 +1,64 @@ +import { Card, CardBody, CardHeader, Skeleton } from "@nextui-org/react"; + +export const SkeletonProvidersOverview = () => { + const rows = 4; + + return ( + + + +
+
+
+ +
+ {/* Header Skeleton */} +
+ +
+
+ +
+
+ +
+
+ +
+
+
+ + {/* Row Skeletons */} + {Array.from({ length: rows }).map((_, index) => ( +
+ {/* Provider Name */} +
+ +
+
+ +
+
+
+ {/* Percent Passing */} + +
+
+ {/* Failing Checks */} + +
+
+ {/* Total Resources */} + +
+
+
+ ))} +
+
+
+ ); +}; diff --git a/types/components.ts b/types/components.ts index 7ec383d408..bbc7024646 100644 --- a/types/components.ts +++ b/types/components.ts @@ -26,6 +26,27 @@ export type NextUIColors = | "danger" | "default"; +export interface ProviderOverviewProps { + data: { + type: "provider-overviews"; + id: "aws" | "gcp" | "azure" | "kubernetes"; + attributes: { + findings: { + pass: number; + fail: number; + manual: number; + total: number; + }; + resources: { + total: number; + }; + }; + }[]; + meta: { + version: string; + }; +} + export interface TaskDetails { attributes: { state: string; From cfd4339c411652a2bf294d6815b06220600e36ec Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 25 Nov 2024 12:11:27 +0100 Subject: [PATCH 2/2] feat: render all providers with or without data --- .../provider-overview/provider-overview.tsx | 99 +++++++++++++------ 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/components/overview/provider-overview/provider-overview.tsx b/components/overview/provider-overview/provider-overview.tsx index 6c5edfb102..cd69f02595 100644 --- a/components/overview/provider-overview/provider-overview.tsx +++ b/components/overview/provider-overview/provider-overview.tsx @@ -40,6 +40,13 @@ export const ProvidersOverview = ({ } }; + const providers = [ + { id: "aws", name: "AWS" }, + { id: "azure", name: "Azure" }, + { id: "gcp", name: "GCP" }, + { id: "kubernetes", name: "Kubernetes" }, + ]; + return ( @@ -63,46 +70,78 @@ export const ProvidersOverview = ({ - {providersOverview.data.length === 0 ? ( -
- - - - - - - - -
- ) : ( - providersOverview.data.map((provider) => { - const { pass, fail, total } = provider.attributes.findings; - const resourcesTotal = provider.attributes.resources.total; + {providers.map((providerTemplate) => { + const providerData = providersOverview.data.find( + (p) => p.id === providerTemplate.id, + ); - return ( -
- - {renderProviderBadge(provider.id)} - - - {calculatePassingPercentage(pass, total)}% - - {fail} - {resourcesTotal} -
- ); - }) - )} + return ( +
+ + {renderProviderBadge(providerTemplate.id)} + + + {providerData + ? calculatePassingPercentage( + providerData.attributes.findings.pass, + providerData.attributes.findings.total, + ) + : "0.00"} + % + + + {providerData ? providerData.attributes.findings.fail : "-"} + + + {providerData ? providerData.attributes.resources.total : "-"} + +
+ ); + })} + + {/* Totals row */} +
+ Total + + {calculatePassingPercentage( + providersOverview.data.reduce( + (sum, provider) => sum + provider.attributes.findings.pass, + 0, + ), + providersOverview.data.reduce( + (sum, provider) => sum + provider.attributes.findings.total, + 0, + ), + )} + % + + + {providersOverview.data.reduce( + (sum, provider) => sum + provider.attributes.findings.fail, + 0, + )} + + + {providersOverview.data.reduce( + (sum, provider) => sum + provider.attributes.resources.total, + 0, + )} + +
} > - Go to Providers + Add Provider