feat(compliance): add DORA framework for AWS (#11131)

This commit is contained in:
Pedro Martín
2026-06-03 11:43:55 +02:00
committed by GitHub
parent d573af911d
commit f7f8747512
50 changed files with 2357 additions and 38277 deletions
+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 { 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";
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
@@ -47,6 +48,10 @@ import {
mapComplianceData as mapCSAComplianceData,
toAccordionItems as toCSAAccordionItems,
} from "./csa";
import {
mapComplianceData as mapDORAComplianceData,
toAccordionItems as toDORAAccordionItems,
} from "./dora";
import {
mapComplianceData as mapENSComplianceData,
toAccordionItems as toENSAccordionItems,
@@ -208,6 +213,19 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
getDetailsComponent: (requirement: Requirement) =>
createElement(CSACustomDetails, { requirement }),
},
// DORA (Regulation (EU) 2022/2554) — universal framework keyed by the
// `framework` field of `prowler/compliance/dora.json` ("DORA"). Groups by
// Pillar (5 enum values) and surfaces Pillar / Article / ArticleTitle in
// the requirement detail drawer.
DORA: {
mapComplianceData: mapDORAComplianceData,
toAccordionItems: toDORAAccordionItems,
getTopFailedSections,
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
createElement(DORACustomDetails, { requirement }),
},
});
/**
@@ -4,6 +4,7 @@ import {
COMPLIANCE_REPORT_TYPES,
getReportTypeForCompliance,
getReportTypeForFramework,
isOcsfSupported,
pickLatestCisPerProvider,
} from "./compliance-report-types";
@@ -34,6 +35,24 @@ describe("getReportTypeForFramework", () => {
});
});
describe("isOcsfSupported", () => {
it("returns true for universal frameworks shipping an OCSF artifact", () => {
expect(isOcsfSupported("dora")).toBe(true);
expect(isOcsfSupported("csa_ccm_4.0")).toBe(true);
});
it("returns false for legacy/per-provider frameworks without OCSF output", () => {
expect(isOcsfSupported("cis_5.0_aws")).toBe(false);
expect(isOcsfSupported("ens_rd2022_aws")).toBe(false);
expect(isOcsfSupported("nis2_aws")).toBe(false);
});
it("returns false for missing or empty inputs", () => {
expect(isOcsfSupported(undefined)).toBe(false);
expect(isOcsfSupported("")).toBe(false);
});
});
describe("pickLatestCisPerProvider", () => {
it("returns an empty set for an empty input", () => {
const latest = pickLatestCisPerProvider([]);
@@ -95,7 +114,7 @@ describe("pickLatestCisPerProvider", () => {
const latest = pickLatestCisPerProvider([
"ens_rd2022_aws",
"nis2_aws",
"csa_ccm_4.0_aws",
"csa_ccm_4.0",
"prowler_threatscore_aws",
"cis_5.0_aws",
]);
@@ -161,6 +161,30 @@ export const pickLatestCisPerProvider = (
return latest;
};
/**
* Compliance IDs that ship a per-framework OCSF JSON export.
*
* 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).
*
* Keep this Set in lock-step with the backend: ``get_prowler_provider_compliance``
* + ``ComplianceFramework.outputs`` is the source of truth. The API will
* 404 on ``GET /scans/{id}/compliance/{name}/ocsf`` for any framework not
* in this set, so showing the OCSF button for an unsupported framework
* would surface a broken download — gate every call site through
* ``isOcsfSupported``.
*/
const OCSF_SUPPORTED_COMPLIANCE_IDS: ReadonlySet<string> = new Set([
"dora",
"csa_ccm_4.0",
]);
export const isOcsfSupported = (complianceId: string | undefined): boolean =>
!!complianceId && OCSF_SUPPORTED_COMPLIANCE_IDS.has(complianceId);
/**
* Resolve the report type for a compliance card.
*
+154
View File
@@ -0,0 +1,154 @@
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,
DORAAttributesMetadata,
Framework,
Requirement,
REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import {
calculateFrameworkCounters,
createRequirementsMap,
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
} from "./commons";
// Display order for DORA pillars in the accordion and any grouped chart. The
// regulation arranges them in this exact order (Articles 5-14, 17-19, 24-25,
// 28+30, 45) — preserving it here means the UI always renders pillars in the
// "logical" reading order regardless of how the API returns them.
export const DORA_PILLAR_ORDER: readonly string[] = [
"ICT Risk Management",
"ICT-Related Incident Reporting",
"Digital Operational Resilience Testing",
"ICT Third-Party Risk Management",
"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,
): 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 DORAAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
// Group by Pillar (top-level accordion section). Article + ArticleTitle
// live inside the requirement so they show up on the detail drawer.
const categoryName = attrs.Pillar;
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: pillar → 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,
...getStatusCounters(finalStatus),
pillar: attrs.Pillar,
article: attrs.Article,
article_title: attrs.ArticleTitle,
};
control.requirements.push(requirement);
}
// Sort categories by canonical pillar order so DORA always reads from "ICT
// Risk Management" down to "Information Sharing", regardless of map insertion
// order driven by the API response.
for (const framework of frameworks) {
framework.categories.sort((a, b) => {
const ia = DORA_PILLAR_ORDER.indexOf(a.name);
const ib = DORA_PILLAR_ORDER.indexOf(b.name);
// Unknown pillars (defensive — shouldn't happen) sink to the bottom.
const orderA = ia === -1 ? DORA_PILLAR_ORDER.length : ia;
const orderB = ib === -1 ? DORA_PILLAR_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: "",
// Pillar → requirements (flat, no intermediate "control" 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: [],
})),
),
})),
);
};
+27
View File
@@ -1,5 +1,6 @@
import {
getComplianceCsv,
getComplianceOcsf,
getCompliancePdfReport,
type ScanBinaryResult,
} from "@/actions/scans";
@@ -247,6 +248,32 @@ export const downloadComplianceCsv = async (
);
};
/**
* Download the per-framework OCSF JSON export.
*
* Only universal frameworks declaring an ``outputs`` block produce this
* artifact (currently DORA and CSA CCM 4.0); callers must gate the call
* via ``isOcsfSupported`` to avoid surfacing a broken download on
* frameworks the API will 404 on.
*/
export const downloadComplianceOcsf = async (
scanId: string,
complianceId: string,
toast: ReturnType<typeof useToast>["toast"],
): Promise<void> => {
toast({
title: "Download Started",
description: "Preparing the OCSF report. This may take a moment.",
});
const result = await getComplianceOcsf(scanId, complianceId);
await downloadFile(
result,
"application/json",
"The compliance OCSF report has been downloaded successfully.",
toast,
);
};
/**
* Download a compliance PDF report.
*