mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat: ThreatScore compliance detailed view (#7979)
This commit is contained in:
+2
-1
@@ -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 = () => {
|
||||
|
||||
+61
-65
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user