Skip to content

Commit

Permalink
fix: reduce calls to backend on vulnerabilities page (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosthe19916 authored Nov 25, 2024
1 parent 15cb4e9 commit 9ecb9b1
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 189 deletions.
19 changes: 19 additions & 0 deletions client/src/app/components/VulnerabilityStatusLabel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Label>
{value.charAt(0).toUpperCase() + value.slice(1).replace("_", " ")}
</Label>
);
};
208 changes: 97 additions & 111 deletions client/src/app/hooks/domain-controls/useSbomsOfVulnerability.ts
Original file line number Diff line number Diff line change
@@ -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<SbomOfVulnerability[]>([]);
const [sbomsById, setSbomsById] = React.useState<Map<string, SbomSummary>>(
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<string, SbomSummary>();
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,
};
};
12 changes: 6 additions & 6 deletions client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
useFetchSbomsAdvisoryBatch,
} from "@app/queries/sboms";

const areEqualVulnerabilityOfSbomEqual = (
const areVulnerabilityOfSbomEqual = (
a: VulnerabilityOfSbom,
b: VulnerabilityOfSbom | FlatVulnerabilityOfSbom
) => {
Expand Down Expand Up @@ -58,7 +58,7 @@ const DEFAULT_SUMMARY: VulnerabilityOfSbomSummary = {
},
};

const advisoryStatusToModels = (advisories: SbomAdvisory[]) => {
const advisoryToModels = (advisories: SbomAdvisory[]) => {
const vulnerabilities = advisories.flatMap((advisory) => {
return (
(advisory.status ?? [])
Expand All @@ -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 = {
Expand Down Expand Up @@ -147,7 +147,7 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => {
} = useFetchSbomsAdvisory(sbomId);

const result = React.useMemo(() => {
return advisoryStatusToModels(advisories || []);
return advisoryToModels(advisories || []);
}, [advisories]);

return {
Expand All @@ -163,7 +163,7 @@ export const useVulnerabilitiesOfSboms = (sbomIds: string[]) => {

const result = React.useMemo(() => {
return (advisories ?? []).map((item) => {
return advisoryStatusToModels(item || []);
return advisoryToModels(item || []);
});
}, [advisories]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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: {
Expand Down Expand Up @@ -276,10 +277,7 @@ export const PackagesByVulnerability: React.FC<
modifier="truncate"
{...getTdProps({ columnKey: "status" })}
>
<Label>
{item.status.charAt(0).toUpperCase() +
item.status.slice(1).replace("_", " ")}
</Label>
<VulnerabilityStatusLabel value={item.status} />
</Td>
</TableRowContentWithControls>
</Tr>
Expand Down
Loading

0 comments on commit 9ecb9b1

Please sign in to comment.