0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

feat(cve): implemented trivy image scan for multiarch images (#1510)

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-07-06 11:36:26 +03:00 committed by GitHub
parent 96d9d318df
commit 0a04b2a4ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1617 additions and 370 deletions

View file

@ -129,6 +129,7 @@ $(TESTDATA): check-skopeo
skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:7 oci:${TESTDATA}/zot-test:0.0.1; \ skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:7 oci:${TESTDATA}/zot-test:0.0.1; \
skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:8 oci:${TESTDATA}/zot-cve-test:0.0.1; \ skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:8 oci:${TESTDATA}/zot-cve-test:0.0.1; \
skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/java:0.0.1 oci:${TESTDATA}/zot-cve-java-test:0.0.1; \ skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/java:0.0.1 oci:${TESTDATA}/zot-cve-java-test:0.0.1; \
skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/alpine:3.17.3 oci:${TESTDATA}/alpine:3.17.3; \
chmod -R a=rwx ${TESTDATA} chmod -R a=rwx ${TESTDATA}
.PHONY: run-bench .PHONY: run-bench

View file

@ -73,7 +73,7 @@ var (
ErrEmptyRepoName = errors.New("repodb: repo name can't be empty string") ErrEmptyRepoName = errors.New("repodb: repo name can't be empty string")
ErrEmptyTag = errors.New("repodb: tag can't be empty string") ErrEmptyTag = errors.New("repodb: tag can't be empty string")
ErrEmptyDigest = errors.New("repodb: digest can't be empty string") ErrEmptyDigest = errors.New("repodb: digest can't be empty string")
ErrInvalidRepoTagFormat = errors.New("invalid format for tag search, not following repo:tag") ErrInvalidRepoRefFormat = errors.New("invalid image reference format")
ErrLimitIsNegative = errors.New("pageturner: limit has negative value") ErrLimitIsNegative = errors.New("pageturner: limit has negative value")
ErrOffsetIsNegative = errors.New("pageturner: offset has negative value") ErrOffsetIsNegative = errors.New("pageturner: offset has negative value")
ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported") ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported")
@ -96,4 +96,5 @@ var (
ErrSyncPingRegistry = errors.New("sync: unable to ping any registry URLs") ErrSyncPingRegistry = errors.New("sync: unable to ping any registry URLs")
ErrSyncImageNotSigned = errors.New("sync: image is not signed") ErrSyncImageNotSigned = errors.New("sync: image is not signed")
ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config") ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config")
ErrCallerInfo = errors.New("runtime: failed to get info regarding the current runtime")
) )

View file

@ -29,6 +29,7 @@ import (
zotErrors "zotregistry.io/zot/errors" zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
@ -1035,7 +1036,7 @@ func TestServerCVEResponse(t *testing.T) {
space := regexp.MustCompile(`\s+`) space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ") str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual, So(strings.TrimSpace(str), ShouldResemble,
"IMAGE NAME TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 40d1f749 false 605B") "IMAGE NAME TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 40d1f749 false 605B")
}) })
@ -1172,7 +1173,8 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
// Setup test CVE data in mock scanner // Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{ scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
if image == "zot-cve-test:0.0.1" { if image == "zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495" ||
image == "zot-cve-test:0.0.1" {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE-1": { "CVE-1": {
ID: "CVE-1", ID: "CVE-1",
@ -1223,12 +1225,20 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
return false, err return false, err
} }
manifestDigestStr, ok := repoMeta.Tags[inputTag] manifestDigestStr := reference
if !ok {
return false, zotErrors.ErrTagMetaNotFound if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zotErrors.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
} }
manifestDigest, err := godigest.Parse(manifestDigestStr.Digest) manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil { if err != nil {
return false, err return false, err
} }

View file

@ -1891,8 +1891,8 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo
func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string, imageName, cveID string,
) (*common.FixedTags, error) { ) (*common.ImageListWithCVEFixedResponse, error) {
fixedTags := &common.FixedTags{ fixedTags := &common.ImageListWithCVEFixedResponse{
Errors: nil, Errors: nil,
ImageListWithCVEFixed: struct { ImageListWithCVEFixed: struct {
common.PaginatedImagesResult `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema common.PaginatedImagesResult `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema

View file

@ -43,7 +43,7 @@ type SearchService interface { //nolint:interfacebloat
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*common.ImagesForCve, error) cveID string) (*common.ImagesForCve, error)
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*common.FixedTags, error) cveID string) (*common.ImageListWithCVEFixedResponse, error)
getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string, getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string) (*common.DerivedImageListResponse, error) derivedImage string) (*common.DerivedImageListResponse, error)
getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
@ -377,7 +377,7 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search
func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string, username, password, imageName, cveID string,
) (*common.FixedTags, error) { ) (*common.ImageListWithCVEFixedResponse, error) {
query := fmt.Sprintf(` query := fmt.Sprintf(`
{ {
ImageListWithCVEFixed(id: "%s", image: "%s") { ImageListWithCVEFixed(id: "%s", image: "%s") {
@ -398,7 +398,7 @@ func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config s
}`, }`,
cveID, imageName) cveID, imageName)
result := &common.FixedTags{} result := &common.ImageListWithCVEFixedResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result) err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -847,7 +847,7 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear
} }
}`, cvid, imageName) }`, cvid, imageName)
result := &common.FixedTags{} result := &common.ImageListWithCVEFixedResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result) err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if err != nil { if err != nil {

View file

@ -112,7 +112,7 @@ type Annotation struct {
Value string `json:"value"` Value string `json:"value"`
} }
type FixedTags struct { type ImageListWithCVEFixedResponse struct {
Errors []ErrorGQL `json:"errors"` Errors []ErrorGQL `json:"errors"`
ImageListWithCVEFixed `json:"data"` ImageListWithCVEFixed `json:"data"`
} }

View file

@ -4,6 +4,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
@ -101,7 +102,7 @@ func GetRepoRefference(repo string) (string, string, bool, error) {
repoName, tag, found := strings.Cut(repo, ":") repoName, tag, found := strings.Cut(repo, ":")
if !found { if !found {
return "", "", false, zerr.ErrInvalidRepoTagFormat return "", "", false, zerr.ErrInvalidRepoRefFormat
} }
return repoName, tag, true, nil return repoName, tag, true, nil
@ -109,3 +110,22 @@ func GetRepoRefference(repo string) (string, string, bool, error) {
return repoName, digest, false, nil return repoName, digest, false, nil
} }
// GetFullImageName returns the formated string for the given repo/tag or repo/digest.
func GetFullImageName(repo, ref string) string {
if IsTag(ref) {
return repo + ":" + ref
}
return repo + "@" + ref
}
func IsDigest(ref string) bool {
_, err := digest.Parse(ref)
return err == nil
}
func IsTag(ref string) bool {
return !IsDigest(ref)
}

View file

@ -13,7 +13,7 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/extensions/search/convert" "zotregistry.io/zot/pkg/extensions/search/convert"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/gql_generated" "zotregistry.io/zot/pkg/extensions/search/gql_generated"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/bolt"
@ -74,9 +74,9 @@ func TestConvertErrors(t *testing.T) {
map[string]repodb.IndexData{}, map[string]repodb.IndexData{},
convert.SkipQGLField{}, convert.SkipQGLField{},
mocks.CveInfoMock{ mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo string, reference string, GetCVESummaryForImageMediaFn: func(repo string, digest, mediaType string,
) (cveinfo.ImageCVESummary, error) { ) (cvemodel.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError return cvemodel.ImageCVESummary{}, ErrTestError
}, },
}, },
) )
@ -120,9 +120,8 @@ func TestConvertErrors(t *testing.T) {
}, },
map[string]repodb.ManifestMetadata{}, map[string]repodb.ManifestMetadata{},
mocks.CveInfoMock{ mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string, GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
) (cveinfo.ImageCVESummary, error) { return cvemodel.ImageCVESummary{}, ErrTestError
return cveinfo.ImageCVESummary{}, ErrTestError
}, },
}, },
) )
@ -153,9 +152,8 @@ func TestConvertErrors(t *testing.T) {
ConfigBlob: configBlob, ConfigBlob: configBlob,
}, },
mocks.CveInfoMock{ mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string, GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
) (cveinfo.ImageCVESummary, error) { return cvemodel.ImageCVESummary{}, ErrTestError
return cveinfo.ImageCVESummary{}, ErrTestError
}, },
}, },
) )
@ -187,12 +185,7 @@ func TestConvertErrors(t *testing.T) {
ConfigBlob: []byte("bad json"), ConfigBlob: []byte("bad json"),
}, },
nil, nil,
mocks.CveInfoMock{ mocks.CveInfoMock{},
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
},
},
) )
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
@ -227,9 +220,8 @@ func TestConvertErrors(t *testing.T) {
}, },
nil, nil,
mocks.CveInfoMock{ mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string, GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
) (cveinfo.ImageCVESummary, error) { return cvemodel.ImageCVESummary{}, ErrTestError
return cveinfo.ImageCVESummary{}, ErrTestError
}, },
}, },
) )
@ -259,9 +251,8 @@ func TestConvertErrors(t *testing.T) {
Vulnerabilities: false, Vulnerabilities: false,
}, },
mocks.CveInfoMock{ mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string, GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
) (cveinfo.ImageCVESummary, error) { return cvemodel.ImageCVESummary{}, ErrTestError
return cveinfo.ImageCVESummary{}, ErrTestError
}, },
}, log.NewLogger("debug", ""), }, log.NewLogger("debug", ""),
) )
@ -286,9 +277,8 @@ func TestConvertErrors(t *testing.T) {
Vulnerabilities: false, Vulnerabilities: false,
}, },
mocks.CveInfoMock{ mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string, GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
) (cveinfo.ImageCVESummary, error) { return cvemodel.ImageCVESummary{}, ErrTestError
return cveinfo.ImageCVESummary{}, ErrTestError
}, },
}, log.NewLogger("debug", ""), }, log.NewLogger("debug", ""),
) )

View file

