diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
index 6bdb0508..5c980ca5 100644
--- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
+++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
@@ -483,6 +483,12 @@ const mockCVEFixed = {
}
]
}
+ },
+ pageNotFixed: {
+ ImageListWithCVEFixed: {
+ Page: { TotalCount: 0, ItemCount: 0 },
+ Results: []
+ }
}
};
@@ -504,15 +510,23 @@ afterEach(() => {
describe('Vulnerabilties page', () => {
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();
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/)).toHaveLength(20));
});
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();
const cveSearchInput = screen.getByPlaceholderText(/search/i);
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 () => {
- 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();
const cveSearchInput = screen.getByPlaceholderText(/search/i);
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));
});
- it('should open and close description dropdown for vulnerabilities', async () => {
- jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
+ it('should show description for vulnerabilities', async () => {
+ jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
+ .mockResolvedValue({ status: 200, data: { data: mockCVEFixed.pageNotFixed } });
render();
- await waitFor(() => expect(screen.getAllByText(/description/i)).toHaveLength(20));
- const openText = screen.getAllByText(/description/i);
- await fireEvent.click(openText[0]);
+ await waitFor(() => expect(screen.getAllByText(/Description/)).toHaveLength(20));
await waitFor(() =>
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 () => {
@@ -574,12 +587,12 @@ describe('Vulnerabilties page', () => {
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
render();
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());
- const loadMoreBtn = screen.getByText(/load more/i);
- expect(loadMoreBtn).toBeInTheDocument();
+ await waitFor(() => expect(screen.getAllByText(/load more/i).length).toBeGreaterThan(0));
+ const nrLoadButtons = screen.getAllByText(/load more/i).length
+ const loadMoreBtn = screen.getAllByText(/load more/i)[0];
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();
});
@@ -587,10 +600,11 @@ describe('Vulnerabilties page', () => {
const xlsxMock = jest.createMockFromModule('xlsx');
xlsxMock.writeFile = jest.fn();
- jest
- .spyOn(api, 'get')
- .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
- .mockResolvedValueOnce({ 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.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
render();
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
@@ -610,10 +624,11 @@ describe('Vulnerabilties page', () => {
});
it('should expand/collapse the list of CVEs', async () => {
- jest
- .spyOn(api, 'get')
- .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
- .mockResolvedValueOnce({ 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.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } });
render();
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
@@ -629,12 +644,11 @@ describe('Vulnerabilties page', () => {
jest
.spyOn(api, 'get')
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
- .mockRejectedValue({ status: 500, data: {} });
+ .mockRejectedValueOnce({ status: 500, data: {} });
render();
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
- await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
- await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
- await waitFor(() => expect(error).toBeCalledTimes(1));
+ await waitFor(() => expect(screen.getAllByText(/not fixed/i).length).toBeGreaterThan(0));
+ await waitFor(() => expect(error).toBeCalled());
});
});
diff --git a/src/api.js b/src/api.js
index a8d9435a..0589e992 100644
--- a/src/api.js
+++ b/src/api.js
@@ -100,7 +100,7 @@ const endpoints = {
if (!isEmpty(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) =>
`/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/VulnerabilityCard.jsx b/src/components/Shared/VulnerabilityCard.jsx
index ea3a0a69..c201c789 100644
--- a/src/components/Shared/VulnerabilityCard.jsx
+++ b/src/components/Shared/VulnerabilityCard.jsx
@@ -29,18 +29,46 @@ const useStyles = makeStyles((theme) => ({
marginTop: '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: {
textAlign: 'left',
color: '#606060',
padding: '2% 3% 2% 3%',
width: '100%'
},
+ contentCollapsed: {
+ textAlign: 'left',
+ color: '#606060',
+ padding: '1% 3% 1% 3%',
+ width: '100%',
+ '&:last-child': {
+ paddingBottom: '1%'
+ }
+ },
cveId: {
color: theme.palette.primary.main,
fontSize: '1rem',
fontWeight: 400,
textDecoration: 'underline'
},
+ cveIdCollapsed: {
+ color: theme.palette.primary.main,
+ fontSize: '0.75rem',
+ fontWeight: 500,
+ textDecoration: 'underline',
+ flexBasis: '19%'
+ },
cveSummary: {
color: theme.palette.secondary.dark,
fontSize: '0.75rem',
@@ -48,6 +76,13 @@ const useStyles = makeStyles((theme) => ({
textOverflow: 'ellipsis',
marginTop: '0.5rem'
},
+ cveSummaryCollapsed: {
+ color: theme.palette.secondary.dark,
+ fontSize: '0.75rem',
+ fontWeight: '600',
+ textOverflow: 'ellipsis',
+ flexBasis: '82%'
+ },
link: {
color: '#52637A',
fontSize: '1rem',
@@ -72,14 +107,15 @@ const useStyles = makeStyles((theme) => ({
},
vulnerabilityCardDivider: {
margin: '1rem 0'
+ },
+ cveInfo: {
+ marginTop: '2%'
}
}));
function VulnerabilitiyCard(props) {
const classes = useStyles();
const { cve, name, platform, expand } = props;
const [openCVE, setOpenCVE] = useState(expand);
- const [openDesc, setOpenDesc] = useState(false);
- const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true);
const [fixedInfo, setFixedInfo] = useState([]);
const abortController = useMemo(() => new AbortController(), []);
@@ -89,7 +125,7 @@ function VulnerabilitiyCard(props) {
const [isEndOfList, setIsEndOfList] = useState(false);
const getPaginatedResults = () => {
- if (!openFixed || isEndOfList) {
+ if (isEndOfList) {
return;
}
setLoadingFixed(true);
@@ -125,7 +161,7 @@ function VulnerabilitiyCard(props) {
return () => {
abortController.abort();
};
- }, [openFixed, pageNumber]);
+ }, [pageNumber]);
useEffect(() => {
setOpenCVE(expand);
@@ -172,59 +208,99 @@ function VulnerabilitiyCard(props) {
};
return (
-
-
-
+
+
+
{!openCVE ? (
setOpenCVE(!openCVE)} />
) : (
setOpenCVE(!openCVE)} />
)}
-
+
{cve.id}
-
+ {openCVE ? (
+
+ ) : (
+
+
+
+
+
+ {cve.title}
+
+
+ )}
{cve.title}
- setOpenFixed(!openFixed)}>
- {!openFixed ? (
-
- ) : (
-
- )}
- Fixed in
-
-
-
- {loadingFixed ? (
- 'Loading...'
- ) : (
-
- {renderFixedVer()}
- {renderLoadMore()}
-
- )}
-
-
- setOpenDesc(!openDesc)}>
- {!openDesc ? (
-
- ) : (
-
- )}
- Description
-
-
-
-
- {cve.description}
+
+ External reference
+
+
+ {cve.reference}
+
+
+ Packages
+
+
+
+
+ Name
-
-
+
+ Installed Version
+
+
+ Fixed Version
+
+
+ {cve.packageList.map((el) => (
+
+
+ {el.packageName}
+
+
+ {el.packageInstalledVersion}
+
+
+ {el.packageFixedVersion}
+
+
+ ))}
+
+
+ Fixed in
+
+
+ {loadingFixed ? (
+ 'Loading...'
+ ) : (
+
+ {renderFixedVer()}
+ {renderLoadMore()}
+
+ )}
+
+
+ Description
+
+
+
+ {cve.description}
+
+
diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
index 0edc14df..38c1a614 100644
--- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
+++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
@@ -481,8 +481,10 @@ function VulnerabilitiesDetails(props) {
- {renderCVEs()}
- {renderListBottom()}
+
+ {renderCVEs()}
+ {renderListBottom()}
+
);
}
diff --git a/src/utilities/objectModels.js b/src/utilities/objectModels.js
index 4635fd60..6caa3f65 100644
--- a/src/utilities/objectModels.js
+++ b/src/utilities/objectModels.js
@@ -96,7 +96,13 @@ const mapCVEInfo = (cveInfo) => {
id: cve.Id,
severity: cve.Severity,
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;
diff --git a/tests/tag.spec.js b/tests/tag.spec.js
index 88c0e810..0f4fe41a 100644
--- a/tests/tag.spec.js
+++ b/tests/tag.spec.js
@@ -37,8 +37,8 @@ test.describe('Tag page test', () => {
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
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(await page.getByText('CVE-').count()).toBeGreaterThan(0);
- await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
+ 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()).toBeLessThanOrEqual(pageSizes.EXPLORE);
});
});