Files
prowler/ui/lib/compliance/asd-essential-eight.test.ts

391 lines
14 KiB
TypeScript

import { isValidElement } from "react";
import { describe, expect, it, vi } from "vitest";
// `asd-essential-eight.tsx` re-exports `toAccordionItems` which builds JSX
// referencing 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 {
ASDEssentialEightAttributesMetadata,
AttributesData,
AttributesItemData,
REQUIREMENT_STATUS,
RequirementItemData,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
import { mapComplianceData, toAccordionItems } from "./asd-essential-eight";
const FRAMEWORK = "ASD-Essential-Eight";
const baseMetadata = (
overrides: Partial<ASDEssentialEightAttributesMetadata> = {},
): ASDEssentialEightAttributesMetadata => ({
Section: "1 Patch applications",
MaturityLevel: "ML1",
AssessmentStatus: "Automated",
CloudApplicability: "full",
MitigatedThreats: ["T1190"],
Description: "Provider-specific implementation note.",
RationaleStatement: "Why this matters.",
ImpactStatement: "Impact when not in place.",
RemediationProcedure: "Steps to remediate.",
AuditProcedure: "Steps to audit.",
AdditionalInformation: "Extra context.",
References: "https://example.com/a, https://example.com/b",
...overrides,
});
const buildAttribute = (
id: string,
description: string,
metadata: ASDEssentialEightAttributesMetadata,
checks: string[] = ["check_one"],
): AttributesItemData => ({
type: "compliance-requirements-attributes",
id,
attributes: {
framework_description: "ASD Essential Eight",
framework: FRAMEWORK,
version: "1.0",
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: "1.0",
description: "Canonical ASD 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) },
});
describe("mapComplianceData (ASD Essential Eight)", () => {
it("returns an empty list when there are no attributes", () => {
const { attributesData, requirementsData } = buildInputs([]);
expect(mapComplianceData(attributesData, requirementsData)).toEqual([]);
});
it("creates one framework with one category containing one control per requirement", () => {
const attribute = buildAttribute(
"E8-PA-1",
"Apply patches to applications.",
baseMetadata(),
);
const requirement = buildRequirement("E8-PA-1");
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.name).toBe(FRAMEWORK);
expect(framework.categories).toHaveLength(1);
expect(framework.categories[0].controls).toHaveLength(1);
expect(framework.categories[0].controls[0].requirements).toHaveLength(1);
});
it("normalizes 'N Foo' Section names to 'N. Foo' for the accordion header", () => {
const attribute = buildAttribute(
"E8-PA-1",
"Apply patches.",
baseMetadata({ Section: "1 Patch applications" }),
);
const requirement = buildRequirement("E8-PA-1");
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories[0].name).toBe("1. Patch applications");
});
it("uses the literal API description (not the metadata Description) for the requirement description", () => {
// Regression: an earlier draft surfaced `attrs.Description` (provider
// commentary) in place of the canonical clause. The literal API
// description must win.
const attribute = buildAttribute(
"E8-PA-1",
"Canonical clause text.",
baseMetadata({ Description: "Provider-specific commentary." }),
);
const requirement = buildRequirement("E8-PA-1");
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut = framework.categories[0].controls[0].requirements[0];
expect(requirementOut.description).toBe("Canonical clause text.");
expect(framework.categories[0].controls[0].label).toBe(
"E8-PA-1 - Canonical clause text.",
);
});
it("exposes provider commentary as `implementation_notes` (not `aws_description`)", () => {
const attribute = buildAttribute(
"E8-PA-1",
"Canonical clause text.",
baseMetadata({ Description: "Provider commentary." }),
);
const requirement = buildRequirement("E8-PA-1");
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut = framework.categories[0].controls[0].requirements[0];
expect(requirementOut.implementation_notes).toBe("Provider commentary.");
// The legacy field name must NOT be set, so a stale UI reading
// `aws_description` surfaces the regression instead of silently
// falling back to undefined.
expect(requirementOut.aws_description).toBeUndefined();
});
it("propagates every metadata field onto the requirement", () => {
const metadata = baseMetadata({
Section: "2 Patch operating systems",
MaturityLevel: "ML1",
AssessmentStatus: "Manual",
CloudApplicability: "partial",
MitigatedThreats: ["T1059", "T1190"],
RationaleStatement: "Rationale.",
ImpactStatement: "Impact.",
RemediationProcedure: "Remediate.",
AuditProcedure: "Audit.",
AdditionalInformation: "More info.",
References: "https://example.com/x",
});
const attribute = buildAttribute("E8-OS-1", "OS patching.", metadata);
const requirement = buildRequirement("E8-OS-1");
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
const requirementOut = framework.categories[0].controls[0].requirements[0];
expect(requirementOut.maturity_level).toBe("ML1");
expect(requirementOut.assessment_status).toBe("Manual");
expect(requirementOut.cloud_applicability).toBe("partial");
expect(requirementOut.mitigated_threats).toEqual(["T1059", "T1190"]);
expect(requirementOut.rationale_statement).toBe("Rationale.");
expect(requirementOut.impact_statement).toBe("Impact.");
expect(requirementOut.remediation_procedure).toBe("Remediate.");
expect(requirementOut.audit_procedure).toBe("Audit.");
expect(requirementOut.additional_information).toBe("More info.");
expect(requirementOut.references).toBe("https://example.com/x");
});
it("skips attributes whose ASD metadata does not match the typed model", () => {
const attribute = buildAttribute("E8-BAD-1", "Invalid metadata.", {
...baseMetadata(),
MaturityLevel: "ML4",
} as unknown as ASDEssentialEightAttributesMetadata);
const requirement = buildRequirement("E8-BAD-1");
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
expect(mapComplianceData(attributesData, requirementsData)).toEqual([]);
});
it("derives counters from RequirementStatus, not from metadata flags", () => {
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 attribute = buildAttribute(
`E8-${status}`,
"clause",
baseMetadata(),
);
const requirement = buildRequirement(`E8-${status}`, status);
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement },
]);
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("groups requirements with the same Section under one category", () => {
const attrA = buildAttribute(
"E8-PA-1",
"App patching A.",
baseMetadata({ Section: "1 Patch applications" }),
);
const attrB = buildAttribute(
"E8-PA-2",
"App patching B.",
baseMetadata({ Section: "1 Patch applications" }),
);
const attrC = buildAttribute(
"E8-OS-1",
"OS patching.",
baseMetadata({ Section: "2 Patch operating systems" }),
);
const { attributesData, requirementsData } = buildInputs([
{ attribute: attrA, requirement: buildRequirement("E8-PA-1") },
{ attribute: attrB, requirement: buildRequirement("E8-PA-2") },
{ attribute: attrC, requirement: buildRequirement("E8-OS-1") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories.map((c) => c.name)).toEqual([
"1. Patch applications",
"2. Patch operating systems",
]);
expect(framework.categories[0].controls).toHaveLength(2);
expect(framework.categories[1].controls).toHaveLength(1);
});
it("skips attribute items whose metadata is missing", () => {
const valid = buildAttribute("E8-PA-1", "valid", baseMetadata());
const broken: AttributesItemData = {
...buildAttribute("E8-PA-2", "broken", baseMetadata()),
attributes: {
...buildAttribute("E8-PA-2", "broken", baseMetadata()).attributes,
attributes: {
metadata: [],
check_ids: [],
},
},
};
const { attributesData, requirementsData } = buildInputs([
{ attribute: valid, requirement: buildRequirement("E8-PA-1") },
{ attribute: broken, requirement: buildRequirement("E8-PA-2") },
]);
const [framework] = mapComplianceData(attributesData, requirementsData);
expect(framework.categories[0].controls).toHaveLength(1);
expect(framework.categories[0].controls[0].requirements[0].name).toBe(
"E8-PA-1",
);
});
it("skips attribute items without a matching requirement entry", () => {
const attribute = buildAttribute("E8-PA-1", "clause", baseMetadata());
const orphan = buildAttribute("E8-PA-2", "orphan", baseMetadata());
const result = mapComplianceData(
{ data: [attribute, orphan] },
{ data: [buildRequirement("E8-PA-1")] },
);
expect(result[0].categories[0].controls).toHaveLength(1);
});
it("accepts a `_filter` parameter without altering output (placeholder for ML2/ML3)", () => {
const attribute = buildAttribute("E8-PA-1", "clause", baseMetadata());
const { attributesData, requirementsData } = buildInputs([
{ attribute, requirement: buildRequirement("E8-PA-1") },
]);
const withoutFilter = mapComplianceData(attributesData, requirementsData);
const withFilter = mapComplianceData(
attributesData,
requirementsData,
"ML2",
);
expect(withFilter).toEqual(withoutFilter);
});
});
describe("toAccordionItems (ASD Essential Eight)", () => {
it("produces one accordion item per category", () => {
const attrA = buildAttribute(
"E8-PA-1",
"App patching.",
baseMetadata({ Section: "1 Patch applications" }),
);
const attrB = buildAttribute(
"E8-OS-1",
"OS patching.",
baseMetadata({ Section: "2 Patch operating systems" }),
);
const frameworks = mapComplianceData(
{ data: [attrA, attrB] },
{
data: [buildRequirement("E8-PA-1"), buildRequirement("E8-OS-1")],
},
);
const items = toAccordionItems(frameworks, "scan-1");
expect(items).toHaveLength(2);
expect(items[0].key).toBe(`${FRAMEWORK}-1. Patch applications`);
expect(items[1].key).toBe(`${FRAMEWORK}-2. Patch operating systems`);
// Every accordion item exposes a renderable React element title and
// children — both of which we assert structurally (we mocked the
// underlying components, but the elements themselves must exist).
expect(isValidElement(items[0].title)).toBe(true);
expect(items[0].items).toHaveLength(1);
});
it("returns an empty list when given no frameworks", () => {
expect(toAccordionItems([], "scan-1")).toEqual([]);
});
});