@ -103,7 +103,8 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata,
// We only scan the latest image on the repo for performance reasons // We only scan the latest image on the repo for performance reasons
// Check if vulnerability scanning is disabled // Check if vulnerability scanning is disabled
if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities { if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities {
imageCveSummary, err := cveInfo.GetCVESummaryForImage(repoMeta.Name, *lastUpdatedImageSummary.Tag) imageCveSummary, err := cveInfo.GetCVESummaryForImageMedia(repoMeta.Name, *lastUpdatedImageSummary.Digest,
*lastUpdatedImageSummary.MediaType)
if err != nil { if err != nil {
// Log the error, but we should still include the image in results // Log the error, but we should still include the image in results
graphql.AddError( graphql.AddError(
@ -227,10 +228,10 @@ func ImageIndex2ImageSummary(ctx context.Context, repo, tag string, indexDigest
} }
} }
imageCveSummary := cveinfo.ImageCVESummary{} imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE { if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, indexDigestStr, ispec.MediaTypeImageIndex)
if err != nil { if err != nil {
// Log the error, but we should still include the manifest in results // Log the error, but we should still include the manifest in results
@ -345,10 +346,10 @@ func ImageManifest2ImageSummary(ctx context.Context, repo, tag string, digest go
"manifest digest: %s, error: %s", tag, repo, manifestDigest, err.Error())) "manifest digest: %s, error: %s", tag, repo, manifestDigest, err.Error()))
} }
imageCveSummary := cveinfo.ImageCVESummary{} imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE { if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigest, ispec.MediaTypeImageManifest)
if err != nil { if err != nil {
// Log the error, but we should still include the manifest in results // Log the error, but we should still include the manifest in results
@ -500,10 +501,10 @@ func ImageManifest2ManifestSummary(ctx context.Context, repo, tag string, descri
"manifest digest: %s, error: %s", tag, repo, manifestDigestStr, err.Error())) "manifest digest: %s, error: %s", tag, repo, manifestDigestStr, err.Error()))
} }
imageCveSummary := cveinfo.ImageCVESummary{} imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE { if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
if err != nil { if err != nil {
// Log the error, but we should still include the manifest in results // Log the error, but we should still include the manifest in results
@ -662,7 +663,8 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata
// We only scan the latest image on the repo for performance reasons // We only scan the latest image on the repo for performance reasons
// Check if vulnerability scanning is disabled // Check if vulnerability scanning is disabled
if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities { if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities {
imageCveSummary, err := cveInfo.GetCVESummaryForImage(repoMeta.Name, *lastUpdatedImageSummary.Tag) imageCveSummary, err := cveInfo.GetCVESummaryForImageMedia(repoMeta.Name, *lastUpdatedImageSummary.Digest,
*lastUpdatedImageSummary.MediaType)
if err != nil { if err != nil {
// Log the error, but we should still include the image in results // Log the error, but we should still include the image in results
graphql.AddError( graphql.AddError(

View file

@ -2,15 +2,14 @@ package cveinfo
import ( import (
"encoding/json" "encoding/json"
"fmt"
"sort" "sort"
"strings" "strings"
"time"
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
"zotregistry.io/zot/errors" zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/common"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/cve/trivy" "zotregistry.io/zot/pkg/extensions/search/cve/trivy"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
@ -21,24 +20,22 @@ import (
type CveInfo interface { type CveInfo interface {
GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error)
GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, error)
GetCVEListForImage(repo, tag string, searchedCVE string, pageinput PageInput) ([]cvemodel.CVE, common.PageInfo, error) GetCVEListForImage(repo, tag string, searchedCVE string, pageinput cvemodel.PageInput,
GetCVESummaryForImage(repo, tag string) (ImageCVESummary, error) ) ([]cvemodel.CVE, zcommon.PageInfo, error)
GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error)
GetCVESummaryForImageMedia(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error)
CompareSeverities(severity1, severity2 string) int CompareSeverities(severity1, severity2 string) int
UpdateDB() error UpdateDB() error
} }
type Scanner interface { type Scanner interface {
ScanImage(image string) (map[string]cvemodel.CVE, error) ScanImage(image string) (map[string]cvemodel.CVE, error)
IsImageFormatScannable(repo, tag string) (bool, error) IsImageFormatScannable(repo, ref string) (bool, error)
IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error)
CompareSeverities(severity1, severity2 string) int CompareSeverities(severity1, severity2 string) int
UpdateDB() error UpdateDB() error
} }
type ImageCVESummary struct {
Count int
MaxSeverity string
}
type BaseCveInfo struct { type BaseCveInfo struct {
Log log.Logger Log log.Logger
Scanner Scanner Scanner Scanner
@ -70,19 +67,19 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta
for tag, descriptor := range repoMeta.Tags { for tag, descriptor := range repoMeta.Tags {
switch descriptor.MediaType { switch descriptor.MediaType {
case ispec.MediaTypeImageManifest: case ispec.MediaTypeImageManifest, ispec.MediaTypeImageIndex:
manifestDigestStr := descriptor.Digest manifestDigestStr := descriptor.Digest
manifestDigest := godigest.Digest(manifestDigestStr) manifestDigest := godigest.Digest(manifestDigestStr)
isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr)
if !isScanableImage || err != nil { if !isScanableImage || err != nil {
cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable") cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable")
continue continue
} }
cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag)) cveMap, err := cveinfo.Scanner.ScanImage(zcommon.GetFullImageName(repo, tag))
if err != nil { if err != nil {
cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed") cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed")
@ -91,7 +88,7 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta
if _, hasCVE := cveMap[cveID]; hasCVE { if _, hasCVE := cveMap[cveID]; hasCVE {
imgList = append(imgList, cvemodel.TagInfo{ imgList = append(imgList, cvemodel.TagInfo{
Name: tag, Tag: tag,
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: manifestDigest, Digest: manifestDigest,
MediaType: descriptor.MediaType, MediaType: descriptor.MediaType,
@ -118,87 +115,81 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
vulnerableTags := make([]cvemodel.TagInfo, 0) vulnerableTags := make([]cvemodel.TagInfo, 0)
allTags := make([]cvemodel.TagInfo, 0) allTags := make([]cvemodel.TagInfo, 0)
var hasCVE bool
for tag, descriptor := range repoMeta.Tags { for tag, descriptor := range repoMeta.Tags {
manifestDigestStr := descriptor.Digest
switch descriptor.MediaType { switch descriptor.MediaType {
case ispec.MediaTypeImageManifest: case ispec.MediaTypeImageManifest:
manifestDigest, err := godigest.Parse(manifestDigestStr) manifestDigestStr := descriptor.Digest
tagInfo, err := getTagInfoForManifest(tag, manifestDigestStr, cveinfo.RepoDB)
if err != nil { if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag). cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Str("digest", manifestDigestStr).Msg("unable to parse digest") Str("cve-id", cveID).Msg("unable to retrieve manifest and config")
continue continue
} }
manifestMeta, err := cveinfo.RepoDB.GetManifestMeta(repo, manifestDigest)
if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Msg("unable to obtain manifest meta")
continue
}
var configContent ispec.Image
err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent)
if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Msg("unable to unmashal manifest blob")
continue
}
tagInfo := cvemodel.TagInfo{
Name: tag,
Timestamp: common.GetImageLastUpdated(configContent),
Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: descriptor.MediaType},
}
allTags = append(allTags, tagInfo) allTags = append(allTags, tagInfo)
image := fmt.Sprintf("%s:%s", repo, tag) if cveinfo.isManifestVulnerable(repo, tag, manifestDigestStr, cveID) {
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag)
if !isValidImage || err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("image media type not supported for scanning, adding as a vulnerable image")
vulnerableTags = append(vulnerableTags, tagInfo) vulnerableTags = append(vulnerableTags, tagInfo)
continue
} }
case ispec.MediaTypeImageIndex:
indexDigestStr := descriptor.Digest
cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag)) indexContent, err := getIndexContent(cveinfo.RepoDB, indexDigestStr)
if err != nil { if err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("scanning failed, adding as a vulnerable image")
vulnerableTags = append(vulnerableTags, tagInfo)
continue continue
} }
hasCVE = false vulnerableManifests := []cvemodel.DescriptorInfo{}
allManifests := []cvemodel.DescriptorInfo{}
for id := range cveMap { for _, manifest := range indexContent.Manifests {
if id == cveID { tagInfo, err := getTagInfoForManifest(tag, manifest.Digest.String(), cveinfo.RepoDB)
hasCVE = true if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Msg("unable to retrieve manifest and config")
break continue
}
manifestDescriptorInfo := cvemodel.DescriptorInfo{
Descriptor: tagInfo.Descriptor,
Timestamp: tagInfo.Timestamp,
}
allManifests = append(allManifests, manifestDescriptorInfo)
if cveinfo.isManifestVulnerable(repo, tag, manifest.Digest.String(), cveID) {
vulnerableManifests = append(vulnerableManifests, manifestDescriptorInfo)
} }
} }
if hasCVE { if len(allManifests) > 0 {
vulnerableTags = append(vulnerableTags, tagInfo) allTags = append(allTags, cvemodel.TagInfo{
Tag: tag,
Descriptor: cvemodel.Descriptor{
Digest: godigest.Digest(indexDigestStr),
MediaType: ispec.MediaTypeImageIndex,
},
Manifests: allManifests,
Timestamp: mostRecentUpdate(allManifests),
})
}
if len(vulnerableManifests) > 0 {
vulnerableTags = append(vulnerableTags, cvemodel.TagInfo{
Tag: tag,
Descriptor: cvemodel.Descriptor{
Digest: godigest.Digest(indexDigestStr),
MediaType: ispec.MediaTypeImageIndex,
},
Manifests: vulnerableManifests,
Timestamp: mostRecentUpdate(vulnerableManifests),
})
} }
default: default:
cveinfo.Log.Error().Str("mediaType", descriptor.MediaType).Msg("media type not supported") cveinfo.Log.Error().Str("mediaType", descriptor.MediaType).Msg("media type not supported")
return []cvemodel.TagInfo{},
fmt.Errorf("media type '%s' is not supported: %w", descriptor.MediaType, errors.ErrNotImplemented)
} }
} }
@ -219,6 +210,117 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
return fixedTags, nil return fixedTags, nil
} }
func mostRecentUpdate(allManifests []cvemodel.DescriptorInfo) time.Time {
if len(allManifests) == 0 {
return time.Time{}
}
timeStamp := allManifests[0].Timestamp
for i := range allManifests {
if timeStamp.Before(allManifests[i].Timestamp) {
timeStamp = allManifests[i].Timestamp
}
}
return timeStamp
}
func getTagInfoForManifest(tag, manifestDigestStr string, repoDB repodb.RepoDB) (cvemodel.TagInfo, error) {
configContent, manifestDigest, err := getConfigAndDigest(repoDB, manifestDigestStr)
if err != nil {
return cvemodel.TagInfo{}, err
}
lastUpdated := zcommon.GetImageLastUpdated(configContent)
return cvemodel.TagInfo{
Tag: tag,
Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
Manifests: []cvemodel.DescriptorInfo{
{
Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
Timestamp: lastUpdated,
},
},
Timestamp: lastUpdated,
}, nil
}
func (cveinfo *BaseCveInfo) isManifestVulnerable(repo, tag, manifestDigestStr, cveID string) bool {
image := zcommon.GetFullImageName(repo, tag)
isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
if !isValidImage || err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("image media type not supported for scanning, adding as a vulnerable image")
return true
}
cveMap, err := cveinfo.Scanner.ScanImage(zcommon.GetFullImageName(repo, manifestDigestStr))
if err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("scanning failed, adding as a vulnerable image")
return true
}
hasCVE := false
for id := range cveMap {
if id == cveID {
hasCVE = true
break
}
}
return hasCVE
}
func getIndexContent(repoDB repodb.RepoDB, indexDigestStr string) (ispec.Index, error) {
indexDigest, err := godigest.Parse(indexDigestStr)
if err != nil {
return ispec.Index{}, err
}
indexData, err := repoDB.GetIndexData(indexDigest)
if err != nil {
return ispec.Index{}, err
}
var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return ispec.Index{}, err
}
return indexContent, nil
}
func getConfigAndDigest(repoDB repodb.RepoDB, manifestDigestStr string) (ispec.Image, godigest.Digest, error) {
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return ispec.Image{}, "", err
}
manifestData, err := repoDB.GetManifestData(manifestDigest)
if err != nil {
return ispec.Image{}, "", err
}
var configContent ispec.Image
err = json.Unmarshal(manifestData.ConfigBlob, &configContent)
if err != nil {
return ispec.Image{}, "", err
}
return configContent, manifestDigest, nil
}
func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) { func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) {
searchedCVE = strings.ToUpper(searchedCVE) searchedCVE = strings.ToUpper(searchedCVE)
@ -230,26 +332,26 @@ func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinde
} }
} }
func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, searchedCVE string, pageInput PageInput) ( func (cveinfo BaseCveInfo) GetCVEListForImage(repo, ref string, searchedCVE string, pageInput cvemodel.PageInput) (
[]cvemodel.CVE, []cvemodel.CVE,
common.PageInfo, zcommon.PageInfo,
error, error,
) { ) {
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
if !isValidImage { if !isValidImage {
return []cvemodel.CVE{}, common.PageInfo{}, err return []cvemodel.CVE{}, zcommon.PageInfo{}, err
} }
image := getImageString(repo, tag) image := zcommon.GetFullImageName(repo, ref)
cveMap, err := cveinfo.Scanner.ScanImage(image) cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil { if err != nil {
return []cvemodel.CVE{}, common.PageInfo{}, err return []cvemodel.CVE{}, zcommon.PageInfo{}, err
} }
pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy, cveinfo) pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy, cveinfo)
if err != nil { if err != nil {
return []cvemodel.CVE{}, common.PageInfo{}, err return []cvemodel.CVE{}, zcommon.PageInfo{}, err
} }
filterCVEList(cveMap, searchedCVE, pageFinder) filterCVEList(cveMap, searchedCVE, pageFinder)
@ -259,23 +361,22 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, searchedCVE stri
return cveList, pageInfo, nil return cveList, pageInfo, nil
} }
func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string, func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error) {
) (ImageCVESummary, error) {
// There are several cases, expected returned values below: // There are several cases, expected returned values below:
// not scannable / error during scan - max severity "" - cve count 0 - Errors // not scannable / error during scan - max severity "" - cve count 0 - Errors
// scannable no issues found - max severity "NONE" - cve count 0 - no Errors // scannable no issues found - max severity "NONE" - cve count 0 - no Errors
// scannable issues found - max severity from Scanner - cve count >0 - no Errors // scannable issues found - max severity from Scanner - cve count >0 - no Errors
imageCVESummary := ImageCVESummary{ imageCVESummary := cvemodel.ImageCVESummary{
Count: 0, Count: 0,
MaxSeverity: "", MaxSeverity: "",
} }
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
if !isValidImage { if !isValidImage {
return imageCVESummary, err return imageCVESummary, err
} }
image := getImageString(repo, tag) image := zcommon.GetFullImageName(repo, ref)
cveMap, err := cveinfo.Scanner.ScanImage(image) cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil { if err != nil {
@ -300,20 +401,41 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string,
return imageCVESummary, nil return imageCVESummary, nil
} }
func referenceIsDigest(reference string) bool { func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType string,
_, err := godigest.Parse(reference) ) (cvemodel.ImageCVESummary, error) {
imageCVESummary := cvemodel.ImageCVESummary{
return err == nil Count: 0,
} MaxSeverity: "",
func getImageString(repo, reference string) string {
image := repo + ":" + reference
if referenceIsDigest(reference) {
image = repo + "@" + reference
} }
return image isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType)
if !isValidImage {
return imageCVESummary, err
}
image := repo + "@" + digest
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
return imageCVESummary, err
}
imageCVESummary.Count = len(cveMap)
if imageCVESummary.Count == 0 {
imageCVESummary.MaxSeverity = "NONE"
return imageCVESummary, nil
}
imageCVESummary.MaxSeverity = "UNKNOWN"
for _, cve := range cveMap {
if cveinfo.Scanner.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
imageCVESummary.MaxSeverity = cve.Severity
}
}
return imageCVESummary, nil
} }
func (cveinfo BaseCveInfo) UpdateDB() error { func (cveinfo BaseCveInfo) UpdateDB() error {
@ -333,10 +455,21 @@ func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo
vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags)) vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags))
for _, tag := range vulnerableTags { for _, tag := range vulnerableTags {
vulnerableTagMap[tag.Name] = tag vulnerableTagMap[tag.Tag] = tag
if tag.Timestamp.Before(earliestVulnerable.Timestamp) { switch tag.Descriptor.MediaType {
earliestVulnerable = tag case ispec.MediaTypeImageManifest:
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
earliestVulnerable = tag
}
case ispec.MediaTypeImageIndex:
for _, manifestDesc := range tag.Manifests {
if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) {
earliestVulnerable = tag
}
}
default:
continue
} }
} }
@ -348,18 +481,62 @@ func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo
// There may be older images which have a fix or // There may be older images which have a fix or
// newer images which don't // newer images which don't
for _, tag := range allTags { for _, tag := range allTags {
if tag.Timestamp.Before(earliestVulnerable.Timestamp) { switch tag.Descriptor.MediaType {
// The vulnerability did not exist at the time this case ispec.MediaTypeImageManifest:
// image was built if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
// The vulnerability did not exist at the time this
// image was built
continue
}
// If the image is old enough for the vulnerability to
// exist, but it was not detected, it means it contains
// the fix
if _, ok := vulnerableTagMap[tag.Tag]; !ok {
fixedTags = append(fixedTags, tag)
}
case ispec.MediaTypeImageIndex:
fixedManifests := []cvemodel.DescriptorInfo{}
// If the latest update inside the index is before the earliest vulnerability found then
// the index can't contain a fix
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
continue
}
vulnTagInfo, indexHasVulnerableManifest := vulnerableTagMap[tag.Tag]
for _, manifestDesc := range tag.Manifests {
if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) {
// The vulnerability did not exist at the time this image was built
continue
}
// check if the current manifest doesn't have the vulnerability
if !indexHasVulnerableManifest || !containsDescriptorInfo(vulnTagInfo.Manifests, manifestDesc) {
fixedManifests = append(fixedManifests, manifestDesc)
}
}
if len(fixedManifests) > 0 {
fixedTag := tag
fixedTag.Manifests = fixedManifests
fixedTags = append(fixedTags, fixedTag)
}
default:
continue continue
} }
// If the image is old enough for the vulnerability to
// exist, but it was not detected, it means it contains
// the fix
if _, ok := vulnerableTagMap[tag.Name]; !ok {
fixedTags = append(fixedTags, tag)
}
} }
return fixedTags return fixedTags
} }
func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvemodel.DescriptorInfo) bool {
for _, di := range slice {
if di.Digest == descriptorInfo.Digest {
return true
}
}
return false
}

