diff --git a/client/src/app/components/VulnerabilityStatusLabel.tsx b/client/src/app/components/VulnerabilityStatusLabel.tsx new file mode 100644 index 00000000..e1bae54b --- /dev/null +++ b/client/src/app/components/VulnerabilityStatusLabel.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import { Label } from "@patternfly/react-core"; + +import { VulnerabilityStatus } from "@app/api/models"; + +interface VulnerabilityStatusLabelProps { + value: VulnerabilityStatus; +} + +export const VulnerabilityStatusLabel: React.FC< + VulnerabilityStatusLabelProps +> = ({ value }) => { + return ( + + ); +}; diff --git a/client/src/app/hooks/domain-controls/useSbomsOfVulnerability.ts b/client/src/app/hooks/domain-controls/useSbomsOfVulnerability.ts index a2cf3756..6d11ff30 100644 --- a/client/src/app/hooks/domain-controls/useSbomsOfVulnerability.ts +++ b/client/src/app/hooks/domain-controls/useSbomsOfVulnerability.ts @@ -1,141 +1,127 @@ import React from "react"; import { VulnerabilityStatus } from "@app/api/models"; -import { client } from "@app/axios-config/apiInit"; -import { - getSbom, - SbomSummary, - VulnerabilityAdvisorySummary, -} from "@app/client"; +import { SbomHead, VulnerabilityAdvisorySummary } from "@app/client"; import { useFetchVulnerabilityById } from "@app/queries/vulnerabilities"; -interface SbomOfVulnerability { - sbomId: string; +const areSbomOfVulnerabilityEqual = ( + a: SbomOfVulnerability, + b: SbomOfVulnerability | FlatSbomOfVulnerability +) => { + return a.sbom.id === b.sbom.id && a.sbomStatus === b.sbomStatus; +}; + +interface FlatSbomOfVulnerability { + sbom: SbomHead & { version: string | null }; + sbomStatus: VulnerabilityStatus; advisory: VulnerabilityAdvisorySummary; - status: VulnerabilityStatus; - sbom?: SbomSummary; +} + +interface SbomOfVulnerability { + sbom: SbomHead & { version: string | null }; + sbomStatus: VulnerabilityStatus; + relatedPackages: { + advisory: VulnerabilityAdvisorySummary; + }[]; } export interface SbomOfVulnerabilitySummary { total: number; - status: { [key in VulnerabilityStatus]: number }; + sbomStatus: { [key in VulnerabilityStatus]: number }; } const DEFAULT_SUMMARY: SbomOfVulnerabilitySummary = { total: 0, - status: { affected: 0, fixed: 0, known_not_affected: 0, not_affected: 0 }, + sbomStatus: { affected: 0, fixed: 0, known_not_affected: 0, not_affected: 0 }, }; -export const useSbomsOfVulnerability = (sbomId: string) => { - const { - vulnerability, - isFetching: isFetchingAdvisories, - fetchError: fetchErrorAdvisories, - } = useFetchVulnerabilityById(sbomId); - - const [allSboms, setAllSboms] = React.useState([]); - const [sbomsById, setSbomsById] = React.useState>( - new Map() - ); - const [isFetchingSboms, setIsFetchingSboms] = React.useState(false); - - React.useEffect(() => { - if (vulnerability?.advisories?.length === 0) { - return; - } - - const sboms = (vulnerability?.advisories ?? []) - .flatMap((advisory) => { - return (advisory.sboms ?? []).flatMap((sbom) => { - return sbom.status.map((status) => { - const result: SbomOfVulnerability = { - sbomId: sbom.id, - status: status as VulnerabilityStatus, - advisory: { ...advisory }, +const advisoryToModels = (advisories: VulnerabilityAdvisorySummary[]) => { + const sboms = advisories.flatMap((advisory) => { + return ( + (advisory.sboms ?? []) + .flatMap((sbomStatuses) => { + return sbomStatuses.status.map((sbomStatus) => { + const result: FlatSbomOfVulnerability = { + sbom: { + ...sbomStatuses, + version: sbomStatuses.version || null, + }, + sbomStatus: sbomStatus as VulnerabilityStatus, + advisory: advisory, }; return result; }); - }); - }) - // Remove duplicates if exists - .reduce((prev, current) => { - const exists = prev.find( - (item) => - item.sbomId === current.sbomId && - item.advisory.uuid === current.advisory.uuid - ); - if (!exists) { - return [...prev, current]; - } else { - return prev; - } - }, [] as SbomOfVulnerability[]); - - setAllSboms(sboms); - setIsFetchingSboms(true); - - Promise.all( - sboms - .map(async (item) => { - const response = await getSbom({ - client, - path: { id: item.sbomId }, - }); - return response.data; }) - .map((sbom) => sbom.catch(() => null)) - ).then((sboms) => { - const validSboms = sboms.reduce((prev, current) => { - if (current) { - return [...prev, current]; - } else { - // Filter out error responses - return prev; - } - }, [] as SbomSummary[]); + // group + .reduce((prev, current) => { + const existingElement = prev.find((item) => { + return areSbomOfVulnerabilityEqual(item, current); + }); - const sbomsById = new Map(); - validSboms.forEach((sbom) => sbomsById.set(sbom.id, sbom)); + if (existingElement) { + const arrayWithoutExistingItem = prev.filter( + (item) => !areSbomOfVulnerabilityEqual(item, existingElement) + ); + + const updatedItemInArray: SbomOfVulnerability = { + ...existingElement, + relatedPackages: [ + ...existingElement.relatedPackages, + { + advisory: current.advisory, + }, + ], + }; - setSbomsById(sbomsById); - setIsFetchingSboms(false); - }); - }, [vulnerability]); + return [...arrayWithoutExistingItem, updatedItemInArray]; + } else { + const newItemInArray: SbomOfVulnerability = { + sbom: current.sbom, + sbomStatus: current.sbomStatus, + relatedPackages: [ + { + advisory: current.advisory, + }, + ], + }; + return [...prev, newItemInArray]; + } + }, [] as SbomOfVulnerability[]) + ); + }); + + const summary = sboms.reduce((prev, current) => { + const sbomStatus = current.sbomStatus; + return { + ...prev, + total: prev.total + 1, + sbomStatus: { + ...prev.sbomStatus, + [sbomStatus]: prev.sbomStatus[sbomStatus] + 1, + }, + }; + }, DEFAULT_SUMMARY); - const allSbomsWithMappedData = React.useMemo(() => { - return allSboms.map((item) => { - const result: SbomOfVulnerability = { - ...item, - sbom: sbomsById.get(item.sbomId), - }; - return result; - }); - }, [allSboms, sbomsById]); + return { + sboms, + summary, + }; +}; - // Summary +export const useSbomsOfVulnerability = (sbomId: string) => { + const { + vulnerability, + isFetching: isFetchingAdvisories, + fetchError: fetchErrorAdvisories, + } = useFetchVulnerabilityById(sbomId); - const sbomsSummary = React.useMemo(() => { - return allSbomsWithMappedData.reduce((prev, current) => { - if (current.status) { - const status = current.status; - return { - ...prev, - total: prev.total + 1, - status: { - ...prev.status, - [status]: prev.status[status] + 1, - }, - }; - } else { - return prev; - } - }, DEFAULT_SUMMARY); - }, [allSbomsWithMappedData]); + const result = React.useMemo(() => { + return advisoryToModels(vulnerability?.advisories || []); + }, [vulnerability]); return { - isFetching: isFetchingAdvisories || isFetchingSboms, + data: result, + isFetching: isFetchingAdvisories, fetchError: fetchErrorAdvisories, - sboms: allSbomsWithMappedData, - summary: sbomsSummary, }; }; diff --git a/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts b/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts index 0cd280c2..76db699e 100644 --- a/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts +++ b/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts @@ -7,7 +7,7 @@ import { useFetchSbomsAdvisoryBatch, } from "@app/queries/sboms"; -const areEqualVulnerabilityOfSbomEqual = ( +const areVulnerabilityOfSbomEqual = ( a: VulnerabilityOfSbom, b: VulnerabilityOfSbom | FlatVulnerabilityOfSbom ) => { @@ -58,7 +58,7 @@ const DEFAULT_SUMMARY: VulnerabilityOfSbomSummary = { }, }; -const advisoryStatusToModels = (advisories: SbomAdvisory[]) => { +const advisoryToModels = (advisories: SbomAdvisory[]) => { const vulnerabilities = advisories.flatMap((advisory) => { return ( (advisory.status ?? []) @@ -74,12 +74,12 @@ const advisoryStatusToModels = (advisories: SbomAdvisory[]) => { // group .reduce((prev, current) => { const existingElement = prev.find((item) => { - return areEqualVulnerabilityOfSbomEqual(item, current); + return areVulnerabilityOfSbomEqual(item, current); }); if (existingElement) { const arrayWithoutExistingItem = prev.filter( - (item) => !areEqualVulnerabilityOfSbomEqual(item, existingElement) + (item) => !areVulnerabilityOfSbomEqual(item, existingElement) ); const updatedItemInArray: VulnerabilityOfSbom = { @@ -147,7 +147,7 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => { } = useFetchSbomsAdvisory(sbomId); const result = React.useMemo(() => { - return advisoryStatusToModels(advisories || []); + return advisoryToModels(advisories || []); }, [advisories]); return { @@ -163,7 +163,7 @@ export const useVulnerabilitiesOfSboms = (sbomIds: string[]) => { const result = React.useMemo(() => { return (advisories ?? []).map((item) => { - return advisoryStatusToModels(item || []); + return advisoryToModels(item || []); }); }, [advisories]); diff --git a/client/src/app/pages/vulnerability-details/packages-by-vulnerability.tsx b/client/src/app/pages/vulnerability-details/packages-by-vulnerability.tsx index 9a029152..f005bd7b 100644 --- a/client/src/app/pages/vulnerability-details/packages-by-vulnerability.tsx +++ b/client/src/app/pages/vulnerability-details/packages-by-vulnerability.tsx @@ -22,6 +22,7 @@ import { } from "@patternfly/react-table"; import { DecomposedPurl, VulnerabilityStatus } from "@app/api/models"; +import { Purl, StatusContext, VulnerabilityAdvisorySummary } from "@app/client"; import { AdvisoryInDrawerInfo } from "@app/components/AdvisoryInDrawerInfo"; import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; import { PackageInDrawerInfo } from "@app/components/PackageInDrawerInfo"; @@ -32,10 +33,10 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; +import { VulnerabilityStatusLabel } from "@app/components/VulnerabilityStatusLabel"; import { useLocalTableControls } from "@app/hooks/table-controls"; import { useWithUiId } from "@app/utils/query-utils"; import { decomposePurl } from "@app/utils/utils"; -import { Purl, StatusContext, VulnerabilityAdvisorySummary } from "@app/client"; interface TableData { basePurl: { @@ -276,10 +277,7 @@ export const PackagesByVulnerability: React.FC< modifier="truncate" {...getTdProps({ columnKey: "status" })} > - + diff --git a/client/src/app/pages/vulnerability-details/sboms-by-vulnerability.tsx b/client/src/app/pages/vulnerability-details/sboms-by-vulnerability.tsx index 9edda82b..1fb04a9c 100644 --- a/client/src/app/pages/vulnerability-details/sboms-by-vulnerability.tsx +++ b/client/src/app/pages/vulnerability-details/sboms-by-vulnerability.tsx @@ -3,29 +3,9 @@ import { Link } from "react-router-dom"; import dayjs from "dayjs"; -import { - Label, - Toolbar, - ToolbarContent, - ToolbarItem, -} from "@patternfly/react-core"; -import { - Table, - TableProps, - Tbody, - Td, - Th, - Thead, - Tr, -} from "@patternfly/react-table"; +import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; +import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; -import { VulnerabilityStatus } from "@app/api/models"; -import { client } from "@app/axios-config/apiInit"; -import { - getSbom, - SbomSummary, - VulnerabilityAdvisorySummary, -} from "@app/client"; import { FilterType } from "@app/components/FilterToolbar"; import { SimplePagination } from "@app/components/SimplePagination"; import { @@ -33,17 +13,11 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; +import { VulnerabilityStatusLabel } from "@app/components/VulnerabilityStatusLabel"; +import { useSbomsOfVulnerability } from "@app/hooks/domain-controls/useSbomsOfVulnerability"; import { useLocalTableControls } from "@app/hooks/table-controls"; import { useWithUiId } from "@app/utils/query-utils"; import { formatDate } from "@app/utils/utils"; -import { useSbomsOfVulnerability } from "@app/hooks/domain-controls/useSbomsOfVulnerability"; - -interface TableData { - sbomId: string; - sbom?: SbomSummary; - status: VulnerabilityStatus; - advisory: VulnerabilityAdvisorySummary; -} interface SbomsByVulnerabilityProps { vulnerabilityId: string; @@ -52,12 +26,15 @@ interface SbomsByVulnerabilityProps { export const SbomsByVulnerability: React.FC = ({ vulnerabilityId, }) => { - const { sboms, isFetching, fetchError } = - useSbomsOfVulnerability(vulnerabilityId); + const { + data: { sboms }, + isFetching, + fetchError, + } = useSbomsOfVulnerability(vulnerabilityId); const tableDataWithUiId = useWithUiId( sboms, - (d) => `${d.sbomId}-${d.advisory.identifier}-${d.advisory.uuid}` + (d) => `${d.sbom.id}-${d.sbomStatus}` ); const tableControls = useLocalTableControls({ @@ -68,21 +45,18 @@ export const SbomsByVulnerability: React.FC = ({ columnNames: { name: "Name", version: "Version", + status: "Status", dependencies: "Dependencies", supplier: "Supplier", created: "Created on", - published: "Published", - labels: "Labels", - status: "Status", - advisory: "Advisory", }, hasActionsColumn: false, isSortEnabled: true, - sortableColumns: ["published"], + sortableColumns: ["name", "dependencies", "created"], getSortValues: (item) => ({ - published: item.sbom?.published - ? dayjs(item.sbom.published).valueOf() - : 0, + name: item.sbom.name, + dependencies: item.sbom.number_of_packages, + created: item.sbom?.published ? dayjs(item.sbom.published).valueOf() : 0, }), isPaginationEnabled: true, isFilterEnabled: true, @@ -151,7 +125,7 @@ export const SbomsByVulnerability: React.FC = ({ > {currentPageItems?.map((item, rowIndex) => { return ( - + = ({ rowIndex={rowIndex} > - + {item?.sbom?.name} - {item.sbom?.described_by[0]?.version} + {item.sbom?.version} - + = ({ {item?.sbom?.number_of_packages} - {item?.sbom?.authors} + {item?.sbom?.authors.join(", ")} new Object(), name: "A name here", }, - summary: { - total: 5, - status: { - affected: 3, - fixed: 2, - not_affected: 0, - known_not_affected: 0, + data: { + summary: { + total: 5, + sbomStatus: { + affected: 3, + fixed: 2, + not_affected: 0, + known_not_affected: 0, + }, }, + sboms: [], }, - sboms: [], }); }, parameters: { @@ -202,16 +204,18 @@ export const PopulatedState: Story = { toJSON: () => new Object(), name: "A name here", }, - summary: { - total: 0, - status: { - affected: 0, - fixed: 0, - not_affected: 0, - known_not_affected: 0, + data: { + summary: { + total: 0, + sbomStatus: { + affected: 0, + fixed: 0, + not_affected: 0, + known_not_affected: 0, + }, }, + sboms: [], }, - sboms: [], }); }, parameters: { diff --git a/client/src/app/pages/vulnerability-list/components/SbomsCount.tsx b/client/src/app/pages/vulnerability-list/components/SbomsCount.tsx index 35ae9049..da540dad 100644 --- a/client/src/app/pages/vulnerability-list/components/SbomsCount.tsx +++ b/client/src/app/pages/vulnerability-list/components/SbomsCount.tsx @@ -10,7 +10,7 @@ interface SbomsCountProps { } export const SbomsCount: React.FC = ({ vulnerabilityId }) => { - const { summary, isFetching, fetchError } = + const { data, isFetching, fetchError } = useSbomsOfVulnerability(vulnerabilityId); return ( @@ -20,7 +20,7 @@ export const SbomsCount: React.FC = ({ vulnerabilityId }) => { isFetchingState={} fetchErrorState={} > - {summary.status.affected} + {data.summary.sbomStatus.affected} ); };