From f869b571b62135a39d78d5f81c8620c2a6df7b70 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Tue, 29 Oct 2024 08:51:52 +0100 Subject: [PATCH 01/11] Test Storybook --- .github/workflows/ci-repo.yaml | 2 + .github/workflows/storybook-pr-build.yaml | 33 ++++++++ .github/workflows/storybook-pr-deploy.yaml | 88 ++++++++++++++++++++++ .github/workflows/storybook.yaml | 50 ++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 .github/workflows/storybook-pr-build.yaml create mode 100644 .github/workflows/storybook-pr-deploy.yaml create mode 100644 .github/workflows/storybook.yaml diff --git a/.github/workflows/ci-repo.yaml b/.github/workflows/ci-repo.yaml index 46fcfd9d..25408a76 100644 --- a/.github/workflows/ci-repo.yaml +++ b/.github/workflows/ci-repo.yaml @@ -30,6 +30,8 @@ jobs: # run: npm run lint - name: Build run: npm run build + - name: Build storybook + run: npm run build-storybook - name: Test run: npm run test -- --coverage --watchAll=false - name: Format check diff --git a/.github/workflows/storybook-pr-build.yaml b/.github/workflows/storybook-pr-build.yaml new file mode 100644 index 00000000..52ae198c --- /dev/null +++ b/.github/workflows/storybook-pr-build.yaml @@ -0,0 +1,33 @@ +name: Storybook - PR Preview Build + +on: + pull_request: + paths: + - "**/*.stories.tsx" + +jobs: + build-storybook-pr-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies + run: npm clean-install --ignore-scripts + - name: Build + run: npm run build + - name: Build storybook + run: npm run build-storybook + - name: Save PR number + run: | + mkdir -p ./pr + echo ${{ github.event.number }} > ./pr/NR + - uses: actions/upload-artifact@v4 + with: + name: storybook-static + path: storybook-static/ + - uses: actions/upload-artifact@v4 + with: + name: pr + path: pr/ diff --git a/.github/workflows/storybook-pr-deploy.yaml b/.github/workflows/storybook-pr-deploy.yaml new file mode 100644 index 00000000..df3752bb --- /dev/null +++ b/.github/workflows/storybook-pr-deploy.yaml @@ -0,0 +1,88 @@ +name: Storybook PR Preview Deploy +on: + workflow_run: + workflows: ["Storybook - PR Preview Build"] + types: + - completed + +jobs: + deploy-storybook-pr-preview: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: Download storybook-static + uses: actions/github-script@v6 + with: + script: | + var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "storybook-static" + })[0]; + var download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/storybook-static.zip', Buffer.from(download.data)); + - name: Download PR number + uses: actions/github-script@v6 + with: + script: | + var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{github.event.workflow_run.id }}, + }); + var matchArtifact = artifacts.data.artifacts.filter((artifact) => { + return artifact.name == "pr" + })[0]; + var download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + var fs = require('fs'); + fs.writeFileSync('${{github.workspace}}/pr.zip', Buffer.from(download.data)); + + - run: unzip storybook-static.zip -d storybook-static/ + - run: unzip pr.zip + + - name: Generate issue_number + uses: actions/github-script@v6 + id: issue_number + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var fs = require('fs'); + return Number(fs.readFileSync('./NR')); + result-encoding: string + + - name: Generate Surge URL + uses: actions/github-script@v6 + id: surge-url + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const issue_number = ${{steps.issue_number.outputs.result}}; + return `trustify-ui-pr-${issue_number}-preview.surge.sh`; + result-encoding: string + - name: Install Surge + run: npm install -g surge + - name: Deploy to Surge + run: surge ./storybook-static/ "${{steps.surge-url.outputs.result}}" --token ${{ secrets.SURGE_TOKEN }} + + - name: Post URL as PR comment + uses: mshick/add-pr-comment@v2 + with: + message: "🚀 Storybook Deployed Preview: https://${{steps.surge-url.outputs.result}} ✨" + repo-token: ${{ secrets.PREVIEW_BOT_TOKEN }} + issue: ${{steps.issue_number.outputs.result}} diff --git a/.github/workflows/storybook.yaml b/.github/workflows/storybook.yaml new file mode 100644 index 00000000..68ca241b --- /dev/null +++ b/.github/workflows/storybook.yaml @@ -0,0 +1,50 @@ +name: Storybook Deployment + +on: + push: + branches: + - main + paths: + - "**/*.stories.tsx" + workflow_dispatch: + workflow_call: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + gh-pages: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - name: Install dependencies + run: npm clean-install --ignore-scripts + - name: Build + run: npm run build + - name: Build storybook + run: npm run build-storybook + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "./storybook-static" + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 From c6df5df6b8a5f962390dec6f416ee327f76e4676 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:46:43 +0100 Subject: [PATCH 02/11] feat: align Package List and Package Details pages to v1 --- client/src/app/api/model-utils.ts | 2 +- .../pages/package-details/package-details.tsx | 133 ++++------ .../package-details/sboms-by-package.tsx | 85 ++----- .../vulnerabilities-by-package.tsx | 229 +++++------------- .../pages/package-list/package-context.tsx | 23 +- .../app/pages/package-list/package-table.tsx | 39 ++- 6 files changed, 163 insertions(+), 348 deletions(-) diff --git a/client/src/app/api/model-utils.ts b/client/src/app/api/model-utils.ts index 53ab71ff..c01208f9 100644 --- a/client/src/app/api/model-utils.ts +++ b/client/src/app/api/model-utils.ts @@ -45,7 +45,7 @@ export const severityList: ListType = { }, }; -const getSeverityPriority = (val: Severity) => { +export const getSeverityPriority = (val: Severity) => { switch (val) { case "low": return 1; diff --git a/client/src/app/pages/package-details/package-details.tsx b/client/src/app/pages/package-details/package-details.tsx index 94e94237..e3ade10d 100644 --- a/client/src/app/pages/package-details/package-details.tsx +++ b/client/src/app/pages/package-details/package-details.tsx @@ -1,20 +1,20 @@ import React from "react"; -import { Link } from "react-router-dom"; import { - Breadcrumb, - BreadcrumbItem, + Flex, + FlexItem, PageSection, - Popover, - TabAction, + Stack, + StackItem, + Tab, + Tabs, + TabTitleText, Text, TextContent, } from "@patternfly/react-core"; -import HelpIcon from "@patternfly/react-icons/dist/esm/icons/help-icon"; - -import DetailsPage from "@patternfly/react-component-groups/dist/dynamic/DetailsPage"; import { PathParam, useRouteParams } from "@app/Routes"; +import { PackageQualifiers } from "@app/components/PackageQualifiers"; import { useFetchPackageById } from "@app/queries/packages"; import { decomposePurl } from "@app/utils/utils"; @@ -32,86 +32,43 @@ export const PackageDetails: React.FC = () => { return ( <> - - - Packages - - Package details - - } - pageHeading={{ - title: decomposedPurl?.name ?? packageId ?? "", - iconAfterTitle: pkg ? ( - - {`version: ${decomposedPurl?.version}`} - - ) : undefined, - label: pkg - ? { - children: pkg ? `type=${decomposedPurl?.type}` : "", - isCompact: true, - } - : undefined, - }} - actionButtons={[]} - tabs={[ - { - eventKey: "vulnerabilities", - title: "Vulnerabilities", - children: ( -
- {packageId && ( - - )} -
- ), - actions: ( - <> - - Vulnerabilities associated to the - current package. - - } - position="top" - > - - - - - - ), - }, - { - eventKey: "sboms", - title: "SBOMs", - children: ( -
- {packageId && } -
- ), - actions: ( - <> - - SBOMs that contain the current package. - - } - position="top" - > - - - - - - ), - }, - ]} - /> + + + + + {decomposedPurl?.name ?? packageId ?? ""} + + + + + + +

version: {decomposedPurl?.version}