View file

@ -0,0 +1,65 @@
package cveinfo
import (
"testing"
"time"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
)
func TestUtils(t *testing.T) {
Convey("Utils", t, func() {
Convey("mostRecentUpdate", func() {
// empty
timestamp := mostRecentUpdate([]cvemodel.DescriptorInfo{})
So(timestamp, ShouldResemble, time.Time{})
timestamp = mostRecentUpdate([]cvemodel.DescriptorInfo{
{
Timestamp: time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
Timestamp: time.Date(2005, 1, 1, 1, 1, 1, 1, time.UTC),
},
})
So(timestamp, ShouldResemble, time.Date(2005, 1, 1, 1, 1, 1, 1, time.UTC))
})
Convey("GetFixedTags", func() {
tags := GetFixedTags(
[]cvemodel.TagInfo{
{},
},
[]cvemodel.TagInfo{
{
Descriptor: cvemodel.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
},
Timestamp: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
Descriptor: cvemodel.Descriptor{
MediaType: ispec.MediaTypeImageIndex,
},
Manifests: []cvemodel.DescriptorInfo{
{
Timestamp: time.Date(2002, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
Timestamp: time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC),
},
},
},
{
Descriptor: cvemodel.Descriptor{
MediaType: "bad Type",
},
},
})
So(tags, ShouldBeEmpty)
})
})
}

View file

