feat: Implemented fixed-in, fixed tagdetails page query

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2022-10-10 12:50:09 +03:00
parent 988249588f
commit 9baeb5bba2
8 changed files with 202 additions and 154 deletions

View File

@ -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();

View File

@ -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 <VulnerabilitiesDetails name="mongo" />;
return (
<MemoryRouter>
<VulnerabilitiesDetails name="mongo" />
</MemoryRouter>
);
};
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(<StateVulnerabilitiesWrapper />);
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(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
await waitFor(() => expect(screen.getAllByText('1.0.16')).toHaveLength(20));
});
});

View File

@ -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 = {} }) => {

View File

@ -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 (

View File

@ -175,18 +175,6 @@ function RepoDetails() {
// return list[Math.floor(Math.random() * list.length)];
// }
// const vulnerabilityCheck = () => {
// const noneVulnerability = <Chip label="None Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
// const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
// return(getRandom(arrVulnerability));
// };
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;

View File

@ -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 <Route here (i.e. image name)
const { name } = useParams();
const { name, tag } = useParams();
const classes = useStyles();
// const { description, overviewTitle, dependencies, dependents } = props;
useEffect(() => {
// 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 = <Chip label="No Vulnerability" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const unknownVulnerability = <Chip label="Unknown Vulnerability" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const lowVulnerability = <Chip label="Low Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const mediumVulnerability = <Chip label="Medium Vulnerability" sx={{backgroundColor: "#FFF3E0",color: "#FB8C00",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#FB8C00!important" }} />}/>;
// const highVulnerability = <Chip label="High Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const criticalVulnerability = <Chip label="Critical Vulnerability" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <PestControlIcon sx={{ color: "#E53935!important" }} />}/>;
// const arrVulnerability = [noneVulnerability, unknownVulnerability, lowVulnerability, mediumVulnerability, highVulnerability, criticalVulnerability]
// return(getRandom(arrVulnerability));
// };
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
@ -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 (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Dependecies ({dependencies || '---'})</Typography>
// </CardContent>
// </Card>);
// };
// const renderDependents = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Dependents ({dependents || '---'})</Typography>
// </CardContent>
// </Card>);
// };
// const renderVulnerabilities = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Vulnerabilities</Typography>
// </CardContent>
// </Card>);
// };
return (
<div className={classes.pageWrapper}>
<Card className={classes.cardRoot}>
@ -237,7 +203,7 @@ function TagDetails() {
{name}:
{
// @ts-ignore
repoDetailData?.newestTag
tag
}
</Typography>
{/* {vulnerabilityCheck()}
@ -253,7 +219,7 @@ function TagDetails() {
Digest:{' '}
{
// @ts-ignore
repoDetailData?.latestDigest
imageDetailData?.digest
}
</Typography>
</Grid>
@ -280,13 +246,19 @@ function TagDetails() {
<Grid container>
<Grid item xs={12}>
<TabPanel value="Layers" className={classes.tabPanel}>
<HistoryLayers name={tagName} />
<HistoryLayers
name={fullName}
history={
// @ts-ignore
imageDetailData.history
}
/>
</TabPanel>
<TabPanel value="DependsOn" className={classes.tabPanel}>
<DependsOn name={tagName} />
<DependsOn name={fullName} />
</TabPanel>
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
<IsDependentOn name={tagName} />
<IsDependentOn name={fullName} />
</TabPanel>
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
<VulnerabilitiesDetails name={name} />
@ -299,11 +271,11 @@ function TagDetails() {
<Grid item xs={4} className={classes.metadata}>
<TagDetailsMetadata
// @ts-ignore
platforms={getPlatform()}
platform={getPlatform()}
// @ts-ignore
size={repoDetailData?.size}
size={imageDetailData?.size}
// @ts-ignore
lastUpdated={repoDetailData?.lastUpdated}
lastUpdated={imageDetailData?.lastUpdated}
/>
</Grid>
</Grid>

View File

@ -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
</Typography>
<Typography variant="body1" className={classes.metadataBody}>
{platforms.Os || `----`} / {platforms.Arch || `----`}
{platform?.Os || `----`} / {platform?.Arch || `----`}
</Typography>
</CardContent>
</Card>

View File

@ -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 (
<Link key={index} to={`/image/${encodeURIComponent(name)}/tag/${tag}`} className={classes.link}>
{tag}
</Link>
);
});
} else {
return 'Not fixed';
}
};
return (
<Card className={classes.card} raised>
@ -179,6 +221,15 @@ function VulnerabilitiyCard(props) {
{cve.Title}
</Typography>
</Stack>
<Stack sx={{ flexDirection: 'row' }}>
<Typography variant="body1" align="left" className={classes.title}>
Fixed In:{' '}
</Typography>
<Typography variant="body1" align="left" className={classes.values} noWrap>
{' '}
{loadingFixed ? 'Loading...' : renderFixedVer()}
</Typography>
</Stack>
<Typography
sx={{
color: '#1479FF',
@ -234,7 +285,7 @@ function VulnerabilitiesDetails(props) {
return (
cves &&
cves.map((cve, index) => {
return <VulnerabilitiyCard key={index} cve={cve} />;
return <VulnerabilitiyCard key={index} cve={cve} name={name} />;
})
);
} else {