From 868615fa89d6653fb6979fadc017a968965f74c9 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 16 Oct 2024 07:08:22 +0200 Subject: [PATCH 1/8] chore: clean finding folder --- components/findings/FindingsCard.tsx | 93 ------------ components/findings/FindingsCardContent.tsx | 27 ---- components/findings/FindingsCardDetail.tsx | 59 -------- components/findings/FindingsCardScan.tsx | 25 ---- components/findings/FindingsCardType.tsx | 25 ---- components/findings/index.ts | 7 - components/findings/table/ColumnsFindings.tsx | 80 ----------- .../findings/table/DataTableFindings.tsx | 133 ------------------ 8 files changed, 449 deletions(-) delete mode 100644 components/findings/FindingsCard.tsx delete mode 100644 components/findings/FindingsCardContent.tsx delete mode 100644 components/findings/FindingsCardDetail.tsx delete mode 100644 components/findings/FindingsCardScan.tsx delete mode 100644 components/findings/FindingsCardType.tsx delete mode 100644 components/findings/table/ColumnsFindings.tsx delete mode 100644 components/findings/table/DataTableFindings.tsx diff --git a/components/findings/FindingsCard.tsx b/components/findings/FindingsCard.tsx deleted file mode 100644 index b4f9cab7a6..0000000000 --- a/components/findings/FindingsCard.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Divider } from "@nextui-org/react"; -import clsx from "clsx"; -import React from "react"; - -import { - FindingsCardContent, - FindingsCardDetail, - FindingsCardScan, - FindingsCardType, -} from "@/components/findings"; -import { FindingProps } from "@/types"; -interface FindingsCardProps { - selectedRow: FindingProps; -} - -export const FindingsCard: React.FC = ({ selectedRow }) => { - const { attributes, card } = selectedRow || {}; - const { CheckTitle } = attributes || {}; - const { - resourceLink, - resourceId, - resourceARN, - checkLink, - checkId, - type, - scanTime, - findingLink, - findingId, - details, - riskDetails, - riskLink, - recommendationDetails, - recommendationLink, - referenceInformation, - referenceLink, - } = card || {}; - - return ( - <> -
-

{CheckTitle}

- - - - - - - - - - - - - -
- - ); -}; diff --git a/components/findings/FindingsCardContent.tsx b/components/findings/FindingsCardContent.tsx deleted file mode 100644 index 09bba0150a..0000000000 --- a/components/findings/FindingsCardContent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Link } from "@nextui-org/react"; -import React from "react"; - -interface FindingsCardContentProps { - title: string; - url?: string; - description: string; -} - -export const FindingsCardContent: React.FC = ({ - title, - url, - description, -}) => { - return ( - <> -

{title}

- {url ? ( - - {description} - - ) : ( -

{description}

- )} - - ); -}; diff --git a/components/findings/FindingsCardDetail.tsx b/components/findings/FindingsCardDetail.tsx deleted file mode 100644 index 957cc6809e..0000000000 --- a/components/findings/FindingsCardDetail.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Button, Link } from "@nextui-org/react"; -import React from "react"; - -interface FindingsCardDetailProps { - title: string; - url?: string; - description: string; - type?: DetailType; -} - -type DetailType = "default" | "risk" | "recommendation" | "reference"; - -const getDetailColorClass = (type: DetailType): string => { - switch (type) { - case "risk": - return "border-red-200"; - case "recommendation": - return "border-green-200"; - case "reference": - return "border-gray-200"; - case "default": - default: - return "border-yellow-200"; - } -}; - -export const FindingsCardDetail: React.FC = ({ - title, - url, - description, - type = "default", -}) => { - return ( - <> - {description && ( -
-

- {title} - {url && ( - - )} -

-

{description}

-
- )} - - ); -}; diff --git a/components/findings/FindingsCardScan.tsx b/components/findings/FindingsCardScan.tsx deleted file mode 100644 index 1ccab7a0a8..0000000000 --- a/components/findings/FindingsCardScan.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { format, parseISO } from "date-fns"; -import React from "react"; - -interface FindingsCardScanProps { - title: string; - dateTime: string; -} - -export const FindingsCardScan: React.FC = ({ - title, - dateTime = "", -}) => { - const date = dateTime && parseISO(dateTime); - const formattedDate = date && format(date, "MMM dd, yyyy"); - const formattedTime = date && format(date, "p 'UTC'"); - - return ( - <> -

{title}

-

- {formattedDate} at {formattedTime} -

- - ); -}; diff --git a/components/findings/FindingsCardType.tsx b/components/findings/FindingsCardType.tsx deleted file mode 100644 index a525d0d1ab..0000000000 --- a/components/findings/FindingsCardType.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -interface FindingsCardTypeProps { - type: string[]; -} - -export const FindingsCardType: React.FC = ({ - type = [], -}) => { - const typeContent = () => { - if (type.length > 0) { - return type.join(", "); - } - return type[0]; - }; - - return ( - <> -

- {type.length > 1 ? "Types:" : "Type:"} -

-

{typeContent()}

- - ); -}; diff --git a/components/findings/index.ts b/components/findings/index.ts index e7c1764519..6f9176cda7 100644 --- a/components/findings/index.ts +++ b/components/findings/index.ts @@ -1,8 +1 @@ -export * from "./FindingsCard"; -export * from "./FindingsCardContent"; -export * from "./FindingsCardDetail"; -export * from "./FindingsCardScan"; -export * from "./FindingsCardType"; -export * from "./table/ColumnsFindings"; -export * from "./table/DataTableFindings"; export * from "./table/SkeletonTableFindings"; diff --git a/components/findings/table/ColumnsFindings.tsx b/components/findings/table/ColumnsFindings.tsx deleted file mode 100644 index b27f4a6dbb..0000000000 --- a/components/findings/table/ColumnsFindings.tsx +++ /dev/null @@ -1,80 +0,0 @@ -"use client"; - -import { - Button, - Dropdown, - DropdownItem, - DropdownMenu, - DropdownTrigger, -} from "@nextui-org/react"; -import { ColumnDef } from "@tanstack/react-table"; - -import { VerticalDotsIcon } from "@/components/icons"; -import { SeverityBadge } from "@/components/ui/table"; -import { FindingProps } from "@/types"; - -const getFindingsAttributes = (row: { original: FindingProps }) => { - return row.original.attributes; -}; - -export const ColumnsFindings: ColumnDef[] = [ - { - accessorKey: "checkTitle", - header: "Check", - cell: ({ row }) => { - const { CheckTitle } = getFindingsAttributes(row); - return

{CheckTitle}

; - }, - }, - { - accessorKey: "severity", - header: "Severity", - cell: ({ row }) => { - const { severity } = getFindingsAttributes(row); - return ; - }, - }, - - { - accessorKey: "region", - header: "Region", - cell: ({ row }) => { - const { region } = getFindingsAttributes(row); - return

{region}

; - }, - }, - { - accessorKey: "service", - header: "Service", - cell: ({ row }) => { - const { service } = getFindingsAttributes(row); - return

{service}

; - }, - }, - { - accessorKey: "account", - header: "Account", - cell: ({ row }) => { - const { account } = getFindingsAttributes(row); - return

{account}

; - }, - }, - { - accessorKey: "actions", - header: () => ( -
- - - - - - Mute Findings for Selected Filters - Configure Muted Findings - - -
- ), - }, -]; diff --git a/components/findings/table/DataTableFindings.tsx b/components/findings/table/DataTableFindings.tsx deleted file mode 100644 index 1bc3884492..0000000000 --- a/components/findings/table/DataTableFindings.tsx +++ /dev/null @@ -1,133 +0,0 @@ -"use client"; - -import { Button } from "@nextui-org/react"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - getPaginationRowModel, - Row, - useReactTable, -} from "@tanstack/react-table"; -import clsx from "clsx"; - -import { FindingsCard } from "@/components/findings"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { FindingProps } from "@/types"; - -interface DataTableFindingsProps { - columns: ColumnDef[]; - data: TData[]; -} - -export function DataTableFindings({ - columns, - data, -}: DataTableFindingsProps) { - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - }); - - const selectedRow = table.getSelectedRowModel().rows[0]?.original; - - const onClick = (row: Row) => { - table.resetRowSelection(true); - row.toggleSelected(); - return; - }; - - return ( - <> -
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - onClick(row)} - className={"hover:cursor-pointer"} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} - - )) - ) : ( - - - No results. - - - )} - -
-
-
- - -
-
- -
- - ); -} From b3c905c95a70065b5a7e51cf8629f76cffc4cf61 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 16 Oct 2024 07:09:30 +0200 Subject: [PATCH 2/8] chore: add Findings props type --- app/(prowler)/findings/page.tsx | 412 +------------------------------- types/components.ts | 91 +++++-- 2 files changed, 69 insertions(+), 434 deletions(-) diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx index 280ecf293b..a79b080508 100644 --- a/app/(prowler)/findings/page.tsx +++ b/app/(prowler)/findings/page.tsx @@ -1,11 +1,7 @@ import { Spacer } from "@nextui-org/react"; import React, { Suspense } from "react"; -import { - ColumnsFindings, - DataTableFindings, - SkeletonTableFindings, -} from "@/components/findings"; +import { SkeletonTableFindings } from "@/components/findings"; import { Header } from "@/components/ui"; export default async function Findings() { @@ -15,412 +11,8 @@ export default async function Findings() {
- }> - - + }>
); } - -const SSRDataTable = async () => { - return ( - ", - resourceLink: - "https://app.prowler.pro/app/findings?search=%3Croot_account%3E", - resourceARN: "arn:aws:iam::714274078102:root", - checkId: "iam_root_mfa_enabled", - checkLink: - "https://app.prowler.pro/app/findings?search=iam_root_mfa_enabled", - type: [ - "Software and Configuration Checks", - "Industry and Regulatory Standards", - "CIS AWS Foundations Benchmark", - ], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "bc3a34e0-16f0-4ea1-ac62-f796c8af3448", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-05&search=bc3a34e0-16f0-4ea1-ac62-f796c8af3448", - details: "MFA is not enabled for root account.", - riskLink: "", - riskDetails: - "The root account is the most privileged user in an AWS account. MFA adds an extra layer of protection on top of a user name and password. With MFA enabled when a user signs in to an AWS website they will be prompted for their user name and password as well as for an authentication code from their AWS MFA device. When virtual MFA is used for root accounts it is recommended that the device used is NOT a personal device but rather a dedicated mobile device (tablet or phone) that is managed to be kept charged and secured independent of any individual personal devices. (non-personal virtual MFA) This lessens the risks of losing access to the MFA due to device loss / trade-in or if the individual owning the device is no longer employed at the company.", - recommendationLink: - "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_root-user.html#id_root-user_manage_mfa", - recommendationDetails: - "Using IAM console navigate to Dashboard and expand Activate MFA on your root account.", - referenceInformation: "", - referenceLink: "", - }, - }, - { - id: "67890", - attributes: { - CheckTitle: - "Ensure S3 buckets with public read/write access are not allowed", - severity: "high", - status: "fail", - region: "us-east-1", - service: "s3", - account: "prod (987654321012)", - }, - card: { - resourceId: "bucket-example-public-read-write", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=bucket-example-public-read-write", - resourceARN: "arn:aws:s3:::bucket-example-public-read-write", - checkId: "s3_bucket_public_access", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=s3_bucket_public_access", - type: ["Security"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "e7b3d6a2-39a1-4f0e-9b78-29f4e6f292d1", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=e7b3d6a2-39a1-4f0e-9b78-29f4e6f292d1", - details: - "S3 bucket example-public-read-write allows public read/write access.", - riskLink: - "https://docs.aws.amazon.com/AmazonS3/latest/dev/security-best-practices.html", - riskDetails: - "Publicly accessible S3 buckets can expose sensitive data and be exploited by attackers.", - recommendationLink: - "https://docs.aws.amazon.com/AmazonS3/latest/user-guide/block-public-access.html", - recommendationDetails: - "Use S3 Block Public Access to prevent public access to your S3 buckets.", - referenceInformation: "AWS Console", - referenceLink: - "https://docs.prowler.com/checks/aws/s3-policies/bc_aws_s3_1/#console", - }, - }, - { - id: "11223", - attributes: { - CheckTitle: - "Ensure IAM password policy requires minimum length of 12 characters", - severity: "medium", - status: "fail", - region: "eu-central-1", - service: "iam", - account: "staging (123456789012)", - }, - card: { - resourceId: "password-policy", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=password-policy", - resourceARN: "arn:aws:iam::123456789012:password-policy", - checkId: "iam_password_policy_min_length", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=iam_password_policy_min_length", - type: ["Security"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "c2b3d1a4-7a9f-4d5c-a9ef-765a1d7f421c", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=c2b3d1a4-7a9f-4d5c-a9ef-765a1d7f421c", - details: - "IAM password policy does not require a minimum length of 12 characters.", - riskLink: - "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html", - riskDetails: - "Weak password policies increase the risk of unauthorized access to AWS resources.", - recommendationLink: - "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html", - recommendationDetails: - "Enforce a minimum password length of 12 characters in your IAM password policy.", - referenceInformation: "AWS CLI", - referenceLink: - "https://docs.prowler.com/checks/aws/iam-policies/bc_aws_iam_1/#cli-command", - }, - }, - { - id: "44556", - attributes: { - CheckTitle: "Ensure RDS instances are not publicly accessible", - severity: "high", - status: "muted", - region: "ap-southeast-1", - service: "rds", - account: "prod (234567890123)", - }, - card: { - resourceId: "rds-instance-public-access", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=rds-instance-public-access", - resourceARN: - "arn:aws:rds:ap-southeast-1:234567890123:db:rds-instance-public-access", - checkId: "rds_instance_public_access", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=rds_instance_public_access", - type: ["Security"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "f3b4c5d6-19a7-45d8-bc3e-8c5f6a7d8e9b", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-06&search=f3b4c5d6-19a7-45d8-bc3e-8c5f6a7d8e9b", - details: - "RDS instance is not publicly accessible, which adheres to security best practices.", - riskLink: - "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.RDSSecurity.html", - riskDetails: - "Publicly accessible RDS instances can be attacked by unauthorized users.", - recommendationLink: - "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.Scenarios.html", - recommendationDetails: - "Ensure RDS instances are not publicly accessible by placing them in private subnets.", - referenceInformation: "AWS Console", - referenceLink: - "https://docs.prowler.com/checks/aws/rds-policies/bc_aws_rds_1/#console", - }, - }, - { - id: "77889", - attributes: { - CheckTitle: "Ensure EBS volumes are encrypted", - severity: "critical", - status: "fail", - region: "eu-west-1", - service: "ec2", - account: "prod (345678901234)", - }, - card: { - resourceId: "volume-0123456789abcdef0", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=volume-0123456789abcdef0", - resourceARN: - "arn:aws:ec2:eu-west-1:345678901234:volume/volume-0123456789abcdef0", - checkId: "ebs_volume_encryption", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=ebs_volume_encryption", - type: ["Encryption"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=a1b2c3d4-e5f6-7890-abcd-ef1234567890", - details: "EBS volume volume-0123456789abcdef0 is not encrypted.", - riskLink: - "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html", - riskDetails: - "Unencrypted EBS volumes can expose sensitive data if compromised.", - recommendationLink: - "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html", - recommendationDetails: - "Enable encryption for EBS volumes using AWS-managed or customer-managed keys.", - referenceInformation: "AWS CLI", - referenceLink: - "https://docs.prowler.com/checks/aws/ec2-policies/bc_aws_ec2_1/#cli-command", - }, - }, - { - id: "99100", - attributes: { - CheckTitle: "Ensure CloudTrail is enabled in all regions", - severity: "critical", - status: "fail", - region: "us-west-2", - service: "cloudtrail", - account: "dev (456789012345)", - }, - card: { - resourceId: "cloudtrail-all-regions", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=cloudtrail-all-regions", - resourceARN: - "arn:aws:cloudtrail:us-west-2:456789012345:trail/cloudtrail-all-regions", - checkId: "cloudtrail_all_regions_enabled", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=cloudtrail_all_regions_enabled", - type: ["Logging"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "b2c3d4e5-f6a7-8901-bcde-f123456789ab", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=b2c3d4e5-f6a7-8901-bcde-f123456789ab", - details: - "CloudTrail is not enabled for all regions in the account.", - riskLink: - "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-best-practices.html", - riskDetails: - "Without CloudTrail enabled in all regions, activities in non-monitored regions may go unnoticed.", - recommendationLink: - "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/creating-trail-using-console.html", - recommendationDetails: - "Enable CloudTrail across all regions to ensure comprehensive monitoring.", - referenceInformation: "AWS Console", - referenceLink: - "https://docs.prowler.com/checks/aws/cloudtrail-policies/bc_aws_cloudtrail_1/#console", - }, - }, - { - id: "22334", - attributes: { - CheckTitle: "Ensure EC2 instances do not use outdated AMIs", - severity: "medium", - status: "fail", - region: "us-east-1", - service: "ec2", - account: "prod (567890123456)", - }, - card: { - resourceId: "instance-ami-outdated", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=instance-ami-outdated", - resourceARN: - "arn:aws:ec2:us-east-1:567890123456:instance/instance-ami-outdated", - checkId: "ec2_instance_outdated_ami", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=ec2_instance_outdated_ami", - type: ["Configuration"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "c3d4e5f6-a789-0123-bcde-f234567890ab", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-07&search=c3d4e5f6-a789-0123-bcde-f234567890ab", - details: - "EC2 instance instance-ami-outdated is using an outdated AMI.", - riskLink: - "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html", - riskDetails: - "Outdated AMIs may have unpatched vulnerabilities that can be exploited.", - recommendationLink: - "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/creating-an-ami.html", - recommendationDetails: - "Update EC2 instances to use the latest AMIs with all security patches applied.", - referenceInformation: "AWS CLI", - referenceLink: - "https://docs.prowler.com/checks/aws/ec2-policies/bc_aws_ec2_2/#cli-command", - }, - }, - { - id: "55667", - attributes: { - CheckTitle: - "Ensure CloudWatch alarms exist for critical system events", - severity: "high", - status: "muted", - region: "eu-west-2", - service: "cloudwatch", - account: "prod (678901234567)", - }, - card: { - resourceId: "cloudwatch-alarms", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-08&search=cloudwatch-alarms", - resourceARN: - "arn:aws:cloudwatch:eu-west-2:678901234567:alarm/cloudwatch-alarms", - checkId: "cloudwatch_alarms_for_critical_events", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-08&search=cloudwatch_alarms_for_critical_events", - type: ["Monitoring"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "d4e5f6a7-8901-bcde-f345678901ab", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-08&search=d4e5f6a7-8901-bcde-f345678901ab", - details: - "CloudWatch alarms are correctly configured for critical system events.", - riskLink: - "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatchAlarms.html", - riskDetails: - "Without alarms for critical events, key issues may go unnoticed, leading to outages.", - recommendationLink: - "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Create_Alarm.html", - recommendationDetails: - "Ensure alarms are set up for critical events to trigger timely notifications.", - referenceInformation: "AWS Console", - referenceLink: - "https://docs.prowler.com/checks/aws/cloudwatch-policies/bc_aws_cloudwatch_1/#console", - }, - }, - { - id: "88900", - attributes: { - CheckTitle: "Ensure IAM users have least privilege permissions", - severity: "medium", - status: "fail", - region: "ap-northeast-1", - service: "iam", - account: "dev (789012345678)", - }, - card: { - resourceId: "iam-user-least-privilege", - resourceLink: - "https://app.prowler.pro/app/findings?date=2024-08-08&search=iam-user-least-privilege", - resourceARN: - "arn:aws:iam::789012345678:user/iam-user-least-privilege", - checkId: "iam_user_least_privilege", - checkLink: - "https://app.prowler.pro/app/findings?date=2024-08-08&search=iam_user_least_privilege", - type: ["Security"], - scanTime: "2024-07-17T09:55:14.191475Z", - findingId: "e5f6a7b8-9012-bcde-f456789012ab", - findingLink: - "https://app.prowler.pro/app/findings?date=2024-08-08&search=e5f6a7b8-9012-bcde-f456789012ab", - details: - "IAM user iam-user-least-privilege has more permissions than necessary.", - riskLink: - "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html", - riskDetails: - "Excessive permissions increase the risk of privilege escalation and security breaches.", - recommendationLink: - "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html", - recommendationDetails: - "Apply the principle of least privilege to IAM users by restricting their permissions to the minimum necessary.", - referenceInformation: "AWS CLI", - referenceLink: - "https://docs.prowler.com/checks/aws/iam-policies/bc_aws_iam_2/#cli-command", - }, - }, - ]} - /> - ); -}; diff --git a/types/components.ts b/types/components.ts index f88967e85e..392a252335 100644 --- a/types/components.ts +++ b/types/components.ts @@ -107,33 +107,76 @@ export interface ScanProps { }; } -export interface FindingProps { +export interface FindingsProps { + type: "Findings"; id: string; attributes: { - CheckTitle: string; - severity: "critical" | "high" | "medium" | "low"; - status: "fail" | "success" | "muted"; - region: string; - service: string; - account: string; + uid: string; + delta: "new" | "changed" | null; + status: "PASS" | "FAIL" | "MANUAL" | "MUTED"; + status_extended: string; + severity: "informational" | "low" | "medium" | "high" | "critical"; + check_id: string; + check_metadata: { + check_id: string; + metadata: { + Risk: string; + Notes: string; + CheckID: string; + Provider: string; + Severity: "informational" | "low" | "medium" | "high" | "critical"; + CheckType: string[]; + DependsOn: string[]; + RelatedTo: string[]; + Categories: string[]; + CheckTitle: string; + RelatedUrl: string; + Description: string; + Remediation: { + Code: { + CLI: string; + Other: string; + NativeIaC: string; + Terraform: string; + }; + Recommendation: { + Url: string; + Text: string; + }; + }; + ServiceName: string; + ResourceType: string; + SubServiceName: string; + ResourceIdTemplate: string; + }; + }; + raw_result: { + impact: string; + status: "PASS" | "FAIL" | "MANUAL" | "MUTED"; + severity: "informational" | "low" | "medium" | "high" | "critical"; + }; + inserted_at: string; + updated_at: string; }; - card: { - resourceId: string; - resourceLink: string; - resourceARN: string; - checkId: string; - checkLink: string; - type: string[]; - scanTime: string; - findingId: string; - findingLink: string; - details: string; - riskLink: string; - riskDetails: string; - recommendationLink: string; - recommendationDetails: string; - referenceInformation: string; - referenceLink: string; + relationships: { + scan: { + data: { + type: "Scan"; + id: string; + }; + }; + resources: { + data: { + type: "Resource"; + id: string; + }[]; + meta: { + count: number; + }; + }; + }; + links: { + self: string; }; } From efd2805602853fdd7c9e0a40142412af8ef9f853 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 16 Oct 2024 11:07:57 +0200 Subject: [PATCH 3/8] feat: render finding table --- actions/findings/findings.ts | 50 ++++++++ actions/findings/index.ts | 1 + app/(prowler)/findings/page.tsx | 54 ++++++++- components/filters/data-filters.ts | 19 +++ components/findings/index.ts | 1 - components/findings/table/column-findings.tsx | 90 +++++++++++++++ .../findings/table/data-table-row-actions.tsx | 108 ++++++++++++++++++ components/findings/table/index.ts | 3 + ...ndings.tsx => skeleton-table-findings.tsx} | 0 9 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 actions/findings/findings.ts create mode 100644 actions/findings/index.ts delete mode 100644 components/findings/index.ts create mode 100644 components/findings/table/column-findings.tsx create mode 100644 components/findings/table/data-table-row-actions.tsx create mode 100644 components/findings/table/index.ts rename components/findings/table/{SkeletonTableFindings.tsx => skeleton-table-findings.tsx} (100%) diff --git a/actions/findings/findings.ts b/actions/findings/findings.ts new file mode 100644 index 0000000000..228f7dba30 --- /dev/null +++ b/actions/findings/findings.ts @@ -0,0 +1,50 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; + +import { auth } from "@/auth.config"; +import { parseStringify } from "@/lib"; + +export const getFindings = async ({ + page = 1, + query = "", + sort = "", + filters = {}, +}) => { + const session = await auth(); + + if (isNaN(Number(page)) || page < 1) redirect("/findings"); + + const keyServer = process.env.API_BASE_URL; + const url = new URL( + `${keyServer}/findings?filter[inserted_at__gte]=2024-01-01`, + ); + + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } + }); + + try { + const providers = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await providers.json(); + const parsedData = parseStringify(data); + revalidatePath("/findings"); + return parsedData; + } catch (error) { + console.error("Error fetching findings:", error); + return undefined; + } +}; diff --git a/actions/findings/index.ts b/actions/findings/index.ts new file mode 100644 index 0000000000..eb3a674c67 --- /dev/null +++ b/actions/findings/index.ts @@ -0,0 +1 @@ +export * from "./findings"; diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx index a79b080508..3558db664b 100644 --- a/app/(prowler)/findings/page.tsx +++ b/app/(prowler)/findings/page.tsx @@ -1,18 +1,60 @@ import { Spacer } from "@nextui-org/react"; import React, { Suspense } from "react"; -import { SkeletonTableFindings } from "@/components/findings"; +import { getFindings } from "@/actions/findings"; +import { filterFindings } from "@/components/filters/data-filters"; +import { FilterControls } from "@/components/filters/filter-controls"; +import { + ColumnFindings, + SkeletonTableFindings, +} from "@/components/findings/table"; import { Header } from "@/components/ui"; +import { DataTable } from "@/components/ui/table"; +import { SearchParamsProps } from "@/types/components"; -export default async function Findings() { +export default async function Findings({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { return ( <>
-
- - }> -
+ + + + }> + + ); } + +const SSRDataTable = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString(); + + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; + + const findingsData = await getFindings({ query, page, sort, filters }); + + return ( + + ); +}; diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts index d6d876b522..2b2d319783 100644 --- a/components/filters/data-filters.ts +++ b/components/filters/data-filters.ts @@ -27,3 +27,22 @@ export const filterScans = [ }, // Add more filter categories as needed ]; + +export const filterFindings = [ + { + key: "severity", + labelCheckboxGroup: "Severity", + values: ["informational", "low", "medium", "high", "critical"], + }, + { + key: "status", + labelCheckboxGroup: "Status", + values: ["PASS", "FAIL", "MANUAL", "MUTED"], + }, + { + key: "delta", + labelCheckboxGroup: "Delta", + values: ["new", "changed"], + }, + // Add more filter categories as needed +]; diff --git a/components/findings/index.ts b/components/findings/index.ts deleted file mode 100644 index 6f9176cda7..0000000000 --- a/components/findings/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./table/SkeletonTableFindings"; diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx new file mode 100644 index 0000000000..096e9ab7ee --- /dev/null +++ b/components/findings/table/column-findings.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; + +import { DateWithTime, SnippetId } from "@/components/ui/entities"; +import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; +import { FindingsProps } from "@/types"; + +import { DataTableRowActions } from "./data-table-row-actions"; + +const getFindingsData = (row: { original: FindingsProps }) => { + return row.original; +}; + +export const ColumnFindings: ColumnDef[] = [ + // { + // header: " ", + // cell: ({ row }) =>