@ -8,6 +8,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/url"
"os" "os"
"path" "path"
"strings" "strings"
@ -25,10 +26,12 @@ import (
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/api/constants"
apiErr "zotregistry.io/zot/pkg/api/errors" apiErr "zotregistry.io/zot/pkg/api/errors"
zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config" extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/meta/repodb"
@ -382,10 +385,16 @@ func TestImageFormat(t *testing.T) {
GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{ return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{ Tags: map[string]repodb.Descriptor{
"tag": {MediaType: ispec.MediaTypeImageIndex}, "tag": {
MediaType: ispec.MediaTypeImageIndex,
Digest: godigest.FromString("digest").String(),
},
}, },
}, nil }, nil
}, },
GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`{}`)}, nil
},
} }
storeController := storage.StoreController{ storeController := storage.StoreController{
DefaultStore: mocks.MockedImageStore{}, DefaultStore: mocks.MockedImageStore{},
@ -395,7 +404,7 @@ func TestImageFormat(t *testing.T) {
isScanable, err := cveInfo.Scanner.IsImageFormatScannable("repo", "tag") isScanable, err := cveInfo.Scanner.IsImageFormatScannable("repo", "tag")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(isScanable, ShouldBeFalse) So(isScanable, ShouldBeTrue)
}) })
} }
@ -1024,8 +1033,11 @@ func TestCVEStruct(t *testing.T) {
// Setup test CVE data in mock scanner // Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{ scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
repo1 := "repo1"
repo, ref, _ := zcommon.GetImageDirAndReference(image)
// Images in chronological order // Images in chronological order
if image == "repo1:0.1.0" { if image == "repo1:0.1.0" || ref == digest11.String() {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE1": { "CVE1": {
ID: "CVE1", ID: "CVE1",
@ -1036,7 +1048,8 @@ func TestCVEStruct(t *testing.T) {
}, nil }, nil
} }
if image == "repo1:1.0.0" { if image == "repo1:1.0.0" || (repo == repo1 &&
zcommon.Contains([]string{digest12.String(), digest21.String()}, ref)) {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE1": { "CVE1": {
ID: "CVE1", ID: "CVE1",
@ -1059,7 +1072,7 @@ func TestCVEStruct(t *testing.T) {
}, nil }, nil
} }
if image == "repo1:1.1.0" { if image == "repo1:1.1.0" || (repo == repo1 && ref == digest13.String()) {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE3": { "CVE3": {
ID: "CVE3", ID: "CVE3",
@ -1072,7 +1085,7 @@ func TestCVEStruct(t *testing.T) {
// As a minor release on 1.0.0 banch // As a minor release on 1.0.0 banch
// does not include all fixes published in 1.1.0 // does not include all fixes published in 1.1.0
if image == "repo1:1.0.1" { if image == "repo1:1.0.1" || (repo == repo1 && ref == digest14.String()) {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE1": { "CVE1": {
ID: "CVE1", ID: "CVE1",
@ -1089,7 +1102,7 @@ func TestCVEStruct(t *testing.T) {
}, nil }, nil
} }
if image == "repoIndex:tagIndex" { if image == "repoIndex:tagIndex" || (repo == "repoIndex" && ref == indexDigest.String()) {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE1": { "CVE1": {
ID: "CVE1", ID: "CVE1",
@ -1119,12 +1132,20 @@ func TestCVEStruct(t *testing.T) {
return false, err return false, err
} }
manifestDigestStr, ok := repoMeta.Tags[inputTag] manifestDigestStr := reference
if !ok {
return false, zerr.ErrTagMetaNotFound if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
} }
manifestDigest, err := godigest.Parse(manifestDigestStr.Digest) manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -1154,6 +1175,15 @@ func TestCVEStruct(t *testing.T) {
return false, nil return false, nil
}, },
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
if repo == "repo2" {
if digest == digest21.String() {
return false, nil
}
}
return true, nil
},
} }
log := log.NewLogger("debug", "") log := log.NewLogger("debug", "")
@ -1213,7 +1243,7 @@ func TestCVEStruct(t *testing.T) {
t.Log("Test GetCVEListForImage") t.Log("Test GetCVEListForImage")
pageInput := cveinfo.PageInput{ pageInput := cvemodel.PageInput{
SortBy: cveinfo.SeverityDsc, SortBy: cveinfo.SeverityDsc,
} }
@ -1289,14 +1319,14 @@ func TestCVEStruct(t *testing.T) {
tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1") tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 1) So(len(tagList), ShouldEqual, 1)
So(tagList[0].Name, ShouldEqual, "1.1.0") So(tagList[0].Tag, ShouldEqual, "1.1.0")
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE2") tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE2")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 2) So(len(tagList), ShouldEqual, 2)
expectedTags := []string{"1.0.1", "1.1.0"} expectedTags := []string{"1.0.1", "1.1.0"}
So(expectedTags, ShouldContain, tagList[0].Name) So(expectedTags, ShouldContain, tagList[0].Tag)
So(expectedTags, ShouldContain, tagList[1].Name) So(expectedTags, ShouldContain, tagList[1].Tag)
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE3") tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE3")
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -1309,7 +1339,7 @@ func TestCVEStruct(t *testing.T) {
tagList, err = cveInfo.GetImageListWithCVEFixed("repo6", "CVE1") tagList, err = cveInfo.GetImageListWithCVEFixed("repo6", "CVE1")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 1) So(len(tagList), ShouldEqual, 1)
So(tagList[0].Name, ShouldEqual, "1.0.0") So(tagList[0].Tag, ShouldEqual, "1.0.0")
// Image is not scannable // Image is not scannable
tagList, err = cveInfo.GetImageListWithCVEFixed("repo2", "CVE100") tagList, err = cveInfo.GetImageListWithCVEFixed("repo2", "CVE100")
@ -1341,22 +1371,22 @@ func TestCVEStruct(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 3) So(len(tagList), ShouldEqual, 3)
expectedTags = []string{"0.1.0", "1.0.0", "1.0.1"} expectedTags = []string{"0.1.0", "1.0.0", "1.0.1"}
So(expectedTags, ShouldContain, tagList[0].Name) So(expectedTags, ShouldContain, tagList[0].Tag)
So(expectedTags, ShouldContain, tagList[1].Name) So(expectedTags, ShouldContain, tagList[1].Tag)
So(expectedTags, ShouldContain, tagList[2].Name) So(expectedTags, ShouldContain, tagList[2].Tag)
tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE2") tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE2")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 1) So(len(tagList), ShouldEqual, 1)
So(tagList[0].Name, ShouldEqual, "1.0.0") So(tagList[0].Tag, ShouldEqual, "1.0.0")
tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE3") tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE3")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 3) So(len(tagList), ShouldEqual, 3)
expectedTags = []string{"1.0.0", "1.0.1", "1.1.0"} expectedTags = []string{"1.0.0", "1.0.1", "1.1.0"}
So(expectedTags, ShouldContain, tagList[0].Name) So(expectedTags, ShouldContain, tagList[0].Tag)
So(expectedTags, ShouldContain, tagList[1].Name) So(expectedTags, ShouldContain, tagList[1].Tag)
So(expectedTags, ShouldContain, tagList[2].Name) So(expectedTags, ShouldContain, tagList[2].Tag)
// Image/repo doesn't have the CVE at all // Image/repo doesn't have the CVE at all
tagList, err = cveInfo.GetImageListForCVE("repo6", "CVE1") tagList, err = cveInfo.GetImageListForCVE("repo6", "CVE1")
@ -1419,7 +1449,7 @@ func TestCVEStruct(t *testing.T) {
tagList, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1") tagList, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1")
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 0) So(len(tagList), ShouldEqual, 1)
cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{ cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{
IsImageFormatScannableFn: func(repo, reference string) (bool, error) { IsImageFormatScannableFn: func(repo, reference string) (bool, error) {
@ -1448,7 +1478,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
tags := make([]cvemodel.TagInfo, 0) tags := make([]cvemodel.TagInfo, 0)
firstTag := cvemodel.TagInfo{ firstTag := cvemodel.TagInfo{
Name: "1.0.0", Tag: "1.0.0",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -1456,7 +1486,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
secondTag := cvemodel.TagInfo{ secondTag := cvemodel.TagInfo{
Name: "1.0.1", Tag: "1.0.1",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -1464,7 +1494,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
thirdTag := cvemodel.TagInfo{ thirdTag := cvemodel.TagInfo{
Name: "1.0.2", Tag: "1.0.2",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -1472,7 +1502,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
fourthTag := cvemodel.TagInfo{ fourthTag := cvemodel.TagInfo{
Name: "1.0.3", Tag: "1.0.3",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -1496,10 +1526,298 @@ func TestFixedTags(t *testing.T) {
So(len(fixedTags), ShouldEqual, 2) So(len(fixedTags), ShouldEqual, 2)
fixedTags = cveinfo.GetFixedTags(allTags, append(vulnerableTags, cvemodel.TagInfo{ fixedTags = cveinfo.GetFixedTags(allTags, append(vulnerableTags, cvemodel.TagInfo{
Name: "taginfo", Tag: "taginfo",
Descriptor: cvemodel.Descriptor{}, Descriptor: cvemodel.Descriptor{
Timestamp: time.Date(2000, time.July, 20, 10, 10, 10, 10, time.UTC), MediaType: ispec.MediaTypeImageManifest,
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
},
Timestamp: time.Date(2000, time.July, 20, 10, 10, 10, 10, time.UTC),
})) }))
So(len(fixedTags), ShouldEqual, 3) So(len(fixedTags), ShouldEqual, 3)
}) })
} }
func TestFixedTagsWithIndex(t *testing.T) {
Convey("Test fixed tags", t, func() {
tempDir := t.TempDir()
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Storage.RootDirectory = tempDir
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
CVE: &extconf.CVEConfig{
UpdateInterval: 24 * time.Hour,
Trivy: &extconf.TrivyConfig{},
},
},
}
ctlr := api.NewController(conf)
So(ctlr, ShouldNotBeNil)
cm := NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
// push index with 2 manifests: one with vulns and one without
vulnManifestCreated := time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)
vulnManifest, err := GetVulnImageWithConfig("", ispec.Image{
Created: &vulnManifestCreated,
Platform: ispec.Platform{OS: "linux", Architecture: "amd64"},
})
So(err, ShouldBeNil)
vulnDigest, err := vulnManifest.Digest()
So(err, ShouldBeNil)
fixedManifestCreated := time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)
fixedManifest, err := GetImageWithConfig(ispec.Image{
Created: &fixedManifestCreated,
Platform: ispec.Platform{OS: "windows", Architecture: "amd64"},
})
So(err, ShouldBeNil)
fixedDigest, err := fixedManifest.Digest()
So(err, ShouldBeNil)
multiArch := GetMultiarchImageForImages("multi-arch-tag", []Image{fixedManifest, vulnManifest})
multiArchDigest, err := multiArch.Digest()
So(err, ShouldBeNil)
err = UploadMultiarchImage(multiArch, baseURL, "repo")
So(err, ShouldBeNil)
// oldest vulnerability
simpleVulnCreated := time.Date(2005, 1, 1, 1, 1, 1, 1, time.UTC)
simpleVulnImg, err := GetVulnImageWithConfig("vuln-img", ispec.Image{
Created: &simpleVulnCreated,
Platform: ispec.Platform{OS: "windows", Architecture: "amd64"},
})
So(err, ShouldBeNil)
err = UploadImage(simpleVulnImg, baseURL, "repo")
So(err, ShouldBeNil)
scanner := trivy.NewScanner(ctlr.StoreController, ctlr.RepoDB, "ghcr.io/project-zot/trivy-db", "", ctlr.Log)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
cveInfo := cveinfo.NewCVEInfo(ctlr.StoreController, ctlr.RepoDB, "ghcr.io/project-zot/trivy-db", "", ctlr.Log)
tagsInfo, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
So(len(tagsInfo), ShouldEqual, 1)
So(len(tagsInfo[0].Manifests), ShouldEqual, 1)
So(tagsInfo[0].Manifests[0].Digest, ShouldResemble, fixedDigest)
_ = tagsInfo
_ = vulnDigest
_ = multiArchDigest
const query = `
{
ImageListWithCVEFixed(id:"%s",image:"%s"){
Results{
RepoName
Manifests {Digest}
}
}
}`
resp, _ := resty.R().Get(baseURL + constants.FullSearchPrefix + "?query=" +
url.QueryEscape(fmt.Sprintf(query, Vulnerability1ID, "repo")))
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := &zcommon.ImageListWithCVEFixedResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(len(responseStruct.Results[0].Manifests), ShouldEqual, 1)
fixedManifestResp := responseStruct.Results[0].Manifests[0]
So(fixedManifestResp.Digest, ShouldResemble, fixedDigest.String())
})
}
func TestImageListWithCVEFixedErrors(t *testing.T) {
indexDigest := godigest.FromString("index")
manifestDigest := "sha256:1111111111111111111111111111111111111111111111111111111111111111"
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("getIndexContent errors", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: indexDigest.String(),
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrIndexDataNotFount
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getIndexContent bad indexDigest", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: "bad digest",
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrIndexDataNotFount
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getIndexContent bad index content", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: indexDigest.String(),
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`bad index`)}, nil
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getTagInfoForManifest bad manifest digest", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: "bad digest",
MediaType: ispec.MediaTypeImageManifest,
},
},
}, nil
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getTagInfoForManifest fails for index", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: indexDigest.String(),
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{
IndexBlob: []byte(fmt.Sprintf(`{
"manifests": [
{
"digest": "%s",
"mediaType": "application/vnd.oci.image.manifest.v1+json"
}
]}`, manifestDigest)),
}, nil
}
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
return repodb.ManifestData{}, zerr.ErrManifestDataNotFound
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
tagsInfo, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
So(tagsInfo, ShouldBeEmpty)
})
Convey("media type not supported", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: godigest.FromString("media type").String(),
MediaType: "bad media type",
},
},
}, nil
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
tagsInfo, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
So(tagsInfo, ShouldBeEmpty)
})
})
}
func TestGetCVESummaryForImageMediaErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("IsImageMediaScannable returns false", func() {
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
cveInfo.Scanner = mocks.CveScannerMock{
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
return false, zerr.ErrScanNotSupported
},
}
_, err := cveInfo.GetCVESummaryForImageMedia("repo", "digest", ispec.MediaTypeImageManifest)
So(err, ShouldNotBeNil)
})
Convey("Scan fails", func() {
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
cveInfo.Scanner = mocks.CveScannerMock{
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
return true, nil
},
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
return nil, zerr.ErrScanNotSupported
},
}
_, err := cveInfo.GetCVESummaryForImageMedia("repo", "digest", ispec.MediaTypeImageManifest)
So(err, ShouldNotBeNil)
})
})
}

View file

