mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
feat: aws-well-architected compliance detailed view (#7925)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -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
@@ -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[];
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user