From cd8a62cd35b711415c9dbe134bf53a0d04f1a953 Mon Sep 17 00:00:00 2001 From: Carlos Feria Date: Wed, 19 Jun 2024 16:08:17 +0200 Subject: [PATCH] Add SBOM's packages table view (#65) --- client/src/app/Constants.ts | 1 + client/src/app/api/models.ts | 15 +- client/src/app/api/rest.ts | 3 +- .../src/app/components/PackageQualifiers.tsx | 20 ++ .../src/app/pages/sbom-details/packages.tsx | 294 ++++++++++++------ client/src/app/utils/utils.ts | 19 ++ 6 files changed, 256 insertions(+), 96 deletions(-) create mode 100644 client/src/app/components/PackageQualifiers.tsx diff --git a/client/src/app/Constants.ts b/client/src/app/Constants.ts index f5ebdc82..8c3a030d 100644 --- a/client/src/app/Constants.ts +++ b/client/src/app/Constants.ts @@ -9,6 +9,7 @@ export const TablePersistenceKeyPrefixes = { vulnerabilities: "vn", sboms: "sb", packages: "pk", + sbom_packages: "spk", }; // URL param prefixes: should be short, must be unique for each table that uses one diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index c0f2e007..067f9d5d 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -35,11 +35,6 @@ export interface HubPaginatedResult { // Base -export interface PackageBase { - uuid: string; - purl: string; -} - export interface SBOMBase { id: string; type: "CycloneDX" | "SPDX"; @@ -101,7 +96,9 @@ export interface VulnerabilityAdvisory extends AdvisoryBase { // Package -export interface Package extends PackageBase { +export interface Package { + uuid: string; + purl: string; // This field is added by the UI package?: { name: string; @@ -115,6 +112,12 @@ export interface Package extends PackageBase { related_sboms: SBOMBase[]; } +export interface SBOMPackage { + id: string; + name: string; + purl: string[]; +} + // SBOM export interface SBOM extends SBOMBase { diff --git a/client/src/app/api/rest.ts b/client/src/app/api/rest.ts index 768172a1..6c8d033f 100644 --- a/client/src/app/api/rest.ts +++ b/client/src/app/api/rest.ts @@ -11,6 +11,7 @@ import { ImporterReport, Package, SBOM, + SBOMPackage, Vulnerability, } from "./models"; @@ -120,7 +121,7 @@ export const downloadSBOMById = (id: number | string) => export const getPackagesBySbomId = ( id: number | string, params: HubRequestParams = {} -) => getHubPaginatedResult(`${SBOMS}/${id}/packages`, params); +) => getHubPaginatedResult(`${SBOMS}/${id}/packages`, params); export const getVulnerabilitiesBySbomId = (id: string | number) => axios diff --git a/client/src/app/components/PackageQualifiers.tsx b/client/src/app/components/PackageQualifiers.tsx new file mode 100644 index 00000000..3d3df659 --- /dev/null +++ b/client/src/app/components/PackageQualifiers.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import { Label } from "@patternfly/react-core"; + +interface PackageQualifiersProps { + value: { [key: string]: string } +} + +export const PackageQualifiers: React.FC = ({ + value, +}) => { + return <> + {Object.entries(value).map(([k, v], index) => ( + + ))} + +}; diff --git a/client/src/app/pages/sbom-details/packages.tsx b/client/src/app/pages/sbom-details/packages.tsx index 33580271..f0b737e7 100644 --- a/client/src/app/pages/sbom-details/packages.tsx +++ b/client/src/app/pages/sbom-details/packages.tsx @@ -1,10 +1,9 @@ import React from "react"; +import { NavLink } from "react-router-dom"; import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; import { ExpandableRowContent, - Td as PFTd, - Tr as PFTr, Table, Tbody, Td, @@ -13,59 +12,74 @@ import { Tr, } from "@patternfly/react-table"; +import { TablePersistenceKeyPrefixes } from "@app/Constants"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; +import { PackageQualifiers } from "@app/components/PackageQualifiers"; import { SimplePagination } from "@app/components/SimplePagination"; import { ConditionalTableBody, TableHeaderContentWithControls, + TableRowContentWithControls, } from "@app/components/TableControls"; -import { useLocalTableControls } from "@app/hooks/table-controls"; +import { getHubRequestParams, useLocalTableControls, useTableControlProps, useTableControlState } from "@app/hooks/table-controls"; +import { useSelectionState } from "@app/hooks/useSelectionState"; import { useFetchPackagesBySbomId } from "@app/queries/sboms"; +import { decomposePurl } from "@app/utils/utils"; interface PackagesProps { sbomId: string; } export const Packages: React.FC = ({ sbomId }) => { - const { - result: { data: packages }, - isFetching, - fetchError, - } = useFetchPackagesBySbomId(sbomId); - - const tableControls = useLocalTableControls({ + const tableControlState = useTableControlState({ tableName: "packages-table", - idProperty: "uuid", - items: packages, - isLoading: isFetching, + persistenceKeyPrefix: TablePersistenceKeyPrefixes.packages, columnNames: { + id: "Id", name: "Name", - namespace: "Namespace", - version: "Version", - type: "Type", - path: "Path", - qualifiers: "Qualifiers", - cves: "CVEs", }, isPaginationEnabled: true, - initialItemsPerPage: 10, - isExpansionEnabled: true, - expandableVariant: "single", + isSortEnabled: true, + sortableColumns: [], isFilterEnabled: true, filterCategories: [ { - categoryKey: "filterText", + categoryKey: "", title: "Filter tex", type: FilterType.search, placeholderText: "Search...", - getItemValue: (item) => item.purl, }, ], + isExpansionEnabled: true, + expandableVariant: "single", + }); + + 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 { - currentPageItems, numRenderedColumns, + currentPageItems, propHelpers: { toolbarProps, filterToolbarProps, @@ -86,7 +100,7 @@ export const Packages: React.FC = ({ sbomId }) => { @@ -94,17 +108,12 @@ export const Packages: React.FC = ({ sbomId }) => { - +
+ @@ -116,69 +125,37 @@ export const Packages: React.FC = ({ sbomId }) => { > {currentPageItems?.map((item, rowIndex) => { return ( - + - - - - - - - + + + {isCellExpanded(item) ? ( - - + + + ) : null} ); @@ -186,7 +163,7 @@ export const Packages: React.FC = ({ sbomId }) => {
- - - - - -
- {/* {item.name} */} - - {/* {item.namespace} */} - - {/* {item.version} */} - - {/* {item.type} */} - - {/* {item.path} */} - - {/* {item.qualifiers && - Object.entries(item.qualifiers || {}).map( - ([k, v], index) => ( - - ) - )} */} - - TODO list of CVEs - + {item.id} + + {item.name} +
- TODO: dependency tree + cve list +
- - +
= ({ sbomId }) => { ); }; + +interface PackageExpandedAreaProps { + purls: string[]; +} + +export const PackageExpandedArea: React.FC = ({ purls }) => { + const packages = React.useMemo(() => { + return purls.map(purl => { + return { purl, ...decomposePurl(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 && } +
+ + ); +}; \ No newline at end of file diff --git a/client/src/app/utils/utils.ts b/client/src/app/utils/utils.ts index 28d7da3b..06fc6292 100644 --- a/client/src/app/utils/utils.ts +++ b/client/src/app/utils/utils.ts @@ -2,6 +2,7 @@ import { RENDER_DATE_FORMAT } from "@app/Constants"; import { ToolbarChip } from "@patternfly/react-core"; import { AxiosError } from "axios"; import dayjs from "dayjs"; +import { PackageURL } from "packageurl-js"; // Axios error @@ -83,3 +84,21 @@ export const getValidatedFromErrors = ( export const getValidatedFromError = (error: unknown | undefined) => { return error ? "error" : "default"; }; + +export const decomposePurl = (purl: string) => { + try { + const packageData = PackageURL.fromString(purl); + const result = { + type: packageData.type, + name: packageData.name, + namespace: packageData.namespace ?? undefined, + version: packageData.version ?? undefined, + qualifiers: packageData.qualifiers ?? undefined, + path: packageData.subpath ?? undefined, + }; + return result; + } catch (error) { + console.error(error); + return undefined; + } +};