@ -6,6 +6,11 @@ import (
godigest "github.com/opencontainers/go-digest" godigest "github.com/opencontainers/go-digest"
) )
type ImageCVESummary struct {
Count int
MaxSeverity string
}
//nolint:tagliatelle // graphQL schema //nolint:tagliatelle // graphQL schema
type CVE struct { type CVE struct {
ID string `json:"Id"` ID string `json:"Id"`
@ -47,8 +52,15 @@ type Descriptor struct {
MediaType string MediaType string
} }
type DescriptorInfo struct {
Descriptor
Timestamp time.Time
}
type TagInfo struct { type TagInfo struct {
Name string Tag string
Descriptor Descriptor Descriptor Descriptor
Manifests []DescriptorInfo
Timestamp time.Time Timestamp time.Time
} }

View file

@ -0,0 +1,9 @@
package model
type SortCriteria string
type PageInput struct {
Limit int
Offset int
SortBy SortCriteria
}

View file

@ -9,16 +9,14 @@ import (
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
) )
type SortCriteria string
const ( const (
AlphabeticAsc = SortCriteria("ALPHABETIC_ASC") AlphabeticAsc = cvemodel.SortCriteria("ALPHABETIC_ASC")
AlphabeticDsc = SortCriteria("ALPHABETIC_DSC") AlphabeticDsc = cvemodel.SortCriteria("ALPHABETIC_DSC")
SeverityDsc = SortCriteria("SEVERITY") SeverityDsc = cvemodel.SortCriteria("SEVERITY")
) )
func SortFunctions() map[SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool { func SortFunctions() map[cvemodel.SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return map[SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool{ return map[cvemodel.SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool{
AlphabeticAsc: SortByAlphabeticAsc, AlphabeticAsc: SortByAlphabeticAsc,
AlphabeticDsc: SortByAlphabeticDsc, AlphabeticDsc: SortByAlphabeticDsc,
SeverityDsc: SortBySeverity, SeverityDsc: SortBySeverity,
@ -56,12 +54,12 @@ type PageFinder interface {
type CvePageFinder struct { type CvePageFinder struct {
limit int limit int
offset int offset int
sortBy SortCriteria sortBy cvemodel.SortCriteria
pageBuffer []cvemodel.CVE pageBuffer []cvemodel.CVE
cveInfo CveInfo cveInfo CveInfo
} }
func NewCvePageFinder(limit, offset int, sortBy SortCriteria, cveInfo CveInfo) (*CvePageFinder, error) { func NewCvePageFinder(limit, offset int, sortBy cvemodel.SortCriteria, cveInfo CveInfo) (*CvePageFinder, error) {
if sortBy == "" { if sortBy == "" {
sortBy = SeverityDsc sortBy = SeverityDsc
} }
@ -131,9 +129,3 @@ func (bpt *CvePageFinder) Page() ([]cvemodel.CVE, common.PageInfo) {
return cves, *pageInfo return cves, *pageInfo
} }
type PageInput struct {
Limit int
Offset int
SortBy SortCriteria
}

View file

@ -187,7 +187,7 @@ func TestCVEPagination(t *testing.T) {
Convey("Page", func() { Convey("Page", func() {
Convey("defaults", func() { Convey("defaults", func() {
// By default expect unlimitted results sorted by severity // By default expect unlimitted results sorted by severity
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{}) cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5) So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5)
@ -198,7 +198,7 @@ func TestCVEPagination(t *testing.T) {
previousSeverity = severityToInt[cve.Severity] previousSeverity = severityToInt[cve.Severity]
} }
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cveinfo.PageInput{}) cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cvemodel.PageInput{})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30) So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30)
@ -217,7 +217,7 @@ func TestCVEPagination(t *testing.T) {
} }
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "",
cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5) So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5)
@ -228,7 +228,7 @@ func TestCVEPagination(t *testing.T) {
sort.Strings(cveIds) sort.Strings(cveIds)
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "",
cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30) So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30)
@ -239,7 +239,7 @@ func TestCVEPagination(t *testing.T) {
sort.Sort(sort.Reverse(sort.StringSlice(cveIds))) sort.Sort(sort.Reverse(sort.StringSlice(cveIds)))
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "",
cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc}) cvemodel.PageInput{SortBy: cveinfo.AlphabeticDsc})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30) So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30)
@ -249,7 +249,7 @@ func TestCVEPagination(t *testing.T) {
} }
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "",
cveinfo.PageInput{SortBy: cveinfo.SeverityDsc}) cvemodel.PageInput{SortBy: cveinfo.SeverityDsc})
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30) So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30)
@ -267,7 +267,7 @@ func TestCVEPagination(t *testing.T) {
cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) cveIds = append(cveIds, fmt.Sprintf("CVE%d", i))
} }
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 3, Limit: 3,
Offset: 1, Offset: 1,
SortBy: cveinfo.AlphabeticAsc, SortBy: cveinfo.AlphabeticAsc,
@ -281,7 +281,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[1].ID, ShouldEqual, "CVE2") So(cves[1].ID, ShouldEqual, "CVE2")
So(cves[2].ID, ShouldEqual, "CVE3") So(cves[2].ID, ShouldEqual, "CVE3")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 2, Limit: 2,
Offset: 1, Offset: 1,
SortBy: cveinfo.AlphabeticDsc, SortBy: cveinfo.AlphabeticDsc,
@ -294,7 +294,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[0].ID, ShouldEqual, "CVE3") So(cves[0].ID, ShouldEqual, "CVE3")
So(cves[1].ID, ShouldEqual, "CVE2") So(cves[1].ID, ShouldEqual, "CVE2")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 3, Limit: 3,
Offset: 1, Offset: 1,
SortBy: cveinfo.SeverityDsc, SortBy: cveinfo.SeverityDsc,
@ -311,7 +311,7 @@ func TestCVEPagination(t *testing.T) {
} }
sort.Strings(cveIds) sort.Strings(cveIds)
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cveinfo.PageInput{ cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cvemodel.PageInput{
Limit: 5, Limit: 5,
Offset: 20, Offset: 20,
SortBy: cveinfo.AlphabeticAsc, SortBy: cveinfo.AlphabeticAsc,
@ -327,7 +327,7 @@ func TestCVEPagination(t *testing.T) {
}) })
Convey("limit > len(cves)", func() { Convey("limit > len(cves)", func() {
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 6, Limit: 6,
Offset: 3, Offset: 3,
SortBy: cveinfo.AlphabeticAsc, SortBy: cveinfo.AlphabeticAsc,
@ -340,7 +340,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[0].ID, ShouldEqual, "CVE3") So(cves[0].ID, ShouldEqual, "CVE3")
So(cves[1].ID, ShouldEqual, "CVE4") So(cves[1].ID, ShouldEqual, "CVE4")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 6, Limit: 6,
Offset: 3, Offset: 3,
SortBy: cveinfo.AlphabeticDsc, SortBy: cveinfo.AlphabeticDsc,
@ -353,7 +353,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[0].ID, ShouldEqual, "CVE1") So(cves[0].ID, ShouldEqual, "CVE1")
So(cves[1].ID, ShouldEqual, "CVE0") So(cves[1].ID, ShouldEqual, "CVE0")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 6, Limit: 6,
Offset: 3, Offset: 3,
SortBy: cveinfo.SeverityDsc, SortBy: cveinfo.SeverityDsc,

View file

@ -22,6 +22,7 @@ import (
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/meta/repodb"
@ -181,54 +182,61 @@ func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) {
return report, nil return report, nil
} }
func (scanner Scanner) IsImageFormatScannable(repo, tag string) (bool, error) { func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) {
image := repo + ":" + tag var (
digestStr = ref
mediaType string
)
if scanner.cache.Get(image) != nil { if zcommon.IsTag(ref) {
return true, nil imgDescriptor, err := repodb.GetImageDescriptor(scanner.repoDB, repo, ref)
if err != nil {
return false, err
}
digestStr = imgDescriptor.Digest
mediaType = imgDescriptor.MediaType
} else {
var found bool
found, mediaType = repodb.FindMediaTypeForDigest(scanner.repoDB, godigest.Digest(ref))
if !found {
return false, zerr.ErrManifestNotFound
}
} }
repoMeta, err := scanner.repoDB.GetRepoMeta(repo) return scanner.IsImageMediaScannable(repo, digestStr, mediaType)
if err != nil { }
return false, err
}
var ok bool func (scanner Scanner) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) {
image := repo + "@" + digestStr
imageDescriptor, ok := repoMeta.Tags[tag] switch mediaType {
if !ok {
return false, zerr.ErrTagMetaNotFound
}
switch imageDescriptor.MediaType {
case ispec.MediaTypeImageManifest: case ispec.MediaTypeImageManifest:
ok, err := scanner.isManifestScanable(imageDescriptor) ok, err := scanner.isManifestScanable(digestStr)
if err != nil { if err != nil {
return ok, fmt.Errorf("image '%s' %w", image, err) return ok, fmt.Errorf("image '%s' %w", image, err)
} }
return ok, nil return ok, nil
case ispec.MediaTypeImageIndex: case ispec.MediaTypeImageIndex:
ok, err := scanner.isIndexScanable(imageDescriptor) ok, err := scanner.isIndexScanable(digestStr)
if err != nil { if err != nil {
return ok, fmt.Errorf("image '%s' %w", image, err) return ok, fmt.Errorf("image '%s' %w", image, err)
} }
return ok, nil return ok, nil
default:
return false, nil
} }
return false, nil
} }
func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, error) { func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) {
manifestDigestStr := descriptor.Digest if scanner.cache.Get(digestStr) != nil {
return true, nil
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return false, err
} }
manifestData, err := scanner.repoDB.GetManifestData(manifestDigest) manifestData, err := scanner.repoDB.GetManifestData(godigest.Digest(digestStr))
if err != nil { if err != nil {
return false, err return false, err
} }
@ -257,18 +265,98 @@ func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, e
return true, nil return true, nil
} }
func (scanner Scanner) isIndexScanable(descriptor repodb.Descriptor) (bool, error) { func (scanner Scanner) isIndexScanable(digestStr string) (bool, error) {
if scanner.cache.Get(digestStr) != nil {
return true, nil
}
indexData, err := scanner.repoDB.GetIndexData(godigest.Digest(digestStr))
if err != nil {
return false, err
}
var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return false, err
}
if len(indexContent.Manifests) == 0 {
return true, nil
}
for _, manifest := range indexContent.Manifests {
isScannable, err := scanner.isManifestScanable(manifest.Digest.String())
if err != nil {
continue
}
// if at least 1 manifest is scanable, the whole index is scanable
if isScannable {
return true, nil
}
}
return false, nil return false, nil
} }
func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) { func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) {
if scanner.cache.Get(image) != nil { var (
return scanner.cache.Get(image), nil originalImageInput = image
digest string
mediaType string
)
repo, ref, isTag := zcommon.GetImageDirAndReference(image)
digest = ref
if isTag {
imgDescriptor, err := repodb.GetImageDescriptor(scanner.repoDB, repo, ref)
if err != nil {
return map[string]cvemodel.CVE{}, err
}
digest = imgDescriptor.Digest
mediaType = imgDescriptor.MediaType
} else {
var found bool
found, mediaType = repodb.FindMediaTypeForDigest(scanner.repoDB, godigest.Digest(ref))
if !found {
return map[string]cvemodel.CVE{}, zerr.ErrManifestNotFound
}
} }
cveidMap := make(map[string]cvemodel.CVE) var (
cveIDMap map[string]cvemodel.CVE
err error
)
scanner.log.Debug().Str("image", image).Msg("scanning image") switch mediaType {
case ispec.MediaTypeImageIndex:
cveIDMap, err = scanner.scanIndex(repo, digest)
default:
cveIDMap, err = scanner.scanManifest(repo, digest)
}
if err != nil {
scanner.log.Error().Err(err).Str("image", originalImageInput).Msg("unable to scan image")
return map[string]cvemodel.CVE{}, err
}
return cveIDMap, nil
}
func (scanner Scanner) scanManifest(repo, digest string) (map[string]cvemodel.CVE, error) {
if cachedMap := scanner.cache.Get(digest); cachedMap != nil {
return cachedMap, nil
}
cveidMap := map[string]cvemodel.CVE{}
image := repo + "@" + digest
scanner.dbLock.Lock() scanner.dbLock.Lock()
opts := scanner.getTrivyOptions(image) opts := scanner.getTrivyOptions(image)
@ -276,8 +364,6 @@ func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error)
scanner.dbLock.Unlock() scanner.dbLock.Unlock()
if err != nil { //nolint: wsl if err != nil { //nolint: wsl
scanner.log.Error().Err(err).Str("image", image).Msg("unable to scan image")
return cveidMap, err return cveidMap, err
} }
@ -335,11 +421,42 @@ func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error)
} }
} }
scanner.cache.Add(image, cveidMap) scanner.cache.Add(digest, cveidMap)
return cveidMap, nil return cveidMap, nil
} }
func (scanner Scanner) scanIndex(repo, digest string) (map[string]cvemodel.CVE, error) {
indexData, err := scanner.repoDB.GetIndexData(godigest.Digest(digest))
if err != nil {
return map[string]cvemodel.CVE{}, err
}
var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return map[string]cvemodel.CVE{}, err
}
indexCveIDMap := map[string]cvemodel.CVE{}
for _, manifest := range indexContent.Manifests {
if isScannable, err := scanner.isManifestScanable(manifest.Digest.String()); isScannable && err == nil {
manifestCveIDMap, err := scanner.scanManifest(repo, manifest.Digest.String())
if err != nil {
return nil, err
}
for vulnerabilityID, CVE := range manifestCveIDMap {
indexCveIDMap[vulnerabilityID] = CVE
}
}
}
return indexCveIDMap, nil
}
// UpdateDB downloads the Trivy DB / Cache under the store root directory. // UpdateDB downloads the Trivy DB / Cache under the store root directory.
func (scanner Scanner) UpdateDB() error { func (scanner Scanner) UpdateDB() error {
// We need a lock as using multiple substores each with it's own DB // We need a lock as using multiple substores each with it's own DB

View file

@ -18,6 +18,7 @@ import (
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/meta/repodb"
@ -27,6 +28,7 @@ import (
"zotregistry.io/zot/pkg/storage/local" "zotregistry.io/zot/pkg/storage/local"
storageTypes "zotregistry.io/zot/pkg/storage/types" storageTypes "zotregistry.io/zot/pkg/storage/types"
"zotregistry.io/zot/pkg/test" "zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
) )
func generateTestImage(storeController storage.StoreController, image string) { func generateTestImage(storeController storage.StoreController, image string) {
@ -100,9 +102,6 @@ func TestMultipleStoragePath(t *testing.T) {
repoDB, err := boltdb_wrapper.NewBoltDBWrapper(boltDriver, log) repoDB, err := boltdb_wrapper.NewBoltDBWrapper(boltDriver, log)
So(err, ShouldBeNil) So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil)
scanner := NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", "", log) scanner := NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", "", log)
So(scanner.storeController.DefaultStore, ShouldNotBeNil) So(scanner.storeController.DefaultStore, ShouldNotBeNil)
@ -125,6 +124,9 @@ func TestMultipleStoragePath(t *testing.T) {
generateTestImage(storeController, img1) generateTestImage(storeController, img1)
generateTestImage(storeController, img2) generateTestImage(storeController, img2)
err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil)
// Try to scan without the DB being downloaded // Try to scan without the DB being downloaded
_, err = scanner.ScanImage(img0) _, err = scanner.ScanImage(img0)
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
@ -508,3 +510,138 @@ func TestDefaultTrivyDBUrl(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
} }
func TestIsIndexScanable(t *testing.T) {
Convey("IsIndexScanable", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = &local.ImageStoreLocal{}
repoDB := &boltdb_wrapper.DBWrapper{}
log := log.NewLogger("debug", "")
Convey("Find index in cache", func() {
scanner := NewScanner(storeController, repoDB, "", "", log)
scanner.cache.Add("digest", make(map[string]model.CVE))
found, err := scanner.isIndexScanable("digest")
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
})
}
func TestScanIndexErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("GetIndexData fails", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, godigest.ErrDigestUnsupported
}
scanner := NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.scanIndex("repo", "digest")
So(err, ShouldNotBeNil)
})
Convey("Bad Index Blob, Unamrshal fails", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{
IndexBlob: []byte(`bad-blob`),
}, nil
}
scanner := NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.scanIndex("repo", "digest")
So(err, ShouldNotBeNil)
})
})
}
func TestIsIndexScanableErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("GetIndexData errors", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrManifestDataNotFound
}
scanner := NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.isIndexScanable("digest")
So(err, ShouldNotBeNil)
})
Convey("bad index data, can't unmarshal", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`bad`)}, nil
}
scanner := NewScanner(storeController, repoDB, "", "", log)
ok, err := scanner.isIndexScanable("digest")
So(err, ShouldNotBeNil)
So(ok, ShouldBeFalse)
})
Convey("is Manifest Scanable errors", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`{
"manifests": [{
"digest": "digest2"
},
{
"digest": "digest1"
}
]
}`)}, nil
}
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
switch manifestDigest {
case "digest1":
return repodb.ManifestData{
ManifestBlob: []byte("{}"),
}, nil
case "digest2":
return repodb.ManifestData{}, zerr.ErrBadBlob
}
return repodb.ManifestData{}, nil
}
scanner := NewScanner(storeController, repoDB, "", "", log)
ok, err := scanner.isIndexScanable("digest")
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
})
Convey("is Manifest Scanable returns false because no manifest is scanable", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`{
"manifests": [{
"digest": "digest2"
}
]
}`)}, nil
}
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
return repodb.ManifestData{}, zerr.ErrBadBlob
}
scanner := NewScanner(storeController, repoDB, "", "", log)
ok, err := scanner.isIndexScanable("digest")
So(err, ShouldBeNil)
So(ok, ShouldBeFalse)
})
})
}

