From c32be5e8f15da056ebd78e4b82c14b99960b5ed9 Mon Sep 17 00:00:00 2001 From: Carlos Feria Date: Sun, 27 Oct 2024 08:08:36 +0100 Subject: [PATCH] SBOM Details Page V1 parity (#201) --- client/package.json | 2 +- client/src/app/api/model-utils.ts | 12 +- .../app/components/SeverityShieldAndText.tsx | 4 +- .../SimplePagination/SimplePagination.tsx | 1 - .../src/app/pages/sbom-details/overview.tsx | 62 +- .../pages/sbom-details/packages-by-sbom.tsx | 338 ++++----- .../app/pages/sbom-details/sbom-details.tsx | 211 ++++-- .../sbom-details/vulnerabilities-by-sbom.tsx | 686 +++++++++--------- package-lock.json | 8 +- 9 files changed, 646 insertions(+), 678 deletions(-) diff --git a/client/package.json b/client/package.json index c717fbf4..8248e056 100644 --- a/client/package.json +++ b/client/package.json @@ -38,7 +38,7 @@ "file-saver": "^2.0.5", "monaco-editor": "0.34.1", "oidc-client-ts": "^2.4.0", - "packageurl-js": "^1.2.1", + "packageurl-js": "^2.0.1", "pretty-bytes": "^6.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/client/src/app/api/model-utils.ts b/client/src/app/api/model-utils.ts index 8f527c28..53ab71ff 100644 --- a/client/src/app/api/model-utils.ts +++ b/client/src/app/api/model-utils.ts @@ -12,7 +12,7 @@ import { Severity } from "@app/client"; type ListType = { [key in Severity]: { name: string; - shieldIconColor: { name: string; value: string; var: string }; + color: { name: string; value: string; var: string }; progressProps: Pick; }; }; @@ -20,27 +20,27 @@ type ListType = { export const severityList: ListType = { none: { name: "None", - shieldIconColor: noneColor, + color: noneColor, progressProps: { variant: undefined }, }, low: { name: "Low", - shieldIconColor: lowColor, + color: lowColor, progressProps: { variant: undefined }, }, medium: { name: "Medium", - shieldIconColor: moderateColor, + color: moderateColor, progressProps: { variant: "warning" }, }, high: { name: "High", - shieldIconColor: importantColor, + color: importantColor, progressProps: { variant: "danger" }, }, critical: { name: "Critical", - shieldIconColor: criticalColor, + color: criticalColor, progressProps: { variant: "danger" }, }, }; diff --git a/client/src/app/components/SeverityShieldAndText.tsx b/client/src/app/components/SeverityShieldAndText.tsx index a6e13214..b5146c0c 100644 --- a/client/src/app/components/SeverityShieldAndText.tsx +++ b/client/src/app/components/SeverityShieldAndText.tsx @@ -28,10 +28,10 @@ export const SeverityShieldAndText: React.FC = ({ {hideLabel ? ( - + ) : ( - + )} {!hideLabel && {label}} diff --git a/client/src/app/components/SimplePagination/SimplePagination.tsx b/client/src/app/components/SimplePagination/SimplePagination.tsx index e61bd0d6..d0f65d24 100644 --- a/client/src/app/components/SimplePagination/SimplePagination.tsx +++ b/client/src/app/components/SimplePagination/SimplePagination.tsx @@ -33,7 +33,6 @@ export const SimplePagination: React.FC = ({ isTop ? "top" : "bottom" }`} variant={isTop ? PaginationVariant.top : PaginationVariant.bottom} - className={isTop || noMargin ? "" : spacing.mtMd} isCompact={isCompact} {...paginationProps} widgetId="pagination-id" diff --git a/client/src/app/pages/sbom-details/overview.tsx b/client/src/app/pages/sbom-details/overview.tsx index d6fe29fe..264ffe7d 100644 --- a/client/src/app/pages/sbom-details/overview.tsx +++ b/client/src/app/pages/sbom-details/overview.tsx @@ -2,6 +2,8 @@ import React from "react"; import { ChartDonut } from "@patternfly/react-charts"; import { + Card, + CardBody, DescriptionList, DescriptionListDescription, DescriptionListGroup, @@ -20,32 +22,38 @@ interface InfoProps { export const Overview: React.FC = ({ sbom }) => { return ( - - - - - Name - {sbom.name} - - - Author - - {sbom.authors} - - - - Published - - {formatDate(sbom.published)} - - - - - + + + + + + + Name + + {sbom.name} + + + + Author + + {sbom.authors} + + + + Published + + {formatDate(sbom.published)} + + + + + + + ); }; @@ -70,7 +78,7 @@ export const CVEsChart: React.FC = ({ data }) => { const result: ChartData = { severity: severity as Severity, legend: severityProps.name, - color: severityProps.shieldIconColor.value, + color: severityProps.color.value, count: count, }; diff --git a/client/src/app/pages/sbom-details/packages-by-sbom.tsx b/client/src/app/pages/sbom-details/packages-by-sbom.tsx index b4e93bb9..8c8fa68e 100644 --- a/client/src/app/pages/sbom-details/packages-by-sbom.tsx +++ b/client/src/app/pages/sbom-details/packages-by-sbom.tsx @@ -1,11 +1,24 @@ -import React from "react"; -import { NavLink } from "react-router-dom"; +import React, { useMemo } from "react"; +import { Link } from "react-router-dom"; -import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Flex, + FlexItem, + Label, + List, + ListItem, + Toolbar, + ToolbarContent, + ToolbarItem, +} from "@patternfly/react-core"; +import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { ExpandableRowContent, Table, - TableProps, Tbody, Td, Th, @@ -13,7 +26,8 @@ import { Tr, } from "@patternfly/react-table"; -import { TablePersistenceKeyPrefixes } from "@app/Constants"; +import { DecomposedPurl } from "@app/api/models"; +import { PurlSummary } from "@app/client"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { PackageQualifiers } from "@app/components/PackageQualifiers"; import { SimplePagination } from "@app/components/SimplePagination"; @@ -22,42 +36,70 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; -import { - getHubRequestParams, - useLocalTableControls, - useTableControlProps, - useTableControlState, -} from "@app/hooks/table-controls"; -import { useSelectionState } from "@app/hooks/useSelectionState"; +import { useLocalTableControls } from "@app/hooks/table-controls"; import { useFetchPackagesBySbomId } from "@app/queries/packages"; import { decomposePurl } from "@app/utils/utils"; +interface TableData extends PurlSummary { + decomposedPurl?: DecomposedPurl; +} + interface PackagesProps { - variant?: TableProps["variant"]; sbomId: string; } -export const PackagesBySbom: React.FC = ({ - variant, - sbomId, -}) => { - const tableControlState = useTableControlState({ - tableName: "packages-table", - persistenceKeyPrefix: TablePersistenceKeyPrefixes.packages, +export const PackagesBySbom: React.FC = ({ sbomId }) => { + const { + result: { data: allPackages }, + isFetching, + fetchError, + } = useFetchPackagesBySbomId(sbomId, { + page: { pageNumber: 1, itemsPerPage: 0 }, + }); + + const tableData = useMemo(() => { + return allPackages + .flatMap((item) => item.purl) + .map((item) => { + const result: TableData = { + ...item, + decomposedPurl: decomposePurl(item.purl), + }; + return result; + }); + }, [allPackages]); + + const tableControls = useLocalTableControls({ + tableName: "package-table", + idProperty: "uuid", + items: tableData, + isLoading: isFetching, columnNames: { name: "Name", version: "Version", + qualifiers: "Qualifiers", }, - isPaginationEnabled: true, + hasActionsColumn: false, isSortEnabled: true, - sortableColumns: [], + sortableColumns: ["name", "version"], + getSortValues: (item) => ({ + name: `${item.decomposedPurl?.name}/${item.decomposedPurl?.namespace}`, + version: item.version.version, + }), + isPaginationEnabled: true, isFilterEnabled: true, filterCategories: [ { - categoryKey: "", + categoryKey: "filterText", title: "Filter tex", type: FilterType.search, - placeholderText: "Search...", + placeholderText: "Filter", + getItemValue: (item) => { + return ( + `${item.decomposedPurl?.name}/${item.decomposedPurl?.namespace}` || + "" + ); + }, }, ], isExpansionEnabled: true, @@ -65,31 +107,8 @@ export const PackagesBySbom: React.FC = ({ }); const { - result: { data: packages, total: totalItemCount }, - isFetching, - fetchError, - } = useFetchPackagesBySbomId( - sbomId, - getHubRequestParams({ - ...tableControlState, - }) - ); - - const tableControls = useTableControlProps({ - ...tableControlState, - idProperty: "id", - currentPageItems: packages, - totalItemCount, - isLoading: isFetching, - selectionState: useSelectionState({ - items: packages, - isEqual: (a, b) => a.name === b.name, - }), - }); - - const { - numRenderedColumns, currentPageItems, + numRenderedColumns, propHelpers: { toolbarProps, filterToolbarProps, @@ -99,6 +118,7 @@ export const PackagesBySbom: React.FC = ({ getThProps, getTrProps, getTdProps, + getExpandedContentTdProps, }, expansionDerivedState: { isCellExpanded }, } = tableControls; @@ -124,40 +144,94 @@ export const PackagesBySbom: React.FC = ({ + {currentPageItems?.map((item, rowIndex) => { return ( - + - - {item.name} + + + {`${item.decomposedPurl?.name}/${item.decomposedPurl?.namespace}`} + + + + + + + {item.decomposedPurl?.version} - - {item.version} + + {item.decomposedPurl?.qualifiers && ( + + )} {isCellExpanded(item) ? ( - -
- - - -
+ + + + + + Packages + + + + + {item.purl} + + + + + + + + Base package + + + {item.base.purl} + + + + Versions + + + {item.version.version} + + + + + ) : null} @@ -175,147 +249,3 @@ export const PackagesBySbom: React.FC = ({ ); }; - -interface PackageExpandedAreaProps { - purls: { - uuid: string; - purl: string; - }[]; -} - -export const PackageExpandedArea: React.FC = ({ - purls, -}) => { - const packages = React.useMemo(() => { - return purls.map((purl) => { - return { - uuid: purl.uuid, - purl: purl.purl, - ...decomposePurl(purl.purl), - }; - }); - }, [purls]); - - const tableControls = useLocalTableControls({ - variant: "compact", - tableName: "purl-table", - idProperty: "purl", - items: packages, - columnNames: { - name: "Name", - namespace: "Namespace", - version: "Version", - type: "Type", - path: "Path", - qualifiers: "qualifiers", - }, - isPaginationEnabled: false, - isSortEnabled: true, - sortableColumns: [], - isFilterEnabled: true, - filterCategories: [ - { - categoryKey: "", - title: "Filter tex", - type: FilterType.search, - placeholderText: "Search...", - getItemValue: (item) => { - return item.purl; - }, - }, - ], - isExpansionEnabled: false, - }); - - const { - currentPageItems, - numRenderedColumns, - propHelpers: { tableProps, getThProps, getTrProps, getTdProps }, - } = tableControls; - - return ( - <> - - - - - - - - {currentPageItems?.map((item, rowIndex) => { - return ( - - - - - - - - - - - - - ); - })} - -
- - - - - - -
- - {item.name} - - - {item.namespace} - - {item.version} - - {item.type} - - {item.path} - - {item.qualifiers && ( - - )} -
- - ); -}; diff --git a/client/src/app/pages/sbom-details/sbom-details.tsx b/client/src/app/pages/sbom-details/sbom-details.tsx index ad9bc013..3212d6a4 100644 --- a/client/src/app/pages/sbom-details/sbom-details.tsx +++ b/client/src/app/pages/sbom-details/sbom-details.tsx @@ -1,14 +1,24 @@ import React from "react"; -import { Link } from "react-router-dom"; import { - Breadcrumb, - BreadcrumbItem, + Button, + Flex, + FlexItem, + Label, PageSection, + Popover, + Split, + SplitItem, + Tab, + TabAction, + TabContent, + Tabs, + TabTitleText, + Text, + TextContent, } from "@patternfly/react-core"; - -import DetailsPage from "@patternfly/react-component-groups/dist/dynamic/DetailsPage"; import DownloadIcon from "@patternfly/react-icons/dist/esm/icons/download-icon"; +import HelpIcon from "@patternfly/react-icons/dist/esm/icons/help-icon"; import { PathParam, useRouteParams } from "@app/Routes"; @@ -21,8 +31,24 @@ import { PackagesBySbom } from "./packages-by-sbom"; import { VulnerabilitiesBySbom } from "./vulnerabilities-by-sbom"; export const SbomDetails: React.FC = () => { - const sbomId = useRouteParams(PathParam.SBOM_ID); + const [activeTabKey, setActiveTabKey] = React.useState(0); + + const handleTabClick = ( + event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + tabIndex: string | number + ) => { + setActiveTabKey(tabIndex); + }; + + const infoTabRef = React.createRef(); + const packagesTabRef = React.createRef(); + const vulnerabilitiesTabRef = React.createRef(); + + const vulnerabilitiesTabPopoverRef = React.createRef(); + // + + const sbomId = useRouteParams(PathParam.SBOM_ID); const { sbom, isFetching, fetchError } = useFetchSBOMById(sbomId); const { downloadSBOM } = useDownload(); @@ -30,71 +56,114 @@ export const SbomDetails: React.FC = () => { return ( <> - - - SBOMs - - SBOM details - - } - pageHeading={{ - title: sbom?.name ?? sbomId ?? "", - }} - actionButtons={[ - { - children: ( - <> - Download - - ), - onClick: () => { - if (sbomId) { - downloadSBOM( - sbomId, - sbom?.name ? `${sbom?.name}.json` : sbomId - ); - } - }, - variant: "secondary", - }, - ]} - tabs={[ - { - eventKey: "overview", - title: "Overview", - children: ( -
- - {sbom && } - -
- ), - }, - { - eventKey: "vulnerabilities", - title: "Vulnerabilities", - children: ( -
- {sbomId && } -
- ), - }, - { - eventKey: "packages", - title: "Packages", - children: ( -
- {sbomId && } -
- ), - }, - ]} - /> + + + + + + {sbom?.name ?? sbomId ?? ""} + + + + {sbom?.labels.type && ( + + )} + + + + + {!isFetching && ( + + )} + + +
+ + + Info} + tabContentId="refTabInfoSection" + tabContentRef={infoTabRef} + /> + Packages} + tabContentId="refTabPackagesSection" + tabContentRef={packagesTabRef} + /> + Vulnerabilities} + tabContentId="refVulnerabilitiesSection" + tabContentRef={vulnerabilitiesTabRef} + actions={ + <> + + + + + Any found vulnerabilities related to this SBOM. Fixed + vulnerabilities are not listed. + + } + /> + + } + /> + + + + + + {sbom && } + + + + ); diff --git a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx index 4f120a66..ad971615 100644 --- a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx +++ b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx @@ -1,18 +1,23 @@ import React from "react"; +import { Link } from "react-router-dom"; +import { ChartDonut } from "@patternfly/react-charts"; import { - Button, - ButtonVariant, - Label, - TextContent, - Title, + Card, + CardBody, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, + Stack, + StackItem, Toolbar, ToolbarContent, ToolbarItem, } from "@patternfly/react-core"; -import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; import { - Caption, ExpandableRowContent, Table, Tbody, @@ -22,17 +27,18 @@ import { Tr, } from "@patternfly/react-table"; +import { compareBySeverityFn, severityList } from "@app/api/model-utils"; import { VulnerabilityStatus } from "@app/api/models"; import { client } from "@app/axios-config/apiInit"; import { getVulnerability, SbomAdvisory, SbomPackage, + Severity, VulnerabilityDetails, } from "@app/client"; -import { AdvisoryInDrawerInfo } from "@app/components/AdvisoryInDrawerInfo"; -import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; -import { PageDrawerContent } from "@app/components/PageDrawerContext"; +import { LoadingWrapper } from "@app/components/LoadingWrapper"; +import { PackageQualifiers } from "@app/components/PackageQualifiers"; import { SeverityShieldAndText } from "@app/components/SeverityShieldAndText"; import { SimplePagination } from "@app/components/SimplePagination"; import { @@ -40,16 +46,25 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; -import { VulnerabilityInDrawerInfo } from "@app/components/VulnerabilityInDrawerInfo"; import { useLocalTableControls } from "@app/hooks/table-controls"; -import { useFetchSbomsAdvisory } from "@app/queries/sboms"; +import { useFetchSBOMById, useFetchSbomsAdvisory } from "@app/queries/sboms"; import { useWithUiId } from "@app/utils/query-utils"; +import { decomposePurl, formatDate } from "@app/utils/utils"; + +interface DonutChartData { + total: number; + summary: { [key in Severity]: number }; +} + +const DEFAULT_DONUT_CHART_DATA: DonutChartData = { + total: 0, + summary: { none: 0, low: 0, medium: 0, high: 0, critical: 0 }, +}; interface TableData { vulnerabilityId: string; advisory: SbomAdvisory; status: VulnerabilityStatus; - context: { cpe: string }; packages: SbomPackage[]; vulnerability?: VulnerabilityDetails; } @@ -61,18 +76,16 @@ interface VulnerabilitiesBySbomProps { export const VulnerabilitiesBySbom: React.FC = ({ sbomId, }) => { - type RowAction = "showVulnerability" | "showAdvisory"; - const [selectedRowAction, setSelectedRowAction] = - React.useState(null); - const [selectedRow, setSelectedRow] = React.useState(null); - - const showDrawer = (action: RowAction, row: TableData) => { - setSelectedRowAction(action); - setSelectedRow(row); - }; - - // - const { advisories, isFetching, fetchError } = useFetchSbomsAdvisory(sbomId); + const { + sbom, + isFetching: isFetchingSbom, + fetchError: fetchErrorSbom, + } = useFetchSBOMById(sbomId); + const { + advisories, + isFetching: isFetchingAdvisories, + fetchError: fetchErrorAdvisories, + } = useFetchSbomsAdvisory(sbomId); const [allVulnerabilities, setAllVulnerabilities] = React.useState< TableData[] @@ -83,10 +96,6 @@ export const VulnerabilitiesBySbom: React.FC = ({ const [isFetchingVulnerabilities, setIsFetchingVulnerabilities] = React.useState(false); - const [allAdvisoryStatus, setAllAdvisoryStatus] = React.useState< - Set - >(new Set()); - React.useEffect(() => { if (advisories.length === 0) { return; @@ -94,17 +103,19 @@ export const VulnerabilitiesBySbom: React.FC = ({ const vulnerabilities = (advisories ?? []) .flatMap((advisory) => { - return (advisory.status ?? []).map( - (status) => - ({ - vulnerabilityId: status.vulnerability_id, - status: status.status, - context: { ...status.context }, - packages: status.packages || [], - advisory: { ...advisory }, - }) as TableData - ); + return (advisory.status ?? []).map((status) => { + const result: TableData = { + vulnerabilityId: status.vulnerability_id, + status: status.status as VulnerabilityStatus, + packages: status.packages || [], + advisory: { ...advisory }, + }; + return result; + }); }) + // Take only "affected" + .filter((item) => item.status === "affected") + // Remove dupplicates if exists .reduce((prev, current) => { const exists = prev.find( (item) => @@ -118,24 +129,18 @@ export const VulnerabilitiesBySbom: React.FC = ({ } }, [] as TableData[]); - const allUniqueStatus = new Set(); - vulnerabilities.forEach((item) => allUniqueStatus.add(item.status)); - setAllVulnerabilities(vulnerabilities); - setAllAdvisoryStatus(allUniqueStatus); setIsFetchingVulnerabilities(true); Promise.all( vulnerabilities - .map( - async (item) => - ( - await getVulnerability({ - client, - path: { id: item.vulnerabilityId }, - }) - ).data - ) + .map(async (item) => { + const response = await getVulnerability({ + client, + path: { id: item.vulnerabilityId }, + }); + return response.data; + }) .map((vulnerability) => vulnerability.catch(() => null)) ).then((vulnerabilities) => { const validVulnerabilities = vulnerabilities.reduce((prev, current) => { @@ -148,9 +153,9 @@ export const VulnerabilitiesBySbom: React.FC = ({ }, [] as VulnerabilityDetails[]); const vulnerabilitiesById = new Map(); - validVulnerabilities.forEach((vulnerability) => - vulnerabilitiesById.set(vulnerability.identifier, vulnerability) - ); + validVulnerabilities.forEach((vulnerability) => { + vulnerabilitiesById.set(vulnerability.identifier, vulnerability); + }); setVulnerabilitiesById(vulnerabilitiesById); setIsFetchingVulnerabilities(false); @@ -176,42 +181,22 @@ export const VulnerabilitiesBySbom: React.FC = ({ tableName: "vulnerability-table", idProperty: "_ui_unique_id", items: tableDataWithUiId, - isLoading: false, + isLoading: isFetchingAdvisories || isFetchingVulnerabilities, columnNames: { - name: "Name", + id: "Id", description: "Description", cvss: "CVSS", - advisory: "Advisory", - context: "Context", - status: "Status", + affectedDependencies: "Affected dependencies", + published: "Published", + updated: "Updated", }, hasActionsColumn: false, isSortEnabled: true, - sortableColumns: ["name"], + sortableColumns: ["id"], isPaginationEnabled: true, - isFilterEnabled: true, - filterCategories: [ - { - categoryKey: "filterText", - title: "Filter tex", - type: FilterType.search, - placeholderText: "Search...", - getItemValue: (item) => item.vulnerabilityId, - }, - { - categoryKey: "status", - title: "Status", - placeholderText: "Status", - type: FilterType.multiselect, - selectOptions: Array.from(allAdvisoryStatus).map((item) => ({ - value: item, - label: item.charAt(0).toUpperCase() + item.slice(1).replace("_", " "), - })), - matcher: (filter: string, item: TableData) => item.status === filter, - }, - ], + isFilterEnabled: false, isExpansionEnabled: true, - expandableVariant: "single", + expandableVariant: "compound", }); const { @@ -226,296 +211,273 @@ export const VulnerabilitiesBySbom: React.FC = ({ getThProps, getTrProps, getTdProps, + getExpandedContentTdProps, }, expansionDerivedState: { isCellExpanded }, } = tableControls; - return ( - <> - - - - - - - - - - - - - - - - - {currentPageItems?.map((item, rowIndex) => { - return ( - - - - - - - - - - - - {isCellExpanded(item) ? ( - - - - ) : null} - - ); - })} - -
- - - - - - -
- - - {item.vulnerability?.title} - - {item.vulnerability?.average_severity && ( - - )} - - - - {item.context.cpe} - - -
-
- - - -
-
- - - setSelectedRowAction(null)} - pageKey="drawer" - drawerPanelContentProps={{ defaultSize: "600px" }} - header={ - <> - {selectedRowAction === "showVulnerability" && ( - - - Vulnerability - - - )} - {selectedRowAction === "showAdvisory" && ( - - - Advisory - - - )} - - } - > - {selectedRowAction === "showVulnerability" && ( - <> - {selectedRow?.vulnerabilityId && ( - - )} - - )} - {selectedRowAction === "showAdvisory" && ( - <> - {selectedRow?.advisory && ( - - )} - - )} - - - ); -}; - -interface VulnerabilitiesExpandedAreaProps { - packages: SbomPackage[]; -} + // -export const VulnerabilitiesExpandedArea: React.FC< - VulnerabilitiesExpandedAreaProps -> = ({ packages }) => { - const tableControls = useLocalTableControls({ - variant: "compact", - tableName: "package-table", - idProperty: "id", - items: packages ?? [], - columnNames: { - name: "Name", - version: "Version", - }, - isPaginationEnabled: true, - initialItemsPerPage: 5, - isSortEnabled: true, - sortableColumns: ["name"], - getSortValues: (item) => ({ - name: item.name, - }), - isFilterEnabled: true, - filterCategories: [ - { - categoryKey: "name", - title: "Name", - type: FilterType.search, - placeholderText: "Search by name...", - getItemValue: (item) => item.name || "", - }, - ], - isExpansionEnabled: false, - }); + const donutChartData = React.useMemo(() => { + return tableData.reduce((prev, current) => { + if (current.vulnerability?.average_severity) { + const severity = current.vulnerability?.average_severity; + return { + ...prev, + total: prev.total + 1, + summary: { + ...prev.summary, + [severity]: prev.summary[severity] + 1, + }, + }; + } else { + return prev; + } + }, DEFAULT_DONUT_CHART_DATA); + }, [tableData]); - const { - currentPageItems, - numRenderedColumns, - propHelpers: { - toolbarProps, - filterToolbarProps, - tableProps, - paginationToolbarItemProps, - paginationProps, - getThProps, - getTrProps, - getTdProps, - }, - } = tableControls; + const donutChart = React.useMemo(() => { + return Object.keys(donutChartData.summary) + .map((item) => { + const severity = item as Severity; + const count = donutChartData.summary[severity]; + const severityProps = severityList[severity]; + return { + severity, + count, + label: severityProps.name, + color: severityProps.color.value, + }; + }) + .sort(compareBySeverityFn((item) => item.severity)); + }, [donutChartData]); return ( <> - - - - - - - - + + + + + + + +
+ ({ + name: `${label}: ${count}`, + }))} + data={donutChart.map(({ label, count }) => ({ + x: label, + y: count, + }))} + labels={({ datum }) => `${datum.x}: ${datum.y}`} + colorScale={donutChart.map(({ color }) => color)} + /> +
+
+ + + + Name + + {sbom?.name} + + + + Version + + {sbom?.described_by + .map((item) => item.version) + .join(", ")} + + + + Creation date + + {formatDate(sbom?.published)} + + + + +
+
+
+
+
+ + + + + + + + - - - - - - - - - {currentPageItems?.map((item, rowIndex) => { - return ( - - - + + + + + + + {currentPageItems?.map((item, rowIndex) => { + return ( + - - - - - - ); - })} - -
Packages
- - -
+ + + + + + +
- {item.name} - - {item.version} -
- + + + + + {item.vulnerabilityId} + + + + {item.vulnerability?.title || + item.vulnerability?.description} + + + {item.vulnerability?.average_severity && ( + + )} + + + {item.packages.length} + + + {formatDate(item.vulnerability?.published)} + + + CREATE_ISSUE + + + + {isCellExpanded(item) ? ( + + + + {isCellExpanded(item, "affectedDependencies") ? ( + <> + + + + + + + + + + + + + {item.packages + .flatMap((item) => item.purl) + .map((purl, index) => { + const props = decomposePurl(purl.purl); + return ( + + + + + + + + + ); + })} + +
TypeNamespaceNameVersionPathQualifiers
{props?.type}{props?.namespace}{props?.name}{props?.version}{props?.path} + {props?.qualifiers && ( + + )} +
+ + ) : null} +
+ + + ) : null} + + ); + })} +
+ + + + ); }; diff --git a/package-lock.json b/package-lock.json index f7e233d5..a1659a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "file-saver": "^2.0.5", "monaco-editor": "0.34.1", "oidc-client-ts": "^2.4.0", - "packageurl-js": "^1.2.1", + "packageurl-js": "^2.0.1", "pretty-bytes": "^6.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -12459,9 +12459,9 @@ "dev": true }, "node_modules/packageurl-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.1.tgz", - "integrity": "sha512-cZ6/MzuXaoFd16/k0WnwtI298UCaDHe/XlSh85SeOKbGZ1hq0xvNbx3ILyCMyk7uFQxl6scF3Aucj6/EO9NwcA==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-2.0.1.tgz", + "integrity": "sha512-N5ixXjzTy4QDQH0Q9YFjqIWd6zH6936Djpl2m9QNFmDv5Fum8q8BjkpAcHNMzOFE0IwQrFhJWex3AN6kS0OSwg==" }, "node_modules/param-case": { "version": "3.0.4",