feat: aws-well-architected compliance detailed view (#7925)

This commit is contained in:
Alejandro Bailo
2025-06-04 12:26:27 +02:00
committed by GitHub
parent 6e7a32cb51
commit 4dc4d82d42
7 changed files with 360 additions and 6 deletions
+1
View File
@@ -13,6 +13,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- Add compliance detail view: ENS [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853)
- Add compliance detail view: ISO [(#7897)](https://github.com/prowler-cloud/prowler/pull/7897)
- 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)
### 🔄 Changed
+6 -1
View File
@@ -73,7 +73,12 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
const navigateToDetail = () => {
// We will unlock this while developing the rest of complainces.
if (!id.includes("ens") && !id.includes("iso") && !id.includes("cis_")) {
if (
!id.includes("ens") &&
!id.includes("iso") &&
!id.includes("cis_") &&
!id.includes("pillar")
) {
return;
}
@@ -0,0 +1,91 @@
import Link from "next/link";
import { SeverityBadge } from "@/components/ui/table";
import { Requirement } from "@/types/compliance";
export const AWSWellArchitectedCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<div className="space-y-4">
{requirement.description && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Description
</h4>
<p className="text-sm">{requirement.description}</p>
</div>
)}
{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>
)}
{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>
)}
{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"
}
/>
</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.implementation_guidance_url && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Implementation Guidance
</h4>
<Link
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>
)}
</div>
);
};
@@ -1,3 +1,4 @@
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { Requirement } from "@/types/compliance";
@@ -83,7 +84,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
</h4>
{/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */}
{/* eslint-disable-next-line */}
<div className="prose prose-sm dark:prose-invert max-w-none">
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{requirement.remediation_procedure}</ReactMarkdown>
</div>
</div>
@@ -96,7 +97,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
Audit Procedure
</h4>
{/* eslint-disable-next-line */}
<div className="prose prose-sm dark:prose-invert max-w-none">
<div className="prose prose-sm max-w-none dark:prose-invert">
<ReactMarkdown>{requirement.audit_procedure}</ReactMarkdown>
</div>
</div>
@@ -131,14 +132,14 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
{processReferences(requirement.references).map(
(url: string, index: number) => (
<div key={index}>
<a
<Link
href={url}
target="_blank"
rel="noopener noreferrer"
className="break-all text-blue-600 underline hover:text-blue-800"
>
{url}
</a>
</Link>
</div>
),
)}
+221
View File
@@ -0,0 +1,221 @@
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,
AWSWellArchitectedAttributesMetadata,
Framework,
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 AWSWellArchitectedAttributesMetadata[];
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 description = attributeItem.attributes.description;
const status = requirementData.attributes.status || "";
const checks = attributeItem.attributes.attributes.check_ids || [];
const requirementName = id;
if (!sectionName || !subSectionName) {
continue;
}
// 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,
well_architected_name: attrs.Name,
well_architected_question_id: attrs.WellArchitectedQuestionId,
well_architected_practice_id: attrs.WellArchitectedPracticeId,
level_of_risk: attrs.LevelOfRisk,
assessment_method: attrs.AssessmentMethod,
implementation_guidance_url: attrs.ImplementationGuidanceUrl,
};
control.requirements.push(requirement);
}
// Calculate counters
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;
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++;
}
});
category.pass += control.pass;
category.fail += control.fail;
category.manual += control.manual;
});
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) => {
return {
key: `${framework.name}-${category.name}`,
title: (
<ComplianceAccordionTitle
label={category.name}
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.well_architected_name as string) ||
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,
};
}),
};
}),
);
};
+19
View File
@@ -1,5 +1,6 @@
import React from "react";
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
@@ -14,6 +15,10 @@ import {
RequirementsData,
} from "@/types/compliance";
import {
mapComplianceData as mapAWSWellArchitectedComplianceData,
toAccordionItems as toAWSWellArchitectedAccordionItems,
} from "./aws-well-architected";
import {
mapComplianceData as mapCISComplianceData,
toAccordionItems as toCISAccordionItems,
@@ -101,6 +106,20 @@ const complianceMappers: Record<string, ComplianceMapper> = {
getDetailsComponent: (requirement: Requirement) =>
React.createElement(CISCustomDetails, { requirement }),
},
"AWS-Well-Architected-Framework-Security-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData,
toAccordionItems: toAWSWellArchitectedAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
},
"AWS-Well-Architected-Framework-Reliability-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData,
toAccordionItems: toAWSWellArchitectedAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
},
};
// Default mapper (fallback to ENS for backward compatibility)
+17 -1
View File
@@ -112,6 +112,18 @@ export interface CISAttributesMetadata {
References: string;
}
export interface AWSWellArchitectedAttributesMetadata {
Name: string;
WellArchitectedQuestionId: string;
WellArchitectedPracticeId: string;
Section: string;
SubSection: string;
LevelOfRisk: string;
AssessmentMethod: string;
Description: string;
ImplementationGuidanceUrl: string;
}
export interface AttributesItemData {
type: "compliance-requirements-attributes";
id: string;
@@ -120,7 +132,11 @@ export interface AttributesItemData {
version: string;
description: string;
attributes: {
metadata: ENSAttributesMetadata[] | ISO27001AttributesMetadata[];
metadata:
| ENSAttributesMetadata[]
| ISO27001AttributesMetadata[]
| CISAttributesMetadata[]
| AWSWellArchitectedAttributesMetadata[];
check_ids: string[];
};
};