mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(ui): ASD Essential Eight compliance framework support (#11071)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
+5
-1
@@ -4,6 +4,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.26.0] (Prowler v5.26.0)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- ASD Essential Eight compliance framework support: AWS scans now surface the Essential Eight overview card, accordion view (Sections normalised to `N. <name>`, controls grouped per ML1 clause) and a dedicated requirement detail panel with maturity level, assessment, cloud applicability, mitigated threats and References [(#11071)](https://github.com/prowler-cloud/prowler/pull/11071)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Standardized "Providers" wording across UI and documentation, replacing legacy "Cloud Providers" / "Accounts" / "Account Groups" copy [(#10971)](https://github.com/prowler-cloud/prowler/pull/10971)
|
||||
@@ -48,7 +52,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
### 🔄 Changed
|
||||
|
||||
- Redesign compliance page, client-side search for compliance frameworks, compact scan selector trigger, enhanced compliance cards [(#10767)](https://github.com/prowler-cloud/prowler/pull/10767)
|
||||
- Allows tenant owners to expel users from their organizations [(#10787)](https://github.com/prowler-cloud/prowler/pull/10787)
|
||||
- Allows tenant owners to expel users from their organizations [(#10787)](https://github.com/prowler-cloud/prowler/pull/10787)
|
||||
- Shared filter dropdowns now support local option search and auto-scroll to the first visible match across table and provider filters [(#10859)](https://github.com/prowler-cloud/prowler/pull/10859)
|
||||
- Backward-compatibility middleware redirect from `/sign-up?invitation_token=…` to `/invitation/accept?invitation_token=…`; new invitation emails use `/invitation/accept` directly [(#10797)](https://github.com/prowler-cloud/prowler/pull/10797)
|
||||
- Mutelist improvements: table now supports name/reason search and visual count badges for finding targets [(#10846)](https://github.com/prowler-cloud/prowler/pull/10846)
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// `CustomLink` re-imports the `@/lib` barrel which transitively pulls in
|
||||
// `next-auth` (server-only). Stub it with a plain anchor — we only need
|
||||
// the `<a>` semantics here so the regex/extraction tests can assert on
|
||||
// `href` and accessible name.
|
||||
vi.mock("@/components/ui/custom/custom-link", () => ({
|
||||
CustomLink: ({ href, children }: { href: string; children: ReactNode }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
import {
|
||||
type ASDEssentialEightRequirement,
|
||||
type Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import { ASDEssentialEightCustomDetails } from "./asd-essential-eight-details";
|
||||
|
||||
const fullRequirement: ASDEssentialEightRequirement = {
|
||||
name: "E8-PA-1",
|
||||
description: "Apply patches to internet-facing applications.",
|
||||
status: REQUIREMENT_STATUS.PASS,
|
||||
pass: 1,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
check_ids: ["check_one"],
|
||||
maturity_level: "ML1",
|
||||
assessment_status: "Automated",
|
||||
cloud_applicability: "full",
|
||||
mitigated_threats: ["T1190", "T1059"],
|
||||
implementation_notes: "Use SSM Patch Manager for AWS workloads.",
|
||||
rationale_statement: "Unpatched apps are commonly exploited.",
|
||||
impact_statement: "Increases blast radius of public-facing CVEs.",
|
||||
remediation_procedure: "Run **patch baseline** weekly.",
|
||||
audit_procedure: "Verify *baseline compliance*.",
|
||||
additional_information: "Refer to internal SOPs.",
|
||||
references: "https://example.com/a, https://example.com/b",
|
||||
};
|
||||
|
||||
const emptyRequirement: Requirement = {
|
||||
name: "E8-EMPTY",
|
||||
description: "",
|
||||
status: REQUIREMENT_STATUS.MANUAL,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 1,
|
||||
check_ids: [],
|
||||
};
|
||||
|
||||
describe("ASDEssentialEightCustomDetails", () => {
|
||||
describe("with a fully populated requirement", () => {
|
||||
it("renders every textual section", () => {
|
||||
render(<ASDEssentialEightCustomDetails requirement={fullRequirement} />);
|
||||
|
||||
expect(screen.getByText("Description")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Apply patches to internet-facing applications."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Implementation Notes")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText("Use SSM Patch Manager for AWS workloads."),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Rationale Statement")).toBeInTheDocument();
|
||||
expect(screen.getByText("Impact Statement")).toBeInTheDocument();
|
||||
expect(screen.getByText("Additional Information")).toBeInTheDocument();
|
||||
expect(screen.getByText("Refer to internal SOPs.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the three classification badges with their values", () => {
|
||||
render(<ASDEssentialEightCustomDetails requirement={fullRequirement} />);
|
||||
|
||||
expect(screen.getByText("Maturity Level:")).toBeInTheDocument();
|
||||
expect(screen.getByText("ML1")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Assessment:")).toBeInTheDocument();
|
||||
expect(screen.getByText("Automated")).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText("Cloud Applicability:")).toBeInTheDocument();
|
||||
expect(screen.getByText("full")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render invalid ASD classification values", () => {
|
||||
render(
|
||||
<ASDEssentialEightCustomDetails
|
||||
requirement={{
|
||||
...fullRequirement,
|
||||
maturity_level: "ML4",
|
||||
assessment_status: "Partially automated",
|
||||
cloud_applicability: "hybrid",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Maturity Level:")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Assessment:")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Cloud Applicability:"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders mitigated threats as individual chips", () => {
|
||||
render(<ASDEssentialEightCustomDetails requirement={fullRequirement} />);
|
||||
|
||||
expect(screen.getByText("Mitigated Threats")).toBeInTheDocument();
|
||||
expect(screen.getByText("T1190")).toBeInTheDocument();
|
||||
expect(screen.getByText("T1059")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Remediation and Audit procedures as markdown", () => {
|
||||
render(<ASDEssentialEightCustomDetails requirement={fullRequirement} />);
|
||||
|
||||
// The markdown renderer transforms `**patch baseline**` into a <strong>
|
||||
// and `*baseline compliance*` into an <em>. Asserting on the rendered
|
||||
// tags is what makes this a behavioral test rather than a string grep.
|
||||
expect(screen.getByText("Remediation Procedure")).toBeInTheDocument();
|
||||
expect(screen.getByText("patch baseline").tagName).toBe("STRONG");
|
||||
|
||||
expect(screen.getByText("Audit Procedure")).toBeInTheDocument();
|
||||
expect(screen.getByText("baseline compliance").tagName).toBe("EM");
|
||||
});
|
||||
|
||||
it("extracts every URL from the comma-separated References field", () => {
|
||||
render(<ASDEssentialEightCustomDetails requirement={fullRequirement} />);
|
||||
|
||||
expect(screen.getByText("References")).toBeInTheDocument();
|
||||
const linkA = screen.getByRole("link", {
|
||||
name: "https://example.com/a",
|
||||
});
|
||||
const linkB = screen.getByRole("link", {
|
||||
name: "https://example.com/b",
|
||||
});
|
||||
expect(linkA).toHaveAttribute("href", "https://example.com/a");
|
||||
expect(linkB).toHaveAttribute("href", "https://example.com/b");
|
||||
});
|
||||
|
||||
it("preserves http:// references (regex must not silently drop plain HTTP)", () => {
|
||||
render(
|
||||
<ASDEssentialEightCustomDetails
|
||||
requirement={{
|
||||
...fullRequirement,
|
||||
references:
|
||||
"http://insecure.example.com/x https://secure.example.com/y",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByRole("link", { name: "http://insecure.example.com/x" }),
|
||||
).toHaveAttribute("href", "http://insecure.example.com/x");
|
||||
expect(
|
||||
screen.getByRole("link", { name: "https://secure.example.com/y" }),
|
||||
).toHaveAttribute("href", "https://secure.example.com/y");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with an empty requirement", () => {
|
||||
it("renders nothing inside the container when every optional field is missing", () => {
|
||||
const { container } = render(
|
||||
<ASDEssentialEightCustomDetails requirement={emptyRequirement} />,
|
||||
);
|
||||
|
||||
// No section headings should be rendered for an empty requirement.
|
||||
for (const heading of [
|
||||
"Description",
|
||||
"Implementation Notes",
|
||||
"Rationale Statement",
|
||||
"Impact Statement",
|
||||
"Remediation Procedure",
|
||||
"Audit Procedure",
|
||||
"Additional Information",
|
||||
"Mitigated Threats",
|
||||
"References",
|
||||
]) {
|
||||
expect(screen.queryByText(heading)).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
// No badges either.
|
||||
for (const label of [
|
||||
"Maturity Level:",
|
||||
"Assessment:",
|
||||
"Cloud Applicability:",
|
||||
]) {
|
||||
expect(screen.queryByText(label)).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
// The outer container still exists (an empty flex column) but it
|
||||
// shouldn't carry any rendered children.
|
||||
const outer = container.firstElementChild as HTMLElement | null;
|
||||
expect(outer).not.toBeNull();
|
||||
expect(outer?.children.length).toBe(1); // only the empty badge container
|
||||
});
|
||||
|
||||
it("ignores a non-string References field (no broken link rendered)", () => {
|
||||
render(
|
||||
<ASDEssentialEightCustomDetails
|
||||
requirement={{
|
||||
...emptyRequirement,
|
||||
references: undefined,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("References")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("link")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("ignores a non-string-array `mitigated_threats` field", () => {
|
||||
render(
|
||||
<ASDEssentialEightCustomDetails
|
||||
requirement={{
|
||||
...emptyRequirement,
|
||||
mitigated_threats: [{ not: "a string" }],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("Mitigated Threats")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import {
|
||||
isASDAssessmentStatus,
|
||||
isASDCloudApplicability,
|
||||
isASDMaturityLevel,
|
||||
type Requirement,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceChipContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
interface ASDEssentialEightDetailsProps {
|
||||
requirement: Requirement;
|
||||
}
|
||||
|
||||
// Each requirement's References field is a single URL or a comma/space
|
||||
// separated list of URLs. The regex matches both http:// and https:// so
|
||||
// plain-http references aren't silently dropped.
|
||||
const URL_REGEX = /https?:\/\/[^\s,]+/g;
|
||||
|
||||
const extractUrls = (references: unknown): string[] => {
|
||||
if (typeof references !== "string") return [];
|
||||
return references.match(URL_REGEX) ?? [];
|
||||
};
|
||||
|
||||
const isNonEmptyString = (value: unknown): value is string =>
|
||||
typeof value === "string" && value.length > 0;
|
||||
|
||||
const isStringArray = (value: unknown): value is string[] =>
|
||||
Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
|
||||
export const ASDEssentialEightCustomDetails = ({
|
||||
requirement,
|
||||
}: ASDEssentialEightDetailsProps) => {
|
||||
const {
|
||||
description,
|
||||
implementation_notes,
|
||||
maturity_level,
|
||||
assessment_status,
|
||||
cloud_applicability,
|
||||
mitigated_threats,
|
||||
rationale_statement,
|
||||
impact_statement,
|
||||
remediation_procedure,
|
||||
audit_procedure,
|
||||
additional_information,
|
||||
references,
|
||||
} = requirement;
|
||||
|
||||
const referenceUrls = extractUrls(references);
|
||||
const maturityLevel = isASDMaturityLevel(maturity_level)
|
||||
? maturity_level
|
||||
: undefined;
|
||||
const assessmentStatus = isASDAssessmentStatus(assessment_status)
|
||||
? assessment_status
|
||||
: undefined;
|
||||
const cloudApplicability = isASDCloudApplicability(cloud_applicability)
|
||||
? cloud_applicability
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{isNonEmptyString(implementation_notes) && (
|
||||
<ComplianceDetailSection title="Implementation Notes">
|
||||
<ComplianceDetailText>{implementation_notes}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{maturityLevel && (
|
||||
<ComplianceBadge
|
||||
label="Maturity Level"
|
||||
value={maturityLevel}
|
||||
color="purple"
|
||||
/>
|
||||
)}
|
||||
|
||||
{assessmentStatus && (
|
||||
<ComplianceBadge
|
||||
label="Assessment"
|
||||
value={assessmentStatus}
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
|
||||
{cloudApplicability && (
|
||||
<ComplianceBadge
|
||||
label="Cloud Applicability"
|
||||
value={cloudApplicability}
|
||||
color="orange"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
{/* `isStringArray` narrows the index-signature union to string[], so no cast is needed. `ComplianceChipContainer` returns null on empty arrays, so no length check is needed here either. */}
|
||||
{isStringArray(mitigated_threats) && (
|
||||
<ComplianceChipContainer
|
||||
title="Mitigated Threats"
|
||||
items={mitigated_threats}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isNonEmptyString(rationale_statement) && (
|
||||
<ComplianceDetailSection title="Rationale Statement">
|
||||
<ComplianceDetailText>{rationale_statement}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{isNonEmptyString(impact_statement) && (
|
||||
<ComplianceDetailSection title="Impact Statement">
|
||||
<ComplianceDetailText>{impact_statement}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{isNonEmptyString(remediation_procedure) && (
|
||||
<ComplianceDetailSection title="Remediation Procedure">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown>{remediation_procedure}</ReactMarkdown>
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{isNonEmptyString(audit_procedure) && (
|
||||
<ComplianceDetailSection title="Audit Procedure">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown>{audit_procedure}</ReactMarkdown>
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{isNonEmptyString(additional_information) && (
|
||||
<ComplianceDetailSection title="Additional Information">
|
||||
<ComplianceDetailText className="whitespace-pre-wrap">
|
||||
{additional_information}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{referenceUrls.length > 0 && (
|
||||
<ComplianceDetailSection title="References">
|
||||
<div className="flex flex-col gap-1">
|
||||
{referenceUrls.map((url) => (
|
||||
// URLs are unique within this list, so they outperform the
|
||||
// positional index as a React key (avoids reconciliation
|
||||
// glitches if the order ever shifts).
|
||||
<div key={url}>
|
||||
<CustomLink href={url}>{url}</CustomLink>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { getComplianceIcon } from "./IconCompliance";
|
||||
|
||||
describe("getComplianceIcon", () => {
|
||||
describe("framework name matching", () => {
|
||||
it("resolves ASD Essential Eight via the `essential` keyword", () => {
|
||||
expect(getComplianceIcon("ASD-Essential-Eight")).toBeDefined();
|
||||
expect(getComplianceIcon("asd-essential-eight")).toBeDefined();
|
||||
expect(getComplianceIcon("ASD Essential Eight Maturity Model")).toBe(
|
||||
getComplianceIcon("ASD-Essential-Eight"),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns undefined for an unknown framework name", () => {
|
||||
expect(getComplianceIcon("Made-Up-Framework")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for an empty string", () => {
|
||||
expect(getComplianceIcon("")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("compliance_id matching (with provider suffix)", () => {
|
||||
// Regression coverage for the icon-shadowing bug: every AWS-hosted
|
||||
// compliance_id ends with `_aws`, so `getComplianceIcon` MUST resolve
|
||||
// by the framework keyword (cis, iso, ...) before falling through to
|
||||
// the provider-level `aws` keyword. If `aws` ever moves up in
|
||||
// COMPLIANCE_LOGOS, every assertion below will flip and surface the
|
||||
// regression.
|
||||
|
||||
it("resolves CIS variants by the framework keyword, not by `aws`", () => {
|
||||
const cisLogo = getComplianceIcon("CIS");
|
||||
expect(cisLogo).toBeDefined();
|
||||
expect(getComplianceIcon("cis_4.0_aws")).toBe(cisLogo);
|
||||
expect(getComplianceIcon("cis_5.0_aws")).toBe(cisLogo);
|
||||
expect(getComplianceIcon("cis_6.0_aws")).toBe(cisLogo);
|
||||
});
|
||||
|
||||
it("resolves CISA before falling back to CIS or AWS", () => {
|
||||
const cisLogo = getComplianceIcon("CIS");
|
||||
const cisaLogo = getComplianceIcon("cisa");
|
||||
expect(cisaLogo).toBeDefined();
|
||||
expect(cisaLogo).not.toBe(cisLogo);
|
||||
expect(getComplianceIcon("cisa_aws")).toBe(cisaLogo);
|
||||
});
|
||||
|
||||
it("resolves ISO 27001 by the framework keyword, not by `aws`", () => {
|
||||
const isoLogo = getComplianceIcon("ISO27001");
|
||||
expect(isoLogo).toBeDefined();
|
||||
expect(getComplianceIcon("iso27001_2022_aws")).toBe(isoLogo);
|
||||
expect(getComplianceIcon("iso27001_2013_aws")).toBe(isoLogo);
|
||||
});
|
||||
|
||||
it("resolves Prowler ThreatScore by the framework keyword, not by `aws`", () => {
|
||||
const threatLogo = getComplianceIcon("ProwlerThreatScore");
|
||||
expect(threatLogo).toBeDefined();
|
||||
expect(getComplianceIcon("prowler_threatscore_aws")).toBe(threatLogo);
|
||||
});
|
||||
|
||||
it("resolves ASD Essential Eight by the framework keyword, not by `aws`", () => {
|
||||
const essentialLogo = getComplianceIcon("ASD-Essential-Eight");
|
||||
expect(essentialLogo).toBeDefined();
|
||||
expect(getComplianceIcon("asd_essential_eight_aws")).toBe(essentialLogo);
|
||||
});
|
||||
|
||||
it("resolves NIS2 distinctly from NIST", () => {
|
||||
const nis2Logo = getComplianceIcon("NIS2");
|
||||
const nistLogo = getComplianceIcon("NIST-800-53");
|
||||
expect(nis2Logo).toBeDefined();
|
||||
expect(nistLogo).toBeDefined();
|
||||
expect(nis2Logo).not.toBe(nistLogo);
|
||||
expect(getComplianceIcon("nis2_aws")).toBe(nis2Logo);
|
||||
expect(getComplianceIcon("nist_800_53_revision_5_aws")).toBe(nistLogo);
|
||||
});
|
||||
|
||||
it("resolves PCI/HIPAA/GDPR/SOC2/ENS/FedRAMP/MITRE/RBI/KISA/SecNumCloud by their framework keyword", () => {
|
||||
// Spot-check the rest of the framework keywords against AWS-suffixed ids.
|
||||
// Each must resolve to a distinct logo from `aws` so the watchlist
|
||||
// surface (which keys icons by compliance_id) renders correctly.
|
||||
const awsLogo = getComplianceIcon(
|
||||
"AWS-Well-Architected-Framework-Security-Pillar",
|
||||
);
|
||||
const cases = [
|
||||
"pci_4.0_aws",
|
||||
"hipaa_aws",
|
||||
"gdpr_aws",
|
||||
"soc2_aws",
|
||||
"ens_rd2022_aws",
|
||||
"fedramp_low_revision_4_aws",
|
||||
"mitre_attack_aws",
|
||||
"rbi_cyber_security_framework_aws",
|
||||
"kisa_isms_p_2023_aws",
|
||||
"secnumcloud_3.2_aws",
|
||||
];
|
||||
for (const id of cases) {
|
||||
const resolved = getComplianceIcon(id);
|
||||
expect(
|
||||
resolved,
|
||||
`${id} should resolve to a framework-specific logo, not the AWS fallback`,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
resolved,
|
||||
`${id} should not collapse to the generic AWS logo`,
|
||||
).not.toBe(awsLogo);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("AWS-only frameworks fall through to the AWS logo", () => {
|
||||
// These frameworks are genuinely AWS-specific and have no other matching
|
||||
// keyword in the registry. They must resolve to the AWS logo via the
|
||||
// tail-end fallback.
|
||||
|
||||
it("resolves AWS Well-Architected pillars to the AWS logo", () => {
|
||||
const awsLogo = getComplianceIcon(
|
||||
"AWS-Well-Architected-Framework-Security-Pillar",
|
||||
);
|
||||
expect(awsLogo).toBeDefined();
|
||||
expect(
|
||||
getComplianceIcon("AWS-Well-Architected-Framework-Reliability-Pillar"),
|
||||
).toBe(awsLogo);
|
||||
expect(
|
||||
getComplianceIcon("aws_well_architected_framework_security_pillar_aws"),
|
||||
).toBe(awsLogo);
|
||||
});
|
||||
|
||||
it("resolves AWS Foundational frameworks to the AWS logo", () => {
|
||||
const awsLogo = getComplianceIcon(
|
||||
"AWS-Well-Architected-Framework-Security-Pillar",
|
||||
);
|
||||
expect(
|
||||
getComplianceIcon("aws_foundational_security_best_practices_aws"),
|
||||
).toBe(awsLogo);
|
||||
expect(getComplianceIcon("aws_foundational_technical_review_aws")).toBe(
|
||||
awsLogo,
|
||||
);
|
||||
expect(
|
||||
getComplianceIcon("aws_audit_manager_control_tower_guardrails_aws"),
|
||||
).toBe(awsLogo);
|
||||
expect(getComplianceIcon("aws_account_security_onboarding_aws")).toBe(
|
||||
awsLogo,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import ANSSILogo from "./anssi.png";
|
||||
import ASDEssentialEightLogo from "./asd-essential-eight.svg";
|
||||
import AWSLogo from "./aws.svg";
|
||||
import C5Logo from "./c5.svg";
|
||||
import CCCLogo from "./ccc.svg";
|
||||
@@ -21,34 +22,58 @@ import PROWLERTHREATLogo from "./prowlerThreat.svg";
|
||||
import RBILogo from "./rbi.svg";
|
||||
import SOC2Logo from "./soc2.svg";
|
||||
|
||||
const COMPLIANCE_LOGOS = {
|
||||
aws: AWSLogo,
|
||||
cisa: CISALogo,
|
||||
cis: CISLogo,
|
||||
ens: ENSLogo,
|
||||
ffiec: FFIECLogo,
|
||||
fedramp: FedRAMPLogo,
|
||||
gdpr: GDPRLogo,
|
||||
gxp: GxPLogo,
|
||||
hipaa: HIPAALogo,
|
||||
iso: ISOLogo,
|
||||
mitre: MITRELogo,
|
||||
nist: NISTLogo,
|
||||
pci: PCILogo,
|
||||
rbi: RBILogo,
|
||||
soc2: SOC2Logo,
|
||||
kisa: KISALogo,
|
||||
prowlerthreatscore: PROWLERTHREATLogo,
|
||||
nis2: NIS2Logo,
|
||||
c5: C5Logo,
|
||||
ccc: CCCLogo,
|
||||
csa: CSALogo,
|
||||
secnumcloud: ANSSILogo,
|
||||
} as const;
|
||||
// Framework-specific keywords MUST come before the generic provider-level
|
||||
// `aws` keyword. `getComplianceIcon` resolves by substring `includes`, and
|
||||
// AWS compliance ids carry a `_aws` provider suffix (e.g. `cis_4.0_aws`,
|
||||
// `iso27001_2022_aws`, `prowler_threatscore_aws`, `asd_essential_eight_aws`).
|
||||
// Without this ordering the generic `aws` entry would shadow every
|
||||
// framework-specific logo on watchlist surfaces that resolve by id. The
|
||||
// list is a tuple array (rather than an object literal) because lookup
|
||||
// order is semantically meaningful here — JavaScript engines preserve
|
||||
// insertion order for string keys, but a tuple makes that contract
|
||||
// explicit and prevents an accidental object-literal sort or
|
||||
// `Object.fromEntries` round-trip from silently breaking resolution.
|
||||
// `aws` is intentionally last so the framework keywords win, while genuinely
|
||||
// AWS-only frameworks (Well-Architected, Audit Manager, Foundational Security
|
||||
// Best Practices, Account Security Onboarding, Foundational Technical Review)
|
||||
// fall through to it because they expose no other matching keyword.
|
||||
const COMPLIANCE_LOGOS = [
|
||||
["essential", ASDEssentialEightLogo],
|
||||
["cisa", CISALogo],
|
||||
["cis", CISLogo],
|
||||
["ens", ENSLogo],
|
||||
["ffiec", FFIECLogo],
|
||||
["fedramp", FedRAMPLogo],
|
||||
["gdpr", GDPRLogo],
|
||||
["gxp", GxPLogo],
|
||||
["hipaa", HIPAALogo],
|
||||
["iso", ISOLogo],
|
||||
["mitre", MITRELogo],
|
||||
// `nist` comes before `nis2` because NIST 800-53 etc. would otherwise be
|
||||
// checked after `nis2`; both are unambiguous, but pinning the order avoids
|
||||
// surprises if a future id contains "nis2" inside a NIST acronym.
|
||||
["nist", NISTLogo],
|
||||
["nis2", NIS2Logo],
|
||||
["pci", PCILogo],
|
||||
["rbi", RBILogo],
|
||||
["soc2", SOC2Logo],
|
||||
["kisa", KISALogo],
|
||||
// `threatscore` (not `prowlerthreatscore`) matches both the framework name
|
||||
// `ProwlerThreatScore` (lowercased "prowlerthreatscore") AND the
|
||||
// compliance_id `prowler_threatscore_aws` (which separates the words with
|
||||
// an underscore). The previous one-word keyword silently failed for the
|
||||
// watchlist surface — only fixed in concert with moving `aws` to the end.
|
||||
["threatscore", PROWLERTHREATLogo],
|
||||
["c5", C5Logo],
|
||||
["ccc", CCCLogo],
|
||||
["csa", CSALogo],
|
||||
["secnumcloud", ANSSILogo],
|
||||
["aws", AWSLogo],
|
||||
] as const;
|
||||
|
||||
export const getComplianceIcon = (complianceTitle: string) => {
|
||||
const lowerTitle = complianceTitle.toLowerCase();
|
||||
return Object.entries(COMPLIANCE_LOGOS).find(([keyword]) =>
|
||||
return COMPLIANCE_LOGOS.find(([keyword]) =>
|
||||
lowerTitle.includes(keyword),
|
||||
)?.[1];
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
|
||||
<path fill="#1f2937" fill-rule="evenodd" d="M22.343 4h19.314L56 18.343v27.314L41.657 60H22.343L8 45.657V18.343L22.343 4Zm.828 4L12 19.171v25.658L23.171 56h17.658L52 44.829V19.171L40.829 8H23.171ZM32 18.5c-3.59 0-6.5 2.91-6.5 6.5 0 1.792.726 3.415 1.9 4.59A7.498 7.498 0 0 0 24.5 35.5c0 4.142 3.358 7.5 7.5 7.5s7.5-3.358 7.5-7.5a7.498 7.498 0 0 0-2.9-5.91A6.476 6.476 0 0 0 38.5 25c0-3.59-2.91-6.5-6.5-6.5Zm0 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5Zm0 9a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 604 B |
@@ -0,0 +1,390 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,172 @@
|
||||
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 type { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import type { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
type ASDEssentialEightRequirement,
|
||||
type AttributesData,
|
||||
type Framework,
|
||||
isASDEssentialEightAttributesMetadata,
|
||||
type Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
type RequirementsData,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
calculateFrameworkCounters,
|
||||
createRequirementsMap,
|
||||
findOrCreateCategory,
|
||||
findOrCreateFramework,
|
||||
updateCounters,
|
||||
} from "./commons";
|
||||
|
||||
// TODO(PROWLER-1470): `_filter` is reserved for future Maturity Level
|
||||
// filtering (analogous to CIS's Profile filter). Today the JSON only
|
||||
// contains ML1 requirements, so the parameter is a no-op; once ML2/ML3
|
||||
// ship, mirror the CIS pattern of skipping requirements whose
|
||||
// `attrs.MaturityLevel` !== filter. The leading underscore tells eslint
|
||||
// and TypeScript-ESLint that the parameter is intentionally unused.
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
_filter?: string,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirementsMap = createRequirementsMap(requirementsData);
|
||||
const frameworks: Framework[] = [];
|
||||
|
||||
// Process attributes and merge with requirements data
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes?.metadata;
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!isASDEssentialEightAttributesMetadata(attrs)) continue;
|
||||
|
||||
// Get corresponding requirement data
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
const sectionName = attrs.Section;
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status;
|
||||
const checks = attributeItem.attributes.attributes.check_ids;
|
||||
const requirementName = id;
|
||||
|
||||
// Find or create framework using common helper
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
|
||||
// Sections in the source JSON are formatted "1 Patch applications";
|
||||
// normalize to "1. Patch applications" so the leading clause number reads
|
||||
// as a sentence in the accordion header. Order is preserved by JSON
|
||||
// document order (categories materialize in insertion order via
|
||||
// `findOrCreateCategory`); this rewrite is purely cosmetic.
|
||||
const normalizedSectionName = sectionName.replace(/^(\d+)\s/, "$1. ");
|
||||
const category = findOrCreateCategory(
|
||||
framework.categories,
|
||||
normalizedSectionName,
|
||||
);
|
||||
|
||||
// Each requirement is its own control (matches CIS rendering): keeps
|
||||
// the framework's clause-level granularity visible in the accordion.
|
||||
// The accordion title and the requirement.description must surface the
|
||||
// *literal ASD clause* (`description`, the canonical standard text).
|
||||
// The Attributes[].Description field carries Prowler's
|
||||
// provider-specific implementation note; we expose it separately as
|
||||
// `implementation_notes` so the details panel can render it under
|
||||
// "Implementation Notes" without coupling the field to a single
|
||||
// provider.
|
||||
const controlLabel = `${id} - ${description}`;
|
||||
const control = {
|
||||
label: controlLabel,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
requirements: [] as Requirement[],
|
||||
};
|
||||
|
||||
const requirement = {
|
||||
name: requirementName,
|
||||
description: description,
|
||||
status: status,
|
||||
check_ids: checks,
|
||||
pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
maturity_level: attrs.MaturityLevel,
|
||||
assessment_status: attrs.AssessmentStatus,
|
||||
cloud_applicability: attrs.CloudApplicability,
|
||||
mitigated_threats: attrs.MitigatedThreats,
|
||||
implementation_notes: attrs.Description,
|
||||
rationale_statement: attrs.RationaleStatement,
|
||||
impact_statement: attrs.ImpactStatement,
|
||||
remediation_procedure: attrs.RemediationProcedure,
|
||||
audit_procedure: attrs.AuditProcedure,
|
||||
additional_information: attrs.AdditionalInformation,
|
||||
references: attrs.References,
|
||||
} satisfies ASDEssentialEightRequirement;
|
||||
|
||||
control.requirements.push(requirement);
|
||||
|
||||
// Update control counters using common helper
|
||||
updateCounters(control, requirement.status);
|
||||
|
||||
category.controls.push(control);
|
||||
}
|
||||
|
||||
// Calculate counters using common helper
|
||||
calculateFrameworkCounters(frameworks);
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => {
|
||||
return {
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.map((control, i: number) => {
|
||||
const requirement = control.requirements[0]; // Each control has one requirement
|
||||
const itemKey = `${framework.name}-${category.name}-control-${i}`;
|
||||
|
||||
return {
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={control.label}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${itemKey}`}
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
import { isValidElement, ReactElement } from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Custom-details components and the `ClientAccordionContent` chain
|
||||
// transitively import server-only code (next-auth → next/server). Mocking
|
||||
// them with identifiable stubs lets us load the registry under vitest and
|
||||
// assert that `getDetailsComponent` returns the *correct* stub for each
|
||||
// framework — i.e. that the wiring is actually behavioral.
|
||||
type DetailsStubProps = { requirement: { name?: string } };
|
||||
|
||||
// `vi.hoisted` runs *before* the hoisted `vi.mock` factories, so we can
|
||||
// safely close over `stubFactory` from inside each mock without tripping
|
||||
// the temporal-dead-zone error vitest raises for top-level helpers.
|
||||
const { stubFactory } = vi.hoisted(() => ({
|
||||
stubFactory: (label: string) => {
|
||||
const Stub = (_props: DetailsStubProps) => null;
|
||||
Stub.displayName = label;
|
||||
return Stub;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/asd-essential-eight-details",
|
||||
() => ({ ASDEssentialEightCustomDetails: stubFactory("ASDStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/aws-well-architected-details",
|
||||
() => ({ AWSWellArchitectedCustomDetails: stubFactory("AWSWAStub") }),
|
||||
);
|
||||
vi.mock("@/components/compliance/compliance-custom-details/c5-details", () => ({
|
||||
C5CustomDetails: stubFactory("C5Stub"),
|
||||
}));
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/ccc-details",
|
||||
() => ({ CCCCustomDetails: stubFactory("CCCStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/cis-details",
|
||||
() => ({ CISCustomDetails: stubFactory("CISStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/csa-details",
|
||||
() => ({ CSACustomDetails: stubFactory("CSAStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/ens-details",
|
||||
() => ({ ENSCustomDetails: stubFactory("ENSStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/generic-details",
|
||||
() => ({ GenericCustomDetails: stubFactory("GenericStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/iso-details",
|
||||
() => ({ ISOCustomDetails: stubFactory("ISOStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/kisa-details",
|
||||
() => ({ KISACustomDetails: stubFactory("KISAStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/mitre-details",
|
||||
() => ({ MITRECustomDetails: stubFactory("MITREStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/threat-details",
|
||||
() => ({ ThreatCustomDetails: stubFactory("ThreatStub") }),
|
||||
);
|
||||
|
||||
// Each per-framework mapper file (cis.tsx, ens.tsx, etc.) re-exports JSX
|
||||
// builders that pull in the same client-side accordion chain. Stub them
|
||||
// out so the registry module can load without booting Next's server-only
|
||||
// runtime — the registry is what we actually test here.
|
||||
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 { Requirement } from "@/types/compliance";
|
||||
|
||||
import { getComplianceMapper } from "./compliance-mapper";
|
||||
|
||||
const fakeRequirement: Requirement = {
|
||||
name: "test",
|
||||
description: "test",
|
||||
status: "PASS",
|
||||
pass: 1,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
check_ids: [],
|
||||
};
|
||||
|
||||
const detailsStubName = (component: unknown): string | undefined => {
|
||||
if (!isValidElement(component)) return undefined;
|
||||
// `type` of a React element holds the component function (the stub we
|
||||
// registered above); `displayName` is what we keyed each stub on.
|
||||
const element = component as ReactElement<DetailsStubProps>;
|
||||
const type = element.type as { displayName?: string };
|
||||
return type.displayName;
|
||||
};
|
||||
|
||||
describe("getComplianceMapper", () => {
|
||||
it("falls back to the generic mapper when no framework is supplied", () => {
|
||||
const mapper = getComplianceMapper(undefined);
|
||||
expect(detailsStubName(mapper.getDetailsComponent(fakeRequirement))).toBe(
|
||||
"GenericStub",
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the generic mapper for an unknown framework", () => {
|
||||
const mapper = getComplianceMapper("Made-Up-Framework");
|
||||
expect(detailsStubName(mapper.getDetailsComponent(fakeRequirement))).toBe(
|
||||
"GenericStub",
|
||||
);
|
||||
});
|
||||
|
||||
it("wires each registered framework to its dedicated details component", () => {
|
||||
// The keys MUST match the `framework` field the API returns
|
||||
// (case- and hyphen-sensitive).
|
||||
const wiring: Array<{ framework: string; expected: string }> = [
|
||||
{ framework: "ASD-Essential-Eight", expected: "ASDStub" },
|
||||
{ framework: "C5", expected: "C5Stub" },
|
||||
{ framework: "ENS", expected: "ENSStub" },
|
||||
{ framework: "ISO27001", expected: "ISOStub" },
|
||||
{ framework: "CIS", expected: "CISStub" },
|
||||
{
|
||||
framework: "AWS-Well-Architected-Framework-Security-Pillar",
|
||||
expected: "AWSWAStub",
|
||||
},
|
||||
{
|
||||
framework: "AWS-Well-Architected-Framework-Reliability-Pillar",
|
||||
expected: "AWSWAStub",
|
||||
},
|
||||
{ framework: "KISA-ISMS-P", expected: "KISAStub" },
|
||||
{ framework: "MITRE-ATTACK", expected: "MITREStub" },
|
||||
{ framework: "ProwlerThreatScore", expected: "ThreatStub" },
|
||||
{ framework: "CCC", expected: "CCCStub" },
|
||||
{ framework: "CSA-CCM", expected: "CSAStub" },
|
||||
];
|
||||
|
||||
for (const { framework, expected } of wiring) {
|
||||
const mapper = getComplianceMapper(framework);
|
||||
expect(
|
||||
detailsStubName(mapper.getDetailsComponent(fakeRequirement)),
|
||||
`framework "${framework}" should resolve to ${expected}`,
|
||||
).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes the four functions every consumer relies on", () => {
|
||||
const mapper = getComplianceMapper("ASD-Essential-Eight");
|
||||
expect(typeof mapper.mapComplianceData).toBe("function");
|
||||
expect(typeof mapper.toAccordionItems).toBe("function");
|
||||
expect(typeof mapper.getTopFailedSections).toBe("function");
|
||||
expect(typeof mapper.calculateCategoryHeatmapData).toBe("function");
|
||||
expect(typeof mapper.getDetailsComponent).toBe("function");
|
||||
});
|
||||
|
||||
it("returns the same reference shape for every supported framework", () => {
|
||||
// A regression sentinel: if a future entry forgets one of the five
|
||||
// functions the registry contract requires, this assertion catches
|
||||
// it before the runtime errors leak into the UI.
|
||||
const expectedKeys = [
|
||||
"mapComplianceData",
|
||||
"toAccordionItems",
|
||||
"getTopFailedSections",
|
||||
"calculateCategoryHeatmapData",
|
||||
"getDetailsComponent",
|
||||
].sort();
|
||||
|
||||
for (const framework of [
|
||||
"ASD-Essential-Eight",
|
||||
"C5",
|
||||
"ENS",
|
||||
"ISO27001",
|
||||
"CIS",
|
||||
"AWS-Well-Architected-Framework-Security-Pillar",
|
||||
"KISA-ISMS-P",
|
||||
"MITRE-ATTACK",
|
||||
"ProwlerThreatScore",
|
||||
"CCC",
|
||||
"CSA-CCM",
|
||||
]) {
|
||||
const mapper = getComplianceMapper(framework);
|
||||
expect(Object.keys(mapper).sort(), framework).toEqual(expectedKeys);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createElement, ReactNode } from "react";
|
||||
|
||||
import { ASDEssentialEightCustomDetails } from "@/components/compliance/compliance-custom-details/asd-essential-eight-details";
|
||||
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
|
||||
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
|
||||
import { CCCCustomDetails } from "@/components/compliance/compliance-custom-details/ccc-details";
|
||||
@@ -21,6 +22,10 @@ import {
|
||||
TopFailedResult,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
mapComplianceData as mapASDEssentialEightComplianceData,
|
||||
toAccordionItems as toASDEssentialEightAccordionItems,
|
||||
} from "./asd-essential-eight";
|
||||
import {
|
||||
mapComplianceData as mapAWSWellArchitectedComplianceData,
|
||||
toAccordionItems as toAWSWellArchitectedAccordionItems,
|
||||
@@ -96,6 +101,15 @@ const getDefaultMapper = (): ComplianceMapper => ({
|
||||
});
|
||||
|
||||
const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
"ASD-Essential-Eight": {
|
||||
mapComplianceData: mapASDEssentialEightComplianceData,
|
||||
toAccordionItems: toASDEssentialEightAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(ASDEssentialEightCustomDetails, { requirement }),
|
||||
},
|
||||
C5: {
|
||||
mapComplianceData: mapC5ComplianceData,
|
||||
toAccordionItems: toC5AccordionItems,
|
||||
|
||||
@@ -224,6 +224,109 @@ export interface CCCAttributesMetadata {
|
||||
}>;
|
||||
}
|
||||
|
||||
// ASD Essential Eight enums — modelled on the canonical Maturity Model
|
||||
// (Nov 2023). Only ML1 ships today; ML2/ML3 are scoped out of the framework
|
||||
// but kept here so the type covers any future expansion without a schema
|
||||
// edit. AssessmentStatus and CloudApplicability are exhaustive per the JSON
|
||||
// fixture; new variants must be added explicitly.
|
||||
export const ASD_MATURITY_LEVEL = {
|
||||
ML1: "ML1",
|
||||
ML2: "ML2",
|
||||
ML3: "ML3",
|
||||
} as const;
|
||||
export type ASDMaturityLevel =
|
||||
(typeof ASD_MATURITY_LEVEL)[keyof typeof ASD_MATURITY_LEVEL];
|
||||
|
||||
export const ASD_ASSESSMENT_STATUS = {
|
||||
AUTOMATED: "Automated",
|
||||
MANUAL: "Manual",
|
||||
} as const;
|
||||
export type ASDAssessmentStatus =
|
||||
(typeof ASD_ASSESSMENT_STATUS)[keyof typeof ASD_ASSESSMENT_STATUS];
|
||||
|
||||
export const ASD_CLOUD_APPLICABILITY = {
|
||||
FULL: "full",
|
||||
PARTIAL: "partial",
|
||||
LIMITED: "limited",
|
||||
NON_APPLICABLE: "non-applicable",
|
||||
} as const;
|
||||
export type ASDCloudApplicability =
|
||||
(typeof ASD_CLOUD_APPLICABILITY)[keyof typeof ASD_CLOUD_APPLICABILITY];
|
||||
|
||||
export interface ASDEssentialEightAttributesMetadata {
|
||||
Section: string;
|
||||
MaturityLevel: ASDMaturityLevel;
|
||||
AssessmentStatus: ASDAssessmentStatus;
|
||||
CloudApplicability: ASDCloudApplicability;
|
||||
MitigatedThreats: string[];
|
||||
Description: string;
|
||||
RationaleStatement: string;
|
||||
ImpactStatement: string;
|
||||
RemediationProcedure: string;
|
||||
AuditProcedure: string;
|
||||
AdditionalInformation: string;
|
||||
References: string;
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const isOneOf = <T extends string>(
|
||||
values: Record<string, T>,
|
||||
value: unknown,
|
||||
): value is T => (Object.values(values) as T[]).includes(value as T);
|
||||
|
||||
const isStringArray = (value: unknown): value is string[] =>
|
||||
Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
|
||||
const ASD_METADATA_STRING_FIELDS = [
|
||||
"Section",
|
||||
"Description",
|
||||
"RationaleStatement",
|
||||
"ImpactStatement",
|
||||
"RemediationProcedure",
|
||||
"AuditProcedure",
|
||||
"AdditionalInformation",
|
||||
"References",
|
||||
] as const satisfies readonly (keyof ASDEssentialEightAttributesMetadata)[];
|
||||
|
||||
export const isASDMaturityLevel = (value: unknown): value is ASDMaturityLevel =>
|
||||
isOneOf(ASD_MATURITY_LEVEL, value);
|
||||
|
||||
export const isASDAssessmentStatus = (
|
||||
value: unknown,
|
||||
): value is ASDAssessmentStatus => isOneOf(ASD_ASSESSMENT_STATUS, value);
|
||||
|
||||
export const isASDCloudApplicability = (
|
||||
value: unknown,
|
||||
): value is ASDCloudApplicability => isOneOf(ASD_CLOUD_APPLICABILITY, value);
|
||||
|
||||
export const isASDEssentialEightAttributesMetadata = (
|
||||
value: unknown,
|
||||
): value is ASDEssentialEightAttributesMetadata =>
|
||||
isRecord(value) &&
|
||||
ASD_METADATA_STRING_FIELDS.every(
|
||||
(field) => typeof value[field] === "string",
|
||||
) &&
|
||||
isASDMaturityLevel(value.MaturityLevel) &&
|
||||
isASDAssessmentStatus(value.AssessmentStatus) &&
|
||||
isASDCloudApplicability(value.CloudApplicability) &&
|
||||
isStringArray(value.MitigatedThreats);
|
||||
|
||||
export interface ASDEssentialEightRequirement extends Requirement {
|
||||
maturity_level: ASDEssentialEightAttributesMetadata["MaturityLevel"];
|
||||
assessment_status: ASDEssentialEightAttributesMetadata["AssessmentStatus"];
|
||||
cloud_applicability: ASDEssentialEightAttributesMetadata["CloudApplicability"];
|
||||
mitigated_threats: ASDEssentialEightAttributesMetadata["MitigatedThreats"];
|
||||
implementation_notes: ASDEssentialEightAttributesMetadata["Description"];
|
||||
rationale_statement: ASDEssentialEightAttributesMetadata["RationaleStatement"];
|
||||
impact_statement: ASDEssentialEightAttributesMetadata["ImpactStatement"];
|
||||
remediation_procedure: ASDEssentialEightAttributesMetadata["RemediationProcedure"];
|
||||
audit_procedure: ASDEssentialEightAttributesMetadata["AuditProcedure"];
|
||||
additional_information: ASDEssentialEightAttributesMetadata["AdditionalInformation"];
|
||||
references: ASDEssentialEightAttributesMetadata["References"];
|
||||
}
|
||||
|
||||
export interface AttributesItemData {
|
||||
type: "compliance-requirements-attributes";
|
||||
id: string;
|
||||
@@ -245,6 +348,7 @@ export interface AttributesItemData {
|
||||
| MITREAttributesMetadata[]
|
||||
| CCCAttributesMetadata[]
|
||||
| CSAAttributesMetadata[]
|
||||
| ASDEssentialEightAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
// MITRE structure
|
||||
|
||||
Reference in New Issue
Block a user