From 43c76ca85c8bb0f3037cd57e8c3ce9344bfcca1e Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 17 Jan 2025 10:19:10 +0100 Subject: [PATCH] feat(findings): add first seen in findings details (#6575) --- .../findings/table/finding-detail.tsx | 418 +++++++++++------- ui/components/ui/entities/index.ts | 1 + ui/components/ui/entities/info-field.tsx | 35 ++ ui/types/components.ts | 4 + 4 files changed, 292 insertions(+), 166 deletions(-) create mode 100644 ui/components/ui/entities/info-field.tsx diff --git a/ui/components/findings/table/finding-detail.tsx b/ui/components/findings/table/finding-detail.tsx index d2febae564..71f2de2eb1 100644 --- a/ui/components/findings/table/finding-detail.tsx +++ b/ui/components/findings/table/finding-detail.tsx @@ -3,11 +3,49 @@ import { Snippet } from "@nextui-org/react"; import Link from "next/link"; -import { SnippetId } from "@/components/ui/entities"; +import { InfoField } from "@/components/ui/entities"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; +import { + getProviderLogo, + type ProviderType, +} from "@/components/ui/entities/get-provider-logo"; import { SeverityBadge } from "@/components/ui/table/severity-badge"; import { FindingProps } from "@/types"; +const renderValue = (value: string | null | undefined) => { + return value && value.trim() !== "" ? value : "-"; +}; + +const Section = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( +
+

+ {title} +

+ {children} +
+); + +// Add new utility function for duration formatting +const formatDuration = (seconds: number) => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const remainingSeconds = seconds % 60; + + const parts = []; + if (hours > 0) parts.push(`${hours}h`); + if (minutes > 0) parts.push(`${minutes}m`); + if (remainingSeconds > 0 || parts.length === 0) + parts.push(`${remainingSeconds}s`); + + return parts.join(" "); +}; + export const FindingDetail = ({ findingDetails, }: { @@ -16,21 +54,19 @@ export const FindingDetail = ({ const finding = findingDetails; const attributes = finding.attributes; const resource = finding.relationships.resource.attributes; - - const remediation = attributes.check_metadata.remediation; + const scan = finding.relationships.scan.attributes; + const provider = finding.relationships.provider.attributes; return (
{/* Header */}
-

- {attributes.check_metadata.checktitle} +

+ {renderValue(attributes.check_metadata.checktitle)}

-

- {resource.service} -

+
- {attributes.status} + {renderValue(attributes.status)}
{/* Check Metadata */} -
-
-

- Finding details -

- +
+
+ +
+ {getProviderLogo( + attributes.check_metadata.provider as ProviderType, + )} +
+
+ + {attributes.check_metadata.servicename} + + {resource.region} + + +
+ +
+ + + {attributes.check_id} + + + + + +
+ {attributes.status === "FAIL" && ( - -

- Risk -

-

- {attributes.check_metadata.risk} -

-
+ + +

+ {attributes.check_metadata.risk} +

+
+
)} -
-

- Description -

-

- {attributes.check_metadata.description} -

-
+ + {renderValue(attributes.check_metadata.description)} + -
-

- Remediation -

