mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
522 lines
17 KiB
TypeScript
522 lines
17 KiB
TypeScript
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);
|
|
});
|
|
});
|