feat: MittreAtack compliance detailed view (#8002)

This commit is contained in:
Alejandro Bailo
2025-06-12 09:27:47 +02:00
committed by GitHub
parent e7c17ab0b3
commit 8751615faa
15 changed files with 466 additions and 71 deletions
+2
View File
@@ -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"
+1 -8
View File
@@ -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 ? (
+32
View File
@@ -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
View File
@@ -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: [],
};
}),
};
+252
View File
@@ -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
View File
@@ -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[];
};