View file

@ -0,0 +1,186 @@
package trivy_test
import (
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb"
boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
)
func TestScanningByDigest(t *testing.T) {
Convey("Scan the individual manifests inside an index", t, func() {
// start server
tempDir := t.TempDir()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Storage.RootDirectory = tempDir
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
So(ctlr, ShouldNotBeNil)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
// push index with 2 manifests: one with vulns and one without
vulnImage, err := test.GetVulnImage("")
So(err, ShouldBeNil)
vulnDigest, err := vulnImage.Digest()
So(err, ShouldBeNil)
simpleImage, err := test.GetRandomImage("")
So(err, ShouldBeNil)
simpleDigest, err := simpleImage.Digest()
So(err, ShouldBeNil)
multiArch := test.GetMultiarchImageForImages("multi-arch-tag", []test.Image{simpleImage, vulnImage})
multiArchDigest, err := multiArch.Digest()
So(err, ShouldBeNil)
err = test.UploadMultiarchImage(multiArch, baseURL, "multi-arch")
So(err, ShouldBeNil)
// scan
scanner := trivy.NewScanner(ctlr.StoreController, ctlr.RepoDB, "ghcr.io/project-zot/trivy-db", "", ctlr.Log)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage("multi-arch@" + vulnDigest.String())
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, test.Vulnerability1ID)
So(cveMap, ShouldContainKey, test.Vulnerability2ID)
cveMap, err = scanner.ScanImage("multi-arch@" + simpleDigest.String())
So(err, ShouldBeNil)
So(cveMap, ShouldBeEmpty)
cveMap, err = scanner.ScanImage("multi-arch@" + multiArchDigest.String())
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, test.Vulnerability1ID)
So(cveMap, ShouldContainKey, test.Vulnerability2ID)
cveMap, err = scanner.ScanImage("multi-arch:multi-arch-tag")
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, test.Vulnerability1ID)
So(cveMap, ShouldContainKey, test.Vulnerability2ID)
})
}
func TestScannerErrors(t *testing.T) {
digest := godigest.FromString("dig")
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("IsImageFormatSanable", func() {
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
return repodb.ManifestData{}, zerr.ErrManifestDataNotFound
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrManifestDataNotFound
}
scanner := trivy.NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.ScanImage("repo@" + digest.String())
So(err, ShouldNotBeNil)
})
})
}
func TestVulnerableLayer(t *testing.T) {
Convey("Vulnerable layer", t, func() {
vulnerableLayer, err := test.GetLayerWithVulnerability(1)
So(err, ShouldBeNil)
created, err := time.Parse(time.RFC3339, "2023-03-29T18:19:24Z")
So(err, ShouldBeNil)
config := ispec.Image{
Created: &created,
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
}
img, err := test.GetImageWithComponents(
config,
[][]byte{
vulnerableLayer,
},
)
So(err, ShouldBeNil)
imgDigest, err := img.Digest()
So(err, ShouldBeNil)
tempDir := t.TempDir()
log := log.NewLogger("debug", "")
imageStore := local.NewImageStore(tempDir, false, 0, false, false,
log, monitoring.NewMetricsServer(false, log), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
err = test.WriteImageToFileSystem(img, "repo", storeController)
So(err, ShouldBeNil)
params := bolt.DBParameters{
RootDir: tempDir,
}
boltDriver, err := bolt.GetBoltDriver(params)
So(err, ShouldBeNil)
repoDB, err := boltdb_wrapper.NewBoltDBWrapper(boltDriver, log)
So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil)
scanner := trivy.NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", "", log)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage("repo@" + imgDigest.String())
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 2)
})
}

View file

