diff --git a/ui/changelog.d/fedramp-20x-cross-provider-breakdown.fixed.md b/ui/changelog.d/fedramp-20x-cross-provider-breakdown.fixed.md
new file mode 100644
index 0000000000..dc7c71ce1b
--- /dev/null
+++ b/ui/changelog.d/fedramp-20x-cross-provider-breakdown.fixed.md
@@ -0,0 +1 @@
+Per-provider breakdown and OCSF download for FedRAMP 20x KSI and Class C FRR in the cross-provider compliance view
diff --git a/ui/components/compliance/compliance-custom-details/fedramp-20x-details.tsx b/ui/components/compliance/compliance-custom-details/fedramp-20x-details.tsx
new file mode 100644
index 0000000000..31f684a33b
--- /dev/null
+++ b/ui/components/compliance/compliance-custom-details/fedramp-20x-details.tsx
@@ -0,0 +1,89 @@
+import { Requirement } from "@/types/compliance";
+
+import {
+ ComplianceBadge,
+ ComplianceBadgeContainer,
+ ComplianceDetailContainer,
+ ComplianceDetailSection,
+ ComplianceDetailText,
+} from "./shared-components";
+
+interface FedRAMP20xDetailsProps {
+ requirement: Requirement;
+}
+
+const DescriptionSection = ({ requirement }: FedRAMP20xDetailsProps) =>
+ requirement.description ? (
+
+ {requirement.description}
+
+ ) : null;
+
+export const FedRAMP20xKSICustomDetails = ({
+ requirement,
+}: FedRAMP20xDetailsProps) => {
+ return (
+
+
+
+
+ {requirement.theme && (
+
+ )}
+ {requirement.class_applicability && (
+
+ )}
+
+
+ {requirement.nist_controls && (
+
+
+ {requirement.nist_controls as string}
+
+
+ )}
+
+ );
+};
+
+export const FedRAMP20xFRRCustomDetails = ({
+ requirement,
+}: FedRAMP20xDetailsProps) => {
+ return (
+
+
+
+
+ {requirement.ruleset && (
+
+ )}
+ {requirement.subset && (
+
+ )}
+ {requirement.force && (
+
+ )}
+
+
+ );
+};
diff --git a/ui/lib/compliance/c5.tsx b/ui/lib/compliance/c5.tsx
index 4839df3d66..c39a2dd538 100644
--- a/ui/lib/compliance/c5.tsx
+++ b/ui/lib/compliance/c5.tsx
@@ -3,13 +3,12 @@ import { ComplianceAccordionRequirementTitle } from "@/components/compliance/com
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
import { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
-import {
+import type {
AttributesData,
C5AttributesMetadata,
Control,
Framework,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -20,14 +19,9 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
diff --git a/ui/lib/compliance/cis-controls.tsx b/ui/lib/compliance/cis-controls.tsx
index 7a7f283fc6..a1fab412e1 100644
--- a/ui/lib/compliance/cis-controls.tsx
+++ b/ui/lib/compliance/cis-controls.tsx
@@ -3,12 +3,11 @@ import { ComplianceAccordionRequirementTitle } from "@/components/compliance/com
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
import { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
-import {
+import type {
AttributesData,
CISControlsAttributesMetadata,
Framework,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -19,14 +18,9 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
// Sort the 18 CIS Controls by their leading number ("1. ...", "2. ...", ...,
// "18. ...") so the accordion always reads in canonical control order
// regardless of how the API returns the sections.
diff --git a/ui/lib/compliance/cmmc.tsx b/ui/lib/compliance/cmmc.ts
similarity index 56%
rename from ui/lib/compliance/cmmc.tsx
rename to ui/lib/compliance/cmmc.ts
index 557c28b95c..748d0ed39c 100644
--- a/ui/lib/compliance/cmmc.tsx
+++ b/ui/lib/compliance/cmmc.ts
@@ -1,14 +1,8 @@
-import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
-import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
-import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
-import { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
-import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
-import {
+import type {
AttributesData,
CMMCAttributesMetadata,
Framework,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -19,8 +13,11 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
+export { toGroupedAccordionItems as toAccordionItems } from "./grouped-accordion";
+
// Canonical NIST SP 800-171 family order for the 14 CMMC domains, so the
// accordion always reads in the same order regardless of the API response.
export const CMMC_DOMAIN_ORDER: readonly string[] = [
@@ -40,12 +37,6 @@ export const CMMC_DOMAIN_ORDER: readonly string[] = [
"System and Information Integrity",
];
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
@@ -109,56 +100,3 @@ export const mapComplianceData = (
return frameworks;
};
-
-export const toAccordionItems = (
- data: Framework[],
- scanId: string | undefined,
-): AccordionItemProps[] => {
- const safeId = scanId || "";
-
- return data.flatMap((framework) =>
- framework.categories.map((category) => ({
- key: `${framework.name}-${category.name}`,
- title: (
-
- ),
- content: "",
- // Domain → requirements (flat, no intermediate "control" level).
- // Keys are derived from the requirement name (which starts with the
- // unique CMMC id, e.g. "AC.L1-b.1.i") instead of the array index, so
- // expanded state stays attached to the right requirement even if the
- // list is reordered or filtered.
- items: category.controls.flatMap((control) =>
- control.requirements.map((requirement) => ({
- key: `${framework.name}-${category.name}-${requirement.name}`,
- title: (
-
- ),
- content: (
-
- ),
- items: [],
- })),
- ),
- })),
- );
-};
diff --git a/ui/lib/compliance/commons.tsx b/ui/lib/compliance/commons.tsx
index 24a7b8407c..15b315be4d 100644
--- a/ui/lib/compliance/commons.tsx
+++ b/ui/lib/compliance/commons.tsx
@@ -7,6 +7,7 @@ import {
REQUIREMENT_STATUS,
RequirementItemData,
RequirementsData,
+ RequirementsTotals,
RequirementStatus,
TOP_FAILED_DATA_TYPE,
TopFailedDataType,
@@ -66,7 +67,7 @@ const incrementFailedCount = (
};
export const updateCounters = (
- target: { pass: number; fail: number; manual: number },
+ target: RequirementsTotals,
status: RequirementStatus,
) => {
if (status === REQUIREMENT_STATUS.MANUAL) {
@@ -78,6 +79,14 @@ export const updateCounters = (
}
};
+export const getStatusCounters = (
+ status: RequirementStatus,
+): RequirementsTotals => {
+ const counters: RequirementsTotals = { pass: 0, fail: 0, manual: 0 };
+ updateCounters(counters, status);
+ return counters;
+};
+
export const getTopFailedSections = (
mappedData: Framework[],
): TopFailedResult => {
diff --git a/ui/lib/compliance/compliance-mapper.test.ts b/ui/lib/compliance/compliance-mapper.test.ts
index c7ce69040e..be7ba8074c 100644
--- a/ui/lib/compliance/compliance-mapper.test.ts
+++ b/ui/lib/compliance/compliance-mapper.test.ts
@@ -54,6 +54,13 @@ vi.mock(
"@/components/compliance/compliance-custom-details/ens-details",
() => ({ ENSCustomDetails: stubFactory("ENSStub") }),
);
+vi.mock(
+ "@/components/compliance/compliance-custom-details/fedramp-20x-details",
+ () => ({
+ FedRAMP20xFRRCustomDetails: stubFactory("FedRAMP20xFRRStub"),
+ FedRAMP20xKSICustomDetails: stubFactory("FedRAMP20xKSIStub"),
+ }),
+);
vi.mock(
"@/components/compliance/compliance-custom-details/generic-details",
() => ({ GenericCustomDetails: stubFactory("GenericStub") }),
@@ -159,6 +166,8 @@ describe("getComplianceMapper", () => {
{ framework: "CMMC", expected: "CMMCStub" },
{ framework: "Okta-IDaaS-STIG", expected: "OktaIDaaSStigStub" },
{ framework: "Cyber-Essentials", expected: "CyberEssentialsStub" },
+ { framework: "FedRAMP-20x-KSI", expected: "FedRAMP20xKSIStub" },
+ { framework: "FedRAMP-20x-FRR-Class-C", expected: "FedRAMP20xFRRStub" },
];
for (const { framework, expected } of wiring) {
@@ -206,6 +215,8 @@ describe("getComplianceMapper", () => {
"CMMC",
"Okta-IDaaS-STIG",
"Cyber-Essentials",
+ "FedRAMP-20x-KSI",
+ "FedRAMP-20x-FRR-Class-C",
]) {
const mapper = getComplianceMapper(framework);
expect(Object.keys(mapper).sort(), framework).toEqual(expectedKeys);
diff --git a/ui/lib/compliance/compliance-mapper.ts b/ui/lib/compliance/compliance-mapper.ts
index 80b3558aeb..d80a980485 100644
--- a/ui/lib/compliance/compliance-mapper.ts
+++ b/ui/lib/compliance/compliance-mapper.ts
@@ -11,6 +11,10 @@ import { CSACustomDetails } from "@/components/compliance/compliance-custom-deta
import { CyberEssentialsCustomDetails } from "@/components/compliance/compliance-custom-details/cyber-essentials-details";
import { DORACustomDetails } from "@/components/compliance/compliance-custom-details/dora-details";
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
+import {
+ FedRAMP20xFRRCustomDetails,
+ FedRAMP20xKSICustomDetails,
+} from "@/components/compliance/compliance-custom-details/fedramp-20x-details";
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
import { KISACustomDetails } from "@/components/compliance/compliance-custom-details/kisa-details";
@@ -72,6 +76,11 @@ import {
mapComplianceData as mapENSComplianceData,
toAccordionItems as toENSAccordionItems,
} from "./ens";
+import {
+ mapFRRComplianceData as mapFedRAMP20xFRRComplianceData,
+ mapKSIComplianceData as mapFedRAMP20xKSIComplianceData,
+ toAccordionItems as toFedRAMP20xAccordionItems,
+} from "./fedramp-20x";
import {
mapComplianceData as mapGenericComplianceData,
toAccordionItems as toGenericAccordionItems,
@@ -296,6 +305,27 @@ const getComplianceMappers = (): Record => ({
getDetailsComponent: (requirement: Requirement) =>
createElement(CMMCCustomDetails, { requirement }),
},
+ // Universal frameworks must compose requirement names as `${id} - ${name}`
+ // (see `composeRequirementName`); the generic mapper does not, so they need
+ // a dedicated entry for the cross-provider breakdown to render.
+ "FedRAMP-20x-KSI": {
+ mapComplianceData: mapFedRAMP20xKSIComplianceData,
+ toAccordionItems: toFedRAMP20xAccordionItems,
+ getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
+ getDetailsComponent: (requirement: Requirement) =>
+ createElement(FedRAMP20xKSICustomDetails, { requirement }),
+ },
+ "FedRAMP-20x-FRR-Class-C": {
+ mapComplianceData: mapFedRAMP20xFRRComplianceData,
+ toAccordionItems: toFedRAMP20xAccordionItems,
+ getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
+ getDetailsComponent: (requirement: Requirement) =>
+ createElement(FedRAMP20xFRRCustomDetails, { requirement }),
+ },
});
/**
diff --git a/ui/lib/compliance/compliance-report-types.test.ts b/ui/lib/compliance/compliance-report-types.test.ts
index 8a431d9748..59b26beed3 100644
--- a/ui/lib/compliance/compliance-report-types.test.ts
+++ b/ui/lib/compliance/compliance-report-types.test.ts
@@ -41,6 +41,8 @@ describe("isOcsfSupported", () => {
expect(isOcsfSupported("csa_ccm_4.0")).toBe(true);
expect(isOcsfSupported("cis_controls_8.1")).toBe(true);
expect(isOcsfSupported("cmmc_2.0")).toBe(true);
+ expect(isOcsfSupported("fedramp_20x_ksi_2026")).toBe(true);
+ expect(isOcsfSupported("fedramp_20x_frr_class_c_2026")).toBe(true);
});
it("returns false for legacy/per-provider frameworks without OCSF output", () => {
diff --git a/ui/lib/compliance/compliance-report-types.ts b/ui/lib/compliance/compliance-report-types.ts
index 68ef3072de..2553df013d 100644
--- a/ui/lib/compliance/compliance-report-types.ts
+++ b/ui/lib/compliance/compliance-report-types.ts
@@ -167,7 +167,8 @@ export const pickLatestCisPerProvider = (
* Only universal compliance frameworks that declare an ``outputs`` block in
* their schema (see ``prowler/compliance/.json``) produce a dedicated
* OCSF artifact during scan output generation. Today that is DORA,
- * CSA CCM 4.0, CIS Controls 8.1 and CMMC 2.0. Any other framework only
+ * CSA CCM 4.0, CIS Controls 8.1, CMMC 2.0 and FedRAMP 20x (KSI and Class C
+ * FRR). Any other framework only
* offers CSV (and, for the curated list above, PDF).
*
* Keep this Set in lock-step with the backend: ``get_prowler_provider_compliance``
@@ -182,6 +183,8 @@ const OCSF_SUPPORTED_COMPLIANCE_IDS: ReadonlySet = new Set([
"csa_ccm_4.0",
"cis_controls_8.1",
"cmmc_2.0",
+ "fedramp_20x_ksi_2026",
+ "fedramp_20x_frr_class_c_2026",
]);
export const isOcsfSupported = (complianceId: string | undefined): boolean =>
diff --git a/ui/lib/compliance/csa.tsx b/ui/lib/compliance/csa.tsx
index 3bb601de0c..c38136b19f 100644
--- a/ui/lib/compliance/csa.tsx
+++ b/ui/lib/compliance/csa.tsx
@@ -4,12 +4,11 @@ import { ComplianceAccordionTitle } from "@/components/compliance/compliance-acc
import { ComplianceBadgeVariant } from "@/components/compliance/compliance-custom-details/shared-components";
import { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
-import {
+import type {
AttributesData,
CSAAttributesMetadata,
Framework,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -20,6 +19,7 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
export interface CSAMappingSection {
@@ -36,12 +36,6 @@ export const CSA_MAPPING_SECTIONS: CSAMappingSection[] = [
},
];
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
diff --git a/ui/lib/compliance/cyber-essentials.tsx b/ui/lib/compliance/cyber-essentials.tsx
index aee4d8c5db..ebdd73209d 100644
--- a/ui/lib/compliance/cyber-essentials.tsx
+++ b/ui/lib/compliance/cyber-essentials.tsx
@@ -3,12 +3,11 @@ import { ComplianceAccordionRequirementTitle } from "@/components/compliance/com
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
import { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
-import {
+import type {
AttributesData,
CyberEssentialsAttributesMetadata,
Framework,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -19,6 +18,7 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
// Display order for the five Cyber Essentials control themes in the accordion
@@ -33,12 +33,6 @@ export const CYBER_ESSENTIALS_THEME_ORDER: readonly string[] = [
"Malware Protection",
];
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
diff --git a/ui/lib/compliance/dora.tsx b/ui/lib/compliance/dora.tsx
index 8ac364f29f..7fc6bba26c 100644
--- a/ui/lib/compliance/dora.tsx
+++ b/ui/lib/compliance/dora.tsx
@@ -3,12 +3,11 @@ import { ComplianceAccordionRequirementTitle } from "@/components/compliance/com
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
import { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
-import {
+import type {
AttributesData,
DORAAttributesMetadata,
Framework,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -19,6 +18,7 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
// Display order for DORA pillars in the accordion and any grouped chart. The
@@ -33,12 +33,6 @@ export const DORA_PILLAR_ORDER: readonly string[] = [
"Information Sharing",
];
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
diff --git a/ui/lib/compliance/fedramp-20x.test.ts b/ui/lib/compliance/fedramp-20x.test.ts
new file mode 100644
index 0000000000..21d40c69bc
--- /dev/null
+++ b/ui/lib/compliance/fedramp-20x.test.ts
@@ -0,0 +1,183 @@
+import { describe, expect, it, vi } from "vitest";
+
+vi.mock(
+ "@/components/compliance/compliance-accordion/client-accordion-content",
+ () => ({ ClientAccordionContent: () => null }),
+);
+vi.mock(
+ "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title",
+ () => ({ ComplianceAccordionRequirementTitle: () => null }),
+);
+vi.mock(
+ "@/components/compliance/compliance-accordion/compliance-accordion-title",
+ () => ({ ComplianceAccordionTitle: () => null }),
+);
+
+import {
+ buildRequirementExtrasMap,
+ crossProviderToMapperInput,
+} from "@/app/(prowler)/compliance/_lib/cross-provider-adapter";
+import type { CrossProviderOverviewAttributes } from "@/app/(prowler)/compliance/_types";
+import { Framework, Requirement } from "@/types/compliance";
+
+import {
+ mapFRRComplianceData,
+ mapKSIComplianceData,
+ toAccordionItems,
+} from "./fedramp-20x";
+
+type OverviewRequirement =
+ CrossProviderOverviewAttributes["requirements"][number];
+
+const buildOverview = (
+ framework: string,
+ requirements: Array>,
+): CrossProviderOverviewAttributes =>
+ ({
+ framework,
+ version: "2026",
+ description: "",
+ providers: ["aws"],
+ compatible_providers: ["aws"],
+ scan_ids_by_provider: {},
+ requirements: requirements.map((requirement) => ({
+ ...requirement,
+ description: "Requirement text.",
+ status: "PASS",
+ providers: { aws: "PASS" },
+ check_ids_by_provider: { aws: ["check_one"] },
+ })),
+ }) as unknown as CrossProviderOverviewAttributes;
+
+const mapOverview = (
+ overview: CrossProviderOverviewAttributes,
+ mapper: typeof mapKSIComplianceData,
+): Framework[] => {
+ const { attributesData, requirementsData } =
+ crossProviderToMapperInput(overview);
+ return mapper(attributesData, requirementsData);
+};
+
+const allRequirements = (frameworks: Framework[]): Requirement[] =>
+ frameworks.flatMap((framework) =>
+ framework.categories.flatMap((category) =>
+ category.controls.flatMap((control) => control.requirements),
+ ),
+ );
+
+const KSI_OVERVIEW = buildOverview("FedRAMP-20x-KSI", [
+ {
+ id: "KSI-SVC-VRI",
+ name: "Validating Resource Integrity",
+ attributes: {
+ Theme: "KSI-SVC: Service Configuration",
+ NISTControls: "SC-13",
+ ClassApplicability: "Required for Classes B and C",
+ },
+ },
+ {
+ id: "KSI-CED-RAT",
+ name: "Reviewing All Training",
+ attributes: {
+ Theme: "KSI-CED: Cybersecurity Education",
+ NISTControls: null,
+ ClassApplicability: "Required for Classes B and C",
+ },
+ },
+]);
+
+const FRR_OVERVIEW = buildOverview("FedRAMP-20x-FRR-Class-C", [
+ {
+ id: "CMU-CSO-UVM",
+ name: "Using Validated Cryptographic Modules",
+ attributes: {
+ Ruleset: "CMU: Cryptographic Module Use",
+ Subset: "CSO: Cloud Service Provider Responsibilities",
+ Force: "MUST",
+ },
+ },
+ {
+ id: "AFC-CSO-INB",
+ name: "Maintain a FedRAMP Security Inbox",
+ attributes: {
+ Ruleset: "AFC: Addressing FedRAMP Communication",
+ Subset: "CSO: General Provider Responsibilities",
+ Force: "MUST",
+ },
+ },
+]);
+
+describe.each([
+ { label: "KSI", overview: KSI_OVERVIEW, mapper: mapKSIComplianceData },
+ {
+ label: "FRR Class C",
+ overview: FRR_OVERVIEW,
+ mapper: mapFRRComplianceData,
+ },
+])("FedRAMP 20x $label cross-provider join", ({ overview, mapper }) => {
+ it("names every requirement with a key of the per-provider breakdown", () => {
+ const extras = buildRequirementExtrasMap(overview);
+ const names = allRequirements(mapOverview(overview, mapper)).map(
+ (requirement) => requirement.name,
+ );
+
+ expect(names).toHaveLength(overview.requirements.length);
+ for (const name of names) {
+ expect(extras.has(name), name).toBe(true);
+ }
+ });
+});
+
+describe("mapKSIComplianceData", () => {
+ it("groups by Theme in catalog order and exposes KSI attributes", () => {
+ const [framework] = mapOverview(KSI_OVERVIEW, mapKSIComplianceData);
+
+ expect(framework.categories.map((category) => category.name)).toEqual([
+ "KSI-CED: Cybersecurity Education",
+ "KSI-SVC: Service Configuration",
+ ]);
+
+ const [svc] = framework.categories[1].controls[0].requirements;
+ expect(svc.name).toBe("KSI-SVC-VRI - Validating Resource Integrity");
+ expect(svc.theme).toBe("KSI-SVC: Service Configuration");
+ expect(svc.nist_controls).toBe("SC-13");
+ expect(svc.class_applicability).toBe("Required for Classes B and C");
+
+ const [ced] = framework.categories[0].controls[0].requirements;
+ expect(ced.nist_controls).toBeUndefined();
+ });
+});
+
+describe("mapFRRComplianceData", () => {
+ it("groups by Ruleset in catalog order and exposes FRR attributes", () => {
+ const [framework] = mapOverview(FRR_OVERVIEW, mapFRRComplianceData);
+
+ expect(framework.categories.map((category) => category.name)).toEqual([
+ "AFC: Addressing FedRAMP Communication",
+ "CMU: Cryptographic Module Use",
+ ]);
+
+ const [cmu] = framework.categories[1].controls[0].requirements;
+ expect(cmu.ruleset).toBe("CMU: Cryptographic Module Use");
+ expect(cmu.subset).toBe("CSO: Cloud Service Provider Responsibilities");
+ expect(cmu.force).toBe("MUST");
+ expect(framework.pass).toBe(2);
+ });
+});
+
+describe("toAccordionItems (FedRAMP 20x)", () => {
+ it("keys requirement leaves by name instead of position", () => {
+ const items = toAccordionItems(
+ mapOverview(FRR_OVERVIEW, mapFRRComplianceData),
+ "scan-1",
+ );
+
+ expect(items.map((item) => item.key)).toEqual([
+ "FedRAMP-20x-FRR-Class-C-AFC: Addressing FedRAMP Communication",
+ "FedRAMP-20x-FRR-Class-C-CMU: Cryptographic Module Use",
+ ]);
+ expect(items[1].items?.[0]?.key).toBe(
+ "FedRAMP-20x-FRR-Class-C-CMU: Cryptographic Module Use-CMU-CSO-UVM - Using Validated Cryptographic Modules",
+ );
+ });
+});
diff --git a/ui/lib/compliance/fedramp-20x.ts b/ui/lib/compliance/fedramp-20x.ts
new file mode 100644
index 0000000000..fd7d9fb1a3
--- /dev/null
+++ b/ui/lib/compliance/fedramp-20x.ts
@@ -0,0 +1,110 @@
+import type {
+ AttributesData,
+ FedRAMP20xFRRAttributesMetadata,
+ FedRAMP20xKSIAttributesMetadata,
+ Framework,
+ Requirement,
+ RequirementsData,
+ RequirementStatus,
+} from "@/types/compliance";
+
+import {
+ calculateFrameworkCounters,
+ createRequirementsMap,
+ findOrCreateCategory,
+ findOrCreateControl,
+ findOrCreateFramework,
+ getStatusCounters,
+} from "./commons";
+
+export { toGroupedAccordionItems as toAccordionItems } from "./grouped-accordion";
+
+type RequirementFields = Record;
+
+const mapByGroup = (
+ attributesData: AttributesData,
+ requirementsData: RequirementsData,
+ getGroup: (attrs: TMetadata) => string,
+ getFields: (attrs: TMetadata) => RequirementFields,
+): Framework[] => {
+ const attributes = attributesData?.data || [];
+ const requirementsMap = createRequirementsMap(requirementsData);
+ const frameworks: Framework[] = [];
+
+ for (const attributeItem of attributes) {
+ const id = attributeItem.id;
+ const metadataArray = attributeItem.attributes?.attributes
+ ?.metadata as unknown as TMetadata[];
+ const attrs = metadataArray?.[0];
+ if (!attrs) continue;
+
+ const requirementData = requirementsMap.get(id);
+ if (!requirementData) continue;
+
+ const categoryName = getGroup(attrs);
+ const requirementName = attributeItem.attributes.name || "";
+ const status = (requirementData.attributes.status ||
+ "") as RequirementStatus;
+
+ const framework = findOrCreateFramework(
+ frameworks,
+ attributeItem.attributes.framework,
+ );
+ const category = findOrCreateCategory(framework.categories, categoryName);
+ const control = findOrCreateControl(category.controls, categoryName);
+
+ // The name must match `composeRequirementName` in the cross-provider
+ // adapter, or the per-provider breakdown cannot be joined.
+ const requirement: Requirement = {
+ ...getFields(attrs),
+ name: requirementName ? `${id} - ${requirementName}` : id,
+ description: attributeItem.attributes.description,
+ status,
+ check_ids: attributeItem.attributes.attributes.check_ids || [],
+ invalid_config: requirementData.attributes.invalid_config || false,
+ ...getStatusCounters(status),
+ };
+
+ control.requirements.push(requirement);
+ }
+
+ // Theme and Ruleset names start with their catalog code, so alphabetical
+ // order is the catalog order.
+ for (const framework of frameworks) {
+ framework.categories.sort((a, b) => a.name.localeCompare(b.name));
+ }
+
+ calculateFrameworkCounters(frameworks);
+
+ return frameworks;
+};
+
+export const mapKSIComplianceData = (
+ attributesData: AttributesData,
+ requirementsData: RequirementsData,
+): Framework[] =>
+ mapByGroup(
+ attributesData,
+ requirementsData,
+ (attrs) => attrs.Theme,
+ (attrs) => ({
+ theme: attrs.Theme,
+ nist_controls: attrs.NISTControls || undefined,
+ class_applicability: attrs.ClassApplicability,
+ }),
+ );
+
+export const mapFRRComplianceData = (
+ attributesData: AttributesData,
+ requirementsData: RequirementsData,
+): Framework[] =>
+ mapByGroup(
+ attributesData,
+ requirementsData,
+ (attrs) => attrs.Ruleset,
+ (attrs) => ({
+ ruleset: attrs.Ruleset,
+ subset: attrs.Subset,
+ force: attrs.Force,
+ }),
+ );
diff --git a/ui/lib/compliance/grouped-accordion.tsx b/ui/lib/compliance/grouped-accordion.tsx
new file mode 100644
index 0000000000..cea6651079
--- /dev/null
+++ b/ui/lib/compliance/grouped-accordion.tsx
@@ -0,0 +1,55 @@
+import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
+import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
+import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
+import type { AccordionItemProps } from "@/components/shadcn/accordion/Accordion";
+import type { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
+import type { Framework } from "@/types/compliance";
+
+/** Category → requirement accordion, with stable keys across reordering. */
+export const toGroupedAccordionItems = (
+ data: Framework[],
+ scanId: string | undefined,
+): AccordionItemProps[] => {
+ const safeId = scanId || "";
+
+ return data.flatMap((framework) =>
+ framework.categories.map((category) => ({
+ key: `${framework.name}-${category.name}`,
+ title: (
+
+ ),
+ content: "",
+ items: category.controls.flatMap((control) =>
+ control.requirements.map((requirement) => ({
+ key: `${framework.name}-${category.name}-${requirement.name}`,
+ title: (
+
+ ),
+ content: (
+
+ ),
+ items: [],
+ })),
+ ),
+ })),
+ );
+};
diff --git a/ui/lib/compliance/okta-idaas-stig.tsx b/ui/lib/compliance/okta-idaas-stig.tsx
index 8603c33711..3d8b797b8a 100644
--- a/ui/lib/compliance/okta-idaas-stig.tsx
+++ b/ui/lib/compliance/okta-idaas-stig.tsx
@@ -10,7 +10,6 @@ import {
isOktaIDaaSStigAttributesMetadata,
OktaIDaaSStigRequirement,
Requirement,
- REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
@@ -21,14 +20,9 @@ import {
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
+ getStatusCounters,
} from "./commons";
-const getStatusCounters = (status: RequirementStatus) => ({
- pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
- fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
- manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
-});
-
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts
index be7462f549..e7f27a8e09 100644
--- a/ui/types/compliance.ts
+++ b/ui/types/compliance.ts
@@ -474,6 +474,20 @@ export interface CMMCAttributesMetadata {
SourceRequirement: string;
}
+// FedRAMP 20x KSI (`prowler/compliance/fedramp_20x_ksi_2026.json`), grouped by Theme.
+export interface FedRAMP20xKSIAttributesMetadata {
+ Theme: string;
+ NISTControls?: string | null;
+ ClassApplicability: string;
+}
+
+// FedRAMP 20x Class C FRR (`prowler/compliance/fedramp_20x_frr_class_c_2026.json`), grouped by Ruleset.
+export interface FedRAMP20xFRRAttributesMetadata {
+ Ruleset: string;
+ Subset: string;
+ Force: string;
+}
+
export interface AttributesItemData {
type: "compliance-requirements-attributes";
id: string;
@@ -501,6 +515,8 @@ export interface AttributesItemData {
| CISControlsAttributesMetadata[]
| CyberEssentialsAttributesMetadata[]
| CMMCAttributesMetadata[]
+ | FedRAMP20xKSIAttributesMetadata[]
+ | FedRAMP20xFRRAttributesMetadata[]
| GenericAttributesMetadata[];
check_ids: string[];
// MITRE structure