feat(cve): filter cves by severity

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea-Lupu 2024-02-13 17:29:29 +02:00 committed by Ramkumar Chinchani
parent c268991495
commit e037c6c577
4 changed files with 87 additions and 19 deletions

View File

@ -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 = { const mockCVEListFilteredExclude = {
CVEListForImage: { CVEListForImage: {
Tag: '', 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)); 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(<StateVulnerabilitiesWrapper />);
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 () => { it('sends filtered query if user types in the search bar', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);

View File

@ -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}}}}`, `/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) => 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 }}`, `/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:${ let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize (pageNumber - 1) * pageSize
}}`; }}`;
@ -100,6 +106,9 @@ const endpoints = {
if (!isEmpty(excludedTerm)) { if (!isEmpty(excludedTerm)) {
query += `, excludedCVE: "${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}}}`; 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) => allVulnerabilitiesForRepo: (name) =>

View File

@ -18,6 +18,8 @@ const lowBorderColor = '#f0ed94';
const unknownColor = '#f2ffdd'; const unknownColor = '#f2ffdd';
const unknownBorderColor = '#e9f4d7'; const unknownBorderColor = '#e9f4d7';
const totalBorderColor = '#e0e5eb';
const fontSize = '0.75rem'; const fontSize = '0.75rem';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -30,7 +32,11 @@ const useStyles = makeStyles((theme) => ({
fontSize: fontSize, fontSize: fontSize,
fontWeight: '600', fontWeight: '600',
borderRadius: '3px', borderRadius: '3px',
marginBottom: '0' marginBottom: '0',
cursor: 'pointer'
},
totalSeverity: {
border: '1px solid ' + totalBorderColor
}, },
severityList: { severityList: {
fontSize: fontSize, fontSize: fontSize,
@ -63,25 +69,27 @@ const useStyles = makeStyles((theme) => ({
function VulnerabilitiyCountCard(props) { function VulnerabilitiyCountCard(props) {
const classes = useStyles(); const classes = useStyles();
const { total, critical, high, medium, low, unknown } = props; const { total, critical, high, medium, low, unknown, filterBySeverity } = props;
return ( return (
<Stack direction="row" spacing="0.5em"> <Stack direction="row" spacing="0.5em">
<div className={[classes.cveCountCard].join(' ')}>Total {total}</div> <Tooltip title="Total" onClick={() => filterBySeverity('')}>
<div className={[classes.cveCountCard, classes.totalSeverity].join(' ')}>Total {total}</div>
</Tooltip>
<div className={classes.severityList}> <div className={classes.severityList}>
<Tooltip title="Critical"> <Tooltip title="Critical" onClick={() => filterBySeverity('CRITICAL')}>
<div className={[classes.cveCountCard, classes.criticalSeverity].join(' ')}>C {critical}</div> <div className={[classes.cveCountCard, classes.criticalSeverity].join(' ')}>C {critical}</div>
</Tooltip> </Tooltip>
<Tooltip title="High"> <Tooltip title="High" onClick={() => filterBySeverity('HIGH')}>
<div className={[classes.cveCountCard, classes.highSeverity].join(' ')}>H {high}</div> <div className={[classes.cveCountCard, classes.highSeverity].join(' ')}>H {high}</div>
</Tooltip> </Tooltip>
<Tooltip title="Medium"> <Tooltip title="Medium" onClick={() => filterBySeverity('MEDIUM')}>
<div className={[classes.cveCountCard, classes.mediumSeverity].join(' ')}>M {medium}</div> <div className={[classes.cveCountCard, classes.mediumSeverity].join(' ')}>M {medium}</div>
</Tooltip> </Tooltip>
<Tooltip title="Low"> <Tooltip title="Low" onClick={() => filterBySeverity('LOW')}>
<div className={[classes.cveCountCard, classes.lowSeverity].join(' ')}>L {low}</div> <div className={[classes.cveCountCard, classes.lowSeverity].join(' ')}>L {low}</div>
</Tooltip> </Tooltip>
<Tooltip title="Unknown"> <Tooltip title="Unknown" onClick={() => filterBySeverity('UNKNOWN')}>
<div className={[classes.cveCountCard, classes.unknownSeverity].join(' ')}>U {unknown}</div> <div className={[classes.cveCountCard, classes.unknownSeverity].join(' ')}>U {unknown}</div>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -158,6 +158,7 @@ function VulnerabilitiesDetails(props) {
// pagination props // pagination props
const [cveFilter, setCveFilter] = useState(''); const [cveFilter, setCveFilter] = useState('');
const [cveExcludeFilter, setCveExcludeFilter] = useState(''); const [cveExcludeFilter, setCveExcludeFilter] = useState('');
const [cveSeverityFilter, setCveSeverityFilter] = useState('');
const [pageNumber, setPageNumber] = useState(1); const [pageNumber, setPageNumber] = useState(1);
const [isEndOfList, setIsEndOfList] = useState(false); const [isEndOfList, setIsEndOfList] = useState(false);
const listBottom = useRef(null); const listBottom = useRef(null);
@ -178,7 +179,8 @@ function VulnerabilitiesDetails(props) {
getCVERequestName(), getCVERequestName(),
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE }, { pageNumber, pageSize: EXPLORE_PAGE_SIZE },
cveFilter, cveFilter,
cveExcludeFilter cveExcludeFilter,
cveSeverityFilter
)}`, )}`,
abortController.signal abortController.signal
) )
@ -321,7 +323,7 @@ function VulnerabilitiesDetails(props) {
useEffect(() => { useEffect(() => {
if (isLoading) return; if (isLoading) return;
resetPagination(); resetPagination();
}, [cveFilter, cveExcludeFilter]); }, [cveFilter, cveExcludeFilter, cveSeverityFilter]);
useEffect(() => { useEffect(() => {
return () => { return () => {
@ -352,8 +354,6 @@ function VulnerabilitiesDetails(props) {
return; return;
} }
console.log('Test');
return !isEmpty(cveSummary) ? ( return !isEmpty(cveSummary) ? (
<VulnerabilityCountCard <VulnerabilityCountCard
total={cveSummary.Count} total={cveSummary.Count}
@ -362,6 +362,7 @@ function VulnerabilitiesDetails(props) {
medium={cveSummary.MediumCount} medium={cveSummary.MediumCount}
low={cveSummary.LowCount} low={cveSummary.LowCount}
unknown={cveSummary.UnknownCount} unknown={cveSummary.UnknownCount}
filterBySeverity={setCveSeverityFilter}
/> />
) : ( ) : (
<></> <></>