mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-04 19:21:51 +00:00
feat(compliance): add Okta IDaaS STIG V1R2 framework (#11428)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Co-authored-by: Daniel Barranquero <danielbo2001@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- DISA Okta IDaaS STIG V1R2 compliance framework support with its dedicated mapper, details panel, and icon [(#11428)](https://github.com/prowler-cloud/prowler/pull/11428)
|
||||
- DORA compliance framework support [(#11131)](https://github.com/prowler-cloud/prowler/pull/11131)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -85,7 +85,7 @@ export const ASDEssentialEightCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Maturity Level"
|
||||
value={maturityLevel}
|
||||
color="purple"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const ASDEssentialEightCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Assessment"
|
||||
value={assessmentStatus}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -101,7 +101,7 @@ export const ASDEssentialEightCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Cloud Applicability"
|
||||
value={cloudApplicability}
|
||||
color="orange"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const AWSWellArchitectedCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Question ID"
|
||||
value={requirement.well_architected_question_id as string}
|
||||
color="indigo"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const AWSWellArchitectedCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Practice ID"
|
||||
value={requirement.well_architected_practice_id as string}
|
||||
color="indigo"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -68,7 +68,7 @@ export const AWSWellArchitectedCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Assessment"
|
||||
value={requirement.assessment_method as string}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@/lib";
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import { CCC_MAPPING_SECTIONS, CCC_TEXT_SECTIONS } from "@/lib/compliance/ccc";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
@@ -46,7 +46,7 @@ export const CCCCustomDetails = ({ requirement }: CCCDetailsProps) => {
|
||||
<ComplianceBadge
|
||||
label="Family"
|
||||
value={requirement.family_name as string}
|
||||
color="purple"
|
||||
variant="secondary"
|
||||
/>
|
||||
</ComplianceBadgeContainer>
|
||||
)}
|
||||
@@ -68,15 +68,9 @@ export const CCCCustomDetails = ({ requirement }: CCCDetailsProps) => {
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mapping.Identifiers.map((identifier, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
section.colorClasses,
|
||||
)}
|
||||
>
|
||||
<Badge key={idx} variant={section.variant}>
|
||||
{identifier}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
<ComplianceBadge
|
||||
label="Profile"
|
||||
value={requirement.profile as string}
|
||||
color="purple"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
<ComplianceBadge
|
||||
label="Assessment"
|
||||
value={requirement.assessment_status as string}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cn } from "@/lib";
|
||||
import { Badge } from "@/components/shadcn/badge/badge";
|
||||
import { CSA_MAPPING_SECTIONS } from "@/lib/compliance/csa";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
@@ -36,28 +36,28 @@ export const CSACustomDetails = ({ requirement }: CSADetailsProps) => {
|
||||
<ComplianceBadge
|
||||
label="CCM Lite"
|
||||
value={requirement.ccm_lite as string}
|
||||
color={requirement.ccm_lite === "Yes" ? "green" : "gray"}
|
||||
variant={requirement.ccm_lite === "Yes" ? "success" : "secondary"}
|
||||
/>
|
||||
)}
|
||||
{requirement.iaas && (
|
||||
<ComplianceBadge
|
||||
label="IaaS"
|
||||
value={requirement.iaas as string}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
{requirement.paas && (
|
||||
<ComplianceBadge
|
||||
label="PaaS"
|
||||
value={requirement.paas as string}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
{requirement.saas && (
|
||||
<ComplianceBadge
|
||||
label="SaaS"
|
||||
value={requirement.saas as string}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
@@ -72,15 +72,9 @@ export const CSACustomDetails = ({ requirement }: CSADetailsProps) => {
|
||||
</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{mapping.Identifiers.map((identifier, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
section.colorClasses,
|
||||
)}
|
||||
>
|
||||
<Badge key={idx} variant={section.variant}>
|
||||
{identifier}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,21 +26,21 @@ export const DORACustomDetails = ({ requirement }: DORADetailsProps) => {
|
||||
<ComplianceBadge
|
||||
label="Pillar"
|
||||
value={requirement.pillar as string}
|
||||
color="blue"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
{requirement.article && (
|
||||
<ComplianceBadge
|
||||
label="Article"
|
||||
value={requirement.article as string}
|
||||
color="indigo"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
{requirement.article_title && (
|
||||
<ComplianceBadge
|
||||
label="Article Title"
|
||||
value={requirement.article_title as string}
|
||||
color="gray"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
@@ -28,7 +28,7 @@ export const ENSCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Type"
|
||||
value={translateType(requirement.type as string)}
|
||||
color="orange"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ENSCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Level"
|
||||
value={requirement.nivel as string}
|
||||
color="red"
|
||||
variant="error"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const GenericCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Item ID"
|
||||
value={requirement.item_id as string}
|
||||
color="indigo"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const GenericCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Service"
|
||||
value={requirement.service as string}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const GenericCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Type"
|
||||
value={requirement.type as string}
|
||||
color="orange"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
@@ -37,7 +37,7 @@ export const MITRECustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Technique ID"
|
||||
value={requirement.technique_id as string}
|
||||
color="indigo"
|
||||
variant="tag"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
@@ -81,17 +81,17 @@ export const MITRECustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Service"
|
||||
value={service.service}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
<ComplianceBadge
|
||||
label="Category"
|
||||
value={service.category}
|
||||
color="indigo"
|
||||
variant="tag"
|
||||
/>
|
||||
<ComplianceBadge
|
||||
label="Coverage"
|
||||
value={service.value}
|
||||
color="orange"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
{service.comment && (
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Severity, SeverityBadge } from "@/components/ui/table";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceChipContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
} from "./shared-components";
|
||||
|
||||
export const OktaIDaaSStigCustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
const severity = requirement.severity as string | undefined;
|
||||
const stigId = requirement.stig_id as string | undefined;
|
||||
const ruleId = requirement.rule_id as string | undefined;
|
||||
const cci = requirement.cci as string[] | undefined;
|
||||
const checkText = requirement.check_text as string | undefined;
|
||||
const fixText = requirement.fix_text as string | undefined;
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
<ComplianceBadgeContainer>
|
||||
{severity && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm font-medium">
|
||||
Severity:
|
||||
</span>
|
||||
<SeverityBadge severity={severity.toLowerCase() as Severity} />
|
||||
</div>
|
||||
)}
|
||||
{stigId && (
|
||||
<ComplianceBadge label="STIG ID" value={stigId} variant="tag" />
|
||||
)}
|
||||
{ruleId && (
|
||||
<ComplianceBadge label="Rule ID" value={ruleId} variant="tag" />
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceChipContainer title="CCI" items={cci || []} />
|
||||
|
||||
{checkText && (
|
||||
<ComplianceDetailSection title="Check">
|
||||
<ComplianceDetailText>{checkText}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{fixText && (
|
||||
<ComplianceDetailSection title="Fix">
|
||||
<ComplianceDetailText>{fixText}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,12 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { VariantProps } from "class-variance-authority";
|
||||
|
||||
import { Badge, badgeVariants } from "@/components/shadcn/badge/badge";
|
||||
|
||||
// Variants come straight from the canonical shadcn Badge so compliance panels
|
||||
// share the same badge vocabulary (and tokens) as the rest of the app.
|
||||
export type ComplianceBadgeVariant = NonNullable<
|
||||
VariantProps<typeof badgeVariants>["variant"]
|
||||
>;
|
||||
|
||||
export const ComplianceDetailContainer = ({
|
||||
children,
|
||||
@@ -43,55 +51,28 @@ export const ComplianceBadgeContainer = ({
|
||||
return <div className="flex flex-wrap items-center gap-3">{children}</div>;
|
||||
};
|
||||
|
||||
type BadgeColor =
|
||||
| "red" // Risk/Level/Severity
|
||||
| "blue" // Assessment/Method
|
||||
| "orange" // Type/Category
|
||||
| "green" // Weight/Score (positive)
|
||||
| "purple" // Profile
|
||||
| "indigo" // IDs/References
|
||||
| "gray"; // Additional Info/Neutral
|
||||
|
||||
export const ComplianceBadge = ({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
variant,
|
||||
conditional = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
color: BadgeColor;
|
||||
variant: ComplianceBadgeVariant;
|
||||
conditional?: boolean;
|
||||
}) => {
|
||||
const actualColor = conditional && Number(value) === 0 ? "gray" : color;
|
||||
|
||||
const colorClasses = {
|
||||
red: "bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20",
|
||||
blue: "bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
|
||||
orange:
|
||||
"bg-orange-50 text-orange-700 ring-orange-600/10 dark:bg-orange-400/10 dark:text-orange-400 dark:ring-orange-400/20",
|
||||
green:
|
||||
"bg-green-50 text-green-700 ring-green-600/10 dark:bg-green-400/10 dark:text-green-400 dark:ring-green-400/20",
|
||||
purple:
|
||||
"bg-purple-50 text-purple-700 ring-purple-600/10 dark:bg-purple-400/10 dark:text-purple-400 dark:ring-purple-400/20",
|
||||
indigo:
|
||||
"bg-indigo-50 text-indigo-700 ring-indigo-600/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:ring-indigo-400/20",
|
||||
gray: "bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20",
|
||||
};
|
||||
// A "conditional" metric badge with a zero value drops to a neutral variant
|
||||
// so empty scores don't read as a meaningful (e.g. positive) result.
|
||||
const actualVariant: ComplianceBadgeVariant =
|
||||
conditional && Number(value) === 0 ? "secondary" : variant;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-sm font-medium">
|
||||
{label}:
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset",
|
||||
colorClasses[actualColor],
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
<Badge variant={actualVariant}>{value}</Badge>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -132,12 +113,9 @@ export const ComplianceChipContainer = ({
|
||||
<ComplianceDetailSection title={title}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{items.map((item: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-gray-500/10 ring-inset dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20"
|
||||
>
|
||||
<Badge key={index} variant="tag">
|
||||
{item}
|
||||
</span>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ThreatCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Risk Level"
|
||||
value={requirement.levelOfRisk}
|
||||
color="red"
|
||||
variant="error"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ThreatCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Weight"
|
||||
value={requirement.weight}
|
||||
color="green"
|
||||
variant="success"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const ThreatCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Score"
|
||||
value={requirement.score}
|
||||
color="green"
|
||||
variant="success"
|
||||
conditional={true}
|
||||
/>
|
||||
)}
|
||||
@@ -61,13 +61,13 @@ export const ThreatCustomDetails = ({
|
||||
<ComplianceBadge
|
||||
label="Findings"
|
||||
value={`${requirement.passedFindings}/${requirement.totalFindings}`}
|
||||
color="blue"
|
||||
variant="info"
|
||||
/>
|
||||
{requirement.totalFindings > 0 && (
|
||||
<ComplianceBadge
|
||||
label="Pass Rate"
|
||||
value={`${Math.round((requirement.passedFindings / requirement.totalFindings) * 100)}%`}
|
||||
color="green"
|
||||
variant="success"
|
||||
conditional={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -58,6 +58,12 @@ describe("getComplianceIcon", () => {
|
||||
expect(getComplianceIcon("prowler_threatscore_aws")).toBe(threatLogo);
|
||||
});
|
||||
|
||||
it("resolves the Okta IDaaS STIG via the `okta` keyword", () => {
|
||||
const oktaLogo = getComplianceIcon("Okta-IDaaS-STIG");
|
||||
expect(oktaLogo).toBeDefined();
|
||||
expect(getComplianceIcon("okta_idaas_stig_v1r2_okta")).toBe(oktaLogo);
|
||||
});
|
||||
|
||||
it("resolves ASD Essential Eight by the framework keyword, not by `aws`", () => {
|
||||
const essentialLogo = getComplianceIcon("ASD-Essential-Eight");
|
||||
expect(essentialLogo).toBeDefined();
|
||||
|
||||
@@ -18,6 +18,7 @@ import KISALogo from "./kisa.svg";
|
||||
import MITRELogo from "./mitre-attack.svg";
|
||||
import NIS2Logo from "./nis2.svg";
|
||||
import NISTLogo from "./nist.svg";
|
||||
import OktaLogo from "./okta.svg";
|
||||
import PCILogo from "./pci-dss.svg";
|
||||
import PROWLERTHREATLogo from "./prowlerThreat.svg";
|
||||
import RBILogo from "./rbi.svg";
|
||||
@@ -72,6 +73,7 @@ const COMPLIANCE_LOGOS = [
|
||||
// compliance_id is just `dora`, no provider suffix.
|
||||
["dora", DORALogo],
|
||||
["secnumcloud", ANSSILogo],
|
||||
["okta", OktaLogo],
|
||||
["aws", AWSLogo],
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 63 63" width="40" height="40"><path fill="#00297A" d="M34.6,0.4l-1.3,16c-0.6-0.1-1.2-0.1-1.9-0.1c-0.8,0-1.6,0.1-2.3,0.2l-0.7-7.7c0-0.2,0.2-0.5,0.4-0.5h1.3l-0.6-7.8c0-0.2,0.2-0.5,0.4-0.5h4.3C34.5,0,34.7,0.2,34.6,0.4L34.6,0.4L34.6,0.4z M23.8,1.2c-0.1-0.2-0.3-0.4-0.5-0.3l-4,1.5C19,2.5,18.9,2.8,19,3l3.3,7.1l-1.2,0.5c-0.2,0.1-0.3,0.3-0.2,0.6l3.3,7c1.2-0.7,2.5-1.2,3.9-1.5L23.8,1.2L23.8,1.2z M14,5.7l9.3,13.1c-1.2,0.8-2.2,1.7-3.1,2.7L14.5,16c-0.2-0.2-0.2-0.5,0-0.6l1-0.8L10,9c-0.2-0.2-0.2-0.5,0-0.6l3.3-2.7C13.5,5.4,13.8,5.5,14,5.7L14,5.7z M6.2,13.2c-0.2-0.1-0.5-0.1-0.6,0.1l-2.1,3.7c-0.1,0.2,0,0.5,0.2,0.6l7.1,3.4l-0.7,1.1c-0.1,0.2,0,0.5,0.2,0.6l7.1,3.2c0.5-1.3,1.2-2.5,2-3.6L6.2,13.2z M0.9,23.3c0-0.2,0.3-0.4,0.5-0.3l15.5,4c-0.4,1.3-0.6,2.7-0.7,4.1l-7.8-0.6c-0.2,0-0.4-0.2-0.4-0.5l0.2-1.3L0.6,28c-0.2,0-0.4-0.2-0.4-0.5L0.9,23.3L0.9,23.3L0.9,23.3z M0.4,33.8C0.1,33.8,0,34,0,34.3l0.8,4.2c0,0.2,0.3,0.4,0.5,0.3l7.6-2l0.2,1.3c0,0.2,0.3,0.4,0.5,0.3l7.5-2.1c-0.4-1.3-0.7-2.7-0.8-4.1L0.4,33.8L0.4,33.8z M2.9,44.9c-0.1-0.2,0-0.5,0.2-0.6l14.5-6.9c0.5,1.3,1.3,2.5,2.2,3.6l-6.3,4.5c-0.2,0.1-0.5,0.1-0.6-0.1L12,44.3l-6.5,4.5c-0.2,0.1-0.5,0.1-0.6-0.1L2.9,44.9L2.9,44.9z M20.4,41.9L9.1,53.3c-0.2,0.2-0.2,0.5,0,0.6l3.3,2.7c0.2,0.2,0.5,0.1,0.6-0.1l4.6-6.4l1,0.9c0.2,0.2,0.5,0.1,0.6-0.1l4.4-6.4C22.4,43.8,21.3,42.9,20.4,41.9L20.4,41.9z M18.2,60.1c-0.2-0.1-0.3-0.3-0.2-0.6L24.6,45c1.2,0.6,2.6,1.1,3.9,1.4l-2,7.5c-0.1,0.2-0.3,0.4-0.5,0.3l-1.2-0.5l-2.1,7.6c-0.1,0.2-0.3,0.4-0.5,0.3L18.2,60.1L18.2,60.1L18.2,60.1z M29.6,46.6l-1.3,16c0,0.2,0.2,0.5,0.4,0.5H33c0.2,0,0.4-0.2,0.4-0.5l-0.6-7.8h1.3c0.2,0,0.4-0.2,0.4-0.5l-0.7-7.7c-0.8,0.1-1.5,0.2-2.3,0.2C30.9,46.7,30.2,46.7,29.6,46.6L29.6,46.6z M45.1,3.4c0.1-0.2,0-0.5-0.2-0.6l-4-1.5c-0.2-0.1-0.5,0.1-0.5,0.3l-2.1,7.6l-1.2-0.5c-0.2-0.1-0.5,0.1-0.5,0.3l-2,7.5c1.4,0.3,2.7,0.8,3.9,1.4L45.1,3.4L45.1,3.4z M53.9,9.7L42.6,21.1c-0.9-1-2-1.9-3.2-2.6l4.4-6.4c0.1-0.2,0.4-0.2,0.6-0.1l1,0.9l4.6-6.4c0.1-0.2,0.4-0.2,0.6-0.1l3.3,2.7C54,9.3,54,9.6,53.9,9.7L53.9,9.7z M59.9,18.7c0.2-0.1,0.3-0.4,0.2-0.6L58,14.4c-0.1-0.2-0.4-0.3-0.6-0.1l-6.5,4.5l-0.7-1.1c-0.1-0.2-0.4-0.3-0.6-0.1L43.3,22c0.9,1.1,1.6,2.3,2.2,3.6L59.9,18.7L59.9,18.7z M62.2,24.5l0.7,4.2c0,0.2-0.1,0.5-0.4,0.5l-15.9,1.5c-0.1-1.4-0.4-2.8-0.8-4.1l7.5-2.1c0.2-0.1,0.5,0.1,0.5,0.3l0.2,1.3l7.6-2C61.9,24.1,62.1,24.3,62.2,24.5L62.2,24.5L62.2,24.5z M61.5,40c0.2,0.1,0.5-0.1,0.5-0.3l0.7-4.2c0-0.2-0.1-0.5-0.4-0.5l-7.8-0.7l0.2-1.3c0-0.2-0.1-0.5-0.4-0.5l-7.8-0.6c0,1.4-0.3,2.8-0.7,4.1L61.5,40L61.5,40L61.5,40z M57.4,49.6c-0.1,0.2-0.4,0.3-0.6,0.1l-13.2-9.1c0.8-1.1,1.5-2.3,2-3.6l7.1,3.2c0.2,0.1,0.3,0.4,0.2,0.6L52.2,42l7.1,3.4c0.2,0.1,0.3,0.4,0.2,0.6L57.4,49.6C57.4,49.6,57.4,49.6,57.4,49.6z M39.7,44.2L49,57.3c0.1,0.2,0.4,0.2,0.6,0.1l3.3-2.7c0.2-0.2,0.2-0.4,0-0.6l-5.5-5.6l1-0.8c0.2-0.2,0.2-0.4,0-0.6l-5.5-5.5C42,42.6,40.9,43.5,39.7,44.2L39.7,44.2L39.7,44.2z M39.7,62c-0.2,0.1-0.5-0.1-0.5-0.3l-4.2-15.4c1.4-0.3,2.7-0.8,3.9-1.5l3.3,7c0.1,0.2,0,0.5-0.2,0.6l-1.2,0.5l3.3,7.1c0.1,0.2,0,0.5-0.2,0.6L39.7,62L39.7,62L39.7,62z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,30 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Badge } from "./badge";
|
||||
|
||||
describe("Badge", () => {
|
||||
it("renders its children", () => {
|
||||
render(<Badge variant="info">Assessment</Badge>);
|
||||
expect(screen.getByText("Assessment")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies the info variant token classes", () => {
|
||||
const { container } = render(<Badge variant="info">Info</Badge>);
|
||||
const badge = container.querySelector("[data-slot='badge']");
|
||||
// The info variant is built from the existing design-system blue token
|
||||
// (bg-data-info) rather than a bespoke palette.
|
||||
expect(badge?.className).toContain("bg-bg-data-info/15");
|
||||
expect(badge?.className).toContain("text-bg-data-info");
|
||||
});
|
||||
|
||||
it("merges a custom className", () => {
|
||||
const { container } = render(
|
||||
<Badge variant="tag" className="extra-class">
|
||||
Tag
|
||||
</Badge>,
|
||||
);
|
||||
const badge = container.querySelector("[data-slot='badge']");
|
||||
expect(badge?.className).toContain("extra-class");
|
||||
});
|
||||
});
|
||||
@@ -24,6 +24,7 @@ const badgeVariants = cva(
|
||||
"border-bg-warning/30 bg-bg-warning-secondary/20 text-text-warning-primary",
|
||||
error:
|
||||
"border-transparent bg-bg-fail-secondary text-text-error-primary",
|
||||
info: "border-transparent bg-bg-data-info/15 text-bg-data-info",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { ComplianceBadgeVariant } from "@/components/compliance/compliance-custom-details/shared-components";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
@@ -39,7 +40,7 @@ export interface CCCTextSection {
|
||||
export interface CCCMappingSection {
|
||||
title: string;
|
||||
key: keyof Requirement;
|
||||
colorClasses: string;
|
||||
variant: ComplianceBadgeVariant;
|
||||
}
|
||||
|
||||
export const CCC_TEXT_SECTIONS: CCCTextSection[] = [
|
||||
@@ -71,14 +72,12 @@ export const CCC_MAPPING_SECTIONS: CCCMappingSection[] = [
|
||||
{
|
||||
title: "Threat Mappings",
|
||||
key: "section_threat_mappings",
|
||||
colorClasses:
|
||||
"bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20",
|
||||
variant: "error",
|
||||
},
|
||||
{
|
||||
title: "Guideline Mappings",
|
||||
key: "section_guideline_mappings",
|
||||
colorClasses:
|
||||
"bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
|
||||
variant: "info",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/mitre-details",
|
||||
() => ({ MITRECustomDetails: stubFactory("MITREStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/okta-idaas-stig-details",
|
||||
() => ({ OktaIDaaSStigCustomDetails: stubFactory("OktaIDaaSStigStub") }),
|
||||
);
|
||||
vi.mock(
|
||||
"@/components/compliance/compliance-custom-details/threat-details",
|
||||
() => ({ ThreatCustomDetails: stubFactory("ThreatStub") }),
|
||||
@@ -144,6 +148,7 @@ describe("getComplianceMapper", () => {
|
||||
{ framework: "ProwlerThreatScore", expected: "ThreatStub" },
|
||||
{ framework: "CCC", expected: "CCCStub" },
|
||||
{ framework: "CSA-CCM", expected: "CSAStub" },
|
||||
{ framework: "Okta-IDaaS-STIG", expected: "OktaIDaaSStigStub" },
|
||||
];
|
||||
|
||||
for (const { framework, expected } of wiring) {
|
||||
@@ -188,6 +193,7 @@ describe("getComplianceMapper", () => {
|
||||
"ProwlerThreatScore",
|
||||
"CCC",
|
||||
"CSA-CCM",
|
||||
"Okta-IDaaS-STIG",
|
||||
]) {
|
||||
const mapper = getComplianceMapper(framework);
|
||||
expect(Object.keys(mapper).sort(), framework).toEqual(expectedKeys);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { GenericCustomDetails } from "@/components/compliance/compliance-custom-
|
||||
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
|
||||
import { KISACustomDetails } from "@/components/compliance/compliance-custom-details/kisa-details";
|
||||
import { MITRECustomDetails } from "@/components/compliance/compliance-custom-details/mitre-details";
|
||||
import { OktaIDaaSStigCustomDetails } from "@/components/compliance/compliance-custom-details/okta-idaas-stig-details";
|
||||
import { ThreatCustomDetails } from "@/components/compliance/compliance-custom-details/threat-details";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import {
|
||||
@@ -74,6 +75,10 @@ import {
|
||||
mapComplianceData as mapMITREComplianceData,
|
||||
toAccordionItems as toMITREAccordionItems,
|
||||
} from "./mitre";
|
||||
import {
|
||||
mapComplianceData as mapOktaIDaaSStigComplianceData,
|
||||
toAccordionItems as toOktaIDaaSStigAccordionItems,
|
||||
} from "./okta-idaas-stig";
|
||||
import {
|
||||
getTopFailedSections as getThreatScoreTopFailedSections,
|
||||
mapComplianceData as mapThetaComplianceData,
|
||||
@@ -213,6 +218,15 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(CSACustomDetails, { requirement }),
|
||||
},
|
||||
"Okta-IDaaS-STIG": {
|
||||
mapComplianceData: mapOktaIDaaSStigComplianceData,
|
||||
toAccordionItems: toOktaIDaaSStigAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
createElement(OktaIDaaSStigCustomDetails, { requirement }),
|
||||
},
|
||||
// DORA (Regulation (EU) 2022/2554) — universal framework keyed by the
|
||||
// `framework` field of `prowler/compliance/dora.json` ("DORA"). Groups by
|
||||
// Pillar (5 enum values) and surfaces Pillar / Article / ArticleTitle in
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { ComplianceBadgeVariant } from "@/components/compliance/compliance-custom-details/shared-components";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
@@ -24,15 +25,14 @@ import {
|
||||
export interface CSAMappingSection {
|
||||
title: string;
|
||||
key: keyof Requirement;
|
||||
colorClasses: string;
|
||||
variant: ComplianceBadgeVariant;
|
||||
}
|
||||
|
||||
export const CSA_MAPPING_SECTIONS: CSAMappingSection[] = [
|
||||
{
|
||||
title: "Scope Applicability",
|
||||
key: "scope_applicability",
|
||||
colorClasses:
|
||||
"bg-blue-50 text-blue-700 ring-blue-600/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/20",
|
||||
variant: "info",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
Control,
|
||||
Framework,
|
||||
isOktaIDaaSStigAttributesMetadata,
|
||||
OktaIDaaSStigRequirement,
|
||||
Requirement,
|
||||
REQUIREMENT_STATUS,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
calculateFrameworkCounters,
|
||||
createRequirementsMap,
|
||||
findOrCreateCategory,
|
||||
findOrCreateControl,
|
||||
findOrCreateFramework,
|
||||
} from "./commons";
|
||||
|
||||
const getStatusCounters = (status: RequirementStatus) => ({
|
||||
pass: status === REQUIREMENT_STATUS.PASS ? 1 : 0,
|
||||
fail: status === REQUIREMENT_STATUS.FAIL ? 1 : 0,
|
||||
manual: status === REQUIREMENT_STATUS.MANUAL ? 1 : 0,
|
||||
});
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirementsMap = createRequirementsMap(requirementsData);
|
||||
const frameworks: Framework[] = [];
|
||||
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes?.metadata;
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!isOktaIDaaSStigAttributesMetadata(attrs)) continue;
|
||||
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
// Level 1: Section maps to the STIG severity category (e.g. "CAT II (Medium)")
|
||||
const categoryName = attrs.Section;
|
||||
// Level 2: each requirement is its own control, labelled by its STIG ID
|
||||
const controlLabel = id;
|
||||
const description = attributeItem.attributes.description;
|
||||
// Human-readable STIG title (e.g. "Okta must log out a session after a
|
||||
// 15-minute period of inactivity."). Surface it next to the STIG ID and
|
||||
// fall back to the bare ID when missing, mirroring DORA/CSA.
|
||||
const requirementName = attributeItem.attributes.name || "";
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
|
||||
const framework = findOrCreateFramework(frameworks, frameworkName);
|
||||
const category = findOrCreateCategory(framework.categories, categoryName);
|
||||
const control = findOrCreateControl(category.controls, controlLabel);
|
||||
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
const requirement = {
|
||||
name: requirementName ? `${id} - ${requirementName}` : id,
|
||||
description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
...getStatusCounters(finalStatus),
|
||||
severity: attrs.Severity,
|
||||
rule_id: attrs.RuleID,
|
||||
stig_id: attrs.StigID,
|
||||
cci: attrs.CCI,
|
||||
check_text: attrs.CheckText,
|
||||
fix_text: attrs.FixText,
|
||||
} satisfies OktaIDaaSStigRequirement;
|
||||
|
||||
control.requirements.push(requirement);
|
||||
}
|
||||
|
||||
calculateFrameworkCounters(frameworks);
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
const createRequirementItem = (
|
||||
requirement: Requirement,
|
||||
frameworkName: string,
|
||||
categoryName: string,
|
||||
controlIndex: number,
|
||||
scanId: string,
|
||||
): AccordionItemProps => ({
|
||||
key: `${frameworkName}-${categoryName}-control-${controlIndex}`,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
key={`content-${frameworkName}-${categoryName}-control-${controlIndex}`}
|
||||
requirement={requirement}
|
||||
scanId={scanId}
|
||||
framework={frameworkName}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
});
|
||||
|
||||
const createControlItem = (
|
||||
control: Control,
|
||||
frameworkName: string,
|
||||
categoryName: string,
|
||||
controlIndex: number,
|
||||
scanId: string,
|
||||
): AccordionItemProps =>
|
||||
createRequirementItem(
|
||||
control.requirements[0],
|
||||
frameworkName,
|
||||
categoryName,
|
||||
controlIndex,
|
||||
scanId,
|
||||
);
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
const safeId = scanId || "";
|
||||
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => ({
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.map((control, controlIndex) =>
|
||||
createControlItem(
|
||||
control,
|
||||
framework.name,
|
||||
category.name,
|
||||
controlIndex,
|
||||
safeId,
|
||||
),
|
||||
),
|
||||
})),
|
||||
);
|
||||
};
|
||||
@@ -279,6 +279,12 @@ const isOneOf = <T extends string>(
|
||||
const isStringArray = (value: unknown): value is string[] =>
|
||||
Array.isArray(value) && value.every((item) => typeof item === "string");
|
||||
|
||||
const isOptionalString = (value: unknown): value is string | undefined =>
|
||||
value === undefined || typeof value === "string";
|
||||
|
||||
const isOptionalStringArray = (value: unknown): value is string[] | undefined =>
|
||||
value === undefined || isStringArray(value);
|
||||
|
||||
const ASD_METADATA_STRING_FIELDS = [
|
||||
"Section",
|
||||
"Description",
|
||||
@@ -327,6 +333,43 @@ export interface ASDEssentialEightRequirement extends Requirement {
|
||||
references: ASDEssentialEightAttributesMetadata["References"];
|
||||
}
|
||||
|
||||
export interface OktaIDaaSStigAttributesMetadata {
|
||||
Section: string;
|
||||
Severity: string;
|
||||
RuleID: string;
|
||||
StigID: string;
|
||||
CCI?: string[];
|
||||
CheckText?: string;
|
||||
FixText?: string;
|
||||
}
|
||||
|
||||
const OKTA_IDAAS_STIG_REQUIRED_STRING_FIELDS = [
|
||||
"Section",
|
||||
"Severity",
|
||||
"RuleID",
|
||||
"StigID",
|
||||
] as const satisfies readonly (keyof OktaIDaaSStigAttributesMetadata)[];
|
||||
|
||||
export const isOktaIDaaSStigAttributesMetadata = (
|
||||
value: unknown,
|
||||
): value is OktaIDaaSStigAttributesMetadata =>
|
||||
isRecord(value) &&
|
||||
OKTA_IDAAS_STIG_REQUIRED_STRING_FIELDS.every(
|
||||
(field) => typeof value[field] === "string",
|
||||
) &&
|
||||
isOptionalStringArray(value.CCI) &&
|
||||
isOptionalString(value.CheckText) &&
|
||||
isOptionalString(value.FixText);
|
||||
|
||||
export interface OktaIDaaSStigRequirement extends Requirement {
|
||||
severity: OktaIDaaSStigAttributesMetadata["Severity"];
|
||||
rule_id: OktaIDaaSStigAttributesMetadata["RuleID"];
|
||||
stig_id: OktaIDaaSStigAttributesMetadata["StigID"];
|
||||
cci: OktaIDaaSStigAttributesMetadata["CCI"];
|
||||
check_text: OktaIDaaSStigAttributesMetadata["CheckText"];
|
||||
fix_text: OktaIDaaSStigAttributesMetadata["FixText"];
|
||||
}
|
||||
|
||||
// DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554).
|
||||
// Universal framework — flat attributes dict with Pillar/Article/ArticleTitle.
|
||||
// `Pillar` is the canonical grouping key for tables and PDF; the enum mirrors
|
||||
@@ -374,6 +417,7 @@ export interface AttributesItemData {
|
||||
| CCCAttributesMetadata[]
|
||||
| CSAAttributesMetadata[]
|
||||
| ASDEssentialEightAttributesMetadata[]
|
||||
| OktaIDaaSStigAttributesMetadata[]
|
||||
| DORAAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
|
||||
Reference in New Issue
Block a user