From afc4189577a9d823d7dd6abc65da9f965092e90e Mon Sep 17 00:00:00 2001 From: Sophia Dao Date: Wed, 14 Aug 2024 08:06:27 -0500 Subject: [PATCH] Findings Page - Findings Card Components (#37) --- app/(prowler)/findings/page.tsx | 44 +++--- app/(prowler)/layout.tsx | 3 +- 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 | 6 +- .../findings/table/DataTableFindings.tsx | 12 +- components/findings/table/FindingsCard.tsx | 134 ------------------ .../providers/table/ColumnsProvider.tsx | 2 +- .../providers/table/DataTableProvider.tsx | 2 +- components/ui/table/SeverityBadge.tsx | 55 +++---- components/ui/table/StatusBadge.tsx | 31 ++-- types/components.ts | 2 +- 15 files changed, 311 insertions(+), 209 deletions(-) create mode 100644 components/findings/FindingsCard.tsx create mode 100644 components/findings/FindingsCardContent.tsx create mode 100644 components/findings/FindingsCardDetail.tsx create mode 100644 components/findings/FindingsCardScan.tsx create mode 100644 components/findings/FindingsCardType.tsx delete mode 100644 components/findings/table/FindingsCard.tsx diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx index c551a3cb9f..5ef11d41ed 100644 --- a/app/(prowler)/findings/page.tsx +++ b/app/(prowler)/findings/page.tsx @@ -49,8 +49,8 @@ const SSRDataTable = async () => { checkId: "cloudformation_stack_outputs_find_secrets", checkLink: "https://app.prowler.pro/app/findings?date=2024-08-05&search=cloudformation_stack_outputs_find_secrets", - type: "Not applicable", - scanTime: "2024-08-05 @ 14:22:00 UTC", + type: ["Not applicable"], + scanTime: "2024-07-17T09:55:14.191475Z", findingId: "ba123291-03a5-49a1-b962-6fdb1d2b9c9b", findingLink: "https://app.prowler.pro/app/findings?date=2024-08-05&search=ba123291-03a5-49a1-b962-6fdb1d2b9c9b", @@ -87,8 +87,12 @@ const SSRDataTable = async () => { 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-08-05 @ 14:22:00 UTC", + 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", @@ -123,8 +127,8 @@ const SSRDataTable = async () => { 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-08-06 @ 10:45:00 UTC", + 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", @@ -162,8 +166,8 @@ const SSRDataTable = async () => { 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-08-06 @ 11:10:00 UTC", + 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", @@ -201,8 +205,8 @@ const SSRDataTable = async () => { 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-08-06 @ 13:15:00 UTC", + 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", @@ -240,8 +244,8 @@ const SSRDataTable = async () => { checkId: "ebs_volume_encryption", checkLink: "https://app.prowler.pro/app/findings?date=2024-08-07&search=ebs_volume_encryption", - type: "Encryption", - scanTime: "2024-08-07 @ 09:30:00 UTC", + 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", @@ -278,8 +282,8 @@ const SSRDataTable = async () => { 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-08-07 @ 11:00:00 UTC", + 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", @@ -317,8 +321,8 @@ const SSRDataTable = async () => { 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-08-07 @ 12:45:00 UTC", + 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", @@ -357,8 +361,8 @@ const SSRDataTable = async () => { 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-08-08 @ 09:50:00 UTC", + 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", @@ -396,8 +400,8 @@ const SSRDataTable = async () => { 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-08-08 @ 10:15:00 UTC", + 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", diff --git a/app/(prowler)/layout.tsx b/app/(prowler)/layout.tsx index ec533b5892..0faab3f593 100644 --- a/app/(prowler)/layout.tsx +++ b/app/(prowler)/layout.tsx @@ -3,8 +3,7 @@ import "@/styles/globals.css"; import { Metadata, Viewport } from "next"; import React from "react"; -import { Toaster } from "@/components/ui"; -import { SidebarWrap } from "@/components/ui/sidebar"; +import { SidebarWrap, Toaster } from "@/components/ui"; import { fontSans } from "@/config/fonts"; import { siteConfig } from "@/config/site"; import { cn } from "@/lib/utils"; diff --git a/components/findings/FindingsCard.tsx b/components/findings/FindingsCard.tsx new file mode 100644 index 0000000000..cec81ffb54 --- /dev/null +++ b/components/findings/FindingsCard.tsx @@ -0,0 +1,93 @@ +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 new file mode 100644 index 0000000000..f0f0f7becf --- /dev/null +++ b/components/findings/FindingsCardContent.tsx @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000000..5b3ffc59bd --- /dev/null +++ b/components/findings/FindingsCardDetail.tsx @@ -0,0 +1,59 @@ +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 new file mode 100644 index 0000000000..b4dd7bd656 --- /dev/null +++ b/components/findings/FindingsCardScan.tsx @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000000..e98fca6f1f --- /dev/null +++ b/components/findings/FindingsCardType.tsx @@ -0,0 +1,25 @@ +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 20c1920f4e..e7c1764519 100644 --- a/components/findings/index.ts +++ b/components/findings/index.ts @@ -1,4 +1,8 @@ +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/FindingsCard"; export * from "./table/SkeletonTableFindings"; diff --git a/components/findings/table/DataTableFindings.tsx b/components/findings/table/DataTableFindings.tsx index 5004dc2e6d..1c1fa7da9c 100644 --- a/components/findings/table/DataTableFindings.tsx +++ b/components/findings/table/DataTableFindings.tsx @@ -9,8 +9,9 @@ import { Row, useReactTable, } from "@tanstack/react-table"; +import clsx from "clsx"; -import { FindingsCard } from "@/components/findings/table/FindingsCard"; +import { FindingsCard } from "@/components/findings"; import { Table, TableBody, @@ -18,7 +19,7 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table/Table"; +} from "@/components/ui"; import { FindingProps } from "@/types"; interface DataTableFindingsProps { @@ -48,7 +49,12 @@ export function DataTableFindings({ return ( <>
-
+
diff --git a/components/findings/table/FindingsCard.tsx b/components/findings/table/FindingsCard.tsx deleted file mode 100644 index 22056b29eb..0000000000 --- a/components/findings/table/FindingsCard.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { Button, Divider, Link } from "@nextui-org/react"; -import React from "react"; - -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}

