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:
Pedro Martín
2026-06-10 11:22:42 +02:00
committed by GitHub
parent 01b49f0743
commit 61cd4aea3f
39 changed files with 1521 additions and 96 deletions
+1
View File
@@ -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;
+1
View File
@@ -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

+30
View File
@@ -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");
});
});
+1
View File
@@ -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: {
+4 -5
View File
@@ -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);
+14
View File
@@ -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
+3 -3
View File
@@ -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",
},
];
+163
View File
@@ -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,
),
),
})),
);
};
+44
View File
@@ -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[];