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:
@@ -2,6 +2,14 @@
|
||||
|
||||
All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.30.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- DORA compliance framework support [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131)
|
||||
|
||||
---
|
||||
|
||||
## [1.29.0] (Prowler v5.29.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
@@ -394,6 +394,27 @@ export const getComplianceCsv = async (scanId: string, complianceId: string) =>
|
||||
"compliance report",
|
||||
);
|
||||
|
||||
/**
|
||||
* Get the OCSF JSON export for a universal compliance framework.
|
||||
*
|
||||
* Only universal frameworks that declare an ``outputs`` block (today: DORA,
|
||||
* CSA CCM 4.0) produce a per-framework OCSF artifact. For any other framework
|
||||
* the backend returns 404; callers should gate this download via
|
||||
* ``isOcsfSupported(framework)``.
|
||||
*
|
||||
* NOTE: this is a dedicated path (``compliance/{id}/ocsf``), not a query
|
||||
* param. The API's JSON:API ``QueryParameterValidationFilter`` rejects any
|
||||
* non-JSON:API query param with 400, so ``?type=`` / ``?format=`` is not an
|
||||
* option — the format must be encoded in the route.
|
||||
*/
|
||||
export const getComplianceOcsf = async (scanId: string, complianceId: string) =>
|
||||
_fetchScanBinary(
|
||||
scanId,
|
||||
`compliance/${complianceId}/ocsf`,
|
||||
`scan-${scanId}-compliance-${complianceId}.ocsf.json`,
|
||||
"compliance OCSF report",
|
||||
);
|
||||
|
||||
/**
|
||||
* Get a compliance PDF report for any supported framework.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
interface DORADetailsProps {
|
||||
requirement: Requirement;
|
||||
}
|
||||
|
||||
export const DORACustomDetails = ({ requirement }: DORADetailsProps) => {
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.pillar && (
|
||||
<ComplianceBadge
|
||||
label="Pillar"
|
||||
value={requirement.pillar as string}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
{requirement.article && (
|
||||
<ComplianceBadge
|
||||
label="Article"
|
||||
value={requirement.article as string}
|
||||
color="indigo"
|
||||
/>
|
||||
)}
|
||||
{requirement.article_title && (
|
||||
<ComplianceBadge
|
||||
label="Article Title"
|
||||
value={requirement.article_title as string}
|
||||
color="gray"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -6,15 +6,19 @@ import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { downloadComplianceCsvMock, downloadCompliancePdfMock } = vi.hoisted(
|
||||
() => ({
|
||||
downloadComplianceCsvMock: vi.fn(),
|
||||
downloadCompliancePdfMock: vi.fn(),
|
||||
}),
|
||||
);
|
||||
const {
|
||||
downloadComplianceCsvMock,
|
||||
downloadComplianceOcsfMock,
|
||||
downloadCompliancePdfMock,
|
||||
} = vi.hoisted(() => ({
|
||||
downloadComplianceCsvMock: vi.fn(),
|
||||
downloadComplianceOcsfMock: vi.fn(),
|
||||
downloadCompliancePdfMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/helper", () => ({
|
||||
downloadComplianceCsv: downloadComplianceCsvMock,
|
||||
downloadComplianceOcsf: downloadComplianceOcsfMock,
|
||||
downloadCompliancePdf: downloadCompliancePdfMock,
|
||||
}));
|
||||
|
||||
@@ -131,4 +135,51 @@ describe("ComplianceDownloadContainer", () => {
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("should hide the OCSF action for frameworks without OCSF support", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ComplianceDownloadContainer
|
||||
compact
|
||||
presentation="dropdown"
|
||||
scanId="scan-1"
|
||||
complianceId="compliance-1"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Open compliance export actions" }),
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Download OCSF report")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should surface and trigger the OCSF download for universal frameworks", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ComplianceDownloadContainer
|
||||
compact
|
||||
presentation="dropdown"
|
||||
scanId="scan-1"
|
||||
complianceId="dora"
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "Open compliance export actions" }),
|
||||
);
|
||||
expect(screen.getByText("Download OCSF report")).toBeInTheDocument();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("menuitem", { name: /Download OCSF report/i }),
|
||||
);
|
||||
|
||||
expect(downloadComplianceOcsfMock).toHaveBeenCalledWith(
|
||||
"scan-1",
|
||||
"dora",
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { DownloadIcon, FileTextIcon } from "lucide-react";
|
||||
import { DownloadIcon, FileJsonIcon, FileTextIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@/components/shadcn/button/button";
|
||||
@@ -14,8 +14,15 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/shadcn/tooltip";
|
||||
import { toast } from "@/components/ui";
|
||||
import type { ComplianceReportType } from "@/lib/compliance/compliance-report-types";
|
||||
import { downloadComplianceCsv, downloadCompliancePdf } from "@/lib/helper";
|
||||
import {
|
||||
type ComplianceReportType,
|
||||
isOcsfSupported,
|
||||
} from "@/lib/compliance/compliance-report-types";
|
||||
import {
|
||||
downloadComplianceCsv,
|
||||
downloadComplianceOcsf,
|
||||
downloadCompliancePdf,
|
||||
} from "@/lib/helper";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ComplianceDownloadContainerProps {
|
||||
@@ -40,9 +47,14 @@ export const ComplianceDownloadContainer = ({
|
||||
presentation = "buttons",
|
||||
}: ComplianceDownloadContainerProps) => {
|
||||
const [isDownloadingCsv, setIsDownloadingCsv] = useState(false);
|
||||
const [isDownloadingOcsf, setIsDownloadingOcsf] = useState(false);
|
||||
const [isDownloadingPdf, setIsDownloadingPdf] = useState(false);
|
||||
const isIconWidth = buttonWidth === "icon";
|
||||
const isDropdown = presentation === "dropdown";
|
||||
// Only universal frameworks declaring an ``outputs`` block expose a
|
||||
// per-framework OCSF artifact (today: DORA, CSA CCM 4.0). Hide the
|
||||
// action everywhere else so the user never hits a guaranteed 404.
|
||||
const ocsfAvailable = isOcsfSupported(complianceId);
|
||||
|
||||
const handleDownloadCsv = async () => {
|
||||
if (isDownloadingCsv) return;
|
||||
@@ -54,6 +66,16 @@ export const ComplianceDownloadContainer = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadOcsf = async () => {
|
||||
if (!ocsfAvailable || isDownloadingOcsf) return;
|
||||
setIsDownloadingOcsf(true);
|
||||
try {
|
||||
await downloadComplianceOcsf(scanId, complianceId, toast);
|
||||
} finally {
|
||||
setIsDownloadingOcsf(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadPdf = async () => {
|
||||
if (!reportType || isDownloadingPdf) return;
|
||||
setIsDownloadingPdf(true);
|
||||
@@ -105,6 +127,18 @@ export const ComplianceDownloadContainer = ({
|
||||
onSelect={handleDownloadCsv}
|
||||
disabled={disabled || isDownloadingCsv}
|
||||
/>
|
||||
{ocsfAvailable && (
|
||||
<ActionDropdownItem
|
||||
icon={
|
||||
<FileJsonIcon
|
||||
className={isDownloadingOcsf ? "animate-download-icon" : ""}
|
||||
/>
|
||||
}
|
||||
label="Download OCSF report"
|
||||
onSelect={handleDownloadOcsf}
|
||||
disabled={disabled || isDownloadingOcsf}
|
||||
/>
|
||||
)}
|
||||
{reportType && (
|
||||
<ActionDropdownItem
|
||||
icon={
|
||||
@@ -152,6 +186,29 @@ export const ComplianceDownloadContainer = ({
|
||||
<TooltipContent>Download CSV report</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
{ocsfAvailable && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
onClick={handleDownloadOcsf}
|
||||
disabled={disabled || isDownloadingOcsf}
|
||||
aria-label="Download compliance OCSF report"
|
||||
>
|
||||
<FileJsonIcon
|
||||
size={14}
|
||||
className={isDownloadingOcsf ? "animate-download-icon" : ""}
|
||||
/>
|
||||
<span className={labelClassName}>OCSF</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{showTooltip && (
|
||||
<TooltipContent>Download OCSF report</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
{reportType && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -6,6 +6,7 @@ import CCCLogo from "./ccc.svg";
|
||||
import CISLogo from "./cis.svg";
|
||||
import CISALogo from "./cisa.svg";
|
||||
import CSALogo from "./csa.svg";
|
||||
import DORALogo from "./dora.svg";
|
||||
import ENSLogo from "./ens.png";
|
||||
import FedRAMPLogo from "./fedramp.svg";
|
||||
import FFIECLogo from "./ffiec.svg";
|
||||
@@ -67,6 +68,9 @@ const COMPLIANCE_LOGOS = [
|
||||
["c5", C5Logo],
|
||||
["ccc", CCCLogo],
|
||||
["csa", CSALogo],
|
||||
// DORA — universal framework (`prowler/compliance/dora.json`). The
|
||||
// compliance_id is just `dora`, no provider suffix.
|
||||
["dora", DORALogo],
|
||||
["secnumcloud", ANSSILogo],
|
||||
["aws", AWSLogo],
|
||||
] as const;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 170" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="doraGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#003399"/>
|
||||
<stop offset="100%" style="stop-color:#0055A5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g>
|
||||
<rect x="0" y="20" width="400" height="130" rx="16" fill="url(#doraGradient)"/>
|
||||
<text x="200" y="100" font-family="Helvetica, Arial, sans-serif" font-size="76" font-weight="700" fill="#FFFFFF" text-anchor="middle" letter-spacing="6">DORA</text>
|
||||
<text x="200" y="135" font-family="Helvetica, Arial, sans-serif" font-size="14" font-weight="500" fill="#FFD700" text-anchor="middle" letter-spacing="3">EU 2022/2554</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 747 B |
@@ -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.
|
||||
*
|
||||
|
||||
@@ -327,6 +327,31 @@ export interface ASDEssentialEightRequirement extends Requirement {
|
||||
references: ASDEssentialEightAttributesMetadata["References"];
|
||||
}
|
||||
|
||||
// DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554).
|
||||
// Universal framework — flat attributes dict with Pillar/Article/ArticleTitle.
|
||||
// `Pillar` is the canonical grouping key for tables and PDF; the enum mirrors
|
||||
// the five DORA pillars declared in `prowler/compliance/dora.json`.
|
||||
export const DORA_PILLAR = {
|
||||
ICT_RISK_MANAGEMENT: "ICT Risk Management",
|
||||
INCIDENT_REPORTING: "ICT-Related Incident Reporting",
|
||||
RESILIENCE_TESTING: "Digital Operational Resilience Testing",
|
||||
THIRD_PARTY_RISK: "ICT Third-Party Risk Management",
|
||||
INFORMATION_SHARING: "Information Sharing",
|
||||
} as const;
|
||||
export type DORAPillar = (typeof DORA_PILLAR)[keyof typeof DORA_PILLAR];
|
||||
|
||||
export interface DORAAttributesMetadata {
|
||||
Pillar: DORAPillar;
|
||||
Article: string;
|
||||
ArticleTitle: string;
|
||||
}
|
||||
|
||||
export interface DORARequirement extends Requirement {
|
||||
pillar: DORAAttributesMetadata["Pillar"];
|
||||
article: DORAAttributesMetadata["Article"];
|
||||
article_title: DORAAttributesMetadata["ArticleTitle"];
|
||||
}
|
||||
|
||||
export interface AttributesItemData {
|
||||
type: "compliance-requirements-attributes";
|
||||
id: string;
|
||||
@@ -349,6 +374,7 @@ export interface AttributesItemData {
|
||||
| CCCAttributesMetadata[]
|
||||
| CSAAttributesMetadata[]
|
||||
| ASDEssentialEightAttributesMetadata[]
|
||||
| DORAAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
// MITRE structure
|
||||
|
||||
Reference in New Issue
Block a user