diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index c6e0b8630f..d6fd178995 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added +- Attack Surface component to Overview page [(#9412)](https://github.com/prowler-cloud/prowler/pull/9412) - Compliance Watchlist component to Overview page [(#9199)](https://github.com/prowler-cloud/prowler/pull/9199) - Service Watchlist component to Overview page [(#9316)](https://github.com/prowler-cloud/prowler/pull/9316) - Risk Pipeline component with Sankey chart to Overview page [(#9317)](https://github.com/prowler-cloud/prowler/pull/9317) diff --git a/ui/actions/overview/attack-surface.adapter.ts b/ui/actions/overview/attack-surface.adapter.ts new file mode 100644 index 0000000000..a35a026682 --- /dev/null +++ b/ui/actions/overview/attack-surface.adapter.ts @@ -0,0 +1,84 @@ +import { + AttackSurfaceOverview, + AttackSurfaceOverviewResponse, +} from "./types/attack-surface"; + +const ATTACK_SURFACE_IDS = { + INTERNET_EXPOSED: "internet-exposed", + SECRETS: "secrets", + PRIVILEGE_ESCALATION: "privilege-escalation", + EC2_IMDSV1: "ec2-imdsv1", +} as const; + +export type AttackSurfaceId = + (typeof ATTACK_SURFACE_IDS)[keyof typeof ATTACK_SURFACE_IDS]; + +export interface AttackSurfaceItem { + id: AttackSurfaceId; + label: string; + failedFindings: number; + totalFindings: number; +} + +const ATTACK_SURFACE_LABELS: Record = { + [ATTACK_SURFACE_IDS.INTERNET_EXPOSED]: "Internet Exposed Resources", + [ATTACK_SURFACE_IDS.SECRETS]: "Exposed Secrets", + [ATTACK_SURFACE_IDS.PRIVILEGE_ESCALATION]: "IAM Policy Privilege Escalation", + [ATTACK_SURFACE_IDS.EC2_IMDSV1]: "EC2 with IMDSv1 Enabled", +}; + +const ATTACK_SURFACE_ORDER: AttackSurfaceId[] = [ + ATTACK_SURFACE_IDS.INTERNET_EXPOSED, + ATTACK_SURFACE_IDS.SECRETS, + ATTACK_SURFACE_IDS.PRIVILEGE_ESCALATION, + ATTACK_SURFACE_IDS.EC2_IMDSV1, +]; + +function mapAttackSurfaceItem(item: AttackSurfaceOverview): AttackSurfaceItem { + const id = item.id as AttackSurfaceId; + return { + id, + label: ATTACK_SURFACE_LABELS[id] || item.id, + failedFindings: item.attributes.failed_findings, + totalFindings: item.attributes.total_findings, + }; +} + +/** + * Adapts the attack surface overview API response to a format suitable for the UI. + * Returns the items in a consistent order as defined by ATTACK_SURFACE_ORDER. + * + * @param response - The attack surface overview API response + * @returns An array of AttackSurfaceItem objects sorted by the predefined order + */ +export function adaptAttackSurfaceOverview( + response: AttackSurfaceOverviewResponse | undefined, +): AttackSurfaceItem[] { + if (!response?.data || response.data.length === 0) { + return []; + } + + // Create a map for quick lookup + const itemsMap = new Map(); + for (const item of response.data) { + itemsMap.set(item.id, item); + } + + // Return items in the predefined order + const sortedItems: AttackSurfaceItem[] = []; + for (const id of ATTACK_SURFACE_ORDER) { + const item = itemsMap.get(id); + if (item) { + sortedItems.push(mapAttackSurfaceItem(item)); + } + } + + // Include any items that might be in the response but not in our predefined order + for (const item of response.data) { + if (!ATTACK_SURFACE_ORDER.includes(item.id as AttackSurfaceId)) { + sortedItems.push(mapAttackSurfaceItem(item)); + } + } + + return sortedItems; +} diff --git a/ui/actions/overview/index.ts b/ui/actions/overview/index.ts index e78b414c0c..ab7cba34a0 100644 --- a/ui/actions/overview/index.ts +++ b/ui/actions/overview/index.ts @@ -1,3 +1,4 @@ +export * from "./attack-surface.adapter"; export * from "./overview"; export * from "./sankey.adapter"; export * from "./threat-map.adapter"; diff --git a/ui/actions/overview/overview.ts b/ui/actions/overview/overview.ts index 16be21970a..fed9c8ac14 100644 --- a/ui/actions/overview/overview.ts +++ b/ui/actions/overview/overview.ts @@ -5,6 +5,7 @@ import { apiBaseUrl, getAuthHeaders } from "@/lib"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { + AttackSurfaceOverviewResponse, FindingsSeverityOverviewResponse, ProvidersOverviewResponse, RegionsOverviewResponse, @@ -84,7 +85,12 @@ export const getFindingsByStatus = async ({ query = "", sort = "", filters = {}, -}) => { +}: { + page?: number; + query?: string; + sort?: string; + filters?: Record; +} = {}) => { const headers = await getAuthHeaders({ contentType: false }); if (isNaN(Number(page)) || page < 1) redirect("/"); @@ -207,3 +213,31 @@ export const getRegionsOverview = async ({ return undefined; } }; + +export const getAttackSurfaceOverview = async ({ + filters = {}, +}: { + filters?: Record; +} = {}): Promise => { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/overviews/attack-surfaces`); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]" && value !== undefined) { + url.searchParams.append(key, String(value)); + } + }); + + try { + const response = await fetch(url.toString(), { + headers, + }); + + return handleApiResponse(response); + } catch (error) { + console.error("Error fetching attack surface overview:", error); + return undefined; + } +}; diff --git a/ui/actions/overview/types/attack-surface.ts b/ui/actions/overview/types/attack-surface.ts new file mode 100644 index 0000000000..c7a57802f7 --- /dev/null +++ b/ui/actions/overview/types/attack-surface.ts @@ -0,0 +1,22 @@ +// Attack Surface Overview Types +// Corresponds to the /overviews/attack-surfaces endpoint + +import { OverviewResponseMeta } from "./common"; + +export interface AttackSurfaceOverviewAttributes { + total_findings: number; + failed_findings: number; + muted_failed_findings: number; + check_ids: string[]; +} + +export interface AttackSurfaceOverview { + type: "attack-surface-overviews"; + id: string; + attributes: AttackSurfaceOverviewAttributes; +} + +export interface AttackSurfaceOverviewResponse { + data: AttackSurfaceOverview[]; + meta: OverviewResponseMeta; +} diff --git a/ui/actions/overview/types/index.ts b/ui/actions/overview/types/index.ts index 6af679407b..d3b9248bc7 100644 --- a/ui/actions/overview/types/index.ts +++ b/ui/actions/overview/types/index.ts @@ -1,3 +1,4 @@ +export * from "./attack-surface"; export * from "./common"; export * from "./findings-severity"; export * from "./providers"; diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-card-item.tsx b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-card-item.tsx new file mode 100644 index 0000000000..b652f4e189 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-card-item.tsx @@ -0,0 +1,29 @@ +import { AttackSurfaceItem } from "@/actions/overview"; +import { Card, CardContent } from "@/components/shadcn"; + +interface AttackSurfaceCardItemProps { + item: AttackSurfaceItem; +} + +export function AttackSurfaceCardItem({ item }: AttackSurfaceCardItemProps) { + return ( + + + + + {item.label} + + + + ); +} diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-skeleton.tsx b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-skeleton.tsx new file mode 100644 index 0000000000..9df2e1afb0 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface-skeleton.tsx @@ -0,0 +1,30 @@ +import { Card, CardContent, Skeleton } from "@/components/shadcn"; + +export function AttackSurfaceSkeleton() { + return ( + + + + {Array.from({ length: 4 }).map((_, index) => ( + + ))} + + + ); +} diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.ssr.tsx b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.ssr.tsx new file mode 100644 index 0000000000..0fd3b0604b --- /dev/null +++ b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.ssr.tsx @@ -0,0 +1,22 @@ +import { + adaptAttackSurfaceOverview, + getAttackSurfaceOverview, +} from "@/actions/overview"; +import { SearchParamsProps } from "@/types"; + +import { pickFilterParams } from "../../lib/filter-params"; +import { AttackSurface } from "./attack-surface"; + +export const AttackSurfaceSSR = async ({ + searchParams, +}: { + searchParams: SearchParamsProps | undefined | null; +}) => { + const filters = pickFilterParams(searchParams); + + const response = await getAttackSurfaceOverview({ filters }); + + const items = adaptAttackSurfaceOverview(response); + + return ; +}; diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.tsx b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.tsx new file mode 100644 index 0000000000..83f7cf3d76 --- /dev/null +++ b/ui/app/(prowler)/_new-overview/components/attack-surface/attack-surface.tsx @@ -0,0 +1,34 @@ +import { AttackSurfaceItem } from "@/actions/overview/attack-surface.adapter"; +import { Card, CardContent, CardTitle } from "@/components/shadcn"; + +import { AttackSurfaceCardItem } from "./attack-surface-card-item"; + +interface AttackSurfaceProps { + items: AttackSurfaceItem[]; +} + +export function AttackSurface({ items }: AttackSurfaceProps) { + const isEmpty = items.length === 0; + + return ( + + Attack Surface + + {isEmpty ? ( +
+

+ No attack surface data available. +

+
+ ) : ( + items.map((item) => ( + + )) + )} +
+
+ ); +} diff --git a/ui/app/(prowler)/_new-overview/components/attack-surface/index.ts b/ui/app/(prowler)/_new-overview/components/attack-surface/index.ts new file mode 100644 index 0000000000..8ab335b4ba --- /dev/null +++ b/ui/app/(prowler)/_new-overview/components/attack-surface/index.ts @@ -0,0 +1,4 @@ +export { AttackSurface } from "./attack-surface"; +export { AttackSurfaceSSR } from "./attack-surface.ssr"; +export { AttackSurfaceCardItem } from "./attack-surface-card-item"; +export { AttackSurfaceSkeleton } from "./attack-surface-skeleton"; diff --git a/ui/app/(prowler)/page.tsx b/ui/app/(prowler)/page.tsx index 56b6f59087..13876ee96b 100644 --- a/ui/app/(prowler)/page.tsx +++ b/ui/app/(prowler)/page.tsx @@ -5,6 +5,10 @@ import { ContentLayout } from "@/components/ui"; import { SearchParamsProps } from "@/types"; import { AccountsSelector } from "./_new-overview/components/accounts-selector"; +import { + AttackSurfaceSkeleton, + AttackSurfaceSSR, +} from "./_new-overview/components/attack-surface"; import { CheckFindingsSSR } from "./_new-overview/components/check-findings"; import { GraphsTabsWrapper } from "./_new-overview/components/graphs-tabs/graphs-tabs-wrapper"; import { RiskPipelineViewSkeleton } from "./_new-overview/components/graphs-tabs/risk-pipeline-view"; @@ -50,7 +54,15 @@ export default async function Home({ }> + + +
+ }> + + +
+
}>