- -
-

Resource ID:

- - {resourceId} - -

Resource ARN:

-

{resourceARN}

-

Check ID:

- - {checkId} - -

Type:

-

{type}

-

Scan Time:

-

{scanTime}

-

Prowler Finding ID:

- - {findingId} - -
- - {details && ( -
-

Details:

-

{details}

-
- )} - - {riskDetails && ( -
-

- Risk: - {riskLink && ( - - )} -

-

{riskDetails}

-
- )} - - {recommendationDetails && ( -
-
- Recommendation: - {recommendationLink && ( - - )} -
-

{recommendationDetails}

-
- )} - - {referenceInformation && ( -
-
- Reference Information: - {referenceLink && ( - - )} -
-

{referenceInformation}

-
- )} -
- - ); -}; diff --git a/components/providers/table/ColumnsProvider.tsx b/components/providers/table/ColumnsProvider.tsx index 663c076477..82fe4992e2 100644 --- a/components/providers/table/ColumnsProvider.tsx +++ b/components/providers/table/ColumnsProvider.tsx @@ -11,7 +11,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { add } from "date-fns"; import { VerticalDotsIcon } from "@/components/icons"; -import { StatusBadge } from "@/components/ui/table/StatusBadge"; +import { StatusBadge } from "@/components/ui"; import { ProviderProps } from "@/types"; import { CheckConnectionProvider } from "../CheckConnectionProvider"; diff --git a/components/providers/table/DataTableProvider.tsx b/components/providers/table/DataTableProvider.tsx index 172af2d891..2a037faf4f 100644 --- a/components/providers/table/DataTableProvider.tsx +++ b/components/providers/table/DataTableProvider.tsx @@ -18,7 +18,7 @@ import { TableHead, TableHeader, TableRow, -} from "@/components/ui/table/Table"; +} from "@/components/ui"; import { MetaDataProps } from "@/types"; import { DataTablePagination } from "./DataTablePagination"; diff --git a/components/ui/table/SeverityBadge.tsx b/components/ui/table/SeverityBadge.tsx index 7d9760c22e..2ce9d9e65c 100644 --- a/components/ui/table/SeverityBadge.tsx +++ b/components/ui/table/SeverityBadge.tsx @@ -1,46 +1,47 @@ import { Chip } from "@nextui-org/react"; +import clsx from "clsx"; import React from "react"; import { AlertIcon } from "@/components/icons"; type Severity = "critical" | "high" | "medium" | "low"; -const severityColorMap: Record< - Severity, - | "text-white bg-red-800" - | "text-white bg-red-600" - | "text-white bg-orange-500" - | "bg-yellow-200" -> = { - critical: "text-white bg-red-800", - high: "text-white bg-red-600", - medium: "text-white bg-orange-500", - low: "bg-yellow-200", -}; - -const severityIconMap: Partial> = { +const severityIconMap = { critical: , +} as const; + +const getSeverityColor = ( + severity: Severity, +): "danger" | "warning" | "default" => { + switch (severity) { + case "critical": + return "danger"; + case "high": + return "danger"; + case "medium": + return "warning"; + case "low": + return "default"; + default: + return "default"; // this is a fallback, though unnecessary due to typing + } }; -const getSeverityColor: (severity: Severity) => string = (severity) => - // eslint-disable-next-line security/detect-object-injection - severityColorMap[severity]; - -const getSeverityIcon: (severity: Severity) => React.ReactNode | null = ( - severity, -) => - // eslint-disable-next-line security/detect-object-injection - severityIconMap[severity] || null; +const getSeverityIcon = (severity: Severity): React.ReactNode | null => { + return severity === "critical" ? severityIconMap.critical : null; +}; export const SeverityBadge = ({ severity }: { severity: Severity }) => { + const color = getSeverityColor(severity); + return ( {severity} diff --git a/components/ui/table/StatusBadge.tsx b/components/ui/table/StatusBadge.tsx index d696e89ccf..c1f667f26e 100644 --- a/components/ui/table/StatusBadge.tsx +++ b/components/ui/table/StatusBadge.tsx @@ -11,32 +11,25 @@ type Status = const statusColorMap: Record< Status, - | "text-white bg-green-600" - | "bg-yellow-200" - | "text-white bg-red-600" - | "bg-gray-300" + "danger" | "warning" | "success" | "default" > = { - completed: "text-white bg-green-600", - pending: "bg-yellow-200", - cancelled: "text-white bg-red-600", - fail: "text-white bg-red-600", - success: "text-white bg-green-600", - muted: "bg-gray-300", + completed: "success", + pending: "warning", + cancelled: "danger", + fail: "danger", + success: "success", + muted: "default", }; -const getStatusColor: (status: Status) => string = (status) => - // eslint-disable-next-line security/detect-object-injection - statusColorMap[status]; - export const StatusBadge = ({ status }: { status: Status }) => { + const color = statusColorMap[status as keyof typeof statusColorMap]; + return ( {status} diff --git a/types/components.ts b/types/components.ts index 3d8a49393d..5580d151eb 100644 --- a/types/components.ts +++ b/types/components.ts @@ -58,7 +58,7 @@ export interface FindingProps { resourceARN: string; checkId: string; checkLink: string; - type: string; + type: string[]; scanTime: string; findingId: string; findingLink: string;