From e965241b8526d49ee24cabc0fc22690f2257e94d Mon Sep 17 00:00:00 2001 From: Raul-Cristian Kele <raulkeleblk@gmail.com> Date: Fri, 13 Oct 2023 12:09:16 +0300 Subject: [PATCH] patch: signature display redesign Signed-off-by: Raul-Cristian Kele <raulkeleblk@gmail.com> Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com> --- src/__tests__/Explore/Explore.test.js | 85 ++++++++- src/__tests__/HomePage/Home.test.js | 45 ++++- src/components/Explore/Explore.jsx | 1 - src/components/Home/Home.jsx | 1 - src/components/Repo/RepoDetails.jsx | 49 +++-- src/components/Shared/PreviewCard.jsx | 5 +- src/components/Shared/RepoCard.jsx | 30 +++- src/components/Shared/SignatureTooltip.jsx | 20 +-- src/components/Tag/Tabs/DependsOn.jsx | 2 +- src/components/Tag/Tabs/IsDependentOn.jsx | 2 +- src/components/Tag/TagDetails.jsx | 30 +++- src/utilities/filterConstants.js | 7 +- src/utilities/objectModels.js | 4 +- .../vulnerabilityAndSignatureCheck.jsx | 49 +++-- .../vulnerabilityAndSignatureComponents.jsx | 169 +++++++++++++++--- tests/utils/test-data-parser.js | 2 +- 16 files changed, 406 insertions(+), 95 deletions(-) diff --git a/src/__tests__/Explore/Explore.test.js b/src/__tests__/Explore/Explore.test.js index b19c54b9..98e5b464 100644 --- a/src/__tests__/Explore/Explore.test.js +++ b/src/__tests__/Explore/Explore.test.js @@ -38,7 +38,7 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: 'w', - IsSigned: false, + SignatureInfo: [], Licenses: '', Vendor: '', Labels: '', @@ -63,7 +63,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: false, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: false, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -88,7 +99,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -113,7 +135,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -138,7 +171,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -167,7 +211,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -192,7 +247,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -225,7 +291,7 @@ const filteredMockImageListWindows = () => { }; const filteredMockImageListSigned = () => { - const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) => r.NewestImage.IsSigned); + const filteredRepos = mockImageList.GlobalSearch.Repos.filter((r) => r.NewestImage.SignatureInfo?.length > 0); return { GlobalSearch: { Page: { TotalCount: 6, ItemCount: 6 }, @@ -273,7 +339,8 @@ describe('Explore component', () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageList } }); render(<StateExploreWrapper />); expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(1); - expect(await screen.findAllByTestId('verified-icon')).toHaveLength(6); + expect(await screen.findAllByTestId('untrusted-icon')).toHaveLength(2); + expect(await screen.findAllByTestId('verified-icon')).toHaveLength(10); }); it('renders vulnerability icons', async () => { diff --git a/src/__tests__/HomePage/Home.test.js b/src/__tests__/HomePage/Home.test.js index 0ef55a52..3a5073f0 100644 --- a/src/__tests__/HomePage/Home.test.js +++ b/src/__tests__/HomePage/Home.test.js @@ -32,7 +32,7 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: 'w', - IsSigned: false, + SignatureInfo: [], Licenses: '', Vendor: '', Labels: '', @@ -49,7 +49,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -66,7 +77,18 @@ const mockImageList = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -91,7 +113,7 @@ const mockImageListRecent = { NewestImage: { Tag: 'latest', Description: 'w', - IsSigned: false, + SignatureInfo: [], Licenses: '', Vendor: '', Labels: '', @@ -108,7 +130,18 @@ const mockImageListRecent = { NewestImage: { Tag: 'latest', Description: '', - IsSigned: true, + SignatureInfo: [ + { + Tool: 'cosign', + IsTrusted: true, + Author: '' + }, + { + Tool: 'notation', + IsTrusted: true, + Author: '' + } + ], Licenses: '', Vendor: '', Labels: '', @@ -230,7 +263,7 @@ describe('Home component', () => { jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImageListRecent } }); render(<HomeWrapper />); expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(4); - expect(await screen.findAllByTestId('verified-icon')).toHaveLength(5); + expect(await screen.findAllByTestId('verified-icon')).toHaveLength(10); }); it('renders vulnerability icons', async () => { diff --git a/src/components/Explore/Explore.jsx b/src/components/Explore/Explore.jsx index 8ccfaf5f..ccaf3931 100644 --- a/src/components/Explore/Explore.jsx +++ b/src/components/Explore/Explore.jsx @@ -221,7 +221,6 @@ function Explore({ searchInputValue }) { description={item.description} downloads={item.downloads} stars={item.stars} - isSigned={item.isSigned} signatureInfo={item.signatureInfo} isBookmarked={item.isBookmarked} isStarred={item.isStarred} diff --git a/src/components/Home/Home.jsx b/src/components/Home/Home.jsx index addd47d2..c36424d5 100644 --- a/src/components/Home/Home.jsx +++ b/src/components/Home/Home.jsx @@ -261,7 +261,6 @@ function Home() { description={item.description} downloads={item.downloads} stars={item.stars} - isSigned={item.isSigned} signatureInfo={item.signatureInfo} isBookmarked={item.isBookmarked} isStarred={item.isStarred} diff --git a/src/components/Repo/RepoDetails.jsx b/src/components/Repo/RepoDetails.jsx index f31d2dc2..d04b76b1 100644 --- a/src/components/Repo/RepoDetails.jsx +++ b/src/components/Repo/RepoDetails.jsx @@ -11,6 +11,7 @@ import { host } from '../../host'; import { useParams, useNavigate, createSearchParams } from 'react-router-dom'; import { mapToRepoFromRepoInfo } from 'utilities/objectModels'; import { isAuthenticated } from 'utilities/authUtilities'; +import filterConstants from 'utilities/filterConstants'; // components import { Card, CardContent, CardMedia, Chip, Grid, Stack, Tooltip, Typography, IconButton } from '@mui/material'; @@ -260,6 +261,28 @@ function RepoDetails() { return lastDate; }; + const getSignatureChips = () => { + const cosign = repoDetailData.signatureInfo + ?.map((s) => s.tool) + .includes(filterConstants.signatureToolConstants.COSIGN) + ? repoDetailData.signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.COSIGN) + : null; + const notation = repoDetailData.signatureInfo + ?.map((s) => s.tool) + .includes(filterConstants.signatureToolConstants.NOTATION) + ? repoDetailData.signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.NOTATION) + : null; + const sigArray = []; + if (cosign) sigArray.push(cosign); + if (notation) sigArray.push(notation); + if (sigArray.length === 0) return <SignatureIconCheck />; + return sigArray.map((sig, index) => ( + <div className="hide-on-mobile" key={`${name}sig${index}`}> + <SignatureIconCheck signatureInfo={sig} /> + </div> + )); + }; + return ( <> {isLoading ? ( @@ -288,10 +311,7 @@ function RepoDetails() { </Stack> <Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}> <VulnerabilityIconCheck vulnerabilitySeverity={repoDetailData?.vulnerabilitySeverity} /> - <SignatureIconCheck - isSigned={repoDetailData.isSigned} - signatureInfo={repoDetailData.signatureInfo} - /> + {getSignatureChips()} </Stack> <Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={1}> {isAuthenticated() && ( @@ -304,13 +324,20 @@ function RepoDetails() { </IconButton> )} {isAuthenticated() && ( - <IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button"> - {repoDetailData?.isBookmarked ? ( - <BookmarkIcon data-testid="bookmarked" /> - ) : ( - <BookmarkBorderIcon data-testid="not-bookmarked" /> - )} - </IconButton> + <Stack + alignItems="center" + sx={{ width: { xs: '100%', md: 'auto' } }} + direction="row" + spacing={2} + > + <IconButton component="span" onClick={handleBookmarkClick} data-testid="bookmark-button"> + {repoDetailData?.isBookmarked ? ( + <BookmarkIcon data-testid="bookmarked" /> + ) : ( + <BookmarkBorderIcon data-testid="not-bookmarked" /> + )} + </IconButton> + </Stack> )} </Stack> </Stack> diff --git a/src/components/Shared/PreviewCard.jsx b/src/components/Shared/PreviewCard.jsx index 919d91c2..380f3081 100644 --- a/src/components/Shared/PreviewCard.jsx +++ b/src/components/Shared/PreviewCard.jsx @@ -10,7 +10,7 @@ import repocube3 from '../../assets/repocube-3.png'; import repocube4 from '../../assets/repocube-4.png'; import { isEmpty } from 'lodash'; -import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; +import { VulnerabilityIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; // temporary utility to get image const randomIntFromInterval = (min, max) => { @@ -67,7 +67,7 @@ const useStyles = makeStyles(() => ({ function PreviewCard(props) { const classes = useStyles(); const navigate = useNavigate(); - const { name, isSigned, vulnerabilityData, logo } = props; + const { name, vulnerabilityData, logo } = props; const goToDetails = () => { navigate(`/image/${encodeURIComponent(name)}`); @@ -108,7 +108,6 @@ function PreviewCard(props) { </Tooltip> <Stack direction="row" spacing={0.5} sx={{ marginLeft: 'auto', marginRight: 0 }}> <VulnerabilityIconCheck {...vulnerabilityData} /> - <SignatureIconCheck isSigned={isSigned} /> </Stack> </Stack> </Grid> diff --git a/src/components/Shared/RepoCard.jsx b/src/components/Shared/RepoCard.jsx index 57188857..85093c63 100644 --- a/src/components/Shared/RepoCard.jsx +++ b/src/components/Shared/RepoCard.jsx @@ -32,15 +32,16 @@ import StarIcon from '@mui/icons-material/Star'; import StarBorderIcon from '@mui/icons-material/StarBorder'; import { useTheme } from '@emotion/react'; +import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; +import { Markdown } from 'utilities/MarkdowntojsxWrapper'; +import filterConstants from 'utilities/filterConstants'; + // placeholder images import repocube1 from '../../assets/repocube-1.png'; import repocube2 from '../../assets/repocube-2.png'; import repocube3 from '../../assets/repocube-3.png'; import repocube4 from '../../assets/repocube-4.png'; -import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck'; -import { Markdown } from 'utilities/MarkdowntojsxWrapper'; - // temporary utility to get image const randomIntFromInterval = (min, max) => { return Math.floor(Math.random() * (max - min + 1) + min); @@ -186,7 +187,6 @@ function RepoCard(props) { description, downloads, stars, - isSigned, signatureInfo, lastUpdated, version, @@ -296,6 +296,24 @@ function RepoCard(props) { ); }; + const getSignatureChips = () => { + const cosign = signatureInfo?.map((s) => s.tool).includes(filterConstants.signatureToolConstants.COSIGN) + ? signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.COSIGN) + : null; + const notation = signatureInfo?.map((s) => s.tool).includes(filterConstants.signatureToolConstants.NOTATION) + ? signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.NOTATION) + : null; + const sigArray = []; + if (cosign) sigArray.push(cosign); + if (notation) sigArray.push(notation); + if (sigArray.length === 0) return <SignatureIconCheck />; + return sigArray.map((sig, index) => ( + <div className="hide-on-mobile" key={`${name}sig${index}`}> + <SignatureIconCheck signatureInfo={sig} /> + </div> + )); + }; + return ( <Card variant="outlined" className={classes.card} data-testid="repo-card"> <CardActionArea @@ -326,9 +344,7 @@ function RepoCard(props) { <div className="hide-on-mobile"> <VulnerabilityIconCheck {...vulnerabilityData} className="hide-on-mobile" /> </div> - <div className="hide-on-mobile"> - <SignatureIconCheck isSigned={isSigned} signatureInfo={signatureInfo} className="hide-on-mobile" /> - </div> + {getSignatureChips()} </Stack> <Tooltip title={description || 'Description not available'} placement="top"> <Typography className={classes.description} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap> diff --git a/src/components/Shared/SignatureTooltip.jsx b/src/components/Shared/SignatureTooltip.jsx index c4433b48..bde8ae17 100644 --- a/src/components/Shared/SignatureTooltip.jsx +++ b/src/components/Shared/SignatureTooltip.jsx @@ -1,19 +1,17 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Typography, Stack } from '@mui/material'; - import { isEmpty } from 'lodash'; +import { getStrongestSignature, getAllAuthorsOfSignatures } from 'utilities/vulnerabilityAndSignatureCheck'; -function SignatureTooltip({ isSigned, signatureInfo }) { - const { tool, isTrusted, author } = !isEmpty(signatureInfo) - ? signatureInfo[0] - : { tool: 'Unknown', isTrusted: 'Unknown', author: 'Unknown' }; +function SignatureTooltip({ signatureInfo }) { + const strongestSignature = useMemo(() => getStrongestSignature(signatureInfo)); - return ( + return isEmpty(strongestSignature) ? ( + <Typography>Not signed</Typography> + ) : ( <Stack direction="column"> - <Typography>{isSigned ? 'Verified Signature' : 'Unverified Signature'}</Typography> - <Typography>Tool: {tool}</Typography> - <Typography>Trusted: {!isEmpty(isTrusted) ? isTrusted : 'Unknown'}</Typography> - <Typography>Author: {!isEmpty(author) ? author : 'Unknown'}</Typography> + <Typography>Tool: {strongestSignature?.tool || 'Unknown'}</Typography> + <Typography>Signed-by: {getAllAuthorsOfSignatures(signatureInfo) || 'Unknown'}</Typography> </Stack> ); } diff --git a/src/components/Tag/Tabs/DependsOn.jsx b/src/components/Tag/Tabs/DependsOn.jsx index 56b0ed51..1cfafb37 100644 --- a/src/components/Tag/Tabs/DependsOn.jsx +++ b/src/components/Tag/Tabs/DependsOn.jsx @@ -107,7 +107,7 @@ function DependsOn(props) { repoName={dependence.repoName} tag={dependence.tag} vendor={dependence.vendor} - isSigned={dependence.isSigned} + signatureInfo={dependence.signatureInfo} manifests={dependence.manifests} key={index} lastUpdated={dependence.lastUpdated} diff --git a/src/components/Tag/Tabs/IsDependentOn.jsx b/src/components/Tag/Tabs/IsDependentOn.jsx index 4afaf675..03ec2b3d 100644 --- a/src/components/Tag/Tabs/IsDependentOn.jsx +++ b/src/components/Tag/Tabs/IsDependentOn.jsx @@ -107,7 +107,7 @@ function IsDependentOn(props) { repoName={dependence.repoName} tag={dependence.tag} vendor={dependence.vendor} - isSigned={dependence.isSigned} + signatureInfo={dependence.signatureInfo} manifests={dependence.manifests} key={index} lastUpdated={dependence.lastUpdated} diff --git a/src/components/Tag/TagDetails.jsx b/src/components/Tag/TagDetails.jsx index 8242edc5..abe90d48 100644 --- a/src/components/Tag/TagDetails.jsx +++ b/src/components/Tag/TagDetails.jsx @@ -5,6 +5,7 @@ import React, { useEffect, useMemo, useState, useRef } from 'react'; import { api, endpoints } from '../../api'; import { host } from '../../host'; import { mapToImage } from '../../utilities/objectModels'; +import filterConstants from 'utilities/filterConstants'; import { isEmpty, head } from 'lodash'; // components @@ -224,6 +225,28 @@ function TagDetails() { } }; + const getSignatureChips = () => { + const cosign = imageDetailData.signatureInfo + ?.map((s) => s.tool) + .includes(filterConstants.signatureToolConstants.COSIGN) + ? imageDetailData.signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.COSIGN) + : null; + const notation = imageDetailData.signatureInfo + ?.map((s) => s.tool) + .includes(filterConstants.signatureToolConstants.NOTATION) + ? imageDetailData.signatureInfo.filter((si) => si.tool == filterConstants.signatureToolConstants.NOTATION) + : null; + const sigArray = []; + if (cosign) sigArray.push(cosign); + if (notation) sigArray.push(notation); + if (sigArray.length === 0) return <SignatureIconCheck />; + return sigArray.map((sig, index) => ( + <div className="hide-on-mobile" key={`${name}sig${index}`}> + <SignatureIconCheck signatureInfo={sig} /> + </div> + )); + }; + return ( <> {isLoading ? ( @@ -256,15 +279,12 @@ function TagDetails() { </Typography> </Stack> - <Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={1}> + <Stack alignItems="center" sx={{ width: { xs: '100%', md: 'auto' } }} direction="row" spacing={2}> <VulnerabilityIconCheck vulnerabilitySeverity={imageDetailData.vulnerabiltySeverity} count={imageDetailData.vulnerabilityCount} /> - <SignatureIconCheck - isSigned={imageDetailData.isSigned} - signatureInfo={imageDetailData.signatureInfo} - /> + {getSignatureChips()} </Stack> </Stack> <Stack direction="row" alignItems="center" spacing="1rem"> diff --git a/src/utilities/filterConstants.js b/src/utilities/filterConstants.js index 8a95591f..25d0501e 100644 --- a/src/utilities/filterConstants.js +++ b/src/utilities/filterConstants.js @@ -69,6 +69,11 @@ const archFilters = [ } ]; -const filterConstants = { osFilters, imageFilters, archFilters }; +const signatureToolConstants = { + COSIGN: 'cosign', + NOTATION: 'notation' +}; + +const filterConstants = { osFilters, imageFilters, archFilters, signatureToolConstants }; export default filterConstants; diff --git a/src/utilities/objectModels.js b/src/utilities/objectModels.js index 03403858..c8c5d6bb 100644 --- a/src/utilities/objectModels.js +++ b/src/utilities/objectModels.js @@ -132,12 +132,12 @@ const mapSignatureInfo = (signatureInfo) => { return signatureInfo ? { tool: signatureInfo.Tool, - isTrusted: signatureInfo.IsTrusted?.toString(), + isTrusted: signatureInfo.IsTrusted, author: signatureInfo.Author } : { tool: 'Unknown', - isTrusted: 'Unknown', + isTrusted: false, author: 'Unknown' }; }; diff --git a/src/utilities/vulnerabilityAndSignatureCheck.jsx b/src/utilities/vulnerabilityAndSignatureCheck.jsx index 5f679199..e1f410cb 100644 --- a/src/utilities/vulnerabilityAndSignatureCheck.jsx +++ b/src/utilities/vulnerabilityAndSignatureCheck.jsx @@ -1,3 +1,4 @@ +import { isEmpty } from 'lodash'; import React from 'react'; import { NoneVulnerabilityIcon, @@ -17,9 +18,24 @@ import { UnknownVulnerabilityIcon, UnknownVulnerabilityChip, FailedScanIcon, - FailedScanChip + FailedScanChip, + NotTrustedSignatureIcon, + NotTrustedSignatureChip } from './vulnerabilityAndSignatureComponents'; +const getStrongestSignature = (signatureInfo) => { + if (isEmpty(signatureInfo)) return null; + const trusted = signatureInfo.find((si) => si.isTrusted); + if (!isEmpty(trusted)) return trusted; + return signatureInfo[0]; +}; + +const getAllAuthorsOfSignatures = (signatureInfo) => { + if (isEmpty(signatureInfo)) return 0; + const signatureAuthors = signatureInfo.filter((si) => si.isTrusted).map((si) => si.author); + return signatureAuthors.join(','); +}; + const VulnerabilityIconCheck = ({ vulnerabilitySeverity }) => { let result; let vulnerabilityStringTitle = ''; @@ -84,20 +100,25 @@ const VulnerabilityChipCheck = ({ vulnerabilitySeverity }) => { return result; }; -const SignatureIconCheck = ({ isSigned, signatureInfo }) => { - if (isSigned) { - return <VerifiedSignatureIcon signatureInfo={signatureInfo} />; - } else { - return <UnverifiedSignatureIcon signatureInfo={signatureInfo} />; - } +const SignatureIconCheck = ({ signatureInfo }) => { + const strongestSignature = getStrongestSignature(signatureInfo); + if (strongestSignature === null) return <UnverifiedSignatureIcon signatureInfo={signatureInfo} />; + if (strongestSignature.isTrusted) return <VerifiedSignatureIcon signatureInfo={signatureInfo} />; + return <NotTrustedSignatureIcon signatureInfo={signatureInfo} />; }; -const SignatureChipCheck = ({ isSigned }) => { - if (isSigned) { - return <VerifiedSignatureChip />; - } else { - return <UnverifiedSignatureChip />; - } +const SignatureChipCheck = ({ signatureInfo }) => { + const strongestSignature = getStrongestSignature(signatureInfo); + if (strongestSignature === null) return <UnverifiedSignatureChip signatureInfo={signatureInfo} />; + if (strongestSignature.isTrusted) return <VerifiedSignatureChip signatureInfo={signatureInfo} />; + return <NotTrustedSignatureChip signatureInfo={signatureInfo} />; }; -export { VulnerabilityIconCheck, VulnerabilityChipCheck, SignatureIconCheck, SignatureChipCheck }; +export { + VulnerabilityIconCheck, + VulnerabilityChipCheck, + SignatureIconCheck, + SignatureChipCheck, + getStrongestSignature, + getAllAuthorsOfSignatures +}; diff --git a/src/utilities/vulnerabilityAndSignatureComponents.jsx b/src/utilities/vulnerabilityAndSignatureComponents.jsx index 76e55177..299463ca 100644 --- a/src/utilities/vulnerabilityAndSignatureComponents.jsx +++ b/src/utilities/vulnerabilityAndSignatureComponents.jsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Chip, Tooltip } from '@mui/material'; +import { Chip, Tooltip, Badge } from '@mui/material'; import SvgIcon from '@mui/material/SvgIcon'; import { ReactComponent as failedScanBug } from '../assets/failedScan.svg'; import { createSvgIcon } from '@mui/material/utils'; import SignatureTooltip from 'components/Shared/SignatureTooltip'; +import filterConstants from 'utilities/filterConstants'; const FilledBugIcon = createSvgIcon( <path d="M17.0293 5.13093V6.1543H18.3828L21.2414 3.24068L22.2621 4.27812L19.5552 7.03876L19.5879 7.12668C20.1841 8.73695 20.4862 10.4449 20.4793 12.1662C20.4793 12.5064 20.4678 12.8466 20.4448 13.186L20.4397 13.2634H24V14.7334H20.2569L20.2466 14.7932C19.9431 16.4882 19.3517 18.0338 18.5466 19.335L18.4862 19.4335L21.9276 22.9608L20.9052 24L17.6121 20.6239L17.5138 20.7365C16.0259 22.4333 14.0983 23.4514 11.9983 23.4514C9.86724 23.4514 7.91207 22.4016 6.41552 20.6573L6.31552 20.5413L3.08966 23.833L2.06897 22.792L5.45345 19.3403L5.39483 19.2436C4.61897 17.9618 4.04655 16.4478 3.75 14.7932L3.73966 14.7334H0V13.2634H3.55862L3.55345 13.1843C3.53103 12.8502 3.51897 12.509 3.51897 12.1644C3.51202 10.4654 3.80581 8.77905 4.38621 7.18646L4.41897 7.1003L1.64138 4.2535L2.66379 3.21606L5.53103 6.1543H6.96724V5.13093C6.96724 3.77012 7.49729 2.46505 8.4408 1.50281C9.3843 0.540578 10.664 0 11.9983 0C13.3326 0 14.6123 0.540578 15.5558 1.50281C16.4993 2.46505 17.0293 3.77012 17.0293 5.13093Z" />, @@ -14,11 +15,15 @@ const OutlinedBugIcon = createSvgIcon( 'OutlinedBug' ); const UnverifiedShieldIcon = createSvgIcon( - <path d="M12.4837 2C13.6167 2 19.5627 4.041 20.3487 4.828C21.0047 5.484 20.9947 6.014 20.9487 8.557C20.9307 9.575 20.9057 10.962 20.9057 12.879C20.9057 19.761 13.0357 22.223 12.7007 22.324C12.6297 22.346 12.5567 22.356 12.4837 22.356C12.4107 22.356 12.3377 22.346 12.2667 22.324C11.9317 22.223 4.06165 19.761 4.06165 12.879C4.06165 10.959 4.03665 9.572 4.01865 8.554C4.01044 8.10043 4.00337 7.71095 4.00104 7.37341L4.00073 6.9925C4.00922 5.74112 4.1264 5.32 4.61765 4.828C5.40465 4.041 11.3507 2 12.4837 2ZM12.4837 3.5C11.6357 3.5 6.28465 5.384 5.66765 5.899C5.54931 6.018 5.50535 6.19514 5.49972 6.89808L5.49926 7.16877C5.50045 7.51182 5.50742 7.95335 5.51765 8.526C5.53665 9.552 5.56165 10.947 5.56165 12.879C5.56165 18.08 11.2837 20.389 12.4827 20.814C13.6807 20.387 19.4057 18.065 19.4057 12.879C19.4057 10.949 19.4307 9.555 19.4487 8.529C19.4592 7.95581 19.4663 7.51389 19.4674 7.17033L19.4668 6.89918C19.4605 6.19482 19.4138 6.01519 19.2877 5.889C18.6817 5.384 13.3317 3.5 12.4837 3.5ZM11.1346 9.5372L12.4837 10.887L13.8328 9.5372C14.1258 9.2442 14.5998 9.2442 14.8928 9.5372C15.1858 9.8302 15.1858 10.3042 14.8928 10.5972L13.5437 11.947L14.8926 13.2952C15.1856 13.5882 15.1856 14.0622 14.8926 14.3552C14.7466 14.5022 14.5546 14.5752 14.3626 14.5752C14.1706 14.5752 13.9786 14.5022 13.8326 14.3552L12.4837 13.007L11.1348 14.3552C10.9888 14.5022 10.7968 14.5752 10.6048 14.5752C10.4128 14.5752 10.2208 14.5022 10.0748 14.3552C9.78175 14.0622 9.78175 13.5882 10.0748 13.2952L11.4237 11.947L10.0746 10.5972C9.78155 10.3042 9.78155 9.8302 10.0746 9.5372C10.3676 9.2442 10.8416 9.2442 11.1346 9.5372Z" />, + <path d="M9,0,0,4v6c0,5.55,3.84,10.74,9,12,5.16-1.26,9-6.45,9-12V4Zm7,10a10.47,10.47,0,0,1-7,9.93A10.47,10.47,0,0,1,2,10V5.3L9,2.19,16,5.3ZM7,7.74l1.22,1.4c.32.36.58.7.86,1.06H9.1c.28-.39.56-.72.84-1.07l1.2-1.39h1.68L9.91,10.89l3,3.37H11.14L9.89,12.79c-.33-.38-.62-.74-.92-1.13h0c-.28.39-.58.74-.9,1.13L6.8,14.26H5.09l3-3.33L5.23,7.74Z" />, 'UnverifiedShield' ); -const VerifiedShieldIcon = createSvgIcon( - <path d="M12.4836 2C13.6166 2 19.5616 4.041 20.3486 4.828C21.0046 5.484 20.9946 6.014 20.9486 8.554C20.9306 9.572 20.9056 10.959 20.9056 12.879C20.9056 19.761 13.0356 22.223 12.7006 22.324C12.6296 22.346 12.5566 22.356 12.4836 22.356C12.4106 22.356 12.3376 22.346 12.2666 22.324C11.9316 22.223 4.06162 19.761 4.06162 12.879C4.06162 10.962 4.03662 9.575 4.01862 8.557C4.01041 8.10289 4.00334 7.71298 4.00102 7.37507L4.00073 6.99377C4.00931 5.74113 4.12687 5.32 4.61962 4.828C5.40462 4.041 11.3496 2 12.4836 2ZM12.4836 3.5C11.6356 3.5 6.28562 5.384 5.66862 5.899C5.48662 6.082 5.47962 6.4 5.51862 8.529C5.53662 9.555 5.56162 10.949 5.56162 12.879C5.56162 18.08 11.2836 20.389 12.4826 20.814C13.6806 20.387 19.4056 18.065 19.4056 12.879C19.4056 10.947 19.4306 9.552 19.4496 8.526C19.4876 6.399 19.4806 6.081 19.2876 5.889C18.6826 5.384 13.3316 3.5 12.4836 3.5ZM16.2051 9.3395C16.4981 9.6325 16.4981 10.1075 16.2051 10.4005L12.3071 14.2995C12.1951 14.4123 12.0505 14.4854 11.8952 14.5102L11.7771 14.5195C11.5781 14.5195 11.3871 14.4405 11.2461 14.2995L9.35412 12.4055C9.06212 12.1125 9.06212 11.6365 9.35512 11.3445C9.64712 11.0515 10.1231 11.0515 10.4161 11.3445L11.7771 12.7075L15.1451 9.3395C15.4381 9.0465 15.9121 9.0465 16.2051 9.3395Z" />, +const CVerifiedShieldIcon = createSvgIcon( + <path d="M11.8,13.64a1.85,1.85,0,0,1,0,.25.9.9,0,0,1,0,.18.33.33,0,0,1,0,.12.47.47,0,0,1-.09.12,1.25,1.25,0,0,1-.25.18c-.13.07-.28.13-.45.2a4.13,4.13,0,0,1-.61.16,4.35,4.35,0,0,1-.74.06,3.93,3.93,0,0,1-1.41-.24A2.91,2.91,0,0,1,7.1,14a3.32,3.32,0,0,1-.67-1.2A5.2,5.2,0,0,1,6.2,11.1a5.37,5.37,0,0,1,.25-1.72,3.85,3.85,0,0,1,.72-1.26,3,3,0,0,1,1.12-.77,3.63,3.63,0,0,1,1.42-.26,3,3,0,0,1,.61,0,2.66,2.66,0,0,1,.54.14,2.34,2.34,0,0,1,.45.19,1.84,1.84,0,0,1,.28.19l.11.13s0,.09.05.14,0,.12,0,.19a2.35,2.35,0,0,1,0,.28,2.63,2.63,0,0,1,0,.3.88.88,0,0,1,0,.2.52.52,0,0,1-.07.11.17.17,0,0,1-.1,0,.38.38,0,0,1-.22-.1c-.09-.07-.21-.14-.35-.23a3.08,3.08,0,0,0-.51-.23,2.41,2.41,0,0,0-.7-.1A1.67,1.67,0,0,0,9,8.57a1.58,1.58,0,0,0-.6.52A2.54,2.54,0,0,0,8,9.92,4.15,4.15,0,0,0,7.86,11,4.31,4.31,0,0,0,8,12.17a2.19,2.19,0,0,0,.39.81,1.55,1.55,0,0,0,.61.47,2,2,0,0,0,.82.16,2.17,2.17,0,0,0,.71-.1A2.45,2.45,0,0,0,11,13.3l.35-.21a.38.38,0,0,1,.21-.1.17.17,0,0,1,.1,0,.17.17,0,0,1,.06.09c0,.05,0,.11,0,.2A3,3,0,0,1,11.8,13.64ZM9,0,0,4v6c0,5.55,3.84,10.74,9,12,5.16-1.26,9-6.45,9-12V4Zm7,10a10.47,10.47,0,0,1-7,9.93A10.47,10.47,0,0,1,2,10V5.3L9,2.19,16,5.3Z" />, + 'VerifiedShield' +); +const NVerifiedShieldIcon = createSvgIcon( + <path d="M12.13,14.25a.6.6,0,0,1-.05.24.45.45,0,0,1-.13.17.39.39,0,0,1-.19.1.52.52,0,0,1-.21,0h-.66a1.79,1.79,0,0,1-.36,0,.72.72,0,0,1-.27-.15,1.06,1.06,0,0,1-.24-.3c-.08-.12-.17-.28-.27-.47L7.87,10.29c-.11-.21-.22-.44-.34-.68s-.21-.48-.3-.71h0l0,.84c0,.28,0,.56,0,.86v4a.17.17,0,0,1,0,.1.19.19,0,0,1-.11.08.81.81,0,0,1-.21.05l-.35,0-.34,0A.81.81,0,0,1,6,14.75a.19.19,0,0,1-.11-.08.17.17,0,0,1,0-.1V7.75A.51.51,0,0,1,6,7.34a.56.56,0,0,1,.39-.14h.83a1.82,1.82,0,0,1,.37,0,.84.84,0,0,1,.27.13,1.06,1.06,0,0,1,.23.24A3.9,3.9,0,0,1,8.35,8l1.47,2.78.26.49.24.49.23.47c.07.16.15.32.22.47h0c0-.27,0-.56,0-.85s0-.58,0-.85V7.43a.17.17,0,0,1,0-.1.29.29,0,0,1,.12-.09.9.9,0,0,1,.22,0h.68a.72.72,0,0,1,.2,0s.09,0,.11.09a.17.17,0,0,1,0,.1ZM9,0,0,4v6c0,5.55,3.84,10.74,9,12,5.16-1.26,9-6.45,9-12V4Zm7,10a10.47,10.47,0,0,1-7,9.93A10.47,10.47,0,0,1,2,10V5.3L9,2.19,16,5.3Z" />, 'VerifiedShield' ); @@ -222,8 +227,9 @@ const CriticalVulnerabilityChip = () => { const UnverifiedSignatureIcon = ({ signatureInfo }) => { return ( - <Tooltip title={<SignatureTooltip isSigned={false} signatureInfo={signatureInfo} />} placement="top"> + <Tooltip title={<SignatureTooltip signatureInfo={signatureInfo} />} placement="top"> <UnverifiedShieldIcon + viewBox="0 0 18 22" sx={{ color: '#E53935', padding: '0.2rem', @@ -237,22 +243,119 @@ const UnverifiedSignatureIcon = ({ signatureInfo }) => { </Tooltip> ); }; + +const NotTrustedSignatureIcon = ({ signatureInfo }) => { + return ( + <Tooltip title={<SignatureTooltip signatureInfo={signatureInfo} />} placement="top"> + {(signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.NOTATION && ( + <Badge + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right' + }} + overlap="circular" + color="warning" + badgeContent={signatureInfo.length} + > + <NVerifiedShieldIcon + viewBox="0 0 18 22" + sx={{ + backgroundColor: '#FCE2B8!important', + color: '#FB8C00', + alignSelf: 'center', + padding: '0.2rem', + background: '#E8F5E9', + borderRadius: '1rem', + height: '1.5rem', + width: '1.6rem' + }} + data-testid="untrusted-icon" + /> + </Badge> + )) || + (signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.COSIGN && ( + <Badge + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right' + }} + overlap="circular" + color="warning" + badgeContent={signatureInfo.length} + > + <CVerifiedShieldIcon + viewBox="0 0 18 22" + sx={{ + backgroundColor: '#FCE2B8!important', + color: '#FB8C00', + alignSelf: 'center', + padding: '0.2rem', + background: '#E8F5E9', + borderRadius: '1rem', + height: '1.5rem', + width: '1.6rem' + }} + data-testid="untrusted-icon" + /> + </Badge> + ))} + </Tooltip> + ); +}; + const VerifiedSignatureIcon = ({ signatureInfo }) => { return ( - <Tooltip title={<SignatureTooltip isSigned={true} signatureInfo={signatureInfo} />} placement="top"> - <VerifiedShieldIcon - viewBox="0 0 24 24" - sx={{ - color: '#43A047', - alignSelf: 'center', - padding: '0.2rem', - background: '#E8F5E9', - borderRadius: '1rem', - height: '1.5rem', - width: '1.6rem' - }} - data-testid="verified-icon" - /> + <Tooltip title={<SignatureTooltip signatureInfo={signatureInfo} />} placement="top"> + {(signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.NOTATION && ( + <Badge + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right' + }} + overlap="circular" + color="success" + badgeContent={signatureInfo.length} + > + <NVerifiedShieldIcon + viewBox="0 0 18 22" + sx={{ + color: '#43A047', + alignSelf: 'center', + padding: '0.2rem', + background: '#E8F5E9', + borderRadius: '1rem', + height: '1.5rem', + width: '1.6rem' + }} + data-testid="verified-icon" + /> + </Badge> + )) || + (signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.COSIGN && ( + <Badge + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right' + }} + overlap="circular" + color="success" + badgeContent={signatureInfo.length} + > + <CVerifiedShieldIcon + viewBox="0 0 18 22" + sx={{ + color: '#43A047', + alignSelf: 'center', + padding: '0.2rem', + background: '#E8F5E9', + borderRadius: '1rem', + height: '1.5rem', + width: '1.6rem' + }} + data-testid="verified-icon" + /> + </Badge> + ))} </Tooltip> ); }; @@ -270,7 +373,22 @@ const UnverifiedSignatureChip = () => { /> ); }; -const VerifiedSignatureChip = () => { + +const NotTrustedSignatureChip = ({ signatureInfo }) => { + return ( + <Chip + label="Signature not trusted" + sx={{ backgroundColor: '#FCE2B8!important', color: '#FB8C00', fontSize: '0.8125rem' }} + variant="filled" + onDelete={() => { + return; + }} + deleteIcon={<NotTrustedSignatureIcon signatureInfo={signatureInfo} sx={{ color: '#FB8C00!important' }} />} + /> + ); +}; + +const VerifiedSignatureChip = ({ signatureInfo }) => { return ( <Chip label="Verified Signature" @@ -279,7 +397,14 @@ const VerifiedSignatureChip = () => { onDelete={() => { return; }} - deleteIcon={<VerifiedShieldIcon sx={{ color: '#388E3C!important' }} />} + deleteIcon={ + (signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.COSIGN && ( + <CVerifiedShieldIcon sx={{ color: '#388E3C!important' }} /> + )) || + (signatureInfo[0]?.tool && signatureInfo[0].tool == filterConstants.signatureToolConstants.NOTATION && ( + <NVerifiedShieldIcon sx={{ color: '#388E3C!important' }} /> + )) + } /> ); }; @@ -298,8 +423,10 @@ export { HighVulnerabilityChip, CriticalVulnerabilityChip, UnverifiedSignatureIcon, + NotTrustedSignatureIcon, VerifiedSignatureIcon, UnverifiedSignatureChip, + NotTrustedSignatureChip, VerifiedSignatureChip, FailedScanIcon, FailedScanChip diff --git a/tests/utils/test-data-parser.js b/tests/utils/test-data-parser.js index 24420ec4..fad0971e 100644 --- a/tests/utils/test-data-parser.js +++ b/tests/utils/test-data-parser.js @@ -149,7 +149,7 @@ const getRepoListOrderedAlpha = () => { // }; const getRepoCardNameForLocator = (repo) => { - return `${repo?.repo} ${repo?.tags[0]?.description?.slice(0, 10)}`; + return new RegExp(`${repo?.repo} \\d${repo?.tags[0]?.description?.slice(0, 10)}`); }; export {