Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -10,12 +10,15 @@ All notable changes to the **Prowler UI** are documented in this file.
- New findings table UI with new design system components, improved filtering UX, and enhanced table interactions [(#9699)](https://github.com/prowler-cloud/prowler/pull/9699)
- Add gradient background to Risk Plot for visual risk context [(#9664)](https://github.com/prowler-cloud/prowler/pull/9664)
- Add ThreatScore pillar breakdown to Compliance Summary page and detail view [(#9773)](https://github.com/prowler-cloud/prowler/pull/9773)
- Add Provider and Group filters to Resources page [(#9492)](https://github.com/prowler-cloud/prowler/pull/9492)
- Compliance Watchlist component in Overview page [(#9786)](https://github.com/prowler-cloud/prowler/pull/9786)

### 🔄 Changed

- Refactor Lighthouse AI MCP tool filtering from blacklist to whitelist approach for improved security [(#9802)](https://github.com/prowler-cloud/prowler/pull/9802)
- 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 @@ -83,6 +86,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
12 changes: 12 additions & 0 deletions ui/actions/overview/resources-inventory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export { getResourceGroupOverview } from "./resources-inventory";
export {
adaptResourceGroupOverview,
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
Loading