- {isRegionFiltered
- ? capitalizeFirstLetter(item.name)
- : item.name}
+ {capitalizeFirstLetter(item.name)}
- {isRegionFiltered
- ? capitalizeFirstLetter(hoveredItem.name)
- : hoveredItem.name}
+ {capitalizeFirstLetter(hoveredItem.name)}
Failure Rate: {hoveredItem.failurePercentage}%
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) => (
),
)}
diff --git a/ui/components/compliance/compliance-header/compliance-header.tsx b/ui/components/compliance/compliance-header/compliance-header.tsx
index e6bfe0dd2d..abe54adf11 100644
--- a/ui/components/compliance/compliance-header/compliance-header.tsx
+++ b/ui/components/compliance/compliance-header/compliance-header.tsx
@@ -14,6 +14,7 @@ interface ComplianceHeaderProps {
showSearch?: boolean;
showRegionFilter?: boolean;
framework?: string; // Framework name to show specific filters
+ showProviders?: boolean;
}
export const ComplianceHeader = ({
@@ -22,6 +23,7 @@ export const ComplianceHeader = ({
showSearch = true,
showRegionFilter = true,
framework,
+ showProviders = true,
}: ComplianceHeaderProps) => {
const frameworkFilters = [];
@@ -56,10 +58,10 @@ export const ComplianceHeader = ({
<>
{showSearch &&
}
-
+ {showProviders &&
}
{allFilters.length > 0 && (
<>
-
+ {showProviders &&
}
>
)}
diff --git a/ui/components/compliance/compliance-header/compliance-scan-info.tsx b/ui/components/compliance/compliance-header/compliance-scan-info.tsx
index 5e422ffee2..732c2b09fa 100644
--- a/ui/components/compliance/compliance-header/compliance-scan-info.tsx
+++ b/ui/components/compliance/compliance-header/compliance-scan-info.tsx
@@ -9,10 +9,10 @@ interface ComplianceScanInfoProps {
providerInfo: {
provider: ProviderType;
alias?: string;
- uid: string;
+ uid?: string;
};
- attributes: {
- name: string;
+ attributes?: {
+ name?: string;
completed_at: string;
};
};
@@ -29,13 +29,17 @@ export const ComplianceScanInfo: React.FC
= ({
entityId={scan.providerInfo.uid}
hideCopyButton
/>
-
-
-
- {scan.attributes.name || "- -"}
-
-
-
+ {scan.attributes && (
+ <>
+
+
+
+ {scan.attributes.name || "- -"}
+
+
+
+ >
+ )}
);
};
diff --git a/ui/components/ui/accordion/Accordion.tsx b/ui/components/ui/accordion/Accordion.tsx
index 1bbcbc9ed8..8117d5eea6 100644
--- a/ui/components/ui/accordion/Accordion.tsx
+++ b/ui/components/ui/accordion/Accordion.tsx
@@ -138,7 +138,7 @@ export const Accordion = ({
indicator={
}
classNames={{
base: index === 0 || index === 1 ? "my-1" : "my-1",
- title: "text-sm font-medium max-w-full overflow-hidden truncate",
+ title: "text-sm",
subtitle: "text-xs text-gray-500",
trigger:
"py-2 px-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center",
diff --git a/ui/components/ui/custom/custom-dropdown-filter.tsx b/ui/components/ui/custom/custom-dropdown-filter.tsx
index 8cd0d7dd75..be27e6ef7a 100644
--- a/ui/components/ui/custom/custom-dropdown-filter.tsx
+++ b/ui/components/ui/custom/custom-dropdown-filter.tsx
@@ -44,57 +44,75 @@ export const CustomDropdownFilter = ({
return filterParam ? filterParam.split(",") : [];
}, [searchParams, filter?.key]);
- // Sync URL state with component state
- useEffect(() => {
- if (activeFilterValue.length > 0) {
- const newSelection = new Set(activeFilterValue);
+ // Helper function to handle URL filter values sync
+ const syncWithActiveFilters = useCallback(() => {
+ const newSelection = new Set(activeFilterValue);
+ if (
+ newSelection.size === filterValues.length &&
+ filter?.showSelectAll !== false
+ ) {
+ newSelection.add("all");
+ }
+ setGroupSelected(newSelection);
+ }, [activeFilterValue, filterValues, filter?.showSelectAll]);
+
+ const resetComponentState = useCallback(() => {
+ setGroupSelected(new Set());
+ hasUserInteracted.current = false;
+ }, []);
+
+ const applyDefaultValues = useCallback(() => {
+ if (filter?.defaultToSelectAll && filterValues.length > 0) {
+ const newSelection = new Set(filterValues);
+ if (filter?.showSelectAll !== false) {
+ newSelection.add("all");
+ }
+ setGroupSelected(newSelection);
+ } else if (filter?.defaultValues && filter.defaultValues.length > 0) {
+ const validDefaultValues = filter.defaultValues.filter((value) =>
+ filterValues.includes(value),
+ );
+ const newSelection = new Set(validDefaultValues);
+
+ // Add "all" if all items are selected and showSelectAll is not false
if (
- newSelection.size === filterValues.length &&
+ validDefaultValues.length === filterValues.length &&
filter?.showSelectAll !== false
) {
newSelection.add("all");
}
setGroupSelected(newSelection);
- } else if (!hasUserInteracted.current) {
- // Handle default behavior when no URL params exist
- // Only apply defaults if user hasn't interacted yet
- // Only set visual state, don't trigger URL changes automatically
- if (filter?.defaultToSelectAll && filterValues.length > 0) {
- const newSelection = new Set(filterValues);
- if (filter?.showSelectAll !== false) {
- newSelection.add("all");
- }
- setGroupSelected(newSelection);
- // DON'T notify parent automatically - wait for user interaction
- } else if (filter?.defaultValues && filter.defaultValues.length > 0) {
- // Handle specific default values
- const validDefaultValues = filter.defaultValues.filter((value) =>
- filterValues.includes(value),
- );
- const newSelection = new Set(validDefaultValues);
-
- // Add "all" if all items are selected and showSelectAll is not false
- if (
- validDefaultValues.length === filterValues.length &&
- filter?.showSelectAll !== false
- ) {
- newSelection.add("all");
- }
-
- setGroupSelected(newSelection);
- // DON'T notify parent automatically - wait for user interaction
- } else {
- setGroupSelected(new Set());
- }
+ } else {
+ setGroupSelected(new Set());
}
}, [
- activeFilterValue,
filterValues,
filter?.defaultToSelectAll,
filter?.defaultValues,
filter?.showSelectAll,
]);
+ useEffect(() => {
+ const hasActiveFilters = activeFilterValue.length > 0;
+ const userHasInteracted = hasUserInteracted.current;
+
+ if (hasActiveFilters) {
+ // URL has filter values - sync component state with URL
+ syncWithActiveFilters();
+ } else if (userHasInteracted) {
+ // URL has no filters but user had interacted - reset component state
+ resetComponentState();
+ } else {
+ // URL has no filters and user hasn't interacted - apply defaults
+ applyDefaultValues();
+ }
+ }, [
+ activeFilterValue,
+ syncWithActiveFilters,
+ resetComponentState,
+ applyDefaultValues,
+ ]);
+
const updateSelection = useCallback(
(newValues: string[]) => {
// Mark that user has interacted with the filter
diff --git a/ui/hooks/use-url-filters.ts b/ui/hooks/use-url-filters.ts
index c29de37327..83bd225791 100644
--- a/ui/hooks/use-url-filters.ts
+++ b/ui/hooks/use-url-filters.ts
@@ -16,8 +16,10 @@ export const useUrlFilters = () => {
(key: string, value: string | string[] | null) => {
const params = new URLSearchParams(searchParams.toString());
- // Always reset page to 1 when a filter is applied
- params.set("page", "1");
+ // Only reset page to 1 if page parameter already exists
+ if (params.has("page")) {
+ params.set("page", "1");
+ }
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
@@ -40,7 +42,11 @@ export const useUrlFilters = () => {
const filterKey = key.startsWith("filter[") ? key : `filter[${key}]`;
params.delete(filterKey);
- params.set("page", "1");
+
+ // Only reset page to 1 if page parameter already exists
+ if (params.has("page")) {
+ params.set("page", "1");
+ }
router.push(`${pathname}?${params.toString()}`, { scroll: false });
},
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..c62933b635 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";
@@ -9,11 +10,14 @@ import {
CategoryData,
FailedSection,
Framework,
- RegionData,
Requirement,
RequirementsData,
} from "@/types/compliance";
+import {
+ mapComplianceData as mapAWSWellArchitectedComplianceData,
+ toAccordionItems as toAWSWellArchitectedAccordionItems,
+} from "./aws-well-architected";
import {
mapComplianceData as mapCISComplianceData,
toAccordionItems as toCISAccordionItems,
@@ -101,6 +105,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)
@@ -119,85 +137,6 @@ export const getComplianceMapper = (framework?: string): ComplianceMapper => {
return complianceMappers[framework] || defaultMapper;
};
-export const calculateRegionHeatmapData = async (
- complianceId: string,
- scanId: string,
- uniqueRegions: string[],
- attributesData: AttributesData,
- mapper: ComplianceMapper,
-): Promise => {
- if (!complianceId || !scanId || !uniqueRegions?.length) {
- return [];
- }
-
- try {
- const { getComplianceRequirements } = await import("@/actions/compliances");
-
- // Get data for each region in parallel
- const regionPromises = uniqueRegions.map(async (region) => {
- try {
- // Only need to fetch requirements data per region
- const regionRequirementsData = await getComplianceRequirements({
- complianceId,
- scanId,
- region, // Filter by specific region
- });
-
- // Map the data using the provided mapper
- const mappedData = mapper.mapComplianceData(
- attributesData,
- regionRequirementsData,
- );
-
- // Calculate totals for this region
- const regionTotals = mappedData.reduce(
- (acc, framework) => ({
- pass: acc.pass + framework.pass,
- fail: acc.fail + framework.fail,
- manual: acc.manual + framework.manual,
- }),
- { pass: 0, fail: 0, manual: 0 },
- );
-
- const totalRequirements =
- regionTotals.pass + regionTotals.fail + regionTotals.manual;
- const failurePercentage =
- totalRequirements > 0
- ? Math.round((regionTotals.fail / totalRequirements) * 100)
- : 0;
-
- return {
- name: region,
- failurePercentage,
- totalRequirements,
- failedRequirements: regionTotals.fail,
- };
- } catch (error) {
- console.error(`Error fetching data for region ${region}:`, error);
- return {
- name: region,
- failurePercentage: 0,
- totalRequirements: 0,
- failedRequirements: 0,
- };
- }
- });
-
- const regionData = await Promise.all(regionPromises);
-
- // Filter, sort and limit to top 9 regions for 3x3 grid
- const filteredData = regionData
- .filter((region) => region.totalRequirements > 0)
- .sort((a, b) => b.failurePercentage - a.failurePercentage)
- .slice(0, 9);
-
- return filteredData;
- } catch (error) {
- console.error("Error calculating region heatmap data:", error);
- return [];
- }
-};
-
export const calculateCategoryHeatmapData = (
complianceData: Framework[],
): CategoryData[] => {
diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts
index 4fa4cf7e1a..994c90d200 100644
--- a/ui/types/compliance.ts
+++ b/ui/types/compliance.ts
@@ -1,16 +1,5 @@
export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings";
-export type ComplianceId =
- | "ens_rd2022_aws"
- | "iso27001_2013_aws"
- | "iso27001_2022_aws"
- | "cis_1.4_aws"
- | "cis_1.5_aws"
- | "cis_2.0_aws"
- | "cis_3.0_aws"
- | "cis_4.0_aws"
- | "cis_5.0_aws";
-
export interface CompliancesOverview {
data: ComplianceOverviewData[];
}
@@ -112,6 +101,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 +121,11 @@ export interface AttributesItemData {
version: string;
description: string;
attributes: {
- metadata: ENSAttributesMetadata[] | ISO27001AttributesMetadata[];
+ metadata:
+ | ENSAttributesMetadata[]
+ | ISO27001AttributesMetadata[]
+ | CISAttributesMetadata[]
+ | AWSWellArchitectedAttributesMetadata[];
check_ids: string[];
};
};