From e037c6c57731cb6ac4974f1d695908fa561d9f5d Mon Sep 17 00:00:00 2001 From: Andreea-Lupu Date: Tue, 13 Feb 2024 17:29:29 +0200 Subject: [PATCH] feat(cve): filter cves by severity Signed-off-by: Andreea-Lupu --- .../TagPage/VulnerabilitiesDetails.test.js | 62 +++++++++++++++++-- src/api.js | 11 +++- .../Shared/VulnerabilityCountCard.jsx | 24 ++++--- .../Tag/Tabs/VulnerabilitiesDetails.jsx | 9 +-- 4 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index d35819ef..bf41de02 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -462,6 +462,24 @@ const mockCVEListFiltered = { } }; +const mockCVEListFilteredBySeverity = (severity) => { + return { + CVEListForImage: { + Tag: '', + Page: { ItemCount: 20, TotalCount: 20 }, + Summary: { + Count: 5, + UnknownCount: 1, + LowCount: 1, + MediumCount: 1, + HighCount: 1, + CriticalCount: 1, + }, + CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Severity.includes(severity)) + } + }; +}; + const mockCVEListFilteredExclude = { CVEListForImage: { Tag: '', @@ -507,12 +525,6 @@ const mockCVEFixed = { } ] } - }, - pageNotFixed: { - ImageListWithCVEFixed: { - Page: { TotalCount: 0, ItemCount: 0 }, - Results: [] - } } }; @@ -541,6 +553,44 @@ describe('Vulnerabilties page', () => { await waitFor(() => expect(screen.getAllByText(/CVE/)).toHaveLength(20)); }); + it('renders the vulnerabilities by severity', async () => { + jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); + render(); + await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20)); + expect(screen.getByLabelText('Medium')).toBeInTheDocument(); + const mediumSeverity = await screen.getByLabelText('Medium'); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('MEDIUM') } }); + fireEvent.click(mediumSeverity); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(6)); + expect(screen.getByLabelText('High')).toBeInTheDocument(); + const highSeverity = await screen.getByLabelText('High'); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('HIGH') } }); + fireEvent.click(highSeverity); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1)); + expect(screen.getByLabelText('Critical')).toBeInTheDocument(); + const criticalSeverity = await screen.getByLabelText('Critical'); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('CRITICAL') } }); + fireEvent.click(criticalSeverity); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1)); + expect(screen.getByLabelText('Low')).toBeInTheDocument(); + const lowSeverity = await screen.getByLabelText('Low'); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('LOW') } }); + fireEvent.click(lowSeverity); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(10)); + expect(screen.getByLabelText('Unknown')).toBeInTheDocument(); + const unknownSeverity = await screen.getByLabelText('Unknown'); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('UNKNOWN') } }); + fireEvent.click(unknownSeverity); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(1)); + expect(screen.getByText('Total 5')).toBeInTheDocument(); + const totalSeverity = await screen.getByText('Total 5'); + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEListFilteredBySeverity('') } }); + fireEvent.click(totalSeverity); + await waitFor(() => expect(screen.getAllByText(/CVE-/)).toHaveLength(20)); + }); + it('sends filtered query if user types in the search bar', async () => { jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); render(); diff --git a/src/api.js b/src/api.js index 0589e992..acab45f4 100644 --- a/src/api.js +++ b/src/api.js @@ -90,7 +90,13 @@ const endpoints = { `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`, detailedImageInfo: (name, tag) => `/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`, - vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '', excludedTerm = '') => { + vulnerabilitiesForRepo: ( + name, + { pageNumber = 1, pageSize = 15 }, + searchTerm = '', + excludedTerm = '', + severity = '' + ) => { let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${ (pageNumber - 1) * pageSize }}`; @@ -100,6 +106,9 @@ const endpoints = { if (!isEmpty(excludedTerm)) { query += `, excludedCVE: "${excludedTerm}"`; } + if (!isEmpty(severity)) { + query += `, severity: "${severity}"`; + } return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`; }, allVulnerabilitiesForRepo: (name) => diff --git a/src/components/Shared/VulnerabilityCountCard.jsx b/src/components/Shared/VulnerabilityCountCard.jsx index c478136c..be308cbb 100644 --- a/src/components/Shared/VulnerabilityCountCard.jsx +++ b/src/components/Shared/VulnerabilityCountCard.jsx @@ -18,6 +18,8 @@ const lowBorderColor = '#f0ed94'; const unknownColor = '#f2ffdd'; const unknownBorderColor = '#e9f4d7'; +const totalBorderColor = '#e0e5eb'; + const fontSize = '0.75rem'; const useStyles = makeStyles((theme) => ({ @@ -30,7 +32,11 @@ const useStyles = makeStyles((theme) => ({ fontSize: fontSize, fontWeight: '600', borderRadius: '3px', - marginBottom: '0' + marginBottom: '0', + cursor: 'pointer' + }, + totalSeverity: { + border: '1px solid ' + totalBorderColor }, severityList: { fontSize: fontSize, @@ -63,25 +69,27 @@ const useStyles = makeStyles((theme) => ({ function VulnerabilitiyCountCard(props) { const classes = useStyles(); - const { total, critical, high, medium, low, unknown } = props; + const { total, critical, high, medium, low, unknown, filterBySeverity } = props; return ( -
Total {total}
+ filterBySeverity('')}> +
Total {total}
+
- + filterBySeverity('CRITICAL')}>
C {critical}
- + filterBySeverity('HIGH')}>
H {high}
- + filterBySeverity('MEDIUM')}>
M {medium}
- + filterBySeverity('LOW')}>
L {low}
- + filterBySeverity('UNKNOWN')}>
U {unknown}
diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx index 76fb17cd..1111c608 100644 --- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx +++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx @@ -158,6 +158,7 @@ function VulnerabilitiesDetails(props) { // pagination props const [cveFilter, setCveFilter] = useState(''); const [cveExcludeFilter, setCveExcludeFilter] = useState(''); + const [cveSeverityFilter, setCveSeverityFilter] = useState(''); const [pageNumber, setPageNumber] = useState(1); const [isEndOfList, setIsEndOfList] = useState(false); const listBottom = useRef(null); @@ -178,7 +179,8 @@ function VulnerabilitiesDetails(props) { getCVERequestName(), { pageNumber, pageSize: EXPLORE_PAGE_SIZE }, cveFilter, - cveExcludeFilter + cveExcludeFilter, + cveSeverityFilter )}`, abortController.signal ) @@ -321,7 +323,7 @@ function VulnerabilitiesDetails(props) { useEffect(() => { if (isLoading) return; resetPagination(); - }, [cveFilter, cveExcludeFilter]); + }, [cveFilter, cveExcludeFilter, cveSeverityFilter]); useEffect(() => { return () => { @@ -352,8 +354,6 @@ function VulnerabilitiesDetails(props) { return; } - console.log('Test'); - return !isEmpty(cveSummary) ? ( ) : ( <>