mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
srn Fixed PR comments
This commit is contained in:
@@ -46,6 +46,7 @@ export const getScans = async ({
|
||||
revalidatePath("/scans");
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error fetching scans:", error);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SkeletonTableResources } from "@/components/resources/skeleton/skeleton
|
||||
import { ColumnResources } from "@/components/resources/table/column-resources";
|
||||
import { ContentLayout } from "@/components/ui";
|
||||
import { DataTable, DataTableFilterCustom } from "@/components/ui/table";
|
||||
import { createDict } from "@/lib";
|
||||
import { createDict, extractFiltersAndQuery, extractSortAndKey, hasDateOrScanFilter, replaceFilterFieldKey } from "@/lib";
|
||||
import { ResourceProps, SearchParamsProps } from "@/types";
|
||||
|
||||
export default async function Resources({
|
||||
@@ -20,46 +20,31 @@ export default async function Resources({
|
||||
const searchParamsKey = JSON.stringify(searchParams || {});
|
||||
|
||||
// Check if the searchParams contain any date or filter
|
||||
const hasDateOrScanFilter = Object.keys(searchParams).some((key) =>
|
||||
key.includes("inserted_at"),
|
||||
);
|
||||
const hasDateOrScan = hasDateOrScanFilter(searchParams);
|
||||
|
||||
// Default filters for getFindings
|
||||
const defaultFilters: Record<string, string> = hasDateOrScanFilter
|
||||
? {} // Do not apply default filters if there are date or filters
|
||||
: { "page[size]": "100" }; // TODO: Remove page[size] 100 when metadata endpoint implemented
|
||||
const { filters } = extractFiltersAndQuery(searchParams);
|
||||
filters["page[size]"] = "100"; // TODO: Remove page[size] 100 when metadata endpoint implemented
|
||||
|
||||
const filters: Record<string, string> = {
|
||||
...defaultFilters,
|
||||
...Object.fromEntries(
|
||||
Object.entries(searchParams)
|
||||
.filter(([key]) => key.startsWith("filter["))
|
||||
.map(([key, value]) => [
|
||||
key,
|
||||
Array.isArray(value) ? value.join(",") : value?.toString() || "",
|
||||
]),
|
||||
),
|
||||
};
|
||||
if (!hasDateOrScan) {
|
||||
const scansData = await getScans({
|
||||
filters: {
|
||||
"fields[scans]": "inserted_at",
|
||||
},
|
||||
});
|
||||
|
||||
const scansData = await getScans({
|
||||
filters: {
|
||||
"filter[state]": "completed",
|
||||
"fields[scans]": "inserted_at",
|
||||
},
|
||||
});
|
||||
|
||||
if (scansData.data?.length !== 0) {
|
||||
const latestScandate = scansData.data[0].attributes.inserted_at;
|
||||
const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd");
|
||||
if (!hasDateOrScanFilter) {
|
||||
filters["filter[inserted_at]"] = formattedDate;
|
||||
if (scansData.data?.length !== 0) {
|
||||
const latestScandate = scansData.data?.[0]?.attributes?.inserted_at;
|
||||
const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd");
|
||||
filters["filter[updated_at]"] = formattedDate;
|
||||
}
|
||||
}
|
||||
|
||||
const outputFilters = replaceFilterFieldKey(filters, 'inserted_at', 'updated_at');
|
||||
|
||||
// Resource call for filters
|
||||
const resourcesData = await getResourceFields(
|
||||
"name,type,region,service",
|
||||
filters,
|
||||
outputFilters,
|
||||
);
|
||||
|
||||
let resourceNameList: string[] = [];
|
||||
@@ -71,14 +56,14 @@ export default async function Resources({
|
||||
resourceNameList = Array.from(
|
||||
new Set(
|
||||
resourcesData.data.map((item: ResourceProps) => item.attributes.name) ||
|
||||
[],
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
typeList = Array.from(
|
||||
new Set(
|
||||
resourcesData.data.map((item: ResourceProps) => item.attributes.type) ||
|
||||
[],
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -142,49 +127,40 @@ const SSRDataTable = async ({
|
||||
searchParams: SearchParamsProps;
|
||||
}) => {
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
|
||||
const pageSize = parseInt(searchParams.pageSize?.toString() || "10", 10);
|
||||
const defaultSort = "name";
|
||||
const sort = searchParams.sort?.toString() || defaultSort;
|
||||
|
||||
// Check if the searchParams contain any date or filter
|
||||
const hasDateOrScanFilter = Object.keys(searchParams).some((key) =>
|
||||
key.includes("inserted_at"),
|
||||
);
|
||||
|
||||
const filters: Record<string, string> = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(searchParams)
|
||||
.filter(([key]) => key.startsWith("filter["))
|
||||
.map(([key, value]) => [
|
||||
key,
|
||||
Array.isArray(value) ? value.join(",") : value?.toString() || "",
|
||||
]),
|
||||
),
|
||||
};
|
||||
|
||||
// Fetch scans data latest date
|
||||
const scansData = await getScans({
|
||||
filters: {
|
||||
"filter[state]": "completed",
|
||||
"fields[scans]": "inserted_at",
|
||||
},
|
||||
const { encodedSort } = extractSortAndKey({
|
||||
...searchParams,
|
||||
sort: searchParams.sort ?? defaultSort,
|
||||
});
|
||||
|
||||
if (scansData.data?.length !== 0) {
|
||||
const latestScandate = scansData.data[0].attributes.inserted_at;
|
||||
const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd");
|
||||
if (!hasDateOrScanFilter) {
|
||||
filters["filter[inserted_at]"] = formattedDate;
|
||||
// Check if the searchParams contain any date or filter
|
||||
const hasDateOrScan = hasDateOrScanFilter(searchParams);
|
||||
|
||||
const { filters, query } = extractFiltersAndQuery(searchParams);
|
||||
|
||||
if (!hasDateOrScan) {
|
||||
// Fetch scans data latest date
|
||||
const scansData = await getScans({
|
||||
filters: {
|
||||
"fields[scans]": "inserted_at",
|
||||
},
|
||||
});
|
||||
|
||||
if (scansData.data?.length !== 0) {
|
||||
const latestScandate = scansData?.data?.[0]?.attributes?.inserted_at;
|
||||
const formattedDate = format(parseISO(latestScandate), "yyyy-MM-dd");
|
||||
filters["filter[updated_at]"] = formattedDate;
|
||||
}
|
||||
}
|
||||
|
||||
const query = filters["filter[search]"] || "";
|
||||
const outputFilters = replaceFilterFieldKey(filters, 'inserted_at', 'updated_at');
|
||||
const resourcesData = await getResources({
|
||||
query,
|
||||
page,
|
||||
filters,
|
||||
sort,
|
||||
pageSize: 10,
|
||||
filters: outputFilters,
|
||||
sort: encodedSort,
|
||||
pageSize,
|
||||
});
|
||||
|
||||
const findingsDict = createDict("findings", resourcesData);
|
||||
@@ -193,22 +169,22 @@ const SSRDataTable = async ({
|
||||
// Expand each resources with its corresponding findings and provider
|
||||
const expandedResources = resourcesData?.data
|
||||
? resourcesData.data.map((resource: ResourceProps) => {
|
||||
const findings = {
|
||||
meta: resource.relationships.findings.meta,
|
||||
data: resource.relationships.findings.data?.map(
|
||||
(finding) => findingsDict[finding.id],
|
||||
),
|
||||
};
|
||||
const findings = {
|
||||
meta: resource.relationships.findings.meta,
|
||||
data: resource.relationships.findings.data?.map(
|
||||
(finding) => findingsDict[finding.id],
|
||||
),
|
||||
};
|
||||
|
||||
const provider = {
|
||||
data: providerDict[resource.relationships.provider.data.id],
|
||||
};
|
||||
const provider = {
|
||||
data: providerDict[resource.relationships.provider.data.id],
|
||||
};
|
||||
|
||||
return {
|
||||
...resource,
|
||||
relationships: { findings, provider },
|
||||
};
|
||||
})
|
||||
return {
|
||||
...resource,
|
||||
relationships: { findings, provider },
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
const expandedResponse = {
|
||||
|
||||
@@ -18,6 +18,12 @@ const getResourceData = (
|
||||
return row.original.attributes?.[field] || `No ${field} found in resource`;
|
||||
};
|
||||
|
||||
const getChipStyle = (count: number) => {
|
||||
if (count > 10) return "bg-red-100 text-red-800";
|
||||
if (count > 1) return "bg-yellow-100 text-yellow-800";
|
||||
return "bg-green-100 text-green-800";
|
||||
};
|
||||
|
||||
const getProviderData = (
|
||||
row: { original: ResourceProps },
|
||||
field: keyof ResourceProps["relationships"]["provider"]["data"]["attributes"],
|
||||
@@ -69,8 +75,14 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
const resourceName = getResourceData(row, "name");
|
||||
return (
|
||||
<>
|
||||
<div className="w-[120px] whitespace-normal break-words text-xs">
|
||||
{typeof resourceName === "string" ? resourceName : "Invalid name"}
|
||||
<div className="relative flex max-w-[410px] flex-row items-center gap-2 3xl:max-w-[660px]">
|
||||
<div className="flex w-full flex-row items-center gap-4">
|
||||
<p className="w-full whitespace-normal break-words text-sm">
|
||||
{typeof resourceName === "string"
|
||||
? resourceName
|
||||
: "Invalid name"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -78,14 +90,20 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
},
|
||||
{
|
||||
accessorKey: "failedFindings",
|
||||
header: "Failed Findings",
|
||||
header: () => <div className="text-center">Failed Findings</div>,
|
||||
cell: ({ row }) => {
|
||||
const count = row.original.relationships.findings.data.filter(
|
||||
(data) => data.attributes.status === "FAIL",
|
||||
).length;
|
||||
return (
|
||||
<>
|
||||
<div className="text-xs">{count}</div>
|
||||
<p className="text-center">
|
||||
<span
|
||||
className={`inline-block rounded-full px-2 py-1 text-xs font-semibold ${getChipStyle(count)}`}
|
||||
>
|
||||
{count}
|
||||
</span>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
},
|
||||
@@ -99,7 +117,7 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
return (
|
||||
<div className="w-[80px] text-xs">
|
||||
<div className="w-[120px] text-left">
|
||||
{typeof region === "string" ? region : "Invalid region"}
|
||||
</div>
|
||||
);
|
||||
@@ -114,7 +132,7 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
const type = getResourceData(row, "type");
|
||||
|
||||
return (
|
||||
<div className="w-[120px] whitespace-normal break-words text-xs">
|
||||
<div className="w-[110px] whitespace-normal break-words text-left">
|
||||
{typeof type === "string" ? type : "Invalid type"}
|
||||
</div>
|
||||
);
|
||||
@@ -133,7 +151,7 @@ export const ColumnResources: ColumnDef<ResourceProps>[] = [
|
||||
const service = getResourceData(row, "service");
|
||||
|
||||
return (
|
||||
<div className="w-[80px] text-xs">
|
||||
<div className="w-[90px] whitespace-normal break-words text-left">
|
||||
{typeof service === "string" ? service : "Invalid region"}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
DateWithTime,
|
||||
getProviderLogo,
|
||||
EntityInfoShort,
|
||||
InfoField,
|
||||
} from "@/components/ui/entities";
|
||||
import { SeverityBadge, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { ProviderType, ResourceApiResponse, ResourceProps } from "@/types";
|
||||
import { ResourceApiResponse, ResourceProps } from "@/types";
|
||||
|
||||
import { SkeletonFindingSummary } from "../skeleton/skeleton-finding-summary";
|
||||
|
||||
@@ -70,13 +70,35 @@ export const ResourceDetail = ({
|
||||
<div className="flex flex-col gap-6 rounded-lg">
|
||||
{/* Resource Details section */}
|
||||
<Section title="Resource Details">
|
||||
<InfoField label="Resource ID" variant="simple">
|
||||
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800" hideSymbol>
|
||||
<span className="whitespace-pre-line text-xs">
|
||||
{renderValue(resourceData?.attributes.uid)}
|
||||
</span>
|
||||
</Snippet>
|
||||
</InfoField>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InfoField label="Resource ID" variant="simple">
|
||||
<Snippet className="bg-gray-50 py-1 dark:bg-slate-800" hideSymbol>
|
||||
<span className="whitespace-pre-line text-xs">
|
||||
{renderValue(resourceData?.attributes.uid)}
|
||||
</span>
|
||||
</Snippet>
|
||||
</InfoField>
|
||||
<InfoField label="Provider Details">
|
||||
<EntityInfoShort
|
||||
cloudProvider={
|
||||
resourceData.relationships.provider.data.attributes.provider as
|
||||
| "aws"
|
||||
| "azure"
|
||||
| "gcp"
|
||||
| "kubernetes"
|
||||
}
|
||||
entityAlias={
|
||||
resourceData.relationships.provider.data.attributes
|
||||
.alias as string
|
||||
}
|
||||
entityId={
|
||||
resourceData.relationships.provider.data.attributes
|
||||
.uid as string
|
||||
}
|
||||
/>
|
||||
</InfoField>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InfoField label="Resource Name">
|
||||
{renderValue(resourceData.attributes.name)}
|
||||
@@ -109,28 +131,6 @@ export const ResourceDetail = ({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Provider Details section */}
|
||||
<Section title="Provider Details">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{resourceData.relationships.provider.data.attributes.alias && (
|
||||
<InfoField label="Alias">
|
||||
{resourceData.relationships.provider.data.attributes.alias}
|
||||
</InfoField>
|
||||
)}
|
||||
<InfoField label="Account ID">
|
||||
{resourceData.relationships.provider.data.attributes.uid}
|
||||
</InfoField>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<InfoField label="Provider" variant="simple">
|
||||
{getProviderLogo(
|
||||
resourceData.relationships.provider.data.attributes
|
||||
.provider as ProviderType,
|
||||
)}
|
||||
</InfoField>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Finding associated with this resource section */}
|
||||
<div>
|
||||
<h2 className="text-md line-clamp-2 font-medium leading-tight text-gray-800 dark:text-prowler-theme-pale/90">
|
||||
|
||||
@@ -42,7 +42,7 @@ export const InfoField = ({
|
||||
</span>
|
||||
|
||||
{variant === "simple" ? (
|
||||
<div className="text-small text-gray-900 dark:text-prowler-theme-pale">
|
||||
<div className="break-all text-small text-gray-900 dark:text-prowler-theme-pale">
|
||||
{children}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -269,3 +269,23 @@ export const permissionFormFields: PermissionInfo[] = [
|
||||
description: "Provides access to billing settings and invoices",
|
||||
},
|
||||
];
|
||||
|
||||
export function replaceFilterFieldKey(
|
||||
obj: Record<string, string>,
|
||||
oldField: string,
|
||||
newField: string
|
||||
): Record<string, string> {
|
||||
const fieldObj: Record<string, string> = {};
|
||||
|
||||
for (const key in obj) {
|
||||
const match = key.match(/^filter\[(.+)\]$/);
|
||||
if (match && match[1] === oldField) {
|
||||
const newKey = `filter[${newField}]`;
|
||||
fieldObj[newKey] = obj[key];
|
||||
} else {
|
||||
fieldObj[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return fieldObj;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LucideIcon } from "lucide-react";
|
||||
import { SVGProps } from "react";
|
||||
|
||||
import { ProviderProps, ProviderType } from "./providers";
|
||||
import { ProviderType } from "./providers";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number;
|
||||
|
||||
Reference in New Issue
Block a user