From 1870b5365617ad55463235f4352670979089f399 Mon Sep 17 00:00:00 2001 From: Amelia-Maria Breda Date: Wed, 21 Sep 2022 12:21:58 +0300 Subject: [PATCH] Added tag page with vulnerabilities. (#82) Added the Image Tag details page Signed-off-by: Amelia-Maria Breda Signed-off-by: Raul Kele --- src/App.js | 2 + src/__tests__/Explore/FilterCard.test.js | 19 + src/__tests__/HomePage/Home.test.js | 6 +- src/__tests__/RepoPage/Tags.test.js | 57 ++- .../RepoPage/VulnerabilitiesDetails.test.js | 449 ++++++++++++++++++ src/__tests__/TagPage/TagDetails.test.js | 100 ++++ src/__tests__/TagPage/TagPage.test.js | 26 + src/api.js | 5 +- src/components/Explore.jsx | 18 +- src/components/ExploreHeader.jsx | 12 +- src/components/Home.jsx | 7 +- src/components/PreviewCard.jsx | 52 +- src/components/RepoCard.jsx | 99 ++-- src/components/RepoDetails.jsx | 101 ++-- src/components/RepoDetailsMetadata.jsx | 12 +- src/components/SignIn.jsx | 19 +- src/components/TagDetails.jsx | 307 ++++++++++++ src/components/TagDetailsMetadata.jsx | 72 +++ src/components/Tags.jsx | 150 +++--- src/components/VulnerabilitiesDetails.jsx | 175 +++++++ src/index.css | 4 +- src/pages/TagPage.jsx | 50 ++ 22 files changed, 1521 insertions(+), 221 deletions(-) create mode 100644 src/__tests__/Explore/FilterCard.test.js create mode 100644 src/__tests__/RepoPage/VulnerabilitiesDetails.test.js create mode 100644 src/__tests__/TagPage/TagDetails.test.js create mode 100644 src/__tests__/TagPage/TagPage.test.js create mode 100644 src/components/TagDetails.jsx create mode 100644 src/components/TagDetailsMetadata.jsx create mode 100644 src/components/VulnerabilitiesDetails.jsx create mode 100644 src/pages/TagPage.jsx diff --git a/src/App.js b/src/App.js index 6748a5d0..d9f89de7 100644 --- a/src/App.js +++ b/src/App.js @@ -9,6 +9,7 @@ import './App.css'; import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom"; import { AuthWrapper } from 'utilities/AuthWrapper.jsx'; import RepoPage from 'pages/RepoPage.jsx'; +import TagPage from 'pages/TagPage'; import ExplorePage from 'pages/ExplorePage.jsx'; const useStyles = makeStyles((theme) => ({ @@ -38,6 +39,7 @@ function App() { } /> } /> } /> + } /> }> } /> diff --git a/src/__tests__/Explore/FilterCard.test.js b/src/__tests__/Explore/FilterCard.test.js new file mode 100644 index 00000000..2c19c7e4 --- /dev/null +++ b/src/__tests__/Explore/FilterCard.test.js @@ -0,0 +1,19 @@ +import { render, screen , fireEvent } from '@testing-library/react'; +import FilterCard from 'components/FilterCard'; +import React, {useState} from 'react'; + +const StateFilterCardWrapper = () => { + return () + } + +describe('Filters components', () => { + it('renders the filters cards', () => { + render( ); + expect(screen.getAllByRole('checkbox')).toHaveLength(2); + + const checkbox = screen.getAllByRole('checkbox'); + expect(checkbox[0]).not.toBeChecked() + fireEvent.click(checkbox[0]) + expect(checkbox[0]).toBeChecked() + }); +}); diff --git a/src/__tests__/HomePage/Home.test.js b/src/__tests__/HomePage/Home.test.js index 53770057..286fb0ae 100644 --- a/src/__tests__/HomePage/Home.test.js +++ b/src/__tests__/HomePage/Home.test.js @@ -66,9 +66,9 @@ describe('Home component', () => { // @ts-ignore jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }) render(); - await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(3)); - await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(3)); - await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(5)); + await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2)); + await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2)); + await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1)); }); it('should log an error when data can\'t be fetched', async() => { diff --git a/src/__tests__/RepoPage/Tags.test.js b/src/__tests__/RepoPage/Tags.test.js index c7897598..edcf2c6f 100644 --- a/src/__tests__/RepoPage/Tags.test.js +++ b/src/__tests__/RepoPage/Tags.test.js @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent,waitFor, render, screen } from '@testing-library/react'; import Tags from 'components/Tags'; import React from 'react'; @@ -10,27 +10,48 @@ jest.mock('react-router-dom', () => ({ })); const mockedTagsData = { - name: 'test', - tags: [ - { - "Digest": "2aa7ff5ca352d4d25fc6548f9930a436aacd64d56b1bd1f9ff4423711b9c8718", - "Tag": "latest", - "Layers": [ - { - "Size": "2798889", - "Digest": "2408cc74d12b6cd092bb8b516ba7d5e290f485d3eb9672efc00f0583730179e8" - } - ] - } - ] -}; + "name": "alpine", + "images": [ + { + "Digest": "59118d0816d2e8e05cb04c328224056b3ce07d7afc2ad59e2f1f08bb0ba2ff3c", + "Tag": "latest", + "Layers": [ + { + "Size": "2806054", + "Digest": "213ec9aee27d8be045c6a92b7eac22c9a64b44558193775a1a7f626352392b49" + } + ] + } + ], + "lastUpdated": "2022-08-09T17:19:53.274069586Z", + "size": "2806985", + "platforms": [ + { + "Os": "linux", + "Arch": "amd64" + } + ], + "vendors": [ + "" + ], + "newestTag": null +} + describe('Tags component', () => { it('should open and close details dropdown for tags', () => { render(); - const openBtn = screen.getByText(/see layers/i); + const openBtn = screen.getByText(/see digests/i); fireEvent.click(openBtn); - expect(screen.queryByText(/see layers/i)).not.toBeInTheDocument(); - expect(screen.getByText(/hide layers/i)).toBeInTheDocument(); + expect(screen.queryByText(/see digests/i)).not.toBeInTheDocument(); + expect(screen.getByText(/hide digests/i)).toBeInTheDocument(); }); + it('should navigate to tag page details when tag is clicked', async() => { + render(); + const tagLink = await screen.findByText('latest'); + fireEvent.click(tagLink); + await waitFor(() => { + expect(mockedUsedNavigate).toHaveBeenCalledWith('tag/latest'); + }); + }) }) \ No newline at end of file diff --git a/src/__tests__/RepoPage/VulnerabilitiesDetails.test.js b/src/__tests__/RepoPage/VulnerabilitiesDetails.test.js new file mode 100644 index 00000000..d38b53ee --- /dev/null +++ b/src/__tests__/RepoPage/VulnerabilitiesDetails.test.js @@ -0,0 +1,449 @@ +import { render, screen , waitFor, fireEvent } from '@testing-library/react'; +import { api } from 'api'; +import VulnerabilitiesDetails from 'components/VulnerabilitiesDetails'; +import React, {useState} from 'react'; + +const StateVulnerabilitiesWrapper = () => { + return () + } + +const mockCVEList = { + CVEListForImage: { + "Tag": "", + "CVEList": [ + { + "Id": "CVE-2020-16156", + "Title": "perl-CPAN: Bypass of verification of signatures in CHECKSUMS files", + "Description": "CPAN 2.28 allows Signature Verification Bypass.", + "Severity": "MEDIUM", + "PackageList": [ + { + "Name": "perl-base", + "InstalledVersion": "5.30.0-9ubuntu0.2", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2021-36222", + "Title": "krb5: Sending a request containing PA-ENCRYPTED-CHALLENGE padata element without using FAST could result in NULL dereference in KDC which leads to DoS", + "Description": "ec_verify in kdc/kdc_preauth_ec.c in the Key Distribution Center (KDC) in MIT Kerberos 5 (aka krb5) before 1.18.4 and 1.19.x before 1.19.2 allows remote attackers to cause a NULL pointer dereference and daemon crash. This occurs because a return value is not properly managed in a certain situation.", + "Severity": "HIGH", + "PackageList": [ + { + "Name": "krb5-locales", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libgssapi-krb5-2", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libk5crypto3", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libkrb5-3", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libkrb5support0", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2021-4209", + "Title": "GnuTLS: Null pointer dereference in MD_UPDATE", + "Description": "A NULL pointer dereference flaw was found in GnuTLS. As Nettle's hash update functions internally call memcpy, providing zero-length input may cause undefined behavior. This flaw leads to a denial of service after authentication in rare circumstances.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libgnutls30", + "InstalledVersion": "3.6.13-2ubuntu1.6", + "FixedVersion": "3.6.13-2ubuntu1.7" + } + ] + }, + { + "Id": "CVE-2022-1586", + "Title": "pcre2: Out-of-bounds read in compile_xclass_matchingpath in pcre2_jit_compile.c", + "Description": "An out-of-bounds read vulnerability was discovered in the PCRE2 library in the compile_xclass_matchingpath() function of the pcre2_jit_compile.c file. This involves a unicode property matching issue in JIT-compiled regular expressions. The issue occurs because the character was not fully read in case-less matching within JIT.", + "Severity": "CRITICAL", + "PackageList": [ + { + "Name": "libpcre2-8-0", + "InstalledVersion": "10.34-7", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2021-20223", + "Title": "", + "Description": "An issue was found in fts5UnicodeTokenize() in ext/fts5/fts5_tokenize.c in Sqlite. A unicode61 tokenizer configured to treat unicode \"control-characters\" (class Cc), was treating embedded nul characters as tokens. The issue was fixed in sqlite-3.34.0 and later.", + "Severity": "NONE", + "PackageList": [ + { + "Name": "libsqlite3-0", + "InstalledVersion": "3.31.1-4ubuntu0.3", + "FixedVersion": "3.31.1-4ubuntu0.4" + } + ] + }, + { + "Id": "CVE-2017-11164", + "Title": "pcre: OP_KETRMAX feature in the match function in pcre_exec.c", + "Description": "In PCRE 8.41, the OP_KETRMAX feature in the match function in pcre_exec.c allows stack exhaustion (uncontrolled recursion) when processing a crafted regular expression.", + "Severity": "UNKNOWN", + "PackageList": [ + { + "Name": "libpcre3", + "InstalledVersion": "2:8.39-12ubuntu0.1", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2020-35527", + "Title": "sqlite: Out of bounds access during table rename", + "Description": "In SQLite 3.31.1, there is an out of bounds access problem through ALTER TABLE for views that have a nested FROM clause.", + "Severity": "MEDIUM", + "PackageList": [ + { + "Name": "libsqlite3-0", + "InstalledVersion": "3.31.1-4ubuntu0.3", + "FixedVersion": "3.31.1-4ubuntu0.4" + } + ] + }, + { + "Id": "CVE-2013-4235", + "Title": "shadow-utils: TOCTOU race conditions by copying and removing directory trees", + "Description": "shadow: TOCTOU (time-of-check time-of-use) race condition when copying and removing directory trees", + "Severity": "LOW", + "PackageList": [ + { + "Name": "login", + "InstalledVersion": "1:4.8.1-1ubuntu5.20.04.2", + "FixedVersion": "Not Specified" + }, + { + "Name": "passwd", + "InstalledVersion": "1:4.8.1-1ubuntu5.20.04.2", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2021-43618", + "Title": "gmp: Integer overflow and resultant buffer overflow via crafted input", + "Description": "GNU Multiple Precision Arithmetic Library (GMP) through 6.2.1 has an mpz/inp_raw.c integer overflow and resultant buffer overflow via crafted input, leading to a segmentation fault on 32-bit platforms.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libgmp10", + "InstalledVersion": "2:6.2.0+dfsg-4", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2022-2509", + "Title": "gnutls: Double free during gnutls_pkcs7_verify.", + "Description": "A vulnerability found in gnutls. This security flaw happens because of a double free error occurs during verification of pkcs7 signatures in gnutls_pkcs7_verify function.", + "Severity": "MEDIUM", + "PackageList": [ + { + "Name": "libgnutls30", + "InstalledVersion": "3.6.13-2ubuntu1.6", + "FixedVersion": "3.6.13-2ubuntu1.7" + } + ] + }, + { + "Id": "CVE-2021-39537", + "Title": "ncurses: heap-based buffer overflow in _nc_captoinfo() in captoinfo.c", + "Description": "An issue was discovered in ncurses through v6.2-1. _nc_captoinfo in captoinfo.c has a heap-based buffer overflow.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libncurses6", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "libncursesw6", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "libtinfo6", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "ncurses-base", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "ncurses-bin", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2022-1587", + "Title": "pcre2: Out-of-bounds read in get_recurse_data_length in pcre2_jit_compile.c", + "Description": "An out-of-bounds read vulnerability was discovered in the PCRE2 library in the get_recurse_data_length() function of the pcre2_jit_compile.c file. This issue affects recursions in JIT-compiled regular expressions caused by duplicate data transfers.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libpcre2-8-0", + "InstalledVersion": "10.34-7", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2022-29458", + "Title": "ncurses: segfaulting OOB read", + "Description": "ncurses 6.3 before patch 20220416 has an out-of-bounds read and segmentation violation in convert_strings in tinfo/read_entry.c in the terminfo library.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libncurses6", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "libncursesw6", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "libtinfo6", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "ncurses-base", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + }, + { + "Name": "ncurses-bin", + "InstalledVersion": "6.2-0ubuntu2", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2016-2781", + "Title": "coreutils: Non-privileged session can escape to the parent session in chroot", + "Description": "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "coreutils", + "InstalledVersion": "8.30-3ubuntu2", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2021-3671", + "Title": "samba: Null pointer dereference on missing sname in TGS-REQ", + "Description": "A null pointer de-reference was found in the way samba kerberos server handled missing sname in TGS-REQ (Ticket Granting Server - Request). An authenticated user could use this flaw to crash the samba server.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libasn1-8-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libgssapi3-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libhcrypto4-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libheimbase1-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libheimntlm0-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libhx509-5-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libkrb5-26-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libroken18-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libwind0-heimdal", + "InstalledVersion": "7.7.0+dfsg-1ubuntu1", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2016-20013", + "Title": "", + "Description": "sha256crypt and sha512crypt through 0.6 allow attackers to cause a denial of service (CPU consumption) because the algorithm's runtime is proportional to the square of the length of the password.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libc-bin", + "InstalledVersion": "2.31-0ubuntu9.9", + "FixedVersion": "Not Specified" + }, + { + "Name": "libc6", + "InstalledVersion": "2.31-0ubuntu9.9", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2022-35252", + "Title": "curl: control code in cookie denial of service", + "Description": "No description is available for this CVE.", + "Severity": "LOW", + "PackageList": [ + { + "Name": "libcurl4", + "InstalledVersion": "7.68.0-1ubuntu2.12", + "FixedVersion": "7.68.0-1ubuntu2.13" + } + ] + }, + { + "Id": "CVE-2021-37750", + "Title": "krb5: NULL pointer dereference in process_tgs_req() in kdc/do_tgs_req.c via a FAST inner body that lacks server field", + "Description": "The Key Distribution Center (KDC) in MIT Kerberos 5 (aka krb5) before 1.18.5 and 1.19.x before 1.19.3 has a NULL pointer dereference in kdc/do_tgs_req.c via a FAST inner body that lacks a server field.", + "Severity": "MEDIUM", + "PackageList": [ + { + "Name": "krb5-locales", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libgssapi-krb5-2", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libk5crypto3", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libkrb5-3", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + }, + { + "Name": "libkrb5support0", + "InstalledVersion": "1.17-6ubuntu4.1", + "FixedVersion": "Not Specified" + } + ] + }, + { + "Id": "CVE-2020-35525", + "Title": "sqlite: Null pointer derreference in src/select.c", + "Description": "In SQlite 3.31.1, a potential null pointer derreference was found in the INTERSEC query processing.", + "Severity": "MEDIUM", + "PackageList": [ + { + "Name": "libsqlite3-0", + "InstalledVersion": "3.31.1-4ubuntu0.3", + "FixedVersion": "3.31.1-4ubuntu0.4" + } + ] + }, + { + "Id": "CVE-2022-37434", + "Title": "zlib: a heap-based buffer over-read or buffer overflow in inflate in inflate.c via a large gzip header extra field", + "Description": "zlib through 1.2.12 has a heap-based buffer over-read or buffer overflow in inflate in inflate.c via a large gzip header extra field. NOTE: only applications that call inflateGetHeader are affected. Some common applications bundle the affected zlib source code but may be unable to call inflateGetHeader (e.g., see the nodejs/node reference).", + "Severity": "MEDIUM", + "PackageList": [ + { + "Name": "zlib1g", + "InstalledVersion": "1:1.2.11.dfsg-2ubuntu1.3", + "FixedVersion": "Not Specified" + } + ] + } + ] +} +}; + + +afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); +}); + +describe('Vulnerabilties page', () => { + it('renders the vulnerabilities if there are any', async() => { + // @ts-ignore + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }) + render( ); + await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText(/Title/i)).toHaveLength(20)); + }); + + it('renders no vulnerabilities if there are not any', async() => { + // @ts-ignore + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: {CVEListForImage: {"Tag": "", "CVEList": []} } } }) + render( ); + await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); + + }); + + it('should open and close description dropdown for vulnerabilities', async() => { + // @ts-ignore + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }) + render( ); + await waitFor(() => expect(screen.getAllByText(/see description/i)).toHaveLength(20)); + const openText = screen.getAllByText(/see description/i); + fireEvent.click(openText[0]); + await waitFor(() => expect(screen.getAllByText(/hide description/i)).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText(/see description/i)).toHaveLength(19)); + }); + + it('should log an error when data can\'t be fetched', async() => { + // @ts-ignore + jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: { } }) + const error = jest.spyOn(console, 'error').mockImplementation(() => {}); + render( ); + await waitFor(() => expect(error).toBeCalledTimes(1)); + }) +}); diff --git a/src/__tests__/TagPage/TagDetails.test.js b/src/__tests__/TagPage/TagDetails.test.js new file mode 100644 index 00000000..e9bf093c --- /dev/null +++ b/src/__tests__/TagPage/TagDetails.test.js @@ -0,0 +1,100 @@ +import { render, screen , waitFor } from '@testing-library/react'; +import { api } from 'api'; +import TagDetails from 'components/TagDetails'; +import React from 'react'; + +const mockImage = {"ExpandedRepoInfo": { + "Images": [ + { + "Digest": "7374731e3dd3112d41ece21cf2db5a16f11a51b33bf065e98c767893f50d3dec", + "Tag": "latest", + "Layers": [ + { + "Size": "28572596", + "Digest": "3b65ec22a9e96affe680712973e88355927506aa3f792ff03330f3a3eb601a98" + }, + { + "Size": "1835", + "Digest": "016bc871e2b33f0e2a37272769ebd6defdb4b702f0d41ec1e685f0366b64e64a" + }, + { + "Size": "3059542", + "Digest": "9ddd649edd82d79ffc6f573cd5da7909ae50596b95aca684a571aff6e36aa8cb" + }, + { + "Size": "6506025", + "Digest": "39bf776c01e412c9cf35ea7a41f97370c486dee27a2aab228cf2e850a8863e8b" + }, + { + "Size": "149", + "Digest": "f7f0405a2fe343547a60a9d4182261ca02d70bb9e47d6cd248f3285d6b41e64c" + }, + { + "Size": "1447", + "Digest": "89785d0d9c65afe73fbd9bcb29c451090ca84df0e128cf1ecf5712c036e8c9d2" + }, + { + "Size": "261", + "Digest": "fd40d84c80b0302ca13faab8210d8c7082814f6f2ab576b3a61f467d03e1cb0b" + }, + { + "Size": "193228772", + "Digest": "d50d65ac4752500ab9f3c24c86b4aa218bea9a0bb0a837ae54ffe2e6d2454f5a" + }, + { + "Size": "5067", + "Digest": "255e24cbd370c0055e0d31e063e63c792fa68aff9e25a7ac0a21d39cf6d47573" + } + ] + } + ], + "Summary": { + "Name": "mongo", + "LastUpdated": "2022-08-02T01:30:49.193203152Z", + "Size": "231383863", + "Platforms": [ + { + "Os": "linux", + "Arch": "amd64" + } + ], + "Vendors": [ + "" + ], + "NewestImage": null + } +}}; + +jest.mock("react-router-dom", () => ({ + // @ts-ignore + ...jest.requireActual("react-router-dom"), + useParams: () =>{return {name:'test'} }, +})); + + +afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); + }); + +describe('Tags details', () => { + it('should show vulnerability tab', async() => { + // @ts-ignore + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }) + render( ); + await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(1)); + }); + + it('should log an error when data can\'t be fetched', async() => { + jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: { } }) + const error = jest.spyOn(console, 'error').mockImplementation(() => {}); + render( ); + await waitFor(() => expect(error).toBeCalledTimes(2)); + }) + it('should show tag details metadata', async() => { + // @ts-ignore + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } }) + render( ); + expect(await screen.findByTestId('tagDetailsMetadata-container')).toBeInTheDocument(); + }) +}); \ No newline at end of file diff --git a/src/__tests__/TagPage/TagPage.test.js b/src/__tests__/TagPage/TagPage.test.js new file mode 100644 index 00000000..0671e2ce --- /dev/null +++ b/src/__tests__/TagPage/TagPage.test.js @@ -0,0 +1,26 @@ +import { render, screen } from '@testing-library/react'; +import { api } from 'api'; +import TagPage from 'pages/TagPage'; +import React from 'react'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; + +afterEach(() => { + // restore the spy created with spyOn + jest.restoreAllMocks(); +}); + +jest.mock("components/TagDetails", () => () => { + return
; +}); + +it('renders the tags page component', async () => { + render( + + + {}}/>} /> + + + ); + expect(screen.getByTestId('tag-container')).toBeInTheDocument(); +}); + diff --git a/src/api.js b/src/api.js index 1215585a..98a770b5 100644 --- a/src/api.js +++ b/src/api.js @@ -61,8 +61,9 @@ const api = { }; const endpoints = { - imageList: '/v2/_zot/ext/search?query={RepoListWithNewestImage(){ NewestImage {RepoName Tag LastUpdated Description Licenses Vendor Size Labels} }}', - detailedRepoInfo: (name) => `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag Layers {Size Digest}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {Tag}}}}` + imageList: '/v2/_zot/ext/search?query={RepoListWithNewestImage(){Platforms {Os Arch} NewestImage {RepoName Tag LastUpdated Description Licenses Vendor Size Labels} }}', + detailedRepoInfo: (name) => `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag Layers {Size Digest}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {Tag}}}}`, + vulnerabilitiesForRepo: (name) => `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}` } export {api, endpoints}; diff --git a/src/components/Explore.jsx b/src/components/Explore.jsx index cbcfec7a..bf3e5bab 100644 --- a/src/components/Explore.jsx +++ b/src/components/Explore.jsx @@ -41,7 +41,7 @@ const useStyles = makeStyles(() => ({ sortForm:{ backgroundColor: '#ffffff', borderColor: "#E0E0E0", - borderRadius: "6px", + borderRadius: "0.375em", }, })); @@ -63,6 +63,7 @@ function Explore ({ keywords, data, updateData }) { latestVersion: image.NewestImage.Tag, tags: image.NewestImage.Labels, description: image.NewestImage.Description, + platforms: image.Platforms, licenses: image.NewestImage.Licenses, size: image.NewestImage.Size, vendor: image.NewestImage.Vendor, @@ -105,6 +106,7 @@ function Explore ({ keywords, data, updateData }) { description={item.description} tags={item.tags} vendor={item.vendor} + platforms={item.platforms} size={item.size} licenses={item.licenses} key={index} @@ -145,25 +147,25 @@ function Explore ({ keywords, data, updateData }) { ) : ( - + - + Results {filteredData.length} - + {/* Sort - + */} - + {/* {renderFilterCards()} - - + */} + {renderRepoCards()} diff --git a/src/components/ExploreHeader.jsx b/src/components/ExploreHeader.jsx index 05cc3fe4..92e24c47 100644 --- a/src/components/ExploreHeader.jsx +++ b/src/components/ExploreHeader.jsx @@ -3,7 +3,6 @@ import {Link, useLocation, useNavigate} from "react-router-dom"; // components import {Typography, Breadcrumbs} from '@mui/material'; -import NavigateNextIcon from '@mui/icons-material/NavigateNext'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; // styling @@ -26,7 +25,7 @@ const useStyles = makeStyles((theme) => { explore: { color: '#52637A', fontSize: "1rem", - letterSpacing: "0.009375rem" + letterSpacing: "0.009375rem", } } }); @@ -36,13 +35,18 @@ function ExploreHeader() { const navigate = useNavigate(); const location = useLocation(); const path = location.pathname; - + const pathWithoutImage = path.replace('tag/', ''); + const pathToBeDisplayed = pathWithoutImage.replace('/image/', ''); + const pathHeader = pathToBeDisplayed.replace("/", " / ").replace(/%2F/g,'/'); + const pathWithTag = path.substring(0, path.lastIndexOf('/')); + return (
navigate(-1)}/> Home - { path.includes('/image/') && {path.replace('/image/', '')} } + { path.includes('/image/') && {pathHeader} } +
diff --git a/src/components/Home.jsx b/src/components/Home.jsx index 139085ec..1f9a761d 100644 --- a/src/components/Home.jsx +++ b/src/components/Home.jsx @@ -77,6 +77,7 @@ function Home({ keywords, data, updateData }) { latestVersion: image.NewestImage.Tag, tags: image.NewestImage.Labels, description: image.NewestImage.Description, + platforms: image.Platforms, licenses: image.NewestImage.Licenses, size: image.NewestImage.Size, vendor: image.NewestImage.Vendor, @@ -137,6 +138,7 @@ function Home({ keywords, data, updateData }) { description={item.description} tags={item.tags} vendor={item.vendor} + platforms={item.platforms} size={item.size} licenses={item.licenses} key={index} @@ -160,6 +162,7 @@ function Home({ keywords, data, updateData }) { description={item.description} tags={item.tags} vendor={item.vendor} + platforms={item.platforms} size={item.size} licenses={item.licenses} key={index} @@ -188,10 +191,10 @@ function Home({ keywords, data, updateData }) { {renderPreviewCards()}
- + {/* Bookmarks - {renderBookmarks()} + {renderBookmarks()} */} Recently updated repositories diff --git a/src/components/PreviewCard.jsx b/src/components/PreviewCard.jsx index 7b3d50e6..ace801aa 100644 --- a/src/components/PreviewCard.jsx +++ b/src/components/PreviewCard.jsx @@ -67,9 +67,9 @@ const useStyles = makeStyles(() => ({ })); //function that returns a random element from an array -function getRandom (list) { - return list[Math.floor((Math.random()*list.length))]; -} +// function getRandom (list) { +// return list[Math.floor((Math.random()*list.length))]; +// } function PreviewCard(props) { const classes = useStyles(); @@ -77,29 +77,29 @@ function PreviewCard(props) { const { name } = props; const goToDetails = (repo) => { - navigate(`/image/${name}`); + navigate(`/image/${encodeURIComponent(name)}`); }; - const vulnerabilityCheck = () => { - const noneVulnerability = ; - const unknownVulnerability = ; - const lowVulnerability = ; - const mediumVulnerability = ; - const highVulnerability = ; - const criticalVulnerability = ; + // const vulnerabilityCheck = () => { + // const noneVulnerability = ; + // const unknownVulnerability = ; + // const lowVulnerability = ; + // const mediumVulnerability = ; + // const highVulnerability = ; + // const criticalVulnerability = ; - const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] - return(getRandom(arrVulnerability)); - } + // const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] + // return(getRandom(arrVulnerability)); + // } - const signatureCheck = () => { - const unverifiedSignature = ; - const untrustedSignature = ; - const verifiedSignature = ; + // const signatureCheck = () => { + // const unverifiedSignature = ; + // const untrustedSignature = ; + // const verifiedSignature = ; - const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] - return(getRandom(arrSignature)); - } + // const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] + // return(getRandom(arrSignature)); + // } return ( @@ -107,7 +107,7 @@ function PreviewCard(props) { - + - + {name} - {vulnerabilityCheck()} - {signatureCheck()} + {/* {vulnerabilityCheck()} + {signatureCheck()} */} Official - + {/* */} diff --git a/src/components/RepoCard.jsx b/src/components/RepoCard.jsx index eaacff0a..4c14dcbc 100644 --- a/src/components/RepoCard.jsx +++ b/src/components/RepoCard.jsx @@ -93,62 +93,69 @@ const useStyles = makeStyles(() => ({ function RepoCard(props) { const classes = useStyles(); const navigate = useNavigate(); - const { name, vendor, description, lastUpdated, downloads, rating, version } = + const { name, vendor, platforms, description, lastUpdated, downloads, rating, version } = props; + //function that returns a random element from an array - function getRandom(list) { - return list[Math.floor(Math.random() * list.length)]; - } + // function getRandom(list) { + // return list[Math.floor(Math.random() * list.length)]; + // } - const goToDetails = (repo) => { - navigate(`/image/${name}`); + const goToDetails = () => { + navigate(`/image/${encodeURIComponent(name)}`); }; - const vulnerabilityCheck = () => { - const noneVulnerability = { return; }} deleteIcon={ }/>; - const unknownVulnerability = { return; }} deleteIcon={ }/>; - const lowVulnerability = { return; }} deleteIcon={ }/>; - const mediumVulnerability = { return; }} deleteIcon={ }/>; - const highVulnerability = { return; }} deleteIcon={ }/>; - const criticalVulnerability = { return; }} deleteIcon={ }/>; + // const vulnerabilityCheck = () => { + // const noneVulnerability = { return; }} deleteIcon={ }/>; + // const unknownVulnerability = { return; }} deleteIcon={ }/>; + // const lowVulnerability = { return; }} deleteIcon={ }/>; + // const mediumVulnerability = { return; }} deleteIcon={ }/>; + // const highVulnerability = { return; }} deleteIcon={ }/>; + // const criticalVulnerability = { return; }} deleteIcon={ }/>; - const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] - return(getRandom(arrVulnerability)); - }; + // const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] + // return(getRandom(arrVulnerability)); + // }; - const signatureCheck = () => { - const unverifiedSignature = { return; }} deleteIcon={ }/>; - const untrustedSignature = { return; }} deleteIcon={ }/>; - const verifiedSignature = { return; }} deleteIcon={ }/>; + // const signatureCheck = () => { + // const unverifiedSignature = { return; }} deleteIcon={ }/>; + // const untrustedSignature = { return; }} deleteIcon={ }/>; + // const verifiedSignature = { return; }} deleteIcon={ }/>; - const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] - return(getRandom(arrSignature)); - } + // const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] + // return(getRandom(arrSignature)); + // } const platformChips = () => { // if platforms not received, mock data - const platforms = props.platforms || [ - "Windows", - "PowerPC64LE", - "IBM Z", - "Linux", - ]; - return platforms.map((platform, index) => ( - + const platformsOsArch = platforms || []; + return platformsOsArch.map((platform,index) => ( + + + + )); }; const getVendor = () => { - return `${vendor || "andrewc"} •`; + return `${vendor || "N/A"} •`; }; const getVersion = () => { const lastDate = lastUpdated @@ -182,8 +189,8 @@ function RepoCard(props) { {name} - {vulnerabilityCheck()} - {signatureCheck()} + {/* {vulnerabilityCheck()} + {signatureCheck()} */} {/* { return }} deleteIcon={vulnerabilityCheck()} /> */} {description || - "The complete solution for node.js command-line programs"} + "N/A"} {platformChips()} @@ -218,14 +225,14 @@ function RepoCard(props) { className={classes.contentRight} > - + {/* Downloads • {downloads || "-"} Rating • {rating || "-"} - + */} - + {/* */}
diff --git a/src/components/RepoDetails.jsx b/src/components/RepoDetails.jsx index f3a36434..9721bf8e 100644 --- a/src/components/RepoDetails.jsx +++ b/src/components/RepoDetails.jsx @@ -82,7 +82,7 @@ const useStyles = makeStyles((theme) => ({ flexDirection:"row", alignItems:"start", background:"#FFFFFF", - border: "1px solid #E0E5EB", + border: "0.0625rem solid #E0E5EB", borderRadius:"2rem", flex:"none", alignSelf:"stretch", @@ -101,7 +101,7 @@ const useStyles = makeStyles((theme) => ({ }, inputForm:{ '& fieldset':{ - border: "2px solid #52637A", + border: "0.125rem solid #52637A", }, }, @@ -142,12 +142,14 @@ function RepoDetails (props) { let repoInfo = response.data.data.ExpandedRepoInfo; let imageData = { name: name, - tags: repoInfo.Manifests, + images: repoInfo.Images, lastUpdated: repoInfo.Summary?.LastUpdated, size: repoInfo.Summary?.Size, + //latestDigest: repoInfo.Images[0].Digest, + //layers: repoInfo.Images[0].Layers, platforms: repoInfo.Summary?.Platforms, vendors: repoInfo.Summary?.Vendors, - newestTag: repoInfo.Summary?.NewestTag + newestTag: repoInfo.Summary?.NewestImage } setRepoDetailData(imageData); setIsLoading(false); @@ -159,41 +161,57 @@ function RepoDetails (props) { }); }, [name]) //function that returns a random element from an array - function getRandom(list) { - return list[Math.floor(Math.random() * list.length)]; - } + // function getRandom(list) { + // return list[Math.floor(Math.random() * list.length)]; + // } - const vulnerabilityCheck = () => { - const noneVulnerability = { return; }} deleteIcon={ }/>; - const unknownVulnerability = { return; }} deleteIcon={ }/>; - const lowVulnerability = { return; }} deleteIcon={ }/>; - const mediumVulnerability = { return; }} deleteIcon={ }/>; - const highVulnerability = { return; }} deleteIcon={ }/>; - const criticalVulnerability = { return; }} deleteIcon={ }/>; + // const vulnerabilityCheck = () => { + // const noneVulnerability = { return; }} deleteIcon={ }/>; + // const unknownVulnerability = { return; }} deleteIcon={ }/>; + // const lowVulnerability = { return; }} deleteIcon={ }/>; + // const mediumVulnerability = { return; }} deleteIcon={ }/>; + // const highVulnerability = { return; }} deleteIcon={ }/>; + // const criticalVulnerability = { return; }} deleteIcon={ }/>; - const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] - return(getRandom(arrVulnerability)); - }; + // const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] + // return(getRandom(arrVulnerability)); + // }; - const signatureCheck = () => { - const unverifiedSignature = { return; }} deleteIcon={ }/>; - const untrustedSignature = { return; }} deleteIcon={ }/>; - const verifiedSignature = { return; }} deleteIcon={ }/>; + // const signatureCheck = () => { + // const unverifiedSignature = { return; }} deleteIcon={ }/>; + // const untrustedSignature = { return; }} deleteIcon={ }/>; + // const verifiedSignature = { return; }} deleteIcon={ }/>; - const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] - return(getRandom(arrSignature)); - } + // const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] + // return(getRandom(arrSignature)); + // } + const platformChips = () => { - // if platforms not received, mock data // @ts-ignore - const platforms = repoDetailData.platforms || ["Windows","PowerPC64LE","IBM Z","Linux"]; + const platforms = repoDetailData?.platforms || []; + return platforms.map((platform, index) => ( - + + + + )); } @@ -207,7 +225,7 @@ function RepoDetails (props) { {overviewTitle || 'Quickstart'} - {description || mockData.loremIpsum} + {description || "N/A"} ); @@ -256,12 +274,12 @@ function RepoDetails (props) { {name} - {vulnerabilityCheck()} - {signatureCheck()} - + {/* {vulnerabilityCheck()} + {signatureCheck()} */} + {/* */} - {description || 'The complete solution for node.js command-line programs'} + {description || 'N/A'} {platformChips()} @@ -271,9 +289,10 @@ function RepoDetails (props) { Copy and pull to pull this image navigator.clipboard.writeText(`Pull ${name}`)} data-testid='pullcopy-btn'> @@ -332,11 +351,11 @@ function RepoDetails (props) {
diff --git a/src/components/RepoDetailsMetadata.jsx b/src/components/RepoDetailsMetadata.jsx index d7a03769..b790d008 100644 --- a/src/components/RepoDetailsMetadata.jsx +++ b/src/components/RepoDetailsMetadata.jsx @@ -43,7 +43,7 @@ function RepoDetailsMetadata (props) { Repository - {repoURL || `----`} + {repoURL || `N/A`}
@@ -51,7 +51,7 @@ function RepoDetailsMetadata (props) { Weekly downloads - {weeklyDownloads || `----`} + {weeklyDownloads || `N/A`} @@ -60,7 +60,7 @@ function RepoDetailsMetadata (props) { Last publish - {lastDate || `35 days ago`} + {lastDate || `35 days ago`} @@ -68,12 +68,12 @@ function RepoDetailsMetadata (props) { Total size - {transform.formatBytes(size) || `----`} + {transform.formatBytes(size) || `----`} - + {/* @@ -82,7 +82,7 @@ function RepoDetailsMetadata (props) { - + */} ) } diff --git a/src/components/SignIn.jsx b/src/components/SignIn.jsx index a8c375f1..5756831b 100644 --- a/src/components/SignIn.jsx +++ b/src/components/SignIn.jsx @@ -28,7 +28,7 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", justifyContent: "center", margin: "0 auto", - padding: "10px", + padding: "0.625rem", position: "relative", }, loginCard: { @@ -42,7 +42,8 @@ const useStyles = makeStyles((theme) => ({ background: "#FFFFFF", gap: "0.625em", boxShadow: "0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)", - borderRadius: "1.5rem", + borderRadius: "1.5rem", + minWidth: "30rem" }, loginCardContent: { alignItems: "center", @@ -65,33 +66,33 @@ const useStyles = makeStyles((theme) => ({ }, textField: { - borderRadius: "4px", + borderRadius: "0.25rem", }, button: { textTransform: "none", color: "##FFFFFF", fontSize: "1.4375rem", fontWeight: "500", - height: "50px", - borderRadius: "4px", + height: "3.125rem", + borderRadius: "0.25rem", letterSpacing:"0.01rem", }, gitLogo: { height: "24px", - borderRadius: "4px", + borderRadius: "0.25rem", paddingLeft: "1rem", }, line: { width: "100%", textAlign: "center", - borderBottom: "1px solid #C2CBD6", + borderBottom: "0.0625rem solid #C2CBD6", lineHeight: "0.1rem", - margin: "10px 0 20px", + margin: "0.625rem 0 1.25rem", }, lineSpan:{ background: "#ffffff", color: "#C2CBD6", - padding: "0 10px", + padding: "0 0.625rem", fontSize: "1rem", fontWeight: "400", paddingLeft: "1rem", diff --git a/src/components/TagDetails.jsx b/src/components/TagDetails.jsx new file mode 100644 index 00000000..c157f3d3 --- /dev/null +++ b/src/components/TagDetails.jsx @@ -0,0 +1,307 @@ +// react global +import { useParams } from 'react-router-dom' +import React, { useEffect, useState } from 'react'; + +// utility +import {api, endpoints} from '../api'; +import mockData from '../utilities/mockData'; + +// components +import {Box, Card, CardContent, CardMedia, Chip, FormControl, Grid, IconButton, InputAdornment, OutlinedInput, Stack, Tab, Typography} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { host } from '../host'; +import PestControlOutlinedIcon from "@mui/icons-material/PestControlOutlined"; +import PestControlIcon from "@mui/icons-material/PestControl"; +import GppBadOutlinedIcon from "@mui/icons-material/GppBadOutlined"; +import GppGoodOutlinedIcon from "@mui/icons-material/GppGoodOutlined"; +import GppMaybeOutlinedIcon from "@mui/icons-material/GppMaybeOutlined"; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import BookmarkIcon from '@mui/icons-material/Bookmark'; + +// placeholder images +import repocube1 from '../assets/repocube-1.png'; +import repocube2 from '../assets/repocube-2.png'; +import repocube3 from '../assets/repocube-3.png'; +import repocube4 from '../assets/repocube-4.png'; +import { TabContext, TabList, TabPanel } from '@mui/lab'; +import TagDetailsMetadata from './TagDetailsMetadata'; +import VulnerabilitiesDetails from './VulnerabilitiesDetails'; +import { padding } from '@mui/system'; + +// @ts-ignore +const useStyles = makeStyles((theme) => ({ + pageWrapper: { + backgroundColor: "#FFFFFF", + height: '100vh', + }, + container: { + paddingTop: 5, + paddingBottom: 5, + marginTop: 100, + backgroundColor: "#FFFFFF", + }, + repoName: { + fontWeight:"700", + fontSize:"2.5rem", + color:"#0F2139" + }, + avatar: { + height:"3rem", + width:"3rem" + }, + cardBtn: { + height: "100%", + width: "100%" + }, + media: { + borderRadius: '3.125em', + }, + tabs: { + marginTop: "3rem", + padding:"0.5rem", + height: "100%" + }, + tabContent:{ + height:"100%" + }, + selectedTab: { + background:"#D83C0E", + borderRadius:"1.5rem" + }, + tabPanel: { + height:"100%", + paddingLeft: "0rem!important" + }, + metadata: { + marginTop: "8rem", + paddingLeft:"1.5rem", + }, + card: { + marginBottom: 2, + display:"flex", + flexDirection:"row", + alignItems:"start", + background:"#FFFFFF", + border: "0.0625rem solid #E0E5EB", + borderRadius:"2rem", + flex:"none", + alignSelf:"stretch", + flexGrow:0, + order:0, + width:"100%", + boxShadow: "none!important" + }, + platformText:{ + backgroundColor:"#EDE7F6", + color: "#220052", + fontWeight:'400', + fontSize:'0.8125rem', + lineHeight:'1.125rem', + letterSpacing:'0.01rem' + }, + inputForm:{ + '& fieldset':{ + border: "0.125rem solid #52637A", + }, + + }, + cardRoot:{ + boxShadow: "none!important", + }, + header:{ + paddingLeft:"2rem" + } +})); + + +// temporary utility to get image +const randomIntFromInterval = (min, max) => { + return Math.floor(Math.random() * (max - min + 1) + min) +}; + +const randomImage = () => { + const imageArray = [repocube1,repocube2,repocube3,repocube4]; + return imageArray[randomIntFromInterval(0,3)]; +}; + +function TagDetails (props) { + const [repoDetailData, setRepoDetailData] = useState({}); + // @ts-ignore + const [isLoading, setIsLoading] = useState(false); + const [selectedTab, setSelectedTab] = useState("Vulnerabilities"); + + // get url param from { + api.get(`${host()}${endpoints.detailedRepoInfo(name)}`) + .then(response => { + if (response.data && response.data.data) { + let repoInfo = response.data.data.ExpandedRepoInfo; + let imageData = { + name: name, + tags: repoInfo.Images[0].Tag, + lastUpdated: repoInfo.Summary?.LastUpdated, + size: repoInfo.Summary?.Size, + latestDigest: repoInfo.Images[0].Digest, + layers: repoInfo.Images[0].Layers, + platforms: repoInfo.Summary?.Platforms, + vendors: repoInfo.Summary?.Vendors, + newestTag: repoInfo.Summary?.NewestImage + } + setRepoDetailData(imageData); + setIsLoading(false); + } + }) + .catch((e) => { + console.error(e); + setRepoDetailData({}); + }); + }, [name]) + //function that returns a random element from an array + // function getRandom(list) { + // return list[Math.floor(Math.random() * list.length)]; + // } + + // const vulnerabilityCheck = () => { + // const noneVulnerability = { return; }} deleteIcon={ }/>; + // const unknownVulnerability = { return; }} deleteIcon={ }/>; + // const lowVulnerability = { return; }} deleteIcon={ }/>; + // const mediumVulnerability = { return; }} deleteIcon={ }/>; + // const highVulnerability = { return; }} deleteIcon={ }/>; + // const criticalVulnerability = { return; }} deleteIcon={ }/>; + + // const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability] + // return(getRandom(arrVulnerability)); + // }; + + // const signatureCheck = () => { + // const unverifiedSignature = { return; }} deleteIcon={ }/>; + // const untrustedSignature = { return; }} deleteIcon={ }/>; + // const verifiedSignature = { return; }} deleteIcon={ }/>; + + // const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature] + // return(getRandom(arrSignature)); + // } + + const getPlatform = () => { + // @ts-ignore + return repoDetailData?.platforms? repoDetailData.platforms[0] : '--/--'; + } + + // @ts-ignore + const handleTabChange = (event, newValue) => { + setSelectedTab(newValue); + }; + + //will need this but not for now + // const renderDependencies = () => { + // return ( + // + // Dependecies ({dependencies || '---'}) + // + // ); + // }; + + // const renderDependents = () => { + // return ( + // + // Dependents ({dependents || '---'}) + // + // ); + // }; + + // const renderVulnerabilities = () => { + // return ( + // + // Vulnerabilities + // + // ); + // }; + + + return ( +
+ + + + + + + + {name}:{repoDetailData?. +// @ts-ignore + tags} + + {/* {vulnerabilityCheck()} + {signatureCheck()} */} + {/* */} + + + Digest: {repoDetailData?. +// @ts-ignore + latestDigest} + + + + + + + + + + {/* + + */} + + + + + {/* + Layers + + + Depends On + + + Is Dependent On + */} + + + + + + + + + + + + + + +
+ ); +} + +export default TagDetails; diff --git a/src/components/TagDetailsMetadata.jsx b/src/components/TagDetailsMetadata.jsx new file mode 100644 index 00000000..12425cbb --- /dev/null +++ b/src/components/TagDetailsMetadata.jsx @@ -0,0 +1,72 @@ +import { Card, CardContent, Grid, Typography } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { DateTime } from 'luxon'; +import React from 'react'; +import transform from '../utilities/transform'; + +const useStyles = makeStyles(() => ({ + card: { + marginBottom: 2, + display:"flex", + flexDirection:"row", + alignItems:"start", + background:"#FFFFFF", + boxShadow:"0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)", + borderRadius:"1.5rem", + flex:"none", + alignSelf:"stretch", + flexGrow:0, + order:0, + width:"100%" + }, + metadataHeader: { + color: "rgba(0, 0, 0, 0.6)" + }, + metadataBody: { + color: "rgba(0, 0, 0, 0.87)", + fontFamily: 'Roboto', + fontStyle: "normal", + fontWeight: 400, + fontSize: "1rem", + lineHeight: "150%", + align:"left" + } +})); + +function TagDetailsMetadata (props) { + const classes = useStyles(); + const {platforms, lastUpdated, size} = props; + const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({ unit: 'days' }) + return ( + + + + + OS/Arch + {platforms.Os || `----`} / {platforms.Arch || `----`} + + + + + + + Total Size + {transform.formatBytes(size) || `----`} + + + + + + + + Last Published + {lastDate || `----`} + + + + + + ) +} + +export default TagDetailsMetadata; \ No newline at end of file diff --git a/src/components/Tags.jsx b/src/components/Tags.jsx index 01a7c822..dd2b102a 100644 --- a/src/components/Tags.jsx +++ b/src/components/Tags.jsx @@ -1,78 +1,117 @@ // react global import * as React from 'react'; +import { useNavigate } from "react-router-dom"; import PropTypes from 'prop-types'; +import { DateTime } from 'luxon'; // components import Box from '@mui/material/Box'; import Collapse from '@mui/material/Collapse'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; -import TableCell, {tableCellClasses} from '@mui/material/TableCell'; +import TableCell, { tableCellClasses } from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Typography from '@mui/material/Typography'; import transform from 'utilities/transform'; -import { Card, CardContent, Divider } from '@mui/material'; +import { Card, CardContent, Divider, Stack } from '@mui/material'; import { makeStyles } from '@mui/styles'; +import { useEffect } from 'react'; const useStyles = makeStyles(() => ({ + tagCard: { + marginBottom: 2, + display: "flex", + flexDirection: "row", + alignItems: "center", + background: "#FFFFFF", + boxShadow: "none!important", + borderRadius: "1.875rem", + flex: "none", + alignSelf: "stretch", + flexGrow: 0, + order: 0, + width: "100%" + }, card: { marginBottom: 2, - display:"flex", - flexDirection:"row", - alignItems:"center", - background:"#FFFFFF", - boxShadow:"0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)", - borderRadius:"1.875rem", - flex:"none", - alignSelf:"stretch", - flexGrow:0, - order:0, - width:"100%" + display: "flex", + flexDirection: "row", + alignItems: "center", + background: "#FFFFFF", + boxShadow: "0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)", + borderRadius: "1.875rem", + flex: "none", + alignSelf: "stretch", + flexGrow: 0, + order: 0, + width: "100%" }, content: { textAlign: "left", color: "#606060", padding: "2% 3% 2% 3%", - width:"100%" + width: "100%" } })); + + function TagCard(props) { - const {data, row} = props; - const tags = data && data.tags; + const { row, lastUpdated, vendors, size, platform } = props; + + //const tags = data && data.tags; const [open, setOpen] = React.useState(false); + const [digests, setDigests] = React.useState([]); const classes = useStyles(); + const tagRow = row; + const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({ unit: 'days' }) + const navigate = useNavigate(); + + + useEffect(() => { + const tagDigest = [{ digest: tagRow.Digest, osArch: platform[0], size: size }]; + setDigests(tagDigest); + }, []); + + const goToTags = (tag) => { + navigate(`tag/${tag}`); + }; return ( - {row.Tag} - Last pushed {row.lastUpdated || '----'} by {row.vendor || '----'} - setOpen(!open)}>{!open? 'See layers' : 'Hide layers'} + Tag + goToTags(tagRow.Tag)}>{tagRow.Tag} + + + + Last pushed + + + {lastDate || '----'} by {vendors[0] || 'N/A'} + + + + setOpen(!open)}>{!open ? 'See digests' : 'Hide digests'} - - { - // Layers - } - - +
- Digest - OS/ARCH - Size + Digest + OS/ARCH + Size - {row.Layers.map((layer) => ( - {navigator.clipboard.writeText(layer.Digest)}}> - {layer.Digest?.substr(0,12)} - ----------- - - {transform.formatBytes(layer.Size)} + {digests.map((layer) => ( + { navigator.clipboard.writeText(layer.digest) }}> + {layer.digest?.substr(0, 12)} + {layer.osArch?.Os}/{layer.osArch?.Arch} + + {transform.formatBytes(layer.size)} ))} @@ -85,24 +124,24 @@ function TagCard(props) { ); } -TagCard.propTypes = { - row: PropTypes.shape({ - Layers: PropTypes.arrayOf( - PropTypes.shape({ - Digest: PropTypes.string.isRequired, - Size: PropTypes.string.isRequired, - }), - ).isRequired, - Tag: PropTypes.string.isRequired, - }).isRequired, -}; +// TagCard.propTypes = { +// row: PropTypes.shape({ +// Layers: PropTypes.arrayOf( +// PropTypes.shape({ +// Digest: PropTypes.string.isRequired, +// Size: PropTypes.string.isRequired, +// }), +// ).isRequired, +// Tag: PropTypes.string.isRequired, +// }).isRequired, +// }; -const renderTags = (tags) => { +const renderTags = (tags, lastUpdated, vendors, size, platform) => { const cmp = tags && tags.map((tag, index) => { - return ( - - ); + return ( + + ); }); return cmp; } @@ -110,15 +149,16 @@ const renderTags = (tags) => { export default function Tags(props) { const classes = useStyles(); - const {data} = props; - const {tags} = data; + const { data } = props; + const { images, lastUpdated, vendors, size, platforms } = data; + return ( - + - Tags history - - {renderTags(tags)} + Tags History + + {renderTags(images, lastUpdated, vendors, size, platforms)} ); diff --git a/src/components/VulnerabilitiesDetails.jsx b/src/components/VulnerabilitiesDetails.jsx new file mode 100644 index 00000000..3dd4ea02 --- /dev/null +++ b/src/components/VulnerabilitiesDetails.jsx @@ -0,0 +1,175 @@ +import { useParams } from 'react-router-dom' +import React, { useEffect, useState } from 'react'; + +// utility +import { api, endpoints } from '../api'; +import mockData from '../utilities/mockData'; + +// components +import Collapse from '@mui/material/Collapse'; +import { Box, Card, CardContent, Divider, Chip, FormControl, Grid, IconButton, InputAdornment, OutlinedInput, Stack, Tab, Typography } from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import { host } from '../host'; +import PestControlOutlinedIcon from "@mui/icons-material/PestControlOutlined"; +import PestControlIcon from "@mui/icons-material/PestControl"; +import GppBadOutlinedIcon from "@mui/icons-material/GppBadOutlined"; +import GppGoodOutlinedIcon from "@mui/icons-material/GppGoodOutlined"; +import GppMaybeOutlinedIcon from "@mui/icons-material/GppMaybeOutlined"; + + +const useStyles = makeStyles((theme) => ({ + card: { + display: "flex", + flexDirection: "row", + alignItems: "center", + background: "#FFFFFF", + boxShadow: "0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)", + borderRadius: "1.875rem", + flex: "none", + alignSelf: "stretch", + flexGrow: 0, + order: 0, + width: "100%", + marginTop: "2rem", + marginBottom: "2rem" + }, + content: { + textAlign: "left", + color: "#606060", + padding: "2% 3% 2% 3%", + width: "100%" + }, + title: { + color: "#828282", + fontSize: "1rem", + paddingRight: "0.5rem", + paddingBottom: "0.5rem", + paddingTop: "0.5rem" + + }, + values: { + color: "#000000", + fontSize: "1rem", + fontWeight: "600", + paddingBottom: "0.5rem", + paddingTop: "0.5rem" + } +})); + +const vulnerabilityCheck = (status) => { + const noneVulnerability = { return; }} deleteIcon={} />; + const unknownVulnerability = { return; }} deleteIcon={} />; + const lowVulnerability = { return; }} deleteIcon={} />; + const mediumVulnerability = { return; }} deleteIcon={} />; + const highVulnerability = { return; }} deleteIcon={} />; + const criticalVulnerability = { return; }} deleteIcon={} />; + + let result; + switch (status) { + case "NONE": + result = noneVulnerability; + break; + case "LOW": + result = lowVulnerability; + break; + case "MEDIUM": + result = mediumVulnerability; + break; + case "HIGH": + result = highVulnerability; + break; + case "CRITICAL": + result = criticalVulnerability; + break; + default: + result = unknownVulnerability; + } + + return result; +}; + + +function VulnerabilitiyCard(props) { + const classes = useStyles(); + const { cve } = props; + const [open, setOpen] = React.useState(false); + + return ( + + + + ID: + {cve.Id} + + {vulnerabilityCheck(cve.Severity)} + + Title: + {cve.Title} + + setOpen(!open)}>{!open ? 'See description' : 'Hide description'} + + + {cve.Description} + + + + + ) + +} + + + + +function VulnerabilitiesDetails(props) { + const [cveData, setCveData] = useState({}); + const [isLoading, setIsLoading] = useState(true); + const { name } = props; + + + useEffect(() => { + api.get(`${host()}${endpoints.vulnerabilitiesForRepo(name)}`) + .then(response => { + if (response.data && response.data.data) { + let cveInfo = response.data.data.CVEListForImage; + let cveListData = { + cveList: cveInfo?.CVEList + } + setCveData(cveListData); + setIsLoading(false); + } + }) + .catch((e) => { + console.error(e); + setCveData({}); + }); + }, []) + + + + const renderCVEs = (cves) => { + if (cves?.length !== 0) { + return (cves && cves.map((cve, index) => { + return ( + + ); + }) + ); + } + else { + return ( No Vulnerabilities ); + } + } + + return ( +
+ Vulnerabilities + + {renderCVEs(cveData?. + // @ts-ignore + cveList)} +
+ ) +} + +export default VulnerabilitiesDetails; diff --git a/src/index.css b/src/index.css index b9b4a2bb..9a9f4a12 100644 --- a/src/index.css +++ b/src/index.css @@ -1,11 +1,13 @@ body { - margin: 0; + margin: 0rem 0rem 5rem 0rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; min-height: 100vh; + overflow-x: hidden; + /* background-image: url(./assets/background.png); */ background-color: #F6F7F9 !important; } diff --git a/src/pages/TagPage.jsx b/src/pages/TagPage.jsx new file mode 100644 index 00000000..8ce2f982 --- /dev/null +++ b/src/pages/TagPage.jsx @@ -0,0 +1,50 @@ +// react global +import React from 'react'; + +// components + +import makeStyles from '@mui/styles/makeStyles'; +import { Container, Grid, Stack } from '@mui/material'; +import Header from 'components/Header'; +import TagDetails from 'components/TagDetails'; +import ExploreHeader from 'components/ExploreHeader'; + +const useStyles = makeStyles((theme) => ({ + pageWrapper: { + height:"100%" + }, + container: { + paddingTop: 5, + paddingBottom: 5, + backgroundColor: "#FFFFFF", + }, + parentWrapper: { + height: '100vh', + }, + gridWrapper: { + paddingTop: 10, + paddingBottom: 10, + backgroundColor: "#fff", + width:"100%", + }, +})); + +function TagPage(props) { + const classes = useStyles(); + + return ( + +
+ + + + + + + + +
+ ); +} + +export default TagPage;