diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.test.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.test.tsx
new file mode 100644
index 0000000000..bb1bfde5c7
--- /dev/null
+++ b/ui/components/compliance/compliance-accordion/client-accordion-content.test.tsx
@@ -0,0 +1,111 @@
+import { render, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import type { Requirement } from "@/types/compliance";
+
+import { ClientAccordionContent } from "./client-accordion-content";
+
+const { getFindingsMock, getLatestFindingsMock } = vi.hoisted(() => ({
+ getFindingsMock: vi.fn(),
+ getLatestFindingsMock: vi.fn(),
+}));
+
+let currentSearchParams = new URLSearchParams();
+
+vi.mock("next/navigation", () => ({
+ useSearchParams: () => currentSearchParams,
+}));
+
+vi.mock("@/actions/findings/findings", () => ({
+ getFindings: getFindingsMock,
+ getLatestFindings: getLatestFindingsMock,
+}));
+
+vi.mock("@/components/findings/table", () => ({
+ getStandaloneFindingColumns: () => [],
+ SkeletonTableFindings: () =>
,
+}));
+
+vi.mock("@/components/ui/accordion/Accordion", () => ({
+ Accordion: () => ,
+}));
+
+vi.mock("@/components/ui/table", () => ({
+ DataTable: () => ,
+}));
+
+vi.mock("@/lib/compliance/compliance-mapper", () => ({
+ getComplianceMapper: () => ({ getDetailsComponent: () => null }),
+}));
+
+vi.mock("@/lib", () => ({
+ createDict: () => ({}),
+ FINDINGS_DEFAULT_SORT: "severity",
+ MUTED_FILTER: { EXCLUDE: "false" },
+}));
+
+const requirement = {
+ check_ids: ["check-1"],
+ status: "FAIL",
+} as unknown as Requirement;
+
+describe("ClientAccordionContent findings drill-down", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ getFindingsMock.mockResolvedValue({ data: [], meta: {} });
+ getLatestFindingsMock.mockResolvedValue({ data: [], meta: {} });
+ });
+
+ describe("when provider filters drive aggregated mode", () => {
+ it("loads findings from the latest endpoint, not the scan-scoped one", async () => {
+ // Given - the URL carries a provider-scope filter and no scanId
+ currentSearchParams = new URLSearchParams({
+ complianceId: "cis_2.0_aws",
+ "filter[provider_type__in]": "aws",
+ });
+
+ // When
+ render(
+ ,
+ );
+
+ // Then - /findings 400s without a scan or date filter, so aggregated mode
+ // must use /findings/latest, forwarding the provider filters and no scan
+ await waitFor(() =>
+ expect(getLatestFindingsMock).toHaveBeenCalledTimes(1),
+ );
+ expect(getFindingsMock).not.toHaveBeenCalled();
+ const { filters } = getLatestFindingsMock.mock.calls[0][0];
+ expect(filters).toMatchObject({ "filter[provider_type__in]": "aws" });
+ expect(filters).not.toHaveProperty("filter[scan]");
+ });
+ });
+
+ describe("when a single scan drives the scope", () => {
+ it("loads findings from the scan-scoped endpoint", async () => {
+ // Given - no provider filters, a concrete scanId
+ currentSearchParams = new URLSearchParams({
+ complianceId: "cis_2.0_aws",
+ });
+
+ // When
+ render(
+ ,
+ );
+
+ // Then
+ await waitFor(() => expect(getFindingsMock).toHaveBeenCalledTimes(1));
+ expect(getLatestFindingsMock).not.toHaveBeenCalled();
+ const { filters } = getFindingsMock.mock.calls[0][0];
+ expect(filters).toMatchObject({ "filter[scan]": "scan-1" });
+ });
+ });
+});
diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
index 8e9d3ad0b2..43051ebef0 100644
--- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
+++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
@@ -3,7 +3,7 @@
import { useSearchParams } from "next/navigation";
import { useEffect, useRef, useState } from "react";
-import { getFindings } from "@/actions/findings/findings";
+import { getFindings, getLatestFindings } from "@/actions/findings/findings";
import {
getStandaloneFindingColumns,
SkeletonTableFindings,
@@ -51,9 +51,7 @@ export const ClientAccordionContent = ({
// so scope this requirement's findings by those providers rather than one scan.
// Stable string key keeps the effect deps free of a per-render object.
const providerScopeKey = new URLSearchParams(
- extractComplianceProviderFilters(
- new URLSearchParams(searchParams.toString()),
- ),
+ extractComplianceProviderFilters(searchParams),
).toString();
useEffect(() => {
@@ -77,10 +75,15 @@ export const ClientAccordionContent = ({
try {
const checkIds = requirement.check_ids;
const encodedSort = sort.replace(/^\+/, "");
- const scopeFilters = providerScopeKey
+ // Aggregated mode carries provider filters but no scan/date, which the
+ // /findings endpoint rejects (400). Use /findings/latest there — it
+ // needs neither and scopes to the latest scan per matching provider.
+ const isAggregated = providerScopeKey.length > 0;
+ const scopeFilters = isAggregated
? Object.fromEntries(new URLSearchParams(providerScopeKey))
: { "filter[scan]": scanId };
- const findingsData = await getFindings({
+ const loadFindings = isAggregated ? getLatestFindings : getFindings;
+ const findingsData = await loadFindings({
filters: {
"filter[check_id__in]": checkIds.join(","),
...scopeFilters,
diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx
index 4546d9f13c..7dd53eadb5 100644
--- a/ui/components/compliance/compliance-card.tsx
+++ b/ui/components/compliance/compliance-card.tsx
@@ -58,9 +58,7 @@ export const ComplianceCard: React.FC = ({
// 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 providerFilters = extractComplianceProviderFilters(searchParams);
const isAggregated = Object.keys(providerFilters).length > 0;
const formatTitle = (title: string) => {
diff --git a/ui/components/compliance/compliance-overview-grid.tsx b/ui/components/compliance/compliance-overview-grid.tsx
index f3e3ac5ebb..24d187320c 100644
--- a/ui/components/compliance/compliance-overview-grid.tsx
+++ b/ui/components/compliance/compliance-overview-grid.tsx
@@ -45,9 +45,7 @@ export const ComplianceOverviewGrid = ({
const [searchTerm, setSearchTerm] = useState("");
// Aggregated mode: provider filters in the URL replace the single-scan scope.
- const providerFilters = extractComplianceProviderFilters(
- new URLSearchParams(searchParams.toString()),
- );
+ const providerFilters = extractComplianceProviderFilters(searchParams);
const isAggregated = Object.keys(providerFilters).length > 0;
const filteredFrameworks = frameworks.filter((compliance) =>
diff --git a/ui/lib/compliance/compliance-provider-filters.ts b/ui/lib/compliance/compliance-provider-filters.ts
index bb2ace6784..be0e28ef72 100644
--- a/ui/lib/compliance/compliance-provider-filters.ts
+++ b/ui/lib/compliance/compliance-provider-filters.ts
@@ -1,16 +1,29 @@
+import type { ReadonlyURLSearchParams } from "next/navigation";
+
import type { SearchParamsProps } from "@/types/components";
import { FILTER_FIELD, FilterParam } from "@/types/filters";
/**
- * Provider-scope filter param keys the compliance UI sets — the same three the
- * overview dashboard uses, derived from the shared `FILTER_FIELD` source of
- * truth. The backend (`ComplianceOverviewViewSet`) treats these as an
- * alternative to `filter[scan_id]` (XOR) and aggregates compliance across the
- * latest completed scan of each matching provider.
+ * Provider-scope filter fields the compliance UI sets — a subset of the shared
+ * `FILTER_FIELD` source of truth, the same three the overview dashboard uses.
*/
-export type ComplianceProviderFilterParam = FilterParam<
- (typeof FILTER_FIELD)["PROVIDER_TYPE" | "PROVIDER_ID" | "PROVIDER_GROUPS"]
->;
+const COMPLIANCE_PROVIDER_FILTER_FIELD = {
+ PROVIDER_TYPE: FILTER_FIELD.PROVIDER_TYPE,
+ PROVIDER_ID: FILTER_FIELD.PROVIDER_ID,
+ PROVIDER_GROUPS: FILTER_FIELD.PROVIDER_GROUPS,
+} as const;
+
+type ComplianceProviderFilterField =
+ (typeof COMPLIANCE_PROVIDER_FILTER_FIELD)[keyof typeof COMPLIANCE_PROVIDER_FILTER_FIELD];
+
+/**
+ * Provider-scope filter param keys (e.g. `filter[provider_type__in]`). The
+ * backend (`ComplianceOverviewViewSet`) treats these as an alternative to
+ * `filter[scan_id]` (XOR) and aggregates compliance across the latest completed
+ * scan of each matching provider.
+ */
+export type ComplianceProviderFilterParam =
+ FilterParam;
/** Present, CSV-joined provider-scope filters (aggregated mode). */
export type ComplianceProviderFilters = Partial<
@@ -26,12 +39,20 @@ export type ComplianceFilters = Partial<
>;
export const COMPLIANCE_PROVIDER_FILTER_KEYS = [
- `filter[${FILTER_FIELD.PROVIDER_TYPE}]`,
- `filter[${FILTER_FIELD.PROVIDER_ID}]`,
- `filter[${FILTER_FIELD.PROVIDER_GROUPS}]`,
+ `filter[${COMPLIANCE_PROVIDER_FILTER_FIELD.PROVIDER_TYPE}]`,
+ `filter[${COMPLIANCE_PROVIDER_FILTER_FIELD.PROVIDER_ID}]`,
+ `filter[${COMPLIANCE_PROVIDER_FILTER_FIELD.PROVIDER_GROUPS}]`,
] as const satisfies ReadonlyArray;
-type SearchParamsLike = SearchParamsProps | URLSearchParams;
+/**
+ * Accepts either an SSR plain search-params object or the client
+ * `useSearchParams()` result (`ReadonlyURLSearchParams`), so callers don't need
+ * to wrap the latter in a fresh `URLSearchParams`.
+ */
+type SearchParamsLike =
+ | SearchParamsProps
+ | URLSearchParams
+ | ReadonlyURLSearchParams;
const readParam = (params: SearchParamsLike, key: string): string => {
if (params instanceof URLSearchParams) {