feat: ThreatScore compliance detailed view (#7979)

This commit is contained in:
Alejandro Bailo
2025-06-10 10:43:27 +02:00
committed by GitHub
parent e24b211d22
commit c119cece89
13 changed files with 734 additions and 273 deletions
+2 -1
View File
@@ -20,7 +20,8 @@ All notable changes to the **Prowler UI** are documented in this file.
- Add compliance detail view: CIS [(#7913)](https://github.com/prowler-cloud/prowler/pull/7913)
- Add compliance detail view: AWS Well-Architected Framework [(#7925)](https://github.com/prowler-cloud/prowler/pull/7925)
- Add compliance detail view: KISA [(#7965)](https://github.com/prowler-cloud/prowler/pull/7965)
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7949)](https://github.com/prowler-cloud/prowler/pull/7949)
- Add compliance detail view: ProwlerThreatScore [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966)
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7979)](https://github.com/prowler-cloud/prowler/pull/7979)
### 🔄 Changed
@@ -125,8 +125,13 @@ export const ClientAccordionContent = ({
const checks = requirement.check_ids || [];
const checksList = (
<div className="mb-2 flex items-center">
<span>{checks.join(", ")}</span>
<div className="flex items-center px-2 text-sm">
<div className="w-full flex-col">
<div className="mb-1 mt-[-8px] h-1 w-full border-b border-gray-200 dark:border-gray-800" />
<span className="text-gray-600 dark:text-gray-200" aria-label="Checks">
{checks.join(", ")}
</span>
</div>
</div>
);
@@ -150,7 +155,9 @@ export const ClientAccordionContent = ({
if (findings?.data?.length && findings.data.length > 0) {
return (
<div className="p-1">
<>
<h4 className="mb-2 text-sm font-medium">Findings</h4>
<DataTable
// Remove the updated_at column as compliance is for the last scan
columns={ColumnFindings.filter(
@@ -160,7 +167,7 @@ export const ClientAccordionContent = ({
metadata={findings?.meta}
disableScroll={true}
/>
</div>
</>
);
}
@@ -176,12 +183,12 @@ export const ClientAccordionContent = ({
{renderDetails()}
{checks.length > 0 && (
<div className="mb-2 mt-2">
<div className="my-4">
<Accordion
items={accordionChecksItems}
variant="light"
defaultExpandedKeys={[""]}
className="rounded-lg bg-white dark:bg-prowler-blue-400"
className="rounded-lg bg-gray-50 dark:bg-prowler-blue-400"
/>
</div>
)}
@@ -76,6 +76,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
id.includes("iso") ||
id.includes("cis_") ||
id.includes("kisa") ||
id.includes("threat") ||
id.includes("pillar");
const navigateToDetail = () => {
@@ -1,91 +1,87 @@
import Link from "next/link";
import { SeverityBadge } from "@/components/ui/table";
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
ComplianceLink,
} from "./shared-components";
export const AWSWellArchitectedCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<div className="space-y-4">
<ComplianceDetailContainer>
{requirement.description && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Description
</h4>
<p className="text-sm">{requirement.description}</p>
</div>
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.well_architected_name && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Best Practice
</h4>
<p className="text-sm">{requirement.well_architected_name}</p>
</div>
<ComplianceDetailSection title="Best Practice">
<ComplianceDetailText>
{requirement.well_architected_name as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.well_architected_question_id && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Question ID
</h4>
<p className="text-sm">{requirement.well_architected_question_id}</p>
</div>
)}
<ComplianceBadgeContainer>
{requirement.level_of_risk && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-sm font-medium">
Risk Level:
</span>
<SeverityBadge
severity={
requirement.level_of_risk.toString().toLowerCase() as
| "low"
| "medium"
| "high"
}
/>
</div>
)}
{requirement.well_architected_practice_id && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Practice ID
</h4>
<p className="text-sm">{requirement.well_architected_practice_id}</p>
</div>
)}
{requirement.level_of_risk && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Level of Risk
</h4>
<SeverityBadge
severity={
requirement.level_of_risk.toString().toLowerCase() as
| "low"
| "medium"
| "high"
}
{requirement.well_architected_question_id && (
<ComplianceBadge
label="Question ID"
value={requirement.well_architected_question_id as string}
color="indigo"
/>
</div>
)}
)}
{requirement.assessment_method && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Assessment Method
</h4>
<p className="text-sm">{requirement.assessment_method}</p>
</div>
)}
{requirement.well_architected_practice_id && (
<ComplianceBadge
label="Practice ID"
value={requirement.well_architected_practice_id as string}
color="indigo"
/>
)}
{requirement.assessment_method && (
<ComplianceBadge
label="Assessment"
value={requirement.assessment_method as string}
color="blue"
/>
)}
</ComplianceBadgeContainer>
{requirement.implementation_guidance_url && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Implementation Guidance
</h4>
<Link
<ComplianceDetailSection title="Implementation Guidance">
<ComplianceLink
href={requirement.implementation_guidance_url as string}
target="_blank"
rel="noopener noreferrer"
className="break-all text-sm text-blue-600 underline hover:text-blue-800"
>
{requirement.implementation_guidance_url}
</Link>
</div>
</ComplianceLink>
</ComplianceDetailSection>
)}
</div>
</ComplianceDetailContainer>
);
};
@@ -1,8 +1,16 @@
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
ComplianceLink,
} from "./shared-components";
interface CISDetailsProps {
requirement: Requirement;
}
@@ -21,131 +29,105 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
};
return (
<div className="space-y-4">
{requirement.profile && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Profile Level
</h4>
<p className="text-sm">{requirement.profile}</p>
</div>
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{requirement.profile && (
<ComplianceBadge
label="Profile"
value={requirement.profile as string}
color="purple"
/>
)}
{requirement.assessment_status && (
<ComplianceBadge
label="Assessment"
value={requirement.assessment_status as string}
color="blue"
/>
)}
</ComplianceBadgeContainer>
{requirement.subsection && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
SubSection
</h4>
<p className="text-sm">{requirement.subsection}</p>
</div>
)}
{requirement.assessment_status && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Assessment Status
</h4>
<p className="text-sm">{requirement.assessment_status}</p>
</div>
)}
{requirement.description && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Description
</h4>
<p className="text-sm">{requirement.description}</p>
</div>
<ComplianceDetailSection title="SubSection">
<ComplianceDetailText>
{requirement.subsection as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.rationale_statement && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Rationale Statement
</h4>
<p className="text-sm">{requirement.rationale_statement}</p>
</div>
<ComplianceDetailSection title="Rationale Statement">
<ComplianceDetailText>
{requirement.rationale_statement as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.impact_statement && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Impact Statement
</h4>
<p className="text-sm">{requirement.impact_statement}</p>
</div>
<ComplianceDetailSection title="Impact Statement">
<ComplianceDetailText>
{requirement.impact_statement as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.remediation_procedure &&
typeof requirement.remediation_procedure === "string" && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Remediation Procedure
</h4>
<ComplianceDetailSection title="Remediation Procedure">
{/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */}
{/* eslint-disable-next-line */}
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{requirement.remediation_procedure}</ReactMarkdown>
</div>
</div>
</ComplianceDetailSection>
)}
{requirement.audit_procedure &&
typeof requirement.audit_procedure === "string" && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Audit Procedure
</h4>
<ComplianceDetailSection title="Audit Procedure">
{/* eslint-disable-next-line */}
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{requirement.audit_procedure}</ReactMarkdown>
</div>
</div>
</ComplianceDetailSection>
)}
{requirement.additional_information && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Additional Information
</h4>
<p className="whitespace-pre-wrap text-sm">
{requirement.additional_information}
</p>
</div>
<ComplianceDetailSection title="Additional Information">
<ComplianceDetailText className="whitespace-pre-wrap">
{requirement.additional_information as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.default_value && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Default Value
</h4>
<p className="text-sm">{requirement.default_value}</p>
</div>
<ComplianceDetailSection title="Default Value">
<ComplianceDetailText>
{requirement.default_value as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.references && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
References
</h4>
<div className="text-sm">
<ComplianceDetailSection title="References">
<div className="space-y-1">
{processReferences(requirement.references).map(
(url: string, index: number) => (
<div key={index}>
<Link
href={url}
target="_blank"
rel="noopener noreferrer"
className="break-all text-blue-600 underline hover:text-blue-800"
>
{url}
</Link>
<ComplianceLink href={url}>{url}</ComplianceLink>
</div>
),
)}
</div>
</div>
</ComplianceDetailSection>
)}
</div>
</ComplianceDetailContainer>
);
};
@@ -1,47 +1,50 @@
import { translateType } from "@/lib/compliance/ens";
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceChipContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
export const ENSCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<div className="mb-4">
<div className="mb-2 text-sm text-gray-600">
{requirement.description}
</div>
<div className="flex flex-col gap-2 text-sm">
<div className="flex items-center gap-2">
<span className="font-medium">Type:</span>
<span className="capitalize">
{translateType(requirement.type as string)}
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">Level:</span>
<span className="capitalize">{requirement.nivel}</span>
</div>
{requirement.dimensiones &&
Array.isArray(requirement.dimensiones) &&
requirement.dimensiones.length > 0 && (
<div className="flex items-center gap-2">
<span className="font-medium">Dimensions:</span>
<div className="flex flex-wrap gap-1">
{requirement.dimensiones.map(
(dimension: string, index: number) => (
<span
key={index}
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
>
{dimension}
</span>
),
)}
</div>
</div>
)}
</div>
</div>
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{requirement.type && (
<ComplianceBadge
label="Type"
value={translateType(requirement.type as string)}
color="orange"
/>
)}
{requirement.nivel && (
<ComplianceBadge
label="Level"
value={requirement.nivel as string}
color="red"
/>
)}
</ComplianceBadgeContainer>
<ComplianceChipContainer
title="Dimensions"
items={(requirement.dimensiones as string[]) || []}
/>
</ComplianceDetailContainer>
);
};
@@ -1,23 +1,31 @@
import { Requirement } from "@/types/compliance";
import {
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
export const ISOCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<div className="mb-4">
<div className="mb-2 text-sm text-gray-800 dark:text-gray-200">
{requirement.description}
</div>
<div className="flex flex-col gap-2 text-sm">
{requirement.objetive_name && (
<div className="flex items-center gap-2">
<span className="font-medium">Objective:</span>
<span>{requirement.objetive_name}</span>
</div>
)}
</div>
</div>
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.objetive_name && (
<ComplianceDetailSection title="Objective">
<ComplianceDetailText>
{requirement.objetive_name as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
</ComplianceDetailContainer>
);
};
@@ -1,5 +1,12 @@
import { Requirement } from "@/types/compliance";
import {
ComplianceBulletList,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
export const KISACustomDetails = ({
requirement,
}: {
@@ -15,79 +22,32 @@ export const KISACustomDetails = ({
| undefined;
return (
<div className="space-y-4">
<ComplianceDetailContainer>
{requirement.description && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Description
</h4>
<p className="text-sm">{requirement.description}</p>
</div>
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
{auditChecklist && auditChecklist.length > 0 && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Audit Checklist
</h4>
<div className="space-y-2">
{auditChecklist.map((item: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<span className="text-muted-foreground mt-1 text-xs"></span>
<p className="text-sm">{item}</p>
</div>
))}
</div>
</div>
)}
<ComplianceBulletList
title="Audit Checklist"
items={auditChecklist || []}
/>
{relatedRegulations && relatedRegulations.length > 0 && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Related Regulations
</h4>
<div className="space-y-2">
{relatedRegulations.map((regulation: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<span className="text-muted-foreground mt-1 text-xs"></span>
<p className="text-sm">{regulation}</p>
</div>
))}
</div>
</div>
)}
<ComplianceBulletList
title="Related Regulations"
items={relatedRegulations || []}
/>
{auditEvidence && auditEvidence.length > 0 && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Audit Evidence
</h4>
<div className="space-y-2">
{auditEvidence.map((evidence: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<span className="text-muted-foreground mt-1 text-xs"></span>
<p className="text-sm">{evidence}</p>
</div>
))}
</div>
</div>
)}
<ComplianceBulletList
title="Audit Evidence"
items={auditEvidence || []}
/>
{nonComplianceCases && nonComplianceCases.length > 0 && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Non-Compliance Cases
</h4>
<div className="space-y-2">
{nonComplianceCases.map((caseItem: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<span className="text-muted-foreground mt-1 text-xs"></span>
<p className="text-sm">{caseItem}</p>
</div>
))}
</div>
</div>
)}
</div>
<ComplianceBulletList
title="Non-Compliance Cases"
items={nonComplianceCases || []}
/>
</ComplianceDetailContainer>
);
};
@@ -0,0 +1,166 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
export const ComplianceLink = ({
href,
children,
}: {
href: string;
children: React.ReactNode;
}) => {
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
className="break-all text-sm text-blue-600 decoration-1 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{children}
</Link>
);
};
export const ComplianceDetailContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
return <div className="space-y-4">{children}</div>;
};
export const ComplianceDetailSection = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => {
return (
<div>
<h4 className="text-muted-foreground mb-2 text-sm font-medium">
{title}
</h4>
{children}
</div>
);
};
export const ComplianceDetailText = ({
children,
className = "",
}: {
children: React.ReactNode;
className?: string;
}) => {
return <p className={`text-sm leading-relaxed ${className}`}>{children}</p>;
};
export const ComplianceBadgeContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
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,
conditional = false,
}: {
label: string;
value: string | number;
color: BadgeColor;
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",
};
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>
</div>
);
};
export const ComplianceBulletList = ({
title,
items,
}: {
title: string;
items: string[];
}) => {
if (!items || items.length === 0) return null;
return (
<ComplianceDetailSection title={title}>
<div className="space-y-2">
{items.map((item: string, index: number) => (
<div key={index} className="flex items-start gap-2">
<span className="text-muted-foreground mt-1 text-xs"></span>
<ComplianceDetailText>{item}</ComplianceDetailText>
</div>
))}
</div>
</ComplianceDetailSection>
);
};
export const ComplianceChipContainer = ({
title,
items,
}: {
title: string;
items: string[];
}) => {
if (!items || items.length === 0) return null;
return (
<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-inset ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20"
>
{item}
</span>
))}
</div>
</ComplianceDetailSection>
);
};
@@ -0,0 +1,68 @@
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
export const ThreatCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.attributeDescription && (
<ComplianceDetailSection title="Attribute Description">
<ComplianceDetailText>
{requirement.attributeDescription}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{typeof requirement.levelOfRisk === "number" && (
<ComplianceBadge
label="Risk Level"
value={requirement.levelOfRisk}
color="red"
/>
)}
{typeof requirement.weight === "number" && (
<ComplianceBadge
label="Weight"
value={requirement.weight}
color="green"
/>
)}
{typeof requirement.score === "number" && (
<ComplianceBadge
label="Score"
value={requirement.score}
color="green"
conditional={true}
/>
)}
</ComplianceBadgeContainer>
{requirement.additionalInformation && (
<ComplianceDetailSection title="Additional Information">
<ComplianceDetailText>
{requirement.additionalInformation}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
</ComplianceDetailContainer>
);
};
+12
View File
@@ -5,6 +5,7 @@ import { CISCustomDetails } from "@/components/compliance/compliance-custom-deta
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
import { KISACustomDetails } from "@/components/compliance/compliance-custom-details/kisa-details";
import { ThreatCustomDetails } from "@/components/compliance/compliance-custom-details/threat-details";
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
import {
AttributesData,
@@ -35,6 +36,10 @@ import {
mapComplianceData as mapKISAComplianceData,
toAccordionItems as toKISAAccordionItems,
} from "./kisa";
import {
mapComplianceData as mapThetaComplianceData,
toAccordionItems as toThetaAccordionItems,
} from "./threat";
export interface ComplianceMapper {
mapComplianceData: (
@@ -131,6 +136,13 @@ const complianceMappers: Record<string, ComplianceMapper> = {
getDetailsComponent: (requirement: Requirement) =>
React.createElement(KISACustomDetails, { requirement }),
},
ProwlerThreatScore: {
mapComplianceData: mapThetaComplianceData,
toAccordionItems: toThetaAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(ThreatCustomDetails, { requirement }),
},
};
// Default mapper (fallback to ENS for backward compatibility)
+246
View File
@@ -0,0 +1,246 @@
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,
Framework,
Requirement,
RequirementItemData,
RequirementsData,
RequirementStatus,
ThreatAttributesMetadata,
} from "@/types/compliance";
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
): Framework[] => {
const attributes = attributesData?.data || [];
const requirements = requirementsData?.data || [];
// Create a map for quick lookup of requirements by id
const requirementsMap = new Map<string, RequirementItemData>();
requirements.forEach((req: RequirementItemData) => {
requirementsMap.set(req.id, req);
});
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 as unknown as ThreatAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
// Get corresponding requirement data
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
const sectionName = attrs.Section;
const subSectionName = attrs.SubSection;
const title = attrs.Title;
const description = attributeItem.attributes.description;
const status = requirementData.attributes.status || "";
const checks = attributeItem.attributes.attributes.check_ids || [];
const requirementName = id;
const levelOfRisk = attrs.LevelOfRisk;
const weight = attrs.Weight;
const attributeDescription = attrs.AttributeDescription;
const additionalInformation = attrs.AdditionalInformation;
// Calculate score: if PASS = levelOfRisk * weight, if FAIL = 0
const score = status === "PASS" ? levelOfRisk * weight : 0;
// Find or create framework
let framework = frameworks.find((f) => f.name === frameworkName);
if (!framework) {
framework = {
name: frameworkName,
pass: 0,
fail: 0,
manual: 0,
categories: [],
};
frameworks.push(framework);
}
// Find or create category (Section)
let category = framework.categories.find((c) => c.name === sectionName);
if (!category) {
category = {
name: sectionName,
pass: 0,
fail: 0,
manual: 0,
controls: [],
};
framework.categories.push(category);
}
// Find or create control (SubSection)
let control = category.controls.find((c) => c.label === subSectionName);
if (!control) {
control = {
label: subSectionName,
pass: 0,
fail: 0,
manual: 0,
requirements: [],
};
category.controls.push(control);
}
// Create requirement
const finalStatus: RequirementStatus = status as RequirementStatus;
const requirement: Requirement = {
name: requirementName,
description: description,
status: finalStatus,
check_ids: checks,
pass: finalStatus === "PASS" ? 1 : 0,
fail: finalStatus === "FAIL" ? 1 : 0,
manual: finalStatus === "MANUAL" ? 1 : 0,
title: title,
levelOfRisk: levelOfRisk,
weight: weight,
score: score,
attributeDescription: attributeDescription,
additionalInformation: additionalInformation,
};
control.requirements.push(requirement);
}
// Calculate counters and percentualScore
frameworks.forEach((framework) => {
framework.pass = 0;
framework.fail = 0;
framework.manual = 0;
framework.categories.forEach((category) => {
category.pass = 0;
category.fail = 0;
category.manual = 0;
// Calculate total score for this section and maximum possible score
let totalSectionScore = 0;
let maxPossibleSectionScore = 0;
category.controls.forEach((control) => {
control.pass = 0;
control.fail = 0;
control.manual = 0;
control.requirements.forEach((requirement) => {
if (requirement.status === "MANUAL") {
control.manual++;
} else if (requirement.status === "PASS") {
control.pass++;
} else if (requirement.status === "FAIL") {
control.fail++;
}
// Add to total section score (actual score obtained)
totalSectionScore += (requirement.score as number) || 0;
// Add to maximum possible score (weight * levelOfRisk for each requirement)
const levelOfRisk = (requirement.levelOfRisk as number) || 0;
const weight = (requirement.weight as number) || 0;
maxPossibleSectionScore += levelOfRisk * weight;
});
category.pass += control.pass;
category.fail += control.fail;
category.manual += control.manual;
});
// Calculate percentualScore for this section: (suma de scores obtenidos / suma de weight * levelOfRisk) * 100
const percentualScore =
maxPossibleSectionScore > 0
? (totalSectionScore / maxPossibleSectionScore) * 100
: 0;
// Add percentualScore to category (we can extend the type or use a custom property)
(category as any).percentualScore =
Math.round(percentualScore * 100) / 100; // Round to 2 decimal places
framework.pass += category.pass;
framework.fail += category.fail;
framework.manual += category.manual;
});
});
return frameworks;
};
export const toAccordionItems = (
data: Framework[],
scanId: string | undefined,
): AccordionItemProps[] => {
return data.flatMap((framework) =>
framework.categories.map((category) => {
const percentualScore = (category as any).percentualScore || 0;
return {
key: `${framework.name}-${category.name}`,
title: (
<ComplianceAccordionTitle
label={`${category.name} - ${percentualScore}%`}
pass={category.pass}
fail={category.fail}
manual={category.manual}
isParentLevel={true}
/>
),
content: "",
items: category.controls.map((control, i: number) => {
return {
key: `${framework.name}-${category.name}-control-${i}`,
title: (
<ComplianceAccordionTitle
label={control.label}
pass={control.pass}
fail={control.fail}
manual={control.manual}
/>
),
content: "",
items: control.requirements.map((requirement, j: number) => {
const itemKey = `${framework.name}-${category.name}-control-${i}-req-${j}`;
return {
key: itemKey,
title: (
<ComplianceAccordionRequirementTitle
type=""
name={`${requirement.name} - ${requirement.title || requirement.description}`}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 &&
requirement.manual === 0
}
/>
),
items: [],
};
}),
isDisabled:
control.pass === 0 && control.fail === 0 && control.manual === 0,
};
}),
};
}),
);
};
+11
View File
@@ -113,6 +113,16 @@ export interface AWSWellArchitectedAttributesMetadata {
ImplementationGuidanceUrl: string;
}
export interface ThreatAttributesMetadata {
Title: string;
Section: string;
SubSection: string;
AttributeDescription: string;
AdditionalInformation: string;
LevelOfRisk: number;
Weight: number;
}
export interface KISAAttributesMetadata {
Domain: string;
Subdomain: string;
@@ -136,6 +146,7 @@ export interface AttributesItemData {
| ISO27001AttributesMetadata[]
| CISAttributesMetadata[]
| AWSWellArchitectedAttributesMetadata[]
| ThreatAttributesMetadata[]
| KISAAttributesMetadata[];
check_ids: string[];
};