mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
fix(ui): add FedRAMP 20x cross-provider mappers (#12810)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
co-authored by
alejandrobailo
parent
61ef44a03b
commit
c0fdd5bdf3
@@ -0,0 +1 @@
|
||||
Per-provider breakdown and OCSF download for FedRAMP 20x KSI and Class C FRR in the cross-provider compliance view
|
||||
@@ -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 ? (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
) : null;
|
||||
|
||||
export const FedRAMP20xKSICustomDetails = ({
|
||||
requirement,
|
||||
}: FedRAMP20xDetailsProps) => {
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
<DescriptionSection requirement={requirement} />
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.theme && (
|
||||
<ComplianceBadge
|
||||
label="Theme"
|
||||
value={requirement.theme as string}
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
{requirement.class_applicability && (
|
||||
<ComplianceBadge
|
||||
label="Class Applicability"
|
||||
value={requirement.class_applicability as string}
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
{requirement.nist_controls && (
|
||||
<ComplianceDetailSection title="NIST SP 800-53 Controls">
|
||||
<ComplianceDetailText>
|
||||
{requirement.nist_controls as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const FedRAMP20xFRRCustomDetails = ({
|
||||
requirement,
|
||||
}: FedRAMP20xDetailsProps) => {
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
<DescriptionSection requirement={requirement} />
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.ruleset && (
|
||||
<ComplianceBadge
|
||||
label="Ruleset"
|
||||
value={requirement.ruleset as string}
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
{requirement.subset && (
|
||||
<ComplianceBadge
|
||||
label="Subset"
|
||||
value={requirement.subset as string}
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
{requirement.force && (
|
||||
<ComplianceBadge
|
||||
label="Force"
|
||||
value={requirement.force as string}
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
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: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
invalidConfig={requirement.invalid_config}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${framework.name}-${category.name}-${requirement.name}`}
|
||||
requirement={requirement}
|
||||
scanId={safeId}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
})),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string, ComplianceMapper> => ({
|
||||
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 }),
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -167,7 +167,8 @@ export const pickLatestCisPerProvider = (
|
||||
* Only universal compliance frameworks that declare an ``outputs`` block in
|
||||
* their schema (see ``prowler/compliance/<name>.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<string> = 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 =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Pick<OverviewRequirement, "id" | "name" | "attributes">>,
|
||||
): 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",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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<string, string | undefined>;
|
||||
|
||||
const mapByGroup = <TMetadata>(
|
||||
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<FedRAMP20xKSIAttributesMetadata>(
|
||||
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<FedRAMP20xFRRAttributesMetadata>(
|
||||
attributesData,
|
||||
requirementsData,
|
||||
(attrs) => attrs.Ruleset,
|
||||
(attrs) => ({
|
||||
ruleset: attrs.Ruleset,
|
||||
subset: attrs.Subset,
|
||||
force: attrs.Force,
|
||||
}),
|
||||
);
|
||||
@@ -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: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.flatMap((control) =>
|
||||
control.requirements.map((requirement) => ({
|
||||
key: `${framework.name}-${category.name}-${requirement.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
invalidConfig={requirement.invalid_config}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${framework.name}-${category.name}-${requirement.name}`}
|
||||
requirement={requirement}
|
||||
scanId={safeId}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
})),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user