mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
feat: render findings first iteration
This commit is contained in:
@@ -25,6 +25,7 @@ module.exports = {
|
||||
eqeqeq: 2,
|
||||
quotes: ["error", "double", "avoid-escape"],
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"security/detect-object-injection": "off",
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@/components/findings/table";
|
||||
import { Header } from "@/components/ui";
|
||||
import { DataTable } from "@/components/ui/table";
|
||||
import { createDict } from "@/lib";
|
||||
import { FindingProps, SearchParamsProps } from "@/types/components";
|
||||
|
||||
export default async function Findings({
|
||||
@@ -22,7 +23,7 @@ export default async function Findings({
|
||||
<Header title="Findings" icon="ph:list-checks-duotone" />
|
||||
<Spacer />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search providers date mutedFindings />
|
||||
<FilterControls search providers date />
|
||||
<Spacer y={4} />
|
||||
<Suspense fallback={<SkeletonTableFindings />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
@@ -49,60 +50,34 @@ const SSRDataTable = async ({
|
||||
|
||||
const findingsData = await getFindings({ query, page, sort, filters });
|
||||
|
||||
// Create dictionaries from the included data
|
||||
const resourceDict = Object.fromEntries(
|
||||
findingsData.included
|
||||
.filter((item: { type: string }) => item.type === "Resource")
|
||||
.map((resource: { id: string }) => [resource.id, resource]),
|
||||
);
|
||||
// Create dictionaries for resources, scans, and providers
|
||||
const resourceDict = createDict("Resource", findingsData);
|
||||
const scanDict = createDict("Scan", findingsData);
|
||||
const providerDict = createDict("Provider", findingsData);
|
||||
|
||||
const scanDict = Object.fromEntries(
|
||||
findingsData.included
|
||||
.filter((item: { type: string }) => item.type === "Scan")
|
||||
.map((scan: { id: string }) => [scan.id, scan]),
|
||||
);
|
||||
|
||||
const providerDict = Object.fromEntries(
|
||||
findingsData.included
|
||||
.filter((item: { type: string }) => item.type === "Provider")
|
||||
.map((provider: { id: string }) => [provider.id, provider]),
|
||||
);
|
||||
|
||||
// Enrich each finding with its corresponding resource, scan, and provider
|
||||
const enrichedFindings = findingsData.data.map((finding: FindingProps) => {
|
||||
const scanId = finding.relationships?.scan?.data?.id;
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const scan = scanId ? scanDict[scanId] : undefined;
|
||||
|
||||
const resourceId = finding.relationships?.resources?.data?.[0]?.id;
|
||||
console.log(resourceId, "resourceId");
|
||||
// Expand each finding with its corresponding resource, scan, and provider
|
||||
const expandedFindings = findingsData.data.map((finding: FindingProps) => {
|
||||
const scan = scanDict[finding.relationships?.scan?.data?.id];
|
||||
const resource =
|
||||
resourceId && resourceDict ? resourceDict[resourceId] : undefined;
|
||||
|
||||
const providerId = resource?.relationships?.provider?.data?.id;
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
const provider = providerId ? providerDict[providerId] : undefined;
|
||||
resourceDict[finding.relationships?.resources?.data?.[0]?.id];
|
||||
const provider = providerDict[resource?.relationships?.provider?.data?.id];
|
||||
|
||||
return {
|
||||
...finding,
|
||||
relationships: {
|
||||
scan: scan,
|
||||
resource: resource,
|
||||
provider: provider,
|
||||
},
|
||||
relationships: { scan, resource, provider },
|
||||
};
|
||||
});
|
||||
|
||||
// Create the new object while maintaining the original structure
|
||||
const enrichedResponse = {
|
||||
const expandedResponse = {
|
||||
...findingsData,
|
||||
data: enrichedFindings,
|
||||
data: expandedFindings,
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={ColumnFindings}
|
||||
data={enrichedResponse?.data || []}
|
||||
data={expandedResponse?.data || []}
|
||||
metadata={findingsData?.meta}
|
||||
customFilters={filterFindings}
|
||||
/>
|
||||
|
||||
@@ -2,18 +2,25 @@
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { DateWithTime } from "@/components/ui/entities";
|
||||
import {
|
||||
DataTableColumnHeader,
|
||||
SeverityBadge,
|
||||
Status,
|
||||
StatusBadge,
|
||||
} from "@/components/ui/table";
|
||||
import { FindingProps } from "@/types";
|
||||
|
||||
import { DataTableRowActions } from "./data-table-row-actions";
|
||||
|
||||
const statusMap: Record<"PASS" | "FAIL" | "MANUAL" | "MUTED", Status> = {
|
||||
PASS: "completed",
|
||||
FAIL: "failed",
|
||||
MANUAL: "completed",
|
||||
MUTED: "cancelled",
|
||||
};
|
||||
|
||||
const getFindingsData = (row: { original: FindingProps }) => {
|
||||
// console.log(row.original);
|
||||
console.log(row.original);
|
||||
return row.original;
|
||||
};
|
||||
|
||||
@@ -26,7 +33,6 @@ const getResourceData = (
|
||||
field: keyof FindingProps["relationships"]["resource"]["attributes"],
|
||||
) => {
|
||||
return (
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
row.original.relationships?.resource?.attributes?.[field] ||
|
||||
`No ${field} found in resource`
|
||||
);
|
||||
@@ -37,7 +43,6 @@ const getProviderData = (
|
||||
field: keyof FindingProps["relationships"]["provider"]["attributes"],
|
||||
) => {
|
||||
return (
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
row.original.relationships?.provider?.attributes?.[field] ||
|
||||
`No ${field} found in provider`
|
||||
);
|
||||
@@ -48,72 +53,38 @@ const getScanData = (
|
||||
field: keyof FindingProps["relationships"]["scan"]["attributes"],
|
||||
) => {
|
||||
return (
|
||||
// eslint-disable-next-line security/detect-object-injection
|
||||
row.original.relationships?.scan?.attributes?.[field] ||
|
||||
`No ${field} found in scan`
|
||||
);
|
||||
};
|
||||
|
||||
export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
// {
|
||||
// header: " ",
|
||||
// cell: ({ row }) => <p className="text-medium">{row.index + 1}</p>,
|
||||
// },
|
||||
{
|
||||
accessorKey: "check",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={"Check"} param="check" />
|
||||
),
|
||||
header: "Check",
|
||||
cell: ({ row }) => {
|
||||
const { checktitle } = getFindingsMetadata(row);
|
||||
return <p className="max-w-96 truncate text-medium">{checktitle}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: "Region",
|
||||
accessorKey: "scanName",
|
||||
header: "Scan Name",
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
const name = getScanData(row, "name");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{region}</div>
|
||||
</>
|
||||
<p className="max-w-96 truncate text-medium">
|
||||
{typeof name === "string" || typeof name === "number"
|
||||
? name
|
||||
: "Invalid data"}
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// {
|
||||
// accessorKey: "uid",
|
||||
// header: ({ column }) => (
|
||||
// <DataTableColumnHeader column={column} title={"Id"} param="uid" />
|
||||
// ),
|
||||
// cell: ({ row }) => {
|
||||
// const {
|
||||
// attributes: { uid },
|
||||
// } = getFindingsData(row);
|
||||
// return <SnippetId className="h-7 max-w-48" entityId={uid} />;
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// accessorKey: "provider",
|
||||
// header: "Provider Alias",
|
||||
// cell: ({ row }) => {
|
||||
// const {
|
||||
// attributes: { alias },
|
||||
// } = getProviderData(row, "alias");
|
||||
// return <p className="max-w-96 truncate text-medium">{alias}</p>;
|
||||
// },
|
||||
// },
|
||||
{
|
||||
accessorKey: "severity",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={"Severity"}
|
||||
param="severity"
|
||||
/>
|
||||
),
|
||||
header: "Severity",
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { severity },
|
||||
@@ -128,38 +99,44 @@ export const ColumnFindings: ColumnDef<FindingProps>[] = [
|
||||
const {
|
||||
attributes: { status },
|
||||
} = getFindingsData(row);
|
||||
// Temporarily overwriting the value until the API is functional.
|
||||
return <StatusBadge status={status} />;
|
||||
|
||||
const mappedStatus = statusMap[status];
|
||||
|
||||
return <StatusBadge status={mappedStatus} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "region",
|
||||
header: "Region",
|
||||
cell: ({ row }) => {
|
||||
const region = getResourceData(row, "region");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{typeof region === "string" ? region : "Invalid region"}</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "service",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={"Service"}
|
||||
param="service"
|
||||
/>
|
||||
),
|
||||
header: "Service",
|
||||
cell: ({ row }) => {
|
||||
const { servicename } = getFindingsMetadata(row);
|
||||
return <p className="max-w-96 truncate text-medium">{servicename}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "added",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={"Added"}
|
||||
param="inserted_at"
|
||||
/>
|
||||
),
|
||||
accessorKey: "account",
|
||||
header: "Account",
|
||||
cell: ({ row }) => {
|
||||
const {
|
||||
attributes: { inserted_at },
|
||||
} = getFindingsData(row);
|
||||
return <DateWithTime dateTime={inserted_at} showTime={false} />;
|
||||
const account = getProviderData(row, "uid");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{typeof account === "string" ? account : "Invalid account"}</div>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
EditDocumentBulkIcon,
|
||||
} from "@nextui-org/shared-icons";
|
||||
import { Row } from "@tanstack/react-table";
|
||||
import clsx from "clsx";
|
||||
|
||||
// import { useState } from "react";
|
||||
import { VerticalDotsIcon } from "@/components/icons";
|
||||
@@ -74,30 +73,22 @@ export function DataTableRowActions<FindingProps>({
|
||||
>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownItem
|
||||
key="edit"
|
||||
description="Allows you to edit the provider"
|
||||
textValue="Edit Provider"
|
||||
key="jira"
|
||||
description="Allows you to send the finding to Jira"
|
||||
textValue="Send to Jira"
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
// onClick={() => setIsEditOpen(true)}
|
||||
>
|
||||
Edit Finding
|
||||
Send to Jira
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection title="Danger zone">
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
description="Delete the provider permanently"
|
||||
textValue="Delete Provider"
|
||||
startContent={
|
||||
<DeleteDocumentBulkIcon
|
||||
className={clsx(iconClasses, "!text-danger")}
|
||||
/>
|
||||
}
|
||||
// onClick={() => setIsDeleteOpen(true)}
|
||||
key="slack"
|
||||
description="Allows you to send the finding to Slack"
|
||||
textValue="Send to Slack"
|
||||
startContent={<EditDocumentBulkIcon className={iconClasses} />}
|
||||
// onClick={() => setIsEditOpen(true)}
|
||||
>
|
||||
Delete Finding
|
||||
Send to Slack
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Chip } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
|
||||
type Status =
|
||||
export type Status =
|
||||
| "available"
|
||||
| "scheduled"
|
||||
| "executing"
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { MetaDataProps } from "@/types";
|
||||
|
||||
// Helper function to create dictionaries by type
|
||||
export const createDict = (
|
||||
type: string,
|
||||
data: any,
|
||||
includedField: string = "included",
|
||||
) =>
|
||||
Object.fromEntries(
|
||||
data[includedField]
|
||||
.filter((item: { type: string }) => item.type === type)
|
||||
.map((item: { id: string }) => [item.id, item]),
|
||||
);
|
||||
|
||||
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
|
||||
|
||||
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export * from "./custom";
|
||||
export * from "./helper";
|
||||
export * from "./utils";
|
||||
|
||||
+21
-1
@@ -154,17 +154,37 @@ export interface FindingProps {
|
||||
updated_at: string;
|
||||
};
|
||||
relationships: {
|
||||
resources: {
|
||||
data: {
|
||||
type: "Resource";
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
scan: {
|
||||
data: {
|
||||
type: "Scan";
|
||||
id: string;
|
||||
};
|
||||
attributes: {
|
||||
name: string;
|
||||
trigger: string;
|
||||
state: string;
|
||||
unique_resource_count: number;
|
||||
progress: number;
|
||||
scanner_args: {
|
||||
checks_to_execute: string[];
|
||||
};
|
||||
duration: number;
|
||||
started_at: string;
|
||||
completed_at: string;
|
||||
scheduled_at: string | null;
|
||||
};
|
||||
};
|
||||
resource: {
|
||||
data: {
|
||||
type: "Resource";
|
||||
id: string;
|
||||
};
|
||||
}[];
|
||||
attributes: {
|
||||
uid: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user