Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reduce calls to backend on vulnerabilities page #272

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading