5 Commits

Author SHA1 Message Date
70a870a616 fix: fixed layer history not updating for multiarch images
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-05-04 09:15:38 +03:00
c09a12facc test(test-data): add layers information to the image metadata json (#347)
* test(test-data): add layers information to the image metadata json

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

* fix(tests): fix username userd as password, fix prerequisite validation

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

* fix(tests): auto-confirm cosign upload to private registry

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>

---------

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-04-27 18:06:52 +03:00
415973e23c build(go): upgrade go version used in end-to-end tests to 1.20 (#346)
Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2023-04-27 14:08:59 +03:00
cb2d8795f5 patch: followup dependency cleanup
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-27 10:38:34 +03:00
ac9d023272 patch: updated vulnerability design, styling cleanup
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
2023-04-27 10:03:28 +03:00
15 changed files with 10764 additions and 9974 deletions

View File

@ -73,7 +73,7 @@ jobs:
- name: Install go - name: Install go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: 1.19.x go-version: 1.20.x
- name: Checkout zot repo - name: Checkout zot repo
uses: actions/checkout@v3 uses: actions/checkout@v3

19789
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -69,7 +69,7 @@ const mockedTagsData = [
describe('Tags component', () => { describe('Tags component', () => {
it('should open and close details dropdown for tags', async () => { it('should open and close details dropdown for tags', async () => {
render(<TagsThemeWrapper />); render(<TagsThemeWrapper />);
const openBtn = screen.getAllByText(/digest/i); const openBtn = screen.getAllByText(/show/i);
fireEvent.click(openBtn[0]); fireEvent.click(openBtn[0]);
expect(screen.getByText(/OS\/ARCH/i)).toBeInTheDocument(); expect(screen.getByText(/OS\/ARCH/i)).toBeInTheDocument();
fireEvent.click(openBtn[0]); fireEvent.click(openBtn[0]);
@ -87,7 +87,7 @@ describe('Tags component', () => {
it('should navigate to specific manifest when clicking the digest', async () => { it('should navigate to specific manifest when clicking the digest', async () => {
render(<TagsThemeWrapper />); render(<TagsThemeWrapper />);
const openBtn = screen.getAllByText(/digest/i); const openBtn = screen.getAllByText(/show/i);
await fireEvent.click(openBtn[0]); await fireEvent.click(openBtn[0]);
const tagLink = await screen.findByText(/sha256:adca4/i); const tagLink = await screen.findByText(/sha256:adca4/i);
fireEvent.click(tagLink); fireEvent.click(tagLink);

View File

@ -1,5 +1,6 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import MockThemeProvier from '__mocks__/MockThemeProvider';
import { api } from 'api'; import { api } from 'api';
import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails'; import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
import React from 'react'; import React from 'react';
@ -7,9 +8,11 @@ import { MemoryRouter } from 'react-router-dom';
const StateVulnerabilitiesWrapper = () => { const StateVulnerabilitiesWrapper = () => {
return ( return (
<MemoryRouter> <MockThemeProvier>
<VulnerabilitiesDetails name="mongo" /> <MemoryRouter>
</MemoryRouter> <VulnerabilitiesDetails name="mongo" />
</MemoryRouter>
</MockThemeProvier>
); );
}; };
@ -500,7 +503,7 @@ describe('Vulnerabilties page', () => {
it('sends filtered query if user types in the search bar', async () => { it('sends filtered query if user types in the search bar', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
const cveSearchInput = screen.getByPlaceholderText(/search for/i); const cveSearchInput = screen.getByPlaceholderText(/search/i);
jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } }); jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
await userEvent.type(cveSearchInput, '2022'); await userEvent.type(cveSearchInput, '2022');
expect((await screen.queryAllByText(/2023/i).length) === 0); expect((await screen.queryAllByText(/2023/i).length) === 0);

View File

@ -77,6 +77,9 @@ const useStyles = makeStyles((theme) => ({
width: '100%', width: '100%',
boxShadow: 'none!important' boxShadow: 'none!important'
}, },
tagsContent: {
padding: '1.5rem'
},
platformText: { platformText: {
backgroundColor: '#EDE7F6', backgroundColor: '#EDE7F6',
color: '#220052', color: '#220052',
@ -290,7 +293,7 @@ function RepoDetails() {
</Grid> </Grid>
<Grid item xs={12} md={8} className={classes.tags}> <Grid item xs={12} md={8} className={classes.tags}>
<Card className={classes.cardRoot}> <Card className={classes.cardRoot}>
<CardContent> <CardContent className={classes.tagsContent}>
<Tags tags={tags} /> <Tags tags={tags} />
</CardContent> </CardContent>
</Card> </Card>

View File

@ -3,33 +3,13 @@ import React, { useState } from 'react';
// components // components
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Card, CardContent, Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material'; import { Stack, InputBase, FormControl, Select, InputLabel, MenuItem } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
import { makeStyles } from '@mui/styles'; import { makeStyles } from '@mui/styles';
import TagCard from '../../Shared/TagCard'; import TagCard from '../../Shared/TagCard';
import { tagsSortByCriteria } from 'utilities/sortCriteria'; import { tagsSortByCriteria } from 'utilities/sortCriteria';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
tagContainer: {
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%'
},
content: {
textAlign: 'left',
color: '#606060',
padding: '2% 3% 2% 3%',
width: '100%'
},
clickCursor: { clickCursor: {
cursor: 'pointer' cursor: 'pointer'
}, },
@ -39,8 +19,6 @@ const useStyles = makeStyles(() => ({
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: '1rem',
marginBottom: '1rem',
boxShadow: 'none', boxShadow: 'none',
border: '0.063rem solid #E7E7E7', border: '0.063rem solid #E7E7E7',
borderRadius: '0.625rem' borderRadius: '0.625rem'
@ -49,11 +27,14 @@ const useStyles = makeStyles(() => ({
color: '#52637A', color: '#52637A',
paddingRight: '3%' paddingRight: '3%'
}, },
searchInputBase: {
width: '90%',
paddingLeft: '1.5rem',
height: 40
},
input: { input: {
color: '#464141', color: '#464141',
fontSize: '1rem', fontSize: '1rem',
paddingLeft: '1rem',
width: '90%',
'&::placeholder': { '&::placeholder': {
opacity: '1' opacity: '1'
} }
@ -99,51 +80,45 @@ export default function Tags(props) {
}; };
return ( return (
<Card className={classes.tagContainer} data-testid="tags-container"> <Stack direction="column" spacing="1rem">
<CardContent className={classes.content}> <Stack direction="row" justifyContent="space-between">
<Stack direction="row" justifyContent="space-between"> <Typography
<Typography variant="h4"
variant="h4" gutterBottom
gutterBottom component="div"
component="div" align="left"
align="left" style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600' }}
style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600' }} >
Tags History
</Typography>
<FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small">
<InputLabel>Sort</InputLabel>
<Select
label="Sort"
value={sortFilter}
onChange={handleTagsSortChange}
MenuProps={{ disableScrollLock: true }}
> >
Tags History {Object.values(tagsSortByCriteria).map((el) => (
</Typography> <MenuItem key={el.value} value={el.value}>
<div> {el.label}
<FormControl sx={{ m: '1', minWidth: '4.6875rem' }} className={classes.sortForm} size="small"> </MenuItem>
<InputLabel>Sort</InputLabel> ))}
<Select </Select>
label="Sort" </FormControl>
value={sortFilter} </Stack>
onChange={handleTagsSortChange} <Stack className={classes.search}>
MenuProps={{ disableScrollLock: true }} <InputBase
> placeholder={'Search tags...'}
{Object.values(tagsSortByCriteria).map((el) => ( classes={{ root: classes.searchInputBase, input: classes.input }}
<MenuItem key={el.value} value={el.value}> value={tagsFilter}
{el.label} onChange={handleTagsFilterChange}
</MenuItem> />
))} <div className={classes.searchIcon}>
</Select> <SearchIcon />
</FormControl> </div>
</div> </Stack>
</Stack> {renderTags(tags)}
<Stack className={classes.search}> </Stack>
<InputBase
style={{ paddingLeft: 10, height: 40, color: 'rgba(0, 0, 0, 0.6)' }}
placeholder={'Search tags...'}
// className={classes.input}
classes={{ input: classes.input }}
value={tagsFilter}
onChange={handleTagsFilterChange}
/>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
</Stack>
{renderTags(tags)}
</CardContent>
</Card>
); );
} }

View File

@ -32,6 +32,13 @@ const useStyles = makeStyles((theme) => ({
clickCursor: { clickCursor: {
cursor: 'pointer' cursor: 'pointer'
}, },
dropdownToggle: {
color: '#1479FF',
paddingTop: '1rem',
fontSize: '0.8125rem',
fontWeight: '600',
cursor: 'pointer'
},
dropdown: { dropdown: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
@ -117,17 +124,7 @@ export default function TagCard(props) {
) : ( ) : (
<KeyboardArrowDown className={classes.dropdownText} /> <KeyboardArrowDown className={classes.dropdownText} />
)} )}
<Typography <Typography className={classes.dropdownToggle}>{!open ? `Show more` : `Show less`}</Typography>
sx={{
color: '#1479FF',
paddingTop: '1rem',
fontSize: '0.8125rem',
fontWeight: '600',
cursor: 'pointer'
}}
>
DIGEST
</Typography>
</Stack> </Stack>
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<Box className={classes.manifsetsTable}> <Box className={classes.manifsetsTable}>

View File

@ -0,0 +1,213 @@
import React, { useEffect, useMemo, useState } from 'react';
// utility
import { api, endpoints } from '../../api';
// components
import Collapse from '@mui/material/Collapse';
import { Box, Card, CardContent, Stack, Typography, Divider } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { host } from '../../host';
import { isEmpty } from 'lodash';
import { Link } from 'react-router-dom';
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { CVE_FIXEDIN_PAGE_SIZE } from 'utilities/paginationConstants';
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)',
border: '1px solid #E0E5EB',
borderRadius: '0.75rem',
flex: 'none',
alignSelf: 'stretch',
width: '100%',
marginTop: '2rem',
marginBottom: '2rem'
},
content: {
textAlign: 'left',
color: '#606060',
padding: '2% 3% 2% 3%',
width: '100%'
},
cveId: {
color: theme.palette.primary.main,
fontSize: '1rem',
fontWeight: 400,
textDecoration: 'underline'
},
cveSummary: {
color: theme.palette.secondary.dark,
fontSize: '0.75rem',
fontWeight: '600',
textOverflow: 'ellipsis',
marginTop: '0.5rem'
},
link: {
color: '#52637A',
fontSize: '1rem',
letterSpacing: '0.009375rem',
paddingRight: '1rem',
textDecorationLine: 'underline'
},
dropdown: {
flexDirection: 'row',
alignItems: 'center'
},
dropdownText: {
color: '#1479FF',
fontSize: '0.75rem',
fontWeight: '600',
cursor: 'pointer',
textAlign: 'center'
},
vulnerabilityCardDivider: {
margin: '1rem 0'
}
}));
function VulnerabilitiyCard(props) {
const classes = useStyles();
const { cve, name } = props;
const [openDesc, setOpenDesc] = useState(false);
const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true);
const [fixedInfo, setFixedInfo] = useState([]);
const abortController = useMemo(() => new AbortController(), []);
// pagination props
const [pageNumber, setPageNumber] = useState(1);
const [isEndOfList, setIsEndOfList] = useState(false);
const getPaginatedResults = () => {
if (!openFixed || isEndOfList) {
return;
}
setLoadingFixed(true);
api
.get(
`${host()}${endpoints.imageListWithCVEFixed(cve.id, name, { pageNumber, pageSize: CVE_FIXEDIN_PAGE_SIZE })}`,
abortController.signal
)
.then((response) => {
if (response.data && response.data.data) {
const fixedTagsList = response.data.data.ImageListWithCVEFixed?.Results?.map((e) => e.Tag);
setFixedInfo((previousState) => [...previousState, ...fixedTagsList]);
setIsEndOfList(
[...fixedInfo, ...fixedTagsList].length >= response.data.data.ImageListWithCVEFixed?.Page?.TotalCount
);
}
setLoadingFixed(false);
})
.catch((e) => {
console.error(e);
setIsEndOfList(true);
setLoadingFixed(false);
});
};
useEffect(() => {
getPaginatedResults();
return () => {
abortController.abort();
};
}, [openFixed, pageNumber]);
const loadMore = () => {
if (loadingFixed || isEndOfList) return;
setPageNumber((pageNumber) => pageNumber + 1);
};
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';
}
};
const renderLoadMore = () => {
return (
!isEndOfList && (
<Typography
sx={{
color: '#3366CC',
cursor: 'pointer',
fontSize: '1rem',
letterSpacing: '0.009375rem',
paddingRight: '1rem',
textDecorationLine: 'underline'
}}
onClick={loadMore}
component="div"
>
Load more
</Typography>
)
);
};
return (
<Card className={classes.card} raised>
<CardContent className={classes.content}>
<Stack direction="row" spacing="1.25rem">
<Typography variant="body1" align="left" className={classes.cveId}>
{cve.id}
</Typography>
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
</Stack>
<Typography variant="body1" align="left" className={classes.cveSummary}>
{cve.title}
</Typography>
<Divider className={classes.vulnerabilityCardDivider} />
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
{!openFixed ? (
<KeyboardArrowRight className={classes.dropdownText} />
) : (
<KeyboardArrowDown className={classes.dropdownText} />
)}
<Typography className={classes.dropdownText}>Fixed in</Typography>
</Stack>
<Collapse in={openFixed} timeout="auto" unmountOnExit>
<Box sx={{ width: '100%', padding: '0.5rem 0' }}>
{loadingFixed ? (
'Loading...'
) : (
<Stack direction="row" sx={{ flexWrap: 'wrap' }}>
{renderFixedVer()}
{renderLoadMore()}
</Stack>
)}
</Box>
</Collapse>
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
{!openDesc ? (
<KeyboardArrowRight className={classes.dropdownText} />
) : (
<KeyboardArrowDown className={classes.dropdownText} />
)}
<Typography className={classes.dropdownText}>Description</Typography>
</Stack>
<Collapse in={openDesc} timeout="auto" unmountOnExit>
<Box sx={{ padding: '0.5rem 0' }}>
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
{cve.description}
</Typography>
</Box>
</Collapse>
</CardContent>
</Card>
);
}
export default VulnerabilitiyCard;

View File

@ -14,52 +14,11 @@ import { mapToImage } from 'utilities/objectModels';
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants'; import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
card: {
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',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start'
},
content: {
textAlign: 'left',
color: '#606060',
padding: '2% 3% 2% 3%',
width: '100%'
},
title: { title: {
color: '#828282', marginBottom: '1.7rem',
fontSize: '1rem', color: 'rgba(0, 0, 0, 0.87)',
paddingRight: '0.5rem', fontSize: '1.5rem',
paddingBottom: '0.5rem', fontWeight: '600'
paddingTop: '0.5rem'
},
values: {
color: '#000000',
fontSize: '1rem',
fontWeight: '600',
paddingBottom: '0.5rem',
paddingTop: '0.5rem'
},
link: {
color: '#52637A',
fontSize: '1rem',
letterSpacing: '0.009375rem',
paddingRight: '1rem',
textDecorationLine: 'underline'
},
monitor: {
width: '27.25rem',
height: '24.625rem',
paddingTop: '2rem'
}, },
none: { none: {
color: '#52637A', color: '#52637A',
@ -172,18 +131,7 @@ function DependsOn(props) {
return ( return (
<div data-testid="depends-on-container"> <div data-testid="depends-on-container">
<Typography <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
variant="h4"
gutterBottom
component="div"
align="left"
style={{
marginBottom: '1.7rem',
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '1.5rem',
fontWeight: '600'
}}
>
Uses Uses
</Typography> </Typography>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>

View File

@ -7,61 +7,11 @@ import makeStyles from '@mui/styles/makeStyles';
import Loading from '../../Shared/Loading'; import Loading from '../../Shared/Loading';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
card: {
display: 'flex',
flexDirection: 'column',
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: '0rem',
marginBottom: '0rem',
padding: '1rem 1.5rem '
},
content: {
textAlign: 'left',
color: '#606060',
width: '100%',
flexDirection: 'column'
},
title: { title: {
color: '#14191F', marginBottom: '1.7rem',
fontSize: '1rem', color: 'rgba(0, 0, 0, 0.87)',
fontWeight: '400', fontSize: '1.5rem',
paddingRight: '0.5rem', fontWeight: '600'
paddingBottom: '0.5rem',
paddingTop: '0.5rem'
},
layer: {
color: '#14191F',
fontSize: '1rem',
fontWeight: '400',
paddingRight: '0.5rem',
paddingBottom: '0.5rem',
paddingTop: '0.5rem',
width: '100%',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
cursor: 'pointer'
},
values: {
color: '#52637A',
fontSize: '1rem',
fontWeight: '400',
paddingBottom: '0.5rem',
paddingTop: '0.5rem',
textAlign: 'right'
},
monitor: {
width: '27.25rem',
height: '24.625rem',
paddingTop: '2rem'
}, },
none: { none: {
color: '#52637A', color: '#52637A',
@ -83,22 +33,11 @@ function HistoryLayers(props) {
return () => { return () => {
abortController.abort(); abortController.abort();
}; };
}, [name]); }, [name, history]);
return ( return (
<> <>
<Typography <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
variant="h4"
gutterBottom
component="div"
align="left"
style={{
marginBottom: '1.7rem',
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '1.5rem',
fontWeight: '600'
}}
>
Layers Layers
</Typography> </Typography>
{isLoading ? ( {isLoading ? (

View File

@ -14,52 +14,11 @@ import { mapToImage } from 'utilities/objectModels';
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants'; import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
card: {
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',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start'
},
content: {
textAlign: 'left',
color: '#606060',
padding: '2% 3% 2% 3%',
width: '100%'
},
title: { title: {
color: '#828282', marginBottom: '1.7rem',
fontSize: '1rem', color: 'rgba(0, 0, 0, 0.87)',
paddingRight: '0.5rem', fontSize: '1.5rem',
paddingBottom: '0.5rem', fontWeight: '600'
paddingTop: '0.5rem'
},
values: {
color: '#000000',
fontSize: '1rem',
fontWeight: '600',
paddingBottom: '0.5rem',
paddingTop: '0.5rem'
},
link: {
color: '#52637A',
fontSize: '1rem',
letterSpacing: '0.009375rem',
paddingRight: '1rem',
textDecorationLine: 'underline'
},
monitor: {
width: '27.25rem',
height: '24.625rem',
paddingTop: '2rem'
}, },
none: { none: {
color: '#52637A', color: '#52637A',
@ -172,18 +131,7 @@ function IsDependentOn(props) {
return ( return (
<div data-testid="dependents-container"> <div data-testid="dependents-container">
<Typography <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
variant="h4"
gutterBottom
component="div"
align="left"
style={{
marginBottom: '1.7rem',
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '1.5rem',
fontWeight: '600'
}}
>
Used by Used by
</Typography> </Typography>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>

View File

@ -1,12 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { makeStyles } from '@mui/styles'; import { makeStyles } from '@mui/styles';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { Divider, Typography, Stack } from '@mui/material'; import { Typography, Stack } from '@mui/material';
import ReferrerCard from '../../Shared/ReferrerCard'; import ReferrerCard from '../../Shared/ReferrerCard';
import Loading from '../../Shared/Loading'; import Loading from '../../Shared/Loading';
import { mapReferrer } from 'utilities/objectModels'; import { mapReferrer } from 'utilities/objectModels';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
title: {
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '1.5rem',
fontWeight: '600',
paddingTop: '0.5rem'
},
none: { none: {
color: '#52637A', color: '#52637A',
fontSize: '1.4rem', fontSize: '1.4rem',
@ -51,24 +57,9 @@ function ReferredBy(props) {
return ( return (
<div data-testid="referred-by-container"> <div data-testid="referred-by-container">
<Typography <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
variant="h4"
gutterBottom
component="div"
align="left"
style={{
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '1.5rem',
fontWeight: '600',
paddingTop: '0.5rem'
}}
>
Referred By Referred By
</Typography> </Typography>
<Divider
variant="fullWidth"
sx={{ margin: '5% 0% 5% 0%', background: 'rgba(0, 0, 0, 0.38)', height: '0.00625rem', width: '100%' }}
/>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
<Stack direction="column" spacing={2}> <Stack direction="column" spacing={2}>
{isLoading ? <Loading /> : renderReferrers()} {isLoading ? <Loading /> : renderReferrers()}

View File

@ -4,250 +4,70 @@ import React, { useEffect, useMemo, useState, useRef } from 'react';
import { api, endpoints } from '../../../api'; import { api, endpoints } from '../../../api';
// components // components
import Collapse from '@mui/material/Collapse'; import { Stack, Typography, InputBase } from '@mui/material';
import { Box, Card, CardContent, Divider, Stack, Typography, InputBase } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles'; import makeStyles from '@mui/styles/makeStyles';
import { host } from '../../../host'; import { host } from '../../../host';
import { debounce, isEmpty } from 'lodash'; import { debounce, isEmpty } from 'lodash';
import { Link } from 'react-router-dom';
import Loading from '../../Shared/Loading'; import Loading from '../../Shared/Loading';
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapCVEInfo } from 'utilities/objectModels'; import { mapCVEInfo } from 'utilities/objectModels';
import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants'; import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
import SearchIcon from '@mui/icons-material/Search'; import SearchIcon from '@mui/icons-material/Search';
const useStyles = makeStyles(() => ({ import VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
card: {
display: 'flex', const useStyles = makeStyles((theme) => ({
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: { title: {
color: '#828282', color: theme.palette.primary.main,
fontSize: '1rem', fontSize: '1.5rem',
paddingRight: '0.5rem',
paddingBottom: '0.5rem',
paddingTop: '0.5rem'
},
values: {
color: '#000000',
fontSize: '1rem',
fontWeight: '600', fontWeight: '600',
paddingBottom: '0.5rem', marginBottom: '0'
paddingTop: '0.5rem',
textOverflow: 'ellipsis'
}, },
link: { cveId: {
color: '#52637A', color: theme.palette.primary.main,
fontSize: '1rem', fontSize: '1rem',
letterSpacing: '0.009375rem', fontWeight: 400,
paddingRight: '1rem', textDecoration: 'underline'
textDecorationLine: 'underline'
}, },
monitor: { cveSummary: {
width: '27.25rem', color: theme.palette.secondary.dark,
height: '24.625rem', fontSize: '0.75rem',
paddingTop: '2rem' fontWeight: '600',
textOverflow: 'ellipsis',
marginTop: '0.5rem'
}, },
none: { none: {
color: '#52637A', color: '#52637A',
fontSize: '1.4rem', fontSize: '1.4rem',
fontWeight: '600' fontWeight: '600'
}, },
dropdown: {
flexDirection: 'row',
alignItems: 'center'
},
dropdownText: {
color: '#1479FF',
paddingTop: '1rem',
fontSize: '1rem',
fontWeight: '600',
cursor: 'pointer',
textAlign: 'center'
},
search: { search: {
position: 'relative', position: 'relative',
minWidth: '100%', maxWidth: '100%',
flexDirection: 'row', flexDirection: 'row',
marginBottom: '1.7rem', alignItems: 'center',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)', justifyContent: 'space-between',
border: '0.125rem solid #E7E7E7', boxShadow: 'none',
borderRadius: '1rem', border: '0.063rem solid #E7E7E7',
zIndex: 1155 borderRadius: '0.625rem'
}, },
searchIcon: { searchIcon: {
color: '#52637A', color: '#52637A',
paddingRight: '3%' paddingRight: '3%'
}, },
searchInputBase: {
width: '90%',
paddingLeft: '1.5rem',
height: 40,
color: 'rgba(0, 0, 0, 0.6)'
},
input: { input: {
color: '#464141', color: '#464141',
marginLeft: 1, '&::placeholder': {
width: '90%' opacity: '1'
}
} }
})); }));
function VulnerabilitiyCard(props) {
const classes = useStyles();
const { cve, name } = props;
const [openDesc, setOpenDesc] = useState(false);
const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true);
const [fixedInfo, setFixedInfo] = useState([]);
const abortController = useMemo(() => new AbortController(), []);
// pagination props
const [pageNumber, setPageNumber] = useState(1);
const [isEndOfList, setIsEndOfList] = useState(false);
const getPaginatedResults = () => {
if (!openFixed || isEndOfList) {
return;
}
setLoadingFixed(true);
api
.get(
`${host()}${endpoints.imageListWithCVEFixed(cve.id, name, { pageNumber, pageSize: CVE_FIXEDIN_PAGE_SIZE })}`,
abortController.signal
)
.then((response) => {
if (response.data && response.data.data) {
const fixedTagsList = response.data.data.ImageListWithCVEFixed?.Results?.map((e) => e.Tag);
setFixedInfo((previousState) => [...previousState, ...fixedTagsList]);
setIsEndOfList(
[...fixedInfo, ...fixedTagsList].length >= response.data.data.ImageListWithCVEFixed?.Page?.TotalCount
);
}
setLoadingFixed(false);
})
.catch((e) => {
console.error(e);
setIsEndOfList(true);
setLoadingFixed(false);
});
};
useEffect(() => {
getPaginatedResults();
return () => {
abortController.abort();
};
}, [openFixed, pageNumber]);
const loadMore = () => {
if (loadingFixed || isEndOfList) return;
setPageNumber((pageNumber) => pageNumber + 1);
};
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';
}
};
const renderLoadMore = () => {
return (
!isEndOfList && (
<Typography
sx={{
color: '#3366CC',
cursor: 'pointer',
fontSize: '1rem',
letterSpacing: '0.009375rem',
paddingRight: '1rem',
textDecorationLine: 'underline'
}}
onClick={loadMore}
component="div"
>
Load more
</Typography>
)
);
};
return (
<Card className={classes.card} raised>
<CardContent className={classes.content}>
<Stack sx={{ flexDirection: 'row' }}>
<Typography variant="body1" align="left" className={classes.values}>
{' '}
{cve.id}
</Typography>
</Stack>
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
<Stack sx={{ flexDirection: 'row' }}>
<Typography variant="body1" align="left" className={classes.values}>
{' '}
{cve.title}
</Typography>
</Stack>
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
{!openFixed ? (
<KeyboardArrowRight className={classes.dropdownText} />
) : (
<KeyboardArrowDown className={classes.dropdownText} />
)}
<Typography className={classes.dropdownText}>Fixed in</Typography>
</Stack>
<Collapse in={openFixed} timeout="auto" unmountOnExit>
<Box sx={{ width: '100%' }}>
{loadingFixed ? (
'Loading...'
) : (
<Stack direction="row" sx={{ flexWrap: 'wrap' }}>
{renderFixedVer()}
{renderLoadMore()}
</Stack>
)}
</Box>
</Collapse>
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
{!openDesc ? (
<KeyboardArrowRight className={classes.dropdownText} />
) : (
<KeyboardArrowDown className={classes.dropdownText} />
)}
<Typography className={classes.dropdownText}>Description</Typography>
</Stack>
<Collapse in={openDesc} timeout="auto" unmountOnExit>
<Box>
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}>
{' '}
{cve.description}{' '}
</Typography>
</Box>
</Collapse>
</CardContent>
</Card>
);
}
function VulnerabilitiesDetails(props) { function VulnerabilitiesDetails(props) {
const classes = useStyles(); const classes = useStyles();
const [cveData, setCveData] = useState([]); const [cveData, setCveData] = useState([]);
@ -369,48 +189,23 @@ function VulnerabilitiesDetails(props) {
}; };
return ( return (
<div data-testid="vulnerability-container"> <Stack direction="column" spacing="1rem" data-testid="vulnerability-container">
<Typography <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
variant="h4"
gutterBottom
component="div"
align="left"
style={{
color: 'rgba(0, 0, 0, 0.87)',
fontSize: '1.5rem',
fontWeight: '600',
paddingTop: '0.5rem'
}}
>
Vulnerabilities Vulnerabilities
</Typography> </Typography>
<Divider <Stack className={classes.search}>
variant="fullWidth"
sx={{
margin: '5% 0% 5% 0%',
background: 'rgba(0, 0, 0, 0.38)',
height: '0.00625rem',
width: '100%'
}}
/>
<Stack className={classes.search} direction="row" alignItems="center" justifyContent="space-between" spacing={2}>
<InputBase <InputBase
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }} placeholder={'Search'}
placeholder={'Search for Vulnerabilities...'} classes={{ root: classes.searchInputBase, input: classes.input }}
className={classes.input}
onChange={debouncedChangeHandler} onChange={debouncedChangeHandler}
/> />
<div className={classes.searchIcon}> <div className={classes.searchIcon}>
<SearchIcon /> <SearchIcon />
</div> </div>
</Stack> </Stack>
<Stack direction="column" spacing={2}> {renderCVEs()}
<Stack direction="column" spacing={2}> {renderListBottom()}
{renderCVEs()} </Stack>
{renderListBottom()}
</Stack>
</Stack>
</div>
); );
} }

View File

@ -109,6 +109,9 @@ const useStyles = makeStyles((theme) => ({
cardContent: { cardContent: {
paddingBottom: '1rem' paddingBottom: '1rem'
}, },
tabCardContent: {
padding: '1.5rem'
},
cardRoot: { cardRoot: {
boxShadow: 'none!important', boxShadow: 'none!important',
borderRadius: '0.75rem' borderRadius: '0.75rem'
@ -314,7 +317,7 @@ function TagDetails() {
</Grid> </Grid>
<Grid item xs={12} md={8}> <Grid item xs={12} md={8}>
<Card className={classes.cardRoot}> <Card className={classes.cardRoot}>
<CardContent className={classes.cardContent}>{renderTabContent()}</CardContent> <CardContent className={classes.tabCardContent}>{renderTabContent()}</CardContent>
</Card> </Card>
</Grid> </Grid>
<Grid item xs={12} md={4} className={classes.metadata}> <Grid item xs={12} md={4} className={classes.metadata}>

View File

@ -7,7 +7,7 @@ cosign_password=""
metafile="" metafile=""
multiarch="" multiarch=""
username="" username=""
username="" password=""
debug=0 debug=0
data_dir=$(pwd) data_dir=$(pwd)
@ -110,28 +110,33 @@ cosign_key_path=${data_dir}/cosign.key
function verify_prerequisites { function verify_prerequisites {
mkdir -p ${data_dir} mkdir -p ${data_dir}
if [ ! command -v regctl ] &>/dev/null; then command -v regctl
echo "you need to install regctl as a prerequisite" >&3 if [ $? -ne 0 ]; then
echo "you need to install regctl as a prerequisite"
exit 1
fi
command -v skopeo
if [ $? -ne 0 ]; then
echo "you need to install skopeo as a prerequisite"
exit 1
fi
command -v cosign
if [ $? -ne 0 ]; then
echo "you need to install cosign as a prerequisite"
return 1 return 1
fi fi
if [ ! command -v skopeo ] &>/dev/null; then command -v trivy
echo "you need to install skopeo as a prerequisite" >&3 if [ $? -ne 0 ]; then
echo "you need to install trivy as a prerequisite"
return 1 return 1
fi fi
if [ ! command -v cosign ] &>/dev/null; then command -v jq
echo "you need to install cosign as a prerequisite" >&3 if [ $? -ne 0 ]; then
return 1 echo "you need to install jq as a prerequisite"
fi
if [ ! command -v trivy ] &>/dev/null; then
echo "you need to install trivy as a prerequisite" >&3
return 1
fi
if [ ! command -v jq ] &>/dev/null; then
echo "you need to install jq as a prerequisite" >&3
return 1 return 1
fi fi
@ -197,7 +202,7 @@ regctl image mod --replace --annotation org.opencontainers.image.documentation="
credentials_args="" credentials_args=""
if [ ! -z "${username}" ]; then if [ ! -z "${username}" ]; then
credentials_args="--dest-creds ${username}:${username}" credentials_args="--dest-creds ${username}:${password}"
fi fi
# Upload image to target registry # Upload image to target registry
@ -224,8 +229,27 @@ else
echo '{"trivy":[]}' > ${trivy_out_file} echo '{"trivy":[]}' > ${trivy_out_file}
fi fi
layers_file=manifests-${image}-${tag}.json
rm -f ${layers_file}
if [ -z "${multiarch}" ]; then
regctl manifest --format raw-body get ${local_image_ref_regtl} | jq '{ manifests: { default: { layers: [ .layers[].digest ] } } }' > ${layers_file}
else
manifests=$(regctl manifest --format raw-body get ${local_image_ref_regtl} | jq '[ .manifests[] | { "digest":.digest, "platform":(.platform | [ .os, .architecture, .variant ] | map(select(.!=null)) | join("/") )} ] ')
echo $manifests | jq -c '.[]' | while read i; do
digest=$(echo $i | jq -r '.digest')
platform=$(echo $i | jq -r '.platform')
regctl manifest --format raw-body get ocidir://${images_dir}@${digest} | jq --arg platform "${platform}" '{ manifests: { ($platform): { layers: [ .layers[].digest ] } } }' >> layers-${image}-${tag}-${digest//:/_}.json
done
jq -n '{ manifests: [ inputs.manifests ] | add }' layers-${image}-${tag}*.json > ${layers_file}
rm -f layers-${image}-${tag}*.json
fi
# Sign new updated image # Sign new updated image
COSIGN_PASSWORD=${cosign_password} cosign sign ${remote_dest_image_ref} --key ${cosign_key_path} --allow-insecure-registry COSIGN_PASSWORD=${cosign_password} cosign sign ${remote_dest_image_ref} --key ${cosign_key_path} --allow-insecure-registry --yes
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
exit 1 exit 1
fi fi
@ -242,5 +266,5 @@ jq -n \
--arg org.opencontainers.image.documentation "${description}" \ --arg org.opencontainers.image.documentation "${description}" \
'$ARGS.named' > ${details_file} '$ARGS.named' > ${details_file}
jq -c -s add ${details_file} ${trivy_out_file} > ${metafile} jq -c -s add ${details_file} ${trivy_out_file} ${layers_file} > ${metafile}
rm ${details_file} ${trivy_out_file} rm ${details_file} ${trivy_out_file} ${layers_file}