feat(compliance): add Cyber Essentials 3.3 for Azure (#11588)

Co-authored-by: pedrooot <pedromarting3@gmail.com>
This commit is contained in:
Muhammad Ibrahim
2026-08-27 13:31:18 +02:00
committed by GitHub
co-authored by pedrooot
parent c89d900aae
commit 301edea7ce
13 changed files with 1495 additions and 0 deletions
@@ -46,6 +46,10 @@ vi.mock(
"@/components/compliance/compliance-custom-details/csa-details",
() => ({ CSACustomDetails: stubFactory("CSAStub") }),
);
vi.mock(
"@/components/compliance/compliance-custom-details/cyber-essentials-details",
() => ({ CyberEssentialsCustomDetails: stubFactory("CyberEssentialsStub") }),
);
vi.mock(
"@/components/compliance/compliance-custom-details/ens-details",
() => ({ ENSCustomDetails: stubFactory("ENSStub") }),
@@ -154,6 +158,7 @@ describe("getComplianceMapper", () => {
{ framework: "CSA-CCM", expected: "CSAStub" },
{ framework: "CMMC", expected: "CMMCStub" },
{ framework: "Okta-IDaaS-STIG", expected: "OktaIDaaSStigStub" },
{ framework: "Cyber-Essentials", expected: "CyberEssentialsStub" },
];
for (const { framework, expected } of wiring) {
@@ -200,6 +205,7 @@ describe("getComplianceMapper", () => {
"CSA-CCM",
"CMMC",
"Okta-IDaaS-STIG",
"Cyber-Essentials",
]) {
const mapper = getComplianceMapper(framework);
expect(Object.keys(mapper).sort(), framework).toEqual(expectedKeys);
+19
View File
@@ -8,6 +8,7 @@ import { CISControlsCustomDetails } from "@/components/compliance/compliance-cus
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
import { CMMCCustomDetails } from "@/components/compliance/compliance-custom-details/cmmc-details";
import { CSACustomDetails } from "@/components/compliance/compliance-custom-details/csa-details";
import { CyberEssentialsCustomDetails } from "@/components/compliance/compliance-custom-details/cyber-essentials-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";
@@ -59,6 +60,10 @@ import {
mapComplianceData as mapCSAComplianceData,
toAccordionItems as toCSAAccordionItems,
} from "./csa";
import {
mapComplianceData as mapCyberEssentialsComplianceData,
toAccordionItems as toCyberEssentialsAccordionItems,
} from "./cyber-essentials";
import {
mapComplianceData as mapDORAComplianceData,
toAccordionItems as toDORAAccordionItems,
@@ -264,6 +269,20 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
getDetailsComponent: (requirement: Requirement) =>
createElement(DORACustomDetails, { requirement }),
},
// Cyber Essentials v3.3 — universal framework keyed by the `framework` field
// of `prowler/compliance/cyber_essentials_3.3.json` ("Cyber-Essentials").
// Groups by Theme (the 5 NCSC control themes) and surfaces Theme /
// AssessmentStatus / CloudApplicability / RemediationProcedure / References in
// the requirement detail drawer.
"Cyber-Essentials": {
mapComplianceData: mapCyberEssentialsComplianceData,
toAccordionItems: toCyberEssentialsAccordionItems,
getTopFailedSections,
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
createElement(CyberEssentialsCustomDetails, { requirement }),
},
// CMMC 2.0 — universal framework keyed by the `framework` field of
// `prowler/compliance/cmmc_2.0.json` ("CMMC"). Groups by Domain (14 NIST
// 800-171 families) and surfaces Domain / Level / Source Requirement in the
+521
View File
@@ -0,0 +1,521 @@
import { describe, expect, it, vi } from "vitest";
// `cyber-essentials.tsx` re-exports `toAccordionItems`, which builds JSX
// referencing the client-side accordion components. Those components
// transitively import server-only code (next-auth → next/server) and would
// crash vitest at load time. Mocking the JSX deps lets us load the module and
// exercise the real `mapComplianceData` and `toAccordionItems` functions,
// which are what we actually want to test.
vi.mock(
"@/components/compliance/compliance-accordion/client-accordion-content",
() => ({
ClientAccordionContent: () => null,
}),
);
vi.mock(
"@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title",
() => ({
ComplianceAccordionRequirementTitle: () => null,
}),
);
vi.mock(
"@/components/compliance/compliance-accordion/compliance-accordion-title",
() => ({
ComplianceAccordionTitle: () => null,
}),
);
import {
AttributesData,
AttributesItemData,
CyberEssentialsAttributesMetadata,
CyberEssentialsTheme,
REQUIREMENT_STATUS,
RequirementItemData,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import {
CYBER_ESSENTIALS_THEME_ORDER,
mapComplianceData,
toAccordionItems,
} from "./cyber-essentials";
const FRAMEWORK = "Cyber-Essentials";
const baseMetadata = (
overrides: Partial<CyberEssentialsAttributesMetadata> = {},
): CyberEssentialsAttributesMetadata => ({
Theme: "Firewalls",
AssessmentStatus: "Automated",
CloudApplicability: "full",
RemediationProcedure: "Steps to remediate.",
References: "https://example.com/a",
...overrides,
});
const buildAttribute = (
id: string,
metadata: CyberEssentialsAttributesMetadata,
{
name,
description = "Canonical Cyber Essentials clause text.",
checks = ["check_one"],
}: { name?: string; description?: string; checks?: string[] } = {},
): AttributesItemData => ({
type: "compliance-requirements-attributes",
id,
attributes: {
framework_description: "NCSC Cyber Essentials",
framework: FRAMEWORK,
...(name !== undefined ? { name } : {}),
version: "3.3",
description,
attributes: {
metadata: [metadata],
check_ids: checks,
},
},
});
const buildRequirement = (
id: string,
status: RequirementStatus = REQUIREMENT_STATUS.PASS,
): RequirementItemData => ({
type: "compliance-requirements-details",
id,
attributes: {
framework: FRAMEWORK,
version: "3.3",
description: "Canonical clause text.",
status,
},
});
const buildInputs = (
pairs: Array<{
attribute: AttributesItemData;
requirement: RequirementItemData;
}>,
): { attributesData: AttributesData; requirementsData: RequirementsData } => ({
attributesData: { data: pairs.map((p) => p.attribute) },
requirementsData: { data: pairs.map((p) => p.requirement) },
});
// One requirement per theme, intentionally supplied OUT of canonical order so
// the sort under test has something to reorder.
const oneRequirementPerThemeUnordered = (): Array<{
attribute: AttributesItemData;
requirement: RequirementItemData;
}> => {
const themesOutOfOrder: CyberEssentialsTheme[] = [
"Malware Protection",
"User Access Control",
"Firewalls",
"Security Update Management",
"Secure Configuration",
];
return themesOutOfOrder.map((theme, index) => {
const id = `CE-${index}`;
return {
attribute: buildAttribute(id, baseMetadata({ Theme: theme })),
requirement: buildRequirement(id),
};
});
};
describe("mapComplianceData (Cyber Essentials)", () => {
it("returns an empty list when there are no attributes", () => {
const { attributesData, requirementsData } = buildInputs([]);
expect(mapComplianceData(attributesData, requirementsData)).toEqual([]);
});
describe("five-theme grouping and order", () => {
it("orders the five themes into the canonical reading order regardless of API order", () => {
const { attributesData, requirementsData } = buildInputs(
oneRequirementPerThemeUnordered(),
);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories.map((c) => c.name)).toEqual([
...CYBER_ESSENTIALS_THEME_ORDER,
]);
});
it("groups every requirement sharing a Theme under a single category", () => {
const pairs = [
{
attribute: buildAttribute(
"CE-FW-1",
baseMetadata({ Theme: "Firewalls" }),
),
requirement: buildRequirement("CE-FW-1"),
},
{
attribute: buildAttribute(
"CE-FW-2",
baseMetadata({ Theme: "Firewalls" }),
),
requirement: buildRequirement("CE-FW-2"),
},
{
attribute: buildAttribute(
"CE-MP-1",
baseMetadata({ Theme: "Malware Protection" }),
),
requirement: buildRequirement("CE-MP-1"),
},
];
const { attributesData, requirementsData } = buildInputs(pairs);
const [framework] = mapComplianceData(attributesData, requirementsData);
const firewalls = framework.categories.find(
(c) => c.name === "Firewalls",
);
const malware = framework.categories.find(
(c) => c.name === "Malware Protection",
);
expect(framework.categories).toHaveLength(2);
// Flat 2-level structure: theme → single control → requirements.
expect(firewalls?.controls).toHaveLength(1);
expect(firewalls?.controls[0].requirements).toHaveLength(2);
expect(malware?.controls[0].requirements).toHaveLength(1);
});
it("sinks an unknown theme below the five canonical themes", () => {
const pairs = [
{
attribute: buildAttribute("CE-X-1", {
...baseMetadata(),
Theme: "Some Future Theme" as CyberEssentialsTheme,
}),
requirement: buildRequirement("CE-X-1"),
},
{
attribute: buildAttribute(
"CE-MP-1",
baseMetadata({ Theme: "Malware Protection" }),
),
requirement: buildRequirement("CE-MP-1"),
},
];
const { attributesData, requirementsData } = buildInputs(pairs);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories.map((c) => c.name)).toEqual([
"Malware Protection",
"Some Future Theme",
]);
});
});
describe("status counters", () => {
it("derives per-requirement counters from RequirementStatus", () => {
const cases: Array<{
status: RequirementStatus;
expected: "pass" | "fail" | "manual";
}> = [
{ status: REQUIREMENT_STATUS.PASS, expected: "pass" },
{ status: REQUIREMENT_STATUS.FAIL, expected: "fail" },
{ status: REQUIREMENT_STATUS.MANUAL, expected: "manual" },
];
for (const { status, expected } of cases) {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute(`CE-${status}`, baseMetadata()),
requirement: buildRequirement(`CE-${status}`, status),
},
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut =
framework.categories[0].controls[0].requirements[0];
expect(requirementOut.pass).toBe(expected === "pass" ? 1 : 0);
expect(requirementOut.fail).toBe(expected === "fail" ? 1 : 0);
expect(requirementOut.manual).toBe(expected === "manual" ? 1 : 0);
}
});
it("aggregates counters up through category and framework levels", () => {
const pairs = [
{
attribute: buildAttribute(
"CE-FW-1",
baseMetadata({ Theme: "Firewalls" }),
),
requirement: buildRequirement("CE-FW-1", REQUIREMENT_STATUS.PASS),
},
{
attribute: buildAttribute(
"CE-FW-2",
baseMetadata({ Theme: "Firewalls" }),
),
requirement: buildRequirement("CE-FW-2", REQUIREMENT_STATUS.FAIL),
},
{
attribute: buildAttribute(
"CE-MP-1",
baseMetadata({ Theme: "Malware Protection" }),
),
requirement: buildRequirement("CE-MP-1", REQUIREMENT_STATUS.MANUAL),
},
];
const { attributesData, requirementsData } = buildInputs(pairs);
const [framework] = mapComplianceData(attributesData, requirementsData);
const firewalls = framework.categories.find(
(c) => c.name === "Firewalls",
)!;
expect(firewalls.pass).toBe(1);
expect(firewalls.fail).toBe(1);
expect(firewalls.manual).toBe(0);
expect(framework.pass).toBe(1);
expect(framework.fail).toBe(1);
expect(framework.manual).toBe(1);
});
});
describe("manual requirements with empty check lists", () => {
it("carries through a MANUAL requirement that has no checks", () => {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute(
"CE-SUM-03",
baseMetadata({
Theme: "Security Update Management",
AssessmentStatus: "Manual",
CloudApplicability: "partial",
}),
{ name: "Automatic updates enabled where possible", checks: [] },
),
requirement: buildRequirement("CE-SUM-03", REQUIREMENT_STATUS.MANUAL),
},
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut =
framework.categories[0].controls[0].requirements[0];
expect(requirementOut.check_ids).toEqual([]);
expect(requirementOut.manual).toBe(1);
expect(requirementOut.assessment_status).toBe("Manual");
});
it("defaults check_ids to an empty array when the attribute omits them", () => {
const attribute = buildAttribute("CE-SUM-01", baseMetadata());
// Drop check_ids entirely to mimic an API payload without the field.
delete (attribute.attributes.attributes as { check_ids?: string[] })
.check_ids;
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement: buildRequirement("CE-SUM-01") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(
framework.categories[0].controls[0].requirements[0].check_ids,
).toEqual([]);
});
it("keeps findings enabled for a manual requirement with no checks", () => {
// A MANUAL requirement carries a manual count of 1, so the accordion
// still surfaces its (manual) finding even though it has no checks —
// findings are only disabled when there is nothing to show at all
// (empty checks AND no manual count).
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-SUM-03", baseMetadata(), {
checks: [],
}),
requirement: buildRequirement("CE-SUM-03", REQUIREMENT_STATUS.MANUAL),
},
]);
const frameworks = mapComplianceData(attributesData, requirementsData);
const items = toAccordionItems(frameworks, "scan-1");
const content = items[0].items![0].content as {
props: { disableFindings: boolean };
};
expect(content.props.disableFindings).toBe(false);
});
it("disables findings for a non-manual requirement that has no checks", () => {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-SUM-01", baseMetadata(), {
checks: [],
}),
requirement: buildRequirement("CE-SUM-01", REQUIREMENT_STATUS.PASS),
},
]);
const frameworks = mapComplianceData(attributesData, requirementsData);
const items = toAccordionItems(frameworks, "scan-1");
const content = items[0].items![0].content as {
props: { disableFindings: boolean };
};
expect(content.props.disableFindings).toBe(true);
});
});
describe("preservation of Cyber Essentials metadata fields", () => {
it("propagates Theme, AssessmentStatus, CloudApplicability, RemediationProcedure and References", () => {
const metadata = baseMetadata({
Theme: "User Access Control",
AssessmentStatus: "Manual",
CloudApplicability: "partial",
RemediationProcedure: "Remediate the access control gap.",
References:
"NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section D",
});
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-UAC-1", metadata),
requirement: buildRequirement("CE-UAC-1"),
},
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut =
framework.categories[0].controls[0].requirements[0];
expect(requirementOut.theme).toBe("User Access Control");
expect(requirementOut.assessment_status).toBe("Manual");
expect(requirementOut.cloud_applicability).toBe("partial");
expect(requirementOut.remediation_procedure).toBe(
"Remediate the access control gap.",
);
expect(requirementOut.references).toBe(
"NCSC Cyber Essentials: Requirements for IT Infrastructure v3.3 (April 2026), Section D",
);
});
it("prefixes the requirement name with its id when a name is present", () => {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-FW-1", baseMetadata(), {
name: "Boundary firewalls in place",
}),
requirement: buildRequirement("CE-FW-1"),
},
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories[0].controls[0].requirements[0].name).toBe(
"CE-FW-1 - Boundary firewalls in place",
);
});
it("falls back to the bare id when no name is supplied", () => {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-FW-1", baseMetadata()),
requirement: buildRequirement("CE-FW-1"),
},
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories[0].controls[0].requirements[0].name).toBe(
"CE-FW-1",
);
});
it("uses the literal API description for the requirement description", () => {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-FW-1", baseMetadata(), {
description: "Boundary firewalls must be configured.",
}),
requirement: buildRequirement("CE-FW-1"),
},
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(
framework.categories[0].controls[0].requirements[0].description,
).toBe("Boundary firewalls must be configured.");
});
});
describe("skipping malformed entries", () => {
it("skips attribute items whose metadata is missing", () => {
const valid = buildAttribute("CE-FW-1", baseMetadata());
const broken = buildAttribute("CE-FW-2", baseMetadata());
broken.attributes.attributes.metadata = [];
const { attributesData, requirementsData } = buildInputs([
{ attribute: valid, requirement: buildRequirement("CE-FW-1") },
{ attribute: broken, requirement: buildRequirement("CE-FW-2") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories[0].controls[0].requirements).toHaveLength(1);
expect(framework.categories[0].controls[0].requirements[0].name).toBe(
"CE-FW-1",
);
});
it("skips attribute items without a matching requirement entry", () => {
const result = mapComplianceData(
{
data: [
buildAttribute("CE-FW-1", baseMetadata()),
buildAttribute("CE-FW-2", baseMetadata()),
],
},
{ data: [buildRequirement("CE-FW-1")] },
);
expect(result[0].categories[0].controls[0].requirements).toHaveLength(1);
});
});
});
describe("toAccordionItems (Cyber Essentials)", () => {
it("produces one accordion item per theme, in canonical order", () => {
const { attributesData, requirementsData } = buildInputs(
oneRequirementPerThemeUnordered(),
);
const frameworks = mapComplianceData(attributesData, requirementsData);
const items = toAccordionItems(frameworks, "scan-1");
expect(items.map((item) => item.key)).toEqual(
CYBER_ESSENTIALS_THEME_ORDER.map((theme) => `${FRAMEWORK}-${theme}`),
);
});
it("returns an empty list when given no frameworks", () => {
expect(toAccordionItems([], "scan-1")).toEqual([]);
});
it("keeps findings enabled for an automated requirement that has checks", () => {
const { attributesData, requirementsData } = buildInputs([
{
attribute: buildAttribute("CE-FW-1", baseMetadata(), {
checks: ["check_one"],
}),
requirement: buildRequirement("CE-FW-1"),
},
]);
const frameworks = mapComplianceData(attributesData, requirementsData);
const items = toAccordionItems(frameworks, "scan-1");
const content = items[0].items![0].content as {
props: { disableFindings: boolean };
};
expect(content.props.disableFindings).toBe(false);
});
});
+158
View File
@@ -0,0 +1,158 @@
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/shadcn/accordion/Accordion";
import { FindingStatus } from "@/components/shadcn/table/status-finding-badge";
import {
AttributesData,
CyberEssentialsAttributesMetadata,
Framework,
Requirement,
REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import {
calculateFrameworkCounters,
createRequirementsMap,
findOrCreateCategory,
findOrCreateControl,
findOrCreateFramework,
} from "./commons";
// Display order for the five Cyber Essentials control themes in the accordion
// and any grouped chart. Mirrors the order declared in
// `prowler/compliance/cyber_essentials_3.3.json` so the UI always renders
// themes in the canonical reading order regardless of API response order.
export const CYBER_ESSENTIALS_THEME_ORDER: readonly string[] = [
"Firewalls",
"Secure Configuration",
"Security Update Management",
"User Access Control",
"Malware Protection",
];
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 CyberEssentialsAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
// Group by Theme (top-level accordion section). The remaining attributes
// live inside the requirement so they show up on the detail drawer.
const categoryName = attrs.Theme;
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: theme → 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,
invalid_config: requirementData.attributes.invalid_config || false,
...getStatusCounters(finalStatus),
theme: attrs.Theme,
assessment_status: attrs.AssessmentStatus,
cloud_applicability: attrs.CloudApplicability,
remediation_procedure: attrs.RemediationProcedure,
references: attrs.References,
};
control.requirements.push(requirement);
}
// Sort categories by canonical theme order so the framework always reads from
// "Firewalls" down to "Malware Protection", regardless of map insertion order
// driven by the API response.
for (const framework of frameworks) {
framework.categories.sort((a, b) => {
const ia = CYBER_ESSENTIALS_THEME_ORDER.indexOf(a.name);
const ib = CYBER_ESSENTIALS_THEME_ORDER.indexOf(b.name);
// Unknown themes (defensive — shouldn't happen) sink to the bottom.
const orderA = ia === -1 ? CYBER_ESSENTIALS_THEME_ORDER.length : ia;
const orderB = ib === -1 ? CYBER_ESSENTIALS_THEME_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: "",
// Theme → 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}
invalidConfig={requirement.invalid_config}
/>
),
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: [],
})),
),
})),
);
};