feat(cve): filter cves by severity
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
parent
c268991495
commit
e037c6c577
@ -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 />);
|
||||||
|
11
src/api.js
11
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}}}}`,
|
`/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) =>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
Loading…
Reference in New Issue
Block a user