Compare commits
5 Commits
commit-6a2
...
commit-70a
Author | SHA1 | Date | |
---|---|---|---|
70a870a616 | |||
c09a12facc | |||
415973e23c | |||
cb2d8795f5 | |||
ac9d023272 |
2
.github/workflows/end-to-end-test.yml
vendored
2
.github/workflows/end-to-end-test.yml
vendored
@ -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
19789
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}>
|
||||||
|
213
src/components/Shared/VulnerabilityCard.jsx
Normal file
213
src/components/Shared/VulnerabilityCard.jsx
Normal 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;
|
@ -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}>
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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}>
|
||||||
|
@ -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()}
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}>
|
||||||
|
@ -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}
|
||||||
|
Reference in New Issue
Block a user