{" "} +
+ + {decomposedPurl?.qualifiers && ( + + )} + +
+
+
+
+ + + Vulnerabilities} + > + {packageId && } + + Products using package} + > + {packageId && } + + ); diff --git a/client/src/app/pages/package-details/sboms-by-package.tsx b/client/src/app/pages/package-details/sboms-by-package.tsx index 48dd4616..9c103692 100644 --- a/client/src/app/pages/package-details/sboms-by-package.tsx +++ b/client/src/app/pages/package-details/sboms-by-package.tsx @@ -1,23 +1,11 @@ import React from "react"; +import { Link } from "react-router-dom"; -import { - Button, - ButtonVariant, - TextContent, - Title, - Toolbar, - ToolbarContent, - ToolbarItem, -} from "@patternfly/react-core"; -import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; import { TablePersistenceKeyPrefixes } from "@app/Constants"; -import { SbomSummary } from "@app/client"; -import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; -import { LabelsAsList } from "@app/components/LabelsAsList"; -import { PageDrawerContent } from "@app/components/PageDrawerContext"; -import { SbomInDrawerInfo } from "@app/components/SbomInDrawerInfo"; +import { FilterType } from "@app/components/FilterToolbar"; import { SimplePagination } from "@app/components/SimplePagination"; import { ConditionalTableBody, @@ -30,7 +18,6 @@ import { } from "@app/hooks/table-controls"; import { useSelectionState } from "@app/hooks/useSelectionState"; import { useFetchSbomsByPackageId } from "@app/queries/sboms"; -import { formatDate } from "@app/utils/utils"; interface SbomsByPackageProps { packageId: string; @@ -39,26 +26,13 @@ interface SbomsByPackageProps { export const SbomsByPackage: React.FC = ({ packageId, }) => { - type RowAction = "showSbom"; - const [selectedRowAction, setSelectedRowAction] = - React.useState(null); - const [selectedRow, setSelectedRow] = React.useState( - null - ); - - const showDrawer = (action: RowAction, row: SbomSummary) => { - setSelectedRowAction(action); - setSelectedRow(row); - }; - - // const tableControlState = useTableControlState({ tableName: "sboms", persistenceKeyPrefix: TablePersistenceKeyPrefixes.sboms_by_package, columnNames: { name: "Name", - published: "Published", - labels: "Labels", + version: "Version", + supplier: "Supplier", }, isPaginationEnabled: true, isSortEnabled: true, @@ -117,7 +91,6 @@ export const SbomsByPackage: React.FC = ({ <> - = ({ - - + + @@ -149,27 +122,21 @@ export const SbomsByPackage: React.FC = ({ - + {item.name} - {formatDate(item.published)} + {item.described_by.map((item) => item.version).join(", ")} - {item.labels && } + {item.authors.join(", ")} @@ -178,33 +145,11 @@ export const SbomsByPackage: React.FC = ({ - - setSelectedRowAction(null)} - pageKey="drawer" - drawerPanelContentProps={{ defaultSize: "600px" }} - header={ - <> - {selectedRowAction === "showSbom" && ( - - - Advisory - - - )} - - } - > - {selectedRowAction === "showSbom" && ( - <>{selectedRow && } - )} - ); }; diff --git a/client/src/app/pages/package-details/vulnerabilities-by-package.tsx b/client/src/app/pages/package-details/vulnerabilities-by-package.tsx index cd2f37c3..56b27936 100644 --- a/client/src/app/pages/package-details/vulnerabilities-by-package.tsx +++ b/client/src/app/pages/package-details/vulnerabilities-by-package.tsx @@ -1,29 +1,19 @@ import React from "react"; +import { Link } from "react-router-dom"; -import { - Button, - ButtonVariant, - Label, - TextContent, - Title, - Toolbar, - ToolbarContent, - ToolbarItem, -} from "@patternfly/react-core"; -import spacing from "@patternfly/react-styles/css/utilities/Spacing/spacing"; +import dayjs from "dayjs"; + +import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; +import { getSeverityPriority } from "@app/api/model-utils"; import { VulnerabilityStatus } from "@app/api/models"; import { client } from "@app/axios-config/apiInit"; import { getVulnerability, PurlAdvisory, - StatusContext, VulnerabilityDetails, } from "@app/client"; -import { AdvisoryInDrawerInfo } from "@app/components/AdvisoryInDrawerInfo"; -import { FilterToolbar, FilterType } from "@app/components/FilterToolbar"; -import { PageDrawerContent } from "@app/components/PageDrawerContext"; import { SeverityShieldAndText } from "@app/components/SeverityShieldAndText"; import { SimplePagination } from "@app/components/SimplePagination"; import { @@ -31,18 +21,15 @@ import { TableHeaderContentWithControls, TableRowContentWithControls, } from "@app/components/TableControls"; -import { VulnerabilityInDrawerInfo } from "@app/components/VulnerabilityInDrawerInfo"; import { useLocalTableControls } from "@app/hooks/table-controls"; import { useFetchPackageById } from "@app/queries/packages"; import { useWithUiId } from "@app/utils/query-utils"; - -import { ShowStatusContext } from "../vulnerability-details/packages-by-vulnerability"; +import { formatDate } from "@app/utils/utils"; interface TableData { vulnerabilityId: string; advisory: PurlAdvisory; status: VulnerabilityStatus; - context: StatusContext | null; vulnerability?: VulnerabilityDetails; } @@ -53,18 +40,11 @@ interface VulnerabilitiesByPackageProps { export const VulnerabilitiesByPackage: React.FC< VulnerabilitiesByPackageProps > = ({ packageId }) => { - type RowAction = "showVulnerability" | "showAdvisory"; - const [selectedRowAction, setSelectedRowAction] = - React.useState(null); - const [selectedRow, setSelectedRow] = React.useState(null); - - const showDrawer = (action: RowAction, row: TableData) => { - setSelectedRowAction(action); - setSelectedRow(row); - }; - - // - const { pkg, isFetching, fetchError } = useFetchPackageById(packageId); + const { + pkg, + isFetching: isFetchingPackage, + fetchError: fetchErrorPackage, + } = useFetchPackageById(packageId); const [allVulnerabilities, setAllVulnerabilities] = React.useState< TableData[] @@ -75,24 +55,21 @@ export const VulnerabilitiesByPackage: React.FC< const [isFetchingVulnerabilities, setIsFetchingVulnerabilities] = React.useState(false); - const [allAdvisoryStatus, setAllAdvisoryStatus] = React.useState< - Set - >(new Set()); - React.useEffect(() => { const vulnerabilities: TableData[] = (pkg?.advisories ?? []) .flatMap((advisory) => { - return advisory.status.map( - (status) => - ({ - vulnerabilityId: status.vulnerability.identifier, - status: status.status, - context: status.context, - advisory: advisory, - }) as TableData - ); + return advisory.status.map((status) => { + const result: TableData = { + vulnerabilityId: status.vulnerability.identifier, + status: status.status as VulnerabilityStatus, + advisory: advisory, + }; + return result; + }); }) - // TODO remove this reduce once https://github.com/trustification/trustify/issues/477 is fixed + // Take only "affected" + .filter((item) => item.status === "affected") + // Remove dupplicates if exists .reduce((prev, current) => { const exists = prev.find( (item) => @@ -106,24 +83,18 @@ export const VulnerabilitiesByPackage: React.FC< } }, [] as TableData[]); - const allUniqueStatus = new Set(); - vulnerabilities.forEach((item) => allUniqueStatus.add(item.status)); - setAllVulnerabilities(vulnerabilities); - setAllAdvisoryStatus(allUniqueStatus); setIsFetchingVulnerabilities(true); Promise.all( vulnerabilities - .map( - async (item) => - ( - await getVulnerability({ - client, - path: { id: item.vulnerabilityId }, - }) - ).data - ) + .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) => { @@ -164,43 +135,28 @@ export const VulnerabilitiesByPackage: React.FC< tableName: "vulnerability-table", idProperty: "_ui_unique_id", items: tableDataWithUiId, - isLoading: false, + isLoading: isFetchingPackage || isFetchingVulnerabilities, columnNames: { - identifier: "Identifier", - title: "Title", - severity: "Severity", - advisory: "Advisory", - context: "Context", - status: "Status", + identifier: "ID", + description: "Description", + severity: "CVSS", + published: "Date published", }, hasActionsColumn: false, isSortEnabled: true, - sortableColumns: ["identifier"], + sortableColumns: ["identifier", "severity", "published"], getSortValues: (item) => ({ identifier: item.vulnerabilityId, + severity: item.vulnerability?.average_severity + ? getSeverityPriority(item.vulnerability?.average_severity) + : 0, + published: item.vulnerability?.published + ? dayjs(item.vulnerability?.published).valueOf() + : 0, }), isPaginationEnabled: true, - isFilterEnabled: true, - filterCategories: [ - { - categoryKey: "filterText", - title: "Filter text", - placeholderText: "Search", - type: FilterType.search, - getItemValue: (item) => item.vulnerabilityId, - }, - { - categoryKey: "status", - title: "Status", - placeholderText: "Status", - type: FilterType.multiselect, - selectOptions: Array.from(allAdvisoryStatus).map((item) => ({ - value: item, - label: item.charAt(0).toUpperCase() + item.slice(1).replace("_", " "), - })), - matcher: (filter: string, item: TableData) => item.status === filter, - }, - ], + isFilterEnabled: false, + filterCategories: [], isExpansionEnabled: false, }); @@ -224,11 +180,10 @@ export const VulnerabilitiesByPackage: React.FC< <> - @@ -240,17 +195,15 @@ export const VulnerabilitiesByPackage: React.FC< - + - - - + @@ -264,57 +217,31 @@ export const VulnerabilitiesByPackage: React.FC< rowIndex={rowIndex} > - + - {item.vulnerability?.title} + {item.vulnerability?.title || + item.vulnerability?.description} - + {item.vulnerability?.average_severity && ( )} - - - - - - - + {formatDate(item.vulnerability?.published)} @@ -329,48 +256,6 @@ export const VulnerabilitiesByPackage: React.FC< isCompact paginationProps={paginationProps} /> - - setSelectedRowAction(null)} - pageKey="drawer" - drawerPanelContentProps={{ defaultSize: "600px" }} - header={ - <> - {selectedRowAction === "showVulnerability" && ( - - - Vulnerability - - - )} - {selectedRowAction === "showAdvisory" && ( - - - Advisory - - - )} - - } - > - {selectedRowAction === "showVulnerability" && ( - <> - {selectedRow?.advisory && ( - - )} - - )}{" "} - {selectedRowAction === "showAdvisory" && ( - <> - {selectedRow?.advisory && ( - - )} - - )} - ); }; diff --git a/client/src/app/pages/package-list/package-context.tsx b/client/src/app/pages/package-list/package-context.tsx index fb2fae85..e10c4f9f 100644 --- a/client/src/app/pages/package-list/package-context.tsx +++ b/client/src/app/pages/package-list/package-context.tsx @@ -23,9 +23,9 @@ interface PackageTableData extends PurlSummary { interface IPackageSearchContext { tableControls: ITableControls< PackageTableData, - "name" | "version" | "type" | "vulnerabilities" | "sboms", + "name" | "namespace" | "version" | "type" | "path" | "qualifiers", never, - "" | "type", + "" | "type" | "arch", string >; @@ -51,10 +51,11 @@ export const PackageSearchProvider: React.FunctionComponent< persistenceKeyPrefix: TablePersistenceKeyPrefixes.packages, columnNames: { name: "Name", + namespace: "Namespace", version: "Version", type: "Type", - vulnerabilities: "Vulnerabilities", - sboms: "SBOMs", + path: "Path", + qualifiers: "Qualifiers", }, isPaginationEnabled: true, isSortEnabled: true, @@ -76,6 +77,20 @@ export const PackageSearchProvider: React.FunctionComponent< { value: "maven", label: "Maven" }, { value: "rpm", label: "RPM" }, { value: "npm", label: "NPM" }, + { value: "oci", label: "OCI" }, + ], + }, + { + categoryKey: "arch", + title: "Architecture", + placeholderText: "Architecture", + type: FilterType.multiselect, + selectOptions: [ + { value: "x86_64", label: "AMD 64bit" }, + { value: "aarch64", label: "ARM 64bit" }, + { value: "ppc64le", label: "PowerPC" }, + { value: "s390x", label: "S390" }, + { value: "noarch", label: "No Arch" }, ], }, ], diff --git a/client/src/app/pages/package-list/package-table.tsx b/client/src/app/pages/package-list/package-table.tsx index fd12acec..b50aa85e 100644 --- a/client/src/app/pages/package-list/package-table.tsx +++ b/client/src/app/pages/package-list/package-table.tsx @@ -3,7 +3,6 @@ import { NavLink } from "react-router-dom"; import { Table, Tbody, Td, Th, Thead, Tr } from "@patternfly/react-table"; -import { SbomsByPackageCount } from "@app/components/SbomsByPackageCount"; import { SimplePagination } from "@app/components/SimplePagination"; import { ConditionalTableBody, @@ -11,6 +10,7 @@ import { TableRowContentWithControls, } from "@app/components/TableControls"; +import { PackageQualifiers } from "@app/components/PackageQualifiers"; import { PackageSearchContext } from "./package-context"; export const PackageTable: React.FC = () => { @@ -37,10 +37,11 @@ export const PackageTable: React.FC = () => { + - - + + @@ -59,17 +60,22 @@ export const PackageTable: React.FC = () => { item={item} rowIndex={rowIndex} > - - + + {item.decomposedPurl ? item.decomposedPurl?.name : item.purl} + {item.decomposedPurl?.namespace} + + @@ -83,11 +89,18 @@ export const PackageTable: React.FC = () => { {item.decomposedPurl?.type} - - + width={10} + modifier="truncate" + {...getTdProps({ columnKey: "path" })} + > + {item.decomposedPurl?.path} + + + {item.decomposedPurl?.qualifiers && ( + + )} From 474020a638bba55dc6e85c36e8cce4f2a5d59ff8 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:23:08 +0100 Subject: [PATCH 03/11] Fix Pamel filter alignment and checkboxes ids --- .../FilterPanel/CheckboxFilterControl.tsx | 2 +- .../components/FilterPanel/FilterPanel.tsx | 32 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/client/src/app/components/FilterPanel/CheckboxFilterControl.tsx b/client/src/app/components/FilterPanel/CheckboxFilterControl.tsx index 4570c3c5..e9fcdead 100644 --- a/client/src/app/components/FilterPanel/CheckboxFilterControl.tsx +++ b/client/src/app/components/FilterPanel/CheckboxFilterControl.tsx @@ -59,7 +59,7 @@ export const CheckboxFilterControl = ({ return ( { filterCategories: FilterCategory[]; @@ -60,18 +60,18 @@ export const FilterPanel = ({ Clear all filters - - {filterCategories - .filter((filterCategory) => { - return ( - omitFilterCategoryKeys.find( - (categoryKey) => categoryKey === filterCategory.categoryKey - ) === undefined - ); - }) - .map((category) => { - return ( - + {filterCategories + .filter((filterCategory) => { + return ( + omitFilterCategoryKeys.find( + (categoryKey) => categoryKey === filterCategory.categoryKey + ) === undefined + ); + }) + .map((category) => { + return ( + + {category.title} @@ -89,9 +89,9 @@ export const FilterPanel = ({ /> - ); - })} - + + ); + })} ); From c5d222cf37d80b0876ed98db56ae6653d11b6b6c Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Wed, 30 Oct 2024 08:35:50 +0100 Subject: [PATCH 04/11] Save dashboard --- client/src/app/pages/home/home.tsx | 284 +++++++++++++++++++++- client/src/app/queries/vulnerabilities.ts | 37 ++- 2 files changed, 318 insertions(+), 3 deletions(-) diff --git a/client/src/app/pages/home/home.tsx b/client/src/app/pages/home/home.tsx index 285af6a7..7a0e4d25 100644 --- a/client/src/app/pages/home/home.tsx +++ b/client/src/app/pages/home/home.tsx @@ -1,5 +1,287 @@ import React from "react"; +import { Link } from "react-router-dom"; + +import { + Chart, + ChartAxis, + ChartBar, + ChartLabel, + ChartLegend, + ChartStack, + ChartThemeColor, + ChartTooltip, +} from "@patternfly/react-charts"; +import { + Bullseye, + Card, + CardBody, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, + PageSection, + Stack, + StackItem, + TextContent, +} from "@patternfly/react-core"; + +import { Severity } from "@app/client"; +import { useFetchSBOMs } from "@app/queries/sboms"; +import { useFetchVulnerabilities } from "@app/queries/vulnerabilities"; +import { formatDate } from "@app/utils/utils"; +import { severityList } from "@app/api/model-utils"; + +interface Legend { + severity: Severity; +} + +const LEGENDS: Legend[] = [ + { severity: "critical" }, + { severity: "high" }, + { severity: "medium" }, + { severity: "low" }, + { severity: "none" }, +]; export const Home: React.FC = () => { - return <>Dashboard here; + const { + result: { data: sboms, total: totalSboms }, + isFetching: isFetchingSboms, + fetchError: fetchErrorSboms, + } = useFetchSBOMs( + { + page: { pageNumber: 1, itemsPerPage: 10 }, + sort: { field: "published", direction: "desc" }, + }, + true + ); + + const { + result: { data: vulnerabilities, total: totalVulnerabilities }, + isFetching: isFetchingVulnerabilities, + fetchError: fetchErrorVulnerabilities, + } = useFetchVulnerabilities( + { + page: { pageNumber: 1, itemsPerPage: 10 }, + sort: { field: "published", direction: "desc" }, + }, + true + ); + + return ( + <> + + + + + Your dashboard + + + + + + + Below is a summary of CVE status for your last 10 + ingested SBOMs. You can click on the SBOM name or CVE + severity number below to be taken to their respective + details page. + + + +
+ { + const severity = severityList[legend.severity]; + return { name: severity.name }; + })} + legendPosition="bottom-left" + height={375} + name="sbom-summary-status" + padding={{ + bottom: 75, + left: 330, + right: 50, + top: 50, + }} + themeColor={ChartThemeColor.multiOrdered} + width={700} + legendComponent={ + { + const severity = + severityList[legend.severity]; + return severity.color.value; + })} + /> + } + > + + } + tickLabelComponent={ + { + // const sbom_name = (event.target as any) + // .innerHTML as string | null; + // const sbom = props.find( + // (item) => item.sbom_name === sbom_name + // ); + // if (sbom) { + // const sbomDetailsPage = `/sbom/content/${sbom.sbom_id}`; + + // const wasmBindings = (window as any) + // .wasmBindings; + // if (wasmBindings) { + // wasmBindings.spogNavigateTo( + // sbomDetailsPage + // ); + // } else { + // window.open(sbomDetailsPage); + // } + // } + // }, + // }} + /> + } + /> + + { + const severity = severityList[legend.severity]; + return severity.color.value; + })} + > + {LEGENDS.map((legend) => { + const severity = severityList[legend.severity]; + return ( + + } + // data={props.map((sbom) => { + // const severityKey = legend.severity; + // const count = sbom.vulnerabilities[ + // severityKey + // ] as number; + // return { + // name: legend.name, + // x: sbom.sbom_name, + // y: count, + // label: `${legend.name}: ${count}`, + // }; + // })} + /> + ); + })} + + +
+
+
+
+ + + + + + + Last SBOM ingested + + + + + {formatDate(sboms?.[0]?.published)} + + + + {sboms?.[0]?.name} + + + + + + + + + + + + Total SBOMs + + + {totalSboms} + + + + + + + + + Last Vulnerability ingested + + + + + {formatDate(vulnerabilities?.[0]?.published)} + + + {vulnerabilities?.[0]?.identifier} + + + + + + + + + + + Total Vulnerabilities + + + {totalVulnerabilities} + + + + + + +
+
+
+
+ + + sdf + + +
+
+ + ); }; diff --git a/client/src/app/queries/vulnerabilities.ts b/client/src/app/queries/vulnerabilities.ts index f0cfc49b..16656ef9 100644 --- a/client/src/app/queries/vulnerabilities.ts +++ b/client/src/app/queries/vulnerabilities.ts @@ -1,7 +1,7 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; +import { useMutation, useQueries, useQuery } from "@tanstack/react-query"; import { AxiosError } from "axios"; -import { HubRequestParams } from "@app/api/models"; +import { HubRequestParams, VulnerabilityStatus } from "@app/api/models"; import { client } from "@app/axios-config/apiInit"; import { deleteVulnerability, @@ -10,6 +10,7 @@ import { VulnerabilityDetails, } from "@app/client"; import { requestParamsQuery } from "@app/hooks/table-controls"; +import { useFetchSbomsAdvisory } from "./sboms"; export const VulnerabilitiesQueryKey = "vulnerabilities"; @@ -65,3 +66,35 @@ export const useDeleteVulnerabilityMutation = ( onError, }); }; + +export const useFetchVulnerabilitiesFromSbom = (sbomId: string) => { + const { + advisories, + isFetching: isFetchingAdvisories, + fetchError: fetchErrorAdvisories, + } = useFetchSbomsAdvisory(sbomId); + + const usersMessages = useQueries({ + queries: (advisories ?? []) + .flatMap((advisory) => { + return (advisory.status ?? []).map((status) => { + const result = { + vulnerabilityId: status.vulnerability_id, + status: status.status as VulnerabilityStatus, + }; + return result; + }); + }) + .map((item) => { + return { + queryKey: ["messages", "id"], + queryFn: () => { + return getVulnerability({ + client, + path: { id: item.vulnerabilityId }, + }); + }, + }; + }), + }); +}; From b709b10d733fe493703d5e43d94937add2e5a6fe Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:17:10 +0100 Subject: [PATCH 05/11] Save 4 donut charts --- client/src/app/api/models.ts | 9 + .../SbomVulnerabilitiesDonutChart.tsx | 59 ++++++ .../domain-controls/useSbomVulnerabilities.ts | 2 +- .../app/pages/home/components/WatchedSbom.tsx | 199 ++++++++++++++++++ .../home/components/WatchedSbomDonutChart.tsx | 26 +++ .../pages/home/components/WatchedSboms.tsx | 52 +++++ client/src/app/pages/home/home.tsx | 23 +- .../app/pages/home/watched-sboms-context.tsx | 61 ++++++ .../sbom-details/vulnerabilities-by-sbom.tsx | 50 +---- client/src/app/queries/dashboard.ts | 65 ++++++ client/src/app/queries/sboms.ts | 8 +- client/src/app/queries/vulnerabilities.ts | 37 +--- 12 files changed, 496 insertions(+), 95 deletions(-) create mode 100644 client/src/app/components/SbomVulnerabilitiesDonutChart.tsx create mode 100644 client/src/app/pages/home/components/WatchedSbom.tsx create mode 100644 client/src/app/pages/home/components/WatchedSbomDonutChart.tsx create mode 100644 client/src/app/pages/home/components/WatchedSboms.tsx create mode 100644 client/src/app/pages/home/watched-sboms-context.tsx create mode 100644 client/src/app/queries/dashboard.ts diff --git a/client/src/app/api/models.ts b/client/src/app/api/models.ts index 4e3e8232..84f96a66 100644 --- a/client/src/app/api/models.ts +++ b/client/src/app/api/models.ts @@ -51,3 +51,12 @@ export interface DecomposedPurl { qualifiers?: Labels; path?: string; } + +// User preferences + +export interface WatchedSboms { + sbom1Id: string | null; + sbom2Id: string | null; + sbom3Id: string | null; + sbom4Id: string | null; +} diff --git a/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx b/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx new file mode 100644 index 00000000..1f317e64 --- /dev/null +++ b/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx @@ -0,0 +1,59 @@ +import React from "react"; + +import { ChartDonut } from "@patternfly/react-charts"; + +import { compareBySeverityFn, severityList } from "@app/api/model-utils"; +import { Severity } from "@app/client"; +import { SbomVulnerabilitySummary } from "@app/hooks/domain-controls/useSbomVulnerabilities"; + +export interface SbomVulnerabilitiesDonutChartProps { + vulnerabilitiesSummary: SbomVulnerabilitySummary; +} + +export const SbomVulnerabilitiesDonutChart: React.FC< + SbomVulnerabilitiesDonutChartProps +> = ({ vulnerabilitiesSummary }) => { + const donutChart = React.useMemo(() => { + return Object.keys(vulnerabilitiesSummary.severities) + .map((item) => { + const severity = item as Severity; + const count = vulnerabilitiesSummary.severities[severity]; + const severityProps = severityList[severity]; + return { + severity, + count, + label: severityProps.name, + color: severityProps.color.value, + }; + }) + .sort(compareBySeverityFn((item) => item.severity)); + }, [vulnerabilitiesSummary]); + + return ( +
+ ({ + name: `${label}: ${count}`, + }))} + data={donutChart.map(({ label, count }) => ({ + x: label, + y: count, + }))} + labels={({ datum }) => `${datum.x}: ${datum.y}`} + colorScale={donutChart.map(({ color }) => color)} + /> +
+ ); +}; diff --git a/client/src/app/hooks/domain-controls/useSbomVulnerabilities.ts b/client/src/app/hooks/domain-controls/useSbomVulnerabilities.ts index d4c8ff0b..2ce26083 100644 --- a/client/src/app/hooks/domain-controls/useSbomVulnerabilities.ts +++ b/client/src/app/hooks/domain-controls/useSbomVulnerabilities.ts @@ -19,7 +19,7 @@ interface SbomVulnerability { vulnerability?: VulnerabilityDetails; } -interface SbomVulnerabilitySummary { +export interface SbomVulnerabilitySummary { total: number; severities: { [key in Severity]: number }; } diff --git a/client/src/app/pages/home/components/WatchedSbom.tsx b/client/src/app/pages/home/components/WatchedSbom.tsx new file mode 100644 index 00000000..1b1dcf6d --- /dev/null +++ b/client/src/app/pages/home/components/WatchedSbom.tsx @@ -0,0 +1,199 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +import { + Button, + Card, + CardBody, + CardFooter, + CardTitle, + EmptyState, + EmptyStateBody, + EmptyStateHeader, + EmptyStateVariant, + MenuToggle, + MenuToggleElement, + Select, + SelectList, + SelectOption, + Stack, + StackItem, + TextInputGroup, + TextInputGroupMain, + TextInputGroupUtilities, +} from "@patternfly/react-core"; +import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; + +import { LoadingWrapper } from "@app/components/LoadingWrapper"; +import { useFetchSBOMById, useFetchSBOMs } from "@app/queries/sboms"; + +import { WatchedSbomsContext } from "../watched-sboms-context"; +import { WatchedSbomDonutChart } from "./WatchedSbomDonutChart"; + +interface WatchedSbomProps { + fieldName: string; + sbomId: string | null; +} + +export const WatchedSbom: React.FC = ({ + fieldName, + sbomId, +}) => { + const { patch } = React.useContext(WatchedSbomsContext); + + const textInputRef = React.useRef(); + const [inputValue, setInputValue] = React.useState(""); + const [debouncedInputValue, setDebouncedInputValue] = React.useState(""); + + React.useEffect(() => { + const delayInputTimeoutId = setTimeout(() => { + setDebouncedInputValue(inputValue); + }, 500); + return () => clearTimeout(delayInputTimeoutId); + }, [inputValue, 500]); + + const [isSelectOpen, setIsSelectOpen] = React.useState(false); + + const { + sbom: currentSbom, + isFetching: isFetchingCurrentSbom, + fetchError: fetchErrorCurrentSbom, + } = useFetchSBOMById(sbomId ?? undefined); + + const { + result: { data: sbomOptions }, + isFetching: isFetchingSbomOptions, + fetchError: fetchErrorSbomOptions, + } = useFetchSBOMs( + { + filters: [{ field: "", operator: "~", value: debouncedInputValue }], + page: { pageNumber: 1, itemsPerPage: 10 }, + }, + true + ); + + const onSelectItem = ( + _event: React.MouseEvent | undefined, + value: string | number | undefined + ) => { + if (value) { + patch(fieldName, value as string); + } + + closeMenu(); + }; + + const closeMenu = () => { + setIsSelectOpen(false); + }; + + const onToggleClick = () => { + setIsSelectOpen(!isSelectOpen); + }; + + const onInputClick = () => { + if (!isSelectOpen) { + setIsSelectOpen(true); + } else if (!inputValue) { + closeMenu(); + } + }; + + const onTextInputChange = ( + _event: React.FormEvent, + value: string + ) => { + setInputValue(value); + }; + + const onClearButtonClick = () => { + setInputValue(""); + textInputRef?.current?.focus(); + }; + + return ( + + + {currentSbom && {currentSbom?.name}} + + {sbomId ? ( + + + + + + View Details + + + ) : ( + + + + You can get started by uploading an SBOM. Once your SBOMs are + uploaded come back to this page to change the SBOMs you would + like to track. + + + )} + + + + + + + ); +}; diff --git a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx new file mode 100644 index 00000000..99a74053 --- /dev/null +++ b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx @@ -0,0 +1,26 @@ +import React from "react"; + + +import { SbomVulnerabilitiesDonutChart } from "@app/components/SbomVulnerabilitiesDonutChart"; +import { useSbomVulnerabilities } from "@app/hooks/domain-controls/useSbomVulnerabilities"; + +interface WatchedSbomDonutChartProps { + sbomId: string; +} + +export const WatchedSbomDonutChart: React.FC = ({ + sbomId, +}) => { + const { + vulnerabilities: vulnerabilities, + summary: vulnerabilitiesSummary, + isFetching: isFetchingVulnerabilities, + fetchError: fetchErrorVulnerabilities, + } = useSbomVulnerabilities(sbomId); + + return ( + + ); +}; diff --git a/client/src/app/pages/home/components/WatchedSboms.tsx b/client/src/app/pages/home/components/WatchedSboms.tsx new file mode 100644 index 00000000..f5c2daa3 --- /dev/null +++ b/client/src/app/pages/home/components/WatchedSboms.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +import { LoadingWrapper } from "@app/components/LoadingWrapper"; +import { + Bullseye, + Card, + CardBody, + Grid, + GridItem, + Spinner, +} from "@patternfly/react-core"; + +import { WatchedSbomsContext } from "../watched-sboms-context"; +import { WatchedSbom } from "./WatchedSbom"; + +export const WatchedSboms: React.FC = () => { + const { sboms, isFetching, fetchError } = + React.useContext(WatchedSbomsContext); + + return ( + + {Array.from(Array(4).keys()).map((_, index) => ( + + + + + + + + + + ))} + + } + > + + {sboms && + Object.entries(sboms).map(([fieldName, fieldValue]) => { + return ( + + + + ); + })} + + + ); +}; diff --git a/client/src/app/pages/home/home.tsx b/client/src/app/pages/home/home.tsx index 7a0e4d25..996d53c0 100644 --- a/client/src/app/pages/home/home.tsx +++ b/client/src/app/pages/home/home.tsx @@ -12,7 +12,6 @@ import { ChartTooltip, } from "@patternfly/react-charts"; import { - Bullseye, Card, CardBody, CardTitle, @@ -28,11 +27,13 @@ import { TextContent, } from "@patternfly/react-core"; +import { severityList } from "@app/api/model-utils"; import { Severity } from "@app/client"; import { useFetchSBOMs } from "@app/queries/sboms"; import { useFetchVulnerabilities } from "@app/queries/vulnerabilities"; import { formatDate } from "@app/utils/utils"; -import { severityList } from "@app/api/model-utils"; +import { WatchedSboms } from "./components/WatchedSboms"; +import { WatchedSbomsProvider } from "./watched-sboms-context"; interface Legend { severity: Severity; @@ -48,9 +49,9 @@ const LEGENDS: Legend[] = [ export const Home: React.FC = () => { const { - result: { data: sboms, total: totalSboms }, - isFetching: isFetchingSboms, - fetchError: fetchErrorSboms, + result: { data: barchartSboms, total: totalSboms }, + isFetching: isFetchingBarchartSboms, + fetchError: fetchErrorBarchartSboms, } = useFetchSBOMs( { page: { pageNumber: 1, itemsPerPage: 10 }, @@ -214,11 +215,11 @@ export const Home: React.FC = () => { - {formatDate(sboms?.[0]?.published)} + {formatDate(barchartSboms?.[0]?.published)} - - {sboms?.[0]?.name} + + {barchartSboms?.[0]?.name} @@ -276,9 +277,9 @@ export const Home: React.FC = () => { - - sdf - + + + diff --git a/client/src/app/pages/home/watched-sboms-context.tsx b/client/src/app/pages/home/watched-sboms-context.tsx new file mode 100644 index 00000000..1cb890ec --- /dev/null +++ b/client/src/app/pages/home/watched-sboms-context.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +import { AxiosError } from "axios"; + +import { WatchedSboms } from "@app/api/models"; +import { NotificationsContext } from "@app/components/NotificationsContext"; +import { + useFetchWatchedSboms, + useUpdateWatchedSbomsMutation, +} from "@app/queries/dashboard"; + +interface IWatchedSbomsContext { + sboms?: WatchedSboms; + isFetching: boolean; + fetchError: AxiosError | null; + + patch: (key: string, value: string) => void; +} + +const contextDefaultValue = {} as IWatchedSbomsContext; + +export const WatchedSbomsContext = + React.createContext(contextDefaultValue); + +interface IWatchedSbomsProvider { + children: React.ReactNode; +} + +export const WatchedSbomsProvider: React.FunctionComponent< + IWatchedSbomsProvider +> = ({ children }) => { + const { sboms, isFetching, fetchError } = useFetchWatchedSboms(); + + const { pushNotification } = React.useContext(NotificationsContext); + + const onUpdateSuccess = () => {}; + const onUpdateError = (_error: AxiosError) => { + pushNotification({ + title: "Error while updating the user preferences", + variant: "danger", + }); + }; + + const { mutate: updateSboms } = useUpdateWatchedSbomsMutation( + onUpdateSuccess, + onUpdateError + ); + + const patch = (key: string, value: string) => { + const newSboms = { ...sboms, [key]: value }; + updateSboms(newSboms as WatchedSboms); + }; + + return ( + + {children} + + ); +}; diff --git a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx index dd7c5424..0c497d28 100644 --- a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx +++ b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Link } from "react-router-dom"; -import { ChartDonut } from "@patternfly/react-charts"; import { Card, CardBody, @@ -27,10 +26,9 @@ import { Tr, } from "@patternfly/react-table"; -import { compareBySeverityFn, severityList } from "@app/api/model-utils"; -import { Severity } from "@app/client"; import { LoadingWrapper } from "@app/components/LoadingWrapper"; import { PackageQualifiers } from "@app/components/PackageQualifiers"; +import { SbomVulnerabilitiesDonutChart } from "@app/components/SbomVulnerabilitiesDonutChart"; import { SeverityShieldAndText } from "@app/components/SeverityShieldAndText"; import { SimplePagination } from "@app/components/SimplePagination"; import { @@ -107,24 +105,6 @@ export const VulnerabilitiesBySbom: React.FC = ({ expansionDerivedState: { isCellExpanded }, } = tableControls; - // - - const donutChart = React.useMemo(() => { - return Object.keys(vulnerabilitiesSummary.severities) - .map((item) => { - const severity = item as Severity; - const count = vulnerabilitiesSummary.severities[severity]; - const severityProps = severityList[severity]; - return { - severity, - count, - label: severityProps.name, - color: severityProps.color.value, - }; - }) - .sort(compareBySeverityFn((item) => item.severity)); - }, [vulnerabilitiesSummary]); - return ( <> @@ -136,31 +116,9 @@ export const VulnerabilitiesBySbom: React.FC = ({ > -
- ({ - name: `${label}: ${count}`, - }))} - data={donutChart.map(({ label, count }) => ({ - x: label, - y: count, - }))} - labels={({ datum }) => `${datum.x}: ${datum.y}`} - colorScale={donutChart.map(({ color }) => color)} - /> -
+
diff --git a/client/src/app/queries/dashboard.ts b/client/src/app/queries/dashboard.ts new file mode 100644 index 00000000..e62329db --- /dev/null +++ b/client/src/app/queries/dashboard.ts @@ -0,0 +1,65 @@ +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { AxiosError } from "axios"; + +import { WatchedSboms } from "@app/api/models"; +import { client } from "@app/axios-config/apiInit"; +import { getUserPreferences, setUserPreferences } from "@app/client"; + +const WATCHED_SBOMS_KEY = "watched-sboms"; + +export const DashboardQueryKey = "dashboard"; + +export const useFetchWatchedSboms = () => { + const { data, isLoading, error } = useQuery({ + queryKey: [DashboardQueryKey, WATCHED_SBOMS_KEY], + queryFn: async () => { + try { + const response = await getUserPreferences({ + client, + path: { key: WATCHED_SBOMS_KEY }, + }); + return response.data as WatchedSboms; + } catch (error) { + const axiosError = error as AxiosError; + if (axiosError.status === 404) { + const defaultValue: WatchedSboms = { + sbom1Id: null, + sbom2Id: null, + sbom3Id: null, + sbom4Id: null, + }; + return defaultValue; + } + } + }, + }); + + return { + sboms: data, + isFetching: isLoading, + fetchError: error as AxiosError | null, + }; +}; + +export const useUpdateWatchedSbomsMutation = ( + onSuccess: () => void, + onError: (err: AxiosError, payload: WatchedSboms) => void +) => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (obj) => { + return setUserPreferences({ + client, + path: { key: WATCHED_SBOMS_KEY }, + body: obj, + }); + }, + onSuccess: (_res, _payload) => { + onSuccess(); + queryClient.invalidateQueries({ + queryKey: [DashboardQueryKey, WATCHED_SBOMS_KEY], + }); + }, + onError: onError, + }); +}; diff --git a/client/src/app/queries/sboms.ts b/client/src/app/queries/sboms.ts index 7b8d14bc..4028d7e2 100644 --- a/client/src/app/queries/sboms.ts +++ b/client/src/app/queries/sboms.ts @@ -46,10 +46,14 @@ export const useFetchSBOMs = ( }; }; -export const useFetchSBOMById = (id: string) => { +export const useFetchSBOMById = (id?: string) => { const { data, isLoading, error } = useQuery({ queryKey: [SBOMsQueryKey, id], - queryFn: () => getSbom({ client, path: { id } }), + queryFn: () => { + return id === undefined + ? Promise.resolve(undefined) + : getSbom({ client, path: { id: id! } }); + }, enabled: id !== undefined, }); diff --git a/client/src/app/queries/vulnerabilities.ts b/client/src/app/queries/vulnerabilities.ts index 16656ef9..f0cfc49b 100644 --- a/client/src/app/queries/vulnerabilities.ts +++ b/client/src/app/queries/vulnerabilities.ts @@ -1,7 +1,7 @@ -import { useMutation, useQueries, useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import { AxiosError } from "axios"; -import { HubRequestParams, VulnerabilityStatus } from "@app/api/models"; +import { HubRequestParams } from "@app/api/models"; import { client } from "@app/axios-config/apiInit"; import { deleteVulnerability, @@ -10,7 +10,6 @@ import { VulnerabilityDetails, } from "@app/client"; import { requestParamsQuery } from "@app/hooks/table-controls"; -import { useFetchSbomsAdvisory } from "./sboms"; export const VulnerabilitiesQueryKey = "vulnerabilities"; @@ -66,35 +65,3 @@ export const useDeleteVulnerabilityMutation = ( onError, }); }; - -export const useFetchVulnerabilitiesFromSbom = (sbomId: string) => { - const { - advisories, - isFetching: isFetchingAdvisories, - fetchError: fetchErrorAdvisories, - } = useFetchSbomsAdvisory(sbomId); - - const usersMessages = useQueries({ - queries: (advisories ?? []) - .flatMap((advisory) => { - return (advisory.status ?? []).map((status) => { - const result = { - vulnerabilityId: status.vulnerability_id, - status: status.status as VulnerabilityStatus, - }; - return result; - }); - }) - .map((item) => { - return { - queryKey: ["messages", "id"], - queryFn: () => { - return getVulnerability({ - client, - path: { id: item.vulnerabilityId }, - }); - }, - }; - }), - }); -}; From 9a439db94b6e95e571ba0ae26ee653008a45f015 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:22:03 +0100 Subject: [PATCH 06/11] Move code to sub components --- .../home/components/MonitoringSection.tsx | 266 +++++++++++++++++ .../home/components/WatchedSbomDonutChart.tsx | 1 - ...tchedSboms.tsx => WatchedSbomsSection.tsx} | 2 +- client/src/app/pages/home/home.tsx | 272 +----------------- 4 files changed, 272 insertions(+), 269 deletions(-) create mode 100644 client/src/app/pages/home/components/MonitoringSection.tsx rename client/src/app/pages/home/components/{WatchedSboms.tsx => WatchedSbomsSection.tsx} (95%) diff --git a/client/src/app/pages/home/components/MonitoringSection.tsx b/client/src/app/pages/home/components/MonitoringSection.tsx new file mode 100644 index 00000000..fe564d13 --- /dev/null +++ b/client/src/app/pages/home/components/MonitoringSection.tsx @@ -0,0 +1,266 @@ +import React from "react"; +import { Link } from "react-router-dom"; + +import { + Chart, + ChartAxis, + ChartBar, + ChartLabel, + ChartLegend, + ChartStack, + ChartThemeColor, + ChartTooltip, +} from "@patternfly/react-charts"; +import { + Card, + CardBody, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, + Stack, + StackItem, + TextContent, +} from "@patternfly/react-core"; + +import { severityList } from "@app/api/model-utils"; +import { Severity } from "@app/client"; +import { useFetchSBOMs } from "@app/queries/sboms"; +import { useFetchVulnerabilities } from "@app/queries/vulnerabilities"; +import { formatDate } from "@app/utils/utils"; + +interface Legend { + severity: Severity; +} + +const LEGENDS: Legend[] = [ + { severity: "critical" }, + { severity: "high" }, + { severity: "medium" }, + { severity: "low" }, + { severity: "none" }, +]; + +export const MonitoringSection: React.FC = () => { + const { + result: { data: barchartSboms, total: totalSboms }, + isFetching: isFetchingBarchartSboms, + fetchError: fetchErrorBarchartSboms, + } = useFetchSBOMs( + { + page: { pageNumber: 1, itemsPerPage: 10 }, + sort: { field: "published", direction: "desc" }, + }, + true + ); + + const { + result: { data: vulnerabilities, total: totalVulnerabilities }, + isFetching: isFetchingVulnerabilities, + fetchError: fetchErrorVulnerabilities, + } = useFetchVulnerabilities( + { + page: { pageNumber: 1, itemsPerPage: 10 }, + sort: { field: "published", direction: "desc" }, + }, + true + ); + + return ( + + Your dashboard + + + + + + + Below is a summary of CVE status for your last 10 ingested + SBOMs. You can click on the SBOM name or CVE severity number + below to be taken to their respective details page. + + + +
+ { + const severity = severityList[legend.severity]; + return { name: severity.name }; + })} + legendPosition="bottom-left" + height={375} + name="sbom-summary-status" + padding={{ + bottom: 75, + left: 330, + right: 50, + top: 50, + }} + themeColor={ChartThemeColor.multiOrdered} + width={700} + legendComponent={ + { + const severity = severityList[legend.severity]; + return severity.color.value; + })} + /> + } + > + } + tickLabelComponent={ + { + // const sbom_name = (event.target as any) + // .innerHTML as string | null; + // const sbom = props.find( + // (item) => item.sbom_name === sbom_name + // ); + // if (sbom) { + // const sbomDetailsPage = `/sbom/content/${sbom.sbom_id}`; + + // const wasmBindings = (window as any) + // .wasmBindings; + // if (wasmBindings) { + // wasmBindings.spogNavigateTo( + // sbomDetailsPage + // ); + // } else { + // window.open(sbomDetailsPage); + // } + // } + // }, + // }} + /> + } + /> + + { + const severity = severityList[legend.severity]; + return severity.color.value; + })} + > + {LEGENDS.map((legend) => { + const severity = severityList[legend.severity]; + return ( + + } + // data={props.map((sbom) => { + // const severityKey = legend.severity; + // const count = sbom.vulnerabilities[ + // severityKey + // ] as number; + // return { + // name: legend.name, + // x: sbom.sbom_name, + // y: count, + // label: `${legend.name}: ${count}`, + // }; + // })} + /> + ); + })} + + +
+
+
+
+ + + + + + + Last SBOM ingested + + + + + {formatDate(barchartSboms?.[0]?.published)} + + + + {barchartSboms?.[0]?.name} + + + + + + + + + + + Total SBOMs + + {totalSboms} + + + + + + + + + Last Vulnerability ingested + + + + + {formatDate(vulnerabilities?.[0]?.published)} + + + {vulnerabilities?.[0]?.identifier} + + + + + + + + + + + Total Vulnerabilities + + + {totalVulnerabilities} + + + + + + +
+
+
+ ); +}; diff --git a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx index 99a74053..497bd813 100644 --- a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx +++ b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx @@ -1,6 +1,5 @@ import React from "react"; - import { SbomVulnerabilitiesDonutChart } from "@app/components/SbomVulnerabilitiesDonutChart"; import { useSbomVulnerabilities } from "@app/hooks/domain-controls/useSbomVulnerabilities"; diff --git a/client/src/app/pages/home/components/WatchedSboms.tsx b/client/src/app/pages/home/components/WatchedSbomsSection.tsx similarity index 95% rename from client/src/app/pages/home/components/WatchedSboms.tsx rename to client/src/app/pages/home/components/WatchedSbomsSection.tsx index f5c2daa3..bc88c661 100644 --- a/client/src/app/pages/home/components/WatchedSboms.tsx +++ b/client/src/app/pages/home/components/WatchedSbomsSection.tsx @@ -13,7 +13,7 @@ import { import { WatchedSbomsContext } from "../watched-sboms-context"; import { WatchedSbom } from "./WatchedSbom"; -export const WatchedSboms: React.FC = () => { +export const WatchedSbomsSection: React.FC = () => { const { sboms, isFetching, fetchError } = React.useContext(WatchedSbomsContext); diff --git a/client/src/app/pages/home/home.tsx b/client/src/app/pages/home/home.tsx index 996d53c0..1cc5f979 100644 --- a/client/src/app/pages/home/home.tsx +++ b/client/src/app/pages/home/home.tsx @@ -1,284 +1,22 @@ import React from "react"; -import { Link } from "react-router-dom"; -import { - Chart, - ChartAxis, - ChartBar, - ChartLabel, - ChartLegend, - ChartStack, - ChartThemeColor, - ChartTooltip, -} from "@patternfly/react-charts"; -import { - Card, - CardBody, - CardTitle, - DescriptionList, - DescriptionListDescription, - DescriptionListGroup, - DescriptionListTerm, - Grid, - GridItem, - PageSection, - Stack, - StackItem, - TextContent, -} from "@patternfly/react-core"; +import { PageSection, Stack, StackItem } from "@patternfly/react-core"; -import { severityList } from "@app/api/model-utils"; -import { Severity } from "@app/client"; -import { useFetchSBOMs } from "@app/queries/sboms"; -import { useFetchVulnerabilities } from "@app/queries/vulnerabilities"; -import { formatDate } from "@app/utils/utils"; -import { WatchedSboms } from "./components/WatchedSboms"; +import { MonitoringSection } from "./components/MonitoringSection"; +import { WatchedSbomsSection } from "./components/WatchedSbomsSection"; import { WatchedSbomsProvider } from "./watched-sboms-context"; -interface Legend { - severity: Severity; -} - -const LEGENDS: Legend[] = [ - { severity: "critical" }, - { severity: "high" }, - { severity: "medium" }, - { severity: "low" }, - { severity: "none" }, -]; - export const Home: React.FC = () => { - const { - result: { data: barchartSboms, total: totalSboms }, - isFetching: isFetchingBarchartSboms, - fetchError: fetchErrorBarchartSboms, - } = useFetchSBOMs( - { - page: { pageNumber: 1, itemsPerPage: 10 }, - sort: { field: "published", direction: "desc" }, - }, - true - ); - - const { - result: { data: vulnerabilities, total: totalVulnerabilities }, - isFetching: isFetchingVulnerabilities, - fetchError: fetchErrorVulnerabilities, - } = useFetchVulnerabilities( - { - page: { pageNumber: 1, itemsPerPage: 10 }, - sort: { field: "published", direction: "desc" }, - }, - true - ); - return ( <> - - Your dashboard - - - - - - - Below is a summary of CVE status for your last 10 - ingested SBOMs. You can click on the SBOM name or CVE - severity number below to be taken to their respective - details page. - - - -
- { - const severity = severityList[legend.severity]; - return { name: severity.name }; - })} - legendPosition="bottom-left" - height={375} - name="sbom-summary-status" - padding={{ - bottom: 75, - left: 330, - right: 50, - top: 50, - }} - themeColor={ChartThemeColor.multiOrdered} - width={700} - legendComponent={ - { - const severity = - severityList[legend.severity]; - return severity.color.value; - })} - /> - } - > - - } - tickLabelComponent={ - { - // const sbom_name = (event.target as any) - // .innerHTML as string | null; - // const sbom = props.find( - // (item) => item.sbom_name === sbom_name - // ); - // if (sbom) { - // const sbomDetailsPage = `/sbom/content/${sbom.sbom_id}`; - - // const wasmBindings = (window as any) - // .wasmBindings; - // if (wasmBindings) { - // wasmBindings.spogNavigateTo( - // sbomDetailsPage - // ); - // } else { - // window.open(sbomDetailsPage); - // } - // } - // }, - // }} - /> - } - /> - - { - const severity = severityList[legend.severity]; - return severity.color.value; - })} - > - {LEGENDS.map((legend) => { - const severity = severityList[legend.severity]; - return ( - - } - // data={props.map((sbom) => { - // const severityKey = legend.severity; - // const count = sbom.vulnerabilities[ - // severityKey - // ] as number; - // return { - // name: legend.name, - // x: sbom.sbom_name, - // y: count, - // label: `${legend.name}: ${count}`, - // }; - // })} - /> - ); - })} - - -
-
-
-
- - - - - - - Last SBOM ingested - - - - - {formatDate(barchartSboms?.[0]?.published)} - - - - {barchartSboms?.[0]?.name} - - - - - - - - - - - - Total SBOMs - - - {totalSboms} - - - - - - - - - Last Vulnerability ingested - - - - - {formatDate(vulnerabilities?.[0]?.published)} - - - {vulnerabilities?.[0]?.identifier} - - - - - - - - - - - Total Vulnerabilities - - - {totalVulnerabilities} - - - - - - -
-
-
+
- +
From 39815241c0d769452d25437760364e3c807d680d Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:00:55 +0100 Subject: [PATCH 07/11] fix imports --- client/src/app/components/SbomVulnerabilitiesDonutChart.tsx | 4 ++-- .../src/app/pages/home/components/WatchedSbomDonutChart.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx b/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx index 1f317e64..72322d9a 100644 --- a/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx +++ b/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx @@ -4,10 +4,10 @@ import { ChartDonut } from "@patternfly/react-charts"; import { compareBySeverityFn, severityList } from "@app/api/model-utils"; import { Severity } from "@app/client"; -import { SbomVulnerabilitySummary } from "@app/hooks/domain-controls/useSbomVulnerabilities"; +import { VulnerabilityOfSbomSummary } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; export interface SbomVulnerabilitiesDonutChartProps { - vulnerabilitiesSummary: SbomVulnerabilitySummary; + vulnerabilitiesSummary: VulnerabilityOfSbomSummary; } export const SbomVulnerabilitiesDonutChart: React.FC< diff --git a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx index 497bd813..3d591ce5 100644 --- a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx +++ b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx @@ -1,7 +1,7 @@ import React from "react"; import { SbomVulnerabilitiesDonutChart } from "@app/components/SbomVulnerabilitiesDonutChart"; -import { useSbomVulnerabilities } from "@app/hooks/domain-controls/useSbomVulnerabilities"; +import { useVulnerabilitiesOfSbom } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; interface WatchedSbomDonutChartProps { sbomId: string; @@ -15,7 +15,7 @@ export const WatchedSbomDonutChart: React.FC = ({ summary: vulnerabilitiesSummary, isFetching: isFetchingVulnerabilities, fetchError: fetchErrorVulnerabilities, - } = useSbomVulnerabilities(sbomId); + } = useVulnerabilitiesOfSbom(sbomId); return ( Date: Thu, 21 Nov 2024 11:07:00 +0100 Subject: [PATCH 08/11] Based on https://github.com/trustification/trustify/pull/1005 --- client/openapi/trustd.yaml | 27 +-- client/package.json | 4 +- client/src/app/client/schemas.gen.ts | 45 ++--- client/src/app/client/types.gen.ts | 8 +- .../useVulnerabilitiesOfSbom.ts | 164 ++++++++++-------- .../home/components/MonitoringSection.tsx | 28 +-- .../src/app/pages/sbom-details/overview.tsx | 4 +- .../sbom-details/vulnerabilities-by-sbom.tsx | 6 +- client/src/app/queries/organizations.ts | 48 ----- client/src/app/queries/products.ts | 68 -------- client/src/app/queries/sboms.ts | 33 +++- package-lock.json | 26 +-- 12 files changed, 184 insertions(+), 277 deletions(-) delete mode 100644 client/src/app/queries/organizations.ts delete mode 100644 client/src/app/queries/products.ts diff --git a/client/openapi/trustd.yaml b/client/openapi/trustd.yaml index 027059c1..b14f2edd 100644 --- a/client/openapi/trustd.yaml +++ b/client/openapi/trustd.yaml @@ -2245,7 +2245,6 @@ components: - node_id - purl - name - - version properties: name: type: string @@ -2255,8 +2254,6 @@ components: type: string sbom_id: type: string - version: - type: string AncestorSummary: type: object required: @@ -2264,7 +2261,6 @@ components: - node_id - purl - name - - version - published - document_id - product_name @@ -2291,8 +2287,6 @@ components: type: string sbom_id: type: string - version: - type: string BasePurlDetails: allOf: - $ref: '#/components/schemas/BasePurlHead' @@ -2457,10 +2451,8 @@ components: required: - sbom_id - node_id - - relationship - purl - name - - version - deps properties: deps: @@ -2473,12 +2465,8 @@ components: type: string purl: type: string - relationship: - type: string sbom_id: type: string - version: - type: string DepSummary: type: object required: @@ -2486,7 +2474,6 @@ components: - node_id - purl - name - - version - published - document_id - product_name @@ -2513,8 +2500,6 @@ components: type: string sbom_id: type: string - version: - type: string Id: type: string description: A hash/digest prefixed with its type. @@ -3517,7 +3502,7 @@ components: SbomStatus: type: object required: - - vulnerability_id + - vulnerability - status - packages properties: @@ -3529,10 +3514,14 @@ components: type: array items: $ref: '#/components/schemas/SbomPackage' + severity: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/Severity' status: type: string - vulnerability_id: - type: string + vulnerability: + $ref: '#/components/schemas/VulnerabilityHead' SbomSummary: allOf: - $ref: '#/components/schemas/SbomHead' @@ -3931,4 +3920,4 @@ components: oneOf: - type: 'null' - $ref: '#/components/schemas/Severity' - description: Average (arithmetic mean) severity of the vulnerability aggregated from *all* related advisories. + description: Average (arithmetic mean) severity of the vulnerability aggregated from *all* related advisories. \ No newline at end of file diff --git a/client/package.json b/client/package.json index bf8af438..e160ca7f 100644 --- a/client/package.json +++ b/client/package.json @@ -30,8 +30,8 @@ "@patternfly/react-table": "^5.4.0", "@patternfly/react-tokens": "^5.4.0", "@segment/analytics-next": "^1.64.0", - "@tanstack/react-query": "^5.50.1", - "@tanstack/react-query-devtools": "^5.50.1", + "@tanstack/react-query": "^5.61.0", + "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.7.2", "dayjs": "^1.11.7", "ejs": "^3.1.10", diff --git a/client/src/app/client/schemas.gen.ts b/client/src/app/client/schemas.gen.ts index 3f53d01f..1813f0a7 100644 --- a/client/src/app/client/schemas.gen.ts +++ b/client/src/app/client/schemas.gen.ts @@ -276,7 +276,7 @@ export const AnalysisStatusSchema = { export const AncNodeSchema = { type: "object", - required: ["sbom_id", "node_id", "purl", "name", "version"], + required: ["sbom_id", "node_id", "purl", "name"], properties: { name: { type: "string", @@ -290,9 +290,6 @@ export const AncNodeSchema = { sbom_id: { type: "string", }, - version: { - type: "string", - }, }, } as const; @@ -303,7 +300,6 @@ export const AncestorSummarySchema = { "node_id", "purl", "name", - "version", "published", "document_id", "product_name", @@ -341,9 +337,6 @@ export const AncestorSummarySchema = { sbom_id: { type: "string", }, - version: { - type: "string", - }, }, } as const; @@ -594,15 +587,7 @@ export const CweImporterSchema = { export const DepNodeSchema = { type: "object", - required: [ - "sbom_id", - "node_id", - "relationship", - "purl", - "name", - "version", - "deps", - ], + required: ["sbom_id", "node_id", "purl", "name", "deps"], properties: { deps: { type: "array", @@ -619,15 +604,9 @@ export const DepNodeSchema = { purl: { type: "string", }, - relationship: { - type: "string", - }, sbom_id: { type: "string", }, - version: { - type: "string", - }, }, } as const; @@ -638,7 +617,6 @@ export const DepSummarySchema = { "node_id", "purl", "name", - "version", "published", "document_id", "product_name", @@ -676,9 +654,6 @@ export const DepSummarySchema = { sbom_id: { type: "string", }, - version: { - type: "string", - }, }, } as const; @@ -2078,7 +2053,7 @@ export const SbomPackageRelationSchema = { export const SbomStatusSchema = { type: "object", - required: ["vulnerability_id", "status", "packages"], + required: ["vulnerability", "status", "packages"], properties: { context: { oneOf: [ @@ -2096,11 +2071,21 @@ export const SbomStatusSchema = { $ref: "#/components/schemas/SbomPackage", }, }, + severity: { + oneOf: [ + { + type: "null", + }, + { + $ref: "#/components/schemas/Severity", + }, + ], + }, status: { type: "string", }, - vulnerability_id: { - type: "string", + vulnerability: { + $ref: "#/components/schemas/VulnerabilityHead", }, }, } as const; diff --git a/client/src/app/client/types.gen.ts b/client/src/app/client/types.gen.ts index 0eaa7805..1f48ea13 100644 --- a/client/src/app/client/types.gen.ts +++ b/client/src/app/client/types.gen.ts @@ -120,7 +120,6 @@ export type AncNode = { node_id: string; purl: string; sbom_id: string; - version: string; }; export type AncestorSummary = { @@ -133,7 +132,6 @@ export type AncestorSummary = { published: string; purl: string; sbom_id: string; - version: string; }; export type BasePurlDetails = BasePurlHead & { @@ -232,9 +230,7 @@ export type DepNode = { name: string; node_id: string; purl: string; - relationship: string; sbom_id: string; - version: string; }; export type DepSummary = { @@ -247,7 +243,6 @@ export type DepSummary = { published: string; purl: string; sbom_id: string; - version: string; }; /** @@ -770,8 +765,9 @@ export type SbomPackageRelation = { export type SbomStatus = { context?: null | StatusContext; packages: Array; + severity?: null | Severity; status: string; - vulnerability_id: string; + vulnerability: VulnerabilityHead; }; export type SbomSummary = SbomHead & diff --git a/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts b/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts index 51b1f5d6..ce151b37 100644 --- a/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts +++ b/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts @@ -1,22 +1,24 @@ import React from "react"; import { VulnerabilityStatus } from "@app/api/models"; -import { client } from "@app/axios-config/apiInit"; import { - getVulnerability, SbomAdvisory, SbomPackage, Severity, - VulnerabilityDetails, + VulnerabilityHead, } from "@app/client"; -import { useFetchSbomsAdvisory } from "@app/queries/sboms"; +import { + useFetchSbomsAdvisory, + useFetchSbomsAdvisory2, +} from "@app/queries/sboms"; interface VulnerabilityOfSbom { - vulnerabilityId: string; advisory: SbomAdvisory; + vulnerabilityId: string; + vulnerability: VulnerabilityHead; + severity?: Severity | null; status: VulnerabilityStatus; packages: SbomPackage[]; - vulnerability?: VulnerabilityDetails; } export interface VulnerabilityOfSbomSummary { @@ -36,28 +38,17 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => { fetchError: fetchErrorAdvisories, } = useFetchSbomsAdvisory(sbomId); - const [allVulnerabilities, setAllVulnerabilities] = React.useState< - VulnerabilityOfSbom[] - >([]); - const [vulnerabilitiesById, setVulnerabilitiesById] = React.useState< - Map - >(new Map()); - const [isFetchingVulnerabilities, setIsFetchingVulnerabilities] = - React.useState(false); - - React.useEffect(() => { - if (advisories.length === 0) { - return; - } - + const allVulnerabilities = React.useMemo(() => { const vulnerabilities = (advisories ?? []) .flatMap((advisory) => { return (advisory.status ?? []).map((status) => { const result: VulnerabilityOfSbom = { - vulnerabilityId: status.vulnerability_id, + advisory: advisory, + vulnerabilityId: status.vulnerability.identifier, + vulnerability: status.vulnerability, + severity: status.severity, status: status.status as VulnerabilityStatus, packages: status.packages || [], - advisory: { ...advisory }, }; return result; }); @@ -68,7 +59,8 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => { .reduce((prev, current) => { const exists = prev.find( (item) => - item.vulnerabilityId === current.vulnerabilityId && + item.vulnerability.identifier === + current.vulnerability.identifier && item.advisory.uuid === current.advisory.uuid ); if (!exists) { @@ -78,55 +70,15 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => { } }, [] as VulnerabilityOfSbom[]); - 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(); - validVulnerabilities.forEach((vulnerability) => { - vulnerabilitiesById.set(vulnerability.identifier, vulnerability); - }); - - setVulnerabilitiesById(vulnerabilitiesById); - setIsFetchingVulnerabilities(false); - }); + return vulnerabilities; }, [advisories]); - const allVulnerabilitiesWithMappedData = React.useMemo(() => { - return allVulnerabilities.map((item) => { - const result: VulnerabilityOfSbom = { - ...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 allVulnerabilities.reduce((prev, current) => { + if (current.severity) { + const severity = current.severity; return { ...prev, total: prev.total + 1, @@ -139,12 +91,84 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => { return prev; } }, DEFAULT_SUMMARY); - }, [allVulnerabilitiesWithMappedData]); + }, [allVulnerabilities]); return { - isFetching: isFetchingAdvisories || isFetchingVulnerabilities, + isFetching: isFetchingAdvisories, fetchError: fetchErrorAdvisories, - vulnerabilities: allVulnerabilitiesWithMappedData, + vulnerabilities: allVulnerabilities, + summary: vulnerabilitiesSummary, + }; +}; + +export const useVulnerabilitiesOfSboms = (sbomIds: string[]) => { + const { advisories, isFetching, fetchError } = + useFetchSbomsAdvisory2(sbomIds); + + const allVulnerabilities = React.useMemo(() => { + const vulnerabilities = (advisories ?? []).map((advisories) => { + return ( + advisories + .flatMap((advisory) => { + return (advisory.status ?? []).map((status) => { + const result: VulnerabilityOfSbom = { + advisory: advisory, + vulnerabilityId: status.vulnerability.identifier, + vulnerability: status.vulnerability, + severity: status.severity, + status: status.status as VulnerabilityStatus, + packages: status.packages || [], + }; + return result; + }); + }) + // Take only "affected" + .filter((item) => item.status === "affected") + // Remove duplicates if exists + .reduce((prev, current) => { + const exists = prev.find( + (item) => + item.vulnerability.identifier === + current.vulnerability.identifier && + item.advisory.uuid === current.advisory.uuid + ); + if (!exists) { + return [...prev, current]; + } else { + return prev; + } + }, [] as VulnerabilityOfSbom[]) + ); + }); + return vulnerabilities; + }, [advisories]); + + // Summary + + const vulnerabilitiesSummary = React.useMemo(() => { + return allVulnerabilities.map((allVulnerabilities) => { + return allVulnerabilities.reduce((prev, current) => { + if (current.severity) { + const severity = current.severity; + return { + ...prev, + total: prev.total + 1, + severities: { + ...prev.severities, + [severity]: prev.severities[severity] + 1, + }, + }; + } else { + return prev; + } + }, DEFAULT_SUMMARY); + }); + }, [allVulnerabilities]); + + return { + isFetching: isFetching, + fetchError: fetchError, + vulnerabilities: allVulnerabilities, summary: vulnerabilitiesSummary, }; }; diff --git a/client/src/app/pages/home/components/MonitoringSection.tsx b/client/src/app/pages/home/components/MonitoringSection.tsx index fe564d13..1a323909 100644 --- a/client/src/app/pages/home/components/MonitoringSection.tsx +++ b/client/src/app/pages/home/components/MonitoringSection.tsx @@ -28,6 +28,7 @@ import { import { severityList } from "@app/api/model-utils"; import { Severity } from "@app/client"; +import { useVulnerabilitiesOfSboms } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; import { useFetchSBOMs } from "@app/queries/sboms"; import { useFetchVulnerabilities } from "@app/queries/vulnerabilities"; import { formatDate } from "@app/utils/utils"; @@ -69,6 +70,10 @@ export const MonitoringSection: React.FC = () => { true ); + const { summary, isFetching, fetchError } = useVulnerabilitiesOfSboms( + barchartSboms.map((e) => e.id) + ); + return ( Your dashboard @@ -172,18 +177,17 @@ export const MonitoringSection: React.FC = () => { labelComponent={ } - // data={props.map((sbom) => { - // const severityKey = legend.severity; - // const count = sbom.vulnerabilities[ - // severityKey - // ] as number; - // return { - // name: legend.name, - // x: sbom.sbom_name, - // y: count, - // label: `${legend.name}: ${count}`, - // }; - // })} + data={summary.map((item, index) => { + const sbom = barchartSboms[index]; + const severityKey = legend.severity; + const count = item.severities[severityKey]; + return { + name: severityKey, + x: sbom.name, + y: count, + label: `${severity.name}: ${count}`, + }; + })} /> ); })} diff --git a/client/src/app/pages/sbom-details/overview.tsx b/client/src/app/pages/sbom-details/overview.tsx index 5f93b278..b073faf3 100644 --- a/client/src/app/pages/sbom-details/overview.tsx +++ b/client/src/app/pages/sbom-details/overview.tsx @@ -118,8 +118,8 @@ export const Overview: React.FC = ({ sbom }) => { {sbom.described_by .flatMap((e) => e.cpe) - .map((e) => ( - {e} + .map((e, index) => ( + {e} ))} {sbom.described_by diff --git a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx index ca4720f7..a13de9ab 100644 --- a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx +++ b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx @@ -211,10 +211,8 @@ export const VulnerabilitiesBySbom: React.FC = ({ )} - {item.vulnerability?.average_severity && ( - + {item.severity && ( + )} { - const { data, isLoading, error, refetch } = useQuery({ - queryKey: [OrganizationsQueryKey, params], - queryFn: () => { - return listOrganizations({ - client, - query: { ...requestParamsQuery(params) }, - }); - }, - refetchInterval: !refetchDisabled ? 5000 : false, - }); - return { - result: { - data: data?.data?.items || [], - total: data?.data?.total ?? 0, - params: params, - }, - isFetching: isLoading, - fetchError: error, - refetch, - }; -}; - -export const useFetchOrganizationById = (id: string) => { - const { data, isLoading, error } = useQuery({ - queryKey: [OrganizationsQueryKey, id], - queryFn: () => getOrganization({ client, path: { id } }), - }); - return { - organization: data?.data, - isFetching: isLoading, - fetchError: error as AxiosError, - }; -}; diff --git a/client/src/app/queries/products.ts b/client/src/app/queries/products.ts deleted file mode 100644 index 66e8cc99..00000000 --- a/client/src/app/queries/products.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useMutation, useQuery } from "@tanstack/react-query"; -import { AxiosError } from "axios"; - -import { HubRequestParams } from "@app/api/models"; -import { client } from "@app/axios-config/apiInit"; -import { - deleteProduct, - getProduct, - listProducts, - ProductDetails, -} from "@app/client"; - -import { requestParamsQuery } from "../hooks/table-controls"; - -export const ProductsQueryKey = "products"; - -export const useFetchProducts = ( - params: HubRequestParams = {}, - refetchDisabled: boolean = false -) => { - const { data, isLoading, error, refetch } = useQuery({ - queryKey: [ProductsQueryKey, params], - queryFn: () => { - return listProducts({ - client, - query: { ...requestParamsQuery(params) }, - }); - }, - refetchInterval: !refetchDisabled ? 5000 : false, - }); - return { - result: { - data: data?.data?.items || [], - total: data?.data?.total ?? 0, - params: params, - }, - isFetching: isLoading, - fetchError: error, - refetch, - }; -}; - -export const useFetchProductById = (id: string) => { - const { data, isLoading, error } = useQuery({ - queryKey: [ProductsQueryKey, id], - queryFn: () => getProduct({ client, path: { id } }), - }); - return { - product: data?.data, - isFetching: isLoading, - fetchError: error as AxiosError, - }; -}; - -export const useDeleteProductMutation = ( - onError?: (err: AxiosError, id: string) => void, - onSuccess?: (payload: ProductDetails, id: string) => void -) => { - return useMutation({ - mutationFn: async (id: string) => { - const response = await deleteProduct({ client, path: { id } }); - return response.data as ProductDetails; - }, - mutationKey: [ProductsQueryKey], - onSuccess, - onError, - }); -}; diff --git a/client/src/app/queries/sboms.ts b/client/src/app/queries/sboms.ts index 4028d7e2..326a731e 100644 --- a/client/src/app/queries/sboms.ts +++ b/client/src/app/queries/sboms.ts @@ -1,4 +1,9 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + useMutation, + useQueries, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import { AxiosError } from "axios"; import { HubRequestParams } from "@app/api/models"; @@ -137,7 +142,7 @@ export const useFetchSbomsByPackageId = ( params: HubRequestParams = {} ) => { const { data, isLoading, error, refetch } = useQuery({ - queryKey: [SBOMsQueryKey, "by-package", packageId, params], + queryKey: ["SBOMsQueryKeysss", "by-package", packageId, params], queryFn: () => { return listRelatedSboms({ client, @@ -171,6 +176,28 @@ export const useFetchSbomsAdvisory = (sbomId: string) => { return { advisories: data?.data || [], isFetching: isLoading, - fetchError: error, + fetchError: error as AxiosError | null, + }; +}; + +export const useFetchSbomsAdvisory2 = (sbomIds: string[]) => { + const userQueries = useQueries({ + queries: sbomIds.map((sbomId) => { + return { + queryKey: [SBOMsQueryKey, sbomId, "advisory"], + queryFn: () => { + return getSbomAdvisories({ + client, + path: { id: sbomId }, + }); + }, + }; + }), + }); + + return { + advisories: userQueries.map(({ data }) => data?.data || []), + isFetching: userQueries.some(({ isLoading }) => isLoading), + fetchError: userQueries.map(({ error }) => error as AxiosError | null), }; }; diff --git a/package-lock.json b/package-lock.json index b9cb8d8a..6ff9aa26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,8 +82,8 @@ "@patternfly/react-table": "^5.4.0", "@patternfly/react-tokens": "^5.4.0", "@segment/analytics-next": "^1.64.0", - "@tanstack/react-query": "^5.50.1", - "@tanstack/react-query-devtools": "^5.50.1", + "@tanstack/react-query": "^5.61.0", + "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.7.2", "dayjs": "^1.11.7", "ejs": "^3.1.10", @@ -3986,9 +3986,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.59.20", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.59.20.tgz", - "integrity": "sha512-e8vw0lf7KwfGe1if4uPFhvZRWULqHjFcz3K8AebtieXvnMOz5FSzlZe3mTLlPuUBcydCnBRqYs2YJ5ys68wwLg==", + "version": "5.60.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz", + "integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==", "license": "MIT", "funding": { "type": "github", @@ -4006,12 +4006,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.59.20", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.59.20.tgz", - "integrity": "sha512-Zly0egsK0tFdfSbh5/mapSa+Zfc3Et0Zkar7Wo5sQkFzWyB3p3uZWOHR2wrlAEEV2L953eLuDBtbgFvMYiLvUw==", + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.61.0.tgz", + "integrity": "sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.59.20" + "@tanstack/query-core": "5.60.6" }, "funding": { "type": "github", @@ -4022,9 +4022,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.59.20", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.59.20.tgz", - "integrity": "sha512-AL/eQS1NFZhwwzq2Bq9Gd8wTTH+XhPNOJlDFpzPMu9NC5CQVgA0J8lWrte/sXpdWNo5KA4hgHnEdImZsF4h6Lw==", + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.61.0.tgz", + "integrity": "sha512-hd3yXl+KV+OGQmAw946qHAFp6DygcXcYN+1ai9idYddx6uEQyCwYk3jyIBOQEUw9uzN5DOGJLBsgd/QcimDQsA==", "license": "MIT", "dependencies": { "@tanstack/query-devtools": "5.59.20" @@ -4034,7 +4034,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.59.20", + "@tanstack/react-query": "^5.61.0", "react": "^18 || ^19" } }, From 35b94996a51f83a6acfeba1501e802c4fe0e686e Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:15:04 +0100 Subject: [PATCH 09/11] Add dashboard code and match latest backend pr --- client/openapi/trustd.yaml | 81 ++-- client/src/app/client/schemas.gen.ts | 115 +++-- client/src/app/client/types.gen.ts | 26 +- .../SbomVulnerabilitiesDonutChart.tsx | 4 +- .../useVulnerabilitiesOfSbom.ts | 200 ++++----- .../home/components/MonitoringSection.tsx | 401 ++++++++++-------- .../app/pages/home/components/WatchedSbom.tsx | 6 +- .../home/components/WatchedSbomDonutChart.tsx | 16 +- .../app/pages/home/watched-sboms-context.tsx | 11 +- .../sbom-details/vulnerabilities-by-sbom.tsx | 22 +- .../components/SbomVulnerabilities.tsx | 6 +- .../advisories-by-vulnerability.stories.tsx | 35 +- client/src/app/queries/sboms.ts | 2 +- 13 files changed, 459 insertions(+), 466 deletions(-) diff --git a/client/openapi/trustd.yaml b/client/openapi/trustd.yaml index b14f2edd..f4c3ede1 100644 --- a/client/openapi/trustd.yaml +++ b/client/openapi/trustd.yaml @@ -5,7 +5,7 @@ info: license: name: Apache License, Version 2.0 identifier: Apache-2.0 - version: 0.1.0-alpha.23 + version: 0.1.0-alpha.24 paths: /.well-known/trustify: get: @@ -2243,8 +2243,10 @@ components: required: - sbom_id - node_id + - relationship - purl - name + - version properties: name: type: string @@ -2252,8 +2254,12 @@ components: type: string purl: type: string + relationship: + type: string sbom_id: type: string + version: + type: string AncestorSummary: type: object required: @@ -2261,6 +2267,7 @@ components: - node_id - purl - name + - version - published - document_id - product_name @@ -2287,6 +2294,8 @@ components: type: string sbom_id: type: string + version: + type: string BasePurlDetails: allOf: - $ref: '#/components/schemas/BasePurlHead' @@ -2451,8 +2460,10 @@ components: required: - sbom_id - node_id + - relationship - purl - name + - version - deps properties: deps: @@ -2465,8 +2476,12 @@ components: type: string purl: type: string + relationship: + type: string sbom_id: type: string + version: + type: string DepSummary: type: object required: @@ -2474,6 +2489,7 @@ components: - node_id - purl - name + - version - published - document_id - product_name @@ -2500,6 +2516,8 @@ components: type: string sbom_id: type: string + version: + type: string Id: type: string description: A hash/digest prefixed with its type. @@ -3019,17 +3037,11 @@ components: - type: object required: - described_by - - number_of_packages properties: described_by: type: array items: $ref: '#/components/schemas/SbomPackage' - number_of_packages: - type: integer - format: int64 - description: The number of packages this SBOM has - minimum: 0 total: type: integer format: int64 @@ -3329,6 +3341,7 @@ components: - dev_tool_of - described_by - package_of + - undefined Report: type: object required: @@ -3412,6 +3425,7 @@ components: - published - authors - name + - number_of_packages properties: authors: type: array @@ -3429,6 +3443,11 @@ components: $ref: '#/components/schemas/Labels' name: type: string + number_of_packages: + type: integer + format: int64 + description: The number of packages this SBOM has + minimum: 0 published: type: - string @@ -3500,28 +3519,26 @@ components: relationship: $ref: '#/components/schemas/Relationship' SbomStatus: - type: object - required: - - vulnerability - - status - - packages - properties: - context: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/StatusContext' - packages: - type: array - items: - $ref: '#/components/schemas/SbomPackage' - severity: - oneOf: - - type: 'null' - - $ref: '#/components/schemas/Severity' - status: - type: string - vulnerability: - $ref: '#/components/schemas/VulnerabilityHead' + allOf: + - $ref: '#/components/schemas/VulnerabilityHead' + - type: object + required: + - average_severity + - status + - packages + properties: + average_severity: + $ref: '#/components/schemas/Severity' + context: + oneOf: + - type: 'null' + - $ref: '#/components/schemas/StatusContext' + packages: + type: array + items: + $ref: '#/components/schemas/SbomPackage' + status: + type: string SbomSummary: allOf: - $ref: '#/components/schemas/SbomHead' @@ -3531,17 +3548,11 @@ components: - type: object required: - described_by - - number_of_packages properties: described_by: type: array items: $ref: '#/components/schemas/SbomPackage' - number_of_packages: - type: integer - format: int64 - description: The number of packages this SBOM has - minimum: 0 Severity: type: string description: |- diff --git a/client/src/app/client/schemas.gen.ts b/client/src/app/client/schemas.gen.ts index 1813f0a7..c34110de 100644 --- a/client/src/app/client/schemas.gen.ts +++ b/client/src/app/client/schemas.gen.ts @@ -276,7 +276,7 @@ export const AnalysisStatusSchema = { export const AncNodeSchema = { type: "object", - required: ["sbom_id", "node_id", "purl", "name"], + required: ["sbom_id", "node_id", "relationship", "purl", "name", "version"], properties: { name: { type: "string", @@ -287,9 +287,15 @@ export const AncNodeSchema = { purl: { type: "string", }, + relationship: { + type: "string", + }, sbom_id: { type: "string", }, + version: { + type: "string", + }, }, } as const; @@ -300,6 +306,7 @@ export const AncestorSummarySchema = { "node_id", "purl", "name", + "version", "published", "document_id", "product_name", @@ -337,6 +344,9 @@ export const AncestorSummarySchema = { sbom_id: { type: "string", }, + version: { + type: "string", + }, }, } as const; @@ -587,7 +597,15 @@ export const CweImporterSchema = { export const DepNodeSchema = { type: "object", - required: ["sbom_id", "node_id", "purl", "name", "deps"], + required: [ + "sbom_id", + "node_id", + "relationship", + "purl", + "name", + "version", + "deps", + ], properties: { deps: { type: "array", @@ -604,9 +622,15 @@ export const DepNodeSchema = { purl: { type: "string", }, + relationship: { + type: "string", + }, sbom_id: { type: "string", }, + version: { + type: "string", + }, }, } as const; @@ -617,6 +641,7 @@ export const DepSummarySchema = { "node_id", "purl", "name", + "version", "published", "document_id", "product_name", @@ -654,6 +679,9 @@ export const DepSummarySchema = { sbom_id: { type: "string", }, + version: { + type: "string", + }, }, } as const; @@ -1357,7 +1385,7 @@ export const PaginatedResults_SbomSummarySchema = { }, { type: "object", - required: ["described_by", "number_of_packages"], + required: ["described_by"], properties: { described_by: { type: "array", @@ -1365,12 +1393,6 @@ export const PaginatedResults_SbomSummarySchema = { $ref: "#/components/schemas/SbomPackage", }, }, - number_of_packages: { - type: "integer", - format: "int64", - description: "The number of packages this SBOM has", - minimum: 0, - }, }, }, ], @@ -1818,6 +1840,7 @@ export const RelationshipSchema = { "dev_tool_of", "described_by", "package_of", + "undefined", ], } as const; @@ -1927,6 +1950,7 @@ export const SbomHeadSchema = { "published", "authors", "name", + "number_of_packages", ], properties: { authors: { @@ -1953,6 +1977,12 @@ export const SbomHeadSchema = { name: { type: "string", }, + number_of_packages: { + type: "integer", + format: "int64", + description: "The number of packages this SBOM has", + minimum: 0, + }, published: { type: ["string", "null"], format: "date-time", @@ -2052,42 +2082,39 @@ export const SbomPackageRelationSchema = { } as const; export const SbomStatusSchema = { - type: "object", - required: ["vulnerability", "status", "packages"], - properties: { - context: { - oneOf: [ - { - type: "null", + allOf: [ + { + $ref: "#/components/schemas/VulnerabilityHead", + }, + { + type: "object", + required: ["average_severity", "status", "packages"], + properties: { + average_severity: { + $ref: "#/components/schemas/Severity", }, - { - $ref: "#/components/schemas/StatusContext", + context: { + oneOf: [ + { + type: "null", + }, + { + $ref: "#/components/schemas/StatusContext", + }, + ], }, - ], - }, - packages: { - type: "array", - items: { - $ref: "#/components/schemas/SbomPackage", - }, - }, - severity: { - oneOf: [ - { - type: "null", + packages: { + type: "array", + items: { + $ref: "#/components/schemas/SbomPackage", + }, }, - { - $ref: "#/components/schemas/Severity", + status: { + type: "string", }, - ], - }, - status: { - type: "string", - }, - vulnerability: { - $ref: "#/components/schemas/VulnerabilityHead", + }, }, - }, + ], } as const; export const SbomSummarySchema = { @@ -2107,7 +2134,7 @@ export const SbomSummarySchema = { }, { type: "object", - required: ["described_by", "number_of_packages"], + required: ["described_by"], properties: { described_by: { type: "array", @@ -2115,12 +2142,6 @@ export const SbomSummarySchema = { $ref: "#/components/schemas/SbomPackage", }, }, - number_of_packages: { - type: "integer", - format: "int64", - description: "The number of packages this SBOM has", - minimum: 0, - }, }, }, ], diff --git a/client/src/app/client/types.gen.ts b/client/src/app/client/types.gen.ts index 1f48ea13..3b320af7 100644 --- a/client/src/app/client/types.gen.ts +++ b/client/src/app/client/types.gen.ts @@ -119,7 +119,9 @@ export type AncNode = { name: string; node_id: string; purl: string; + relationship: string; sbom_id: string; + version: string; }; export type AncestorSummary = { @@ -132,6 +134,7 @@ export type AncestorSummary = { published: string; purl: string; sbom_id: string; + version: string; }; export type BasePurlDetails = BasePurlHead & { @@ -230,7 +233,9 @@ export type DepNode = { name: string; node_id: string; purl: string; + relationship: string; sbom_id: string; + version: string; }; export type DepSummary = { @@ -243,6 +248,7 @@ export type DepSummary = { published: string; purl: string; sbom_id: string; + version: string; }; /** @@ -532,10 +538,6 @@ export type PaginatedResults_SbomSummary = { SbomHead & (null | SourceDocument) & { described_by: Array; - /** - * The number of packages this SBOM has - */ - number_of_packages: number; } >; total: number; @@ -682,7 +684,8 @@ export type Relationship = | "build_tool_of" | "dev_tool_of" | "described_by" - | "package_of"; + | "package_of" + | "undefined"; export type Report = { /** @@ -736,6 +739,10 @@ export type SbomHead = { id: string; labels: Labels; name: string; + /** + * The number of packages this SBOM has + */ + number_of_packages: number; published: string | null; }; @@ -762,21 +769,16 @@ export type SbomPackageRelation = { relationship: Relationship; }; -export type SbomStatus = { +export type SbomStatus = VulnerabilityHead & { + average_severity: Severity; context?: null | StatusContext; packages: Array; - severity?: null | Severity; status: string; - vulnerability: VulnerabilityHead; }; export type SbomSummary = SbomHead & (null | SourceDocument) & { described_by: Array; - /** - * The number of packages this SBOM has - */ - number_of_packages: number; }; /** diff --git a/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx b/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx index 72322d9a..6e1d43f5 100644 --- a/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx +++ b/client/src/app/components/SbomVulnerabilitiesDonutChart.tsx @@ -4,10 +4,10 @@ import { ChartDonut } from "@patternfly/react-charts"; import { compareBySeverityFn, severityList } from "@app/api/model-utils"; import { Severity } from "@app/client"; -import { VulnerabilityOfSbomSummary } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; +import { SeveritySummary } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; export interface SbomVulnerabilitiesDonutChartProps { - vulnerabilitiesSummary: VulnerabilityOfSbomSummary; + vulnerabilitiesSummary: SeveritySummary; } export const SbomVulnerabilitiesDonutChart: React.FC< diff --git a/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts b/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts index ce151b37..cbfba409 100644 --- a/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts +++ b/client/src/app/hooks/domain-controls/useVulnerabilitiesOfSbom.ts @@ -1,36 +1,85 @@ import React from "react"; import { VulnerabilityStatus } from "@app/api/models"; -import { - SbomAdvisory, - SbomPackage, - Severity, - VulnerabilityHead, -} from "@app/client"; +import { SbomAdvisory, SbomPackage, SbomStatus, Severity } from "@app/client"; import { useFetchSbomsAdvisory, - useFetchSbomsAdvisory2, + useFetchSbomsAdvisoryBatch, } from "@app/queries/sboms"; interface VulnerabilityOfSbom { + vulnerability: SbomStatus; advisory: SbomAdvisory; - vulnerabilityId: string; - vulnerability: VulnerabilityHead; - severity?: Severity | null; - status: VulnerabilityStatus; + vulnerabilityStatus: VulnerabilityStatus; packages: SbomPackage[]; } -export interface VulnerabilityOfSbomSummary { +export type SeveritySummary = { total: number; severities: { [key in Severity]: number }; +}; + +export interface VulnerabilityOfSbomSummary { + vulnerabilityStatus: { + [key in VulnerabilityStatus]: SeveritySummary; + }; } -const DEFAULT_SUMMARY: VulnerabilityOfSbomSummary = { +const DEFAULT_SEVERITY: SeveritySummary = { total: 0, severities: { none: 0, low: 0, medium: 0, high: 0, critical: 0 }, }; +const DEFAULT_SUMMARY: VulnerabilityOfSbomSummary = { + vulnerabilityStatus: { + affected: { ...DEFAULT_SEVERITY }, + fixed: { ...DEFAULT_SEVERITY }, + not_affected: { ...DEFAULT_SEVERITY }, + known_not_affected: { ...DEFAULT_SEVERITY }, + }, +}; + +const advisoryStatusToModels = (advisories: SbomAdvisory[]) => { + const vulnerabilities = advisories.flatMap((advisory) => { + return (advisory.status ?? []).map((sbomStatus) => { + const result: VulnerabilityOfSbom = { + vulnerability: sbomStatus, + advisory: advisory, + vulnerabilityStatus: sbomStatus.status as VulnerabilityStatus, + packages: sbomStatus.packages || [], + }; + return result; + }); + }); + + const summary = vulnerabilities.reduce((prev, current) => { + const vulnStatus = current.vulnerabilityStatus; + const severity = current.vulnerability.average_severity; + + const prevVulnStatusValue = prev.vulnerabilityStatus[vulnStatus]; + + const result: VulnerabilityOfSbomSummary = { + ...prev, + vulnerabilityStatus: { + ...prev.vulnerabilityStatus, + [vulnStatus]: { + total: prevVulnStatusValue.total + 1, + severities: { + ...prevVulnStatusValue.severities, + [severity]: prevVulnStatusValue.severities[severity] + 1, + }, + }, + }, + }; + return result; + }, DEFAULT_SUMMARY); + + return { + vulnerabilities, + summary, + }; +}; + export const useVulnerabilitiesOfSbom = (sbomId: string) => { const { advisories, @@ -38,137 +87,30 @@ export const useVulnerabilitiesOfSbom = (sbomId: string) => { fetchError: fetchErrorAdvisories, } = useFetchSbomsAdvisory(sbomId); - const allVulnerabilities = React.useMemo(() => { - const vulnerabilities = (advisories ?? []) - .flatMap((advisory) => { - return (advisory.status ?? []).map((status) => { - const result: VulnerabilityOfSbom = { - advisory: advisory, - vulnerabilityId: status.vulnerability.identifier, - vulnerability: status.vulnerability, - severity: status.severity, - status: status.status as VulnerabilityStatus, - packages: status.packages || [], - }; - return result; - }); - }) - // Take only "affected" - .filter((item) => item.status === "affected") - // Remove duplicates if exists - .reduce((prev, current) => { - const exists = prev.find( - (item) => - item.vulnerability.identifier === - current.vulnerability.identifier && - item.advisory.uuid === current.advisory.uuid - ); - if (!exists) { - return [...prev, current]; - } else { - return prev; - } - }, [] as VulnerabilityOfSbom[]); - - return vulnerabilities; + const result = React.useMemo(() => { + return advisoryStatusToModels(advisories || []); }, [advisories]); - // Summary - - const vulnerabilitiesSummary = React.useMemo(() => { - return allVulnerabilities.reduce((prev, current) => { - if (current.severity) { - const severity = current.severity; - return { - ...prev, - total: prev.total + 1, - severities: { - ...prev.severities, - [severity]: prev.severities[severity] + 1, - }, - }; - } else { - return prev; - } - }, DEFAULT_SUMMARY); - }, [allVulnerabilities]); - return { + data: result, isFetching: isFetchingAdvisories, fetchError: fetchErrorAdvisories, - vulnerabilities: allVulnerabilities, - summary: vulnerabilitiesSummary, }; }; export const useVulnerabilitiesOfSboms = (sbomIds: string[]) => { const { advisories, isFetching, fetchError } = - useFetchSbomsAdvisory2(sbomIds); - - const allVulnerabilities = React.useMemo(() => { - const vulnerabilities = (advisories ?? []).map((advisories) => { - return ( - advisories - .flatMap((advisory) => { - return (advisory.status ?? []).map((status) => { - const result: VulnerabilityOfSbom = { - advisory: advisory, - vulnerabilityId: status.vulnerability.identifier, - vulnerability: status.vulnerability, - severity: status.severity, - status: status.status as VulnerabilityStatus, - packages: status.packages || [], - }; - return result; - }); - }) - // Take only "affected" - .filter((item) => item.status === "affected") - // Remove duplicates if exists - .reduce((prev, current) => { - const exists = prev.find( - (item) => - item.vulnerability.identifier === - current.vulnerability.identifier && - item.advisory.uuid === current.advisory.uuid - ); - if (!exists) { - return [...prev, current]; - } else { - return prev; - } - }, [] as VulnerabilityOfSbom[]) - ); - }); - return vulnerabilities; - }, [advisories]); + useFetchSbomsAdvisoryBatch(sbomIds); - // Summary - - const vulnerabilitiesSummary = React.useMemo(() => { - return allVulnerabilities.map((allVulnerabilities) => { - return allVulnerabilities.reduce((prev, current) => { - if (current.severity) { - const severity = current.severity; - return { - ...prev, - total: prev.total + 1, - severities: { - ...prev.severities, - [severity]: prev.severities[severity] + 1, - }, - }; - } else { - return prev; - } - }, DEFAULT_SUMMARY); + const result = React.useMemo(() => { + return (advisories ?? []).map((item) => { + return advisoryStatusToModels(item || []); }); - }, [allVulnerabilities]); + }, [advisories]); return { + data: result, isFetching: isFetching, fetchError: fetchError, - vulnerabilities: allVulnerabilities, - summary: vulnerabilitiesSummary, }; }; diff --git a/client/src/app/pages/home/components/MonitoringSection.tsx b/client/src/app/pages/home/components/MonitoringSection.tsx index 1a323909..56996ac4 100644 --- a/client/src/app/pages/home/components/MonitoringSection.tsx +++ b/client/src/app/pages/home/components/MonitoringSection.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { Chart, @@ -27,7 +27,8 @@ import { } from "@patternfly/react-core"; import { severityList } from "@app/api/model-utils"; -import { Severity } from "@app/client"; +import { SbomHead, Severity } from "@app/client"; +import { LoadingWrapper } from "@app/components/LoadingWrapper"; import { useVulnerabilitiesOfSboms } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; import { useFetchSBOMs } from "@app/queries/sboms"; import { useFetchVulnerabilities } from "@app/queries/vulnerabilities"; @@ -46,6 +47,10 @@ const LEGENDS: Legend[] = [ ]; export const MonitoringSection: React.FC = () => { + const navigate = useNavigate(); + + // + const { result: { data: barchartSboms, total: totalSboms }, isFetching: isFetchingBarchartSboms, @@ -58,6 +63,22 @@ export const MonitoringSection: React.FC = () => { true ); + const { + data: barchartSbomsVulnerabilities, + isFetching: isFetchingBarchartSbomsVulnerabilities, + fetchError: fetchErrorBarchartSbomsVulnerabilities, + } = useVulnerabilitiesOfSboms(barchartSboms.map((e) => e.id)); + + const showTickValues = barchartSbomsVulnerabilities + .map((e) => e.summary.vulnerabilityStatus.affected.total) + .every((item) => item === 0); + + const generateSbomBarName = (sbom: SbomHead, index: number) => { + return `${" ".repeat(index + 1)}${sbom.name}`; + }; + + // + const { result: { data: vulnerabilities, total: totalVulnerabilities }, isFetching: isFetchingVulnerabilities, @@ -70,198 +91,216 @@ export const MonitoringSection: React.FC = () => { true ); - const { summary, isFetching, fetchError } = useVulnerabilitiesOfSboms( - barchartSboms.map((e) => e.id) - ); - return ( Your dashboard - - - - Below is a summary of CVE status for your last 10 ingested - SBOMs. You can click on the SBOM name or CVE severity number - below to be taken to their respective details page. - - - -
- { - const severity = severityList[legend.severity]; - return { name: severity.name }; - })} - legendPosition="bottom-left" - height={375} - name="sbom-summary-status" - padding={{ - bottom: 75, - left: 330, - right: 50, - top: 50, - }} - themeColor={ChartThemeColor.multiOrdered} - width={700} - legendComponent={ - !!e) + } + > + + + + Below is a summary of CVE status for your last 10 ingested + SBOMs. You can click on the SBOM name or CVE severity number + below to be taken to their respective details page. + + + +
+ { + const severity = severityList[legend.severity]; + return { name: severity.name }; + })} + legendPosition="bottom-left" + height={375} + name="sbom-summary-status" + padding={{ + bottom: 75, + left: 330, + right: 50, + top: 50, + }} + themeColor={ChartThemeColor.multiOrdered} + width={700} + legendComponent={ + { + const severity = severityList[legend.severity]; + return severity.color.value; + })} + /> + } + > + + } + tickLabelComponent={ + { + const sbomName = (event.target as any) + .innerHTML as string | null; + const sbom = barchartSboms.find( + (item, index) => { + return ( + generateSbomBarName(item, index) === + sbomName + ); + } + ); + if (sbom) { + navigate(`/sboms/${sbom.id}`); + } + }, + }} + /> + } + /> + + { const severity = severityList[legend.severity]; return severity.color.value; })} - /> - } - > - } - tickLabelComponent={ - { - // const sbom_name = (event.target as any) - // .innerHTML as string | null; - // const sbom = props.find( - // (item) => item.sbom_name === sbom_name - // ); - // if (sbom) { - // const sbomDetailsPage = `/sbom/content/${sbom.sbom_id}`; + > + {LEGENDS.map((legend) => { + const severityData = severityList[legend.severity]; + return ( + + } + data={barchartSbomsVulnerabilities.map( + ( + { + summary: { + vulnerabilityStatus: { affected }, + }, + }, + index + ) => { + const sbom = barchartSboms[index]; - // const wasmBindings = (window as any) - // .wasmBindings; - // if (wasmBindings) { - // wasmBindings.spogNavigateTo( - // sbomDetailsPage - // ); - // } else { - // window.open(sbomDetailsPage); - // } - // } - // }, - // }} - /> - } - /> - - { - const severity = severityList[legend.severity]; - return severity.color.value; - })} - > - {LEGENDS.map((legend) => { - const severity = severityList[legend.severity]; - return ( - - } - data={summary.map((item, index) => { - const sbom = barchartSboms[index]; - const severityKey = legend.severity; - const count = item.severities[severityKey]; - return { - name: severityKey, - x: sbom.name, - y: count, - label: `${severity.name}: ${count}`, - }; - })} - /> - ); - })} - - -
-
-
+ const severity = legend.severity; + const count = affected.severities[severity]; + return { + x: generateSbomBarName(sbom, index), + y: count, + label: `${severityData.name}: ${count}`, + }; + } + )} + /> + ); + })} + +
+
+
+
+
- - - - - - Last SBOM ingested - - - - - {formatDate(barchartSboms?.[0]?.published)} - - - - {barchartSboms?.[0]?.name} - - - - - - - - - - - Total SBOMs - - {totalSboms} - - - - - - - - - Last Vulnerability ingested - - - - - {formatDate(vulnerabilities?.[0]?.published)} - - - {vulnerabilities?.[0]?.identifier} - - - - - - - - - - - Total Vulnerabilities - - - {totalVulnerabilities} - - - - - + + + + + + + Last SBOM ingested + + + + + {formatDate(barchartSboms?.[0]?.published)} + + + + {barchartSboms?.[0]?.name} + + + + + + + + + + + Total SBOMs + + {totalSboms} + + + + + + + + + Last Vulnerability ingested + + + + + {formatDate(vulnerabilities?.[0]?.published)} + + + {vulnerabilities?.[0]?.identifier} + + + + + + + + + + + Total Vulnerabilities + + + {totalVulnerabilities} + + + + + +
diff --git a/client/src/app/pages/home/components/WatchedSbom.tsx b/client/src/app/pages/home/components/WatchedSbom.tsx index 1b1dcf6d..46ced431 100644 --- a/client/src/app/pages/home/components/WatchedSbom.tsx +++ b/client/src/app/pages/home/components/WatchedSbom.tsx @@ -20,7 +20,7 @@ import { StackItem, TextInputGroup, TextInputGroupMain, - TextInputGroupUtilities, + TextInputGroupUtilities } from "@patternfly/react-core"; import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; @@ -121,11 +121,11 @@ export const WatchedSbom: React.FC = ({ {sbomId ? ( - + - View Details + View Details ) : ( diff --git a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx index 3d591ce5..f768f3bf 100644 --- a/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx +++ b/client/src/app/pages/home/components/WatchedSbomDonutChart.tsx @@ -1,5 +1,6 @@ import React from "react"; +import { LoadingWrapper } from "@app/components/LoadingWrapper"; import { SbomVulnerabilitiesDonutChart } from "@app/components/SbomVulnerabilitiesDonutChart"; import { useVulnerabilitiesOfSbom } from "@app/hooks/domain-controls/useVulnerabilitiesOfSbom"; @@ -10,16 +11,13 @@ interface WatchedSbomDonutChartProps { export const WatchedSbomDonutChart: React.FC = ({ sbomId, }) => { - const { - vulnerabilities: vulnerabilities, - summary: vulnerabilitiesSummary, - isFetching: isFetchingVulnerabilities, - fetchError: fetchErrorVulnerabilities, - } = useVulnerabilitiesOfSbom(sbomId); + const { data, isFetching, fetchError } = useVulnerabilitiesOfSbom(sbomId); return ( - + + + ); }; diff --git a/client/src/app/pages/home/watched-sboms-context.tsx b/client/src/app/pages/home/watched-sboms-context.tsx index 1cb890ec..9c46009f 100644 --- a/client/src/app/pages/home/watched-sboms-context.tsx +++ b/client/src/app/pages/home/watched-sboms-context.tsx @@ -29,10 +29,10 @@ interface IWatchedSbomsProvider { export const WatchedSbomsProvider: React.FunctionComponent< IWatchedSbomsProvider > = ({ children }) => { - const { sboms, isFetching, fetchError } = useFetchWatchedSboms(); - const { pushNotification } = React.useContext(NotificationsContext); + const { sboms, isFetching, fetchError } = useFetchWatchedSboms(); + const onUpdateSuccess = () => {}; const onUpdateError = (_error: AxiosError) => { pushNotification({ @@ -53,7 +53,12 @@ export const WatchedSbomsProvider: React.FunctionComponent< return ( {children} diff --git a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx index a13de9ab..4e808da5 100644 --- a/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx +++ b/client/src/app/pages/sbom-details/vulnerabilities-by-sbom.tsx @@ -56,15 +56,15 @@ export const VulnerabilitiesBySbom: React.FC = ({ fetchError: fetchErrorSbom, } = useFetchSBOMById(sbomId); const { - vulnerabilities: vulnerabilities, - summary: vulnerabilitiesSummary, + data: { vulnerabilities, summary: vulnerabilitiesSummary }, isFetching: isFetchingVulnerabilities, fetchError: fetchErrorVulnerabilities, } = useVulnerabilitiesOfSbom(sbomId); const tableDataWithUiId = useWithUiId( vulnerabilities, - (d) => `${d.vulnerabilityId}-${d.advisory.identifier}-${d.advisory.uuid}` + (d) => + `${d.vulnerability.identifier}-${d.advisory.identifier}-${d.advisory.uuid}` ); const tableControls = useLocalTableControls({ @@ -118,7 +118,9 @@ export const VulnerabilitiesBySbom: React.FC = ({ @@ -195,8 +197,10 @@ export const VulnerabilitiesBySbom: React.FC = ({ rowIndex={rowIndex} > - - {item.vulnerabilityId} + + {item.vulnerability.identifier} = ({ )} - {item.severity && ( - + {item.vulnerability.average_severity && ( + )} = ({ sbomId, }) => { - const { summary, isFetching, fetchError } = useVulnerabilitiesOfSbom(sbomId); + const { data, isFetching, fetchError } = useVulnerabilitiesOfSbom(sbomId); return ( = ({ isFetchingState={} fetchErrorState={} > - + ); }; diff --git a/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx b/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx index ee492922..2976af31 100644 --- a/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx +++ b/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx @@ -69,40 +69,7 @@ export const PrimaryState: Story = { ], }, number_of_vulnerabilities: 1, - sboms: [ - { - id: "urn:uuid:0193013f-1b8a-7152-8857-8b8f4238b8ba", - document_id: - "https://access.redhat.com/security/data/sbom/spdx/quarkus-bom-3.2.12.Final-redhat-00002", - labels: { - type: "spdx", - }, - data_licenses: ["CC0-1.0"], - published: "2024-07-05T09:40:48Z", - authors: [ - "Organization: Red Hat Product Security (secalert@redhat.com)", - ], - name: "quarkus-bom", - version: "3.2.12.Final-redhat-00002", - status: ["affected"], - }, - { - id: "urn:uuid:0193013f-0f00-77e1-afe4-b7d7f8585b7a", - document_id: - "https://access.redhat.com/security/data/sbom/spdx/quarkus-bom-3.2.11.Final-redhat-00001", - labels: { - type: "spdx", - }, - data_licenses: ["CC0-1.0"], - published: "2024-05-28T09:26:01Z", - authors: [ - "Organization: Red Hat Product Security (secalert@redhat.com)", - ], - name: "quarkus-bom", - version: "3.2.11.Final-redhat-00001", - status: ["affected"], - }, - ], + sboms: [], }, ], }, diff --git a/client/src/app/queries/sboms.ts b/client/src/app/queries/sboms.ts index 326a731e..4de7393e 100644 --- a/client/src/app/queries/sboms.ts +++ b/client/src/app/queries/sboms.ts @@ -180,7 +180,7 @@ export const useFetchSbomsAdvisory = (sbomId: string) => { }; }; -export const useFetchSbomsAdvisory2 = (sbomIds: string[]) => { +export const useFetchSbomsAdvisoryBatch = (sbomIds: string[]) => { const userQueries = useQueries({ queries: sbomIds.map((sbomId) => { return { From 83b9f1b762d20c1dc84c33498945af2951ee953d Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Fri, 22 Nov 2024 13:21:30 +0100 Subject: [PATCH 10/11] Fix format --- client/src/app/pages/home/components/WatchedSbom.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/app/pages/home/components/WatchedSbom.tsx b/client/src/app/pages/home/components/WatchedSbom.tsx index 46ced431..c331700f 100644 --- a/client/src/app/pages/home/components/WatchedSbom.tsx +++ b/client/src/app/pages/home/components/WatchedSbom.tsx @@ -20,7 +20,7 @@ import { StackItem, TextInputGroup, TextInputGroupMain, - TextInputGroupUtilities + TextInputGroupUtilities, } from "@patternfly/react-core"; import TimesIcon from "@patternfly/react-icons/dist/esm/icons/times-icon"; From 47cb3d09030c988cee11114e8c2d322ab1d135cc Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:01:19 +0100 Subject: [PATCH 11/11] fix: restore story book data deleted while developing --- .../advisories-by-vulnerability.stories.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx b/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx index 2976af31..db4182cb 100644 --- a/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx +++ b/client/src/app/pages/vulnerability-details/advisories-by-vulnerability.stories.tsx @@ -69,7 +69,42 @@ export const PrimaryState: Story = { ], }, number_of_vulnerabilities: 1, - sboms: [], + sboms: [ + { + id: "urn:uuid:0193013f-1b8a-7152-8857-8b8f4238b8ba", + document_id: + "https://access.redhat.com/security/data/sbom/spdx/quarkus-bom-3.2.12.Final-redhat-00002", + labels: { + type: "spdx", + }, + data_licenses: ["CC0-1.0"], + published: "2024-07-05T09:40:48Z", + authors: [ + "Organization: Red Hat Product Security (secalert@redhat.com)", + ], + name: "quarkus-bom", + version: "3.2.12.Final-redhat-00002", + status: ["affected"], + number_of_packages: 1, + }, + { + id: "urn:uuid:0193013f-0f00-77e1-afe4-b7d7f8585b7a", + document_id: + "https://access.redhat.com/security/data/sbom/spdx/quarkus-bom-3.2.11.Final-redhat-00001", + labels: { + type: "spdx", + }, + data_licenses: ["CC0-1.0"], + published: "2024-05-28T09:26:01Z", + authors: [ + "Organization: Red Hat Product Security (secalert@redhat.com)", + ], + name: "quarkus-bom", + version: "3.2.11.Final-redhat-00001", + status: ["affected"], + number_of_packages: 1, + }, + ], }, ], },