Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ All notable changes to the **Prowler UI** are documented in this file.

- Add search bar when adding a provider [(#9634)](https://github.com/prowler-cloud/prowler/pull/9634)
- Add gradient background to Risk Plot for visual risk context [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
- Add Provider filter to Resources page [(#9492)](https://github.com/prowler-cloud/prowler/pull/9492)

### 🔄 Changed

- Refactor ScatterPlot as reusable generic component with TypeScript generics [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
- Rename resource_group filter to group in Resources page and Overview cards [(#9492)](https://github.com/prowler-cloud/prowler/pull/9492)
- Update Resources filters to use __in format for multi-select support [(#9492)](https://github.com/prowler-cloud/prowler/pull/9492)
- Swap Risk Plot axes: X = Fail Findings, Y = Prowler ThreatScore [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
- Remove duplicate scan_id filter badge from Findings page [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
- Remove unused hasDots prop from RadialChart component [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
Expand Down Expand Up @@ -66,6 +69,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- Navigation progress bar for page transitions using Next.js `onRouterTransitionStart` [(#9465)](https://github.com/prowler-cloud/prowler/pull/9465)
- Findings Severity Over Time chart component to Overview page [(#9405)](https://github.com/prowler-cloud/prowler/pull/9405)
- Attack Surface component to Overview page [(#9412)](https://github.com/prowler-cloud/prowler/pull/9412)
- Resource Inventory component to Overview page [(#9492)](https://github.com/prowler-cloud/prowler/pull/9492)
- Add Alibaba Cloud provider [(#9501)](https://github.com/prowler-cloud/prowler/pull/9501)

### 🔄 Changed
Expand Down
2 changes: 1 addition & 1 deletion ui/actions/overview/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// Re-export all overview actions from feature-based subfolders
export * from "./attack-surface";
export * from "./findings";
export * from "./providers";
export * from "./regions";
export * from "./resources-inventory";
export * from "./risk-plot";
export * from "./risk-radar";
export * from "./services";
Expand Down
13 changes: 13 additions & 0 deletions ui/actions/overview/resources-inventory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export { getResourceGroupOverview } from "./resources-inventory";
export {
adaptResourceGroupOverview,
getEmptyResourceInventoryItems,
RESOURCE_GROUP_IDS,
type ResourceGroupId,
type ResourceInventoryItem,
} from "./resources-inventory.adapter";
export type {
ResourceGroupOverview,
ResourceGroupOverviewResponse,
SeverityBreakdown,
} from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { LucideIcon } from "lucide-react";
import {
Activity,
BarChart3,
Bot,
Boxes,
Building2,
CloudCog,
Container,
Database,
FolderOpen,
GitBranch,
MessageSquare,
Network,
Server,
Shield,
SquareFunction,
UserRoundSearch,
Webhook,
} from "lucide-react";

import {
ResourceGroupOverview,
ResourceGroupOverviewResponse,
SeverityBreakdown,
} from "./types";

// Resource group IDs matching API values from ResourceGroup field specification
export const RESOURCE_GROUP_IDS = {
COMPUTE: "compute",
CONTAINER: "container",
SERVERLESS: "serverless",
DATABASE: "database",
STORAGE: "storage",
NETWORK: "network",
IAM: "IAM",
MESSAGING: "messaging",
SECURITY: "security",
MONITORING: "monitoring",
API_GATEWAY: "api_gateway",
AI_ML: "ai_ml",
GOVERNANCE: "governance",
COLLABORATION: "collaboration",
DEVOPS: "devops",
ANALYTICS: "analytics",
} as const;

export type ResourceGroupId =
(typeof RESOURCE_GROUP_IDS)[keyof typeof RESOURCE_GROUP_IDS];

export interface ResourceInventoryItem {
id: string;
label: string;
icon: LucideIcon;
totalResources: number;
totalFindings: number;
failedFindings: number;
newFailedFindings: number;
severity: SeverityBreakdown;
}

interface ResourceGroupConfig {
label: string;
icon: LucideIcon;
}

const RESOURCE_GROUP_CONFIG: Record<ResourceGroupId, ResourceGroupConfig> = {
[RESOURCE_GROUP_IDS.COMPUTE]: {
label: "Compute",
icon: Server,
},
[RESOURCE_GROUP_IDS.CONTAINER]: {
label: "Container",
icon: Container,
},
[RESOURCE_GROUP_IDS.SERVERLESS]: {
label: "Serverless",
icon: SquareFunction,
},
[RESOURCE_GROUP_IDS.DATABASE]: {
label: "Database",
icon: Database,
},
[RESOURCE_GROUP_IDS.STORAGE]: {
label: "Storage",
icon: FolderOpen,
},
[RESOURCE_GROUP_IDS.NETWORK]: {
label: "Network",
icon: Network,
},
[RESOURCE_GROUP_IDS.IAM]: {
label: "IAM",
icon: UserRoundSearch,
},
[RESOURCE_GROUP_IDS.MESSAGING]: {
label: "Messaging",
icon: MessageSquare,
},
[RESOURCE_GROUP_IDS.SECURITY]: {
label: "Security",
icon: Shield,
},
[RESOURCE_GROUP_IDS.MONITORING]: {
label: "Monitoring",
icon: Activity,
},
[RESOURCE_GROUP_IDS.API_GATEWAY]: {
label: "API Gateway",
icon: Webhook,
},
[RESOURCE_GROUP_IDS.AI_ML]: {
label: "AI/ML",
icon: Bot,
},
[RESOURCE_GROUP_IDS.GOVERNANCE]: {
label: "Governance",
icon: Building2,
},
[RESOURCE_GROUP_IDS.COLLABORATION]: {
label: "Collaboration",
icon: Boxes,
},
[RESOURCE_GROUP_IDS.DEVOPS]: {
label: "DevOps",
icon: GitBranch,
},
[RESOURCE_GROUP_IDS.ANALYTICS]: {
label: "Analytics",
icon: BarChart3,
},
};

// Default icon for unknown resource groups
const DEFAULT_ICON = CloudCog;

// Order in which resource groups should be displayed
const RESOURCE_GROUP_ORDER: ResourceGroupId[] = [
RESOURCE_GROUP_IDS.COMPUTE,
RESOURCE_GROUP_IDS.CONTAINER,
RESOURCE_GROUP_IDS.SERVERLESS,
RESOURCE_GROUP_IDS.DATABASE,
RESOURCE_GROUP_IDS.STORAGE,
RESOURCE_GROUP_IDS.NETWORK,
RESOURCE_GROUP_IDS.IAM,
RESOURCE_GROUP_IDS.MESSAGING,
RESOURCE_GROUP_IDS.SECURITY,
RESOURCE_GROUP_IDS.MONITORING,
RESOURCE_GROUP_IDS.API_GATEWAY,
RESOURCE_GROUP_IDS.AI_ML,
RESOURCE_GROUP_IDS.GOVERNANCE,
RESOURCE_GROUP_IDS.COLLABORATION,
RESOURCE_GROUP_IDS.DEVOPS,
RESOURCE_GROUP_IDS.ANALYTICS,
];

function mapResourceInventoryItem(
item: ResourceGroupOverview,
): ResourceInventoryItem {
const id = item.id;
const config = RESOURCE_GROUP_CONFIG[id as ResourceGroupId];

return {
id,
label: config?.label || formatResourceGroupLabel(id),
icon: config?.icon || DEFAULT_ICON,
totalResources: item.attributes.resources_count,
totalFindings: item.attributes.total_findings,
failedFindings: item.attributes.failed_findings,
newFailedFindings: item.attributes.new_failed_findings,
severity: item.attributes.severity,
};
}

/**
* Formats a resource group ID into a human-readable label.
* Handles snake_case and capitalizes appropriately.
*/
function formatResourceGroupLabel(id: string): string {
return id
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
}

/**
* Adapts the resource group overview API response to a format suitable for the UI.
* Returns the items in a consistent order as defined by RESOURCE_GROUP_ORDER.
*
* @param response - The resource group overview API response
* @returns An array of ResourceInventoryItem objects sorted by the predefined order
*/
export function adaptResourceGroupOverview(
response: ResourceGroupOverviewResponse | undefined,
): ResourceInventoryItem[] {
if (!response?.data || response.data.length === 0) {
return [];
}

// Create a map for quick lookup
const itemsMap = new Map<string, ResourceGroupOverview>();
for (const item of response.data) {
itemsMap.set(item.id, item);
}

// Return items in the predefined order
const sortedItems: ResourceInventoryItem[] = [];
for (const id of RESOURCE_GROUP_ORDER) {
const item = itemsMap.get(id);
if (item) {
sortedItems.push(mapResourceInventoryItem(item));
}
}

// Include any items that might be in the response but not in our predefined order
for (const item of response.data) {
if (!RESOURCE_GROUP_ORDER.includes(item.id as ResourceGroupId)) {
sortedItems.push(mapResourceInventoryItem(item));
}
}

return sortedItems;
}

/**
* Returns all resource groups with default/empty values.
* Useful for showing all groups even when no data is available.
*/
export function getEmptyResourceInventoryItems(): ResourceInventoryItem[] {
return RESOURCE_GROUP_ORDER.map((id) => {
const config = RESOURCE_GROUP_CONFIG[id];
return {
id,
label: config.label,
icon: config.icon,
totalResources: 0,
totalFindings: 0,
failedFindings: 0,
newFailedFindings: 0,
severity: {
informational: 0,
low: 0,
medium: 0,
high: 0,
critical: 0,
},
};
});
}
34 changes: 34 additions & 0 deletions ui/actions/overview/resources-inventory/resources-inventory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use server";

import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";

import { ResourceGroupOverviewResponse } from "./types";

export const getResourceGroupOverview = async ({
filters = {},
}: {
filters?: Record<string, string | string[] | undefined>;
} = {}): Promise<ResourceGroupOverviewResponse | undefined> => {
const headers = await getAuthHeaders({ contentType: false });

const url = new URL(`${apiBaseUrl}/overviews/resource-groups`);

// 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 resource group overview:", error);
return undefined;
}
};
1 change: 1 addition & 0 deletions ui/actions/overview/resources-inventory/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./resources-inventory.types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// GET /api/v1/overviews/resource-groups endpoint

interface OverviewResponseMeta {
version: string;
}

export interface SeverityBreakdown {
informational: number;
low: number;
medium: number;
high: number;
critical: number;
}

export interface ResourceGroupOverviewAttributes {
id: string;
total_findings: number;
failed_findings: number;
new_failed_findings: number;
resources_count: number;
severity: SeverityBreakdown;
}

export interface ResourceGroupOverview {
type: "resource-group-overview";
id: string;
attributes: ResourceGroupOverviewAttributes;
}

export interface ResourceGroupOverviewResponse {
data: ResourceGroupOverview[];
meta: OverviewResponseMeta;
}
Loading