Compare commits
8 Commits
commit-2e1
...
commit-5bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bf7d5652c | ||
|
|
12f9229320 | ||
|
|
df19fa811c | ||
|
|
6cda89c710 | ||
|
|
12b474e126 | ||
|
|
a9db66bd34 | ||
|
|
f4600b8b79 | ||
|
|
c375c0697a |
103
package-lock.json
generated
103
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"downshift": "^6.1.12",
|
"downshift": "^6.1.12",
|
||||||
|
"export-from-json": "^1.7.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^2.5.2",
|
"luxon": "^2.5.2",
|
||||||
"markdown-to-jsx": "^7.1.7",
|
"markdown-to-jsx": "^7.1.7",
|
||||||
@@ -26,7 +27,8 @@
|
|||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-sticky-el": "^2.0.9",
|
"react-sticky-el": "^2.0.9",
|
||||||
"web-vitals": "^2.1.3"
|
"web-vitals": "^2.1.3",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||||
@@ -5592,6 +5594,14 @@
|
|||||||
"node": ">=8.9"
|
"node": ">=8.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/adler-32": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/agent-base": {
|
"node_modules/agent-base": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
@@ -6547,6 +6557,18 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cfb": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"crc-32": "~1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
@@ -6780,6 +6802,14 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/codepage": {
|
||||||
|
"version": "1.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||||
|
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/collect-v8-coverage": {
|
"node_modules/collect-v8-coverage": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
|
||||||
@@ -7030,6 +7060,17 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crc-32": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||||
|
"bin": {
|
||||||
|
"crc32": "bin/crc32.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.3",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||||
@@ -8926,6 +8967,11 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/export-from-json": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/export-from-json/-/export-from-json-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-Xg0L0saYz+CBz2MnaZvSEAHr17hWtHAfFWXw/frllG9t6aijuQukiU40ElOeM9nDTrtQPhLJMLN0q8lo897FYg=="
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.18.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
@@ -9423,6 +9469,14 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/frac": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
|
||||||
@@ -16997,6 +17051,17 @@
|
|||||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/ssf": {
|
||||||
|
"version": "0.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||||
|
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||||
|
"dependencies": {
|
||||||
|
"frac": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stable": {
|
"node_modules/stable": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
|
||||||
@@ -18746,6 +18811,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wmf": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/word": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||||
@@ -19133,6 +19214,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.18.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||||
|
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"adler-32": "~1.3.0",
|
||||||
|
"cfb": "~1.2.1",
|
||||||
|
"codepage": "~1.15.0",
|
||||||
|
"crc-32": "~1.2.1",
|
||||||
|
"ssf": "~0.11.2",
|
||||||
|
"wmf": "~1.0.1",
|
||||||
|
"word": "~0.3.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"downshift": "^6.1.12",
|
"downshift": "^6.1.12",
|
||||||
|
"export-from-json": "^1.7.3",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"luxon": "^2.5.2",
|
"luxon": "^2.5.2",
|
||||||
"markdown-to-jsx": "^7.1.7",
|
"markdown-to-jsx": "^7.1.7",
|
||||||
@@ -21,17 +22,18 @@
|
|||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-sticky-el": "^2.0.9",
|
"react-sticky-el": "^2.0.9",
|
||||||
"web-vitals": "^2.1.3"
|
"web-vitals": "^2.1.3",
|
||||||
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.16.7",
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
"eslint": "^8.23.1",
|
"eslint": "^8.23.1",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-react": "^7.31.8",
|
"eslint-plugin-react": "^7.31.8",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1"
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.16.7"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const StateFilterCardWrapper = () => {
|
|||||||
describe('Filters components', () => {
|
describe('Filters components', () => {
|
||||||
it('renders the filters cards', async () => {
|
it('renders the filters cards', async () => {
|
||||||
render(<StateFilterCardWrapper />);
|
render(<StateFilterCardWrapper />);
|
||||||
expect(screen.getAllByRole('checkbox')).toHaveLength(2);
|
expect(screen.getAllByRole('checkbox')).toHaveLength(3);
|
||||||
|
|
||||||
const checkbox = screen.getAllByRole('checkbox');
|
const checkbox = screen.getAllByRole('checkbox');
|
||||||
expect(checkbox[0]).not.toBeChecked();
|
expect(checkbox[0]).not.toBeChecked();
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: 'latest',
|
tag: 'latest',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||||
@@ -37,6 +38,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: 'bullseye',
|
tag: 'bullseye',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||||
@@ -52,6 +54,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: '1.5.2',
|
tag: '1.5.2',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||||
@@ -76,6 +79,18 @@ describe('Tags component', () => {
|
|||||||
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should see delete tag button and its dialog', async () => {
|
||||||
|
render(<TagsThemeWrapper />);
|
||||||
|
const deleteBtn = await screen.findAllByTestId('DeleteIcon');
|
||||||
|
fireEvent.click(deleteBtn[0]);
|
||||||
|
expect(await screen.findByTestId('delete-dialog')).toBeInTheDocument();
|
||||||
|
const confirmBtn = await screen.findByTestId('confirm-delete');
|
||||||
|
expect(confirmBtn).toBeInTheDocument();
|
||||||
|
fireEvent.click(confirmBtn);
|
||||||
|
expect(await screen.findByTestId('confirm-delete')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('cancel-delete')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should navigate to tag page details when tag is clicked', async () => {
|
it('should navigate to tag page details when tag is clicked', async () => {
|
||||||
render(<TagsThemeWrapper />);
|
render(<TagsThemeWrapper />);
|
||||||
const tagLink = await screen.findByText('latest');
|
const tagLink = await screen.findByText('latest');
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
jest.mock('xlsx');
|
||||||
|
|
||||||
const StateVulnerabilitiesWrapper = () => {
|
const StateVulnerabilitiesWrapper = () => {
|
||||||
return (
|
return (
|
||||||
<MockThemeProvider>
|
<MockThemeProvider>
|
||||||
@@ -20,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',
|
||||||
@@ -497,6 +507,7 @@ describe('Vulnerabilties page', () => {
|
|||||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
jest.spyOn(api, 'get').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(/fixed in/i)).toHaveLength(20));
|
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -513,7 +524,7 @@ describe('Vulnerabilties page', () => {
|
|||||||
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));
|
||||||
@@ -558,6 +569,48 @@ describe('Vulnerabilties page', () => {
|
|||||||
expect(await screen.findByText('latest')).toBeInTheDocument();
|
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow export of vulnerabilities list', async () => {
|
||||||
|
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 } });
|
||||||
|
render(<StateVulnerabilitiesWrapper />);
|
||||||
|
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
|
||||||
|
const downloadBtn = await screen.findAllByTestId('DownloadIcon');
|
||||||
|
fireEvent.click(downloadBtn[0]);
|
||||||
|
expect(await screen.findByTestId('export-csv-menuItem')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('export-excel-menuItem')).toBeInTheDocument();
|
||||||
|
const exportAsCSVBtn = screen.getByText(/csv/i);
|
||||||
|
expect(exportAsCSVBtn).toBeInTheDocument();
|
||||||
|
global.URL.createObjectURL = jest.fn();
|
||||||
|
await fireEvent.click(exportAsCSVBtn);
|
||||||
|
expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument();
|
||||||
|
fireEvent.click(downloadBtn[0]);
|
||||||
|
const exportAsExcelBtn = screen.getByText(/xlsx/i);
|
||||||
|
expect(exportAsExcelBtn).toBeInTheDocument();
|
||||||
|
await fireEvent.click(exportAsExcelBtn);
|
||||||
|
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
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 } });
|
||||||
|
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')
|
||||||
|
|||||||
@@ -81,12 +81,13 @@ const endpoints = {
|
|||||||
authConfig: `/v2/_zot/ext/mgmt`,
|
authConfig: `/v2/_zot/ext/mgmt`,
|
||||||
openidAuth: `/zot/auth/login`,
|
openidAuth: `/zot/auth/login`,
|
||||||
logout: `/zot/auth/logout`,
|
logout: `/zot/auth/logout`,
|
||||||
|
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
||||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||||
(pageNumber - 1) * pageSize
|
(pageNumber - 1) * pageSize
|
||||||
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
|
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
|
||||||
detailedRepoInfo: (name) =>
|
detailedRepoInfo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } 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 = '') => {
|
||||||
@@ -96,8 +97,10 @@ const endpoints = {
|
|||||||
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}}}}`;
|
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity 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}}}}`,
|
||||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }, filter = {}) => {
|
||||||
let filterParam = '';
|
let filterParam = '';
|
||||||
if (filter.Os || filter.Arch) {
|
if (filter.Os || filter.Arch) {
|
||||||
|
|||||||
@@ -149,14 +149,14 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item className={classes.headerLinkContainer}>
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
<a className={classes.link} href="https://zotregistry.io" target="_blank" rel="noreferrer">
|
<a className={classes.link} href="https://zotregistry.dev" target="_blank" rel="noreferrer">
|
||||||
Product
|
Product
|
||||||
</a>
|
</a>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item className={classes.headerLinkContainer}>
|
<Grid item className={classes.headerLinkContainer}>
|
||||||
<a
|
<a
|
||||||
className={classes.link}
|
className={classes.link}
|
||||||
href="https://zotregistry.io/v1.4.3/general/concepts/"
|
href="https://zotregistry.dev/v2.0.0/general/concepts/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -197,6 +197,10 @@ function RepoDetails() {
|
|||||||
};
|
};
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
|
const handleDeleteTag = (removed) => {
|
||||||
|
setTags((prevState) => prevState.filter((tag) => tag.tag !== removed));
|
||||||
|
};
|
||||||
|
|
||||||
const handlePlatformChipClick = (event) => {
|
const handlePlatformChipClick = (event) => {
|
||||||
const { textContent } = event.target;
|
const { textContent } = event.target;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -223,7 +227,7 @@ function RepoDetails() {
|
|||||||
|
|
||||||
const handleBookmarkClick = () => {
|
const handleBookmarkClick = () => {
|
||||||
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response && response.status === 200) {
|
||||||
setRepoDetailData((prevState) => ({
|
setRepoDetailData((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
isBookmarked: !prevState.isBookmarked
|
isBookmarked: !prevState.isBookmarked
|
||||||
@@ -341,7 +345,7 @@ function RepoDetails() {
|
|||||||
<Grid item xs={12} md={8} className={classes.tags}>
|
<Grid item xs={12} md={8} className={classes.tags}>
|
||||||
<Card className={classes.cardRoot}>
|
<Card className={classes.cardRoot}>
|
||||||
<CardContent className={classes.tagsContent}>
|
<CardContent className={classes.tagsContent}>
|
||||||
<Tags tags={tags} />
|
<Tags tags={tags} repoName={name} onTagDelete={handleDeleteTag} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const useStyles = makeStyles(() => ({
|
|||||||
|
|
||||||
export default function Tags(props) {
|
export default function Tags(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { tags } = props;
|
const { tags, repoName, onTagDelete } = props;
|
||||||
const [tagsFilter, setTagsFilter] = useState('');
|
const [tagsFilter, setTagsFilter] = useState('');
|
||||||
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
||||||
|
|
||||||
@@ -63,6 +63,9 @@ export default function Tags(props) {
|
|||||||
lastUpdated={tag.lastUpdated}
|
lastUpdated={tag.lastUpdated}
|
||||||
vendor={tag.vendor}
|
vendor={tag.vendor}
|
||||||
manifests={tag.manifests}
|
manifests={tag.manifests}
|
||||||
|
repo={repoName}
|
||||||
|
onTagDelete={onTagDelete}
|
||||||
|
isDeletable={tag.isDeletable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
54
src/components/Shared/DeleteTag.jsx
Normal file
54
src/components/Shared/DeleteTag.jsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { IconButton } from '@mui/material';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
// utility
|
||||||
|
import { api, endpoints } from '../../api';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import DeleteTagConfirmDialog from 'components/Shared/DeleteTagConfirmDialog';
|
||||||
|
import { host } from '../../host';
|
||||||
|
|
||||||
|
export default function DeleteTag(props) {
|
||||||
|
const { repo, tag, onTagDelete } = props;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleClickOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTag = (repo, tag) => {
|
||||||
|
api
|
||||||
|
.delete(`${host()}${endpoints.deleteImage(repo, tag)}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response && response.status == 202) {
|
||||||
|
onTagDelete(tag);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
deleteTag(repo, tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<IconButton onClick={handleClickOpen}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
<DeleteTagConfirmDialog
|
||||||
|
onClose={handleClose}
|
||||||
|
open={open}
|
||||||
|
title={`Permanently delete image ${repo}:${tag}?`}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/components/Shared/DeleteTagConfirmDialog.jsx
Normal file
30
src/components/Shared/DeleteTagConfirmDialog.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Button, Dialog, DialogTitle, DialogActions } from '@mui/material';
|
||||||
|
|
||||||
|
export default function DeleteTagConfirmDialog(props) {
|
||||||
|
const { onClose, open, title, onConfirm } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog data-testid="delete-dialog" onClose={onClose} open={open} color="primary">
|
||||||
|
<DialogTitle> {title} </DialogTitle>
|
||||||
|
<DialogActions style={{ justifyContent: 'center' }}>
|
||||||
|
<Button data-testid="cancel-delete" variant="contained" onClick={onClose} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid="confirm-delete"
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
|||||||
import transform from 'utilities/transform';
|
import transform from 'utilities/transform';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
|
import DeleteTag from 'components/Shared/DeleteTag';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
card: {
|
card: {
|
||||||
@@ -78,9 +79,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export default function TagCard(props) {
|
export default function TagCard(props) {
|
||||||
const { repoName, tag, lastUpdated, vendor, manifests } = props;
|
const { repoName, tag, lastUpdated, vendor, manifests, repo, onTagDelete, isDeletable } = props;
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const lastDate = lastUpdated
|
const lastDate = lastUpdated
|
||||||
@@ -99,9 +100,12 @@ export default function TagCard(props) {
|
|||||||
return (
|
return (
|
||||||
<Card className={classes.card} raised>
|
<Card className={classes.card} raised>
|
||||||
<CardContent className={classes.content}>
|
<CardContent className={classes.content}>
|
||||||
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
<Stack direction="row" spacing={2} justifyContent="space-between">
|
||||||
Tag
|
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
||||||
</Typography>
|
Tag
|
||||||
|
</Typography>
|
||||||
|
{isDeletable && <DeleteTag repo={repo} tag={tag} onTagDelete={onTagDelete} />}
|
||||||
|
</Stack>
|
||||||
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
||||||
{repoName && `${repoName}:`}
|
{repoName && `${repoName}:`}
|
||||||
{tag}
|
{tag}
|
||||||
|
|||||||
@@ -66,13 +66,18 @@ 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'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
function VulnerabilitiyCard(props) {
|
function VulnerabilitiyCard(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { cve, name, platform } = props;
|
const { cve, name, platform, expand } = props;
|
||||||
|
const [openCVE, setOpenCVE] = useState(expand);
|
||||||
const [openDesc, setOpenDesc] = useState(false);
|
const [openDesc, setOpenDesc] = useState(false);
|
||||||
const [openFixed, setOpenFixed] = useState(false);
|
const [openFixed, setOpenFixed] = useState(false);
|
||||||
const [loadingFixed, setLoadingFixed] = useState(true);
|
const [loadingFixed, setLoadingFixed] = useState(true);
|
||||||
@@ -122,6 +127,10 @@ function VulnerabilitiyCard(props) {
|
|||||||
};
|
};
|
||||||
}, [openFixed, pageNumber]);
|
}, [openFixed, pageNumber]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpenCVE(expand);
|
||||||
|
}, [expand]);
|
||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = () => {
|
||||||
if (loadingFixed || isEndOfList) return;
|
if (loadingFixed || isEndOfList) return;
|
||||||
setPageNumber((pageNumber) => pageNumber + 1);
|
setPageNumber((pageNumber) => pageNumber + 1);
|
||||||
@@ -166,49 +175,56 @@ function VulnerabilitiyCard(props) {
|
|||||||
<Card className={classes.card} raised>
|
<Card className={classes.card} raised>
|
||||||
<CardContent className={classes.content}>
|
<CardContent className={classes.content}>
|
||||||
<Stack direction="row" spacing="1.25rem">
|
<Stack direction="row" spacing="1.25rem">
|
||||||
|
{!openCVE ? (
|
||||||
|
<KeyboardArrowRight 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={classes.cveId}>
|
||||||
{cve.id}
|
{cve.id}
|
||||||
</Typography>
|
</Typography>
|
||||||
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
<Collapse in={openCVE} timeout="auto" unmountOnExit>
|
||||||
{cve.title}
|
<Typography variant="body1" align="left" className={classes.cveSummary}>
|
||||||
</Typography>
|
{cve.title}
|
||||||
<Divider className={classes.vulnerabilityCardDivider} />
|
</Typography>
|
||||||
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
|
<Divider className={classes.vulnerabilityCardDivider} />
|
||||||
{!openFixed ? (
|
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
|
||||||
<KeyboardArrowRight className={classes.dropdownText} />
|
{!openFixed ? (
|
||||||
) : (
|
<KeyboardArrowRight className={classes.dropdownText} />
|
||||||
<KeyboardArrowDown className={classes.dropdownText} />
|
|
||||||
)}
|
|
||||||
<Typography className={classes.dropdownText}>Fixed in</Typography>
|
|
||||||
</Stack>
|
|
||||||
<Collapse in={openFixed} timeout="auto" unmountOnExit>
|
|
||||||
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
|
|
||||||
{loadingFixed ? (
|
|
||||||
'Loading...'
|
|
||||||
) : (
|
) : (
|
||||||
<Stack direction="row" sx={{ flexWrap: 'wrap' }}>
|
<KeyboardArrowDown className={classes.dropdownText} />
|
||||||
{renderFixedVer()}
|
|
||||||
{renderLoadMore()}
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
</Box>
|
<Typography className={classes.dropdownText}>Fixed in</Typography>
|
||||||
</Collapse>
|
</Stack>
|
||||||
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
|
<Collapse in={openFixed} timeout="auto" unmountOnExit>
|
||||||
{!openDesc ? (
|
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
|
||||||
<KeyboardArrowRight className={classes.dropdownText} />
|
{loadingFixed ? (
|
||||||
) : (
|
'Loading...'
|
||||||
<KeyboardArrowDown className={classes.dropdownText} />
|
) : (
|
||||||
)}
|
<Stack direction="row" sx={{ flexWrap: 'wrap' }}>
|
||||||
<Typography className={classes.dropdownText}>Description</Typography>
|
{renderFixedVer()}
|
||||||
</Stack>
|
{renderLoadMore()}
|
||||||
<Collapse in={openDesc} timeout="auto" unmountOnExit>
|
</Stack>
|
||||||
<Box sx={{ padding: '0.5rem 0' }}>
|
)}
|
||||||
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
|
</Box>
|
||||||
{cve.description}
|
</Collapse>
|
||||||
</Typography>
|
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
|
||||||
</Box>
|
{!openDesc ? (
|
||||||
|
<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' }}>
|
||||||
|
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
|
||||||
|
{cve.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
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;
|
||||||
@@ -4,24 +4,52 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
|
|||||||
import { api, endpoints } from '../../../api';
|
import { api, endpoints } from '../../../api';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Stack, Typography, InputBase } from '@mui/material';
|
import {
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
InputBase,
|
||||||
|
ToggleButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Divider,
|
||||||
|
Snackbar,
|
||||||
|
CircularProgress
|
||||||
|
} from '@mui/material';
|
||||||
import makeStyles from '@mui/styles/makeStyles';
|
import makeStyles from '@mui/styles/makeStyles';
|
||||||
import { host } from '../../../host';
|
import { host } from '../../../host';
|
||||||
import { debounce, isEmpty } from 'lodash';
|
import { debounce, isEmpty } from 'lodash';
|
||||||
import Loading from '../../Shared/Loading';
|
import Loading from '../../Shared/Loading';
|
||||||
import { mapCVEInfo } from 'utilities/objectModels';
|
import { mapCVEInfo, mapAllCVEInfo } from 'utilities/objectModels';
|
||||||
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
|
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
|
|
||||||
|
import * as XLSX from 'xlsx';
|
||||||
|
import exportFromJSON from 'export-from-json';
|
||||||
|
import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline';
|
||||||
|
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
|
||||||
|
|
||||||
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',
|
||||||
@@ -40,9 +68,17 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
fontSize: '1.4rem',
|
fontSize: '1.4rem',
|
||||||
fontWeight: '600'
|
fontWeight: '600'
|
||||||
},
|
},
|
||||||
|
vulnerabilities: {
|
||||||
|
position: 'relative',
|
||||||
|
maxWidth: '100%',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
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',
|
||||||
@@ -50,6 +86,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%'
|
||||||
@@ -65,13 +115,23 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
'&::placeholder': {
|
'&::placeholder': {
|
||||||
opacity: '1'
|
opacity: '1'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
popper: {
|
||||||
|
width: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
padding: '0.3rem',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'left'
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function VulnerabilitiesDetails(props) {
|
function VulnerabilitiesDetails(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [cveData, setCveData] = useState([]);
|
const [cveData, setCveData] = 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 abortController = useMemo(() => new AbortController(), []);
|
const abortController = useMemo(() => new AbortController(), []);
|
||||||
const { name, tag, digest, platform } = props;
|
const { name, tag, digest, platform } = props;
|
||||||
|
|
||||||
@@ -81,6 +141,11 @@ function VulnerabilitiesDetails(props) {
|
|||||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||||
const listBottom = useRef(null);
|
const listBottom = useRef(null);
|
||||||
|
|
||||||
|
const [anchorExport, setAnchorExport] = useState(null);
|
||||||
|
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}`;
|
||||||
};
|
};
|
||||||
@@ -98,9 +163,23 @@ function VulnerabilitiesDetails(props) {
|
|||||||
.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);
|
||||||
}
|
}
|
||||||
@@ -110,10 +189,29 @@ function VulnerabilitiesDetails(props) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setCveData([]);
|
setCveData([]);
|
||||||
|
setCVESummary(() => {});
|
||||||
setIsEndOfList(true);
|
setIsEndOfList(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAllCVEs = () => {
|
||||||
|
api
|
||||||
|
.get(`${host()}${endpoints.allVulnerabilitiesForRepo(getCVERequestName())}`, abortController.signal)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.data && response.data.data) {
|
||||||
|
const cveInfo = response.data.data.CVEListForImage?.CVEList;
|
||||||
|
const cveListData = mapAllCVEInfo(cveInfo);
|
||||||
|
setAllCveData(cveListData);
|
||||||
|
}
|
||||||
|
setIsLoadingAllCve(false);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
setAllCveData([]);
|
||||||
|
setIsLoadingAllCve(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resetPagination = () => {
|
const resetPagination = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsEndOfList(false);
|
setIsEndOfList(false);
|
||||||
@@ -124,11 +222,39 @@ function VulnerabilitiesDetails(props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnExportExcel = () => {
|
||||||
|
const wb = XLSX.utils.book_new(),
|
||||||
|
ws = XLSX.utils.json_to_sheet(allCveData);
|
||||||
|
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, name.replaceAll('/', '_') + '_' + tag);
|
||||||
|
|
||||||
|
XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`);
|
||||||
|
|
||||||
|
handleCloseExport();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnExportCSV = () => {
|
||||||
|
const fileName = `${name}:${tag}-vulnerabilities`;
|
||||||
|
const exportType = exportFromJSON.types.csv;
|
||||||
|
|
||||||
|
exportFromJSON({ data: allCveData, fileName, exportType });
|
||||||
|
|
||||||
|
handleCloseExport();
|
||||||
|
};
|
||||||
|
|
||||||
const handleCveFilterChange = (e) => {
|
const handleCveFilterChange = (e) => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
setCveFilter(value);
|
setCveFilter(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClickExport = (event) => {
|
||||||
|
setAnchorExport(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseExport = () => {
|
||||||
|
setAnchorExport(null);
|
||||||
|
};
|
||||||
|
|
||||||
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,16 +298,43 @@ function VulnerabilitiesDetails(props) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openExport && isEmpty(allCveData)) {
|
||||||
|
getAllCVEs();
|
||||||
|
}
|
||||||
|
}, [openExport]);
|
||||||
|
|
||||||
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 />;
|
||||||
@@ -194,9 +347,76 @@ function VulnerabilitiesDetails(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" spacing="1rem" data-testid="vulnerability-container">
|
<Stack direction="column" spacing="1rem" data-testid="vulnerability-container">
|
||||||
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
<Stack className={classes.vulnerabilities}>
|
||||||
Vulnerabilities
|
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
|
||||||
</Typography>
|
Vulnerabilities
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" spacing="1rem" className={classes.viewModes}>
|
||||||
|
<IconButton disableRipple onClick={handleClickExport}>
|
||||||
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Snackbar
|
||||||
|
open={openExport && isLoadingAllCve}
|
||||||
|
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
|
||||||
|
anchorEl={anchorExport}
|
||||||
|
open={openExport}
|
||||||
|
onClose={handleCloseExport}
|
||||||
|
data-testid="export-dropdown"
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleOnExportCSV}
|
||||||
|
disableRipple
|
||||||
|
disabled={isLoadingAllCve}
|
||||||
|
className={classes.popper}
|
||||||
|
data-testid="export-csv-menuItem"
|
||||||
|
>
|
||||||
|
csv
|
||||||
|
</MenuItem>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleOnExportExcel}
|
||||||
|
disableRipple
|
||||||
|
disabled={isLoadingAllCve}
|
||||||
|
className={classes.popper}
|
||||||
|
data-testid="export-excel-menuItem"
|
||||||
|
>
|
||||||
|
xlsx
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</Stack>
|
||||||
|
{renderCVESummary()}
|
||||||
<Stack className={classes.search}>
|
<Stack className={classes.search}>
|
||||||
<InputBase
|
<InputBase
|
||||||
placeholder={'Search'}
|
placeholder={'Search'}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ const osFilters = [
|
|||||||
{
|
{
|
||||||
label: 'linux',
|
label: 'linux',
|
||||||
value: 'linux'
|
value: 'linux'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'freebsd',
|
||||||
|
value: 'freebsd'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const mapToImage = (responseImage) => {
|
|||||||
authors: responseImage.Authors,
|
authors: responseImage.Authors,
|
||||||
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
||||||
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
||||||
|
isDeletable: responseImage.IsDeletable,
|
||||||
// frontend only prop to increase interop with Repo objects and code reusability
|
// frontend only prop to increase interop with Repo objects and code reusability
|
||||||
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
||||||
};
|
};
|
||||||
@@ -101,6 +102,24 @@ const mapCVEInfo = (cveInfo) => {
|
|||||||
return cveList;
|
return cveList;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mapAllCVEInfo = (cveInfo) => {
|
||||||
|
const cveList = cveInfo.flatMap((cve) => {
|
||||||
|
return cve.PackageList.map((packageInfo) => {
|
||||||
|
return {
|
||||||
|
id: cve.Id,
|
||||||
|
severity: cve.Severity,
|
||||||
|
title: cve.Title,
|
||||||
|
description: cve.Description,
|
||||||
|
reference: cve.Reference,
|
||||||
|
packageName: packageInfo.Name,
|
||||||
|
packageInstalledVersion: packageInfo.InstalledVersion,
|
||||||
|
packageFixedVersion: packageInfo.FixedVersion
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return cveList;
|
||||||
|
};
|
||||||
|
|
||||||
const mapSignatureInfo = (signatureInfo) => {
|
const mapSignatureInfo = (signatureInfo) => {
|
||||||
return signatureInfo
|
return signatureInfo
|
||||||
? {
|
? {
|
||||||
@@ -123,4 +142,4 @@ const mapReferrer = (referrer) => ({
|
|||||||
annotations: referrer.Annotations?.map((annotation) => ({ key: annotation.Key, value: annotation.Value }))
|
annotations: referrer.Annotations?.map((annotation) => ({ key: annotation.Key, value: annotation.Value }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export { mapToRepo, mapToImage, mapToRepoFromRepoInfo, mapCVEInfo, mapReferrer, mapToManifest };
|
export { mapToRepo, mapToImage, mapToRepoFromRepoInfo, mapCVEInfo, mapAllCVEInfo, mapReferrer, mapToManifest };
|
||||||
|
|||||||
@@ -76,8 +76,14 @@ test.describe('explore page test', () => {
|
|||||||
|
|
||||||
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
await expect(exploreFirst).toBeVisible({ timeout: 250000 });
|
||||||
|
|
||||||
|
const windowsFilter = page.getByRole('checkbox', { name: 'windows' });
|
||||||
await linuxFilter.uncheck();
|
await linuxFilter.uncheck();
|
||||||
await page.getByRole('checkbox', { name: 'windows' }).check();
|
await windowsFilter.check();
|
||||||
|
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
||||||
|
|
||||||
|
const freebsdFilter = page.getByRole('checkbox', { name: 'freebsd' });
|
||||||
|
await windowsFilter.uncheck();
|
||||||
|
await freebsdFilter.check();
|
||||||
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
await expect(exploreFirst).not.toBeVisible({ timeout: 250000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ 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(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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const pageSizes = {
|
|||||||
const endpoints = {
|
const endpoints = {
|
||||||
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20StarCount%20DownloadCount}}}`,
|
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20StarCount%20DownloadCount}}}`,
|
||||||
detailedRepoInfo: (name) =>
|
detailedRepoInfo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20IsDeletable%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
||||||
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
||||||
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
||||||
10 * (pageNumber - 1)
|
10 * (pageNumber - 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user