mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-18 09:24:15 +00:00
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
391 lines
14 KiB
TypeScript
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([]);
|
|
});
|
|
});
|