{row.index + 1}

, + // }, + { + accessorKey: "account", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { status }, + } = getFindingsData(row); + return

{status}

; + }, + }, + { + accessorKey: "uid", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { uid }, + } = getFindingsData(row); + return ; + }, + }, + { + accessorKey: "status", + header: "Scan Status", + cell: () => { + // Temporarily overwriting the value until the API is functional. + return ; + }, + }, + { + accessorKey: "lastScan", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { updated_at }, + } = getFindingsData(row); + return ; + }, + }, + { + accessorKey: "added", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { inserted_at }, + } = getFindingsData(row); + return ; + }, + }, + { + id: "actions", + cell: ({ row }) => { + return ; + }, + }, +]; diff --git a/components/findings/table/data-table-row-actions.tsx b/components/findings/table/data-table-row-actions.tsx new file mode 100644 index 0000000000..dd59e24126 --- /dev/null +++ b/components/findings/table/data-table-row-actions.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { + Button, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownSection, + DropdownTrigger, +} from "@nextui-org/react"; +import { + // AddNoteBulkIcon, + DeleteDocumentBulkIcon, + 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"; +// import { CustomAlertModal } from "@/components/ui/custom"; + +// import { EditForm } from "../forms"; +// import { DeleteForm } from "../forms/delete-form"; + +interface DataTableRowActionsProps { + row: Row; +} +const iconClasses = + "text-2xl text-default-500 pointer-events-none flex-shrink-0"; + +export function DataTableRowActions({ + row, +}: DataTableRowActionsProps) { + // const [isEditOpen, setIsEditOpen] = useState(false); + // const [isDeleteOpen, setIsDeleteOpen] = useState(false); + // const providerId = (row.original as { id: string }).id; + // const providerAlias = (row.original as any).attributes?.alias; + return ( + <> + {/* + + + + + */} + +
+ + + + + + + } + // onClick={() => setIsEditOpen(true)} + > + Edit Provider + + + + + } + // onClick={() => setIsDeleteOpen(true)} + > + Delete Provider + + + + +
+ + ); +} diff --git a/components/findings/table/index.ts b/components/findings/table/index.ts new file mode 100644 index 0000000000..89d5150daf --- /dev/null +++ b/components/findings/table/index.ts @@ -0,0 +1,3 @@ +export * from "./column-findings"; +export * from "./data-table-row-actions"; +export * from "./skeleton-table-findings"; diff --git a/components/findings/table/SkeletonTableFindings.tsx b/components/findings/table/skeleton-table-findings.tsx similarity index 100% rename from components/findings/table/SkeletonTableFindings.tsx rename to components/findings/table/skeleton-table-findings.tsx From a694b422cf10f859757eef6024938fa3ee406917 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 16 Oct 2024 18:03:29 +0200 Subject: [PATCH 4/8] WIP --- actions/findings/findings.ts | 9 ++- components/findings/table/column-findings.tsx | 70 ++++++++++++------- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/actions/findings/findings.ts b/actions/findings/findings.ts index 228f7dba30..86f0bf5605 100644 --- a/actions/findings/findings.ts +++ b/actions/findings/findings.ts @@ -17,9 +17,7 @@ export const getFindings = async ({ if (isNaN(Number(page)) || page < 1) redirect("/findings"); const keyServer = process.env.API_BASE_URL; - const url = new URL( - `${keyServer}/findings?filter[inserted_at__gte]=2024-01-01`, - ); + const url = new URL(`${keyServer}/findings`); if (page) url.searchParams.append("page[number]", page.toString()); if (query) url.searchParams.append("filter[search]", query); @@ -33,14 +31,15 @@ export const getFindings = async ({ }); try { - const providers = await fetch(url.toString(), { + const findings = await fetch(url.toString(), { headers: { Accept: "application/vnd.api+json", Authorization: `Bearer ${session?.accessToken}`, }, }); - const data = await providers.json(); + const data = await findings.json(); const parsedData = parseStringify(data); + console.log(parsedData.data); revalidatePath("/findings"); return parsedData; } catch (error) { diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx index 096e9ab7ee..20d7d6198b 100644 --- a/components/findings/table/column-findings.tsx +++ b/components/findings/table/column-findings.tsx @@ -2,45 +2,65 @@ import { ColumnDef } from "@tanstack/react-table"; -import { DateWithTime, SnippetId } from "@/components/ui/entities"; +import { DateWithTime } from "@/components/ui/entities"; import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; import { FindingsProps } from "@/types"; import { DataTableRowActions } from "./data-table-row-actions"; const getFindingsData = (row: { original: FindingsProps }) => { + console.log(row.original); + return row.original; }; +const getFindingsMetadata = (row: { original: FindingsProps }) => { + return row.original.attributes.check_metadata.metadata; +}; + export const ColumnFindings: ColumnDef[] = [ // { // header: " ", // cell: ({ row }) =>

{row.index + 1}

, // }, { - accessorKey: "account", + accessorKey: "check", header: ({ column }) => ( - + ), cell: ({ row }) => { - const { - attributes: { status }, - } = getFindingsData(row); - return

{status}

; - }, - }, - { - accessorKey: "uid", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const { - attributes: { uid }, - } = getFindingsData(row); - return ; + const { CheckTitle } = getFindingsMetadata(row); + return

{CheckTitle}

; }, }, + // { + // accessorKey: "uid", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const { + // attributes: { uid }, + // } = getFindingsData(row); + // return ; + // }, + // }, + // { + // accessorKey: "severity", + // header: ({ column }) => ( + // + // ), + // cell: ({ row }) => { + // const { + // attributes: { severity }, + // } = getFindingsData(row); + // return ; + // }, + // }, { accessorKey: "status", header: "Scan Status", @@ -50,19 +70,17 @@ export const ColumnFindings: ColumnDef[] = [ }, }, { - accessorKey: "lastScan", + accessorKey: "service", header: ({ column }) => ( ), cell: ({ row }) => { - const { - attributes: { updated_at }, - } = getFindingsData(row); - return ; + const { ServiceName } = getFindingsMetadata(row); + return

{ServiceName}

; }, }, { From 5f7a3d0bcf63ff5b8ef6744836f09ef4fdb8384e Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 21 Oct 2024 11:55:02 +0200 Subject: [PATCH 5/8] chore: update FindingProps to the latest version --- components/findings/table/column-findings.tsx | 19 +++--- types/components.ts | 67 +++++++++---------- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx index 20d7d6198b..d8d02ae9c2 100644 --- a/components/findings/table/column-findings.tsx +++ b/components/findings/table/column-findings.tsx @@ -4,21 +4,20 @@ import { ColumnDef } from "@tanstack/react-table"; import { DateWithTime } from "@/components/ui/entities"; import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; -import { FindingsProps } from "@/types"; +import { FindingProps } from "@/types"; import { DataTableRowActions } from "./data-table-row-actions"; -const getFindingsData = (row: { original: FindingsProps }) => { +const getFindingsData = (row: { original: FindingProps }) => { console.log(row.original); - return row.original; }; -const getFindingsMetadata = (row: { original: FindingsProps }) => { - return row.original.attributes.check_metadata.metadata; +const getFindingsMetadata = (row: { original: FindingProps }) => { + return row.original.attributes.check_metadata; }; -export const ColumnFindings: ColumnDef[] = [ +export const ColumnFindings: ColumnDef[] = [ // { // header: " ", // cell: ({ row }) =>

{row.index + 1}

, @@ -29,8 +28,8 @@ export const ColumnFindings: ColumnDef[] = [ ), cell: ({ row }) => { - const { CheckTitle } = getFindingsMetadata(row); - return

{CheckTitle}

; + const { checktitle } = getFindingsMetadata(row); + return

{checktitle}

; }, }, // { @@ -79,8 +78,8 @@ export const ColumnFindings: ColumnDef[] = [ /> ), cell: ({ row }) => { - const { ServiceName } = getFindingsMetadata(row); - return

{ServiceName}

; + const { servicename } = getFindingsMetadata(row); + return

{servicename}

; }, }, { diff --git a/types/components.ts b/types/components.ts index 392a252335..3164bcf3c9 100644 --- a/types/components.ts +++ b/types/components.ts @@ -107,7 +107,7 @@ export interface ScanProps { }; } -export interface FindingsProps { +export interface FindingProps { type: "Findings"; id: string; attributes: { @@ -118,43 +118,38 @@ export interface FindingsProps { severity: "informational" | "low" | "medium" | "high" | "critical"; check_id: string; check_metadata: { - check_id: string; - metadata: { - Risk: string; - Notes: string; - CheckID: string; - Provider: string; - Severity: "informational" | "low" | "medium" | "high" | "critical"; - CheckType: string[]; - DependsOn: string[]; - RelatedTo: string[]; - Categories: string[]; - CheckTitle: string; - RelatedUrl: string; - Description: string; - Remediation: { - Code: { - CLI: string; - Other: string; - NativeIaC: string; - Terraform: string; - }; - Recommendation: { - Url: string; - Text: string; - }; - }; - ServiceName: string; - ResourceType: string; - SubServiceName: string; - ResourceIdTemplate: string; - }; - }; - raw_result: { - impact: string; - status: "PASS" | "FAIL" | "MANUAL" | "MUTED"; + risk: string; + notes: string; + checkid: string; + provider: string; severity: "informational" | "low" | "medium" | "high" | "critical"; + checktype: string[]; + dependson: string[]; + relatedto: string[]; + categories: string[]; + checktitle: string; + compliance: string | null; + relatedurl: string; + description: string; + remediation: { + code: { + cli: string; + other: string; + nativeiac: string; + terraform: string; + }; + recommendation: { + url: string; + text: string; + }; + }; + servicename: string; + checkaliases: string[]; + resourcetype: string; + subservicename: string; + resourceidtemplate: string; }; + raw_result: object | null; inserted_at: string; updated_at: string; }; From cc0923b3c742c47bfaf7c28b8b7073245bfa8333 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 21 Oct 2024 12:02:07 +0200 Subject: [PATCH 6/8] chore: update FindingProps to the latest version --- types/components.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/types/components.ts b/types/components.ts index 3164bcf3c9..d2fbfe5014 100644 --- a/types/components.ts +++ b/types/components.ts @@ -169,6 +169,12 @@ export interface FindingProps { count: number; }; }; + provider: { + data: { + type: "Provider"; + id: string; + }; + }; }; links: { self: string; From af267fede42cf9cf9a59486d29e162017102842f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 21 Oct 2024 20:30:26 +0200 Subject: [PATCH 7/8] chore: WIP --- actions/findings/findings.ts | 6 +- app/(prowler)/findings/page.tsx | 54 ++++++++++- components/findings/table/column-findings.tsx | 94 ++++++++++++++++--- .../findings/table/data-table-row-actions.tsx | 12 +-- components/ui/table/severity-badge.tsx | 2 +- types/components.ts | 59 +++++++++++- 6 files changed, 195 insertions(+), 32 deletions(-) diff --git a/actions/findings/findings.ts b/actions/findings/findings.ts index 86f0bf5605..9fc89b0907 100644 --- a/actions/findings/findings.ts +++ b/actions/findings/findings.ts @@ -14,10 +14,11 @@ export const getFindings = async ({ }) => { const session = await auth(); - if (isNaN(Number(page)) || page < 1) redirect("/findings"); + if (isNaN(Number(page)) || page < 1) + redirect("findings?include=resources.provider,scan"); const keyServer = process.env.API_BASE_URL; - const url = new URL(`${keyServer}/findings`); + const url = new URL(`${keyServer}/findings?include=resources.provider,scan`); if (page) url.searchParams.append("page[number]", page.toString()); if (query) url.searchParams.append("filter[search]", query); @@ -39,7 +40,6 @@ export const getFindings = async ({ }); const data = await findings.json(); const parsedData = parseStringify(data); - console.log(parsedData.data); revalidatePath("/findings"); return parsedData; } catch (error) { diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx index 3558db664b..0e7dd6c9b6 100644 --- a/app/(prowler)/findings/page.tsx +++ b/app/(prowler)/findings/page.tsx @@ -10,7 +10,7 @@ import { } from "@/components/findings/table"; import { Header } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; -import { SearchParamsProps } from "@/types/components"; +import { FindingProps, SearchParamsProps } from "@/types/components"; export default async function Findings({ searchParams, @@ -49,10 +49,60 @@ 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]), + ); + + 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"); + 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; + + return { + ...finding, + relationships: { + scan: scan, + resource: resource, + provider: provider, + }, + }; + }); + + // Create the new object while maintaining the original structure + const enrichedResponse = { + ...findingsData, + data: enrichedFindings, + }; + return ( diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx index d8d02ae9c2..98803d2a7c 100644 --- a/components/findings/table/column-findings.tsx +++ b/components/findings/table/column-findings.tsx @@ -3,13 +3,17 @@ import { ColumnDef } from "@tanstack/react-table"; import { DateWithTime } from "@/components/ui/entities"; -import { DataTableColumnHeader, StatusBadge } from "@/components/ui/table"; +import { + DataTableColumnHeader, + SeverityBadge, + StatusBadge, +} from "@/components/ui/table"; import { FindingProps } from "@/types"; import { DataTableRowActions } from "./data-table-row-actions"; const getFindingsData = (row: { original: FindingProps }) => { - console.log(row.original); + // console.log(row.original); return row.original; }; @@ -17,6 +21,39 @@ const getFindingsMetadata = (row: { original: FindingProps }) => { return row.original.attributes.check_metadata; }; +const getResourceData = ( + row: { original: FindingProps }, + 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` + ); +}; + +const getProviderData = ( + row: { original: FindingProps }, + 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` + ); +}; + +const getScanData = ( + row: { original: FindingProps }, + 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[] = [ // { // header: " ", @@ -32,6 +69,20 @@ export const ColumnFindings: ColumnDef[] = [ return

{checktitle}

; }, }, + { + accessorKey: "region", + header: "Region", + cell: ({ row }) => { + const region = getResourceData(row, "region"); + + return ( + <> +
{region}
+ + ); + }, + }, + // { // accessorKey: "uid", // header: ({ column }) => ( @@ -45,27 +96,40 @@ export const ColumnFindings: ColumnDef[] = [ // }, // }, // { - // accessorKey: "severity", - // header: ({ column }) => ( - // - // ), + // accessorKey: "provider", + // header: "Provider Alias", // cell: ({ row }) => { // const { - // attributes: { severity }, - // } = getFindingsData(row); - // return ; + // attributes: { alias }, + // } = getProviderData(row, "alias"); + // return

{alias}

; // }, // }, + { + accessorKey: "severity", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { + attributes: { severity }, + } = getFindingsData(row); + return ; + }, + }, { accessorKey: "status", header: "Scan Status", - cell: () => { + cell: ({ row }) => { + const { + attributes: { status }, + } = getFindingsData(row); // Temporarily overwriting the value until the API is functional. - return ; + return ; }, }, { diff --git a/components/findings/table/data-table-row-actions.tsx b/components/findings/table/data-table-row-actions.tsx index dd59e24126..dea023d359 100644 --- a/components/findings/table/data-table-row-actions.tsx +++ b/components/findings/table/data-table-row-actions.tsx @@ -23,15 +23,15 @@ import { VerticalDotsIcon } from "@/components/icons"; // import { EditForm } from "../forms"; // import { DeleteForm } from "../forms/delete-form"; -interface DataTableRowActionsProps { - row: Row; +interface DataTableRowActionsProps { + row: Row; } const iconClasses = "text-2xl text-default-500 pointer-events-none flex-shrink-0"; -export function DataTableRowActions({ +export function DataTableRowActions({ row, -}: DataTableRowActionsProps) { +}: DataTableRowActionsProps) { // const [isEditOpen, setIsEditOpen] = useState(false); // const [isDeleteOpen, setIsDeleteOpen] = useState(false); // const providerId = (row.original as { id: string }).id; @@ -80,7 +80,7 @@ export function DataTableRowActions({ startContent={} // onClick={() => setIsEditOpen(true)} > - Edit Provider + Edit Finding @@ -97,7 +97,7 @@ export function DataTableRowActions({ } // onClick={() => setIsDeleteOpen(true)} > - Delete Provider + Delete Finding diff --git a/components/ui/table/severity-badge.tsx b/components/ui/table/severity-badge.tsx index 9539e56bb3..8b852eea36 100644 --- a/components/ui/table/severity-badge.tsx +++ b/components/ui/table/severity-badge.tsx @@ -4,7 +4,7 @@ import React from "react"; import { AlertIcon } from "@/components/icons"; -type Severity = "critical" | "high" | "medium" | "low"; +type Severity = "informational" | "low" | "medium" | "high" | "critical"; const severityIconMap = { critical: , diff --git a/types/components.ts b/types/components.ts index d2fbfe5014..89dee77381 100644 --- a/types/components.ts +++ b/types/components.ts @@ -108,7 +108,7 @@ export interface ScanProps { } export interface FindingProps { - type: "Findings"; + type: "Finding"; id: string; attributes: { uid: string; @@ -160,13 +160,40 @@ export interface FindingProps { id: string; }; }; - resources: { + resource: { data: { type: "Resource"; id: string; - }[]; - meta: { - count: number; + }; + attributes: { + uid: string; + name: string; + region: string; + service: string; + tags: Record; + type: string; + inserted_at: string; + updated_at: string; + }; + relationships: { + provider: { + data: { + type: "Provider"; + id: string; + }; + }; + findings: { + meta: { + count: number; + }; + data: { + type: "Finding"; + id: string; + }[]; + }; + }; + links: { + self: string; }; }; provider: { @@ -174,6 +201,28 @@ export interface FindingProps { type: "Provider"; id: string; }; + attributes: { + provider: string; + uid: string; + alias: string; + connection: { + connected: boolean; + last_checked_at: string; + }; + inserted_at: string; + updated_at: string; + }; + relationships: { + secret: { + data: { + type: "ProviderSecret"; + id: string; + }; + }; + }; + links: { + self: string; + }; }; }; links: { From 9a9a6410e127065d0ded1dec870c3f4a84b094f0 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 22 Oct 2024 11:07:00 +0200 Subject: [PATCH 8/8] feat: render findings first iteration --- .eslintrc.cjs | 1 + app/(prowler)/findings/page.tsx | 55 +++------ components/findings/table/column-findings.tsx | 115 +++++++----------- .../findings/table/data-table-row-actions.tsx | 29 ++--- components/ui/table/status-badge.tsx | 2 +- lib/{custom.ts => helper.ts} | 12 ++ lib/index.ts | 2 +- types/components.ts | 22 +++- 8 files changed, 107 insertions(+), 131 deletions(-) rename lib/{custom.ts => helper.ts} (76%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 04f90c81d7..6f1426da12 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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", { diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx index 0e7dd6c9b6..f75638f792 100644 --- a/app/(prowler)/findings/page.tsx +++ b/app/(prowler)/findings/page.tsx @@ -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({
- + }> @@ -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 ( diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx index 98803d2a7c..3391207093 100644 --- a/components/findings/table/column-findings.tsx +++ b/components/findings/table/column-findings.tsx @@ -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[] = [ - // { - // header: " ", - // cell: ({ row }) =>

{row.index + 1}

, - // }, { accessorKey: "check", - header: ({ column }) => ( - - ), + header: "Check", cell: ({ row }) => { const { checktitle } = getFindingsMetadata(row); return

{checktitle}

; }, }, { - accessorKey: "region", - header: "Region", + accessorKey: "scanName", + header: "Scan Name", cell: ({ row }) => { - const region = getResourceData(row, "region"); + const name = getScanData(row, "name"); return ( - <> -
{region}
- +

+ {typeof name === "string" || typeof name === "number" + ? name + : "Invalid data"} +

); }, }, - - // { - // accessorKey: "uid", - // header: ({ column }) => ( - // - // ), - // cell: ({ row }) => { - // const { - // attributes: { uid }, - // } = getFindingsData(row); - // return ; - // }, - // }, - // { - // accessorKey: "provider", - // header: "Provider Alias", - // cell: ({ row }) => { - // const { - // attributes: { alias }, - // } = getProviderData(row, "alias"); - // return

{alias}

; - // }, - // }, { accessorKey: "severity", - header: ({ column }) => ( - - ), + header: "Severity", cell: ({ row }) => { const { attributes: { severity }, @@ -128,38 +99,44 @@ export const ColumnFindings: ColumnDef[] = [ const { attributes: { status }, } = getFindingsData(row); - // Temporarily overwriting the value until the API is functional. - return ; + + const mappedStatus = statusMap[status]; + + return ; + }, + }, + { + accessorKey: "region", + header: "Region", + cell: ({ row }) => { + const region = getResourceData(row, "region"); + + return ( + <> +
{typeof region === "string" ? region : "Invalid region"}
+ + ); }, }, { accessorKey: "service", - header: ({ column }) => ( - - ), + header: "Service", cell: ({ row }) => { const { servicename } = getFindingsMetadata(row); return

{servicename}

; }, }, { - accessorKey: "added", - header: ({ column }) => ( - - ), + accessorKey: "account", + header: "Account", cell: ({ row }) => { - const { - attributes: { inserted_at }, - } = getFindingsData(row); - return ; + const account = getProviderData(row, "uid"); + + return ( + <> +
{typeof account === "string" ? account : "Invalid account"}
+ + ); }, }, { diff --git a/components/findings/table/data-table-row-actions.tsx b/components/findings/table/data-table-row-actions.tsx index dea023d359..9513bb11a8 100644 --- a/components/findings/table/data-table-row-actions.tsx +++ b/components/findings/table/data-table-row-actions.tsx @@ -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({ > } // onClick={() => setIsEditOpen(true)} > - Edit Finding + Send to Jira - - - } - // onClick={() => setIsDeleteOpen(true)} + key="slack" + description="Allows you to send the finding to Slack" + textValue="Send to Slack" + startContent={} + // onClick={() => setIsEditOpen(true)} > - Delete Finding + Send to Slack diff --git a/components/ui/table/status-badge.tsx b/components/ui/table/status-badge.tsx index c0f0356407..932d4105d6 100644 --- a/components/ui/table/status-badge.tsx +++ b/components/ui/table/status-badge.tsx @@ -1,7 +1,7 @@ import { Chip } from "@nextui-org/react"; import React from "react"; -type Status = +export type Status = | "available" | "scheduled" | "executing" diff --git a/lib/custom.ts b/lib/helper.ts similarity index 76% rename from lib/custom.ts rename to lib/helper.ts index a33f07d6fe..7eeee17a6f 100644 --- a/lib/custom.ts +++ b/lib/helper.ts @@ -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); diff --git a/lib/index.ts b/lib/index.ts index 38d384fb6e..56ebefcc21 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,2 +1,2 @@ -export * from "./custom"; +export * from "./helper"; export * from "./utils"; diff --git a/types/components.ts b/types/components.ts index 89dee77381..086e6e4a60 100644 --- a/types/components.ts +++ b/types/components.ts @@ -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;