diff --git a/ui/app/(prowler)/compliance/page.test.tsx b/ui/app/(prowler)/compliance/page.test.tsx
index 42bbbe672f..e149dd5ade 100644
--- a/ui/app/(prowler)/compliance/page.test.tsx
+++ b/ui/app/(prowler)/compliance/page.test.tsx
@@ -13,4 +13,15 @@ describe("Compliance overview page", () => {
expect(source).toContain("ComplianceOverviewGrid");
expect(source).not.toContain("filter[search]");
});
+
+ it("switches to aggregated mode when provider filters are present", () => {
+ expect(source).toContain("hasComplianceProviderFilters");
+ expect(source).toContain("extractComplianceProviderFilters");
+ });
+
+ it("feeds the provider and provider-group selectors", () => {
+ expect(source).toContain("getAllProviders");
+ expect(source).toContain("getAllProviderGroups");
+ expect(source).toContain("providerGroups={");
+ });
});
diff --git a/ui/app/(prowler)/compliance/page.tsx b/ui/app/(prowler)/compliance/page.tsx
index d4ee609a0c..5334214c6d 100644
--- a/ui/app/(prowler)/compliance/page.tsx
+++ b/ui/app/(prowler)/compliance/page.tsx
@@ -5,7 +5,9 @@ import {
getComplianceOverviewMetadataInfo,
getCompliancesOverview,
} from "@/actions/compliances";
+import { getAllProviderGroups } from "@/actions/manage-groups/manage-groups";
import { getThreatScore } from "@/actions/overview";
+import { getAllProviders } from "@/actions/providers/providers";
import { getScans } from "@/actions/scans";
import {
ComplianceSkeletonGrid,
@@ -17,6 +19,10 @@ import { ComplianceOverviewGrid } from "@/components/compliance/compliance-overv
import { Alert, AlertDescription } from "@/components/shadcn/alert";
import { Card, CardContent } from "@/components/shadcn/card/card";
import { ContentLayout } from "@/components/ui";
+import {
+ extractComplianceProviderFilters,
+ hasComplianceProviderFilters,
+} from "@/lib/compliance/compliance-provider-filters";
import { pickLatestCisPerProvider } from "@/lib/compliance/compliance-report-types";
import {
ExpandedScanData,
@@ -34,16 +40,24 @@ export default async function Compliance({
const resolvedSearchParams = await searchParams;
const searchParamsKey = JSON.stringify(resolvedSearchParams || {});
- const scansData = await getScans({
- filters: {
- "filter[state]": "completed",
- },
- pageSize: 50,
- fields: {
- scans: "name,completed_at,provider",
- },
- include: "provider",
- });
+ const hasProviderFilters = hasComplianceProviderFilters(resolvedSearchParams);
+ const providerFilters =
+ extractComplianceProviderFilters(resolvedSearchParams);
+
+ const [scansData, providersData, providerGroupsData] = await Promise.all([
+ getScans({
+ filters: {
+ "filter[state]": "completed",
+ },
+ pageSize: 50,
+ fields: {
+ scans: "name,completed_at,provider",
+ },
+ include: "provider",
+ }),
+ getAllProviders(),
+ getAllProviderGroups(),
+ ]);
if (!scansData?.data) {
return (
@@ -90,9 +104,12 @@ export default async function Compliance({
const scanIdFromUrl = Array.isArray(scanIdParam)
? scanIdParam[0]
: scanIdParam;
- const selectedScanId: string | null =
- scanIdFromUrl || expandedScansData[0]?.id || null;
- const onboardingAction = selectedScanId
+ // Aggregated mode ignores scanId entirely (backend XOR); provider filters drive scope.
+ const selectedScanId: string | null = hasProviderFilters
+ ? null
+ : scanIdFromUrl || expandedScansData[0]?.id || null;
+ const hasScope = hasProviderFilters || Boolean(selectedScanId);
+ const onboardingAction = hasScope
? { flowId: "view-compliance" }
: {
flowId: "view-compliance",
@@ -115,13 +132,15 @@ export default async function Compliance({
}
: undefined;
- const metadataInfoData = selectedScanId
- ? await getComplianceOverviewMetadataInfo({
- filters: {
- "filter[scan_id]": selectedScanId,
- },
- })
- : { data: { attributes: { regions: [] } } };
+ const metadataInfoData = hasProviderFilters
+ ? await getComplianceOverviewMetadataInfo({ filters: providerFilters })
+ : selectedScanId
+ ? await getComplianceOverviewMetadataInfo({
+ filters: {
+ "filter[scan_id]": selectedScanId,
+ },
+ })
+ : { data: { attributes: { regions: [] } } };
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
@@ -146,13 +165,15 @@ export default async function Compliance({
icon="lucide:shield-check"
onboardingAction={onboardingAction}
>
- {selectedScanId ? (
+ {hasScope ? (
<>
@@ -182,6 +203,8 @@ export default async function Compliance({
searchParams={resolvedSearchParams}
scanId={selectedScanId}
selectedScan={selectedScanData}
+ hasProviderFilters={hasProviderFilters}
+ providerFilters={providerFilters}
/>
>
@@ -196,15 +219,23 @@ const SSRComplianceGrid = async ({
searchParams,
scanId,
selectedScan,
+ hasProviderFilters,
+ providerFilters,
}: {
searchParams: SearchParamsProps;
scanId: string | null;
selectedScan?: ScanEntity;
+ hasProviderFilters: boolean;
+ providerFilters: Record;
}) => {
const regionFilter = searchParams["filter[region__in]"]?.toString() || "";
- const compliancesData =
- scanId && scanId.trim() !== ""
+ const compliancesData = hasProviderFilters
+ ? await getCompliancesOverview({
+ region: regionFilter,
+ filters: providerFilters,
+ })
+ : scanId && scanId.trim() !== ""
? await getCompliancesOverview({
scanId,
region: regionFilter,
@@ -230,8 +261,9 @@ const SSRComplianceGrid = async ({
- This scan has no compliance data available yet, please select a
- different one.
+ {hasProviderFilters
+ ? "No completed scans match the selected providers."
+ : "This scan has no compliance data available yet, please select a different one."}
);
diff --git a/ui/components/compliance/compliance-card.test.tsx b/ui/components/compliance/compliance-card.test.tsx
index 996c8cb979..b5a24b9cc2 100644
--- a/ui/components/compliance/compliance-card.test.tsx
+++ b/ui/components/compliance/compliance-card.test.tsx
@@ -27,4 +27,21 @@ describe("ComplianceCard", () => {
expect(source).toContain('orientation="column"');
expect(source).toContain('buttonWidth="icon"');
});
+
+ it("derives aggregated mode from the provider filters in the URL", () => {
+ expect(source).toContain("extractComplianceProviderFilters");
+ expect(source).toContain("const isAggregated =");
+ });
+
+ it("hides the per-scan PDF download in aggregated mode", () => {
+ expect(source).toContain("{!isAggregated && (");
+ expect(source).toContain(" {
+ expect(source).toContain(
+ "providerFilters: isAggregated ? providerFilters : undefined",
+ );
+ expect(source).toContain("scanId: isAggregated ? null : scanId");
+ });
});
diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx
index 3972bb8db4..4546d9f13c 100644
--- a/ui/components/compliance/compliance-card.tsx
+++ b/ui/components/compliance/compliance-card.tsx
@@ -11,6 +11,7 @@ import {
TooltipTrigger,
} from "@/components/shadcn/tooltip";
import { buildComplianceDetailPath } from "@/lib/compliance/compliance-detail-url";
+import { extractComplianceProviderFilters } from "@/lib/compliance/compliance-provider-filters";
import { getReportTypeForCompliance } from "@/lib/compliance/compliance-report-types";
import {
getScoreIndicatorClass,
@@ -55,6 +56,13 @@ export const ComplianceCard: React.FC = ({
const router = useRouter();
const hasRegionFilter = searchParams.has("filter[region__in]");
+ // Aggregated mode: provider filters replace the single-scan scope, so per-scan
+ // affordances (CIS PDF) are hidden and the drill-down carries provider filters.
+ const providerFilters = extractComplianceProviderFilters(
+ new URLSearchParams(searchParams.toString()),
+ );
+ const isAggregated = Object.keys(providerFilters).length > 0;
+
const formatTitle = (title: string) => {
return title.split("-").join(" ");
};
@@ -75,8 +83,9 @@ export const ComplianceCard: React.FC = ({
title,
complianceId: id,
version,
- scanId,
+ scanId: isAggregated ? null : scanId,
regionFilter: searchParams.get("filter[region__in]"),
+ providerFilters: isAggregated ? providerFilters : undefined,
}),
);
};
@@ -88,32 +97,34 @@ export const ComplianceCard: React.FC = ({
className="relative cursor-pointer transition-shadow hover:shadow-md"
onClick={navigateToDetail}
>
- e.stopPropagation()}
- onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
- e.stopPropagation();
- }
- }}
- role="group"
- tabIndex={0}
- >
-
-
+ {!isAggregated && (
+ e.stopPropagation()}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.stopPropagation();
+ }
+ }}
+ role="group"
+ tabIndex={0}
+ >
+
+
+ )}
diff --git a/ui/components/compliance/compliance-header/compliance-filters.test.tsx b/ui/components/compliance/compliance-header/compliance-filters.test.tsx
new file mode 100644
index 0000000000..88f39e9ea3
--- /dev/null
+++ b/ui/components/compliance/compliance-header/compliance-filters.test.tsx
@@ -0,0 +1,151 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { ComplianceFilters } from "./compliance-filters";
+
+const { pushMock, updateFilterMock } = vi.hoisted(() => ({
+ pushMock: vi.fn(),
+ updateFilterMock: vi.fn(),
+}));
+
+let currentSearchParams = new URLSearchParams();
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: pushMock }),
+ useSearchParams: () => currentSearchParams,
+}));
+
+vi.mock("@/hooks/use-url-filters", () => ({
+ useUrlFilters: () => ({ updateFilter: updateFilterMock }),
+}));
+
+vi.mock("./scan-selector", () => ({
+ ScanSelector: ({
+ onSelectionChange,
+ }: {
+ onSelectionChange: (key: string) => void;
+ }) => (
+