Compare commits
6 Commits
commit-2b3
...
commit-63f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63ff8dabc0 | ||
|
|
f9cafd0b90 | ||
|
|
089d79087f | ||
|
|
2f94cc30ae | ||
|
|
ddf1d9224b | ||
|
|
7471fb58a8 |
7
.github/workflows/end-to-end-test.yml
vendored
7
.github/workflows/end-to-end-test.yml
vendored
@@ -61,6 +61,13 @@ jobs:
|
||||
sudo mv cosign /usr/local/bin/cosign
|
||||
which cosign
|
||||
cosign version
|
||||
pushd $(mktemp -d)
|
||||
curl -L https://github.com/aquasecurity/trivy/releases/download/v0.38.3/trivy_0.38.3_Linux-64bit.tar.gz -o trivy.tar.gz
|
||||
tar -xzvf trivy.tar.gz
|
||||
sudo mv trivy /usr/local/bin/trivy
|
||||
popd
|
||||
which trivy
|
||||
trivy version
|
||||
cd $GITHUB_WORKSPACE
|
||||
|
||||
- name: Install go
|
||||
|
||||
@@ -11,6 +11,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the explore page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -4,6 +4,7 @@ import Home from 'components/Home/Home';
|
||||
import React from 'react';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import { sortByCriteria } from 'utilities/sortCriteria';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@@ -12,6 +13,14 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedUsedNavigate
|
||||
}));
|
||||
|
||||
const HomeWrapper = () => {
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<Home />
|
||||
</MockThemeProvier>
|
||||
);
|
||||
};
|
||||
|
||||
const mockImageList = {
|
||||
GlobalSearch: {
|
||||
Page: { TotalCount: 6, ItemCount: 3 },
|
||||
@@ -126,7 +135,7 @@ describe('Home component', () => {
|
||||
it('fetches image data and renders popular, bookmarks and recently updated', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<Home />);
|
||||
render(<HomeWrapper />);
|
||||
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
|
||||
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
|
||||
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
|
||||
@@ -135,7 +144,7 @@ describe('Home component', () => {
|
||||
it('renders signature icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<Home />);
|
||||
render(<HomeWrapper />);
|
||||
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2);
|
||||
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3);
|
||||
});
|
||||
@@ -143,7 +152,7 @@ describe('Home component', () => {
|
||||
it('renders vulnerability icons', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<Home />);
|
||||
render(<HomeWrapper />);
|
||||
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2);
|
||||
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2);
|
||||
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
|
||||
@@ -152,14 +161,14 @@ describe('Home component', () => {
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<Home />);
|
||||
render(<HomeWrapper />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(2));
|
||||
});
|
||||
|
||||
it('should redirect to explore page when clicking view all popular', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
|
||||
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
|
||||
render(<Home />);
|
||||
render(<HomeWrapper />);
|
||||
const viewAllButtons = await screen.findAllByText(/view all/i);
|
||||
expect(viewAllButtons).toHaveLength(2);
|
||||
fireEvent.click(viewAllButtons[0]);
|
||||
|
||||
@@ -11,6 +11,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the homepage component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import RepoCard from 'components/Shared/RepoCard';
|
||||
import { createSearchParams } from 'react-router-dom';
|
||||
import MockThemeProvier from '__mocks__/MockThemeProvider';
|
||||
|
||||
// usenavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@@ -24,6 +25,23 @@ const mockImage = {
|
||||
platforms: [{ Os: 'linux', Arch: 'amd64' }]
|
||||
};
|
||||
|
||||
const RepoCardWrapper = (props) => {
|
||||
const { image } = props;
|
||||
return (
|
||||
<MockThemeProvier>
|
||||
<RepoCard
|
||||
name={image.name}
|
||||
version={image.latestVersion}
|
||||
description={image.description}
|
||||
vendor={image.vendor}
|
||||
key={1}
|
||||
lastUpdated={image.lastUpdated}
|
||||
platforms={image.platforms}
|
||||
/>
|
||||
</MockThemeProvier>
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
// restore the spy created with spyOn
|
||||
jest.restoreAllMocks();
|
||||
@@ -31,16 +49,7 @@ afterEach(() => {
|
||||
|
||||
describe('Repo card component', () => {
|
||||
it('navigates to repo page when clicked', async () => {
|
||||
render(
|
||||
<RepoCard
|
||||
name={mockImage.name}
|
||||
version={mockImage.latestVersion}
|
||||
description={mockImage.description}
|
||||
vendor={mockImage.vendor}
|
||||
key={1}
|
||||
lastUpdated={mockImage.lastUpdated}
|
||||
/>
|
||||
);
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
@@ -48,15 +57,7 @@ describe('Repo card component', () => {
|
||||
});
|
||||
|
||||
it('renders placeholders for missing data', async () => {
|
||||
render(
|
||||
<RepoCard
|
||||
name={mockImage.name}
|
||||
version={mockImage.latestVersion}
|
||||
description={mockImage.description}
|
||||
vendor={mockImage.vendor}
|
||||
key={1}
|
||||
/>
|
||||
);
|
||||
render(<RepoCardWrapper image={{ ...mockImage, lastUpdated: '' }} />);
|
||||
const cardTitle = await screen.findByText('alpine');
|
||||
expect(cardTitle).toBeInTheDocument();
|
||||
userEvent.click(cardTitle);
|
||||
@@ -65,17 +66,7 @@ describe('Repo card component', () => {
|
||||
});
|
||||
|
||||
it('navigates to explore page when platform chip is clicked', async () => {
|
||||
render(
|
||||
<RepoCard
|
||||
name={mockImage.name}
|
||||
version={mockImage.latestVersion}
|
||||
description={mockImage.description}
|
||||
vendor={mockImage.vendor}
|
||||
key={1}
|
||||
lastUpdated={mockImage.lastUpdated}
|
||||
platforms={mockImage.platforms}
|
||||
/>
|
||||
);
|
||||
render(<RepoCardWrapper image={mockImage} />);
|
||||
const osChip = await screen.findByText(/linux/i);
|
||||
fireEvent.click(osChip);
|
||||
expect(mockedUsedNavigate).toHaveBeenCalledWith({
|
||||
|
||||
@@ -1,47 +1,42 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import ReferredBy from 'components/Tag/Tabs/ReferredBy';
|
||||
import React from 'react';
|
||||
|
||||
const mockReferrersList = {
|
||||
data: {
|
||||
Referrers: [
|
||||
const mockReferrersList = [
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c',
|
||||
Annotations: [
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:be7a3d01c35a2cf53c502e9dc50cdf36b15d9361c81c63bf319f1d5cbe44ab7c',
|
||||
Annotations: [
|
||||
{
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:d9ad22f41d9cb9797c134401416eee2a70446cee1a8eb76fc6b191f4320dade2',
|
||||
Annotations: [
|
||||
{
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
MediaType: 'application/vnd.oci.artifact.manifest.v1+json',
|
||||
ArtifactType: 'application/vnd.example.icecream.v1',
|
||||
Size: 466,
|
||||
Digest: 'sha256:d9ad22f41d9cb9797c134401416eee2a70446cee1a8eb76fc6b191f4320dade2',
|
||||
Annotations: [
|
||||
{
|
||||
Key: 'demo',
|
||||
Value: 'true'
|
||||
},
|
||||
{
|
||||
Key: 'format',
|
||||
Value: 'oci'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
];
|
||||
|
||||
// useNavigate mock
|
||||
const mockedUsedNavigate = jest.fn();
|
||||
@@ -57,27 +52,17 @@ afterEach(() => {
|
||||
|
||||
describe('Referred by tab', () => {
|
||||
it('should render referrers if there are any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
expect(await screen.findAllByText('Media type: application/vnd.oci.artifact.manifest.v1+json')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("renders no referrers if there aren't any", async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: { Referrers: [] } } });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={[]} />);
|
||||
expect(await screen.findByText(/Nothing found/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should log an error if the request fails', async () => {
|
||||
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
|
||||
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
await waitFor(() => expect(error).toBeCalledTimes(1));
|
||||
});
|
||||
|
||||
it('should display the digest when clicking the dropdowns', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
const firstDigest = (await screen.findAllByText(/digest/i))[0];
|
||||
expect(firstDigest).toBeInTheDocument();
|
||||
await userEvent.click(firstDigest);
|
||||
@@ -91,13 +76,11 @@ describe('Referred by tab', () => {
|
||||
});
|
||||
|
||||
it('should display the annotations when clicking the dropdown', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockReferrersList });
|
||||
render(<ReferredBy repoName="golang" digest="test" />);
|
||||
render(<ReferredBy referrers={mockReferrersList} />);
|
||||
const firstAnnotations = (await screen.findAllByText(/ANNOTATIONS/i))[0];
|
||||
expect(firstAnnotations).toBeInTheDocument();
|
||||
await userEvent.click(firstAnnotations);
|
||||
expect(await screen.findByText(/demo: true/i)).toBeInTheDocument();
|
||||
await userEvent.click(firstAnnotations);
|
||||
expect(await screen.findByText(/demo: true/i)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the tags page component', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { api } from 'api';
|
||||
import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
|
||||
import React from 'react';
|
||||
@@ -432,6 +433,14 @@ const mockCVEList = {
|
||||
}
|
||||
};
|
||||
|
||||
const mockCVEListFiltered = {
|
||||
CVEListForImage: {
|
||||
Tag: '',
|
||||
Page: { ItemCount: 20, TotalCount: 20 },
|
||||
CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022'))
|
||||
}
|
||||
};
|
||||
|
||||
const mockCVEFixed = {
|
||||
pageOne: {
|
||||
ImageListWithCVEFixed: {
|
||||
@@ -488,6 +497,16 @@ describe('Vulnerabilties page', () => {
|
||||
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
|
||||
});
|
||||
|
||||
it('sends filtered query if user types in the search bar', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
|
||||
render(<StateVulnerabilitiesWrapper />);
|
||||
const cveSearchInput = screen.getByPlaceholderText(/search for/i);
|
||||
jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
|
||||
await userEvent.type(cveSearchInput, '2022');
|
||||
expect((await screen.queryAllByText(/2023/i).length) === 0);
|
||||
expect((await screen.findAllByText(/2022/i)).length === 6);
|
||||
});
|
||||
|
||||
it('renders no vulnerabilities if there are not any', async () => {
|
||||
jest.spyOn(api, 'get').mockResolvedValue({
|
||||
status: 200,
|
||||
@@ -506,7 +525,7 @@ describe('Vulnerabilties page', () => {
|
||||
await waitFor(() =>
|
||||
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
|
||||
);
|
||||
fireEvent.click(openText[0]);
|
||||
await fireEvent.click(openText[0]);
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
|
||||
);
|
||||
@@ -533,7 +552,7 @@ describe('Vulnerabilties page', () => {
|
||||
expect(loadMoreBtn).toBeInTheDocument();
|
||||
await fireEvent.click(loadMoreBtn);
|
||||
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
|
||||
await expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||
expect(await screen.findByText('latest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle fixed CVE query errors', async () => {
|
||||
|
||||
13
src/api.js
13
src/api.js
@@ -78,11 +78,16 @@ const endpoints = {
|
||||
detailedRepoInfo: (name) =>
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||
detailedImageInfo: (name, tag) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
|
||||
let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
}}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
|
||||
}}`;
|
||||
if (!isEmpty(searchTerm)) {
|
||||
query += `, searchedCVE: "${searchTerm}"`;
|
||||
}
|
||||
return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
|
||||
},
|
||||
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) =>
|
||||
`/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
|
||||
(pageNumber - 1) * pageSize
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
14
src/assets/zotLogoWhite.svg
Normal file
14
src/assets/zotLogoWhite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 32 KiB |
8
src/assets/zotLogoWhiteSmall.svg
Normal file
8
src/assets/zotLogoWhiteSmall.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 13 KiB |
@@ -60,6 +60,11 @@ const useStyles = makeStyles((theme) => ({
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
filterCardsContainer: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
display: 'none'
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -269,7 +274,7 @@ function Explore({ searchInputValue }) {
|
||||
if (!isLoading && !isEndOfList) {
|
||||
return <div ref={listBottom} />;
|
||||
}
|
||||
return '';
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -311,7 +316,7 @@ function Explore({ searchInputValue }) {
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container item xs={12} spacing={5} pt={1}>
|
||||
<Grid item xs={3} md={3} className="hide-on-mobile">
|
||||
<Grid item xs={3} md={3} className={classes.filterCardsContainer}>
|
||||
<Sticky>{renderFilterCards()}</Sticky>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={9}>
|
||||
|
||||
@@ -19,7 +19,7 @@ const useStyles = makeStyles((theme) => {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '2rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
padding: '1rem'
|
||||
@@ -27,7 +27,8 @@ const useStyles = makeStyles((theme) => {
|
||||
},
|
||||
explore: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontSize: '0.813rem',
|
||||
fontWeight: '600',
|
||||
letterSpacing: '0.009375rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
fontSize: '0.8rem'
|
||||
@@ -49,7 +50,7 @@ function ExploreHeader() {
|
||||
return (
|
||||
<div className={classes.exploreHeader}>
|
||||
<ArrowBackIcon
|
||||
sx={{ color: '#14191F', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
|
||||
sx={{ color: '#52637A', marginRight: '1.75rem', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
|
||||
onClick={() => navigate(-1)}
|
||||
/>
|
||||
<Breadcrumbs separator="/" aria-label="breadcrumb">
|
||||
|
||||
@@ -3,16 +3,17 @@ import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
// components
|
||||
import { AppBar, Toolbar, Stack, Grid } from '@mui/material';
|
||||
import { AppBar, Toolbar, Grid } from '@mui/material';
|
||||
|
||||
// styling
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import logo from '../../assets/zotLogo.svg';
|
||||
import logoxs from '../../assets/zotLogoSmall.png';
|
||||
import logo from '../../assets/zotLogoWhite.svg';
|
||||
import logoxs from '../../assets/zotLogoWhiteSmall.svg';
|
||||
import githubLogo from '../../assets/Git.png';
|
||||
import { useState, useEffect } from 'react';
|
||||
import SearchSuggestion from './SearchSuggestion';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
barOpen: {
|
||||
position: 'sticky',
|
||||
minHeight: '10%'
|
||||
@@ -28,7 +29,7 @@ const useStyles = makeStyles(() => ({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 0,
|
||||
backgroundColor: '#fff',
|
||||
backgroundColor: '#0F2139',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderBottom: '0.0625rem solid #BDBDBD',
|
||||
@@ -58,20 +59,41 @@ const useStyles = makeStyles(() => ({
|
||||
logoWrapper: {},
|
||||
logo: {
|
||||
maxWidth: '130px',
|
||||
maxHeight: '50px'
|
||||
maxHeight: '30px'
|
||||
},
|
||||
userAvatar: {
|
||||
height: 46,
|
||||
width: 46
|
||||
headerLinkContainer: {
|
||||
[theme.breakpoints.down('md')]: {
|
||||
display: 'none'
|
||||
}
|
||||
},
|
||||
link: {
|
||||
color: '#000'
|
||||
color: '#F6F7F9',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600
|
||||
},
|
||||
grid: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '2.875rem',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
justifyContent: 'space-between'
|
||||
}
|
||||
},
|
||||
gridItem: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
signInBtn: {
|
||||
border: '1px solid #F6F7F9',
|
||||
borderRadius: '0.625rem',
|
||||
backgroundColor: 'transparent',
|
||||
color: '#F6F7F9',
|
||||
fontSize: '1rem',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -109,26 +131,45 @@ function Header({ setSearchCurrentValue = () => {} }) {
|
||||
const path = useLocation().pathname;
|
||||
|
||||
return (
|
||||
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '10vh' }}>
|
||||
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '5rem' }}>
|
||||
<Toolbar className={classes.header}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" className={classes.headerContainer}>
|
||||
<Grid container className={classes.grid}>
|
||||
<Grid item xs={2} sx={{ display: 'flex', justifyContent: 'start' }}>
|
||||
<Link to="/home" className={classes.grid}>
|
||||
<Grid container className={classes.grid}>
|
||||
<Grid item container xs={3} md={4} spacing="1.5rem" className={classes.gridItem}>
|
||||
<Grid item>
|
||||
<Link to="/home">
|
||||
<picture>
|
||||
<source media="(min-width:600px)" srcSet={logo} />
|
||||
<img alt="zot" src={logoxs} className={classes.logo} />
|
||||
</picture>
|
||||
</Link>
|
||||
</Grid>
|
||||
<Grid item xs={8}>
|
||||
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
|
||||
<Grid item className={classes.headerLinkContainer}>
|
||||
<a className={classes.link} href="https://zotregistry.io" target="_blank" rel="noreferrer">
|
||||
Product
|
||||
</a>
|
||||
</Grid>
|
||||
<Grid item md={2} xs={0}>
|
||||
<div>{''}</div>
|
||||
<Grid item className={classes.headerLinkContainer}>
|
||||
<a
|
||||
className={classes.link}
|
||||
href="https://zotregistry.io/v1.4.3/general/concepts/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
<Grid item xs={6} md={4} className={classes.gridItem}>
|
||||
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
|
||||
</Grid>
|
||||
<Grid item container xs={2} md={3} spacing="1.5rem" className={`${classes.gridItem}`}>
|
||||
<Grid item className={classes.headerLinkContainer}>
|
||||
<a className={classes.link} href="https://github.com/project-zot/zot" target="_blank" rel="noreferrer">
|
||||
<img alt="github repository" src={githubLogo} className={classes.logo} />
|
||||
</a>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
||||
@@ -14,31 +14,30 @@ import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
const useStyles = makeStyles(() => ({
|
||||
searchContainer: {
|
||||
display: 'inline-block',
|
||||
backgroundColor: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '2.5rem',
|
||||
minWidth: '60%',
|
||||
marginLeft: 16,
|
||||
backgroundColor: '#2B3A4E',
|
||||
boxShadow: '0 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '0.625rem',
|
||||
minWidth: '100%',
|
||||
position: 'relative',
|
||||
zIndex: 1150
|
||||
},
|
||||
searchContainerFocused: {
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
search: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #E7E7E7',
|
||||
borderRadius: '2.5rem',
|
||||
border: '0.063rem solid #8A96A8',
|
||||
borderRadius: '0.625rem',
|
||||
zIndex: 1155
|
||||
},
|
||||
searchFocused: {
|
||||
border: '0.125rem solid #E0E5EB',
|
||||
backgroundColor: '#FFFFF'
|
||||
},
|
||||
searchFailed: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #ff0303',
|
||||
borderRadius: '2.5rem',
|
||||
zIndex: 1155
|
||||
border: '0.125rem solid #ff0303'
|
||||
},
|
||||
resultsWrapper: {
|
||||
margin: '0',
|
||||
@@ -47,16 +46,19 @@ const useStyles = makeStyles(() => ({
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
backgroundColor: '#2B3A4E',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderBottomLeftRadius: '2.5rem',
|
||||
borderBottomRightRadius: '2.5rem',
|
||||
borderBottomLeftRadius: '0.625rem',
|
||||
borderBottomRightRadius: '0.625rem',
|
||||
// border: '0.125rem solid #E7E7E7',
|
||||
borderTop: 0,
|
||||
width: '100%',
|
||||
overflowY: 'auto',
|
||||
zIndex: 1
|
||||
},
|
||||
resultsWrapperFocused: {
|
||||
backgroundColor: '#FFFFFF'
|
||||
},
|
||||
resultsWrapperHidden: {
|
||||
display: 'none'
|
||||
},
|
||||
@@ -66,9 +68,19 @@ const useStyles = makeStyles(() => ({
|
||||
cursor: 'pointer'
|
||||
},
|
||||
input: {
|
||||
color: '#464141',
|
||||
marginLeft: 1,
|
||||
width: '90%'
|
||||
width: '90%',
|
||||
paddingLeft: 10,
|
||||
height: '40px',
|
||||
fontSize: '1rem',
|
||||
backgroundColor: '#2B3A4E',
|
||||
borderRadius: '0.625rem',
|
||||
color: '#8A96A8'
|
||||
},
|
||||
inputFocused: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: '0.625rem',
|
||||
color: 'rgba(0, 0, 0, 0.6);'
|
||||
},
|
||||
searchItem: {
|
||||
alignItems: 'center',
|
||||
@@ -102,6 +114,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
const search = queryParams.get('search') || '';
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isFailedSearch, setIsFailedSearch] = useState(false);
|
||||
const [isComponentFocused, setIsComponentFocused] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
|
||||
@@ -217,15 +230,18 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
getComboboxProps,
|
||||
isOpen,
|
||||
openMenu
|
||||
// closeMenu
|
||||
} = useCombobox({
|
||||
items: suggestionData,
|
||||
onInputValueChange: handleSeachChange,
|
||||
onSelectedItemChange: handleSuggestionSelected,
|
||||
initialInputValue: !isEmpty(searchQuery) ? searchQuery : search,
|
||||
itemToString: (item) => item.name ?? item
|
||||
itemToString: (item) => item?.name || item
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setIsComponentFocused(isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
const renderSuggestions = () => {
|
||||
return suggestionData.map((suggestion, index) => (
|
||||
<ListItem
|
||||
@@ -253,9 +269,11 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.searchContainer}>
|
||||
<div className={`${classes.searchContainer} ${isComponentFocused && classes.searchContainerFocused}`}>
|
||||
<Stack
|
||||
className={isFailedSearch && !isLoading ? classes.searchFailed : classes.search}
|
||||
className={`${classes.search} ${isComponentFocused && classes.searchFocused} ${
|
||||
isFailedSearch && !isLoading && classes.searchFailed
|
||||
}`}
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
@@ -263,9 +281,8 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
{...getComboboxProps()}
|
||||
>
|
||||
<InputBase
|
||||
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
|
||||
placeholder={'Search for content...'}
|
||||
className={classes.input}
|
||||
className={`${classes.input} ${isComponentFocused && classes.inputFocused}`}
|
||||
onKeyUp={handleSearch}
|
||||
onFocus={() => openMenu()}
|
||||
{...getInputProps()}
|
||||
@@ -276,7 +293,11 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
|
||||
</Stack>
|
||||
<List
|
||||
{...getMenuProps()}
|
||||
className={isOpen && !isLoading && !isFailedSearch ? classes.resultsWrapper : classes.resultsWrapperHidden}
|
||||
className={
|
||||
isOpen && !isLoading && !isFailedSearch
|
||||
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
|
||||
: classes.resultsWrapperHidden
|
||||
}
|
||||
>
|
||||
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
|
||||
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (
|
||||
|
||||
@@ -40,8 +40,13 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
sectionTitle: {
|
||||
fontWeight: '700',
|
||||
color: '#000000DE',
|
||||
width: '100%'
|
||||
color: '#0F2139',
|
||||
width: '100%',
|
||||
fontSize: '2rem',
|
||||
textAlign: 'center',
|
||||
lineHeight: '2.375rem',
|
||||
letterSpacing: '-0.01rem',
|
||||
marginLeft: '0.5rem'
|
||||
},
|
||||
subtitle: {
|
||||
color: '#00000099',
|
||||
@@ -53,9 +58,12 @@ const useStyles = makeStyles(() => ({
|
||||
width: '65%'
|
||||
},
|
||||
viewAll: {
|
||||
color: '#00000099',
|
||||
color: '#52637A',
|
||||
fontWeight: '600',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left'
|
||||
marginRight: '0.5rem'
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -191,7 +199,7 @@ function Home() {
|
||||
{isLoading ? (
|
||||
<Loading />
|
||||
) : (
|
||||
<Stack spacing={4} alignItems="center" className={classes.gridWrapper}>
|
||||
<Stack alignItems="center" className={classes.gridWrapper}>
|
||||
<Stack
|
||||
justifyContent="space-between"
|
||||
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
|
||||
@@ -203,8 +211,10 @@ function Home() {
|
||||
Most popular images
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={classes.viewAll} onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
|
||||
<Typography variant="body2">View all</Typography>
|
||||
<div onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
|
||||
<Typography variant="body2" className={classes.viewAll}>
|
||||
View all
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
{renderMostPopular()}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// react global
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../../api';
|
||||
@@ -154,7 +154,7 @@ const randomImage = () => {
|
||||
function RepoDetails() {
|
||||
const [repoDetailData, setRepoDetailData] = useState({});
|
||||
const [tags, setTags] = useState([]);
|
||||
|
||||
const placeholderImage = useRef(randomImage());
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedTab, setSelectedTab] = useState('Overview');
|
||||
// get url param from <Route here (i.e. image name)
|
||||
@@ -261,7 +261,7 @@ function RepoDetails() {
|
||||
image={
|
||||
!isEmpty(repoDetailData?.logo)
|
||||
? `data:image/png;base64, ${repoDetailData?.logo}`
|
||||
: randomImage()
|
||||
: placeholderImage.current
|
||||
}
|
||||
alt="icon"
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
// react global
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useNavigate, createSearchParams } from 'react-router-dom';
|
||||
|
||||
// utility
|
||||
import { DateTime } from 'luxon';
|
||||
// components
|
||||
import { Card, CardActionArea, CardMedia, CardContent, Typography, Stack, Chip, Grid, Tooltip } from '@mui/material';
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardMedia,
|
||||
CardContent,
|
||||
Typography,
|
||||
Stack,
|
||||
Chip,
|
||||
Grid,
|
||||
Tooltip,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
|
||||
// placeholder images
|
||||
@@ -17,6 +28,7 @@ import repocube4 from '../../assets/repocube-4.png';
|
||||
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
||||
import { isEmpty, uniq } from 'lodash';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
// temporary utility to get image
|
||||
const randomIntFromInterval = (min, max) => {
|
||||
@@ -30,22 +42,22 @@ const randomImage = () => {
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
marginBottom: 2,
|
||||
marginTop: '1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderColor: '#FFFFFF',
|
||||
borderRadius: '1.5rem',
|
||||
borderRadius: '0.75rem',
|
||||
boxShadow: '0rem 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%',
|
||||
maxWidth: '72rem',
|
||||
'&:hover': {
|
||||
boxShadow: '0rem 1.1875rem 1.4375rem rgba(131, 131, 131, 0.19)',
|
||||
borderRadius: '1.5rem'
|
||||
borderRadius: '0.75rem'
|
||||
}
|
||||
},
|
||||
avatar: {
|
||||
@@ -56,7 +68,7 @@ const useStyles = makeStyles(() => ({
|
||||
cardBtn: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
borderRadius: '1.5rem',
|
||||
borderRadius: '0.75rem',
|
||||
borderColor: '#FFFFFF',
|
||||
'&:hover $focusHighlight': {
|
||||
opacity: 0
|
||||
@@ -71,6 +83,7 @@ const useStyles = makeStyles(() => ({
|
||||
color: '#606060',
|
||||
maxHeight: '9.25rem',
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: '1.188rem 1rem',
|
||||
'&:hover': {
|
||||
backgroundColor: '#FFFFFF'
|
||||
}
|
||||
@@ -78,6 +91,20 @@ const useStyles = makeStyles(() => ({
|
||||
contentRight: {
|
||||
height: '100%'
|
||||
},
|
||||
contentRightLabel: {
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
color: '#52637A',
|
||||
textAlign: 'end'
|
||||
},
|
||||
contentRightValue: {
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
fontWeight: '600',
|
||||
color: '#14191F',
|
||||
textAlign: 'end',
|
||||
marginLeft: '0.5rem'
|
||||
},
|
||||
signedBadge: {
|
||||
color: '#9ccc65',
|
||||
height: '1.375rem',
|
||||
@@ -86,24 +113,54 @@ const useStyles = makeStyles(() => ({
|
||||
},
|
||||
vendor: {
|
||||
color: '#14191F',
|
||||
fontSize: '1rem',
|
||||
fontSize: '0.75rem',
|
||||
maxWidth: '50%',
|
||||
textOverflow: 'ellipsis'
|
||||
textOverflow: 'ellipsis',
|
||||
lineHeight: '1.125rem'
|
||||
},
|
||||
description: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.5rem',
|
||||
textOverflow: 'ellipsis',
|
||||
marginBottom: 0,
|
||||
paddingTop: '1rem'
|
||||
},
|
||||
versionLast: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: '1.125rem',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
cardTitle: {
|
||||
textOverflow: 'ellipsis',
|
||||
maxWidth: '70%'
|
||||
maxWidth: '70%',
|
||||
fontWeight: '600',
|
||||
color: '#0F2139',
|
||||
lineHeight: '2rem'
|
||||
},
|
||||
platformChips: {
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.813rem',
|
||||
lineHeight: '0.813rem',
|
||||
borderRadius: '0.375rem',
|
||||
padding: '0.313rem 0.625rem'
|
||||
},
|
||||
chipLabel: {
|
||||
padding: '0'
|
||||
}
|
||||
}));
|
||||
|
||||
function RepoCard(props) {
|
||||
const classes = useStyles();
|
||||
const navigate = useNavigate();
|
||||
const placeholderImage = useRef(randomImage());
|
||||
// dynamically check device size with mui media query hook
|
||||
const theme = useTheme();
|
||||
const isXsSize = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const MAX_PLATFORM_CHIPS = isXsSize ? 3 : 6;
|
||||
|
||||
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, logo, version, vulnerabilityData } =
|
||||
props;
|
||||
|
||||
@@ -119,16 +176,18 @@ function RepoCard(props) {
|
||||
};
|
||||
|
||||
const platformChips = () => {
|
||||
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);
|
||||
return uniq(filteredPlatforms).map((platform, index) => (
|
||||
const filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
|
||||
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
|
||||
const displayedPlatforms = filteredPlatforms.slice(0, MAX_PLATFORM_CHIPS + 1);
|
||||
if (hiddenChips > 0) displayedPlatforms.push(`+${hiddenChips} more`);
|
||||
return displayedPlatforms.map((platform, index) => (
|
||||
<Chip
|
||||
key={`${name}${platform}${index}`}
|
||||
label={platform}
|
||||
onClick={handlePlatformChipClick}
|
||||
sx={{
|
||||
backgroundColor: '#E0E5EB',
|
||||
color: '#52637A',
|
||||
fontSize: '0.625rem'
|
||||
className={classes.platformChips}
|
||||
classes={{
|
||||
label: classes.chipLabel
|
||||
}}
|
||||
/>
|
||||
));
|
||||
@@ -166,7 +225,7 @@ function RepoCard(props) {
|
||||
img: classes.avatar
|
||||
}}
|
||||
component="img"
|
||||
image={!isEmpty(logo) ? `data:image/png;base64, ${logo}` : randomImage()}
|
||||
image={!isEmpty(logo) ? `data:image/png;base64, ${logo}` : placeholderImage.current}
|
||||
alt="icon"
|
||||
/>
|
||||
<Tooltip title={name} placement="top">
|
||||
@@ -182,14 +241,14 @@ function RepoCard(props) {
|
||||
</div>
|
||||
</Stack>
|
||||
<Tooltip title={description || 'Description not available'} placement="top">
|
||||
<Typography className={classes.versionLast} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
|
||||
<Typography className={classes.description} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
|
||||
{description || 'Description not available'}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Stack alignItems="center" direction="row" spacing={1} pt={1}>
|
||||
{platformChips()}
|
||||
</Stack>
|
||||
<Stack alignItems="center" direction="row" spacing={1} pt={2}>
|
||||
<Stack alignItems="center" direction="row" spacing={1} pt={'0.5rem'}>
|
||||
<Tooltip title={getVendor()} placement="top" className="hide-on-mobile">
|
||||
<Typography className={classes.vendor} variant="body2" noWrap>
|
||||
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
|
||||
@@ -207,19 +266,25 @@ function RepoCard(props) {
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={2} md={2} className="hide-on-mobile">
|
||||
<Stack
|
||||
alignItems="flex-end"
|
||||
justifyContent="space-between"
|
||||
direction="column"
|
||||
className={classes.contentRight}
|
||||
>
|
||||
<Stack direction="column" alignItems="flex-end">
|
||||
<Typography variant="body2">Downloads • {!isNaN(downloads) ? downloads : `not available`}</Typography>
|
||||
{/* <Typography variant="body2">Rating • {rating || '-'}</Typography> */}
|
||||
</Stack>
|
||||
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
|
||||
</Stack>
|
||||
<Grid item xs={2} md={2} className={`hide-on-mobile ${classes.contentRight}`}>
|
||||
<Grid container item justifyContent="flex-end" textAlign="end">
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
|
||||
Downloads •
|
||||
</Typography>
|
||||
<Typography variant="body2" component="span" className={classes.contentRightValue}>
|
||||
{!isNaN(downloads) ? downloads : `not available`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12}>
|
||||
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
|
||||
Rating •
|
||||
</Typography>
|
||||
<Typography variant="body2" component="span" className={classes.contentRightValue}>
|
||||
#1
|
||||
</Typography>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { makeStyles } from '@mui/styles';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Divider, Typography, Stack } from '@mui/material';
|
||||
import ReferrerCard from '../../Shared/ReferrerCard';
|
||||
import Loading from '../../Shared/Loading';
|
||||
import { api, endpoints } from 'api';
|
||||
import { host } from '../../../host';
|
||||
import { mapReferrer } from 'utilities/objectModels';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
@@ -17,36 +15,24 @@ const useStyles = makeStyles(() => ({
|
||||
}));
|
||||
|
||||
function ReferredBy(props) {
|
||||
const { repoName, digest } = props;
|
||||
const [referrers, setReferrers] = useState([]);
|
||||
const { referrers } = props;
|
||||
const [referrersData, setReferrersData] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const classes = useStyles();
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(`${host()}${endpoints.referrers({ repo: repoName, digest })}`, abortController.signal)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let referrersData = response.data.data.Referrers?.map((referrer) => mapReferrer(referrer));
|
||||
if (!isEmpty(referrersData)) {
|
||||
setReferrers(referrersData);
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setIsLoading(false);
|
||||
});
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
if (!isEmpty(referrers)) {
|
||||
const mappedReferrersData = referrers.map((referrer) => mapReferrer(referrer));
|
||||
setReferrersData(mappedReferrersData);
|
||||
} else {
|
||||
setReferrersData([]);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const renderReferrers = () => {
|
||||
return !isEmpty(referrers) ? (
|
||||
referrers.map((referrer, index) => {
|
||||
return !isEmpty(referrersData) ? (
|
||||
referrersData.map((referrer, index) => {
|
||||
return (
|
||||
<ReferrerCard
|
||||
key={index}
|
||||
@@ -63,16 +49,6 @@ function ReferredBy(props) {
|
||||
);
|
||||
};
|
||||
|
||||
const renderListBottom = () => {
|
||||
if (isLoading) {
|
||||
return <Loading />;
|
||||
}
|
||||
if (!isLoading) {
|
||||
return <div />;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="referred-by-container">
|
||||
<Typography
|
||||
@@ -95,8 +71,7 @@ function ReferredBy(props) {
|
||||
/>
|
||||
<Stack direction="column" spacing={2}>
|
||||
<Stack direction="column" spacing={2}>
|
||||
{renderReferrers()}
|
||||
{renderListBottom()}
|
||||
{isLoading ? <Loading /> : renderReferrers()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
@@ -5,16 +5,17 @@ import { api, endpoints } from '../../../api';
|
||||
|
||||
// components
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import { Box, Card, CardContent, Divider, Stack, Typography } from '@mui/material';
|
||||
import { Box, Card, CardContent, Divider, Stack, Typography, InputBase } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../../../host';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { debounce, isEmpty } from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Loading from '../../Shared/Loading';
|
||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
|
||||
import { mapCVEInfo } from 'utilities/objectModels';
|
||||
import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
@@ -81,6 +82,25 @@ const useStyles = makeStyles(() => ({
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'center'
|
||||
},
|
||||
search: {
|
||||
position: 'relative',
|
||||
minWidth: '100%',
|
||||
flexDirection: 'row',
|
||||
marginBottom: '1.7rem',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
border: '0.125rem solid #E7E7E7',
|
||||
borderRadius: '1rem',
|
||||
zIndex: 1155
|
||||
},
|
||||
searchIcon: {
|
||||
color: '#52637A',
|
||||
paddingRight: '3%'
|
||||
},
|
||||
input: {
|
||||
color: '#464141',
|
||||
marginLeft: 1,
|
||||
width: '90%'
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -236,27 +256,27 @@ function VulnerabilitiesDetails(props) {
|
||||
const { name, tag } = props;
|
||||
|
||||
// pagination props
|
||||
const [cveFilter, setCveFilter] = useState('');
|
||||
const [pageNumber, setPageNumber] = useState(1);
|
||||
const [isEndOfList, setIsEndOfList] = useState(false);
|
||||
const listBottom = useRef(null);
|
||||
|
||||
const getPaginatedCVEs = () => {
|
||||
setIsLoading(true);
|
||||
api
|
||||
.get(
|
||||
`${host()}${endpoints.vulnerabilitiesForRepo(`${name}:${tag}`, { pageNumber, pageSize: EXPLORE_PAGE_SIZE })}`,
|
||||
`${host()}${endpoints.vulnerabilitiesForRepo(
|
||||
`${name}:${tag}`,
|
||||
{ pageNumber, pageSize: EXPLORE_PAGE_SIZE },
|
||||
cveFilter
|
||||
)}`,
|
||||
abortController.signal
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let cveInfo = response.data.data.CVEListForImage?.CVEList;
|
||||
let cveListData = mapCVEInfo(cveInfo);
|
||||
const newCVEList = [...cveData, ...cveListData];
|
||||
setCveData(newCVEList);
|
||||
setIsEndOfList(
|
||||
response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE ||
|
||||
newCVEList.length >= response.data.data.CVEListForImage?.Page?.TotalCount
|
||||
);
|
||||
setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
|
||||
setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE);
|
||||
} else if (response.data.errors) {
|
||||
setIsEndOfList(true);
|
||||
}
|
||||
@@ -270,11 +290,25 @@ function VulnerabilitiesDetails(props) {
|
||||
});
|
||||
};
|
||||
|
||||
const resetPagination = () => {
|
||||
setIsLoading(true);
|
||||
setIsEndOfList(false);
|
||||
if (pageNumber !== 1) {
|
||||
setPageNumber(1);
|
||||
} else {
|
||||
getPaginatedCVEs();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCveFilterChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setCveFilter(value);
|
||||
};
|
||||
|
||||
const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
|
||||
|
||||
useEffect(() => {
|
||||
getPaginatedCVEs();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [pageNumber]);
|
||||
|
||||
// setup intersection obeserver for infinite scroll
|
||||
@@ -302,6 +336,18 @@ function VulnerabilitiesDetails(props) {
|
||||
};
|
||||
}, [isLoading, isEndOfList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
resetPagination();
|
||||
}, [cveFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortController.abort();
|
||||
debouncedChangeHandler.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderCVEs = () => {
|
||||
return !isEmpty(cveData) ? (
|
||||
cveData.map((cve, index) => {
|
||||
@@ -347,6 +393,17 @@ function VulnerabilitiesDetails(props) {
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
<Stack className={classes.search} direction="row" alignItems="center" justifyContent="space-between" spacing={2}>
|
||||
<InputBase
|
||||
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
|
||||
placeholder={'Search for Vulnerabilities...'}
|
||||
className={classes.input}
|
||||
onChange={debouncedChangeHandler}
|
||||
/>
|
||||
<div className={classes.searchIcon}>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack direction="column" spacing={2}>
|
||||
<Stack direction="column" spacing={2}>
|
||||
{renderCVEs()}
|
||||
|
||||
@@ -217,6 +217,7 @@ function TagDetails() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedTab, setSelectedTab] = useState('Layers');
|
||||
const [selectedPullTab, setSelectedPullTab] = useState('');
|
||||
const placeholderImage = useRef(randomImage());
|
||||
const abortController = useMemo(() => new AbortController(), []);
|
||||
const mounted = useRef(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -331,7 +332,7 @@ function TagDetails() {
|
||||
image={
|
||||
!isEmpty(imageDetailData?.logo)
|
||||
? `data:image/ png;base64, ${imageDetailData?.logo}`
|
||||
: randomImage()
|
||||
: placeholderImage.current
|
||||
}
|
||||
alt="icon"
|
||||
/>
|
||||
@@ -580,7 +581,7 @@ function TagDetails() {
|
||||
<VulnerabilitiesDetails name={reponame} tag={tag} />
|
||||
</TabPanel>
|
||||
<TabPanel value="ReferredBy" className={classes.tabPanel}>
|
||||
<ReferredBy repoName={reponame} digest={selectedManifest?.digest} />
|
||||
<ReferredBy referrers={imageDetailData.referrers} />
|
||||
</TabPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -44,6 +44,7 @@ const mapToImage = (responseImage) => {
|
||||
repoName: responseImage.RepoName,
|
||||
tag: responseImage.Tag,
|
||||
manifests: responseImage.Manifests?.map((manifest) => mapToManifest(manifest)) || [],
|
||||
referrers: responseImage.Referrers,
|
||||
size: responseImage.Size,
|
||||
downloadCount: responseImage.DownloadCount,
|
||||
lastUpdated: responseImage.LastUpdated,
|
||||
|
||||
@@ -60,8 +60,19 @@ def pull_modify_push_image(logger, registry, image_name, tag, cosign_password,
|
||||
metafile = '{}_{}_metadata.json'.format(image_name, tag)
|
||||
metafile = os.path.join(meta_dir_name, metafile)
|
||||
|
||||
cmd = [image_update_script_path, "-r", registry, "-i", image_name, "-t", tag, "-c", cosign_password,
|
||||
"-f", metafile, "-m", multiarch, "-u", username, "-p", password, "--data-dir", data_dir]
|
||||
cmd = [image_update_script_path, "-r", registry, "-i", image_name, "-t", tag, "-f", metafile]
|
||||
|
||||
if data_dir:
|
||||
cmd.extend(["--data-dir", data_dir])
|
||||
|
||||
if username:
|
||||
cmd.extend(["-u", username, "-p", password])
|
||||
|
||||
if cosign_password:
|
||||
cmd.extend(["-c", cosign_password])
|
||||
|
||||
if multiarch:
|
||||
cmd.extend(["-m", multiarch])
|
||||
|
||||
if debug:
|
||||
cmd.append("-d")
|
||||
@@ -74,10 +85,41 @@ def pull_modify_push_image(logger, registry, image_name, tag, cosign_password,
|
||||
|
||||
with open(metafile) as f:
|
||||
image_metadata = json.load(f)
|
||||
image_metadata[image_name][tag]["multiarch"] = multiarch
|
||||
logger.debug("raw image metadata")
|
||||
logger.debug(image_metadata)
|
||||
image_metadata["multiarch"] = multiarch
|
||||
image_metadata["cves"] = getCVEInfo(image_metadata.pop("trivy"))
|
||||
|
||||
logger.debug("processed image metadata")
|
||||
logger.debug(image_metadata)
|
||||
return image_metadata
|
||||
|
||||
def getCVEInfo(trivy_results):
|
||||
cve_dict = {}
|
||||
|
||||
for result in trivy_results:
|
||||
for vulnerability in result.get("Vulnerabilities", []):
|
||||
cve_id = vulnerability["VulnerabilityID"]
|
||||
|
||||
package = {
|
||||
"PackageName": vulnerability.get("PkgName"),
|
||||
"InstalledVersion": vulnerability.get("InstalledVersion"),
|
||||
"FixedVersion": vulnerability.get("FixedVersion", "Not Specified")
|
||||
}
|
||||
|
||||
if cve_dict.get(cve_id):
|
||||
cve_dict[cve_id]["PackageList"].append(package)
|
||||
else:
|
||||
cve_dict[cve_id] = {
|
||||
"ID": cve_id,
|
||||
"Title": vulnerability.get("Title"),
|
||||
"Description": vulnerability.get("Description"),
|
||||
"Severity": vulnerability.get("Severity"),
|
||||
"PackageList": [package]
|
||||
}
|
||||
|
||||
return cve_dict
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
@@ -126,7 +168,7 @@ def main():
|
||||
image_metadata = pull_modify_push_image(logger, registry, image_name, tag, cosign_password, multiarch, username, password, debug, data_dir)
|
||||
|
||||
metadata.setdefault(image_name, {})
|
||||
metadata[image_name][tag] = image_metadata[image_name][tag]
|
||||
metadata[image_name][tag] = image_metadata
|
||||
|
||||
with open(metadata_file, "w") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
@@ -11,27 +11,91 @@ username=""
|
||||
debug=0
|
||||
data_dir=$(pwd)
|
||||
|
||||
options=$(getopt -o dr:i:t:u:p:c:m:f: -l debug,registry:,image:,tag:,username:,password:,cosign-password:,multiarch:,file:,data-dir: -- "$@")
|
||||
if [ $? -ne 0 ]; then
|
||||
usage $0
|
||||
exit 0
|
||||
fi
|
||||
|
||||
eval set -- "$options"
|
||||
while :; do
|
||||
case "$1" in
|
||||
-r|--registry) registry=$2; shift 2;;
|
||||
-i|--image) image=$2; shift 2;;
|
||||
-t|--tag) tag=$2; shift 2;;
|
||||
-u|--username) username=$2; shift 2;;
|
||||
-p|--password) username=$2; shift 2;;
|
||||
-c|--cosign-password) cosign_password=$2; shift 2;;
|
||||
-m|--multiarch) multiarch=$2; shift 2;;
|
||||
-f|--file) metafile=$2; shift 2;;
|
||||
--data-dir) data_dir=$2; shift 2;;
|
||||
-d|--debug) debug=1; shift 1;;
|
||||
--) shift 1; break;;
|
||||
*) usage $0; exit 1;;
|
||||
while (( "$#" )); do
|
||||
case $1 in
|
||||
-r|--registry)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option registry requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
registry=$2;
|
||||
shift 2
|
||||
;;
|
||||
-i|--image)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option image requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
image=$2
|
||||
shift 2
|
||||
;;
|
||||
-t|--tag)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option tag requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
tag=$2
|
||||
shift 2
|
||||
;;
|
||||
-u|--username)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option username requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
username=$2
|
||||
shift 2
|
||||
;;
|
||||
-p|--password)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option password requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
password=$2
|
||||
shift 2
|
||||
;;
|
||||
-c|--cosign-password)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option cosign-password requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
cosign_password=$2
|
||||
shift 2
|
||||
;;
|
||||
-m|--multiarch)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option multiarch requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
multiarch=$2
|
||||
shift 2
|
||||
;;
|
||||
-f|--file)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option metafile requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
metafile=$2
|
||||
shift 2
|
||||
;;
|
||||
--data-dir)
|
||||
if [ -z "$2" ]; then
|
||||
echo "Option data-dir requires an argument"
|
||||
exit 1
|
||||
fi
|
||||
data_dir=$2
|
||||
shift 2
|
||||
;;
|
||||
-d|--debug)
|
||||
debug=1
|
||||
shift 1
|
||||
;;
|
||||
--)
|
||||
shift 1
|
||||
break
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -61,6 +125,11 @@ function verify_prerequisites {
|
||||
return 1
|
||||
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
|
||||
@@ -90,12 +159,13 @@ license="$(cat ${docker_docs_dir}/${image}/license.md)"
|
||||
vendor="$(cat ${docker_docs_dir}/${image}/maintainer.md)"
|
||||
logo=$(base64 -w 0 ${docker_docs_dir}/${image}/logo.png)
|
||||
echo ${repo}
|
||||
sed -i "s|%%GITHUB-REPO%%|${repo}|g" ${docker_docs_dir}/${image}/maintainer.md
|
||||
sed -i "s|%%IMAGE%%|${image}|g" ${docker_docs_dir}/${image}/content.md
|
||||
sed -i.bak "s|%%GITHUB-REPO%%|${repo}|g" ${docker_docs_dir}/${image}/maintainer.md; rm ${docker_docs_dir}/${image}/maintainer.md.bak
|
||||
sed -i.bak "s|%%IMAGE%%|${image}|g" ${docker_docs_dir}/${image}/content.md; rm ${docker_docs_dir}/${image}/content.md.bak
|
||||
doc=$(cat ${docker_docs_dir}/${image}/content.md)
|
||||
|
||||
local_image_ref_skopeo=oci:${images_dir}:${image}-${tag}
|
||||
local_image_ref_regtl=ocidir://${images_dir}:${image}-${tag}
|
||||
local_image_ref_trivy=${images_dir}:${image}-${tag}
|
||||
remote_src_image_ref=docker://${image}:${tag}
|
||||
remote_dest_image_ref=${registry}/${image}:${tag}
|
||||
|
||||
@@ -145,13 +215,24 @@ if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trivy_out_file=trivy-${image}-${tag}.json
|
||||
if [ ! -z "${multiarch}" ]; then
|
||||
trivy image --scanners vuln --format json --input ${local_image_ref_trivy} -o ${trivy_out_file}
|
||||
jq -n --argfile trivy_file ${trivy_out_file} '.trivy=$trivy_file.Results' > ${trivy_out_file}.tmp
|
||||
mv ${trivy_out_file}.tmp ${trivy_out_file}
|
||||
else
|
||||
echo '{"trivy":[]}' > ${trivy_out_file}
|
||||
fi
|
||||
|
||||
# Sign new updated image
|
||||
COSIGN_PASSWORD=${cosign_password} cosign sign ${remote_dest_image_ref} --key ${cosign_key_path} --allow-insecure-registry
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
details=$(jq -n \
|
||||
details_file=details-${image}-${tag}.json
|
||||
|
||||
jq -n \
|
||||
--arg org.opencontainers.image.title "${image}" \
|
||||
--arg org.opencontainers.image.description " $description" \
|
||||
--arg org.opencontainers.image.url "${repo}" \
|
||||
@@ -159,7 +240,7 @@ details=$(jq -n \
|
||||
--arg org.opencontainers.image.licenses "${license}" \
|
||||
--arg org.opencontainers.image.vendor "${vendor}" \
|
||||
--arg org.opencontainers.image.documentation "${description}" \
|
||||
'$ARGS.named'
|
||||
)
|
||||
'$ARGS.named' > ${details_file}
|
||||
|
||||
jq -n --arg image "${image}" --arg tag "${tag}" --argjson details "${details}" '.[$image][$tag]=$details' > ${metafile}
|
||||
jq -c -s add ${details_file} ${trivy_out_file} > ${metafile}
|
||||
rm ${details_file} ${trivy_out_file}
|
||||
|
||||
Reference in New Issue
Block a user