feat(compliance): add CIS Controls v8.1 universal framework (#11700)

Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
Pedro Martín
2026-06-26 13:27:02 +02:00
committed by GitHub
parent 9d013910e6
commit d6f5f060ca
10 changed files with 4720 additions and 2 deletions
+1
View File
@@ -10,6 +10,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- Scan configuration management page (`/scan-config`) to create, edit, and manage scan configs with live YAML validation against the server JSON Schema (only available in Prowler Cloud) [(#11695)](https://github.com/prowler-cloud/prowler/pull/11695)
- Surface an "invalid scan configuration" note on compliance requirements that fail solely because the applied scan config does not meet them [(#11695)](https://github.com/prowler-cloud/prowler/pull/11695)
- Filter the Overview, Findings, Resources, Scans, and Providers views by provider group [(#11659)](https://github.com/prowler-cloud/prowler/pull/11659)
- CIS Controls v8.1 compliance support, including its detail view and report mapping [(#11700)](https://github.com/prowler-cloud/prowler/pull/11700)
---
@@ -0,0 +1,56 @@
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
interface CISControlsDetailsProps {
requirement: Requirement;
}
export const CISControlsCustomDetails = ({
requirement,
}: CISControlsDetailsProps) => {
const implementationGroups = Array.isArray(requirement.implementation_groups)
? (requirement.implementation_groups as string[])
: [];
return (
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{requirement.function && (
<ComplianceBadge
label="Security Function"
value={requirement.function as string}
variant="info"
/>
)}
{requirement.asset_type && (
<ComplianceBadge
label="Asset Type"
value={requirement.asset_type as string}
variant="secondary"
/>
)}
{implementationGroups.map((group) => (
<ComplianceBadge
key={group}
label="Implementation Group"
value={group}
variant="tag"
/>
))}
</ComplianceBadgeContainer>
</ComplianceDetailContainer>
);
};
+143
View File
@@ -0,0 +1,143 @@
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/ui/accordion/Accordion";
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
import {
AttributesData,
CISControlsAttributesMetadata,
Framework,
Requirement,
REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import {
calculateFrameworkCounters,
createRequirementsMap,
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
} 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.
const sectionOrder = (section: string): number => {
const match = section.match(/^(\d+)/);
return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER;
};
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 CISControlsAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
// Group by Section (the top-level CIS Control). Function, AssetType and
// ImplementationGroups live inside the requirement so they show up on the
// detail drawer.
const categoryName = attrs.Section;
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: control → safeguards (no intermediate level).
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,
...getStatusCounters(finalStatus),
function: attrs.Function ?? undefined,
asset_type: attrs.AssetType ?? undefined,
implementation_groups: attrs.ImplementationGroups ?? undefined,
};
control.requirements.push(requirement);
}
for (const framework of frameworks) {
framework.categories.sort(
(a, b) => sectionOrder(a.name) - sectionOrder(b.name),
);
}
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: "",
// Control → safeguards (flat, no intermediate level).
items: category.controls.flatMap((control) =>
control.requirements.map((requirement, reqIndex) => ({
key: `${framework.name}-${category.name}-req-${reqIndex}`,
title: (
<ComplianceAccordionRequirementTitle
type=""
name={requirement.name}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
key={`content-${framework.name}-${category.name}-req-${reqIndex}`}
requirement={requirement}
scanId={safeId}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0
}
/>
),
items: [],
})),
),
})),
);
};
+19
View File
@@ -4,6 +4,7 @@ import { ASDEssentialEightCustomDetails } from "@/components/compliance/complian
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
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 { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details";
import { DORACustomDetails } from "@/components/compliance/compliance-custom-details/dora-details";
@@ -44,6 +45,10 @@ import {
mapComplianceData as mapCISComplianceData,
toAccordionItems as toCISAccordionItems,
} from "./cis";
import {
mapComplianceData as mapCISControlsComplianceData,
toAccordionItems as toCISControlsAccordionItems,
} from "./cis-controls";
import { calculateCategoryHeatmapData, getTopFailedSections } from "./commons";
import {
mapComplianceData as mapCSAComplianceData,
@@ -156,6 +161,20 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
getDetailsComponent: (requirement: Requirement) =>
createElement(CISCustomDetails, { requirement }),
},
// CIS Controls v8.1 — universal framework keyed by the `framework` field of
// `prowler/compliance/cis_controls_8.1.json` ("CIS-Controls"). Distinct from
// the per-provider CIS Benchmarks (keyed "CIS"). Groups by Section (the 18
// CIS Controls) and surfaces Security Function / Asset Type / Implementation
// Groups in the requirement detail drawer.
"CIS-Controls": {
mapComplianceData: mapCISControlsComplianceData,
toAccordionItems: toCISControlsAccordionItems,
getTopFailedSections,
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
createElement(CISControlsCustomDetails, { requirement }),
},
"AWS-Well-Architected-Framework-Security-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData,
toAccordionItems: toAWSWellArchitectedAccordionItems,
@@ -39,6 +39,7 @@ describe("isOcsfSupported", () => {
it("returns true for universal frameworks shipping an OCSF artifact", () => {
expect(isOcsfSupported("dora_2022_2554")).toBe(true);
expect(isOcsfSupported("csa_ccm_4.0")).toBe(true);
expect(isOcsfSupported("cis_controls_8.1")).toBe(true);
});
it("returns false for legacy/per-provider frameworks without OCSF output", () => {
@@ -180,6 +180,7 @@ export const pickLatestCisPerProvider = (
const OCSF_SUPPORTED_COMPLIANCE_IDS: ReadonlySet<string> = new Set([
"dora_2022_2554",
"csa_ccm_4.0",
"cis_controls_8.1",
]);
export const isOcsfSupported = (complianceId: string | undefined): boolean =>
+14
View File
@@ -397,6 +397,19 @@ export interface DORARequirement extends Requirement {
article_title: DORAAttributesMetadata["ArticleTitle"];
}
export interface CISControlsAttributesMetadata {
Section: string;
Function: string | null;
AssetType: string | null;
ImplementationGroups: string[] | null;
}
export interface CISControlsRequirement extends Requirement {
function?: string;
asset_type?: string;
implementation_groups?: string[];
}
export interface AttributesItemData {
type: "compliance-requirements-attributes";
id: string;
@@ -421,6 +434,7 @@ export interface AttributesItemData {
| ASDEssentialEightAttributesMetadata[]
| OktaIDaaSStigAttributesMetadata[]
| DORAAttributesMetadata[]
| CISControlsAttributesMetadata[]
| GenericAttributesMetadata[];
check_ids: string[];
// MITRE structure