mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 15:24:22 +00:00
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:
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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user