Compare commits
7 Commits
commit-12b
...
commit-0ed
Author | SHA1 | Date | |
---|---|---|---|
0edfe0f73a | |||
f4a6030d93 | |||
9358539e0c | |||
5bf7d5652c | |||
12f9229320 | |||
df19fa811c | |||
6cda89c710 |
@ -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: {
|
||||||
|
@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
const mockMgmtResponse = {
|
const mockMgmtResponse = {
|
||||||
distSpecVersion: '1.1.0-dev',
|
distSpecVersion: '1.1.0-dev',
|
||||||
binaryType: '-apikey-lint-metrics-mgmt-scrub-search-sync-ui-userprefs',
|
binaryType: '-apikey-lint-metrics-mgmt-scrub-search-sync-ui-userprefs',
|
||||||
http: { auth: { htpasswd: {} } }
|
http: { auth: { htpasswd: {}, openid: { providers: { github: {} } } } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// useNavigate mock
|
// useNavigate mock
|
||||||
@ -55,6 +55,7 @@ describe('Sign in form', () => {
|
|||||||
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
fireEvent.change(passwordInput, { target: { value: 'test' } });
|
||||||
expect(usernameInput).toHaveValue('test');
|
expect(usernameInput).toHaveValue('test');
|
||||||
expect(passwordInput).toHaveValue('test');
|
expect(passwordInput).toHaveValue('test');
|
||||||
|
expect(screen.getByTestId('openid-divider')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should display error if username and password values are empty after change', async () => {
|
it('should display error if username and password values are empty after change', async () => {
|
||||||
|
@ -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',
|
||||||
@ -475,6 +483,12 @@ const mockCVEFixed = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
pageNotFixed: {
|
||||||
|
ImageListWithCVEFixed: {
|
||||||
|
Page: { TotalCount: 0, ItemCount: 0 },
|
||||||
|
Results: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -496,14 +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(/fixed in/i)).toHaveLength(20));
|
await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1));
|
||||||
|
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 } });
|
||||||
@ -512,28 +535,41 @@ describe('Vulnerabilties page', () => {
|
|||||||
expect((await screen.findAllByText(/2022/i)).length === 6);
|
expect((await screen.findAllByText(/2022/i)).length === 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have a collapsable search bar', async () => {
|
||||||
|
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 />);
|
||||||
|
const cveSearchInput = screen.getByPlaceholderText(/search/i);
|
||||||
|
const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0];
|
||||||
|
await fireEvent.click(expandSearch);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getAllByPlaceholderText("Exclude")).toHaveLength(1)
|
||||||
|
);
|
||||||
|
const excludeInput = screen.getByPlaceholderText("Exclude");
|
||||||
|
userEvent.type(excludeInput, '2022');
|
||||||
|
expect((await screen.findAllByText(/2022/i)).length === 0);
|
||||||
|
})
|
||||||
|
|
||||||
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));
|
||||||
});
|
});
|
||||||
|
|
||||||
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 () => {
|
||||||
@ -551,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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -564,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');
|
||||||
@ -580,22 +617,38 @@ describe('Vulnerabilties page', () => {
|
|||||||
await fireEvent.click(exportAsCSVBtn);
|
await fireEvent.click(exportAsCSVBtn);
|
||||||
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
||||||
fireEvent.click(downloadBtn[0]);
|
fireEvent.click(downloadBtn[0]);
|
||||||
const exportAsExcelBtn = screen.getByText(/MS Excel/i);
|
const exportAsExcelBtn = screen.getByText(/xlsx/i);
|
||||||
expect(exportAsExcelBtn).toBeInTheDocument();
|
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||||
await fireEvent.click(exportAsExcelBtn);
|
await fireEvent.click(exportAsExcelBtn);
|
||||||
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should expand/collapse the list of CVEs', async () => {
|
||||||
|
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(<StateVulnerabilitiesWrapper />);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
|
||||||
|
const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon');
|
||||||
|
fireEvent.click(collapseListBtn[0]);
|
||||||
|
expect(await screen.findByText('Fixed in')).not.toBeVisible();
|
||||||
|
const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon');
|
||||||
|
fireEvent.click(expandListBtn[0]);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20));
|
||||||
|
});
|
||||||
|
|
||||||
it('should handle fixed CVE query errors', async () => {
|
it('should handle fixed CVE query errors', async () => {
|
||||||
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));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -90,14 +90,17 @@ 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 = '') => {
|
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '', excludedTerm = '') => {
|
||||||
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
|
||||||
}}`;
|
}}`;
|
||||||
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}}}}`;
|
if (!isEmpty(excludedTerm)) {
|
||||||
|
query += `, excludedCVE: "${excludedTerm}"`;
|
||||||
|
}
|
||||||
|
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}}}}`,
|
||||||
|
@ -312,7 +312,13 @@ export default function SignIn({ isLoggedIn, setIsLoggedIn, wrapperSetLoading =
|
|||||||
Welcome back! Please login.
|
Welcome back! Please login.
|
||||||
</Typography>
|
</Typography>
|
||||||
{renderThirdPartyLoginMethods()}
|
{renderThirdPartyLoginMethods()}
|
||||||
{Object.keys(authMethods).length > 1 && <Divider className={classes.divider}>or</Divider>}
|
{Object.keys(authMethods).length > 1 &&
|
||||||
|
Object.keys(authMethods).includes('openid') &&
|
||||||
|
Object.keys(authMethods.openid.providers).length > 0 && (
|
||||||
|
<Divider className={classes.divider} data-testId="openid-divider">
|
||||||
|
or
|
||||||
|
</Divider>
|
||||||
|
)}
|
||||||
{Object.keys(authMethods).includes('htpasswd') && (
|
{Object.keys(authMethods).includes('htpasswd') && (
|
||||||
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
<Box component="form" onSubmit={null} noValidate autoComplete="off">
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -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',
|
||||||
@ -66,15 +101,21 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
},
|
},
|
||||||
|
dropdownCVE: {
|
||||||
|
color: '#1479FF',
|
||||||
|
cursor: 'pointer'
|
||||||
|
},
|
||||||
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 } = props;
|
const { cve, name, platform, expand } = props;
|
||||||
const [openDesc, setOpenDesc] = useState(false);
|
const [openCVE, setOpenCVE] = useState(expand);
|
||||||
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(), []);
|
||||||
@ -84,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);
|
||||||
@ -120,7 +161,11 @@ function VulnerabilitiyCard(props) {
|
|||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
};
|
};
|
||||||
}, [openFixed, pageNumber]);
|
}, [pageNumber]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpenCVE(expand);
|
||||||
|
}, [expand]);
|
||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (loadingFixed || isEndOfList) return;
|
if (loadingFixed || isEndOfList) return;
|
||||||
@ -163,27 +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'}>
|
||||||
<Typography variant="body1" align="left" className={classes.cveId}>
|
{!openCVE ? (
|
||||||
|
<KeyboardArrowRight className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
|
||||||
|
) : (
|
||||||
|
<KeyboardArrowDown className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} />
|
||||||
|
)}
|
||||||
|
<Typography variant="body1" align="left" className={openCVE ? classes.cveId : classes.cveIdCollapsed}>
|
||||||
{cve.id}
|
{cve.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
{openCVE ? (
|
||||||
</Stack>
|
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||||
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
|
||||||
{cve.title}
|
|
||||||
</Typography>
|
|
||||||
<Divider className={classes.vulnerabilityCardDivider} />
|
|
||||||
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
|
|
||||||
{!openFixed ? (
|
|
||||||
<KeyboardArrowRight className={classes.dropdownText} />
|
|
||||||
) : (
|
) : (
|
||||||
<KeyboardArrowDown className={classes.dropdownText} />
|
<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>
|
||||||
)}
|
)}
|
||||||
<Typography className={classes.dropdownText}>Fixed in</Typography>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Collapse in={openFixed} timeout="auto" unmountOnExit>
|
<Collapse in={openCVE} timeout="auto" unmountOnExit>
|
||||||
|
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
||||||
|
{cve.title}
|
||||||
|
</Typography>
|
||||||
|
<Divider className={classes.vulnerabilityCardDivider} />
|
||||||
|
<Typography variant="body2" align="left" className={classes.cveInfo}>
|
||||||
|
External reference
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
align="left"
|
||||||
|
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>
|
||||||
|
{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...'
|
||||||
@ -194,16 +293,9 @@ 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}
|
||||||
|
92
src/components/Shared/VulnerabilityCountCard.jsx
Normal file
92
src/components/Shared/VulnerabilityCountCard.jsx
Normal 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;
|
@ -9,6 +9,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Typography,
|
Typography,
|
||||||
InputBase,
|
InputBase,
|
||||||
|
ToggleButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Divider,
|
Divider,
|
||||||
@ -26,16 +27,32 @@ import DownloadIcon from '@mui/icons-material/Download';
|
|||||||
|
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import exportFromJSON from 'export-from-json';
|
import exportFromJSON from 'export-from-json';
|
||||||
|
import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline';
|
||||||
|
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
|
||||||
|
|
||||||
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
|
import Collapse from '@mui/material/Collapse';
|
||||||
|
|
||||||
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',
|
||||||
@ -64,6 +81,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',
|
||||||
@ -71,6 +89,20 @@ 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: {
|
||||||
|
alignContent: 'right',
|
||||||
|
variant: 'outlined'
|
||||||
|
},
|
||||||
|
viewModes: {
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
maxWidth: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'right'
|
||||||
|
},
|
||||||
searchIcon: {
|
searchIcon: {
|
||||||
color: '#52637A',
|
color: '#52637A',
|
||||||
paddingRight: '3%'
|
paddingRight: '3%'
|
||||||
@ -87,15 +119,27 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
opacity: '1'
|
opacity: '1'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
export: {
|
|
||||||
alignContent: 'right'
|
|
||||||
},
|
|
||||||
popper: {
|
popper: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
padding: '0.3rem',
|
padding: '0.3rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
justifyContent: 'left'
|
||||||
|
},
|
||||||
|
dropdownArrowBox: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
|
},
|
||||||
|
dropdownText: {
|
||||||
|
color: '#1479FF',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: '600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
width: '95%'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -103,13 +147,17 @@ 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(), []);
|
||||||
const { name, tag, digest, platform } = props;
|
const { name, tag, digest, platform } = props;
|
||||||
|
|
||||||
|
const [openExcludeSearch, setOpenExcludeSearch] = useState(false);
|
||||||
|
|
||||||
// pagination props
|
// pagination props
|
||||||
const [cveFilter, setCveFilter] = useState('');
|
const [cveFilter, setCveFilter] = useState('');
|
||||||
|
const [cveExcludeFilter, setCveExcludeFilter] = 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);
|
||||||
@ -117,6 +165,8 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const [anchorExport, setAnchorExport] = useState(null);
|
const [anchorExport, setAnchorExport] = useState(null);
|
||||||
const openExport = Boolean(anchorExport);
|
const openExport = Boolean(anchorExport);
|
||||||
|
|
||||||
|
const [selectedViewMore, setSelectedViewMore] = useState(true);
|
||||||
|
|
||||||
const getCVERequestName = () => {
|
const getCVERequestName = () => {
|
||||||
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
|
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
|
||||||
};
|
};
|
||||||
@ -127,16 +177,31 @@ function VulnerabilitiesDetails(props) {
|
|||||||
`${host()}${endpoints.vulnerabilitiesForRepo(
|
`${host()}${endpoints.vulnerabilitiesForRepo(
|
||||||
getCVERequestName(),
|
getCVERequestName(),
|
||||||
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
|
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
|
||||||
cveFilter
|
cveFilter,
|
||||||
|
cveExcludeFilter
|
||||||
)}`,
|
)}`,
|
||||||
abortController.signal
|
abortController.signal
|
||||||
)
|
)
|
||||||
.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);
|
||||||
}
|
}
|
||||||
@ -146,6 +211,7 @@ function VulnerabilitiesDetails(props) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setCveData([]);
|
setCveData([]);
|
||||||
|
setCVESummary(() => {});
|
||||||
setIsEndOfList(true);
|
setIsEndOfList(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -182,7 +248,7 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const wb = XLSX.utils.book_new(),
|
const wb = XLSX.utils.book_new(),
|
||||||
ws = XLSX.utils.json_to_sheet(allCveData);
|
ws = XLSX.utils.json_to_sheet(allCveData);
|
||||||
|
|
||||||
XLSX.utils.book_append_sheet(wb, ws, name + '_' + tag);
|
XLSX.utils.book_append_sheet(wb, ws, name.replaceAll('/', '_') + '_' + tag);
|
||||||
|
|
||||||
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
||||||
|
|
||||||
@ -211,7 +277,17 @@ function VulnerabilitiesDetails(props) {
|
|||||||
setAnchorExport(null);
|
setAnchorExport(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExpandCVESearch = () => {
|
||||||
|
setOpenExcludeSearch((openExcludeSearch) => !openExcludeSearch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCveExcludeFilterChange = (e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
setCveExcludeFilter(value);
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
||||||
|
const debouncedExcludeFilterChangeHandler = useMemo(() => debounce(handleCveExcludeFilterChange, 300));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getPaginatedCVEs();
|
getPaginatedCVEs();
|
||||||
@ -245,12 +321,13 @@ function VulnerabilitiesDetails(props) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
resetPagination();
|
resetPagination();
|
||||||
}, [cveFilter]);
|
}, [cveFilter, cveExcludeFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
abortController.abort();
|
abortController.abort();
|
||||||
debouncedChangeHandler.cancel();
|
debouncedChangeHandler.cancel();
|
||||||
|
debouncedExcludeFilterChangeHandler.cancel();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -263,13 +340,34 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const renderCVEs = () => {
|
const renderCVEs = () => {
|
||||||
return !isEmpty(cveData) ? (
|
return !isEmpty(cveData) ? (
|
||||||
cveData.map((cve, index) => {
|
cveData.map((cve, index) => {
|
||||||
return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} />;
|
return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} expand={selectedViewMore} />;
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>
|
<div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 />;
|
||||||
@ -286,14 +384,36 @@ function VulnerabilitiesDetails(props) {
|
|||||||
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
||||||
Vulnerabilities
|
Vulnerabilities
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton disableRipple onClick={handleClickExport} className={classes.export}>
|
<Stack direction="row" spacing="1rem" className={classes.viewModes}>
|
||||||
<DownloadIcon />
|
<IconButton disableRipple onClick={handleClickExport}>
|
||||||
</IconButton>
|
<DownloadIcon />
|
||||||
<Snackbar
|
</IconButton>
|
||||||
open={openExport && isLoadingAllCve}
|
<Snackbar
|
||||||
message="Getting your data ready for export"
|
open={openExport && isLoadingAllCve}
|
||||||
action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />}
|
message="Getting your data ready for export"
|
||||||
/>
|
action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />}
|
||||||
|
/>
|
||||||
|
<ToggleButton
|
||||||
|
value="viewLess"
|
||||||
|
title="Collapse list view"
|
||||||
|
size="small"
|
||||||
|
className={classes.view}
|
||||||
|
selected={!selectedViewMore}
|
||||||
|
onChange={() => setSelectedViewMore(false)}
|
||||||
|
>
|
||||||
|
<ViewHeadlineIcon />
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
value="viewMore"
|
||||||
|
title="Expand list view"
|
||||||
|
size="small"
|
||||||
|
className={classes.view}
|
||||||
|
selected={selectedViewMore}
|
||||||
|
onChange={() => setSelectedViewMore(true)}
|
||||||
|
>
|
||||||
|
<ViewAgendaIcon />
|
||||||
|
</ToggleButton>
|
||||||
|
</Stack>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={anchorExport}
|
anchorEl={anchorExport}
|
||||||
open={openExport}
|
open={openExport}
|
||||||
@ -325,22 +445,46 @@ function VulnerabilitiesDetails(props) {
|
|||||||
className={classes.popper}
|
className={classes.popper}
|
||||||
data-testid="export-excel-menuItem"
|
data-testid="export-excel-menuItem"
|
||||||
>
|
>
|
||||||
MS Excel
|
xlsx
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack className={classes.search}>
|
{renderCVESummary()}
|
||||||
<InputBase
|
<Stack direction="row">
|
||||||
placeholder={'Search'}
|
<div className={classes.dropdownArrowBox} onClick={handleExpandCVESearch}>
|
||||||
classes={{ root: classes.searchInputBase, input: classes.input }}
|
{!openExcludeSearch ? (
|
||||||
onChange={debouncedChangeHandler}
|
<KeyboardArrowRight className={classes.dropdownText} />
|
||||||
/>
|
) : (
|
||||||
<div className={classes.searchIcon}>
|
<KeyboardArrowDown className={classes.dropdownText} />
|
||||||
<SearchIcon />
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Stack className={classes.test} direction="column" spacing="0.25em">
|
||||||
|
<Stack className={classes.search}>
|
||||||
|
<InputBase
|
||||||
|
placeholder={'Search'}
|
||||||
|
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||||
|
onChange={debouncedChangeHandler}
|
||||||
|
/>
|
||||||
|
<div className={classes.searchIcon}>
|
||||||
|
<SearchIcon />
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Collapse in={openExcludeSearch} timeout="auto" unmountOnExit>
|
||||||
|
<Stack className={classes.search}>
|
||||||
|
<InputBase
|
||||||
|
placeholder={'Exclude'}
|
||||||
|
classes={{ root: classes.searchInputBase, input: classes.input }}
|
||||||
|
onChange={debouncedExcludeFilterChangeHandler}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Collapse>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="column" spacing={selectedViewMore ? '1rem' : '0.5rem'}>
|
||||||
|
{renderCVEs()}
|
||||||
|
{renderListBottom()}
|
||||||
</Stack>
|
</Stack>
|
||||||
{renderCVEs()}
|
|
||||||
{renderListBottom()}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -37,7 +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(await page.getByText('CVE-').count()).toBeGreaterThan(0);
|
await expect(page.getByText(/CVE-/).nth(0)).toBeVisible({ timeout: 100000 });
|
||||||
await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
await expect(await page.getByText(/CVE-/).count()).toBeGreaterThan(0);
|
||||||
|
await expect(await page.getByText(/CVE-/).count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user