feat: add cve summary in vulnerability tab (#416)

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2024-01-18 22:00:19 +02:00 committed by GitHub
parent 12f9229320
commit 5bf7d5652c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 160 additions and 5 deletions

View File

@ -42,7 +42,8 @@ const config = {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
ignoreHTTPSErrors: true ignoreHTTPSErrors: true,
screenshot: 'only-on-failure'
}, },
/* Configure projects for major browsers */ /* Configure projects for major browsers */
@ -101,7 +102,7 @@ const config = {
], ],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */ /* 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 */ /* Run your local dev server before starting the tests */
// webServer: { // webServer: {

View File

@ -22,6 +22,14 @@ const mockCVEList = {
CVEListForImage: { CVEListForImage: {
Tag: '', Tag: '',
Page: { ItemCount: 20, TotalCount: 20 }, Page: { ItemCount: 20, TotalCount: 20 },
Summary: {
Count: 5,
UnknownCount: 1,
LowCount: 1,
MediumCount: 1,
HighCount: 1,
CriticalCount: 1,
},
CVEList: [ CVEList: [
{ {
Id: 'CVE-2020-16156', Id: 'CVE-2020-16156',
@ -499,6 +507,7 @@ describe('Vulnerabilties page', () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
}); });
@ -515,7 +524,7 @@ describe('Vulnerabilties page', () => {
it('renders no vulnerabilities if there are not any', async () => { it('renders no vulnerabilities if there are not any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ jest.spyOn(api, 'get').mockResolvedValue({
status: 200, status: 200,
data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } } data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [], Summary: {} } } }
}); });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));

View File

@ -97,7 +97,7 @@ const endpoints = {
if (!isEmpty(searchTerm)) { if (!isEmpty(searchTerm)) {
query += `, searchedCVE: "${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) => allVulnerabilitiesForRepo: (name) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`, `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`,

View File

@ -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 (
<Stack direction="row" spacing="0.5em">
<div className={[classes.cveCountCard].join(' ')}>Total {total}</div>
<div className={classes.severityList}>
<Tooltip title="Critical">
<div className={[classes.cveCountCard, classes.criticalSeverity].join(' ')}>C {critical}</div>
</Tooltip>
<Tooltip title="High">
<div className={[classes.cveCountCard, classes.highSeverity].join(' ')}>H {high}</div>
</Tooltip>
<Tooltip title="Medium">
<div className={[classes.cveCountCard, classes.mediumSeverity].join(' ')}>M {medium}</div>
</Tooltip>
<Tooltip title="Low">
<div className={[classes.cveCountCard, classes.lowSeverity].join(' ')}>L {low}</div>
</Tooltip>
<Tooltip title="Unknown">
<div className={[classes.cveCountCard, classes.unknownSeverity].join(' ')}>U {unknown}</div>
</Tooltip>
</div>
</Stack>
);
}
export default VulnerabilitiyCountCard;

View File

@ -31,14 +31,25 @@ import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline';
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda'; import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
import VulnerabilitiyCard from '../../Shared/VulnerabilityCard'; import VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
import VulnerabilityCountCard from '../../Shared/VulnerabilityCountCard';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
searchAndDisplayBar: {
display: 'flex',
justifyContent: 'space-between'
},
title: { title: {
color: theme.palette.primary.main, color: theme.palette.primary.main,
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: '600', fontWeight: '600',
marginBottom: '0' marginBottom: '0'
}, },
cveCountSummary: {
color: theme.palette.primary.main,
fontSize: '1.5rem',
fontWeight: '600',
marginBottom: '0'
},
cveId: { cveId: {
color: theme.palette.primary.main, color: theme.palette.primary.main,
fontSize: '1rem', fontSize: '1rem',
@ -67,6 +78,7 @@ const useStyles = makeStyles((theme) => ({
search: { search: {
position: 'relative', position: 'relative',
maxWidth: '100%', maxWidth: '100%',
flex: 0.95,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
@ -74,15 +86,18 @@ const useStyles = makeStyles((theme) => ({
border: '0.063rem solid #E7E7E7', border: '0.063rem solid #E7E7E7',
borderRadius: '0.625rem' borderRadius: '0.625rem'
}, },
expandableSearchInput: {
flexGrow: 0.95
},
view: { view: {
alignContent: 'right', alignContent: 'right',
variant: 'outlined' variant: 'outlined'
}, },
viewModes: { viewModes: {
position: 'relative', position: 'relative',
alignItems: 'baseline',
maxWidth: '100%', maxWidth: '100%',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'right',
justifyContent: 'right' justifyContent: 'right'
}, },
searchIcon: { searchIcon: {
@ -114,6 +129,7 @@ function VulnerabilitiesDetails(props) {
const classes = useStyles(); const classes = useStyles();
const [cveData, setCveData] = useState([]); const [cveData, setCveData] = useState([]);
const [allCveData, setAllCveData] = useState([]); const [allCveData, setAllCveData] = useState([]);
const [cveSummary, setCVESummary] = useState({});
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isLoadingAllCve, setIsLoadingAllCve] = useState(true); const [isLoadingAllCve, setIsLoadingAllCve] = useState(true);
const abortController = useMemo(() => new AbortController(), []); const abortController = useMemo(() => new AbortController(), []);
@ -147,9 +163,23 @@ function VulnerabilitiesDetails(props) {
.then((response) => { .then((response) => {
if (response.data && response.data.data) { if (response.data && response.data.data) {
let cveInfo = response.data.data.CVEListForImage?.CVEList; let cveInfo = response.data.data.CVEListForImage?.CVEList;
let summary = response.data.data.CVEListForImage?.Summary;
let cveListData = mapCVEInfo(cveInfo); let cveListData = mapCVEInfo(cveInfo);
setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData])); setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE); 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) { } else if (response.data.errors) {
setIsEndOfList(true); setIsEndOfList(true);
} }
@ -159,6 +189,7 @@ function VulnerabilitiesDetails(props) {
console.error(e); console.error(e);
setIsLoading(false); setIsLoading(false);
setCveData([]); setCveData([]);
setCVESummary(() => {});
setIsEndOfList(true); setIsEndOfList(true);
}); });
}; };
@ -283,6 +314,27 @@ function VulnerabilitiesDetails(props) {
); );
}; };
const renderCVESummary = () => {
if (cveSummary === undefined) {
return;
}
console.log('Test');
return !isEmpty(cveSummary) ? (
<VulnerabilityCountCard
total={cveSummary.Count}
critical={cveSummary.CriticalCount}
high={cveSummary.HighCount}
medium={cveSummary.MediumCount}
low={cveSummary.LowCount}
unknown={cveSummary.UnknownCount}
/>
) : (
<></>
);
};
const renderListBottom = () => { const renderListBottom = () => {
if (isLoading) { if (isLoading) {
return <Loading />; return <Loading />;
@ -364,6 +416,7 @@ function VulnerabilitiesDetails(props) {
</MenuItem> </MenuItem>
</Menu> </Menu>
</Stack> </Stack>
{renderCVESummary()}
<Stack className={classes.search}> <Stack className={classes.search}>
<InputBase <InputBase
placeholder={'Search'} placeholder={'Search'}