mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement
This commit is contained in:
+8
-1
@@ -17,6 +17,13 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.3] (Prowler v5.7.3)
|
||||
|
||||
### Fixed
|
||||
- Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916).
|
||||
|
||||
---
|
||||
|
||||
## [v1.8.2] (Prowler v5.7.2)
|
||||
|
||||
### Fixed
|
||||
@@ -30,7 +37,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
## [v1.8.1] (Prowler v5.7.1)
|
||||
|
||||
### Fixed
|
||||
- Added database index to improve performance on finding lookup. [(#7800)](https://github.com/prowler-cloud/prowler/pull/7800)
|
||||
- Added database index to improve performance on finding lookup [(#7800)](https://github.com/prowler-cloud/prowler/pull/7800).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
@@ -47,10 +46,6 @@ class BaseViewSet(ModelViewSet):
|
||||
|
||||
|
||||
class BaseRLSViewSet(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# Ideally, this logic would be in the `.setup()` method but DRF view sets don't call it
|
||||
# https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.setup
|
||||
@@ -61,9 +56,21 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
self.request.tenant_id = tenant_id
|
||||
|
||||
self._rls_cm = rls_transaction(tenant_id)
|
||||
self._rls_cm.__enter__()
|
||||
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
if hasattr(self, "_rls_cm"):
|
||||
self._rls_cm.__exit__(None, None, None)
|
||||
del self._rls_cm
|
||||
|
||||
return response
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
@@ -73,8 +80,7 @@ class BaseRLSViewSet(BaseViewSet):
|
||||
|
||||
class BaseTenantViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
tenant = super().dispatch(request, *args, **kwargs)
|
||||
tenant = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
try:
|
||||
# If the request is a POST, create the admin role
|
||||
@@ -117,15 +123,23 @@ class BaseTenantViewset(BaseViewSet):
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
user_id = str(request.user.id)
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
self._rls_cm = rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR)
|
||||
self._rls_cm.__enter__()
|
||||
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
if hasattr(self, "_rls_cm"):
|
||||
self._rls_cm.__exit__(None, None, None)
|
||||
del self._rls_cm
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class BaseUserViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# TODO refactor after improving RLS on users
|
||||
if request.stream is not None and request.stream.method == "POST":
|
||||
@@ -137,6 +151,18 @@ class BaseUserViewset(BaseViewSet):
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
self.request.tenant_id = tenant_id
|
||||
|
||||
self._rls_cm = rls_transaction(tenant_id)
|
||||
self._rls_cm.__enter__()
|
||||
|
||||
super().initial(request, *args, **kwargs)
|
||||
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
response = super().finalize_response(request, response, *args, **kwargs)
|
||||
|
||||
if hasattr(self, "_rls_cm"):
|
||||
self._rls_cm.__exit__(None, None, None)
|
||||
del self._rls_cm
|
||||
|
||||
return response
|
||||
|
||||
@@ -10,6 +10,9 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Improved `SnippetChip` component and show resource name in new findings table. [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813)
|
||||
- Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
|
||||
- Add GCP credential method (Account Service Key). [(#7872)](https://github.com/prowler-cloud/prowler/pull/7872)
|
||||
- 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)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -9,38 +9,49 @@ import {
|
||||
} from "@/actions/compliances";
|
||||
import { getProvider } from "@/actions/providers";
|
||||
import { getScans } from "@/actions/scans";
|
||||
import { ClientAccordionWrapper } from "@/components/compliance/compliance-accordion/client-accordion-wrapper";
|
||||
import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header";
|
||||
import { SkeletonAccordion } from "@/components/compliance/compliance-skeleton-accordion";
|
||||
import { FailedSectionsChart } from "@/components/compliance/failed-sections-chart";
|
||||
import { FailedSectionsChartSkeleton } from "@/components/compliance/failed-sections-chart-skeleton";
|
||||
import { RequirementsChart } from "@/components/compliance/requirements-chart";
|
||||
import { RequirementsChartSkeleton } from "@/components/compliance/requirements-chart-skeleton";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { mapComplianceData, toAccordionItems } from "@/lib/compliance/ens";
|
||||
import { ScanProps } from "@/types";
|
||||
import {
|
||||
FailedSection,
|
||||
MappedComplianceData,
|
||||
RequirementsTotals,
|
||||
} from "@/types/compliance";
|
||||
BarChart,
|
||||
BarChartSkeleton,
|
||||
ClientAccordionWrapper,
|
||||
ComplianceHeader,
|
||||
HeatmapChart,
|
||||
HeatmapChartSkeleton,
|
||||
PieChart,
|
||||
PieChartSkeleton,
|
||||
SkeletonAccordion,
|
||||
} from "@/components/compliance";
|
||||
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import {
|
||||
calculateCategoryHeatmapData,
|
||||
calculateRegionHeatmapData,
|
||||
getComplianceMapper,
|
||||
} from "@/lib/compliance/commons";
|
||||
import { ScanProps } from "@/types";
|
||||
import { Framework, RequirementsTotals } from "@/types/compliance";
|
||||
|
||||
interface ComplianceDetailSearchParams {
|
||||
complianceId: string;
|
||||
version?: string;
|
||||
scanId?: string;
|
||||
"filter[region__in]"?: string;
|
||||
"filter[cis_profile_level]"?: string;
|
||||
}
|
||||
|
||||
const Logo = ({ logoPath }: { logoPath: string }) => {
|
||||
const ComplianceIconSmall = ({
|
||||
logoPath,
|
||||
title,
|
||||
}: {
|
||||
logoPath: string;
|
||||
title: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="relative ml-auto hidden h-[200px] w-[200px] flex-shrink-0 md:block">
|
||||
<div className="relative h-6 w-6 flex-shrink-0">
|
||||
<Image
|
||||
src={logoPath}
|
||||
alt="Compliance Logo"
|
||||
alt={`${title} logo`}
|
||||
fill
|
||||
priority
|
||||
className="object-contain"
|
||||
className="h-10 w-10 min-w-10 rounded-md border-1 border-gray-300 bg-white object-contain p-[2px]"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -48,17 +59,13 @@ const Logo = ({ logoPath }: { logoPath: string }) => {
|
||||
|
||||
const ChartsWrapper = ({
|
||||
children,
|
||||
logoPath,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
logoPath: string;
|
||||
logoPath?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-8 flex w-full">
|
||||
<div className="flex flex-col items-center gap-16 lg:flex-row">
|
||||
{children}
|
||||
</div>
|
||||
{logoPath && <Logo logoPath={logoPath} />}
|
||||
<div className="mb-8 flex w-full flex-col items-center justify-between lg:flex-row">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -73,8 +80,8 @@ export default async function ComplianceDetail({
|
||||
const { compliancetitle } = params;
|
||||
const { complianceId, version, scanId } = searchParams;
|
||||
const regionFilter = searchParams["filter[region__in]"];
|
||||
|
||||
const logoPath = `/${compliancetitle.toLowerCase()}.png`;
|
||||
const cisProfileFilter = searchParams["filter[cis_profile_level]"];
|
||||
const logoPath = getComplianceIcon(compliancetitle);
|
||||
|
||||
// Create a key that includes region filter for Suspense
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
@@ -92,31 +99,33 @@ export default async function ComplianceDetail({
|
||||
});
|
||||
|
||||
// Expand scans with provider information
|
||||
const expandedScansData = await Promise.all(
|
||||
scansData.data.map(async (scan: ScanProps) => {
|
||||
const providerId = scan.relationships?.provider?.data?.id;
|
||||
const expandedScansData = scansData?.data?.length
|
||||
? await Promise.all(
|
||||
scansData.data.map(async (scan: ScanProps) => {
|
||||
const providerId = scan.relationships?.provider?.data?.id;
|
||||
|
||||
if (!providerId) {
|
||||
return { ...scan, providerInfo: null };
|
||||
}
|
||||
if (!providerId) {
|
||||
return { ...scan, providerInfo: null };
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
const formData = new FormData();
|
||||
formData.append("id", providerId);
|
||||
|
||||
const providerData = await getProvider(formData);
|
||||
const providerData = await getProvider(formData);
|
||||
|
||||
return {
|
||||
...scan,
|
||||
providerInfo: providerData?.data
|
||||
? {
|
||||
provider: providerData.data.attributes.provider,
|
||||
uid: providerData.data.attributes.uid,
|
||||
alias: providerData.data.attributes.alias,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
return {
|
||||
...scan,
|
||||
providerInfo: providerData?.data
|
||||
? {
|
||||
provider: providerData.data.attributes.provider,
|
||||
uid: providerData.data.attributes.uid,
|
||||
alias: providerData.data.attributes.alias,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
const selectedScanId = scanId || expandedScansData[0]?.id || null;
|
||||
|
||||
@@ -130,11 +139,21 @@ export default async function ComplianceDetail({
|
||||
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
|
||||
|
||||
return (
|
||||
<ContentLayout title={pageTitle} icon="fluent-mdl2:compliance-audit">
|
||||
<ContentLayout
|
||||
title={pageTitle}
|
||||
icon={
|
||||
logoPath ? (
|
||||
<ComplianceIconSmall logoPath={logoPath} title={compliancetitle} />
|
||||
) : (
|
||||
"fluent-mdl2:compliance-audit"
|
||||
)
|
||||
}
|
||||
>
|
||||
<ComplianceHeader
|
||||
scans={expandedScansData}
|
||||
uniqueRegions={uniqueRegions}
|
||||
showSearch={false}
|
||||
framework={compliancetitle}
|
||||
/>
|
||||
|
||||
<Suspense
|
||||
@@ -142,8 +161,9 @@ export default async function ComplianceDetail({
|
||||
fallback={
|
||||
<div className="space-y-8">
|
||||
<ChartsWrapper logoPath={logoPath}>
|
||||
<RequirementsChartSkeleton />
|
||||
<FailedSectionsChartSkeleton />
|
||||
<PieChartSkeleton />
|
||||
<BarChartSkeleton />
|
||||
<HeatmapChartSkeleton />
|
||||
</ChartsWrapper>
|
||||
<SkeletonAccordion />
|
||||
</div>
|
||||
@@ -153,18 +173,57 @@ export default async function ComplianceDetail({
|
||||
complianceId={complianceId}
|
||||
scanId={selectedScanId}
|
||||
region={regionFilter}
|
||||
filter={cisProfileFilter}
|
||||
logoPath={logoPath}
|
||||
uniqueRegions={uniqueRegions}
|
||||
isRegionFiltered={
|
||||
!!regionFilter &&
|
||||
regionFilter.split(",").length < uniqueRegions.length
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</ContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const getComplianceData = async (
|
||||
complianceId: string,
|
||||
scanId: string,
|
||||
region?: string,
|
||||
): Promise<MappedComplianceData> => {
|
||||
const SSRComplianceContent = async ({
|
||||
complianceId,
|
||||
scanId,
|
||||
region,
|
||||
filter,
|
||||
logoPath,
|
||||
uniqueRegions,
|
||||
isRegionFiltered,
|
||||
}: {
|
||||
complianceId: string;
|
||||
scanId: string;
|
||||
region?: string;
|
||||
filter?: string;
|
||||
logoPath?: string;
|
||||
uniqueRegions: string[];
|
||||
isRegionFiltered: boolean;
|
||||
}) => {
|
||||
if (!scanId) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<ChartsWrapper logoPath={logoPath}>
|
||||
<PieChart pass={0} fail={0} manual={0} />
|
||||
<BarChart sections={[]} />
|
||||
<HeatmapChart
|
||||
regions={[]}
|
||||
categories={[]}
|
||||
isRegionFiltered={
|
||||
!!region && region.split(",").length < uniqueRegions.length
|
||||
}
|
||||
filteredRegionName={region}
|
||||
/>
|
||||
</ChartsWrapper>
|
||||
<ClientAccordionWrapper items={[]} defaultExpandedKeys={[]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Get compliance data and attributes once
|
||||
const [attributesData, requirementsData] = await Promise.all([
|
||||
getComplianceAttributes(complianceId),
|
||||
getComplianceRequirements({
|
||||
@@ -174,89 +233,56 @@ const getComplianceData = async (
|
||||
}),
|
||||
]);
|
||||
|
||||
const mappedData = mapComplianceData(attributesData, requirementsData);
|
||||
return mappedData;
|
||||
};
|
||||
// Determine framework from the first attribute item
|
||||
const framework = attributesData?.data?.[0]?.attributes?.framework;
|
||||
const mapper = getComplianceMapper(framework);
|
||||
const data = mapper.mapComplianceData(
|
||||
attributesData,
|
||||
requirementsData,
|
||||
filter,
|
||||
);
|
||||
|
||||
const getTopFailedSections = (
|
||||
mappedData: MappedComplianceData,
|
||||
): FailedSection[] => {
|
||||
const failedSectionMap = new Map();
|
||||
// Calculate region heatmap data using already obtained data
|
||||
const regionHeatmapData = await calculateRegionHeatmapData(
|
||||
complianceId,
|
||||
scanId,
|
||||
uniqueRegions,
|
||||
attributesData,
|
||||
mapper,
|
||||
);
|
||||
const categoryHeatmapData = calculateCategoryHeatmapData(data);
|
||||
|
||||
mappedData.forEach((framework) => {
|
||||
framework.categories.forEach((category) => {
|
||||
category.controls.forEach((control) => {
|
||||
control.requirements.forEach((requirement) => {
|
||||
if (requirement.status === "FAIL") {
|
||||
const sectionName = category.name;
|
||||
|
||||
if (!failedSectionMap.has(sectionName)) {
|
||||
failedSectionMap.set(sectionName, { total: 0, types: {} });
|
||||
}
|
||||
|
||||
const sectionData = failedSectionMap.get(sectionName);
|
||||
sectionData.total += 1;
|
||||
|
||||
const type = requirement.type;
|
||||
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
|
||||
};
|
||||
|
||||
const SSRComplianceContent = async ({
|
||||
complianceId,
|
||||
scanId,
|
||||
region,
|
||||
logoPath,
|
||||
}: {
|
||||
complianceId: string;
|
||||
scanId: string;
|
||||
region?: string;
|
||||
logoPath: string;
|
||||
}) => {
|
||||
if (!scanId) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<ChartsWrapper logoPath={logoPath}>
|
||||
<RequirementsChart pass={0} fail={0} manual={0} />
|
||||
<FailedSectionsChart sections={[]} />
|
||||
</ChartsWrapper>
|
||||
<ClientAccordionWrapper items={[]} defaultExpandedKeys={[]} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const data = await getComplianceData(complianceId, scanId, region);
|
||||
const totalRequirements: RequirementsTotals = data.reduce(
|
||||
(acc, framework) => ({
|
||||
(acc: RequirementsTotals, framework: Framework) => ({
|
||||
pass: acc.pass + framework.pass,
|
||||
fail: acc.fail + framework.fail,
|
||||
manual: acc.manual + framework.manual,
|
||||
}),
|
||||
{ pass: 0, fail: 0, manual: 0 },
|
||||
);
|
||||
const topFailedSections = getTopFailedSections(data);
|
||||
const accordionItems = toAccordionItems(data, scanId);
|
||||
const defaultKeys = accordionItems.slice(0, 2).map((item) => item.key);
|
||||
|
||||
const accordionItems = mapper.toAccordionItems(data, scanId);
|
||||
const topFailedSections = mapper.getTopFailedSections(data);
|
||||
|
||||
// Todo: rethink as every compliance has a different number of items
|
||||
// const defaultKeys = accordionItems.slice(0, 2).map((item) => item.key);
|
||||
const defaultKeys = [""];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<ChartsWrapper logoPath={logoPath}>
|
||||
<RequirementsChart
|
||||
<PieChart
|
||||
pass={totalRequirements.pass}
|
||||
fail={totalRequirements.fail}
|
||||
manual={totalRequirements.manual}
|
||||
/>
|
||||
<FailedSectionsChart sections={topFailedSections} />
|
||||
<BarChart sections={topFailedSections} />
|
||||
<HeatmapChart
|
||||
regions={regionHeatmapData}
|
||||
categories={categoryHeatmapData}
|
||||
isRegionFiltered={isRegionFiltered}
|
||||
filteredRegionName={region}
|
||||
/>
|
||||
</ChartsWrapper>
|
||||
|
||||
<Spacer className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-800" />
|
||||
|
||||
@@ -11,19 +11,22 @@ import {
|
||||
import { Accordion } from "@/components/ui/accordion/Accordion";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { createDict } from "@/lib";
|
||||
import { getComplianceMapper } from "@/lib/compliance/commons";
|
||||
import { ComplianceId, Requirement } from "@/types/compliance";
|
||||
import { FindingProps, FindingsResponse } from "@/types/components";
|
||||
|
||||
import { ComplianceCustomDetails } from "../compliance-custom-details/ens-details";
|
||||
|
||||
interface ClientAccordionContentProps {
|
||||
requirement: Requirement;
|
||||
scanId: string;
|
||||
framework: string;
|
||||
disableFindings?: boolean;
|
||||
}
|
||||
|
||||
export const ClientAccordionContent = ({
|
||||
requirement,
|
||||
framework,
|
||||
scanId,
|
||||
disableFindings = false,
|
||||
}: ClientAccordionContentProps) => {
|
||||
const [findings, setFindings] = useState<FindingsResponse | null>(null);
|
||||
const [expandedFindings, setExpandedFindings] = useState<FindingProps[]>([]);
|
||||
@@ -40,6 +43,7 @@ export const ClientAccordionContent = ({
|
||||
useEffect(() => {
|
||||
async function loadFindings() {
|
||||
if (
|
||||
!disableFindings &&
|
||||
requirement.check_ids?.length > 0 &&
|
||||
requirement.status !== "No findings" &&
|
||||
(loadedPageRef.current !== pageNumber ||
|
||||
@@ -95,7 +99,29 @@ export const ClientAccordionContent = ({
|
||||
}
|
||||
|
||||
loadFindings();
|
||||
}, [requirement, scanId, pageNumber, sort, region]);
|
||||
}, [requirement, scanId, pageNumber, sort, region, disableFindings]);
|
||||
|
||||
const renderDetails = () => {
|
||||
if (!complianceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mapper = getComplianceMapper(framework);
|
||||
const detailsComponent = mapper.getDetailsComponent(requirement);
|
||||
|
||||
return <div className="w-full">{detailsComponent}</div>;
|
||||
};
|
||||
|
||||
if (disableFindings) {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{renderDetails()}
|
||||
<p className="text-sm text-gray-500">
|
||||
This requirement has no checks; therefore, there are no findings.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const checks = requirement.check_ids || [];
|
||||
const checksList = (
|
||||
@@ -141,23 +167,6 @@ export const ClientAccordionContent = ({
|
||||
return <div>There are no findings for this regions</div>;
|
||||
};
|
||||
|
||||
const renderDetails = () => {
|
||||
if (!complianceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (complianceId) {
|
||||
case "ens_rd2022_aws":
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ComplianceCustomDetails requirement={requirement} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{renderDetails()}
|
||||
|
||||
+1
-6
@@ -1,5 +1,4 @@
|
||||
import { FindingStatus, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { translateType } from "@/lib/compliance/ens";
|
||||
|
||||
interface ComplianceAccordionRequirementTitleProps {
|
||||
type: string;
|
||||
@@ -8,17 +7,13 @@ interface ComplianceAccordionRequirementTitleProps {
|
||||
}
|
||||
|
||||
export const ComplianceAccordionRequirementTitle = ({
|
||||
type,
|
||||
name,
|
||||
status,
|
||||
}: ComplianceAccordionRequirementTitleProps) => {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex w-3/4 items-center gap-1">
|
||||
<span className="whitespace-nowrap text-sm font-bold capitalize">
|
||||
{translateType(type)}:
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-sm uppercase">{name}</span>
|
||||
<span className="whitespace-nowrap text-sm">{name}</span>
|
||||
</div>
|
||||
<StatusFindingBadge status={status} />
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ interface ComplianceAccordionTitleProps {
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual?: number;
|
||||
isParentLevel?: boolean;
|
||||
}
|
||||
|
||||
export const ComplianceAccordionTitle = ({
|
||||
@@ -12,6 +13,7 @@ export const ComplianceAccordionTitle = ({
|
||||
pass,
|
||||
fail,
|
||||
manual = 0,
|
||||
isParentLevel = false,
|
||||
}: ComplianceAccordionTitleProps) => {
|
||||
const total = pass + fail + manual;
|
||||
const passPercentage = (pass / total) * 100;
|
||||
@@ -30,7 +32,7 @@ export const ComplianceAccordionTitle = ({
|
||||
</div>
|
||||
<div className="mr-4 flex items-center gap-2">
|
||||
<div className="hidden lg:block">
|
||||
{total > 0 && (
|
||||
{total > 0 && isParentLevel && (
|
||||
<span className="whitespace-nowrap text-xs font-medium text-gray-600">
|
||||
Requirements:
|
||||
</span>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
|
||||
const navigateToDetail = () => {
|
||||
// We will unlock this while developing the rest of complainces.
|
||||
if (!id.includes("ens") && !id.includes("cis")) {
|
||||
if (!id.includes("ens") && !id.includes("iso") && !id.includes("cis_")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+89
-46
@@ -3,7 +3,7 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
BarChart as RechartsBarChart,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
@@ -12,17 +12,10 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
import { translateType } from "@/lib/compliance/ens";
|
||||
|
||||
type FailedSectionItem = {
|
||||
name: string;
|
||||
total: number;
|
||||
types: {
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
import { FailedSection } from "@/types/compliance";
|
||||
|
||||
interface FailedSectionsListProps {
|
||||
sections: FailedSectionItem[];
|
||||
sections: FailedSection[];
|
||||
}
|
||||
|
||||
const title = (
|
||||
@@ -31,7 +24,7 @@ const title = (
|
||||
</h3>
|
||||
);
|
||||
|
||||
export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
export const BarChart = ({ sections }: FailedSectionsListProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
@@ -43,7 +36,7 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
case "refuerzo":
|
||||
return "#7FB5FF"; // Increased contrast from #B5D7FF
|
||||
default:
|
||||
return "#868994";
|
||||
return "#ff5356";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,9 +49,28 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
}));
|
||||
|
||||
const allTypes = Array.from(
|
||||
new Set(sections.flatMap((section) => Object.keys(section.types))),
|
||||
new Set(sections.flatMap((section) => Object.keys(section.types || {}))),
|
||||
);
|
||||
|
||||
// Add empty bars to complete up to 5 bars for better distribution
|
||||
while (chartData.length < 5) {
|
||||
const emptyBar: any = { name: "" };
|
||||
allTypes.forEach((type) => {
|
||||
emptyBar[type] = 0;
|
||||
});
|
||||
chartData.push(emptyBar);
|
||||
}
|
||||
|
||||
// Calculate the maximum value to ensure proper scaling
|
||||
const maxValue = Math.max(
|
||||
...chartData.map((item) =>
|
||||
allTypes.reduce((sum, type) => sum + ((item as any)[type] || 0), 0),
|
||||
),
|
||||
);
|
||||
|
||||
// Set minimum domain to ensure bars are always visible
|
||||
const domainMax = Math.max(maxValue, 1);
|
||||
|
||||
// Check if there are no failed sections
|
||||
if (!sections || sections.length === 0) {
|
||||
return (
|
||||
@@ -72,22 +84,25 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[600px]">
|
||||
{title}
|
||||
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
|
||||
<div>{title}</div>
|
||||
|
||||
<div className="h-[320px] w-full">
|
||||
<div className="h-full w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
<RechartsBarChart
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, left: 40, bottom: 5 }}
|
||||
maxBarSize={40}
|
||||
margin={{ top: 12, bottom: 0 }}
|
||||
maxBarSize={32}
|
||||
>
|
||||
<XAxis
|
||||
type="number"
|
||||
fontSize={12}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
allowDecimals={false}
|
||||
hide={true}
|
||||
domain={[0, domainMax]}
|
||||
tick={{
|
||||
fontSize: 12,
|
||||
fill: theme === "dark" ? "#94a3b8" : "#374151",
|
||||
@@ -96,43 +111,57 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
<YAxis
|
||||
type="category"
|
||||
dataKey="name"
|
||||
width={100}
|
||||
width={1}
|
||||
tick={{
|
||||
fontSize: 12,
|
||||
fill: theme === "dark" ? "#94a3b8" : "#374151",
|
||||
textAnchor: "start",
|
||||
style: {
|
||||
transform: "translateX(10px) translateY(-26px)",
|
||||
},
|
||||
width: 400,
|
||||
}}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: theme === "dark" ? "#1e293b" : "white",
|
||||
border: `1px solid ${theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)"}`,
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
fontSize: "12px",
|
||||
padding: "8px 12px",
|
||||
color: theme === "dark" ? "white" : "black",
|
||||
content={(props) => {
|
||||
if (!props.active || !props.payload || !props.payload.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = props.payload[0].payload;
|
||||
if (!data.name || data.name === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasValues = allTypes.some((type) => data[type] > 0);
|
||||
if (!hasValues) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme === "dark" ? "#1e293b" : "white",
|
||||
border: `1px solid ${theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)"}`,
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
fontSize: "12px",
|
||||
padding: "8px 12px",
|
||||
color: theme === "dark" ? "white" : "black",
|
||||
}}
|
||||
>
|
||||
{props.payload.map((entry: any, index: number) => (
|
||||
<div key={index} style={{ color: entry.color }}>
|
||||
{translateType(entry.dataKey)}: {entry.value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
formatter={(value: number, name: string) => [
|
||||
value,
|
||||
translateType(name),
|
||||
]}
|
||||
cursor={false}
|
||||
/>
|
||||
<Legend
|
||||
formatter={(value) => translateType(value)}
|
||||
wrapperStyle={{
|
||||
fontSize: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
iconType="circle"
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
/>
|
||||
{allTypes.map((type, i) => (
|
||||
<Bar
|
||||
key={type}
|
||||
@@ -142,7 +171,21 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
radius={i === allTypes.length - 1 ? [0, 4, 4, 0] : [0, 0, 0, 0]}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
<Legend
|
||||
formatter={(value) => translateType(value)}
|
||||
wrapperStyle={{
|
||||
fontSize: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
paddingTop: "16px",
|
||||
marginBottom: "16px",
|
||||
}}
|
||||
iconType="circle"
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
/>
|
||||
</RechartsBarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,165 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useState } from "react";
|
||||
|
||||
import { CategoryData, RegionData } from "@/types/compliance";
|
||||
|
||||
interface HeatmapChartProps {
|
||||
regions: RegionData[];
|
||||
categories?: CategoryData[];
|
||||
isRegionFiltered?: boolean; // Indicates if a region filter is active
|
||||
filteredRegionName?: string; // Name of the filtered region
|
||||
}
|
||||
|
||||
const getHeatmapColor = (percentage: number): string => {
|
||||
if (percentage === 0) return "#10b981"; // Green for 0% failures
|
||||
if (percentage <= 25) return "#eab308"; // Yellow
|
||||
if (percentage <= 50) return "#f97316"; // Orange
|
||||
if (percentage <= 100) return "#ef4444"; // Red
|
||||
return "#ef4444";
|
||||
};
|
||||
|
||||
const capitalizeFirstLetter = (text: string): string => {
|
||||
const lowerText = text.toLowerCase();
|
||||
const firstLetterIndex = lowerText.search(/[a-zA-Z]/);
|
||||
if (firstLetterIndex === -1) return text; // No letters found
|
||||
|
||||
return (
|
||||
lowerText.slice(0, firstLetterIndex) +
|
||||
lowerText.charAt(firstLetterIndex).toUpperCase() +
|
||||
lowerText.slice(firstLetterIndex + 1)
|
||||
);
|
||||
};
|
||||
|
||||
export const HeatmapChart = ({
|
||||
regions,
|
||||
categories = [],
|
||||
isRegionFiltered = false,
|
||||
}: HeatmapChartProps) => {
|
||||
const { theme } = useTheme();
|
||||
const [hoveredItem, setHoveredItem] = useState<
|
||||
RegionData | CategoryData | null
|
||||
>(null);
|
||||
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
// Determine what data to show and prepare it
|
||||
const dataToShow = isRegionFiltered ? categories : regions;
|
||||
const heatmapData = dataToShow
|
||||
.filter((item) => item.totalRequirements > 0)
|
||||
.sort((a, b) => b.failurePercentage - a.failurePercentage)
|
||||
.slice(0, 9); // Exactly 9 items for 3x3 grid
|
||||
|
||||
// Check if there are no items with data
|
||||
if (!dataToShow || dataToShow.length === 0 || heatmapData.length === 0) {
|
||||
const noDataMessage = isRegionFiltered
|
||||
? "No category data available"
|
||||
: "No regional data available";
|
||||
|
||||
return (
|
||||
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[400px]">
|
||||
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide">
|
||||
{isRegionFiltered
|
||||
? "Categories Failure Rate"
|
||||
: "Failure Rate by Region"}
|
||||
</h3>
|
||||
<div className="flex h-[320px] w-full items-center justify-center">
|
||||
<p className="text-sm text-gray-500">{noDataMessage}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleMouseEnter = (
|
||||
item: RegionData | CategoryData,
|
||||
event: React.MouseEvent,
|
||||
) => {
|
||||
setHoveredItem(item);
|
||||
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
const handleMouseMove = (event: React.MouseEvent) => {
|
||||
setMousePosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setHoveredItem(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
|
||||
<div>
|
||||
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide">
|
||||
{isRegionFiltered
|
||||
? "Categories Failure Rate"
|
||||
: "Failure Rate by Region"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full p-4">
|
||||
{/* 3x3 Grid */}
|
||||
<div className="grid h-full w-full grid-cols-3 gap-1">
|
||||
{heatmapData.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="flex items-center justify-center rounded border"
|
||||
style={{
|
||||
backgroundColor: getHeatmapColor(item.failurePercentage),
|
||||
borderColor: theme === "dark" ? "#374151" : "#e5e7eb",
|
||||
}}
|
||||
onMouseEnter={(e) => handleMouseEnter(item, e)}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div
|
||||
className="text-xs font-semibold"
|
||||
style={{
|
||||
color: theme === "dark" ? "#ffffff" : "#000000",
|
||||
}}
|
||||
>
|
||||
{isRegionFiltered
|
||||
? capitalizeFirstLetter(item.name)
|
||||
: item.name}
|
||||
</div>
|
||||
<div
|
||||
className="text-xs"
|
||||
style={{
|
||||
color: theme === "dark" ? "#ffffff" : "#000000",
|
||||
}}
|
||||
>
|
||||
{item.failurePercentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Custom Tooltip */}
|
||||
{hoveredItem && (
|
||||
<div
|
||||
className="pointer-events-none fixed z-50 rounded border px-3 py-2 text-xs shadow-lg"
|
||||
style={{
|
||||
left: mousePosition.x + 10,
|
||||
top: mousePosition.y - 10,
|
||||
backgroundColor: theme === "dark" ? "#1e293b" : "white",
|
||||
borderColor: theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)",
|
||||
color: theme === "dark" ? "white" : "black",
|
||||
}}
|
||||
>
|
||||
<div className="mb-1 font-semibold">
|
||||
{isRegionFiltered
|
||||
? capitalizeFirstLetter(hoveredItem.name)
|
||||
: hoveredItem.name}
|
||||
</div>
|
||||
<div>Failure Rate: {hoveredItem.failurePercentage}%</div>
|
||||
<div>
|
||||
Failed: {hoveredItem.failedRequirements}/
|
||||
{hoveredItem.totalRequirements}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+11
-9
@@ -1,11 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { Cell, Label, Pie, PieChart, Tooltip } from "recharts";
|
||||
import {
|
||||
Cell,
|
||||
Label,
|
||||
Pie,
|
||||
PieChart as RechartsPieChart,
|
||||
Tooltip,
|
||||
} from "recharts";
|
||||
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart/Chart";
|
||||
|
||||
interface RequirementsChartProps {
|
||||
interface PieChartProps {
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
@@ -29,11 +35,7 @@ const chartConfig = {
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const RequirementsChart = ({
|
||||
pass,
|
||||
fail,
|
||||
manual,
|
||||
}: RequirementsChartProps) => {
|
||||
export const PieChart = ({ pass, fail, manual }: PieChartProps) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
const chartData = [
|
||||
@@ -119,7 +121,7 @@ export const RequirementsChart = ({
|
||||
config={chartConfig}
|
||||
className="aspect-square w-[200px] min-w-[200px]"
|
||||
>
|
||||
<PieChart>
|
||||
<RechartsPieChart>
|
||||
<Tooltip
|
||||
cursor={false}
|
||||
content={<CustomTooltip active={false} payload={[]} />}
|
||||
@@ -168,7 +170,7 @@ export const RequirementsChart = ({
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</RechartsPieChart>
|
||||
</ChartContainer>
|
||||
|
||||
<div className="mt-2 grid grid-cols-3 gap-4">
|
||||
@@ -0,0 +1,150 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
interface CISDetailsProps {
|
||||
requirement: Requirement;
|
||||
}
|
||||
|
||||
export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
const processReferences = (
|
||||
references: string | number | string[] | undefined,
|
||||
): string[] => {
|
||||
if (typeof references !== "string") return [];
|
||||
|
||||
// Use regex to extract all URLs that start with https://
|
||||
const urlRegex = /https:\/\/[^:]+/g;
|
||||
const urls = references.match(urlRegex);
|
||||
|
||||
return urls || [];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{requirement.profile && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Profile Level
|
||||
</h4>
|
||||
<p className="text-sm">{requirement.profile}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.subsection && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
SubSection
|
||||
</h4>
|
||||
<p className="text-sm">{requirement.subsection}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.assessment_status && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Assessment Status
|
||||
</h4>
|
||||
<p className="text-sm">{requirement.assessment_status}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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.rationale_statement && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Rationale Statement
|
||||
</h4>
|
||||
<p className="text-sm">{requirement.rationale_statement}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.impact_statement && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Impact Statement
|
||||
</h4>
|
||||
<p className="text-sm">{requirement.impact_statement}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.remediation_procedure &&
|
||||
typeof requirement.remediation_procedure === "string" && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Remediation Procedure
|
||||
</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">
|
||||
<ReactMarkdown>{requirement.remediation_procedure}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.audit_procedure &&
|
||||
typeof requirement.audit_procedure === "string" && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Audit Procedure
|
||||
</h4>
|
||||
{/* eslint-disable-next-line */}
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
<ReactMarkdown>{requirement.audit_procedure}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.additional_information && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Additional Information
|
||||
</h4>
|
||||
<p className="whitespace-pre-wrap text-sm">
|
||||
{requirement.additional_information}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.default_value && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
Default Value
|
||||
</h4>
|
||||
<p className="text-sm">{requirement.default_value}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{requirement.references && (
|
||||
<div>
|
||||
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
|
||||
References
|
||||
</h4>
|
||||
<div className="text-sm">
|
||||
{processReferences(requirement.references).map(
|
||||
(url: string, index: number) => (
|
||||
<div key={index}>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
{url}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { translateType } from "@/lib/compliance/ens";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
export const ComplianceCustomDetails = ({
|
||||
export const ENSCustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
@@ -11,27 +12,35 @@ export const ComplianceCustomDetails = ({
|
||||
{requirement.description}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Type:</span>
|
||||
<span className="capitalize">
|
||||
{translateType(requirement.type as string)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Level:</span>
|
||||
<span className="capitalize">{requirement.nivel}</span>
|
||||
</div>
|
||||
{requirement.dimensiones && requirement.dimensiones.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Dimensions:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{requirement.dimensiones.map(
|
||||
(dimension: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
|
||||
>
|
||||
{dimension}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
{requirement.dimensiones &&
|
||||
Array.isArray(requirement.dimensiones) &&
|
||||
requirement.dimensiones.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Dimensions:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{requirement.dimensiones.map(
|
||||
(dimension: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
|
||||
>
|
||||
{dimension}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
export const ISOCustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-sm text-gray-600">
|
||||
{requirement.description}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
{requirement.objetive_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Objective:</span>
|
||||
<span>{requirement.objetive_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,7 @@ interface ComplianceHeaderProps {
|
||||
uniqueRegions: string[];
|
||||
showSearch?: boolean;
|
||||
showRegionFilter?: boolean;
|
||||
framework?: string; // Framework name to show specific filters
|
||||
}
|
||||
|
||||
export const ComplianceHeader = ({
|
||||
@@ -20,25 +21,46 @@ export const ComplianceHeader = ({
|
||||
uniqueRegions,
|
||||
showSearch = true,
|
||||
showRegionFilter = true,
|
||||
framework,
|
||||
}: ComplianceHeaderProps) => {
|
||||
const frameworkFilters = [];
|
||||
|
||||
// Add CIS Profile Level filter if framework is CIS
|
||||
if (framework === "CIS") {
|
||||
frameworkFilters.push({
|
||||
key: "cis_profile_level",
|
||||
labelCheckboxGroup: "Level",
|
||||
values: ["Level 1", "Level 2"],
|
||||
index: 0, // Show first
|
||||
showSelectAll: false, // No "Select All" option since Level 2 includes Level 1
|
||||
defaultValues: ["Level 2"], // Default to Level 2 selected (which includes Level 1)
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare region filters
|
||||
const regionFilters = showRegionFilter
|
||||
? [
|
||||
{
|
||||
key: "region__in",
|
||||
labelCheckboxGroup: "Regions",
|
||||
values: uniqueRegions,
|
||||
index: 1, // Show after framework filters
|
||||
defaultToSelectAll: true, // Default to all regions selected
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const allFilters = [...frameworkFilters, ...regionFilters];
|
||||
|
||||
return (
|
||||
<>
|
||||
{showSearch && <FilterControls search />}
|
||||
<Spacer y={8} />
|
||||
<DataCompliance scans={scans} />
|
||||
{showRegionFilter && (
|
||||
{allFilters.length > 0 && (
|
||||
<>
|
||||
<Spacer y={8} />
|
||||
<DataTableFilterCustom
|
||||
filters={[
|
||||
{
|
||||
key: "region__in",
|
||||
labelCheckboxGroup: "Regions",
|
||||
values: uniqueRegions,
|
||||
},
|
||||
]}
|
||||
defaultOpen={true}
|
||||
/>
|
||||
<DataTableFilterCustom filters={allFilters} defaultOpen={true} />
|
||||
</>
|
||||
)}
|
||||
<Spacer y={12} />
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
export * from "./compliance-accordion/client-accordion-content";
|
||||
export * from "./compliance-accordion/client-accordion-wrapper";
|
||||
export * from "./compliance-accordion/compliance-accordion-requeriment-title";
|
||||
export * from "./compliance-accordion/compliance-accordion-title";
|
||||
export * from "./compliance-card";
|
||||
export * from "./compliance-charts/bar-chart";
|
||||
export * from "./compliance-charts/heatmap-chart";
|
||||
export * from "./compliance-charts/pie-chart";
|
||||
export * from "./compliance-custom-details/cis-details";
|
||||
export * from "./compliance-custom-details/ens-details";
|
||||
export * from "./compliance-custom-details/iso-details";
|
||||
export * from "./compliance-header/compliance-header";
|
||||
export * from "./compliance-header/compliance-scan-info";
|
||||
export * from "./compliance-skeleton-grid";
|
||||
export * from "./compliance-header/data-compliance";
|
||||
export * from "./compliance-header/select-scan-compliance-data";
|
||||
export * from "./no-scans-available";
|
||||
export * from "./skeletons/bar-chart-skeleton";
|
||||
export * from "./skeletons/compliance-accordion-skeleton";
|
||||
export * from "./skeletons/compliance-grid-skeleton";
|
||||
export * from "./skeletons/heatmap-chart-skeleton";
|
||||
export * from "./skeletons/pie-chart-skeleton";
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Skeleton } from "@nextui-org/react";
|
||||
|
||||
export const FailedSectionsChartSkeleton = () => {
|
||||
export const BarChartSkeleton = () => {
|
||||
return (
|
||||
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[600px]">
|
||||
{/* Title skeleton */}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { Skeleton } from "@nextui-org/react";
|
||||
|
||||
export const HeatmapChartSkeleton = () => {
|
||||
return (
|
||||
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
|
||||
{/* Title skeleton */}
|
||||
<Skeleton className="h-4 w-36 rounded-lg">
|
||||
<div className="h-4 bg-default-200" />
|
||||
</Skeleton>
|
||||
|
||||
{/* Heatmap area skeleton - 3x3 grid like the real component */}
|
||||
<div className="h-full w-full p-4">
|
||||
<div className="grid h-full w-full grid-cols-3 gap-1">
|
||||
{Array.from({ length: 9 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className="flex items-center justify-center rounded border"
|
||||
>
|
||||
<div className="h-full w-full bg-default-200" />
|
||||
</Skeleton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Skeleton } from "@nextui-org/react";
|
||||
|
||||
export const RequirementsChartSkeleton = () => {
|
||||
export const PieChartSkeleton = () => {
|
||||
return (
|
||||
<div className="flex h-[320px] flex-col items-center justify-between">
|
||||
{/* Title skeleton */}
|
||||
@@ -118,7 +118,7 @@ export const Accordion = ({
|
||||
|
||||
return (
|
||||
<NextUIAccordion
|
||||
className={cn("w-full", className)}
|
||||
className={cn("w-full !px-0", className)}
|
||||
variant={variant}
|
||||
selectionMode={selectionMode}
|
||||
selectedKeys={expandedKeys}
|
||||
@@ -137,12 +137,12 @@ export const Accordion = ({
|
||||
isDisabled={item.isDisabled}
|
||||
indicator={<ChevronDown className="text-gray-500" />}
|
||||
classNames={{
|
||||
base: index === 0 || index === 1 ? "my-2" : "my-1",
|
||||
base: index === 0 || index === 1 ? "my-1" : "my-1",
|
||||
title: "text-sm font-medium max-w-full overflow-hidden truncate",
|
||||
subtitle: "text-xs text-gray-500",
|
||||
trigger:
|
||||
"p-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center",
|
||||
content: "p-2",
|
||||
"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",
|
||||
content: "px-0 py-1",
|
||||
}}
|
||||
>
|
||||
<AccordionContent
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Suspense, use } from "react";
|
||||
import { ReactNode, Suspense, use } from "react";
|
||||
|
||||
import { getUserInfo } from "@/actions/users/users";
|
||||
|
||||
import { Navbar } from "../nav-bar/navbar";
|
||||
import { SkeletonContentLayout } from "./skeleton-content-layout";
|
||||
|
||||
interface ContentLayoutProps {
|
||||
title: string;
|
||||
icon: string;
|
||||
icon: string | ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
} from "@nextui-org/react";
|
||||
import { ChevronDown, X } from "lucide-react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
import { ComplianceScanInfo } from "@/components/compliance";
|
||||
import { CustomDropdownFilterProps } from "@/types";
|
||||
@@ -24,6 +30,7 @@ export const CustomDropdownFilter = ({
|
||||
const searchParams = useSearchParams();
|
||||
const [groupSelected, setGroupSelected] = useState(new Set<string>());
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const hasUserInteracted = useRef(false);
|
||||
|
||||
const filterValues = useMemo(() => filter?.values || [], [filter?.values]);
|
||||
const selectedValues = Array.from(groupSelected).filter(
|
||||
@@ -41,24 +48,66 @@ export const CustomDropdownFilter = ({
|
||||
useEffect(() => {
|
||||
if (activeFilterValue.length > 0) {
|
||||
const newSelection = new Set(activeFilterValue);
|
||||
if (newSelection.size === filterValues.length) {
|
||||
if (
|
||||
newSelection.size === filterValues.length &&
|
||||
filter?.showSelectAll !== false
|
||||
) {
|
||||
newSelection.add("all");
|
||||
}
|
||||
setGroupSelected(newSelection);
|
||||
} else {
|
||||
setGroupSelected(new Set());
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
}, [activeFilterValue, filterValues.length]);
|
||||
}, [
|
||||
activeFilterValue,
|
||||
filterValues,
|
||||
filter?.defaultToSelectAll,
|
||||
filter?.defaultValues,
|
||||
filter?.showSelectAll,
|
||||
]);
|
||||
|
||||
const updateSelection = useCallback(
|
||||
(newValues: string[]) => {
|
||||
// Mark that user has interacted with the filter
|
||||
hasUserInteracted.current = true;
|
||||
|
||||
const actualValues = newValues.filter((key) => key !== "all");
|
||||
const newSelection = new Set(actualValues);
|
||||
|
||||
// Auto-add "all" if all items are selected
|
||||
// Auto-add "all" if all items are selected and showSelectAll is not false
|
||||
if (
|
||||
actualValues.length === filterValues.length &&
|
||||
filterValues.length > 0
|
||||
filterValues.length > 0 &&
|
||||
filter?.showSelectAll !== false
|
||||
) {
|
||||
newSelection.add("all");
|
||||
}
|
||||
@@ -68,7 +117,7 @@ export const CustomDropdownFilter = ({
|
||||
// Notify parent with actual values (excluding "all")
|
||||
onFilterChange?.(filter.key, actualValues);
|
||||
},
|
||||
[filterValues.length, onFilterChange, filter.key],
|
||||
[filterValues.length, onFilterChange, filter.key, filter?.showSelectAll],
|
||||
);
|
||||
|
||||
const onSelectionChange = useCallback(
|
||||
@@ -193,16 +242,20 @@ export const CustomDropdownFilter = ({
|
||||
onValueChange={onSelectionChange}
|
||||
className="font-bold"
|
||||
>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
label: "text-small font-normal",
|
||||
wrapper: "checkbox-update",
|
||||
}}
|
||||
value="all"
|
||||
>
|
||||
Select All
|
||||
</Checkbox>
|
||||
<Divider orientation="horizontal" className="mt-2" />
|
||||
{filter?.showSelectAll !== false && (
|
||||
<>
|
||||
<Checkbox
|
||||
classNames={{
|
||||
label: "text-small font-normal",
|
||||
wrapper: "checkbox-update",
|
||||
}}
|
||||
value="all"
|
||||
>
|
||||
Select All
|
||||
</Checkbox>
|
||||
<Divider orientation="horizontal" className="mt-2" />
|
||||
</>
|
||||
)}
|
||||
<ScrollShadow
|
||||
hideScrollBar
|
||||
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Icon } from "@iconify/react";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { ThemeSwitch } from "@/components/ThemeSwitch";
|
||||
import { UserProfileProps } from "@/types";
|
||||
|
||||
import { SheetMenu } from "../sidebar/sheet-menu";
|
||||
import { UserNav } from "../user-nav/user-nav";
|
||||
|
||||
interface NavbarProps {
|
||||
title: string;
|
||||
icon: string;
|
||||
icon: string | ReactNode;
|
||||
user: UserProfileProps;
|
||||
}
|
||||
|
||||
@@ -17,12 +19,18 @@ export function Navbar({ title, icon, user }: NavbarProps) {
|
||||
<div className="mx-4 flex h-14 items-center sm:mx-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<SheetMenu />
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
height={24}
|
||||
icon={icon}
|
||||
width={24}
|
||||
/>
|
||||
{typeof icon === "string" ? (
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
height={24}
|
||||
icon={icon}
|
||||
width={24}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-10 w-10 items-center justify-center [&>*]:h-full [&>*]:w-full">
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
<h1 className="text-sm font-bold text-default-700">{title}</h1>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end gap-3">
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
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,
|
||||
CISAttributesMetadata,
|
||||
Framework,
|
||||
Requirement,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
filter?: string, // "Level 1" or "Level 2" or undefined (show all)
|
||||
): 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 CISAttributesMetadata[];
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!attrs) continue;
|
||||
|
||||
// Apply profile filter
|
||||
if (filter === "Level 1" && attrs.Profile !== "Level 1") {
|
||||
continue; // Skip Level 2 requirements when Level 1 is selected
|
||||
}
|
||||
|
||||
// Get corresponding requirement data
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
const sectionName = attrs.Section;
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
const requirementName = id;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
const normalizedSectionName = sectionName.replace(/^(\d+)\s/, "$1. ");
|
||||
let category = framework.categories.find(
|
||||
(c) => c.name === normalizedSectionName,
|
||||
);
|
||||
|
||||
if (!category) {
|
||||
category = {
|
||||
name: normalizedSectionName,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
controls: [],
|
||||
};
|
||||
framework.categories.push(category);
|
||||
}
|
||||
|
||||
// Create a control for this requirement (each requirement is its own control)
|
||||
const controlLabel = `${id} - ${description}`;
|
||||
const control = {
|
||||
label: controlLabel,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
requirements: [] as Requirement[],
|
||||
};
|
||||
|
||||
// Create requirement
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
const requirement: Requirement = {
|
||||
name: requirementName,
|
||||
description: attrs.Description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
profile: attrs.Profile,
|
||||
subsection: attrs.SubSection || "",
|
||||
assessment_status: attrs.AssessmentStatus,
|
||||
rationale_statement: attrs.RationaleStatement,
|
||||
impact_statement: attrs.ImpactStatement,
|
||||
remediation_procedure: attrs.RemediationProcedure,
|
||||
audit_procedure: attrs.AuditProcedure,
|
||||
additional_information: attrs.AdditionalInformation,
|
||||
default_value: attrs.DefaultValue || "",
|
||||
references: attrs.References,
|
||||
};
|
||||
|
||||
control.requirements.push(requirement);
|
||||
|
||||
// Update control counters
|
||||
if (requirement.status === "MANUAL") {
|
||||
control.manual++;
|
||||
} else if (requirement.status === "PASS") {
|
||||
control.pass++;
|
||||
} else if (requirement.status === "FAIL") {
|
||||
control.fail++;
|
||||
}
|
||||
|
||||
category.controls.push(control);
|
||||
}
|
||||
|
||||
// Calculate counters for categories and frameworks
|
||||
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) => {
|
||||
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) => {
|
||||
const requirement = control.requirements[0]; // Each control has one requirement
|
||||
const itemKey = `${framework.name}-${category.name}-control-${i}`;
|
||||
|
||||
return {
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={control.label}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 && requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,257 @@
|
||||
import React from "react";
|
||||
|
||||
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";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import {
|
||||
AttributesData,
|
||||
CategoryData,
|
||||
FailedSection,
|
||||
Framework,
|
||||
RegionData,
|
||||
Requirement,
|
||||
RequirementsData,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
mapComplianceData as mapCISComplianceData,
|
||||
toAccordionItems as toCISAccordionItems,
|
||||
} from "./cis";
|
||||
import {
|
||||
mapComplianceData as mapENSComplianceData,
|
||||
toAccordionItems as toENSAccordionItems,
|
||||
} from "./ens";
|
||||
import {
|
||||
mapComplianceData as mapISOComplianceData,
|
||||
toAccordionItems as toISOAccordionItems,
|
||||
} from "./iso";
|
||||
|
||||
export interface ComplianceMapper {
|
||||
mapComplianceData: (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
filter?: string,
|
||||
) => Framework[];
|
||||
toAccordionItems: (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
) => AccordionItemProps[];
|
||||
getTopFailedSections: (mappedData: Framework[]) => FailedSection[];
|
||||
getDetailsComponent: (requirement: Requirement) => React.ReactNode;
|
||||
}
|
||||
|
||||
// Common function for getting top failed sections
|
||||
export const getTopFailedSections = (
|
||||
mappedData: Framework[],
|
||||
): FailedSection[] => {
|
||||
const failedSectionMap = new Map();
|
||||
|
||||
mappedData.forEach((framework) => {
|
||||
framework.categories.forEach((category) => {
|
||||
category.controls.forEach((control) => {
|
||||
control.requirements.forEach((requirement) => {
|
||||
if (requirement.status === "FAIL") {
|
||||
const sectionName = category.name;
|
||||
|
||||
if (!failedSectionMap.has(sectionName)) {
|
||||
failedSectionMap.set(sectionName, { total: 0, types: {} });
|
||||
}
|
||||
|
||||
const sectionData = failedSectionMap.get(sectionName);
|
||||
sectionData.total += 1;
|
||||
|
||||
const type = requirement.type || "Fails";
|
||||
|
||||
sectionData.types[type as string] =
|
||||
(sectionData.types[type as string] || 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
|
||||
};
|
||||
|
||||
// Registry of compliance mappers
|
||||
const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
ENS: {
|
||||
mapComplianceData: mapENSComplianceData,
|
||||
toAccordionItems: toENSAccordionItems,
|
||||
getTopFailedSections,
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ENSCustomDetails, { requirement }),
|
||||
},
|
||||
ISO27001: {
|
||||
mapComplianceData: mapISOComplianceData,
|
||||
toAccordionItems: toISOAccordionItems,
|
||||
getTopFailedSections,
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(ISOCustomDetails, { requirement }),
|
||||
},
|
||||
CIS: {
|
||||
mapComplianceData: mapCISComplianceData,
|
||||
toAccordionItems: toCISAccordionItems,
|
||||
getTopFailedSections,
|
||||
getDetailsComponent: (requirement: Requirement) =>
|
||||
React.createElement(CISCustomDetails, { requirement }),
|
||||
},
|
||||
};
|
||||
|
||||
// Default mapper (fallback to ENS for backward compatibility)
|
||||
const defaultMapper: ComplianceMapper = complianceMappers.ENS;
|
||||
|
||||
/**
|
||||
* Get the appropriate compliance mapper based on the framework name
|
||||
* @param framework - The framework name (e.g., "ENS", "ISO27001", "CIS")
|
||||
* @returns ComplianceMapper object with specific functions for the framework
|
||||
*/
|
||||
export const getComplianceMapper = (framework?: string): ComplianceMapper => {
|
||||
if (!framework) {
|
||||
return defaultMapper;
|
||||
}
|
||||
|
||||
return complianceMappers[framework] || defaultMapper;
|
||||
};
|
||||
|
||||
export const calculateRegionHeatmapData = async (
|
||||
complianceId: string,
|
||||
scanId: string,
|
||||
uniqueRegions: string[],
|
||||
attributesData: AttributesData,
|
||||
mapper: ComplianceMapper,
|
||||
): Promise<RegionData[]> => {
|
||||
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[] => {
|
||||
if (!complianceData?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const categoryMap = new Map<
|
||||
string,
|
||||
{ pass: number; fail: number; manual: number }
|
||||
>();
|
||||
|
||||
// Aggregate data by category
|
||||
complianceData.forEach((framework) => {
|
||||
framework.categories.forEach((category) => {
|
||||
const existing = categoryMap.get(category.name) || {
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
};
|
||||
categoryMap.set(category.name, {
|
||||
pass: existing.pass + category.pass,
|
||||
fail: existing.fail + category.fail,
|
||||
manual: existing.manual + category.manual,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const categoryData: CategoryData[] = Array.from(categoryMap.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 categories
|
||||
|
||||
return filteredData;
|
||||
} catch (error) {
|
||||
console.error("Error calculating category heatmap data:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
+17
-10
@@ -5,8 +5,8 @@ import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
ENSAttributesMetadata,
|
||||
Framework,
|
||||
MappedComplianceData,
|
||||
Requirement,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
@@ -14,6 +14,10 @@ import {
|
||||
} from "@/types/compliance";
|
||||
|
||||
export const translateType = (type: string) => {
|
||||
if (!type) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (type.toLowerCase()) {
|
||||
case "requisito":
|
||||
return "Requirement";
|
||||
@@ -31,7 +35,7 @@ export const translateType = (type: string) => {
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): MappedComplianceData => {
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirements = requirementsData?.data || [];
|
||||
|
||||
@@ -46,7 +50,9 @@ export const mapComplianceData = (
|
||||
// Process attributes and merge with requirements data
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const attrs = attributeItem.attributes?.attributes?.metadata?.[0];
|
||||
const attrs = attributeItem.attributes?.attributes
|
||||
?.metadata?.[0] as ENSAttributesMetadata;
|
||||
|
||||
if (!attrs) continue;
|
||||
|
||||
// Get corresponding requirement data
|
||||
@@ -96,7 +102,6 @@ export const mapComplianceData = (
|
||||
if (!control) {
|
||||
control = {
|
||||
label: groupControlLabel,
|
||||
type,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
@@ -166,7 +171,7 @@ export const mapComplianceData = (
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: MappedComplianceData,
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.map((framework) => {
|
||||
@@ -178,6 +183,7 @@ export const toAccordionItems = (
|
||||
pass={framework.pass}
|
||||
fail={framework.fail}
|
||||
manual={framework.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
@@ -212,7 +218,7 @@ export const toAccordionItems = (
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type={requirement.type}
|
||||
type={requirement.type as string}
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
@@ -221,12 +227,13 @@ export const toAccordionItems = (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
framework={framework.name}
|
||||
disableFindings={
|
||||
requirement.check_ids.length === 0 &&
|
||||
requirement.manual === 0
|
||||
}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
isDisabled:
|
||||
requirement.check_ids.length === 0 &&
|
||||
requirement.manual === 0,
|
||||
};
|
||||
}),
|
||||
isDisabled:
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
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,
|
||||
Framework,
|
||||
ISO27001AttributesMetadata,
|
||||
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 ISO27001AttributesMetadata[];
|
||||
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 categoryName = attrs.Category;
|
||||
const controlLabel = `${attrs.Objetive_ID} - ${attrs.Objetive_Name}`;
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
const requirementName = id;
|
||||
const objetiveName = attrs.Objetive_Name;
|
||||
const checkSummary = attrs.Check_Summary;
|
||||
|
||||
// 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
|
||||
let category = framework.categories.find((c) => c.name === categoryName);
|
||||
if (!category) {
|
||||
category = {
|
||||
name: categoryName,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
controls: [],
|
||||
};
|
||||
framework.categories.push(category);
|
||||
}
|
||||
|
||||
// Find or create control
|
||||
let control = category.controls.find((c) => c.label === controlLabel);
|
||||
if (!control) {
|
||||
control = {
|
||||
label: controlLabel,
|
||||
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,
|
||||
objetive_name: objetiveName,
|
||||
check_summary: checkSummary,
|
||||
};
|
||||
|
||||
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.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,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
Generated
+1219
-7
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@react-aria/ssr": "3.9.4",
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"add": "^2.0.6",
|
||||
"alert": "^6.0.2",
|
||||
@@ -35,6 +36,7 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"recharts": "^2.15.2",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn-ui": "^0.2.3",
|
||||
|
||||
@@ -171,9 +171,9 @@ module.exports = {
|
||||
"100%": { left: "100%", width: "100%" },
|
||||
},
|
||||
dropArrow: {
|
||||
'0%': { transform: 'translateY(-8px)', opacity: '0' },
|
||||
'50%': { opacity: '1' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
"0%": { transform: "translateY(-8px)", opacity: "0" },
|
||||
"50%": { opacity: "1" },
|
||||
"100%": { transform: "translateY(0)", opacity: "1" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
@@ -188,6 +188,7 @@ module.exports = {
|
||||
},
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/typography"),
|
||||
nextui({
|
||||
themes: {
|
||||
dark: {
|
||||
|
||||
+52
-11
@@ -1,6 +1,15 @@
|
||||
export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings";
|
||||
|
||||
export type ComplianceId = "ens_rd2022_aws";
|
||||
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[];
|
||||
@@ -23,19 +32,17 @@ export interface Requirement {
|
||||
name: string;
|
||||
description: string;
|
||||
status: RequirementStatus;
|
||||
type: string;
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
check_ids: string[];
|
||||
// ENS
|
||||
nivel?: string;
|
||||
dimensiones?: 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;
|
||||
}
|
||||
|
||||
export interface Control {
|
||||
label: string;
|
||||
type: string;
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
@@ -58,12 +65,10 @@ export interface Framework {
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
export type MappedComplianceData = Framework[];
|
||||
|
||||
export interface FailedSection {
|
||||
name: string;
|
||||
total: number;
|
||||
types: { [key: string]: number };
|
||||
types?: { [key: string]: number };
|
||||
}
|
||||
|
||||
export interface RequirementsTotals {
|
||||
@@ -73,7 +78,7 @@ export interface RequirementsTotals {
|
||||
}
|
||||
|
||||
// API Responses types:
|
||||
export interface AttributesMetadata {
|
||||
export interface ENSAttributesMetadata {
|
||||
IdGrupoControl: string;
|
||||
Marco: string;
|
||||
Categoria: string;
|
||||
@@ -85,6 +90,28 @@ export interface AttributesMetadata {
|
||||
Dependencias: any[];
|
||||
}
|
||||
|
||||
export interface ISO27001AttributesMetadata {
|
||||
Category: string;
|
||||
Objetive_ID: string;
|
||||
Objetive_Name: string;
|
||||
Check_Summary: string;
|
||||
}
|
||||
|
||||
export interface CISAttributesMetadata {
|
||||
Section: string;
|
||||
SubSection: string | null;
|
||||
Profile: string; // "Level 1" or "Level 2"
|
||||
AssessmentStatus: string; // "Manual" or "Automated"
|
||||
Description: string;
|
||||
RationaleStatement: string;
|
||||
ImpactStatement: string;
|
||||
RemediationProcedure: string;
|
||||
AuditProcedure: string;
|
||||
AdditionalInformation: string;
|
||||
DefaultValue: string | null;
|
||||
References: string;
|
||||
}
|
||||
|
||||
export interface AttributesItemData {
|
||||
type: "compliance-requirements-attributes";
|
||||
id: string;
|
||||
@@ -93,7 +120,7 @@ export interface AttributesItemData {
|
||||
version: string;
|
||||
description: string;
|
||||
attributes: {
|
||||
metadata: AttributesMetadata[];
|
||||
metadata: ENSAttributesMetadata[] | ISO27001AttributesMetadata[];
|
||||
check_ids: string[];
|
||||
};
|
||||
};
|
||||
@@ -117,3 +144,17 @@ export interface AttributesData {
|
||||
export interface RequirementsData {
|
||||
data: RequirementItemData[];
|
||||
}
|
||||
|
||||
export interface RegionData {
|
||||
name: string;
|
||||
failurePercentage: number;
|
||||
totalRequirements: number;
|
||||
failedRequirements: number;
|
||||
}
|
||||
|
||||
export interface CategoryData {
|
||||
name: string;
|
||||
failurePercentage: number;
|
||||
totalRequirements: number;
|
||||
failedRequirements: number;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ export interface FilterOption {
|
||||
};
|
||||
}>;
|
||||
index?: number;
|
||||
showSelectAll?: boolean;
|
||||
defaultToSelectAll?: boolean;
|
||||
defaultValues?: string[];
|
||||
}
|
||||
|
||||
export interface CustomDropdownFilterProps {
|
||||
|
||||
Reference in New Issue
Block a user