Merge branch 'master' into PRWLR-7160-findings-page-scan-id-filter-improvement

# Conflicts:
#	ui/CHANGELOG.md
#	ui/app/(prowler)/compliance/page.tsx
#	ui/types/components.ts
This commit is contained in:
sumit_chaturvedi committed 2025-06-03 12:17:29 +05:30
commit d2d68ded2b
108 files changed
+5916 -1193

No files matched your search

@@ -0,0 +1,269 @@
import { Spacer } from "@nextui-org/react";
import Image from "next/image";
import { Suspense } from "react";
import {
getComplianceAttributes,
getComplianceOverviewMetadataInfo,
getComplianceRequirements,
} 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";
interface ComplianceDetailSearchParams {
complianceId: string;
version?: string;
scanId?: string;
"filter[region__in]"?: string;
}
const Logo = ({ logoPath }: { logoPath: string }) => {
return (
<div className="relative ml-auto hidden h-[200px] w-[200px] flex-shrink-0 md:block">
<Image
src={logoPath}
alt="Compliance Logo"
fill
priority
className="object-contain"
/>
</div>
);
};
const ChartsWrapper = ({
children,
logoPath,
}: {
children: React.ReactNode;
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>
);
};
export default async function ComplianceDetail({
params,
searchParams,
}: {
params: { compliancetitle: string };
searchParams: ComplianceDetailSearchParams;
}) {
const { compliancetitle } = params;
const { complianceId, version, scanId } = searchParams;
const regionFilter = searchParams["filter[region__in]"];
const logoPath = `/${compliancetitle.toLowerCase()}.png`;
// Create a key that includes region filter for Suspense
const searchParamsKey = JSON.stringify(searchParams || {});
const formattedTitle = compliancetitle.split("-").join(" ");
const pageTitle = version
? `Compliance Details: ${formattedTitle} - ${version}`
: `Compliance Details: ${formattedTitle}`;
// Fetch scans data
const scansData = await getScans({
filters: {
"filter[state]": "completed",
},
});
// Expand scans with provider information
const expandedScansData = await Promise.all(
scansData.data.map(async (scan: ScanProps) => {
const providerId = scan.relationships?.provider?.data?.id;
if (!providerId) {
return { ...scan, providerInfo: null };
}
const formData = new FormData();
formData.append("id", providerId);
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,
};
}),
);
const selectedScanId = scanId || expandedScansData[0]?.id || null;
// Fetch metadata info for regions
const metadataInfoData = await getComplianceOverviewMetadataInfo({
filters: {
"filter[scan_id]": selectedScanId,
},
});
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
return (
<ContentLayout title={pageTitle} icon="fluent-mdl2:compliance-audit">
<ComplianceHeader
scans={expandedScansData}
uniqueRegions={uniqueRegions}
showSearch={false}
/>
<Suspense
key={searchParamsKey}
fallback={
<div className="space-y-8">
<ChartsWrapper logoPath={logoPath}>
<RequirementsChartSkeleton />
<FailedSectionsChartSkeleton />
</ChartsWrapper>
<SkeletonAccordion />
</div>
}
>
<SSRComplianceContent
complianceId={complianceId}
scanId={selectedScanId}
region={regionFilter}
logoPath={logoPath}
/>
</Suspense>
</ContentLayout>
);
}
const getComplianceData = async (
complianceId: string,
scanId: string,
region?: string,
): Promise<MappedComplianceData> => {
const [attributesData, requirementsData] = await Promise.all([
getComplianceAttributes(complianceId),
getComplianceRequirements({
complianceId,
scanId,
region,
}),
]);
const mappedData = mapComplianceData(attributesData, requirementsData);
return mappedData;
};
const getTopFailedSections = (
mappedData: MappedComplianceData,
): 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;
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) => ({
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);
return (
<div className="space-y-8">
<ChartsWrapper logoPath={logoPath}>
<RequirementsChart
pass={totalRequirements.pass}
fail={totalRequirements.fail}
manual={totalRequirements.manual}
/>
<FailedSectionsChart sections={topFailedSections} />
</ChartsWrapper>
<Spacer className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-800" />
<ClientAccordionWrapper
items={accordionItems}
defaultExpandedKeys={defaultKeys}
/>
</div>
);
};
+21 -34
View File
@@ -1,6 +1,4 @@
export const dynamic = "force-dynamic";
import { Spacer } from "@nextui-org/react";
import { Suspense } from "react";
import { getCompliancesOverview } from "@/actions/compliances";
@@ -11,12 +9,11 @@ import {
ComplianceSkeletonGrid,
NoScansAvailable,
} from "@/components/compliance";
import { DataCompliance } from "@/components/compliance/data-compliance";
import { FilterControls } from "@/components/filters";
import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header";
import { ContentLayout } from "@/components/ui";
import { DataTableFilterCustom } from "@/components/ui/table/data-table-filter-custom";
import { ScanProps, SearchParamsProps } from "@/types";
import { ComplianceOverviewData } from "@/types/compliance";
import { getProviderDetailsByScan } from "@/lib/provider-helpers";
import { ComplianceOverviewData, ScanProps, SearchParamsProps } from "@/types";
export default async function Compliance({
searchParams,
@@ -64,21 +61,10 @@ export default async function Compliance({
<ContentLayout title="Compliance" icon="fluent-mdl2:compliance-audit">
{selectedScanId ? (
<>
<FilterControls search />
<Spacer y={8} />
<DataCompliance scans={expandedScansData} />
<Spacer y={8} />
<DataTableFilterCustom
filters={[
{
key: "region__in",
labelCheckboxGroup: "Regions",
values: uniqueRegions,
},
]}
defaultOpen={true}
<ComplianceHeader
scans={expandedScansData}
uniqueRegions={uniqueRegions}
/>
<Spacer y={12} />
<Suspense key={searchParamsKey} fallback={<ComplianceSkeletonGrid />}>
<SSRComplianceGrid searchParams={searchParams} />
</Suspense>
@@ -113,7 +99,11 @@ const SSRComplianceGrid = async ({
});
// Check if the response contains no data
if (!compliancesData || compliancesData?.data?.length === 0) {
if (
!compliancesData ||
!compliancesData.data ||
compliancesData.data.length === 0
) {
return (
<div className="flex h-full items-center">
<div className="text-sm text-default-500">
@@ -135,25 +125,22 @@ const SSRComplianceGrid = async ({
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4">
{compliancesData.data.map((compliance: ComplianceOverviewData) => {
const { attributes } = compliance;
const {
framework,
version,
requirements_status: { passed, total },
compliance_id,
} = attributes;
const { attributes, id } = compliance;
const { framework, version, requirements_passed, total_requirements } =
attributes;
return (
<ComplianceCard
key={compliance.id}
key={id}
title={framework}
version={version}
passingRequirements={passed}
totalRequirements={total}
prevPassingRequirements={passed}
prevTotalRequirements={total}
passingRequirements={requirements_passed}
totalRequirements={total_requirements}
prevPassingRequirements={requirements_passed}
prevTotalRequirements={total_requirements}
scanId={scanId}
complianceId={compliance_id}
complianceId={id}
id={id}
/>
);
})}
+3 -1
View File
@@ -28,7 +28,8 @@ import {
extractProviderUIDs,
getProviderDetailsByScan,
} from "@/lib/provider-helpers";
import { FindingProps, ScanProps, SearchParamsProps } from "@/types/components";
import { ScanProps } from "@/types";
import { FindingProps, SearchParamsProps } from "@/types/components";
export default async function Findings({
searchParams,
@@ -132,6 +133,7 @@ export default async function Findings({
defaultOpen={true}
/>
<Spacer y={8} />
<Suspense key={searchParamsKey} fallback={<SkeletonTableFindings />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
+1 -1
View File
@@ -9,7 +9,7 @@ import { MembershipsCard } from "@/components/users/profile/memberships-card";
import { RolesCard } from "@/components/users/profile/roles-card";
import { SkeletonUserInfo } from "@/components/users/profile/skeleton-user-info";
import { isUserOwnerAndHasManageAccount } from "@/lib/permissions";
import { RoleDetail, TenantDetailData } from "@/types/users/users";
import { RoleDetail, TenantDetailData } from "@/types/users";
export default async function Profile() {
return (
@@ -4,10 +4,15 @@ import {
ViaCredentialsForm,
ViaRoleForm,
} from "@/components/providers/workflow/forms";
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-via-aws/select-via-aws";
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
import {
SelectViaGCP,
ViaServiceAccountForm,
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
import { ProviderType } from "@/types/providers";
interface Props {
searchParams: { type: string; id: string; via?: string };
searchParams: { type: ProviderType; id: string; via?: string };
}
export default function AddCredentialsPage({ searchParams }: Props) {
@@ -17,14 +22,24 @@ export default function AddCredentialsPage({ searchParams }: Props) {
<SelectViaAWS initialVia={searchParams.via} />
)}
{searchParams.type === "gcp" && !searchParams.via && (
<SelectViaGCP initialVia={searchParams.via} />
)}
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
searchParams.type !== "aws") && (
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
<ViaCredentialsForm searchParams={searchParams} />
)}
{searchParams.type === "aws" && searchParams.via === "role" && (
<ViaRoleForm searchParams={searchParams} />
)}
{searchParams.type === "gcp" &&
searchParams.via === "service-account" && (
<ViaServiceAccountForm searchParams={searchParams} />
)}
</>
);
}
@@ -1,53 +1,47 @@
import React from "react";
import { InfoIcon } from "@/components/icons";
import { CredentialsUpdateInfo } from "@/components/providers";
import {
UpdateViaCredentialsForm,
UpdateViaRoleForm,
} from "@/components/providers/workflow/forms";
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-via-aws/select-via-aws";
import { UpdateViaServiceAccountForm } from "@/components/providers/workflow/forms/update-via-service-account-key-form";
import { ProviderType } from "@/types/providers";
interface Props {
searchParams: { type: string; id: string; via?: string };
searchParams: {
type: ProviderType;
id: string;
via?: string;
secretId?: string;
};
}
export default function UpdateCredentialsPage({ searchParams }: Props) {
return (
<>
{searchParams.type === "aws" && !searchParams.via && (
<>
<div className="flex flex-col gap-4">
<p className="text-sm text-default-700">
To update provider credentials,{" "}
<strong>
the same type that was originally configured must be used.
</strong>
</p>
<div className="flex items-center rounded-lg border border-system-warning bg-system-warning-medium p-4 text-sm dark:text-default-300">
<InfoIcon className="mr-2 inline h-4 w-4 flex-shrink-0" />
<p>
If the provider was configured with static credentials, updates
must also use static credentials. If it was configured with a
role, updates must use a role.
</p>
</div>
<p className="text-sm text-default-700">
To switch from static credentials to a role (or vice versa), the
provider must be deleted and set up again.
</p>
<SelectViaAWS initialVia={searchParams.via} />
</div>
</>
)}
{(searchParams.type === "aws" || searchParams.type === "gcp") &&
!searchParams.via && (
<CredentialsUpdateInfo
providerType={searchParams.type}
initialVia={searchParams.via}
/>
)}
{((searchParams.type === "aws" && searchParams.via === "credentials") ||
searchParams.type !== "aws") && (
(searchParams.type === "gcp" && searchParams.via === "credentials") ||
(searchParams.type !== "aws" && searchParams.type !== "gcp")) && (
<UpdateViaCredentialsForm searchParams={searchParams} />
)}
{searchParams.type === "aws" && searchParams.via === "role" && (
<UpdateViaRoleForm searchParams={searchParams} />
)}
{searchParams.type === "gcp" &&
searchParams.via === "service-account" && (
<UpdateViaServiceAccountForm searchParams={searchParams} />
)}
</>
);
}