Skip to content

Commit

Permalink
Add SbomList - vulnerabilities column
Browse files Browse the repository at this point in the history
  • Loading branch information
carlosthe19916 committed Oct 27, 2024
1 parent d6a3f02 commit 1f1ba9d
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 9 deletions.
14 changes: 9 additions & 5 deletions client/src/app/components/LoadingWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import { StateError } from "./StateError";

export const LoadingWrapper = (props: {
isFetching: boolean;
fetchError?: Error;
fetchError?: Error | null;
isFetchingState?: React.ReactNode;
fetchErrorState?: React.ReactNode;
children: React.ReactNode;
}) => {
if (props.isFetching) {
return (
<Bullseye>
<Spinner />
</Bullseye>
props.isFetchingState || (
<Bullseye>
<Spinner />
</Bullseye>
)
);
} else if (props.fetchError) {
return <StateError />;
return props.fetchErrorState || <StateError />;
} else {
return props.children;
}
Expand Down
149 changes: 149 additions & 0 deletions client/src/app/hooks/useSbomVulnerabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { client } from "@app/axios-config/apiInit";
import {
getVulnerability,
SbomAdvisory,
SbomPackage,
Severity,
VulnerabilityDetails,
} from "@app/client";
import { useFetchSbomsAdvisory } from "@app/queries/sboms";
import React from "react";
import { VulnerabilityStatus } from "@app/api/models";

interface SbomVulnerability {
vulnerabilityId: string;
advisory: SbomAdvisory;
status: VulnerabilityStatus;
packages: SbomPackage[];
vulnerability?: VulnerabilityDetails;
}

interface SbomVulnerabilitySummary {
total: number;
severities: { [key in Severity]: number };
}

const DEFAULT_SBOM_VULNERABILITY_SUMMARY: SbomVulnerabilitySummary = {
total: 0,
severities: { none: 0, low: 0, medium: 0, high: 0, critical: 0 },
};

export const useSbomVulnerabilities = (sbomId: string) => {
const {
advisories,
isFetching: isFetchingAdvisories,
fetchError: fetchErrorAdvisories,
} = useFetchSbomsAdvisory(sbomId);

const [allVulnerabilities, setAllVulnerabilities] = React.useState<
SbomVulnerability[]
>([]);
const [vulnerabilitiesById, setVulnerabilitiesById] = React.useState<
Map<string, VulnerabilityDetails>
>(new Map());
const [isFetchingVulnerabilities, setIsFetchingVulnerabilities] =
React.useState(false);

React.useEffect(() => {
if (advisories.length === 0) {
return;
}

const vulnerabilities = (advisories ?? [])
.flatMap((advisory) => {
return (advisory.status ?? []).map((status) => {
const result: SbomVulnerability = {
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 duplicates if exists
.reduce((prev, current) => {
const exists = prev.find(
(item) =>
item.vulnerabilityId === current.vulnerabilityId &&
item.advisory.uuid === current.advisory.uuid
);
if (!exists) {
return [...prev, current];
} else {
return prev;
}
}, [] as SbomVulnerability[]);

setAllVulnerabilities(vulnerabilities);
setIsFetchingVulnerabilities(true);

Promise.all(
vulnerabilities
.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) => {
if (current) {
return [...prev, current];
} else {
// Filter out error responses
return prev;
}
}, [] as VulnerabilityDetails[]);

const vulnerabilitiesById = new Map<string, VulnerabilityDetails>();
validVulnerabilities.forEach((vulnerability) => {
vulnerabilitiesById.set(vulnerability.identifier, vulnerability);
});

setVulnerabilitiesById(vulnerabilitiesById);
setIsFetchingVulnerabilities(false);
});
}, [advisories]);

const allVulnerabilitiesWithMappedData = React.useMemo(() => {
return allVulnerabilities.map((item) => {
const result: SbomVulnerability = {
...item,
vulnerability: vulnerabilitiesById.get(item.vulnerabilityId),
};
return result;
});
}, [allVulnerabilities, vulnerabilitiesById]);

// Summary

const vulnerabilitiesSummary = React.useMemo(() => {
return allVulnerabilitiesWithMappedData.reduce((prev, current) => {
if (current.vulnerability?.average_severity) {
const severity = current.vulnerability?.average_severity;
return {
...prev,
total: prev.total + 1,
severities: {
...prev.severities,
[severity]: prev.severities[severity] + 1,
},
};
} else {
return prev;
}
}, DEFAULT_SBOM_VULNERABILITY_SUMMARY);
}, [allVulnerabilitiesWithMappedData]);

return {
isFetching: isFetchingAdvisories || isFetchingVulnerabilities,
fetchError: fetchErrorAdvisories,
vulnerabilities: allVulnerabilitiesWithMappedData,
summary: vulnerabilitiesSummary,
};
};
28 changes: 28 additions & 0 deletions client/src/app/pages/sbom-list/components/SbomVulnerabilities.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";

import { Label, Skeleton } from "@patternfly/react-core";

import { VulnerabilityGallery } from "@app/components/VulnerabilityGallery";
import { useSbomVulnerabilities } from "@app/hooks/useSbomVulnerabilities";
import { LoadingWrapper } from "@app/components/LoadingWrapper";

interface SBOMVulnerabilitiesProps {
sbomId: string;
}

export const SBOMVulnerabilities: React.FC<SBOMVulnerabilitiesProps> = ({
sbomId,
}) => {
const { summary, isFetching, fetchError } = useSbomVulnerabilities(sbomId);

return (
<LoadingWrapper
isFetching={isFetching}
fetchError={fetchError}
isFetchingState={<Skeleton screenreaderText="Loading contents" />}
fetchErrorState={<Label color="red">Error</Label>}
>
<VulnerabilityGallery severities={summary.severities} />
</LoadingWrapper>
);
};
11 changes: 7 additions & 4 deletions client/src/app/pages/sbom-list/sbom-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import {
import { useDownload } from "@app/hooks/useDownload";
import { useDeleteSbomMutation } from "@app/queries/sboms";
import { formatDate } from "@app/utils/utils";

import { ConfirmDialog } from "@app/components/ConfirmDialog";

import { SbomSearchContext } from "./sbom-context";
import { SBOMVulnerabilities } from "./components/SbomVulnerabilities";

export const SbomTable: React.FC = ({}) => {
const { isFetching, fetchError, totalItemCount, tableControls } =
Expand Down Expand Up @@ -114,7 +115,7 @@ export const SbomTable: React.FC = ({}) => {
rowIndex={rowIndex}
>
<Td
width={30}
width={25}
{...getTdProps({
columnKey: "name",
isCompoundExpandToggle: true,
Expand Down Expand Up @@ -148,9 +149,11 @@ export const SbomTable: React.FC = ({}) => {
<PackagesCount sbomId={item.id} />
</Td>
<Td
width={15}
width={20}
{...getTdProps({ columnKey: "vulnerabilities" })}
></Td>
>
<SBOMVulnerabilities sbomId={item.id} />
</Td>
<Td isActionCell>
<ActionsColumn
items={[
Expand Down

0 comments on commit 1f1ba9d

Please sign in to comment.