diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index e668d5e09e..9d916b205a 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -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 diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 10a473236f..63f4c0150b 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -73,7 +73,12 @@ export const ComplianceCard: React.FC = ({ 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; } 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 new file mode 100644 index 0000000000..35cc5c7114 --- /dev/null +++ b/ui/components/compliance/compliance-custom-details/aws-well-architected-details.tsx @@ -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 ( +
+ {requirement.description && ( +
+

+ Description +

+

{requirement.description}

+
+ )} + + {requirement.well_architected_name && ( +
+

+ Best Practice +

+

{requirement.well_architected_name}

+
+ )} + + {requirement.well_architected_question_id && ( +
+

+ Question ID +

+

{requirement.well_architected_question_id}

+
+ )} + + {requirement.well_architected_practice_id && ( +
+

+ Practice ID +

+

{requirement.well_architected_practice_id}

+
+ )} + + {requirement.level_of_risk && ( +
+

+ Level of Risk +

+ +
+ )} + + {requirement.assessment_method && ( +
+

+ Assessment Method +

+

{requirement.assessment_method}

+
+ )} + + {requirement.implementation_guidance_url && ( +
+

+ Implementation Guidance +

+ + {requirement.implementation_guidance_url} + +
+ )} +
+ ); +}; diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx index e4d99db68f..ebb04e48d8 100644 --- a/ui/components/compliance/compliance-custom-details/cis-details.tsx +++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx @@ -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) => { {/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */} {/* eslint-disable-next-line */} -
+
{requirement.remediation_procedure}
@@ -96,7 +97,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { Audit Procedure {/* eslint-disable-next-line */} -
+
{requirement.audit_procedure}
@@ -131,14 +132,14 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {processReferences(requirement.references).map( (url: string, index: number) => (
- {url} - +
), )} diff --git a/ui/lib/compliance/aws-well-architected.tsx b/ui/lib/compliance/aws-well-architected.tsx new file mode 100644 index 0000000000..d6fd809987 --- /dev/null +++ b/ui/lib/compliance/aws-well-architected.tsx @@ -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(); + 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: ( + + ), + content: "", + items: category.controls.map((control, i: number) => { + return { + key: `${framework.name}-${category.name}-control-${i}`, + 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, + }; + }), + }; + }), + ); +}; diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index 6d0c3c7779..7317bb6c0f 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -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 = { 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) diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 4fa4cf7e1a..8cfaab0246 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -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[]; }; };