diff --git a/playwright.config.js b/playwright.config.js index 294d54e..c88309c 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -42,7 +42,8 @@ const config = { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - ignoreHTTPSErrors: true + ignoreHTTPSErrors: true, + screenshot: 'only-on-failure' }, /* Configure projects for major browsers */ @@ -101,7 +102,7 @@ const config = { ], /* Folder for test artifacts such as screenshots, videos, traces, etc. */ - // outputDir: 'test-results/', + outputDir: 'test-results/', /* Run your local dev server before starting the tests */ // webServer: { diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index 9b01527..9b9df38 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -515,7 +515,7 @@ describe('Vulnerabilties page', () => { it('renders no vulnerabilities if there are not any', async () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, - data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } } + data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [], Summary: {} } } } }); render(); await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); diff --git a/src/api.js b/src/api.js index 0596edf..9ea5687 100644 --- a/src/api.js +++ b/src/api.js @@ -97,7 +97,7 @@ const endpoints = { if (!isEmpty(searchTerm)) { query += `, searchedCVE: "${searchTerm}"`; } - return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`; + return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`; }, allVulnerabilitiesForRepo: (name) => `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`, diff --git a/src/components/Shared/VulnerabilityCountCard.jsx b/src/components/Shared/VulnerabilityCountCard.jsx new file mode 100644 index 0000000..c478136 --- /dev/null +++ b/src/components/Shared/VulnerabilityCountCard.jsx @@ -0,0 +1,92 @@ +import React from 'react'; + +import makeStyles from '@mui/styles/makeStyles'; +import { Stack, Tooltip } from '@mui/material'; + +const criticalColor = '#ff5c74'; +const criticalBorderColor = '#f9546d'; + +const highColor = '#ff6840'; +const highBorderColor = '#ee6b49'; + +const mediumColor = '#ffa052'; +const mediumBorderColor = '#f19d5b'; + +const lowColor = '#f9f486'; +const lowBorderColor = '#f0ed94'; + +const unknownColor = '#f2ffdd'; +const unknownBorderColor = '#e9f4d7'; + +const fontSize = '0.75rem'; + +const useStyles = makeStyles((theme) => ({ + cveCountCard: { + display: 'flex', + alignItems: 'center', + paddingLeft: '0.5rem', + paddingRight: '0.5rem', + color: theme.palette.primary.main, + fontSize: fontSize, + fontWeight: '600', + borderRadius: '3px', + marginBottom: '0' + }, + severityList: { + fontSize: fontSize, + display: 'flex', + flexWrap: 'wrap', + alignItems: 'center', + gap: '0.5em' + }, + criticalSeverity: { + backgroundColor: criticalColor, + border: '1px solid ' + criticalBorderColor + }, + highSeverity: { + backgroundColor: highColor, + border: '1px solid ' + highBorderColor + }, + mediumSeverity: { + backgroundColor: mediumColor, + border: '1px solid ' + mediumBorderColor + }, + lowSeverity: { + backgroundColor: lowColor, + border: '1px solid ' + lowBorderColor + }, + unknownSeverity: { + backgroundColor: unknownColor, + border: '1px solid ' + unknownBorderColor + } +})); + +function VulnerabilitiyCountCard(props) { + const classes = useStyles(); + const { total, critical, high, medium, low, unknown } = props; + + return ( + +
Total {total}
+
+ +
C {critical}
+
+ +
H {high}
+
+ +
M {medium}
+
+ +
L {low}
+
+ +
U {unknown}
+
+
+
+ ); +} + +export default VulnerabilitiyCountCard; diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx index f80612e..a1a7e50 100644 --- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx +++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx @@ -31,14 +31,25 @@ import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline'; import ViewAgendaIcon from '@mui/icons-material/ViewAgenda'; import VulnerabilitiyCard from '../../Shared/VulnerabilityCard'; +import VulnerabilityCountCard from '../../Shared/VulnerabilityCountCard'; const useStyles = makeStyles((theme) => ({ + searchAndDisplayBar: { + display: 'flex', + justifyContent: 'space-between' + }, title: { color: theme.palette.primary.main, fontSize: '1.5rem', fontWeight: '600', marginBottom: '0' }, + cveCountSummary: { + color: theme.palette.primary.main, + fontSize: '1.5rem', + fontWeight: '600', + marginBottom: '0' + }, cveId: { color: theme.palette.primary.main, fontSize: '1rem', @@ -67,6 +78,7 @@ const useStyles = makeStyles((theme) => ({ search: { position: 'relative', maxWidth: '100%', + flex: 0.95, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', @@ -74,15 +86,18 @@ const useStyles = makeStyles((theme) => ({ border: '0.063rem solid #E7E7E7', borderRadius: '0.625rem' }, + expandableSearchInput: { + flexGrow: 0.95 + }, view: { alignContent: 'right', variant: 'outlined' }, viewModes: { position: 'relative', + alignItems: 'baseline', maxWidth: '100%', flexDirection: 'row', - alignItems: 'right', justifyContent: 'right' }, searchIcon: { @@ -114,6 +129,7 @@ function VulnerabilitiesDetails(props) { const classes = useStyles(); const [cveData, setCveData] = useState([]); const [allCveData, setAllCveData] = useState([]); + const [cveSummary, setCVESummary] = useState({}); const [isLoading, setIsLoading] = useState(true); const [isLoadingAllCve, setIsLoadingAllCve] = useState(true); const abortController = useMemo(() => new AbortController(), []); @@ -147,9 +163,23 @@ function VulnerabilitiesDetails(props) { .then((response) => { if (response.data && response.data.data) { let cveInfo = response.data.data.CVEListForImage?.CVEList; + let summary = response.data.data.CVEListForImage?.Summary; let cveListData = mapCVEInfo(cveInfo); setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData])); setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE); + setCVESummary((previousState) => { + if (isEmpty(summary)) { + return previousState; + } + return { + Count: summary.Count, + UnknownCount: summary.UnknownCount, + LowCount: summary.LowCount, + MediumCount: summary.MediumCount, + HighCount: summary.HighCount, + CriticalCount: summary.CriticalCount + }; + }); } else if (response.data.errors) { setIsEndOfList(true); } @@ -159,6 +189,7 @@ function VulnerabilitiesDetails(props) { console.error(e); setIsLoading(false); setCveData([]); + setCVESummary(() => {}); setIsEndOfList(true); }); }; @@ -283,6 +314,25 @@ function VulnerabilitiesDetails(props) { ); }; + const renderCVESummary = () => { + if (cveSummary === undefined) { + return; + } + + console.log('Test'); + + return !isEmpty(cveSummary) ? ( + + ) : (<>); + }; + const renderListBottom = () => { if (isLoading) { return ; @@ -364,6 +414,7 @@ function VulnerabilitiesDetails(props) { + {renderCVESummary()}