mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(compliance): add CIS Controls v8.1 universal framework (#11700)
Co-authored-by: Pepe Fagoaga <pepe@prowler.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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: [],
|
||||
})),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user