mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
feat: MittreAtack compliance detailed view (#8002)
This commit is contained in:
@@ -21,6 +21,8 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- 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)
|
||||
- Add compliance detail view: ProwlerThreatScore [(#7966)](https://github.com/prowler-cloud/prowler/pull/7966)
|
||||
- Add compliance detail view: Generic (rest of the compliances) [(#7990)](https://github.com/prowler-cloud/prowler/pull/7990)
|
||||
- Add compliance detail view: MITRE ATTACK [(#8002)](https://github.com/prowler-cloud/prowler/pull/8002)
|
||||
- Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7979)](https://github.com/prowler-cloud/prowler/pull/7979)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
} from "@/components/compliance";
|
||||
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import {
|
||||
calculateCategoryHeatmapData,
|
||||
getComplianceMapper,
|
||||
} from "@/lib/compliance/commons";
|
||||
import { getComplianceMapper } from "@/lib/compliance/commons";
|
||||
import { ScanProps } from "@/types";
|
||||
import { Framework, RequirementsTotals } from "@/types/compliance";
|
||||
|
||||
@@ -239,7 +236,7 @@ const SSRComplianceContent = async ({
|
||||
);
|
||||
|
||||
// Calculate category heatmap data
|
||||
const categoryHeatmapData = calculateCategoryHeatmapData(data);
|
||||
const categoryHeatmapData = mapper.calculateCategoryHeatmapData(data);
|
||||
|
||||
const totalRequirements: RequirementsTotals = data.reduce(
|
||||
(acc: RequirementsTotals, framework: Framework) => ({
|
||||
@@ -271,6 +268,7 @@ const SSRComplianceContent = async ({
|
||||
|
||||
<Spacer className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-800" />
|
||||
<ClientAccordionWrapper
|
||||
hideExpandButton={complianceId.includes("mitre_attack")}
|
||||
items={accordionItems}
|
||||
defaultExpandedKeys={defaultKeys}
|
||||
/>
|
||||
|
||||
@@ -173,7 +173,7 @@ export const ClientAccordionContent = ({
|
||||
|
||||
return (
|
||||
<div className="mb-1 mt-3 text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
⚠️ There are no findings for this regions
|
||||
⚠️ There are no findings for these regions
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,9 +8,11 @@ import { CustomButton } from "@/components/ui/custom";
|
||||
export const ClientAccordionWrapper = ({
|
||||
items,
|
||||
defaultExpandedKeys,
|
||||
hideExpandButton = false,
|
||||
}: {
|
||||
items: AccordionItemProps[];
|
||||
defaultExpandedKeys: string[];
|
||||
hideExpandButton?: boolean;
|
||||
}) => {
|
||||
const [selectedKeys, setSelectedKeys] =
|
||||
useState<string[]>(defaultExpandedKeys);
|
||||
@@ -56,16 +58,18 @@ export const ClientAccordionWrapper = ({
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<CustomButton
|
||||
variant="flat"
|
||||
size="sm"
|
||||
onPress={handleToggleExpand}
|
||||
ariaLabel={isExpanded ? "Collapse all" : "Expand all"}
|
||||
>
|
||||
{isExpanded ? "Collapse all" : "Expand all"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
{!hideExpandButton && (
|
||||
<div className="flex justify-end">
|
||||
<CustomButton
|
||||
variant="flat"
|
||||
size="sm"
|
||||
onPress={handleToggleExpand}
|
||||
ariaLabel={isExpanded ? "Collapse all" : "Expand all"}
|
||||
>
|
||||
{isExpanded ? "Collapse all" : "Expand all"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
)}
|
||||
<Accordion
|
||||
items={items}
|
||||
variant="light"
|
||||
|
||||
@@ -71,14 +71,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
return "success";
|
||||
};
|
||||
|
||||
const isPressable = !id.includes("mitre");
|
||||
|
||||
const navigateToDetail = () => {
|
||||
// We will unlock this while developing the rest of complainces.
|
||||
if (!isPressable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formattedTitleForUrl = encodeURIComponent(title);
|
||||
const path = `/compliance/${formattedTitleForUrl}`;
|
||||
const params = new URLSearchParams();
|
||||
@@ -103,7 +96,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
fullWidth
|
||||
isHoverable
|
||||
shadow="sm"
|
||||
isPressable={isPressable}
|
||||
isPressable
|
||||
onPress={navigateToDetail}
|
||||
>
|
||||
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
|
||||
|
||||
@@ -101,9 +101,7 @@ export const BarChart = ({ sections }: FailedSectionsListProps) => {
|
||||
<div className="flex w-[400px] flex-col items-center justify-between">
|
||||
{title}
|
||||
<div className="flex h-[320px] w-full items-center justify-center">
|
||||
<p className="text-sm text-gray-800 dark:text-gray-200">
|
||||
There are no failed sections
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">There are no failed sections</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ export const AWSWellArchitectedCustomDetails = ({
|
||||
<ComplianceLink
|
||||
href={requirement.implementation_guidance_url as string}
|
||||
>
|
||||
{requirement.implementation_guidance_url}
|
||||
{requirement.implementation_guidance_url as string}
|
||||
</ComplianceLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ interface CISDetailsProps {
|
||||
|
||||
export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
const processReferences = (
|
||||
references: string | number | string[] | undefined,
|
||||
references: string | number | string[] | object[] | undefined,
|
||||
): string[] => {
|
||||
if (typeof references !== "string") return [];
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
ComplianceBadge,
|
||||
ComplianceBadgeContainer,
|
||||
ComplianceChipContainer,
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
ComplianceLink,
|
||||
} from "./shared-components";
|
||||
|
||||
export const MITRECustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
const cloudServices = requirement.cloud_services as
|
||||
| Array<{
|
||||
service: string;
|
||||
category: string;
|
||||
value: string;
|
||||
comment: string;
|
||||
}>
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
<ComplianceDetailContainer>
|
||||
{requirement.description && (
|
||||
<ComplianceDetailSection title="Description">
|
||||
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
<ComplianceBadgeContainer>
|
||||
{requirement.technique_id && (
|
||||
<ComplianceBadge
|
||||
label="Technique ID"
|
||||
value={requirement.technique_id as string}
|
||||
color="indigo"
|
||||
/>
|
||||
)}
|
||||
</ComplianceBadgeContainer>
|
||||
|
||||
<ComplianceChipContainer
|
||||
title="Tactics"
|
||||
items={(requirement.tactics as string[]) || []}
|
||||
/>
|
||||
|
||||
<ComplianceChipContainer
|
||||
title="Platforms"
|
||||
items={(requirement.platforms as string[]) || []}
|
||||
/>
|
||||
|
||||
{requirement.subtechniques &&
|
||||
Array.isArray(requirement.subtechniques) &&
|
||||
requirement.subtechniques.length > 0 && (
|
||||
<ComplianceChipContainer
|
||||
title="Subtechniques"
|
||||
items={requirement.subtechniques as string[]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{requirement.technique_url && (
|
||||
<ComplianceDetailSection title="MITRE ATT&CK Reference">
|
||||
<ComplianceLink href={requirement.technique_url as string}>
|
||||
{requirement.technique_url as string}
|
||||
</ComplianceLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
{cloudServices && cloudServices.length > 0 && (
|
||||
<ComplianceDetailSection title="Cloud Security Mappings">
|
||||
<div className="space-y-4">
|
||||
{cloudServices.map((service, index) => (
|
||||
<div key={index} className="space-y-3 rounded-lg border p-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<ComplianceBadge
|
||||
label="Service"
|
||||
value={service.service}
|
||||
color="blue"
|
||||
/>
|
||||
<ComplianceBadge
|
||||
label="Category"
|
||||
value={service.category}
|
||||
color="indigo"
|
||||
/>
|
||||
<ComplianceBadge
|
||||
label="Coverage"
|
||||
value={service.value}
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
{service.comment && (
|
||||
<div>
|
||||
<h5 className="text-muted-foreground mb-1 text-xs font-medium">
|
||||
Details
|
||||
</h5>
|
||||
<ComplianceDetailText className="text-xs">
|
||||
{service.comment}
|
||||
</ComplianceDetailText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
);
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export const ThreatCustomDetails = ({
|
||||
{requirement.attributeDescription && (
|
||||
<ComplianceDetailSection title="Attribute Description">
|
||||
<ComplianceDetailText>
|
||||
{requirement.attributeDescription}
|
||||
{requirement.attributeDescription as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
@@ -59,7 +59,7 @@ export const ThreatCustomDetails = ({
|
||||
{requirement.additionalInformation && (
|
||||
<ComplianceDetailSection title="Additional Information">
|
||||
<ComplianceDetailText>
|
||||
{requirement.additionalInformation}
|
||||
{requirement.additionalInformation as string}
|
||||
</ComplianceDetailText>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
@@ -31,9 +31,12 @@ export const ScanSelector = ({
|
||||
size="lg"
|
||||
labelPlacement="outside"
|
||||
selectedKeys={new Set([selectedScanId])}
|
||||
onSelectionChange={(keys) =>
|
||||
onSelectionChange(Array.from(keys)[0] as string)
|
||||
}
|
||||
onSelectionChange={(keys) => {
|
||||
const newSelectedId = Array.from(keys)[0] as string;
|
||||
if (newSelectedId && newSelectedId !== selectedScanId) {
|
||||
onSelectionChange(newSelectedId);
|
||||
}
|
||||
}}
|
||||
renderValue={() => {
|
||||
const selectedItem = scans.find((item) => item.id === selectedScanId);
|
||||
return selectedItem ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ENSCustomDetails } from "@/components/compliance/compliance-custom-deta
|
||||
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
|
||||
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 { ThreatCustomDetails } from "@/components/compliance/compliance-custom-details/threat-details";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import {
|
||||
@@ -41,6 +42,12 @@ import {
|
||||
mapComplianceData as mapKISAComplianceData,
|
||||
toAccordionItems as toKISAAccordionItems,
|
||||
} from "./kisa";
|
||||
import {
|
||||
calculateCategoryHeatmapData as calculateMITRECategoryHeatmapData,
|
||||
getTopFailedSections as getMITRETopFailedSections,
|
||||
mapComplianceData as mapMITREComplianceData,
|
||||
toAccordionItems as toMITREAccordionItems,
|
||||
} from "./mitre";
|
||||
import {
|
||||
mapComplianceData as mapThetaComplianceData,
|
||||
toAccordionItems as toThetaAccordionItems,
|
||||
@@ -57,6 +64,7 @@ export interface ComplianceMapper {
|
||||
scanId: string | undefined,
|
||||
) => AccordionItemProps[];
|
||||
getTopFailedSections: (mappedData: Framework[]) => FailedSection[];
|
||||
calculateCategoryHeatmapData: (complianceData: Framework[]) => CategoryData[];
|
||||
getDetailsComponent: (requirement: Requirement) => React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -103,6 +111,8 @@ const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
mapComplianceData: mapENSComplianceData,
|
||||
toAccordionItems: toENSAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ENSCustomDetails, { requirement }),
|
||||
},
|
||||
@@ -110,6 +120,8 @@ const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
mapComplianceData: mapISOComplianceData,
|
||||
toAccordionItems: toISOAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ISOCustomDetails, { requirement }),
|
||||
},
|
||||
@@ -117,6 +129,8 @@ const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
mapComplianceData: mapCISComplianceData,
|
||||
toAccordionItems: toCISAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(CISCustomDetails, { requirement }),
|
||||
},
|
||||
@@ -124,6 +138,8 @@ const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
mapComplianceData: mapAWSWellArchitectedComplianceData,
|
||||
toAccordionItems: toAWSWellArchitectedAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
|
||||
},
|
||||
@@ -131,6 +147,8 @@ const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
mapComplianceData: mapAWSWellArchitectedComplianceData,
|
||||
toAccordionItems: toAWSWellArchitectedAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
|
||||
},
|
||||
@@ -138,13 +156,25 @@ const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
mapComplianceData: mapKISAComplianceData,
|
||||
toAccordionItems: toKISAAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(KISACustomDetails, { requirement }),
|
||||
},
|
||||
"MITRE-ATTACK": {
|
||||
mapComplianceData: mapMITREComplianceData,
|
||||
toAccordionItems: toMITREAccordionItems,
|
||||
getTopFailedSections: getMITRETopFailedSections,
|
||||
calculateCategoryHeatmapData: calculateMITRECategoryHeatmapData,
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(MITRECustomDetails, { requirement }),
|
||||
},
|
||||
ProwlerThreatScore: {
|
||||
mapComplianceData: mapThetaComplianceData,
|
||||
toAccordionItems: toThetaAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (complianceData: Framework[]) =>
|
||||
calculateCategoryHeatmapData(complianceData),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ThreatCustomDetails, { requirement }),
|
||||
},
|
||||
@@ -155,6 +185,8 @@ const defaultMapper: ComplianceMapper = {
|
||||
mapComplianceData: mapGenericComplianceData,
|
||||
toAccordionItems: toGenericAccordionItems,
|
||||
getTopFailedSections,
|
||||
calculateCategoryHeatmapData: (data: Framework[]) =>
|
||||
calculateCategoryHeatmapData(data),
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(GenericCustomDetails, { requirement }),
|
||||
};
|
||||
|
||||
+24
-36
@@ -101,6 +101,7 @@ export const mapComplianceData = (
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
objetive_name: objetiveName,
|
||||
check_summary: checkSummary,
|
||||
control_label: controlLabel,
|
||||
};
|
||||
|
||||
control.requirements.push(requirement);
|
||||
@@ -152,6 +153,10 @@ export const toAccordionItems = (
|
||||
): AccordionItemProps[] => {
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => {
|
||||
const allRequirements = category.controls.flatMap(
|
||||
(control) => control.requirements,
|
||||
);
|
||||
|
||||
return {
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
@@ -164,46 +169,29 @@ export const toAccordionItems = (
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.map((control, i: number) => {
|
||||
items: allRequirements.map((requirement, j: number) => {
|
||||
const itemKey = `${framework.name}-${category.name}-req-${j}`;
|
||||
|
||||
return {
|
||||
key: `${framework.name}-${category.name}-control-${i}`,
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={control.label}
|
||||
pass={control.pass}
|
||||
fail={control.fail}
|
||||
manual={control.manual}
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={(requirement.control_label as string) || requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
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}
|
||||
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,
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
CategoryData,
|
||||
FailedSection,
|
||||
Framework,
|
||||
MITREAttributesMetadata,
|
||||
Requirement,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} 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 MITREAttributesMetadata[];
|
||||
|
||||
if (!metadataArray || metadataArray.length === 0) continue;
|
||||
|
||||
// Get corresponding requirement data
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
const techniqueName = attributeItem.attributes.name || id;
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
const tactics = attributeItem.attributes.tactics || [];
|
||||
const subtechniques = attributeItem.attributes.subtechniques || [];
|
||||
const platforms = attributeItem.attributes.platforms || [];
|
||||
const techniqueUrl = attributeItem.attributes.technique_url || "";
|
||||
const requirementName = `${id} - ${techniqueName}`;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Create requirement directly (flat structure - no categories)
|
||||
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,
|
||||
// MITRE specific fields
|
||||
technique_id: id,
|
||||
technique_name: techniqueName,
|
||||
tactics: tactics,
|
||||
subtechniques: subtechniques,
|
||||
platforms: platforms,
|
||||
technique_url: techniqueUrl,
|
||||
cloud_services: metadataArray.map((m) => {
|
||||
// Dynamically find the service field (AWSService, GCPService, AzureService, etc.)
|
||||
const serviceKey = Object.keys(m).find((key) =>
|
||||
key.toLowerCase().includes("service"),
|
||||
);
|
||||
const serviceName = serviceKey ? m[serviceKey] : "Unknown Service";
|
||||
|
||||
return {
|
||||
service: serviceName,
|
||||
category: m.Category,
|
||||
value: m.Value,
|
||||
comment: m.Comment,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
// Add requirement directly to framework (store in a special property)
|
||||
(framework as any).requirements = (framework as any).requirements || [];
|
||||
(framework as any).requirements.push(requirement);
|
||||
|
||||
// Update framework counters
|
||||
if (requirement.status === "MANUAL") {
|
||||
framework.manual++;
|
||||
} else if (requirement.status === "PASS") {
|
||||
framework.pass++;
|
||||
} else if (requirement.status === "FAIL") {
|
||||
framework.fail++;
|
||||
}
|
||||
}
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.flatMap((framework) => {
|
||||
const requirements = (framework as any).requirements || [];
|
||||
|
||||
return requirements.map((requirement: Requirement, i: number) => {
|
||||
const itemKey = `${framework.name}-req-${i}`;
|
||||
|
||||
return {
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Custom function for MITRE to get top failed sections grouped by tactics
|
||||
export const getTopFailedSections = (
|
||||
mappedData: Framework[],
|
||||
): FailedSection[] => {
|
||||
const failedSectionMap = new Map();
|
||||
|
||||
mappedData.forEach((framework) => {
|
||||
const requirements = (framework as any).requirements || [];
|
||||
|
||||
requirements.forEach((requirement: Requirement) => {
|
||||
if (requirement.status === "FAIL") {
|
||||
const tactics = (requirement.tactics as string[]) || [];
|
||||
|
||||
tactics.forEach((tactic) => {
|
||||
if (!failedSectionMap.has(tactic)) {
|
||||
failedSectionMap.set(tactic, { total: 0, types: {} });
|
||||
}
|
||||
|
||||
const sectionData = failedSectionMap.get(tactic);
|
||||
sectionData.total += 1;
|
||||
|
||||
const type = "Fails";
|
||||
sectionData.types[type] = (sectionData.types[type] || 0) + 1;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Convert in descending order and slice top 5
|
||||
return Array.from(failedSectionMap.entries())
|
||||
.map(([name, data]) => ({ name, ...data }))
|
||||
.sort((a, b) => b.total - a.total)
|
||||
.slice(0, 5); // Top 5
|
||||
};
|
||||
|
||||
// Custom function for MITRE to calculate category heatmap data grouped by tactics
|
||||
export const calculateCategoryHeatmapData = (
|
||||
complianceData: Framework[],
|
||||
): CategoryData[] => {
|
||||
if (!complianceData?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const tacticMap = new Map<
|
||||
string,
|
||||
{ pass: number; fail: number; manual: number }
|
||||
>();
|
||||
|
||||
// Aggregate data by tactics
|
||||
complianceData.forEach((framework) => {
|
||||
const requirements = (framework as any).requirements || [];
|
||||
|
||||
requirements.forEach((requirement: Requirement) => {
|
||||
const tactics = (requirement.tactics as string[]) || [];
|
||||
|
||||
tactics.forEach((tactic) => {
|
||||
const existing = tacticMap.get(tactic) || {
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
};
|
||||
|
||||
tacticMap.set(tactic, {
|
||||
pass: existing.pass + requirement.pass,
|
||||
fail: existing.fail + requirement.fail,
|
||||
manual: existing.manual + requirement.manual,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const categoryData: CategoryData[] = Array.from(tacticMap.entries()).map(
|
||||
([name, stats]) => {
|
||||
const totalRequirements = stats.pass + stats.fail + stats.manual;
|
||||
const failurePercentage =
|
||||
totalRequirements > 0
|
||||
? Math.round((stats.fail / totalRequirements) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
name,
|
||||
failurePercentage,
|
||||
totalRequirements,
|
||||
failedRequirements: stats.fail,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const filteredData = categoryData
|
||||
.filter((category) => category.totalRequirements > 0)
|
||||
.sort((a, b) => b.failurePercentage - a.failurePercentage)
|
||||
.slice(0, 9); // Show top 9 tactics
|
||||
|
||||
return filteredData;
|
||||
} catch (error) {
|
||||
console.error("Error calculating MITRE category heatmap data:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
+15
-1
@@ -27,7 +27,7 @@ export interface Requirement {
|
||||
check_ids: string[];
|
||||
// This is to allow any key to be added to the requirement object
|
||||
// because each compliance has different keys
|
||||
[key: string]: string | string[] | number | undefined;
|
||||
[key: string]: string | string[] | number | object[] | undefined;
|
||||
}
|
||||
|
||||
export interface Control {
|
||||
@@ -133,6 +133,14 @@ export interface KISAAttributesMetadata {
|
||||
NonComplianceCases: string[];
|
||||
}
|
||||
|
||||
export interface MITREAttributesMetadata {
|
||||
// Dynamic cloud service field - could be AWSService, GCPService, AzureService, etc.
|
||||
[key: string]: string;
|
||||
Category: string; // "Protect", "Detect", "Respond"
|
||||
Value: string; // "Minimal", "Partial", "Significant"
|
||||
Comment: string;
|
||||
}
|
||||
|
||||
export interface GenericAttributesMetadata {
|
||||
ItemId: string;
|
||||
Section: string;
|
||||
@@ -150,6 +158,11 @@ export interface AttributesItemData {
|
||||
framework: string;
|
||||
version: string;
|
||||
description: string;
|
||||
// MITRE specific fields
|
||||
tactics?: string[];
|
||||
subtechniques?: string[];
|
||||
platforms?: string[];
|
||||
technique_url?: string;
|
||||
attributes: {
|
||||
metadata:
|
||||
| ENSAttributesMetadata[]
|
||||
@@ -158,6 +171,7 @@ export interface AttributesItemData {
|
||||
| AWSWellArchitectedAttributesMetadata[]
|
||||
| ThreatAttributesMetadata[]
|
||||
| KISAAttributesMetadata[]
|
||||
| MITREAttributesMetadata[]
|
||||
| GenericAttributesMetadata[];
|
||||
check_ids: string[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user