From 8751615faae5249fe3c0d70a9f4afee31a83c43d Mon Sep 17 00:00:00 2001
From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
Date: Thu, 12 Jun 2025 09:27:47 +0200
Subject: [PATCH] feat: MittreAtack compliance detailed view (#8002)
---
ui/CHANGELOG.md | 2 +
.../compliance/[compliancetitle]/page.tsx | 8 +-
.../client-accordion-content.tsx | 2 +-
.../client-accordion-wrapper.tsx | 24 +-
ui/components/compliance/compliance-card.tsx | 9 +-
.../compliance-charts/bar-chart.tsx | 4 +-
.../aws-well-architected-details.tsx | 2 +-
.../compliance-custom-details/cis-details.tsx | 2 +-
.../mitre-details.tsx | 111 ++++++++
.../threat-details.tsx | 4 +-
.../compliance-header/scan-selector.tsx | 9 +-
ui/lib/compliance/commons.ts | 32 +++
ui/lib/compliance/iso.tsx | 60 ++---
ui/lib/compliance/mitre.tsx | 252 ++++++++++++++++++
ui/types/compliance.ts | 16 +-
15 files changed, 466 insertions(+), 71 deletions(-)
create mode 100644 ui/components/compliance/compliance-custom-details/mitre-details.tsx
create mode 100644 ui/lib/compliance/mitre.tsx
diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md
index 6a384b133c..4d5dd03148 100644
--- a/ui/CHANGELOG.md
+++ b/ui/CHANGELOG.md
@@ -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
diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx
index f75c344039..71e7706285 100644
--- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx
+++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx
@@ -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 ({
diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
index 5769e13259..23bedb0459 100644
--- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
+++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx
@@ -173,7 +173,7 @@ export const ClientAccordionContent = ({
return (
- ⚠️ There are no findings for this regions
+ ⚠️ There are no findings for these regions
);
};
diff --git a/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx b/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx
index a47952612b..6288cf9171 100644
--- a/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx
+++ b/ui/components/compliance/compliance-accordion/client-accordion-wrapper.tsx
@@ -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(defaultExpandedKeys);
@@ -56,16 +58,18 @@ export const ClientAccordionWrapper = ({
return (
-
-
- {isExpanded ? "Collapse all" : "Expand all"}
-
-
+ {!hideExpandButton && (
+
+
+ {isExpanded ? "Collapse all" : "Expand all"}
+
+
+ )}
= ({
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 = ({
fullWidth
isHoverable
shadow="sm"
- isPressable={isPressable}
+ isPressable
onPress={navigateToDetail}
>
diff --git a/ui/components/compliance/compliance-charts/bar-chart.tsx b/ui/components/compliance/compliance-charts/bar-chart.tsx
index 4e5fe95995..3afd02ee5d 100644
--- a/ui/components/compliance/compliance-charts/bar-chart.tsx
+++ b/ui/components/compliance/compliance-charts/bar-chart.tsx
@@ -101,9 +101,7 @@ export const BarChart = ({ sections }: FailedSectionsListProps) => {
{title}
-
- There are no failed sections
-
+
There are no failed sections
);
diff --git a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx
index b0069433ee..9317c61485 100644
--- a/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx
+++ b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx
@@ -78,7 +78,7 @@ export const AWSWellArchitectedCustomDetails = ({
- {requirement.implementation_guidance_url}
+ {requirement.implementation_guidance_url as string}
)}
diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx
index 6977c7d101..f804f8e7d0 100644
--- a/ui/components/compliance/compliance-custom-details/cis-details.tsx
+++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx
@@ -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 [];
diff --git a/ui/components/compliance/compliance-custom-details/mitre-details.tsx b/ui/components/compliance/compliance-custom-details/mitre-details.tsx
new file mode 100644
index 0000000000..71eca0d370
--- /dev/null
+++ b/ui/components/compliance/compliance-custom-details/mitre-details.tsx
@@ -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 (
+
+ {requirement.description && (
+
+ {requirement.description}
+
+ )}
+
+
+ {requirement.technique_id && (
+
+ )}
+
+
+
+
+
+
+ {requirement.subtechniques &&
+ Array.isArray(requirement.subtechniques) &&
+ requirement.subtechniques.length > 0 && (
+
+ )}
+
+ {requirement.technique_url && (
+
+
+ {requirement.technique_url as string}
+
+
+ )}
+
+ {cloudServices && cloudServices.length > 0 && (
+
+
+ {cloudServices.map((service, index) => (
+
+
+
+
+
+
+ {service.comment && (
+
+
+ Details
+
+
+ {service.comment}
+
+
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
diff --git a/ui/components/compliance/compliance-custom-details/threat-details.tsx b/ui/components/compliance/compliance-custom-details/threat-details.tsx
index 4f2f5e87da..1cf127bf84 100644
--- a/ui/components/compliance/compliance-custom-details/threat-details.tsx
+++ b/ui/components/compliance/compliance-custom-details/threat-details.tsx
@@ -24,7 +24,7 @@ export const ThreatCustomDetails = ({
{requirement.attributeDescription && (
- {requirement.attributeDescription}
+ {requirement.attributeDescription as string}
)}
@@ -59,7 +59,7 @@ export const ThreatCustomDetails = ({
{requirement.additionalInformation && (
- {requirement.additionalInformation}
+ {requirement.additionalInformation as string}
)}
diff --git a/ui/components/compliance/compliance-header/scan-selector.tsx b/ui/components/compliance/compliance-header/scan-selector.tsx
index 390bc669bc..4739633cc6 100644
--- a/ui/components/compliance/compliance-header/scan-selector.tsx
+++ b/ui/components/compliance/compliance-header/scan-selector.tsx
@@ -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 ? (
diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts
index 286d6e6e78..9befd489c7 100644
--- a/ui/lib/compliance/commons.ts
+++ b/ui/lib/compliance/commons.ts
@@ -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 = {
mapComplianceData: mapENSComplianceData,
toAccordionItems: toENSAccordionItems,
getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
React.createElement(ENSCustomDetails, { requirement }),
},
@@ -110,6 +120,8 @@ const complianceMappers: Record = {
mapComplianceData: mapISOComplianceData,
toAccordionItems: toISOAccordionItems,
getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
React.createElement(ISOCustomDetails, { requirement }),
},
@@ -117,6 +129,8 @@ const complianceMappers: Record = {
mapComplianceData: mapCISComplianceData,
toAccordionItems: toCISAccordionItems,
getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
React.createElement(CISCustomDetails, { requirement }),
},
@@ -124,6 +138,8 @@ const complianceMappers: Record = {
mapComplianceData: mapAWSWellArchitectedComplianceData,
toAccordionItems: toAWSWellArchitectedAccordionItems,
getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
},
@@ -131,6 +147,8 @@ const complianceMappers: Record = {
mapComplianceData: mapAWSWellArchitectedComplianceData,
toAccordionItems: toAWSWellArchitectedAccordionItems,
getTopFailedSections,
+ calculateCategoryHeatmapData: (data: Framework[]) =>
+ calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
},
@@ -138,13 +156,25 @@ const complianceMappers: Record = {
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 }),
};
diff --git a/ui/lib/compliance/iso.tsx b/ui/lib/compliance/iso.tsx
index 8fd2e7ae7e..4fdfd0fc31 100644
--- a/ui/lib/compliance/iso.tsx
+++ b/ui/lib/compliance/iso.tsx
@@ -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: (
-
),
- content: "",
- items: control.requirements.map((requirement, j: number) => {
- const itemKey = `${framework.name}-${category.name}-control-${i}-req-${j}`;
-
- return {
- key: itemKey,
- title: (
-
- ),
- content: (
-
- ),
- items: [],
- };
- }),
- isDisabled:
- control.pass === 0 && control.fail === 0 && control.manual === 0,
+ content: (
+
+ ),
+ items: [],
};
}),
};
diff --git a/ui/lib/compliance/mitre.tsx b/ui/lib/compliance/mitre.tsx
new file mode 100644
index 0000000000..480cb8c967
--- /dev/null
+++ b/ui/lib/compliance/mitre.tsx
@@ -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();
+ 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: (
+
+ ),
+ content: (
+
+ ),
+ 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 [];
+ }
+};
diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts
index 04c39877a8..bbe0e7ce18 100644
--- a/ui/types/compliance.ts
+++ b/ui/types/compliance.ts
@@ -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[];
};