@ -1679,7 +1679,7 @@ type Query {
Returns a CVE list for the image specified in the argument Returns a CVE list for the image specified in the argument
""" """
CVEListForImage( CVEListForImage(
"Image name in format ` + "`" + `repository:tag` + "`" + `" "Image name in format ` + "`" + `repository:tag` + "`" + ` or ` + "`" + `repository@digest` + "`" + `"
image: String!, image: String!,
"Sets the parameters of the requested page" "Sets the parameters of the requested page"
requestedPage: PageInput requestedPage: PageInput

View file

@ -125,10 +125,10 @@ func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.Rep
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
), ),
} }
@ -294,24 +294,20 @@ func getCVEListForImage(
requestedPage = &gql_generated.PageInput{} requestedPage = &gql_generated.PageInput{}
} }
pageInput := cveinfo.PageInput{ pageInput := cvemodel.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: cveinfo.SortCriteria( SortBy: cvemodel.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity),
), ),
} }
repo, ref, isTag := zcommon.GetImageDirAndReference(image) repo, ref, _ := zcommon.GetImageDirAndReference(image)
if ref == "" { if ref == "" {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided") return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided")
} }
if !isTag {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("reference by digest not supported")
}
cveList, pageInfo, err := cveInfo.GetCVEListForImage(repo, ref, searchedCVE, pageInput) cveList, pageInfo, err := cveInfo.GetCVEListForImage(repo, ref, searchedCVE, pageInput)
if err != nil { if err != nil {
return &gql_generated.CVEResultForImage{}, err return &gql_generated.CVEResultForImage{}, err
@ -365,8 +361,17 @@ func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) repodb.FilterFunc {
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String()
for _, tagInfo := range tagsInfo { for _, tagInfo := range tagsInfo {
if tagInfo.Descriptor.Digest.String() == manifestDigest { switch tagInfo.Descriptor.MediaType {
return true case ispec.MediaTypeImageManifest:
if tagInfo.Descriptor.Digest.String() == manifestDigest {
return true
}
case ispec.MediaTypeImageIndex:
for _, manifestDesc := range tagInfo.Manifests {
if manifestDesc.Digest.String() == manifestDigest {
return true
}
}
} }
} }
@ -423,10 +428,10 @@ func getImageListForCVE(
// Actual page requested by user // Actual page requested by user
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
), ),
} }
@ -485,10 +490,10 @@ func getImageListWithCVEFixed(
// Actual page requested by user // Actual page requested by user
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
), ),
} }
@ -535,10 +540,10 @@ func repoListWithNewestImage(
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
), ),
} }
@ -620,10 +625,10 @@ func getFilteredPaginatedRepos(
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
), ),
} }
@ -678,10 +683,10 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
), ),
} }
@ -709,10 +714,10 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
), ),
} }
@ -753,10 +758,10 @@ func derivedImageList(ctx context.Context, image string, digest *string, repoDB
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
), ),
} }
@ -866,10 +871,10 @@ func baseImageList(ctx context.Context, image string, digest *string, repoDB rep
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
), ),
} }
@ -1117,7 +1122,7 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
continue continue
} }
manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(digest)) manifestData, err := repoDB.GetManifestData(godigest.Digest(digest))
if err != nil { if err != nil {
graphql.AddError(ctx, fmt.Errorf("resolver: failed to get manifest meta for image %s:%s with manifest digest %s %w", graphql.AddError(ctx, fmt.Errorf("resolver: failed to get manifest meta for image %s:%s with manifest digest %s %w",
repo, tag, digest, err)) repo, tag, digest, err))
@ -1125,7 +1130,10 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
continue continue
} }
manifestMetaMap[digest] = manifestMeta manifestMetaMap[digest] = repodb.ManifestMetadata{
ManifestBlob: manifestData.ManifestBlob,
ConfigBlob: manifestData.ConfigBlob,
}
case ispec.MediaTypeImageIndex: case ispec.MediaTypeImageIndex:
digest := descriptor.Digest digest := descriptor.Digest
@ -1154,7 +1162,7 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
var errorOccured bool var errorOccured bool
for _, descriptor := range indexContent.Manifests { for _, descriptor := range indexContent.Manifests {
manifestMeta, err := repoDB.GetManifestMeta(repo, descriptor.Digest) manifestData, err := repoDB.GetManifestData(descriptor.Digest)
if err != nil { if err != nil {
graphql.AddError(ctx, graphql.AddError(ctx,
fmt.Errorf("resolver: failed to get manifest meta with digest '%s' for multiarch image %s:%s %w", fmt.Errorf("resolver: failed to get manifest meta with digest '%s' for multiarch image %s:%s %w",
@ -1166,7 +1174,10 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
break break
} }
manifestMetaMap[descriptor.Digest.String()] = manifestMeta manifestMetaMap[descriptor.Digest.String()] = repodb.ManifestMetadata{
ManifestBlob: manifestData.ManifestBlob,
ConfigBlob: manifestData.ConfigBlob,
}
} }
if errorOccured { if errorOccured {
@ -1210,7 +1221,7 @@ func (p timeSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i] p[i], p[j] = p[j], p[i]
} }
func safeDerefferencing[T any](pointer *T, defaultVal T) T { func safeDereferencing[T any](pointer *T, defaultVal T) T {
if pointer != nil { if pointer != nil {
return *pointer return *pointer
} }
@ -1236,10 +1247,10 @@ func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInf
} }
pageInput := repodb.PageInput{ pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0), Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0), Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria( SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
), ),
} }

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings"
"testing" "testing"
"time" "time"
@ -1987,7 +1988,12 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
digest, ok := tagsMap[image] digest, ok := tagsMap[image]
if !ok { if !ok {
return map[string]cvemodel.CVE{}, nil if !strings.Contains(image, "@") {
return map[string]cvemodel.CVE{}, nil
}
_, digestStr := common.GetImageDirAndDigest(image)
digest = godigest.Digest(digestStr)
} }
if digest.String() == digest1.String() { if digest.String() == digest1.String() {
@ -2075,7 +2081,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
repoWithDigestRef := fmt.Sprintf("repo@%s", dig) repoWithDigestRef := fmt.Sprintf("repo@%s", dig)
_, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, "", log) _, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, "", log)
So(err.Error(), ShouldContainSubstring, "reference by digest not supported") So(err, ShouldBeNil)
cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", log) cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", log)
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -3304,12 +3310,12 @@ func TestExpandedRepoInfo(t *testing.T) {
}, },
}, nil }, nil
}, },
GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
switch manifestDigest { switch manifestDigest {
case "errorDigest": case "errorDigest":
return repodb.ManifestMetadata{}, ErrTestError return repodb.ManifestData{}, ErrTestError
default: default:
return repodb.ManifestMetadata{ return repodb.ManifestData{
ManifestBlob: []byte("{}"), ManifestBlob: []byte("{}"),
ConfigBlob: []byte("{}"), ConfigBlob: []byte("{}"),
}, nil }, nil

View file

@ -592,7 +592,7 @@ type Query {
Returns a CVE list for the image specified in the argument Returns a CVE list for the image specified in the argument
""" """
CVEListForImage( CVEListForImage(
"Image name in format `repository:tag`" "Image name in format `repository:tag` or `repository@digest`"
image: String!, image: String!,
"Sets the parameters of the requested page" "Sets the parameters of the requested page"
requestedPage: PageInput requestedPage: PageInput

View file

@ -235,7 +235,9 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
// Setup test CVE data in mock scanner // Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{ scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" { if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" ||
strings.Contains(image, "zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") ||
strings.Contains(image, "a/zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE1": { "CVE1": {
ID: "CVE1", ID: "CVE1",
@ -258,7 +260,9 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
}, nil }, nil
} }
if image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" { if image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" ||
strings.Contains(image, "a/zot-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") ||
strings.Contains(image, "zot-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE3": { "CVE3": {
ID: "CVE3", ID: "CVE3",
@ -275,7 +279,8 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
}, nil }, nil
} }
if image == "test-repo:latest" { if image == "test-repo:latest" ||
image == "test-repo@sha256:9f8e1a125c4fb03a0f157d75999b73284ccc5cba18eb772e4643e3499343607e" {
return map[string]cvemodel.CVE{ return map[string]cvemodel.CVE{
"CVE1": { "CVE1": {
ID: "CVE1", ID: "CVE1",
@ -320,12 +325,20 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
return false, err return false, err
} }
manifestDigestStr, ok := repoMeta.Tags[inputTag] manifestDigestStr := reference
if !ok {
return false, zerr.ErrTagMetaNotFound if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
} }
manifestDigest, err := godigest.Parse(manifestDigestStr.Digest) manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -758,6 +771,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
Name Name
NewestImage{ NewestImage{
Tag Tag
Digest
Vulnerabilities{ Vulnerabilities{
MaxSeverity MaxSeverity
Count Count

View file

@ -176,7 +176,7 @@ func GetRepoTag(searchText string) (string, string, error) {
splitSlice := strings.Split(searchText, ":") splitSlice := strings.Split(searchText, ":")
if len(splitSlice) != repoTagCount { if len(splitSlice) != repoTagCount {
return "", "", zerr.ErrInvalidRepoTagFormat return "", "", zerr.ErrInvalidRepoRefFormat
} }
repo := strings.TrimSpace(splitSlice[0]) repo := strings.TrimSpace(splitSlice[0])
@ -329,6 +329,7 @@ func FilterDataByRepo(foundRepos []repodb.RepoMetadata, manifestMetadataMap map[
foundindexDataMap[descriptor.Digest] = indexData foundindexDataMap[descriptor.Digest] = indexData
default: default:
continue
} }
} }
} }

View file

@ -1339,7 +1339,6 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
matchedTags := make(map[string]repodb.Descriptor) matchedTags := make(map[string]repodb.Descriptor)
// take all manifestMetas // take all manifestMetas
for tag, descriptor := range repoMeta.Tags { for tag, descriptor := range repoMeta.Tags {
matchedTags[tag] = descriptor
switch descriptor.MediaType { switch descriptor.MediaType {
case ispec.MediaTypeImageManifest: case ispec.MediaTypeImageManifest:
manifestDigest := descriptor.Digest manifestDigest := descriptor.Digest
@ -1349,13 +1348,10 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
return fmt.Errorf("repodb: error while unmashaling manifest metadata for digest %s %w", manifestDigest, err) return fmt.Errorf("repodb: error while unmashaling manifest metadata for digest %s %w", manifestDigest, err)
} }
if !filter(repoMeta, manifestMeta) { if filter(repoMeta, manifestMeta) {
delete(matchedTags, tag) matchedTags[tag] = descriptor
manifestMetadataMap[manifestDigest] = manifestMeta
continue
} }
manifestMetadataMap[manifestDigest] = manifestMeta
case ispec.MediaTypeImageIndex: case ispec.MediaTypeImageIndex:
indexDigest := descriptor.Digest indexDigest := descriptor.Digest
@ -1371,7 +1367,7 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
return fmt.Errorf("repodb: error while unmashaling index content for digest %s %w", indexDigest, err) return fmt.Errorf("repodb: error while unmashaling index content for digest %s %w", indexDigest, err)
} }
manifestHasBeenMatched := false matchedManifests := []ispec.Descriptor{}
for _, manifest := range indexContent.Manifests { for _, manifest := range indexContent.Manifests {
manifestDigest := manifest.Digest.String() manifestDigest := manifest.Digest.String()
@ -1381,24 +1377,25 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
return fmt.Errorf("repodb: error while getting manifest data for digest %s %w", manifestDigest, err) return fmt.Errorf("repodb: error while getting manifest data for digest %s %w", manifestDigest, err)
} }
manifestMetadataMap[manifestDigest] = manifestMeta
if filter(repoMeta, manifestMeta) { if filter(repoMeta, manifestMeta) {
manifestHasBeenMatched = true matchedManifests = append(matchedManifests, manifest)
manifestMetadataMap[manifestDigest] = manifestMeta
} }
} }
if !manifestHasBeenMatched { if len(matchedManifests) > 0 {
delete(matchedTags, tag) indexContent.Manifests = matchedManifests
for _, manifest := range indexContent.Manifests { indexBlob, err := json.Marshal(indexContent)
delete(manifestMetadataMap, manifest.Digest.String()) if err != nil {
return err
} }
continue indexData.IndexBlob = indexBlob
}
indexDataMap[indexDigest] = indexData indexDataMap[indexDigest] = indexData
matchedTags[tag] = descriptor
}
default: default:
bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type") bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type")

View file

@ -2,6 +2,11 @@ package repodb
import ( import (
"time" "time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors"
) )
// DetailedRepoMeta is a auxiliary structure used for sorting RepoMeta arrays by information // DetailedRepoMeta is a auxiliary structure used for sorting RepoMeta arrays by information
@ -55,3 +60,33 @@ func SortByDownloads(pageBuffer []DetailedRepoMeta) func(i, j int) bool {
return pageBuffer[i].Downloads > pageBuffer[j].Downloads return pageBuffer[i].Downloads > pageBuffer[j].Downloads
} }
} }
// FindMediaTypeForDigest will look into the buckets for a certain digest. Depending on which bucket that
// digest is found the corresponding mediatype is returned.
func FindMediaTypeForDigest(repoDB RepoDB, digest godigest.Digest) (bool, string) {
_, err := repoDB.GetManifestData(digest)
if err == nil {
return true, ispec.MediaTypeImageManifest
}
_, err = repoDB.GetIndexData(digest)
if err == nil {
return true, ispec.MediaTypeImageIndex
}
return false, ""
}
func GetImageDescriptor(repoDB RepoDB, repo, tag string) (Descriptor, error) {
repoMeta, err := repoDB.GetRepoMeta(repo)
if err != nil {
return Descriptor{}, err
}
imageDescriptor, ok := repoMeta.Tags[tag]
if !ok {
return Descriptor{}, zerr.ErrTagMetaNotFound
}
return imageDescriptor, nil
}

View file

@ -1122,9 +1122,8 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name) repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name)
matchedTags := make(map[string]repodb.Descriptor) matchedTags := make(map[string]repodb.Descriptor)
for tag, descriptor := range repoMeta.Tags {
matchedTags[tag] = descriptor
for tag, descriptor := range repoMeta.Tags {
switch descriptor.MediaType { switch descriptor.MediaType {
case ispec.MediaTypeImageManifest: case ispec.MediaTypeImageManifest:
manifestDigest := descriptor.Digest manifestDigest := descriptor.Digest
@ -1137,13 +1136,10 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
fmt.Errorf("repodb: error while unmashaling manifest metadata for digest %s \n%w", manifestDigest, err) fmt.Errorf("repodb: error while unmashaling manifest metadata for digest %s \n%w", manifestDigest, err)
} }
if !filter(repoMeta, manifestMeta) { if filter(repoMeta, manifestMeta) {
delete(matchedTags, tag) matchedTags[tag] = descriptor
manifestMetadataMap[manifestDigest] = manifestMeta
continue
} }
manifestMetadataMap[manifestDigest] = manifestMeta
case ispec.MediaTypeImageIndex: case ispec.MediaTypeImageIndex:
indexDigest := descriptor.Digest indexDigest := descriptor.Digest
@ -1163,7 +1159,7 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
fmt.Errorf("repodb: error while unmashaling index content for digest %s %w", indexDigest, err) fmt.Errorf("repodb: error while unmashaling index content for digest %s %w", indexDigest, err)
} }
manifestHasBeenMatched := false matchedManifests := []ispec.Descriptor{}
for _, manifest := range indexContent.Manifests { for _, manifest := range indexContent.Manifests {
manifestDigest := manifest.Digest.String() manifestDigest := manifest.Digest.String()
@ -1176,24 +1172,26 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
fmt.Errorf("%w repodb: error while getting manifest data for digest %s", err, manifestDigest) fmt.Errorf("%w repodb: error while getting manifest data for digest %s", err, manifestDigest)
} }
manifestMetadataMap[manifestDigest] = manifestMeta
if filter(repoMeta, manifestMeta) { if filter(repoMeta, manifestMeta) {
manifestHasBeenMatched = true matchedManifests = append(matchedManifests, manifest)
manifestMetadataMap[manifestDigest] = manifestMeta
} }
} }
if !manifestHasBeenMatched { if len(matchedManifests) > 0 {
delete(matchedTags, tag) indexContent.Manifests = matchedManifests
for _, manifest := range indexContent.Manifests { indexBlob, err := json.Marshal(indexContent)
delete(manifestMetadataMap, manifest.Digest.String()) if err != nil {
return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{},
pageInfo, err
} }
continue indexData.IndexBlob = indexBlob
}
indexDataMap[indexDigest] = indexData indexDataMap[indexDigest] = indexData
matchedTags[tag] = descriptor
}
default: default:
dwr.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type") dwr.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type")

