feat(cve): add more information

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea-Lupu 2024-02-05 09:35:29 +02:00 committed by Ramkumar Chinchani
parent f4a6030d93
commit 0edfe0f73a
6 changed files with 177 additions and 79 deletions

View File

@ -483,6 +483,12 @@ const mockCVEFixed = {
} }
] ]
} }
},
pageNotFixed: {
ImageListWithCVEFixed: {
Page: { TotalCount: 0, ItemCount: 0 },
Results: []
}
} }
}; };
@ -504,15 +510,23 @@ afterEach(() => {
describe('Vulnerabilties page', () => { describe('Vulnerabilties page', () => {
it('renders the vulnerabilities if there are any', async () => { it('renders the vulnerabilities if there are any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
for (let i=0; i<21; i++) {
getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
}
getCall.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('Total 5')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/Fixed in/)).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').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
for (let i=0; i<21; i++) {
getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
}
getCall.mockResolvedValue({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
const cveSearchInput = screen.getByPlaceholderText(/search/i); const cveSearchInput = screen.getByPlaceholderText(/search/i);
jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } }); jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
@ -522,7 +536,11 @@ describe('Vulnerabilties page', () => {
}); });
it('should have a collapsable search bar', async () => { it('should have a collapsable search bar', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
for (let i=0; i<21; i++) {
getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
}
getCall.mockResolvedValue({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
const cveSearchInput = screen.getByPlaceholderText(/search/i); const cveSearchInput = screen.getByPlaceholderText(/search/i);
const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0]; const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0];
@ -544,19 +562,14 @@ describe('Vulnerabilties page', () => {
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
}); });
it('should open and close description dropdown for vulnerabilities', async () => { it('should show description for vulnerabilities', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
.mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText(/description/i)).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20));
const openText = screen.getAllByText(/description/i);
await fireEvent.click(openText[0]);
await waitFor(() => await waitFor(() =>
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1) expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
); );
await fireEvent.click(openText[0]);
await waitFor(() =>
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
);
}); });
it("should log an error when data can't be fetched", async () => { it("should log an error when data can't be fetched", async () => {
@ -574,12 +587,12 @@ describe('Vulnerabilties page', () => {
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } }); .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument());
const loadMoreBtn = screen.getByText(/load more/i); await waitFor(() => expect(screen.getAllByText(/load more/i).length).toBeGreaterThan(0));
expect(loadMoreBtn).toBeInTheDocument(); const nrLoadButtons = screen.getAllByText(/load more/i).length
const loadMoreBtn = screen.getAllByText(/load more/i)[0];
await fireEvent.click(loadMoreBtn); await fireEvent.click(loadMoreBtn);
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument()); await waitFor(() => expect(screen.getAllByText(/load more/i).length).toBe(nrLoadButtons-1));
expect(await screen.findByText('latest')).toBeInTheDocument(); expect(await screen.findByText('latest')).toBeInTheDocument();
}); });
@ -587,10 +600,11 @@ describe('Vulnerabilties page', () => {
const xlsxMock = jest.createMockFromModule('xlsx'); const xlsxMock = jest.createMockFromModule('xlsx');
xlsxMock.writeFile = jest.fn(); xlsxMock.writeFile = jest.fn();
jest let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
.spyOn(api, 'get') for (let i=0; i<21; i++) {
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); }
getCall.mockResolvedValueOnce({ 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));
const downloadBtn = await screen.findAllByTestId('DownloadIcon'); const downloadBtn = await screen.findAllByTestId('DownloadIcon');
@ -610,10 +624,11 @@ describe('Vulnerabilties page', () => {
}); });
it('should expand/collapse the list of CVEs', async () => { it('should expand/collapse the list of CVEs', async () => {
jest let getCall = jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
.spyOn(api, 'get') for (let i=0; i<21; i++) {
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) getCall.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); }
getCall.mockResolvedValueOnce({ 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('Fixed in')).toHaveLength(20)); await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
@ -629,12 +644,11 @@ describe('Vulnerabilties page', () => {
jest jest
.spyOn(api, 'get') .spyOn(api, 'get')
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
.mockRejectedValue({ status: 500, data: {} }); .mockRejectedValueOnce({ status: 500, data: {} });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
const error = jest.spyOn(console, 'error').mockImplementation(() => {}); const error = jest.spyOn(console, 'error').mockImplementation(() => {});
await fireEvent.click(screen.getAllByText(/fixed in/i)[0]); await waitFor(() => expect(screen.getAllByText(/not fixed/i).length).toBeGreaterThan(0));
await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument()); await waitFor(() => expect(error).toBeCalled());
await waitFor(() => expect(error).toBeCalledTimes(1));
}); });
}); });

View File

