From 9baeb5bba2fc5bf62c222612b7568ba6d145a136 Mon Sep 17 00:00:00 2001 From: Raul Kele Date: Mon, 10 Oct 2022 12:50:09 +0300 Subject: [PATCH] feat: Implemented fixed-in, fixed tagdetails page query Signed-off-by: Raul Kele --- src/__tests__/TagPage/TagDetails.test.js | 108 +++++++++--------- .../TagPage/VulnerabilitiesDetails.test.js | 33 +++++- src/api.js | 4 + src/components/HistoryLayers.jsx | 36 +++--- src/components/RepoDetails.jsx | 12 -- src/components/TagDetails.jsx | 100 ++++++---------- src/components/TagDetailsMetadata.jsx | 4 +- src/components/VulnerabilitiesDetails.jsx | 59 +++++++++- 8 files changed, 202 insertions(+), 154 deletions(-) diff --git a/src/__tests__/TagPage/TagDetails.test.js b/src/__tests__/TagPage/TagDetails.test.js index 656ff077..e0329836 100644 --- a/src/__tests__/TagPage/TagDetails.test.js +++ b/src/__tests__/TagPage/TagDetails.test.js @@ -4,64 +4,56 @@ import TagDetails from 'components/TagDetails'; import React from 'react'; const mockImage = { - ExpandedRepoInfo: { - Images: [ + Image: { + RepoName: 'centos', + Tag: '8', + Digest: 'sha256:63a795ca90aa6e7cca60941e826810a4cd0a2e73ea02bf458241df2a5c973e29', + LastUpdated: '2020-12-08T00:22:52.526672082Z', + Size: '75183423', + ConfigDigest: 'sha256:8dd57e171a61368ffcfde38045ddb6ed74a32950c271c1da93eaddfb66a77e78', + Platform: { + Os: 'linux', + Arch: 'amd64' + }, + Vendor: 'CentOS', + History: [ { - 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' + Layer: { + Size: '75181999', + Digest: 'sha256:7a0437f04f83f084b7ed68ad9c4a4947e12fc4e1b006b38129bac89114ec3621', + Score: null + }, + HistoryDescription: { + Created: '2020-12-08T00:22:52.526672082Z', + CreatedBy: + '/bin/sh -c #(nop) ADD file:bd7a2aed6ede423b719ceb2f723e4ecdfa662b28639c8429731c878e86fb138b in / ', + Author: '', + Comment: '', + EmptyLayer: false } - ], - Vendors: [''], - NewestImage: null - } + }, + { + Layer: null, + HistoryDescription: { + Created: '2020-12-08T00:22:52.895811646Z', + CreatedBy: + '/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20201204', + Author: '', + Comment: '', + EmptyLayer: true + } + }, + { + Layer: null, + HistoryDescription: { + Created: '2020-12-08T00:22:53.076477777Z', + CreatedBy: '/bin/sh -c #(nop) CMD ["/bin/bash"]', + Author: '', + Comment: '', + EmptyLayer: true + } + } + ] } }; @@ -73,6 +65,10 @@ jest.mock('react-router-dom', () => ({ } })); +beforeEach(() => { + window.scrollTo = jest.fn(); +}); + afterEach(() => { // restore the spy created with spyOn jest.restoreAllMocks(); diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index 0ce24271..88e44eb1 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -2,9 +2,14 @@ import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { api } from 'api'; import VulnerabilitiesDetails from 'components/VulnerabilitiesDetails'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; const StateVulnerabilitiesWrapper = () => { - return ; + return ( + + + + ); }; const mockCVEList = { @@ -426,6 +431,20 @@ const mockCVEList = { } }; +const mockCVEFixed = { + ImageListWithCVEFixed: [ + { + Tag: '1.0.16' + }, + { + Tag: '0.4.33' + }, + { + Tag: '1.0.17' + } + ] +}; + afterEach(() => { // restore the spy created with spyOn jest.restoreAllMocks(); @@ -468,4 +487,16 @@ describe('Vulnerabilties page', () => { render(); await waitFor(() => expect(error).toBeCalledTimes(1)); }); + + it('should find out which version fixes the CVEs', async () => { + jest + .spyOn(api, 'get') + // @ts-ignore + .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) + // @ts-ignore + .mockResolvedValue({ status: 200, data: { data: mockCVEFixed } }); + render(); + await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); + await waitFor(() => expect(screen.getAllByText('1.0.16')).toHaveLength(20)); + }); }); diff --git a/src/api.js b/src/api.js index a46e0e50..b163f7b3 100644 --- a/src/api.js +++ b/src/api.js @@ -64,10 +64,14 @@ const endpoints = { '/v2/_zot/ext/search?query={RepoListWithNewestImage(){Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Description Licenses Title Source Documentation History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}} Vendor Labels} DownloadCount}}', 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 {RepoName Layers {Size Digest} Digest Tag Title Documentation DownloadCount Source Description History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}}}}}}`, + detailedImageInfo: (name, tag) => + `/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName Tag Digest LastUpdated Size ConfigDigest Platform {Os Arch} Vendor History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`, vulnerabilitiesForRepo: (name) => `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`, layersDetailsForImage: (name) => `/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`, + imageListWithCVEFixed: (cveId, repoName) => + `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}") {Tag}}`, dependsOnForImage: (name) => `/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName}}`, isDependentOnForImage: (name) => `/v2/_zot/ext/search?query={DerivedImageList(image: "${name}"){RepoName}}`, globalSearch: ({ searchQuery = '""', pageNumber = 1, pageSize = 15, filter = {} }) => { diff --git a/src/components/HistoryLayers.jsx b/src/components/HistoryLayers.jsx index 2c8bc479..9665f69d 100644 --- a/src/components/HistoryLayers.jsx +++ b/src/components/HistoryLayers.jsx @@ -9,6 +9,7 @@ import { Card, CardContent, Divider, Grid, Stack, Typography } from '@mui/materi import makeStyles from '@mui/styles/makeStyles'; import { host } from '../host'; import Monitor from '../assets/Monitor.png'; +import { isEmpty } from 'lodash'; const useStyles = makeStyles(() => ({ card: { @@ -116,23 +117,28 @@ function HistoryLayers(props) { const [historyData, setHistoryData] = useState([]); const [selectedIndex, setSelectedIndex] = useState(0); const [isLoaded, setIsLoaded] = useState(false); - const { name } = props; + const { name, history } = props; useEffect(() => { - api - .get(`${host()}${endpoints.layersDetailsForImage(name)}`) - .then((response) => { - if (response.data && response.data.data) { - let layersHistory = response.data.data.Image; - setHistoryData(layersHistory?.History); - setIsLoaded(true); - } - }) - .catch((e) => { - console.error(e); - setHistoryData([]); - setIsLoaded(false); - }); + if (history && !isEmpty(history)) { + setHistoryData(history); + setIsLoaded(true); + } else { + api + .get(`${host()}${endpoints.layersDetailsForImage(name)}`) + .then((response) => { + if (response.data && response.data.data) { + let layersHistory = response.data.data.Image; + setHistoryData(layersHistory?.History); + setIsLoaded(true); + } + }) + .catch((e) => { + console.error(e); + setHistoryData([]); + setIsLoaded(false); + }); + } }, [name]); return ( diff --git a/src/components/RepoDetails.jsx b/src/components/RepoDetails.jsx index e6160605..28c58adc 100644 --- a/src/components/RepoDetails.jsx +++ b/src/components/RepoDetails.jsx @@ -175,18 +175,6 @@ function RepoDetails() { // 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={ }/>; diff --git a/src/components/TagDetails.jsx b/src/components/TagDetails.jsx index c0c24185..b97b32e1 100644 --- a/src/components/TagDetails.jsx +++ b/src/components/TagDetails.jsx @@ -118,61 +118,52 @@ const randomImage = () => { }; function TagDetails() { - const [repoDetailData, setRepoDetailData] = useState({}); + const [imageDetailData, setImageDetailData] = useState({}); // @ts-ignore //const [isLoading, setIsLoading] = useState(false); const [selectedTab, setSelectedTab] = useState('Layers'); - const [tagName, setTagName] = useState(''); + const [fullName, setFullName] = useState(''); // get url param from { + // if same-page navigation because of tag update, following 2 lines help ux + setSelectedTab('Layers'); + window?.scrollTo(0, 0); api - .get(`${host()}${endpoints.detailedRepoInfo(name)}`) + .get(`${host()}${endpoints.detailedImageInfo(name, tag)}`) .then((response) => { if (response.data && response.data.data) { - let repoInfo = response.data.data.ExpandedRepoInfo; + let imageInfo = response.data.data.Image; 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?.Tag + name: imageInfo.RepoName, + tag: imageInfo.Tag, + lastUpdated: imageInfo.LastUpdated, + size: imageInfo.Size, + digest: imageInfo.ConfigDigest, + platform: imageInfo.Platform, + vendor: imageInfo.Vendor, + history: imageInfo.History }; - setRepoDetailData(imageData); - setTagName(imageData.name + ':' + imageData.newestTag); + setImageDetailData(imageData); + setFullName(imageData.name + ':' + imageData.tag); + //setIsLoading(false); } }) .catch((e) => { console.error(e); - setRepoDetailData({}); + setImageDetailData({}); }); - }, [name]); + }, [name, tag]); //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={ }/>; @@ -184,7 +175,7 @@ function TagDetails() { const getPlatform = () => { // @ts-ignore - return repoDetailData?.platforms ? repoDetailData.platforms[0] : '--/--'; + return imageDetailData?.platform ? imageDetailData.platform : '--/--'; }; // @ts-ignore @@ -192,31 +183,6 @@ function TagDetails() { setSelectedTab(newValue); }; - //will need this but not for now - // const renderDependencies = () => { - // return ( - // - // Dependecies ({dependencies || '---'}) - // - // ); - // }; - - // const renderDependents = () => { - // return ( - // - // Dependents ({dependents || '---'}) - // - // ); - // }; - - // const renderVulnerabilities = () => { - // return ( - // - // Vulnerabilities - // - // ); - // }; - return (
@@ -237,7 +203,7 @@ function TagDetails() { {name}: { // @ts-ignore - repoDetailData?.newestTag + tag } {/* {vulnerabilityCheck()} @@ -253,7 +219,7 @@ function TagDetails() { Digest:{' '} { // @ts-ignore - repoDetailData?.latestDigest + imageDetailData?.digest } @@ -280,13 +246,19 @@ function TagDetails() { - + - + - + @@ -299,11 +271,11 @@ function TagDetails() { diff --git a/src/components/TagDetailsMetadata.jsx b/src/components/TagDetailsMetadata.jsx index 6216ed2f..dc03bd6a 100644 --- a/src/components/TagDetailsMetadata.jsx +++ b/src/components/TagDetailsMetadata.jsx @@ -35,7 +35,7 @@ const useStyles = makeStyles(() => ({ function TagDetailsMetadata(props) { const classes = useStyles(); - const { platforms, lastUpdated, size } = props; + const { platform, lastUpdated, size } = props; const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({ unit: 'days' }); @@ -48,7 +48,7 @@ function TagDetailsMetadata(props) { OS/Arch - {platforms.Os || `----`} / {platforms.Arch || `----`} + {platform?.Os || `----`} / {platform?.Arch || `----`} diff --git a/src/components/VulnerabilitiesDetails.jsx b/src/components/VulnerabilitiesDetails.jsx index 6717e23f..0e77310c 100644 --- a/src/components/VulnerabilitiesDetails.jsx +++ b/src/components/VulnerabilitiesDetails.jsx @@ -11,6 +11,8 @@ import { host } from '../host'; import PestControlOutlinedIcon from '@mui/icons-material/PestControlOutlined'; import PestControlIcon from '@mui/icons-material/PestControl'; import Monitor from '../assets/Monitor.png'; +import { isEmpty } from 'lodash'; +import { Link } from 'react-router-dom'; const useStyles = makeStyles(() => ({ card: { @@ -46,7 +48,15 @@ const useStyles = makeStyles(() => ({ fontSize: '1rem', fontWeight: '600', paddingBottom: '0.5rem', - paddingTop: '0.5rem' + paddingTop: '0.5rem', + textOverflow: 'ellipsis' + }, + link: { + color: '#52637A', + fontSize: '1rem', + letterSpacing: '0.009375rem', + paddingRight: '1rem', + textDecorationLine: 'underline' }, monitor: { width: '27.25rem', @@ -154,8 +164,40 @@ const vulnerabilityCheck = (status) => { function VulnerabilitiyCard(props) { const classes = useStyles(); - const { cve } = props; - const [open, setOpen] = React.useState(false); + const { cve, name } = props; + const [open, setOpen] = useState(false); + const [loadingFixed, setLoadingFixed] = useState(true); + const [fixedInfo, setFixedInfo] = useState([]); + + useEffect(() => { + setLoadingFixed(true); + api + .get(`${host()}${endpoints.imageListWithCVEFixed(cve.Id, name)}`) + .then((response) => { + if (response.data && response.data.data) { + const fixedTagsList = response.data.data.ImageListWithCVEFixed?.map((e) => e.Tag); + setFixedInfo(fixedTagsList); + } + setLoadingFixed(false); + }) + .catch((e) => { + console.error(e); + }); + }, []); + + const renderFixedVer = () => { + if (!isEmpty(fixedInfo)) { + return fixedInfo.map((tag, index) => { + return ( + + {tag} + + ); + }); + } else { + return 'Not fixed'; + } + }; return ( @@ -179,6 +221,15 @@ function VulnerabilitiyCard(props) { {cve.Title} + + + Fixed In:{' '} + + + {' '} + {loadingFixed ? 'Loading...' : renderFixedVer()} + + { - return ; + return ; }) ); } else {