mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(compliance): add DORA framework for AWS (#11131)
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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: [],
|
||||
})),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user