feat(ui): add CMMC compliance framework (#12414)

This commit is contained in:
Pedro Martín
2026-08-12 14:52:09 +02:00
committed by GitHub
parent 02df22ca19
commit 94c20eb9fe
12 changed files with 534 additions and 3 deletions
@@ -60,6 +60,21 @@ export const CROSS_PROVIDER_FRAMEWORKS: CrossProviderFrameworkEntry[] = [
"Digital Operational Resilience Act (EU 2022/2554) — the EU framework for the digital operational resilience of the financial sector.",
compatibleProviders: ["aws", "azure", "gcp", "alibabacloud", "cloudflare"],
},
{
complianceId: "cmmc_2.0",
title: "CMMC",
version: "2.0",
description:
"Cybersecurity Maturity Model Certification (CMMC) 2.0 (32 CFR Part 170) — the U.S. Department of Defense program verifying that defense contractors protect FCI and CUI across three levels.",
compatibleProviders: [
"aws",
"azure",
"gcp",
"m365",
"alibabacloud",
"oraclecloud",
],
},
];
/** Resolves only canonical catalog links. Missing, unknown, or mismatched
@@ -0,0 +1 @@
CMMC 2.0 universal compliance framework rendering: dedicated icon, Domain/Level requirement mapper and cross-provider catalog tile
@@ -0,0 +1,49 @@
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
interface CMMCDetailsProps {
requirement: Requirement;
}
export const CMMCCustomDetails = ({ requirement }: CMMCDetailsProps) => {
return (
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{requirement.domain && (
<ComplianceBadge
label="Domain"
value={requirement.domain as string}
variant="tag"
/>
)}
{requirement.level && (
<ComplianceBadge
label="Level"
value={requirement.level as string}
variant="tag"
/>
)}
{requirement.source_requirement && (
<ComplianceBadge
label="Source Requirement"
value={requirement.source_requirement as string}
variant="tag"
/>
)}
</ComplianceBadgeContainer>
</ComplianceDetailContainer>
);
};
@@ -5,6 +5,7 @@ import C5Logo from "./c5.svg";
import CCCLogo from "./ccc.svg";
import CISLogo from "./cis.svg";
import CISALogo from "./cisa.svg";
import CMMCLogo from "./cmmc.svg";
import CSALogo from "./csa.svg";
import DORALogo from "./dora.svg";
import ENSLogo from "./ens.png";
@@ -69,6 +70,10 @@ const COMPLIANCE_LOGOS = [
["c5", C5Logo],
["ccc", CCCLogo],
["csa", CSALogo],
// CMMC 2.0 — universal framework (`prowler/compliance/cmmc_2.0.json`). The
// compliance_id is `cmmc_2.0` and the `framework`/title is `CMMC`; the `cmmc`
// keyword matches both via `includes`, with no provider suffix.
["cmmc", CMMCLogo],
// DORA — universal framework (`prowler/compliance/dora_2022_2554.json`).
// The compliance_id is `dora_2022_2554`; the `dora` keyword still matches
// it via `includes`, with no provider suffix.
+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 170" fill="none">
<defs>
<linearGradient id="cmmcGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#0B2A4A"/>
<stop offset="100%" style="stop-color:#1E4E79"/>
</linearGradient>
</defs>
<g>
<rect x="0" y="20" width="400" height="130" rx="16" fill="url(#cmmcGradient)"/>
<text x="200" y="100" font-family="Helvetica, Arial, sans-serif" font-size="66" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="4">CMMC</text>
<text x="200" y="135" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="500" fill="#7FB2E5" text-anchor="middle" letter-spacing="3">2.0</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 738 B

+240
View File
@@ -0,0 +1,240 @@
import { isValidElement } from "react";
import { describe, expect, it, vi } from "vitest";
// `cmmc.tsx` re-exports `toAccordionItems` which builds JSX referencing
// client-side accordion components. Those components transitively import
// server-only code (next-auth → next/server) and would crash vitest at load
// time. Mocking the JSX deps lets us load the module and exercise the real
// `mapComplianceData` and `toAccordionItems` functions.
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 {
AttributesData,
AttributesItemData,
CMMCAttributesMetadata,
CMMCLevel,
REQUIREMENT_STATUS,
RequirementItemData,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import { mapComplianceData, toAccordionItems } from "./cmmc";
const FRAMEWORK = "CMMC";
const baseMetadata = (
overrides: Partial<CMMCAttributesMetadata> = {},
): CMMCAttributesMetadata => ({
Domain: "Access Control",
Level: "Level 1" as CMMCLevel,
SourceRequirement: "48 CFR 52.204-21(b)(1)(i)",
...overrides,
});
const buildAttribute = (
id: string,
metadata: CMMCAttributesMetadata,
{ name = "" }: { name?: string } = {},
): AttributesItemData => ({
type: "compliance-requirements-attributes",
id,
attributes: {
framework_description: "CMMC 2.0",
name,
framework: FRAMEWORK,
version: "2.0",
description: "Requirement clause text.",
attributes: {
metadata: [metadata],
check_ids: ["check_one"],
},
},
});
const buildRequirement = (
id: string,
status: RequirementStatus = REQUIREMENT_STATUS.PASS,
): RequirementItemData => ({
type: "compliance-requirements-details",
id,
attributes: {
framework: FRAMEWORK,
version: "2.0",
description: "Canonical CMMC requirement text.",
status,
},
});
const buildInputs = (
pairs: Array<{
attribute: AttributesItemData;
requirement: RequirementItemData;
}>,
): { attributesData: AttributesData; requirementsData: RequirementsData } => ({
attributesData: { data: pairs.map((p) => p.attribute) },
requirementsData: { data: pairs.map((p) => p.requirement) },
});
describe("mapComplianceData (CMMC 2.0)", () => {
it("returns an empty list when there are no attributes", () => {
const { attributesData, requirementsData } = buildInputs([]);
expect(mapComplianceData(attributesData, requirementsData)).toEqual([]);
});
it("groups requirements by Domain", () => {
const attrA = buildAttribute(
"AC.L1-b.1.i",
baseMetadata({ Domain: "Access Control" }),
);
const attrB = buildAttribute(
"AC.L2-3.1.3",
baseMetadata({ Domain: "Access Control" }),
);
const { attributesData, requirementsData } = buildInputs([
{ attribute: attrA, requirement: buildRequirement("AC.L1-b.1.i") },
{ attribute: attrB, requirement: buildRequirement("AC.L2-3.1.3") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.name).toBe(FRAMEWORK);
expect(framework.categories).toHaveLength(1);
expect(framework.categories[0].name).toBe("Access Control");
expect(framework.categories[0].controls[0].requirements).toHaveLength(2);
});
it("orders domains by the canonical NIST 800-171 family order", () => {
const attrSI = buildAttribute(
"SI.L1-b.1.xiv",
baseMetadata({ Domain: "System and Information Integrity" }),
);
const attrAC = buildAttribute(
"AC.L1-b.1.i",
baseMetadata({ Domain: "Access Control" }),
);
const { attributesData, requirementsData } = buildInputs([
{ attribute: attrSI, requirement: buildRequirement("SI.L1-b.1.xiv") },
{ attribute: attrAC, requirement: buildRequirement("AC.L1-b.1.i") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories.map((c) => c.name)).toEqual([
"Access Control",
"System and Information Integrity",
]);
});
it("propagates Domain, Level and SourceRequirement onto the requirement", () => {
const attribute = buildAttribute(
"AC.L1-b.1.i",
baseMetadata({
Domain: "Access Control",
Level: "Level 1" as CMMCLevel,
SourceRequirement: "48 CFR 52.204-21(b)(1)(i)",
}),
{ name: "Limit information system access" },
);
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement: buildRequirement("AC.L1-b.1.i") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut = framework.categories[0].controls[0].requirements[0];
expect(requirementOut.name).toBe(
"AC.L1-b.1.i - Limit information system access",
);
expect(requirementOut.domain).toBe("Access Control");
expect(requirementOut.level).toBe("Level 1");
expect(requirementOut.source_requirement).toBe("48 CFR 52.204-21(b)(1)(i)");
});
it("derives counters from RequirementStatus", () => {
const STATUS_COUNTER = {
PASS: "pass",
FAIL: "fail",
MANUAL: "manual",
} as const;
type StatusCounter = (typeof STATUS_COUNTER)[keyof typeof STATUS_COUNTER];
const cases: Array<{
status: RequirementStatus;
expected: StatusCounter;
}> = [
{ status: REQUIREMENT_STATUS.PASS, expected: STATUS_COUNTER.PASS },
{ status: REQUIREMENT_STATUS.FAIL, expected: STATUS_COUNTER.FAIL },
{ status: REQUIREMENT_STATUS.MANUAL, expected: STATUS_COUNTER.MANUAL },
];
for (const { status, expected } of cases) {
const attribute = buildAttribute(`AC-${status}`, baseMetadata());
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement: buildRequirement(`AC-${status}`, status) },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework[expected]).toBe(1);
}
});
});
describe("toAccordionItems (CMMC 2.0)", () => {
it("produces one accordion item per domain with its requirement leaves", () => {
const attrAC = buildAttribute(
"AC.L1-b.1.i",
baseMetadata({ Domain: "Access Control" }),
);
const attrIA = buildAttribute(
"IA.L1-b.1.v",
baseMetadata({ Domain: "Identification and Authentication" }),
);
const frameworks = mapComplianceData(
{ data: [attrAC, attrIA] },
{
data: [
buildRequirement("AC.L1-b.1.i"),
buildRequirement("IA.L1-b.1.v"),
],
},
);
const items = toAccordionItems(frameworks, "scan-1");
expect(items).toHaveLength(2);
expect(items[0].key).toBe(`${FRAMEWORK}-Access Control`);
expect(isValidElement(items[0].title)).toBe(true);
expect(items[0].items).toHaveLength(1);
// Requirement keys are stable (derived from the requirement id), not
// positional indexes — reordering must not remap expanded state.
expect(items[0].items?.[0]?.key).toBe(
`${FRAMEWORK}-Access Control-AC.L1-b.1.i`,
);
});
it("returns an empty list when given no frameworks", () => {
expect(toAccordionItems([], "scan-1")).toEqual([]);
});
});
+164
View File
@@ -0,0 +1,164 @@
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 {
AttributesData,
CMMCAttributesMetadata,
Framework,
Requirement,
REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import {
calculateFrameworkCounters,
createRequirementsMap,
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
} from "./commons";
// 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[] = [
"Access Control",
"Awareness and Training",
"Audit and Accountability",
"Configuration Management",
"Identification and Authentication",
"Incident Response",
"Maintenance",
"Media Protection",
"Personnel Security",
"Physical Protection",
"Risk Assessment",
"Security Assessment",
"System and Communications Protection",
"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,
): 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 CMMCAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
// Group by Domain. Level and SourceRequirement live inside the requirement
// so they show up on the detail drawer.
const categoryName = attrs.Domain;
const requirementName = attributeItem.attributes.name || "";
const description = attributeItem.attributes.description;
const status = requirementData.attributes.status || "";
const checks = attributeItem.attributes.attributes.check_ids || [];
const framework = findOrCreateFramework(frameworks, frameworkName);
const category = findOrCreateCategory(framework.categories, categoryName);
// Flat 2-level structure: domain → requirements (no intermediate control).
const control = findOrCreateControl(category.controls, categoryName);
const finalStatus: RequirementStatus = status as RequirementStatus;
const requirement: Requirement = {
name: requirementName ? `${id} - ${requirementName}` : id,
description,
status: finalStatus,
check_ids: checks,
invalid_config: requirementData.attributes.invalid_config || false,
...getStatusCounters(finalStatus),
domain: attrs.Domain,
level: attrs.Level,
source_requirement: attrs.SourceRequirement,
};
control.requirements.push(requirement);
}
// Sort domains by the canonical NIST 800-171 family order.
for (const framework of frameworks) {
framework.categories.sort((a, b) => {
const ia = CMMC_DOMAIN_ORDER.indexOf(a.name);
const ib = CMMC_DOMAIN_ORDER.indexOf(b.name);
const orderA = ia === -1 ? CMMC_DOMAIN_ORDER.length : ia;
const orderB = ib === -1 ? CMMC_DOMAIN_ORDER.length : ib;
return orderA - orderB;
});
}
calculateFrameworkCounters(frameworks);
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: [],
})),
),
})),
);
};
@@ -38,6 +38,10 @@ vi.mock(
"@/components/compliance/compliance-custom-details/cis-details",
() => ({ CISCustomDetails: stubFactory("CISStub") }),
);
vi.mock(
"@/components/compliance/compliance-custom-details/cmmc-details",
() => ({ CMMCCustomDetails: stubFactory("CMMCStub") }),
);
vi.mock(
"@/components/compliance/compliance-custom-details/csa-details",
() => ({ CSACustomDetails: stubFactory("CSAStub") }),
@@ -148,6 +152,7 @@ describe("getComplianceMapper", () => {
{ framework: "ProwlerThreatScore", expected: "ThreatStub" },
{ framework: "CCC", expected: "CCCStub" },
{ framework: "CSA-CCM", expected: "CSAStub" },
{ framework: "CMMC", expected: "CMMCStub" },
{ framework: "Okta-IDaaS-STIG", expected: "OktaIDaaSStigStub" },
];
@@ -193,6 +198,7 @@ describe("getComplianceMapper", () => {
"ProwlerThreatScore",
"CCC",
"CSA-CCM",
"CMMC",
"Okta-IDaaS-STIG",
]) {
const mapper = getComplianceMapper(framework);
+18
View File
@@ -6,6 +6,7 @@ import { C5CustomDetails } from "@/components/compliance/compliance-custom-detai
import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details";
import { CISControlsCustomDetails } from "@/components/compliance/compliance-custom-details/cis-controls-details";
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
import { CMMCCustomDetails } from "@/components/compliance/compliance-custom-details/cmmc-details";
import { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details";
import { DORACustomDetails } from "@/components/compliance/compliance-custom-details/dora-details";
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
@@ -49,6 +50,10 @@ import {
mapComplianceData as mapCISControlsComplianceData,
toAccordionItems as toCISControlsAccordionItems,
} from "./cis-controls";
import {
mapComplianceData as mapCMMCComplianceData,
toAccordionItems as toCMMCAccordionItems,
} from "./cmmc";
import { calculateCategoryHeatmapData, getTopFailedSections } from "./commons";
import {
mapComplianceData as mapCSAComplianceData,
@@ -259,6 +264,19 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
getDetailsComponent: (requirement: Requirement) =>
createElement(DORACustomDetails, { requirement }),
},
// CMMC 2.0 — universal framework keyed by the `framework` field of
// `prowler/compliance/cmmc_2.0.json` ("CMMC"). Groups by Domain (14 NIST
// 800-171 families) and surfaces Domain / Level / Source Requirement in the
// requirement detail drawer.
CMMC: {
mapComplianceData: mapCMMCComplianceData,
toAccordionItems: toCMMCAccordionItems,
getTopFailedSections,
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
createElement(CMMCCustomDetails, { requirement }),
},
});
/**
@@ -40,6 +40,7 @@ describe("isOcsfSupported", () => {
expect(isOcsfSupported("dora_2022_2554")).toBe(true);
expect(isOcsfSupported("csa_ccm_4.0")).toBe(true);
expect(isOcsfSupported("cis_controls_8.1")).toBe(true);
expect(isOcsfSupported("cmmc_2.0")).toBe(true);
});
it("returns false for legacy/per-provider frameworks without OCSF output", () => {
+4 -3
View File
@@ -166,9 +166,9 @@ 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 and
* CSA CCM 4.0. Any other framework only offers CSV (and, for the curated
* list above, PDF).
* 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
* offers CSV (and, for the curated list above, PDF).
*
* Keep this Set in lock-step with the backend: ``get_prowler_provider_compliance``
* + ``ComplianceFramework.outputs`` is the source of truth. The API will
@@ -181,6 +181,7 @@ const OCSF_SUPPORTED_COMPLIANCE_IDS: ReadonlySet<string> = new Set([
"dora_2022_2554",
"csa_ccm_4.0",
"cis_controls_8.1",
"cmmc_2.0",
]);
export const isOcsfSupported = (complianceId: string | undefined): boolean =>
+18
View File
@@ -427,6 +427,23 @@ export interface CISControlsRequirement extends Requirement {
implementation_groups?: string[];
}
// CMMC 2.0 (Cybersecurity Maturity Model Certification, 32 CFR Part 170).
// Universal framework — flat attributes dict with Domain/Level/SourceRequirement.
// `Domain` is the grouping key; `Level` (1/2/3) and `SourceRequirement` are
// surfaced in the requirement detail drawer.
export const CMMC_LEVEL = {
LEVEL_1: "Level 1",
LEVEL_2: "Level 2",
LEVEL_3: "Level 3",
} as const;
export type CMMCLevel = (typeof CMMC_LEVEL)[keyof typeof CMMC_LEVEL];
export interface CMMCAttributesMetadata {
Domain: string;
Level: CMMCLevel;
SourceRequirement: string;
}
export interface AttributesItemData {
type: "compliance-requirements-attributes";
id: string;
@@ -452,6 +469,7 @@ export interface AttributesItemData {
| OktaIDaaSStigAttributesMetadata[]
| DORAAttributesMetadata[]
| CISControlsAttributesMetadata[]
| CMMCAttributesMetadata[]
| GenericAttributesMetadata[];
check_ids: string[];
// MITRE structure