@ -100,7 +100,7 @@ const endpoints = {
if (!isEmpty(excludedTerm)) { if (!isEmpty(excludedTerm)) {
query += `, excludedCVE: "${excludedTerm}"`; query += `, excludedCVE: "${excludedTerm}"`;
} }
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity 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) =>
`/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

@ -29,18 +29,46 @@ const useStyles = makeStyles((theme) => ({
marginTop: '2rem', marginTop: '2rem',
marginBottom: '2rem' marginBottom: '2rem'
}, },
cardCollapsed: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: '#FFFFFF',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
border: '1px solid #E0E5EB',
borderRadius: '0.75rem',
flex: 'none',
alignSelf: 'stretch',
width: '100%'
},
content: { content: {
textAlign: 'left', textAlign: 'left',
color: '#606060', color: '#606060',
padding: '2% 3% 2% 3%', padding: '2% 3% 2% 3%',
width: '100%' width: '100%'
}, },
contentCollapsed: {
textAlign: 'left',
color: '#606060',
padding: '1% 3% 1% 3%',
width: '100%',
'&:last-child': {
paddingBottom: '1%'
}
},
cveId: { cveId: {
color: theme.palette.primary.main, color: theme.palette.primary.main,
fontSize: '1rem', fontSize: '1rem',
fontWeight: 400, fontWeight: 400,
textDecoration: 'underline' textDecoration: 'underline'
}, },
cveIdCollapsed: {
color: theme.palette.primary.main,
fontSize: '0.75rem',
fontWeight: 500,
textDecoration: 'underline',
flexBasis: '19%'
},
cveSummary: { cveSummary: {
color: theme.palette.secondary.dark, color: theme.palette.secondary.dark,
fontSize: '0.75rem', fontSize: '0.75rem',
@ -48,6 +76,13 @@ const useStyles = makeStyles((theme) => ({
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
marginTop: '0.5rem' marginTop: '0.5rem'
}, },
cveSummaryCollapsed: {
color: theme.palette.secondary.dark,
fontSize: '0.75rem',
fontWeight: '600',
textOverflow: 'ellipsis',
flexBasis: '82%'
},
link: { link: {
color: '#52637A', color: '#52637A',
fontSize: '1rem', fontSize: '1rem',
@ -72,14 +107,15 @@ const useStyles = makeStyles((theme) => ({
}, },
vulnerabilityCardDivider: { vulnerabilityCardDivider: {
margin: '1rem 0' margin: '1rem 0'
},
cveInfo: {
marginTop: '2%'
} }
})); }));
function VulnerabilitiyCard(props) { function VulnerabilitiyCard(props) {
const classes = useStyles(); const classes = useStyles();
const { cve, name, platform, expand } = props; const { cve, name, platform, expand } = props;
const [openCVE, setOpenCVE] = useState(expand); const [openCVE, setOpenCVE] = useState(expand);
const [openDesc, setOpenDesc] = useState(false);
const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true); const [loadingFixed, setLoadingFixed] = useState(true);
const [fixedInfo, setFixedInfo] = useState([]); const [fixedInfo, setFixedInfo] = useState([]);
const abortController = useMemo(() => new AbortController(), []); const abortController = useMemo(() => new AbortController(), []);
@ -89,7 +125,7 @@ function VulnerabilitiyCard(props) {
const [isEndOfList, setIsEndOfList] = useState(false); const [isEndOfList, setIsEndOfList] = useState(false);
const getPaginatedResults = () => { const getPaginatedResults = () => {
if (!openFixed || isEndOfList) { if (isEndOfList) {
return; return;
} }
setLoadingFixed(true); setLoadingFixed(true);
@ -125,7 +161,7 @@ function VulnerabilitiyCard(props) {
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [openFixed, pageNumber]); }, [pageNumber]);
useEffect(() => { useEffect(() => {
setOpenCVE(expand); setOpenCVE(expand);
@ -172,33 +208,81 @@ function VulnerabilitiyCard(props) {
}; };
return ( return (
<Card className={classes.card} raised> <Card className={openCVE ? classes.card : classes.cardCollapsed} raised>
<CardContent className={classes.content}> <CardContent className={openCVE ? classes.content : classes.contentCollapsed}>
<Stack direction="row" spacing="1.25rem"> <Stack direction="row" spacing={openCVE ? '1.25rem' : '0.5rem'}>
{!openCVE ? ( {!openCVE ? (
<KeyboardArrowRight className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} /> <KeyboardArrowRight className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
) : ( ) : (
<KeyboardArrowDown className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} /> <KeyboardArrowDown className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
)} )}
<Typography variant="body1" align="left" className={classes.cveId}> <Typography variant="body1" align="left" className={openCVE ? classes.cveId : classes.cveIdCollapsed}>
{cve.id} {cve.id}
</Typography> </Typography>
{openCVE ? (
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} /> <VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
) : (
<Stack direction="row" spacing="0.5rem" flexBasis="90%">
<div style={{ transform: 'scale(0.8)', flexBasis: '18%', flexShrink: '0' }}>
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
</div>
<Typography variant="body1" align="left" className={classes.cveSummaryCollapsed}>
{cve.title}
</Typography>
</Stack>
)}
</Stack> </Stack>
<Collapse in={openCVE} timeout="auto" unmountOnExit> <Collapse in={openCVE} timeout="auto" unmountOnExit>
<Typography variant="body1" align="left" className={classes.cveSummary}> <Typography variant="body1" align="left" className={classes.cveSummary}>
{cve.title} {cve.title}
</Typography> </Typography>
<Divider className={classes.vulnerabilityCardDivider} /> <Divider className={classes.vulnerabilityCardDivider} />
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}> <Typography variant="body2" align="left" className={classes.cveInfo}>
{!openFixed ? ( External reference
<KeyboardArrowRight className={classes.dropdownText} /> </Typography>
) : ( <Typography
<KeyboardArrowDown className={classes.dropdownText} /> variant="body2"
)} align="left"
<Typography className={classes.dropdownText}>Fixed in</Typography> sx={{ color: '#0F2139', fontSize: '1rem', textDecoration: 'underline' }}
component={Link}
to={cve.reference}
target="_blank"
rel="noreferrer"
>
{cve.reference}
</Typography>
<Typography variant="body2" align="left" className={classes.cveInfo}>
Packages
</Typography>
<Stack direction="column" sx={{ width: '100%', padding: '0.5rem 0' }}>
<Stack direction="row" spacing="1.25rem" display="flex">
<Typography variant="body1" flexBasis="33.33%">
Name
</Typography>
<Typography variant="body1" flexBasis="33.33%" textAlign="right">
Installed Version
</Typography>
<Typography variant="body1" flexBasis="33.33%" textAlign="right">
Fixed Version
</Typography>
</Stack> </Stack>
<Collapse in={openFixed} timeout="auto" unmountOnExit> {cve.packageList.map((el) => (
<Stack direction="row" key={cve.packageName} spacing="1.25rem" display="flex">
<Typography variant="body1" color="primary" flexBasis="33.33%">
{el.packageName}
</Typography>
<Typography variant="body1" color="primary" flexBasis="33.33%" textAlign="right">
{el.packageInstalledVersion}
</Typography>
<Typography variant="body1" color="primary" flexBasis="33.33%" textAlign="right">
{el.packageFixedVersion}
</Typography>
</Stack>
))}
</Stack>
<Typography variant="body2" align="left" className={classes.cveInfo}>
Fixed in
</Typography>
<Box sx={{ width: '100%', padding: '0.5rem 0' }}> <Box sx={{ width: '100%', padding: '0.5rem 0' }}>
{loadingFixed ? ( {loadingFixed ? (
'Loading...' 'Loading...'
@ -209,23 +293,15 @@ function VulnerabilitiyCard(props) {
</Stack> </Stack>
)} )}
</Box> </Box>
</Collapse> <Typography variant="body2" align="left" className={classes.cveInfo}>
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}> Description
{!openDesc ? ( </Typography>
<KeyboardArrowRight className={classes.dropdownText} />
) : (
<KeyboardArrowDown className={classes.dropdownText} />
)}
<Typography className={classes.dropdownText}>Description</Typography>
</Stack>
<Collapse in={openDesc} timeout="auto" unmountOnExit>
<Box sx={{ padding: '0.5rem 0' }}> <Box sx={{ padding: '0.5rem 0' }}>
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}> <Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
{cve.description} {cve.description}
</Typography> </Typography>
</Box> </Box>
</Collapse> </Collapse>
</Collapse>
</CardContent> </CardContent>
</Card> </Card>
); );

View File

@ -481,9 +481,11 @@ function VulnerabilitiesDetails(props) {
</Collapse> </Collapse>
</Stack> </Stack>
</Stack> </Stack>
<Stack direction="column" spacing={selectedViewMore ? '1rem' : '0.5rem'}>
{renderCVEs()} {renderCVEs()}
{renderListBottom()} {renderListBottom()}
</Stack> </Stack>
</Stack>
); );
} }

View File

@ -96,7 +96,13 @@ const mapCVEInfo = (cveInfo) => {
id: cve.Id, id: cve.Id,
severity: cve.Severity, severity: cve.Severity,
title: cve.Title, title: cve.Title,
description: cve.Description description: cve.Description,
reference: cve.Reference,
packageList: cve.PackageList?.map((pkg) => ({
packageName: pkg.Name,
packageInstalledVersion: pkg.InstalledVersion,
packageFixedVersion: pkg.FixedVersion
}))
}; };
}); });
return cveList; return cveList;

View File

@ -37,8 +37,8 @@ test.describe('Tag page test', () => {
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`); await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
await page.getByRole('tab', { name: 'Vulnerabilities' }).click(); await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 });
await expect(page.getByText('CVE-').nth(0)).toBeVisible({ timeout: 100000 }); await expect(page.getByText(/CVE-/).nth(0)).toBeVisible({ timeout: 100000 });
await expect(await page.getByText('CVE-').count()).toBeGreaterThan(0); await expect(await page.getByText(/CVE-/).count()).toBeGreaterThan(0);
await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE); await expect(await page.getByText(/CVE-/).count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
}); });
}); });