-
- {remediation.recommendation && ( - <> -

Recommendation:

-

{remediation.recommendation.text}

- - Learn more - - - )} - {remediation.code && - Object.values(remediation.code).some(Boolean) && ( + {attributes.check_metadata.remediation && ( +
+

+ Remediation Details +

+ + {/* Recommendation section */} + {attributes.check_metadata.remediation.recommendation.text && ( +
-

- Reference Information: +

+ {attributes.check_metadata.remediation.recommendation.text}

-
- {remediation.code.cli && ( -
-

CLI Command:

- -

- {remediation.code.cli} -

-
-
- )} -
- {Object.entries(remediation.code) - .filter(([key]) => key !== "cli") - .map(([key, value]) => - value ? ( - - {key === "other" - ? "External doc" - : key.charAt(0).toUpperCase() + - key.slice(1).toLowerCase()} - - ) : null, - )} -
-
+ {attributes.check_metadata.remediation.recommendation.url && ( + + Learn more + + )}
- )} -
-
-
+ + )} - {/* Resources Section */} -
-

- Resource Details -

-
-
-

- Resource ID -

- -

{resource.uid}

-
-
-
-

- Resource Name -

-

- {resource.name} -

-
-
-

- Region -

-

- {resource.region} -

-
-
-

- Resource Type -

-

- {resource.type} -

-
-
-

- Severity -

- -
- {resource.tags && - Object.entries(resource.tags).map(([key, value]) => ( -
-

- Tag: {key} -

- + + + {attributes.check_metadata.remediation.code.cli} + + + + )} + + {/* Additional Resources section */} + {attributes.check_metadata.remediation.code.other && ( + + -

{value}

-
-
- ))} -
-
-

- First seen -

- -
-
-

- Last seen -

- -
+ View documentation + + + )}
+ )} + + + {attributes.check_metadata.categories?.join(", ") || "-"} + +
+ + {/* Resource Details */} +
+ + + + {renderValue(resource.uid)} + + + + +
+ + {renderValue(resource.name)} + + + {renderValue(resource.type)} +
-
+ +
+ {renderValue(resource.service)} + {renderValue(resource.region)} +
+ + {resource.tags && Object.entries(resource.tags).length > 0 && ( +
+

+ Tags +

+ {Object.entries(resource.tags).map(([key, value]) => ( + + {renderValue(value)} + + ))} +
+ )} + +
+ + + + + + +
+ + + {/* Add new Scan Details section */} +
+
+ {scan.name} + + {scan.unique_resource_count} + + {scan.progress}% +
+ +
+ {scan.trigger} + {scan.state} + + {formatDuration(scan.duration)} + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + {scan.scheduled_at && ( + + + + )} +
+ + {/* Provider Details section */} +
+
+ + {getProviderLogo( + attributes.check_metadata.provider as ProviderType, + )} + + {provider.uid} +
+ +
+ {provider.alias} + + + {provider.connection.connected ? "Connected" : "Disconnected"} + + +
+
); }; diff --git a/ui/components/ui/entities/index.ts b/ui/components/ui/entities/index.ts index f1ee0adffc..cb329a03e3 100644 --- a/ui/components/ui/entities/index.ts +++ b/ui/components/ui/entities/index.ts @@ -1,6 +1,7 @@ export * from "./date-with-time"; export * from "./entity-info-short"; export * from "./get-provider-logo"; +export * from "./info-field"; export * from "./scan-status"; export * from "./snippet-id"; export * from "./snippet-label"; diff --git a/ui/components/ui/entities/info-field.tsx b/ui/components/ui/entities/info-field.tsx new file mode 100644 index 0000000000..cd4af6182e --- /dev/null +++ b/ui/components/ui/entities/info-field.tsx @@ -0,0 +1,35 @@ +interface InfoFieldProps { + label: string; + children: React.ReactNode; + variant?: "default" | "simple"; +} + +export const InfoField = ({ + label, + children, + variant = "default", +}: InfoFieldProps) => { + if (variant === "simple") { + return ( +
+ + {label} + +
+ {children} +
+
+ ); + } + + return ( +
+ + {label} + +
+ {children} +
+
+ ); +}; diff --git a/ui/types/components.ts b/ui/types/components.ts index 14677fe284..da95d2dd5d 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -514,6 +514,7 @@ export interface ScanProps { } | null; duration: number; started_at: string; + inserted_at: string; completed_at: string; scheduled_at: string; next_scan_at: string; @@ -584,6 +585,7 @@ export interface FindingProps { raw_result: object | null; inserted_at: string; updated_at: string; + first_seen_at: string | null; }; relationships: { resources: { @@ -608,8 +610,10 @@ export interface FindingProps { }; duration: number; started_at: string; + inserted_at: string; completed_at: string; scheduled_at: string | null; + next_scan_at: string; }; }; resource: {