View file

@ -20,6 +20,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -45,6 +46,7 @@ import (
"oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras-go/v2/registry/remote/auth"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test/inject" "zotregistry.io/zot/pkg/test/inject"
@ -56,6 +58,8 @@ const (
SleepTime = 100 * time.Millisecond SleepTime = 100 * time.Millisecond
) )
var vulnerableLayer []byte //nolint: gochecknoglobals
var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
// which: manifest, config, layer // which: manifest, config, layer
@ -604,15 +608,8 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif
configDigest := godigest.FromBytes(configBlob) configDigest := godigest.FromBytes(configBlob)
layer := make([]byte, layerSize)
_, err = rand.Read(layer)
if err != nil {
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
}
layers := [][]byte{ layers := [][]byte{
layer, GetRandomLayer(layerSize),
} }
schemaVersion := 2 schemaVersion := 2
@ -639,6 +636,138 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif
return config, layers, manifest, nil return config, layers, manifest, nil
} }
// These are the 2 vulnerabilities found for the returned image by the GetVulnImage function.
const (
Vulnerability1ID = "CVE-2023-2650"
Vulnerability2ID = "CVE-2023-1255"
)
func GetVulnImage(ref string) (Image, error) {
const skipStackFrame = 2
vulnerableLayer, err := GetLayerWithVulnerability(skipStackFrame)
if err != nil {
return Image{}, err
}
vulnerableConfig := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
}
img, err := GetImageWithComponents(
vulnerableConfig,
[][]byte{
vulnerableLayer,
})
if err != nil {
return Image{}, err
}
img.Reference = ref
return img, err
}
func GetVulnImageWithConfig(ref string, config ispec.Image) (Image, error) {
const skipStackFrame = 2
vulnerableLayer, err := GetLayerWithVulnerability(skipStackFrame)
if err != nil {
return Image{}, err
}
vulnerableConfig := ispec.Image{
Platform: config.Platform,
Config: config.Config,
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
Created: config.Created,
History: config.History,
}
img, err := GetImageWithComponents(
vulnerableConfig,
[][]byte{
vulnerableLayer,
})
if err != nil {
return Image{}, err
}
img.Reference = ref
return img, err
}
func GetLayerWithVulnerability(skip int) ([]byte, error) {
if vulnerableLayer != nil {
return vulnerableLayer, nil
}
_, b, _, ok := runtime.Caller(skip)
if !ok {
return []byte{}, zerr.ErrCallerInfo
}
absoluteCallerpath := filepath.Dir(b)
fmt.Println(absoluteCallerpath)
// we know pkg folder inside zot must exist, and since all tests are called from within pkg we'll use it as reference
relCallerPath := absoluteCallerpath[strings.LastIndex(absoluteCallerpath, "pkg"):]
relCallerSlice := strings.Split(relCallerPath, string(os.PathSeparator))
fmt.Println(relCallerPath, relCallerSlice)
// we'll calculate how many folder we should go back to reach the root of the zot folder relative
// to the callers position
backPathSlice := make([]string, len(relCallerSlice))
for i := 0; i < len(backPathSlice); i++ {
backPathSlice[i] = ".."
}
backPath := filepath.Join(backPathSlice...)
// this is the path of the blob relative to the root of the zot folder
vulnBlobPath := "test/data/alpine/blobs/sha256/f56be85fc22e46face30e2c3de3f7fe7c15f8fd7c4e5add29d7f64b87abdaa09"
var err error
x, _ := filepath.Abs(filepath.Join(backPath, vulnBlobPath))
_ = x
vulnerableLayer, err = os.ReadFile(filepath.Join(backPath, vulnBlobPath)) //nolint: lll
if err != nil {
return nil, err
}
return vulnerableLayer, nil
}
func GetRandomLayer(size int) []byte {
layer := make([]byte, size)
_, err := rand.Read(layer)
if err != nil {
return layer
}
return layer
}
func GetRandomImage(reference string) (Image, error) { func GetRandomImage(reference string) (Image, error) {
const layerSize = 20 const layerSize = 20

View file

@ -2,17 +2,18 @@ package mocks
import ( import (
"zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/common"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
) )
type CveInfoMock struct { type CveInfoMock struct {
GetImageListForCVEFn func(repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListForCVEFn func(repo, cveID string) ([]cvemodel.TagInfo, error)
GetImageListWithCVEFixedFn func(repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixedFn func(repo, cveID string) ([]cvemodel.TagInfo, error)
GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cveinfo.PageInput, GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, common.PageInfo, error) ) ([]cvemodel.CVE, common.PageInfo, error)
GetCVESummaryForImageFn func(repo string, reference string, GetCVESummaryForImageFn func(repo string, reference string,
) (cveinfo.ImageCVESummary, error) ) (cvemodel.ImageCVESummary, error)
GetCVESummaryForImageMediaFn func(repo string, digest, mediaType string,
) (cvemodel.ImageCVESummary, error)
CompareSeveritiesFn func(severity1, severity2 string) int CompareSeveritiesFn func(severity1, severity2 string) int
UpdateDBFn func() error UpdateDBFn func() error
} }
@ -34,7 +35,7 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
} }
func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string, func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string,
searchedCVE string, pageInput cveinfo.PageInput, searchedCVE string, pageInput cvemodel.PageInput,
) ( ) (
[]cvemodel.CVE, []cvemodel.CVE,
common.PageInfo, common.PageInfo,
@ -48,12 +49,21 @@ func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string,
} }
func (cveInfo CveInfoMock) GetCVESummaryForImage(repo string, reference string, func (cveInfo CveInfoMock) GetCVESummaryForImage(repo string, reference string,
) (cveinfo.ImageCVESummary, error) { ) (cvemodel.ImageCVESummary, error) {
if cveInfo.GetCVESummaryForImageFn != nil { if cveInfo.GetCVESummaryForImageFn != nil {
return cveInfo.GetCVESummaryForImageFn(repo, reference) return cveInfo.GetCVESummaryForImageFn(repo, reference)
} }
return cveinfo.ImageCVESummary{}, nil return cvemodel.ImageCVESummary{}, nil
}
func (cveInfo CveInfoMock) GetCVESummaryForImageMedia(repo, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) {
if cveInfo.GetCVESummaryForImageMediaFn != nil {
return cveInfo.GetCVESummaryForImageMediaFn(repo, digest, mediaType)
}
return cvemodel.ImageCVESummary{}, nil
} }
func (cveInfo CveInfoMock) CompareSeverities(severity1, severity2 string) int { func (cveInfo CveInfoMock) CompareSeverities(severity1, severity2 string) int {
@ -74,6 +84,7 @@ func (cveInfo CveInfoMock) UpdateDB() error {
type CveScannerMock struct { type CveScannerMock struct {
IsImageFormatScannableFn func(repo string, reference string) (bool, error) IsImageFormatScannableFn func(repo string, reference string) (bool, error)
IsImageMediaScannableFn func(repo string, digest, mediaType string) (bool, error)
ScanImageFn func(image string) (map[string]cvemodel.CVE, error) ScanImageFn func(image string) (map[string]cvemodel.CVE, error)
CompareSeveritiesFn func(severity1, severity2 string) int CompareSeveritiesFn func(severity1, severity2 string) int
UpdateDBFn func() error UpdateDBFn func() error
@ -87,6 +98,14 @@ func (scanner CveScannerMock) IsImageFormatScannable(repo string, reference stri
return true, nil return true, nil
} }
func (scanner CveScannerMock) IsImageMediaScannable(repo string, digest, mediaType string) (bool, error) {
if scanner.IsImageMediaScannableFn != nil {
return scanner.IsImageMediaScannableFn(repo, digest, mediaType)
}
return true, nil
}
func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) { func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) {
if scanner.ScanImageFn != nil { if scanner.ScanImageFn != nil {
return scanner.ScanImageFn(image) return scanner.ScanImageFn(image)

View file

@ -216,7 +216,7 @@ func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]cvemodel
tagsInfo = append(tagsInfo, tagsInfo = append(tagsInfo,
cvemodel.TagInfo{ cvemodel.TagInfo{
Name: val, Tag: val,
Timestamp: timeStamp, Timestamp: timeStamp,
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: digest, Digest: digest,

View file

@ -450,7 +450,7 @@ func TestTagsInfo(t *testing.T) {
allTags := make([]cvemodel.TagInfo, 0) allTags := make([]cvemodel.TagInfo, 0)
firstTag := cvemodel.TagInfo{ firstTag := cvemodel.TagInfo{
Name: "1.0.0", Tag: "1.0.0",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -458,7 +458,7 @@ func TestTagsInfo(t *testing.T) {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
secondTag := cvemodel.TagInfo{ secondTag := cvemodel.TagInfo{
Name: "1.0.1", Tag: "1.0.1",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -466,7 +466,7 @@ func TestTagsInfo(t *testing.T) {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
thirdTag := cvemodel.TagInfo{ thirdTag := cvemodel.TagInfo{
Name: "1.0.2", Tag: "1.0.2",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -474,7 +474,7 @@ func TestTagsInfo(t *testing.T) {
Timestamp: time.Now(), Timestamp: time.Now(),
} }
fourthTag := cvemodel.TagInfo{ fourthTag := cvemodel.TagInfo{
Name: "1.0.3", Tag: "1.0.3",
Descriptor: cvemodel.Descriptor{ Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb", Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest, MediaType: ispec.MediaTypeImageManifest,
@ -485,6 +485,6 @@ func TestTagsInfo(t *testing.T) {
allTags = append(allTags, firstTag, secondTag, thirdTag, fourthTag) allTags = append(allTags, firstTag, secondTag, thirdTag, fourthTag)
latestTag := ocilayout.GetLatestTag(allTags) latestTag := ocilayout.GetLatestTag(allTags)
So(latestTag.Name, ShouldEqual, "1.0.3") So(latestTag.Tag, ShouldEqual, "1.0.3")
}) })
} }