From 0a04b2a4edb9a08e1da7d0161eb66001899acb62 Mon Sep 17 00:00:00 2001 From: LaurentiuNiculae Date: Thu, 6 Jul 2023 11:36:26 +0300 Subject: [PATCH] feat(cve): implemented trivy image scan for multiarch images (#1510) Signed-off-by: Laurentiu Niculae --- Makefile | 1 + errors/errors.go | 3 +- pkg/cli/cve_cmd_test.go | 22 +- pkg/cli/image_cmd_test.go | 4 +- pkg/cli/service.go | 8 +- pkg/common/model.go | 2 +- pkg/common/oci.go | 22 +- pkg/extensions/search/convert/convert_test.go | 40 +- pkg/extensions/search/convert/repodb.go | 18 +- pkg/extensions/search/cve/cve.go | 389 +++++++++++++----- .../search/cve/cve_internal_test.go | 65 +++ pkg/extensions/search/cve/cve_test.go | 380 +++++++++++++++-- pkg/extensions/search/cve/model/models.go | 14 +- pkg/extensions/search/cve/model/pagination.go | 9 + pkg/extensions/search/cve/pagination.go | 22 +- pkg/extensions/search/cve/pagination_test.go | 26 +- pkg/extensions/search/cve/trivy/scanner.go | 185 +++++++-- .../search/cve/trivy/scanner_internal_test.go | 143 ++++++- .../search/cve/trivy/scanner_test.go | 186 +++++++++ .../search/gql_generated/generated.go | 2 +- pkg/extensions/search/resolver.go | 105 ++--- pkg/extensions/search/resolver_test.go | 16 +- pkg/extensions/search/schema.graphql | 2 +- pkg/extensions/search/search_test.go | 28 +- pkg/meta/common/common.go | 3 +- .../repodb/boltdb-wrapper/boltdb_wrapper.go | 33 +- pkg/meta/repodb/common.go | 35 ++ .../repodb/dynamodb-wrapper/dynamo_wrapper.go | 36 +- pkg/test/common.go | 145 ++++++- pkg/test/mocks/cve_mock.go | 31 +- pkg/test/oci-layout/oci_layout.go | 2 +- pkg/test/oci-layout/oci_layout_test.go | 10 +- 32 files changed, 1617 insertions(+), 370 deletions(-) create mode 100644 pkg/extensions/search/cve/cve_internal_test.go create mode 100644 pkg/extensions/search/cve/model/pagination.go create mode 100644 pkg/extensions/search/cve/trivy/scanner_test.go diff --git a/Makefile b/Makefile index e9ecc91d..6d1e1290 100644 --- a/Makefile +++ b/Makefile @@ -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: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/alpine:3.17.3 oci:${TESTDATA}/alpine:3.17.3; \ chmod -R a=rwx ${TESTDATA} .PHONY: run-bench diff --git a/errors/errors.go b/errors/errors.go index 7474761b..1912ccc9 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -73,7 +73,7 @@ var ( ErrEmptyRepoName = errors.New("repodb: repo name 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") - 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") ErrOffsetIsNegative = errors.New("pageturner: offset has negative value") 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") ErrSyncImageNotSigned = errors.New("sync: image is not signed") ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config") + ErrCallerInfo = errors.New("runtime: failed to get info regarding the current runtime") ) diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index 1fa6d032..9984bbe6 100644 --- a/pkg/cli/cve_cmd_test.go +++ b/pkg/cli/cve_cmd_test.go @@ -29,6 +29,7 @@ import ( zotErrors "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" + zcommon "zotregistry.io/zot/pkg/common" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" @@ -1035,7 +1036,7 @@ func TestServerCVEResponse(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") 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") }) @@ -1172,7 +1173,8 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { // Setup test CVE data in mock scanner scanner := mocks.CveScannerMock{ 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{ "CVE-1": { ID: "CVE-1", @@ -1223,12 +1225,20 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { return false, err } - manifestDigestStr, ok := repoMeta.Tags[inputTag] - if !ok { - return false, zotErrors.ErrTagMetaNotFound + manifestDigestStr := reference + + 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 { return false, err } diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index 715a942b..bad189ee 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -1891,8 +1891,8 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, -) (*common.FixedTags, error) { - fixedTags := &common.FixedTags{ +) (*common.ImageListWithCVEFixedResponse, error) { + fixedTags := &common.ImageListWithCVEFixedResponse{ Errors: nil, ImageListWithCVEFixed: struct { common.PaginatedImagesResult `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema diff --git a/pkg/cli/service.go b/pkg/cli/service.go index b60bb510..55c532d6 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -43,7 +43,7 @@ type SearchService interface { //nolint:interfacebloat getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string) (*common.ImagesForCve, error) 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, derivedImage string) (*common.DerivedImageListResponse, error) 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, username, password, imageName, cveID string, -) (*common.FixedTags, error) { +) (*common.ImageListWithCVEFixedResponse, error) { query := fmt.Sprintf(` { ImageListWithCVEFixed(id: "%s", image: "%s") { @@ -398,7 +398,7 @@ func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config s }`, cveID, imageName) - result := &common.FixedTags{} + result := &common.ImageListWithCVEFixedResponse{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) @@ -847,7 +847,7 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear } }`, cvid, imageName) - result := &common.FixedTags{} + result := &common.ImageListWithCVEFixedResponse{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if err != nil { diff --git a/pkg/common/model.go b/pkg/common/model.go index 09cec23e..f0d4c685 100644 --- a/pkg/common/model.go +++ b/pkg/common/model.go @@ -112,7 +112,7 @@ type Annotation struct { Value string `json:"value"` } -type FixedTags struct { +type ImageListWithCVEFixedResponse struct { Errors []ErrorGQL `json:"errors"` ImageListWithCVEFixed `json:"data"` } diff --git a/pkg/common/oci.go b/pkg/common/oci.go index 08aadccd..7d8fbe09 100644 --- a/pkg/common/oci.go +++ b/pkg/common/oci.go @@ -4,6 +4,7 @@ import ( "strings" "time" + "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" zerr "zotregistry.io/zot/errors" @@ -101,7 +102,7 @@ func GetRepoRefference(repo string) (string, string, bool, error) { repoName, tag, found := strings.Cut(repo, ":") if !found { - return "", "", false, zerr.ErrInvalidRepoTagFormat + return "", "", false, zerr.ErrInvalidRepoRefFormat } return repoName, tag, true, nil @@ -109,3 +110,22 @@ func GetRepoRefference(repo string) (string, string, bool, error) { 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) +} diff --git a/pkg/extensions/search/convert/convert_test.go b/pkg/extensions/search/convert/convert_test.go index f3d83b83..8c9ef5e0 100644 --- a/pkg/extensions/search/convert/convert_test.go +++ b/pkg/extensions/search/convert/convert_test.go @@ -13,7 +13,7 @@ import ( . "github.com/smartystreets/goconvey/convey" "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/log" "zotregistry.io/zot/pkg/meta/bolt" @@ -74,9 +74,9 @@ func TestConvertErrors(t *testing.T) { map[string]repodb.IndexData{}, convert.SkipQGLField{}, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo string, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError + GetCVESummaryForImageMediaFn: func(repo string, digest, mediaType string, + ) (cvemodel.ImageCVESummary, error) { + return cvemodel.ImageCVESummary{}, ErrTestError }, }, ) @@ -120,9 +120,8 @@ func TestConvertErrors(t *testing.T) { }, map[string]repodb.ManifestMetadata{}, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError + GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) { + return cvemodel.ImageCVESummary{}, ErrTestError }, }, ) @@ -153,9 +152,8 @@ func TestConvertErrors(t *testing.T) { ConfigBlob: configBlob, }, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError + GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) { + return cvemodel.ImageCVESummary{}, ErrTestError }, }, ) @@ -187,12 +185,7 @@ func TestConvertErrors(t *testing.T) { ConfigBlob: []byte("bad json"), }, nil, - mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError - }, - }, + mocks.CveInfoMock{}, ) So(err, ShouldNotBeNil) @@ -227,9 +220,8 @@ func TestConvertErrors(t *testing.T) { }, nil, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError + GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) { + return cvemodel.ImageCVESummary{}, ErrTestError }, }, ) @@ -259,9 +251,8 @@ func TestConvertErrors(t *testing.T) { Vulnerabilities: false, }, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError + GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) { + return cvemodel.ImageCVESummary{}, ErrTestError }, }, log.NewLogger("debug", ""), ) @@ -286,9 +277,8 @@ func TestConvertErrors(t *testing.T) { Vulnerabilities: false, }, mocks.CveInfoMock{ - GetCVESummaryForImageFn: func(repo, reference string, - ) (cveinfo.ImageCVESummary, error) { - return cveinfo.ImageCVESummary{}, ErrTestError + GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) { + return cvemodel.ImageCVESummary{}, ErrTestError }, }, log.NewLogger("debug", ""), ) diff --git a/pkg/extensions/search/convert/repodb.go b/pkg/extensions/search/convert/repodb.go index 9ed1847c..e077afbe 100644 --- a/pkg/extensions/search/convert/repodb.go +++ b/pkg/extensions/search/convert/repodb.go @@ -103,7 +103,8 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata, // We only scan the latest image on the repo for performance reasons // Check if vulnerability scanning is disabled 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 { // Log the error, but we should still include the image in results 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 { - imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) + imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, indexDigestStr, ispec.MediaTypeImageIndex) if err != nil { // 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())) } - imageCveSummary := cveinfo.ImageCVESummary{} + imageCveSummary := cvemodel.ImageCVESummary{} if cveInfo != nil && !skipCVE { - imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) + imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigest, ispec.MediaTypeImageManifest) if err != nil { // 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())) } - imageCveSummary := cveinfo.ImageCVESummary{} + imageCveSummary := cvemodel.ImageCVESummary{} if cveInfo != nil && !skipCVE { - imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag) + imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigestStr, ispec.MediaTypeImageManifest) if err != nil { // 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 // Check if vulnerability scanning is disabled 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 { // Log the error, but we should still include the image in results graphql.AddError( diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index 8f96c5e7..33464518 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -2,15 +2,14 @@ package cveinfo import ( "encoding/json" - "fmt" "sort" "strings" + "time" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - "zotregistry.io/zot/errors" - "zotregistry.io/zot/pkg/common" + zcommon "zotregistry.io/zot/pkg/common" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" "zotregistry.io/zot/pkg/extensions/search/cve/trivy" "zotregistry.io/zot/pkg/log" @@ -21,24 +20,22 @@ import ( type CveInfo interface { GetImageListForCVE(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) - GetCVESummaryForImage(repo, tag string) (ImageCVESummary, error) + GetCVEListForImage(repo, tag string, searchedCVE string, pageinput cvemodel.PageInput, + ) ([]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 UpdateDB() error } type Scanner interface { 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 UpdateDB() error } -type ImageCVESummary struct { - Count int - MaxSeverity string -} - type BaseCveInfo struct { Log log.Logger Scanner Scanner @@ -70,19 +67,19 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta for tag, descriptor := range repoMeta.Tags { switch descriptor.MediaType { - case ispec.MediaTypeImageManifest: + case ispec.MediaTypeImageManifest, ispec.MediaTypeImageIndex: manifestDigestStr := descriptor.Digest manifestDigest := godigest.Digest(manifestDigestStr) - isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) + isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr) if !isScanableImage || err != nil { cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable") continue } - cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag)) + cveMap, err := cveinfo.Scanner.ScanImage(zcommon.GetFullImageName(repo, tag)) if err != nil { 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 { imgList = append(imgList, cvemodel.TagInfo{ - Name: tag, + Tag: tag, Descriptor: cvemodel.Descriptor{ Digest: manifestDigest, MediaType: descriptor.MediaType, @@ -118,87 +115,81 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo vulnerableTags := make([]cvemodel.TagInfo, 0) allTags := make([]cvemodel.TagInfo, 0) - var hasCVE bool - for tag, descriptor := range repoMeta.Tags { - manifestDigestStr := descriptor.Digest - switch descriptor.MediaType { case ispec.MediaTypeImageManifest: - manifestDigest, err := godigest.Parse(manifestDigestStr) + manifestDigestStr := descriptor.Digest + + tagInfo, err := getTagInfoForManifest(tag, manifestDigestStr, cveinfo.RepoDB) if err != nil { 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 } - 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) - image := fmt.Sprintf("%s:%s", repo, tag) - - 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") - + if cveinfo.isManifestVulnerable(repo, tag, manifestDigestStr, cveID) { 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 { - cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). - Msg("scanning failed, adding as a vulnerable image") - - vulnerableTags = append(vulnerableTags, tagInfo) - continue } - hasCVE = false + vulnerableManifests := []cvemodel.DescriptorInfo{} + allManifests := []cvemodel.DescriptorInfo{} - for id := range cveMap { - if id == cveID { - hasCVE = true + for _, manifest := range indexContent.Manifests { + tagInfo, err := getTagInfoForManifest(tag, manifest.Digest.String(), cveinfo.RepoDB) + 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 { - vulnerableTags = append(vulnerableTags, tagInfo) + if len(allManifests) > 0 { + 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: 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 } +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) { 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, - common.PageInfo, + zcommon.PageInfo, error, ) { - isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref) 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) 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) if err != nil { - return []cvemodel.CVE{}, common.PageInfo{}, err + return []cvemodel.CVE{}, zcommon.PageInfo{}, err } filterCVEList(cveMap, searchedCVE, pageFinder) @@ -259,23 +361,22 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, searchedCVE stri return cveList, pageInfo, nil } -func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string, -) (ImageCVESummary, error) { +func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error) { // There are several cases, expected returned values below: // not scannable / error during scan - max severity "" - cve count 0 - 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 - imageCVESummary := ImageCVESummary{ + imageCVESummary := cvemodel.ImageCVESummary{ Count: 0, MaxSeverity: "", } - isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag) + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref) if !isValidImage { return imageCVESummary, err } - image := getImageString(repo, tag) + image := zcommon.GetFullImageName(repo, ref) cveMap, err := cveinfo.Scanner.ScanImage(image) if err != nil { @@ -300,20 +401,41 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string, return imageCVESummary, nil } -func referenceIsDigest(reference string) bool { - _, err := godigest.Parse(reference) - - return err == nil -} - -func getImageString(repo, reference string) string { - image := repo + ":" + reference - - if referenceIsDigest(reference) { - image = repo + "@" + reference +func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType string, +) (cvemodel.ImageCVESummary, error) { + imageCVESummary := cvemodel.ImageCVESummary{ + Count: 0, + MaxSeverity: "", } - 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 { @@ -333,10 +455,21 @@ func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags)) for _, tag := range vulnerableTags { - vulnerableTagMap[tag.Name] = tag + vulnerableTagMap[tag.Tag] = tag - if tag.Timestamp.Before(earliestVulnerable.Timestamp) { - earliestVulnerable = tag + switch tag.Descriptor.MediaType { + 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 // newer images which don't for _, tag := range allTags { - if tag.Timestamp.Before(earliestVulnerable.Timestamp) { - // The vulnerability did not exist at the time this - // image was built + switch tag.Descriptor.MediaType { + case ispec.MediaTypeImageManifest: + 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 } - // 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 } + +func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvemodel.DescriptorInfo) bool { + for _, di := range slice { + if di.Digest == descriptorInfo.Digest { + return true + } + } + + return false +} diff --git a/pkg/extensions/search/cve/cve_internal_test.go b/pkg/extensions/search/cve/cve_internal_test.go new file mode 100644 index 00000000..a890b967 --- /dev/null +++ b/pkg/extensions/search/cve/cve_internal_test.go @@ -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) + }) + }) +} diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index 3b594a19..2b2925b1 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "net/url" "os" "path" "strings" @@ -25,10 +26,12 @@ import ( "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" apiErr "zotregistry.io/zot/pkg/api/errors" + zcommon "zotregistry.io/zot/pkg/common" extconf "zotregistry.io/zot/pkg/extensions/config" "zotregistry.io/zot/pkg/extensions/monitoring" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" 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/meta/bolt" "zotregistry.io/zot/pkg/meta/repodb" @@ -382,10 +385,16 @@ func TestImageFormat(t *testing.T) { GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) { return repodb.RepoMetadata{ Tags: map[string]repodb.Descriptor{ - "tag": {MediaType: ispec.MediaTypeImageIndex}, + "tag": { + MediaType: ispec.MediaTypeImageIndex, + Digest: godigest.FromString("digest").String(), + }, }, }, nil }, + GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) { + return repodb.IndexData{IndexBlob: []byte(`{}`)}, nil + }, } storeController := storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, @@ -395,7 +404,7 @@ func TestImageFormat(t *testing.T) { isScanable, err := cveInfo.Scanner.IsImageFormatScannable("repo", "tag") 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 scanner := mocks.CveScannerMock{ ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { + repo1 := "repo1" + + repo, ref, _ := zcommon.GetImageDirAndReference(image) // Images in chronological order - if image == "repo1:0.1.0" { + if image == "repo1:0.1.0" || ref == digest11.String() { return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", @@ -1036,7 +1048,8 @@ func TestCVEStruct(t *testing.T) { }, 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{ "CVE1": { ID: "CVE1", @@ -1059,7 +1072,7 @@ func TestCVEStruct(t *testing.T) { }, nil } - if image == "repo1:1.1.0" { + if image == "repo1:1.1.0" || (repo == repo1 && ref == digest13.String()) { return map[string]cvemodel.CVE{ "CVE3": { ID: "CVE3", @@ -1072,7 +1085,7 @@ func TestCVEStruct(t *testing.T) { // As a minor release on 1.0.0 banch // 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{ "CVE1": { ID: "CVE1", @@ -1089,7 +1102,7 @@ func TestCVEStruct(t *testing.T) { }, nil } - if image == "repoIndex:tagIndex" { + if image == "repoIndex:tagIndex" || (repo == "repoIndex" && ref == indexDigest.String()) { return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", @@ -1119,12 +1132,20 @@ func TestCVEStruct(t *testing.T) { return false, err } - manifestDigestStr, ok := repoMeta.Tags[inputTag] - if !ok { - return false, zerr.ErrTagMetaNotFound + manifestDigestStr := reference + + 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 { return false, err } @@ -1154,6 +1175,15 @@ func TestCVEStruct(t *testing.T) { 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", "") @@ -1213,7 +1243,7 @@ func TestCVEStruct(t *testing.T) { t.Log("Test GetCVEListForImage") - pageInput := cveinfo.PageInput{ + pageInput := cvemodel.PageInput{ SortBy: cveinfo.SeverityDsc, } @@ -1289,14 +1319,14 @@ func TestCVEStruct(t *testing.T) { tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1") So(err, ShouldBeNil) 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") So(err, ShouldBeNil) So(len(tagList), ShouldEqual, 2) expectedTags := []string{"1.0.1", "1.1.0"} - So(expectedTags, ShouldContain, tagList[0].Name) - So(expectedTags, ShouldContain, tagList[1].Name) + So(expectedTags, ShouldContain, tagList[0].Tag) + So(expectedTags, ShouldContain, tagList[1].Tag) tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE3") So(err, ShouldBeNil) @@ -1309,7 +1339,7 @@ func TestCVEStruct(t *testing.T) { tagList, err = cveInfo.GetImageListWithCVEFixed("repo6", "CVE1") So(err, ShouldBeNil) 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 tagList, err = cveInfo.GetImageListWithCVEFixed("repo2", "CVE100") @@ -1341,22 +1371,22 @@ func TestCVEStruct(t *testing.T) { So(err, ShouldBeNil) So(len(tagList), ShouldEqual, 3) expectedTags = []string{"0.1.0", "1.0.0", "1.0.1"} - So(expectedTags, ShouldContain, tagList[0].Name) - So(expectedTags, ShouldContain, tagList[1].Name) - So(expectedTags, ShouldContain, tagList[2].Name) + So(expectedTags, ShouldContain, tagList[0].Tag) + So(expectedTags, ShouldContain, tagList[1].Tag) + So(expectedTags, ShouldContain, tagList[2].Tag) tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE2") So(err, ShouldBeNil) 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") So(err, ShouldBeNil) So(len(tagList), ShouldEqual, 3) expectedTags = []string{"1.0.0", "1.0.1", "1.1.0"} - So(expectedTags, ShouldContain, tagList[0].Name) - So(expectedTags, ShouldContain, tagList[1].Name) - So(expectedTags, ShouldContain, tagList[2].Name) + So(expectedTags, ShouldContain, tagList[0].Tag) + So(expectedTags, ShouldContain, tagList[1].Tag) + So(expectedTags, ShouldContain, tagList[2].Tag) // Image/repo doesn't have the CVE at all tagList, err = cveInfo.GetImageListForCVE("repo6", "CVE1") @@ -1419,7 +1449,7 @@ func TestCVEStruct(t *testing.T) { tagList, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1") So(err, ShouldBeNil) - So(len(tagList), ShouldEqual, 0) + So(len(tagList), ShouldEqual, 1) cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{ IsImageFormatScannableFn: func(repo, reference string) (bool, error) { @@ -1448,7 +1478,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) { tags := make([]cvemodel.TagInfo, 0) firstTag := cvemodel.TagInfo{ - Name: "1.0.0", + Tag: "1.0.0", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -1456,7 +1486,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) { Timestamp: time.Now(), } secondTag := cvemodel.TagInfo{ - Name: "1.0.1", + Tag: "1.0.1", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -1464,7 +1494,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) { Timestamp: time.Now(), } thirdTag := cvemodel.TagInfo{ - Name: "1.0.2", + Tag: "1.0.2", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -1472,7 +1502,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) { Timestamp: time.Now(), } fourthTag := cvemodel.TagInfo{ - Name: "1.0.3", + Tag: "1.0.3", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -1496,10 +1526,298 @@ func TestFixedTags(t *testing.T) { So(len(fixedTags), ShouldEqual, 2) fixedTags = cveinfo.GetFixedTags(allTags, append(vulnerableTags, cvemodel.TagInfo{ - Name: "taginfo", - Descriptor: cvemodel.Descriptor{}, - Timestamp: time.Date(2000, time.July, 20, 10, 10, 10, 10, time.UTC), + Tag: "taginfo", + Descriptor: cvemodel.Descriptor{ + MediaType: ispec.MediaTypeImageManifest, + Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", + }, + Timestamp: time.Date(2000, time.July, 20, 10, 10, 10, 10, time.UTC), })) 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) + }) + }) +} diff --git a/pkg/extensions/search/cve/model/models.go b/pkg/extensions/search/cve/model/models.go index ca69ae4a..857fe8b1 100644 --- a/pkg/extensions/search/cve/model/models.go +++ b/pkg/extensions/search/cve/model/models.go @@ -6,6 +6,11 @@ import ( godigest "github.com/opencontainers/go-digest" ) +type ImageCVESummary struct { + Count int + MaxSeverity string +} + //nolint:tagliatelle // graphQL schema type CVE struct { ID string `json:"Id"` @@ -47,8 +52,15 @@ type Descriptor struct { MediaType string } +type DescriptorInfo struct { + Descriptor + + Timestamp time.Time +} + type TagInfo struct { - Name string + Tag string Descriptor Descriptor + Manifests []DescriptorInfo Timestamp time.Time } diff --git a/pkg/extensions/search/cve/model/pagination.go b/pkg/extensions/search/cve/model/pagination.go new file mode 100644 index 00000000..f246cf0a --- /dev/null +++ b/pkg/extensions/search/cve/model/pagination.go @@ -0,0 +1,9 @@ +package model + +type SortCriteria string + +type PageInput struct { + Limit int + Offset int + SortBy SortCriteria +} diff --git a/pkg/extensions/search/cve/pagination.go b/pkg/extensions/search/cve/pagination.go index 1bdfd258..53017c64 100644 --- a/pkg/extensions/search/cve/pagination.go +++ b/pkg/extensions/search/cve/pagination.go @@ -9,16 +9,14 @@ import ( cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" ) -type SortCriteria string - const ( - AlphabeticAsc = SortCriteria("ALPHABETIC_ASC") - AlphabeticDsc = SortCriteria("ALPHABETIC_DSC") - SeverityDsc = SortCriteria("SEVERITY") + AlphabeticAsc = cvemodel.SortCriteria("ALPHABETIC_ASC") + AlphabeticDsc = cvemodel.SortCriteria("ALPHABETIC_DSC") + SeverityDsc = cvemodel.SortCriteria("SEVERITY") ) -func SortFunctions() map[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{ +func SortFunctions() map[cvemodel.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, AlphabeticDsc: SortByAlphabeticDsc, SeverityDsc: SortBySeverity, @@ -56,12 +54,12 @@ type PageFinder interface { type CvePageFinder struct { limit int offset int - sortBy SortCriteria + sortBy cvemodel.SortCriteria pageBuffer []cvemodel.CVE 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 == "" { sortBy = SeverityDsc } @@ -131,9 +129,3 @@ func (bpt *CvePageFinder) Page() ([]cvemodel.CVE, common.PageInfo) { return cves, *pageInfo } - -type PageInput struct { - Limit int - Offset int - SortBy SortCriteria -} diff --git a/pkg/extensions/search/cve/pagination_test.go b/pkg/extensions/search/cve/pagination_test.go index 87840743..1c7cedb7 100644 --- a/pkg/extensions/search/cve/pagination_test.go +++ b/pkg/extensions/search/cve/pagination_test.go @@ -187,7 +187,7 @@ func TestCVEPagination(t *testing.T) { Convey("Page", func() { Convey("defaults", func() { // 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(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -198,7 +198,7 @@ func TestCVEPagination(t *testing.T) { 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(len(cves), 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", "", - cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) + cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -228,7 +228,7 @@ func TestCVEPagination(t *testing.T) { sort.Strings(cveIds) cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", - cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) + cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -239,7 +239,7 @@ func TestCVEPagination(t *testing.T) { sort.Sort(sort.Reverse(sort.StringSlice(cveIds))) cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", - cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc}) + cvemodel.PageInput{SortBy: cveinfo.AlphabeticDsc}) So(err, ShouldBeNil) So(len(cves), 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", "", - cveinfo.PageInput{SortBy: cveinfo.SeverityDsc}) + cvemodel.PageInput{SortBy: cveinfo.SeverityDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -267,7 +267,7 @@ func TestCVEPagination(t *testing.T) { 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, Offset: 1, SortBy: cveinfo.AlphabeticAsc, @@ -281,7 +281,7 @@ func TestCVEPagination(t *testing.T) { So(cves[1].ID, ShouldEqual, "CVE2") 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, Offset: 1, SortBy: cveinfo.AlphabeticDsc, @@ -294,7 +294,7 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE3") 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, Offset: 1, SortBy: cveinfo.SeverityDsc, @@ -311,7 +311,7 @@ func TestCVEPagination(t *testing.T) { } 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, Offset: 20, SortBy: cveinfo.AlphabeticAsc, @@ -327,7 +327,7 @@ func TestCVEPagination(t *testing.T) { }) 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, Offset: 3, SortBy: cveinfo.AlphabeticAsc, @@ -340,7 +340,7 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE3") 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, Offset: 3, SortBy: cveinfo.AlphabeticDsc, @@ -353,7 +353,7 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE1") 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, Offset: 3, SortBy: cveinfo.SeverityDsc, diff --git a/pkg/extensions/search/cve/trivy/scanner.go b/pkg/extensions/search/cve/trivy/scanner.go index 9b1fd080..fc47a704 100644 --- a/pkg/extensions/search/cve/trivy/scanner.go +++ b/pkg/extensions/search/cve/trivy/scanner.go @@ -22,6 +22,7 @@ import ( _ "modernc.org/sqlite" zerr "zotregistry.io/zot/errors" + zcommon "zotregistry.io/zot/pkg/common" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/repodb" @@ -181,54 +182,61 @@ func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) { return report, nil } -func (scanner Scanner) IsImageFormatScannable(repo, tag string) (bool, error) { - image := repo + ":" + tag +func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) { + var ( + digestStr = ref + mediaType string + ) - if scanner.cache.Get(image) != nil { - return true, nil + if zcommon.IsTag(ref) { + 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) - if err != nil { - return false, err - } + return scanner.IsImageMediaScannable(repo, digestStr, mediaType) +} - var ok bool +func (scanner Scanner) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) { + image := repo + "@" + digestStr - imageDescriptor, ok := repoMeta.Tags[tag] - if !ok { - return false, zerr.ErrTagMetaNotFound - } - - switch imageDescriptor.MediaType { + switch mediaType { case ispec.MediaTypeImageManifest: - ok, err := scanner.isManifestScanable(imageDescriptor) + ok, err := scanner.isManifestScanable(digestStr) if err != nil { return ok, fmt.Errorf("image '%s' %w", image, err) } return ok, nil case ispec.MediaTypeImageIndex: - ok, err := scanner.isIndexScanable(imageDescriptor) + ok, err := scanner.isIndexScanable(digestStr) if err != nil { return ok, fmt.Errorf("image '%s' %w", image, err) } return ok, nil + default: + return false, nil } - - return false, nil } -func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, error) { - manifestDigestStr := descriptor.Digest - - manifestDigest, err := godigest.Parse(manifestDigestStr) - if err != nil { - return false, err +func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) { + if scanner.cache.Get(digestStr) != nil { + return true, nil } - manifestData, err := scanner.repoDB.GetManifestData(manifestDigest) + manifestData, err := scanner.repoDB.GetManifestData(godigest.Digest(digestStr)) if err != nil { return false, err } @@ -257,18 +265,98 @@ func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, e 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 } func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) { - if scanner.cache.Get(image) != nil { - return scanner.cache.Get(image), nil + var ( + 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() opts := scanner.getTrivyOptions(image) @@ -276,8 +364,6 @@ func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) scanner.dbLock.Unlock() if err != nil { //nolint: wsl - scanner.log.Error().Err(err).Str("image", image).Msg("unable to scan image") - 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 } +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. func (scanner Scanner) UpdateDB() error { // We need a lock as using multiple substores each with it's own DB diff --git a/pkg/extensions/search/cve/trivy/scanner_internal_test.go b/pkg/extensions/search/cve/trivy/scanner_internal_test.go index 3651ceee..56fe8580 100644 --- a/pkg/extensions/search/cve/trivy/scanner_internal_test.go +++ b/pkg/extensions/search/cve/trivy/scanner_internal_test.go @@ -18,6 +18,7 @@ import ( zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/extensions/monitoring" + "zotregistry.io/zot/pkg/extensions/search/cve/model" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/meta/bolt" "zotregistry.io/zot/pkg/meta/repodb" @@ -27,6 +28,7 @@ import ( "zotregistry.io/zot/pkg/storage/local" storageTypes "zotregistry.io/zot/pkg/storage/types" "zotregistry.io/zot/pkg/test" + "zotregistry.io/zot/pkg/test/mocks" ) func generateTestImage(storeController storage.StoreController, image string) { @@ -100,9 +102,6 @@ func TestMultipleStoragePath(t *testing.T) { repoDB, err := boltdb_wrapper.NewBoltDBWrapper(boltDriver, log) So(err, ShouldBeNil) - err = repodb.ParseStorage(repoDB, storeController, log) - So(err, ShouldBeNil) - scanner := NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", "", log) So(scanner.storeController.DefaultStore, ShouldNotBeNil) @@ -125,6 +124,9 @@ func TestMultipleStoragePath(t *testing.T) { generateTestImage(storeController, img1) generateTestImage(storeController, img2) + err = repodb.ParseStorage(repoDB, storeController, log) + So(err, ShouldBeNil) + // Try to scan without the DB being downloaded _, err = scanner.ScanImage(img0) So(err, ShouldNotBeNil) @@ -508,3 +510,138 @@ func TestDefaultTrivyDBUrl(t *testing.T) { 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) + }) + }) +} diff --git a/pkg/extensions/search/cve/trivy/scanner_test.go b/pkg/extensions/search/cve/trivy/scanner_test.go new file mode 100644 index 00000000..a74a9d30 --- /dev/null +++ b/pkg/extensions/search/cve/trivy/scanner_test.go @@ -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) + }) +} diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 8b241ae3..c3c41292 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -1679,7 +1679,7 @@ type Query { Returns a CVE list for the image specified in the argument """ CVEListForImage( - "Image name in format ` + "`" + `repository:tag` + "`" + `" + "Image name in format ` + "`" + `repository:tag` + "`" + ` or ` + "`" + `repository@digest` + "`" + `" image: String!, "Sets the parameters of the requested page" requestedPage: PageInput diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index f3add25e..e33be12a 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -125,10 +125,10 @@ func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.Rep } pageInput := repodb.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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{} } - pageInput := cveinfo.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), - SortBy: cveinfo.SortCriteria( - safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity), + pageInput := cvemodel.PageInput{ + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), + SortBy: cvemodel.SortCriteria( + safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity), ), } - repo, ref, isTag := zcommon.GetImageDirAndReference(image) + repo, ref, _ := zcommon.GetImageDirAndReference(image) if ref == "" { 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) if err != nil { return &gql_generated.CVEResultForImage{}, err @@ -365,8 +361,17 @@ func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) repodb.FilterFunc { manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String() for _, tagInfo := range tagsInfo { - if tagInfo.Descriptor.Digest.String() == manifestDigest { - return true + switch tagInfo.Descriptor.MediaType { + 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 pageInput := repodb.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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 pageInput := repodb.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), SortBy: repodb.SortCriteria( - safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), + safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), ), } @@ -535,10 +540,10 @@ func repoListWithNewestImage( } pageInput := repodb.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), SortBy: repodb.SortCriteria( - safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), + safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), ), } @@ -620,10 +625,10 @@ func getFilteredPaginatedRepos( } pageInput := repodb.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), 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 } - manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(digest)) + manifestData, err := repoDB.GetManifestData(godigest.Digest(digest)) if err != nil { graphql.AddError(ctx, fmt.Errorf("resolver: failed to get manifest meta for image %s:%s with manifest digest %s %w", repo, tag, digest, err)) @@ -1125,7 +1130,10 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv continue } - manifestMetaMap[digest] = manifestMeta + manifestMetaMap[digest] = repodb.ManifestMetadata{ + ManifestBlob: manifestData.ManifestBlob, + ConfigBlob: manifestData.ConfigBlob, + } case ispec.MediaTypeImageIndex: digest := descriptor.Digest @@ -1154,7 +1162,7 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv var errorOccured bool for _, descriptor := range indexContent.Manifests { - manifestMeta, err := repoDB.GetManifestMeta(repo, descriptor.Digest) + manifestData, err := repoDB.GetManifestData(descriptor.Digest) if err != nil { graphql.AddError(ctx, 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 } - manifestMetaMap[descriptor.Digest.String()] = manifestMeta + manifestMetaMap[descriptor.Digest.String()] = repodb.ManifestMetadata{ + ManifestBlob: manifestData.ManifestBlob, + ConfigBlob: manifestData.ConfigBlob, + } } if errorOccured { @@ -1210,7 +1221,7 @@ func (p timeSlice) Swap(i, j int) { 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 { return *pointer } @@ -1236,10 +1247,10 @@ func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInf } pageInput := repodb.PageInput{ - Limit: safeDerefferencing(requestedPage.Limit, 0), - Offset: safeDerefferencing(requestedPage.Offset, 0), + Limit: safeDereferencing(requestedPage.Limit, 0), + Offset: safeDereferencing(requestedPage.Offset, 0), SortBy: repodb.SortCriteria( - safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), + safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), ), } diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index f7634edd..fbe286a4 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "testing" "time" @@ -1987,7 +1988,12 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { digest, ok := tagsMap[image] 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() { @@ -2075,7 +2081,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo repoWithDigestRef := fmt.Sprintf("repo@%s", dig) _, 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) So(err, ShouldBeNil) @@ -3304,12 +3310,12 @@ func TestExpandedRepoInfo(t *testing.T) { }, }, nil }, - GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) { + GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) { switch manifestDigest { case "errorDigest": - return repodb.ManifestMetadata{}, ErrTestError + return repodb.ManifestData{}, ErrTestError default: - return repodb.ManifestMetadata{ + return repodb.ManifestData{ ManifestBlob: []byte("{}"), ConfigBlob: []byte("{}"), }, nil diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 690305c6..26aa47d8 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -592,7 +592,7 @@ type Query { Returns a CVE list for the image specified in the argument """ CVEListForImage( - "Image name in format `repository:tag`" + "Image name in format `repository:tag` or `repository@digest`" image: String!, "Sets the parameters of the requested page" requestedPage: PageInput diff --git a/pkg/extensions/search/search_test.go b/pkg/extensions/search/search_test.go index 087a36c7..d61247e4 100644 --- a/pkg/extensions/search/search_test.go +++ b/pkg/extensions/search/search_test.go @@ -235,7 +235,9 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { // Setup test CVE data in mock scanner scanner := mocks.CveScannerMock{ 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{ "CVE1": { ID: "CVE1", @@ -258,7 +260,9 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { }, 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{ "CVE3": { ID: "CVE3", @@ -275,7 +279,8 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { }, nil } - if image == "test-repo:latest" { + if image == "test-repo:latest" || + image == "test-repo@sha256:9f8e1a125c4fb03a0f157d75999b73284ccc5cba18eb772e4643e3499343607e" { return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", @@ -320,12 +325,20 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo { return false, err } - manifestDigestStr, ok := repoMeta.Tags[inputTag] - if !ok { - return false, zerr.ErrTagMetaNotFound + manifestDigestStr := reference + + 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 { return false, err } @@ -758,6 +771,7 @@ func TestRepoListWithNewestImage(t *testing.T) { Name NewestImage{ Tag + Digest Vulnerabilities{ MaxSeverity Count diff --git a/pkg/meta/common/common.go b/pkg/meta/common/common.go index 387d40dc..e26db3f2 100644 --- a/pkg/meta/common/common.go +++ b/pkg/meta/common/common.go @@ -176,7 +176,7 @@ func GetRepoTag(searchText string) (string, string, error) { splitSlice := strings.Split(searchText, ":") if len(splitSlice) != repoTagCount { - return "", "", zerr.ErrInvalidRepoTagFormat + return "", "", zerr.ErrInvalidRepoRefFormat } repo := strings.TrimSpace(splitSlice[0]) @@ -329,6 +329,7 @@ func FilterDataByRepo(foundRepos []repodb.RepoMetadata, manifestMetadataMap map[ foundindexDataMap[descriptor.Digest] = indexData default: + continue } } } diff --git a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go index 4b6bb767..a49d00c8 100644 --- a/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go +++ b/pkg/meta/repodb/boltdb-wrapper/boltdb_wrapper.go @@ -1339,7 +1339,6 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, matchedTags := make(map[string]repodb.Descriptor) // take all manifestMetas for tag, descriptor := range repoMeta.Tags { - matchedTags[tag] = descriptor switch descriptor.MediaType { case ispec.MediaTypeImageManifest: 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) } - if !filter(repoMeta, manifestMeta) { - delete(matchedTags, tag) - - continue + if filter(repoMeta, manifestMeta) { + matchedTags[tag] = descriptor + manifestMetadataMap[manifestDigest] = manifestMeta } - - manifestMetadataMap[manifestDigest] = manifestMeta case ispec.MediaTypeImageIndex: 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) } - manifestHasBeenMatched := false + matchedManifests := []ispec.Descriptor{} for _, manifest := range indexContent.Manifests { 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) } - manifestMetadataMap[manifestDigest] = manifestMeta - if filter(repoMeta, manifestMeta) { - manifestHasBeenMatched = true + matchedManifests = append(matchedManifests, manifest) + manifestMetadataMap[manifestDigest] = manifestMeta } } - if !manifestHasBeenMatched { - delete(matchedTags, tag) + if len(matchedManifests) > 0 { + indexContent.Manifests = matchedManifests - for _, manifest := range indexContent.Manifests { - delete(manifestMetadataMap, manifest.Digest.String()) + indexBlob, err := json.Marshal(indexContent) + if err != nil { + return err } - continue - } + indexData.IndexBlob = indexBlob - indexDataMap[indexDigest] = indexData + indexDataMap[indexDigest] = indexData + matchedTags[tag] = descriptor + } default: bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type") diff --git a/pkg/meta/repodb/common.go b/pkg/meta/repodb/common.go index 3fdf4917..f9119978 100644 --- a/pkg/meta/repodb/common.go +++ b/pkg/meta/repodb/common.go @@ -2,6 +2,11 @@ package repodb import ( "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 @@ -55,3 +60,33 @@ func SortByDownloads(pageBuffer []DetailedRepoMeta) func(i, j int) bool { 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 +} diff --git a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go index f18c5e12..6f02e19f 100644 --- a/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go +++ b/pkg/meta/repodb/dynamodb-wrapper/dynamo_wrapper.go @@ -1122,9 +1122,8 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc, repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name) matchedTags := make(map[string]repodb.Descriptor) - for tag, descriptor := range repoMeta.Tags { - matchedTags[tag] = descriptor + for tag, descriptor := range repoMeta.Tags { switch descriptor.MediaType { case ispec.MediaTypeImageManifest: 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) } - if !filter(repoMeta, manifestMeta) { - delete(matchedTags, tag) - - continue + if filter(repoMeta, manifestMeta) { + matchedTags[tag] = descriptor + manifestMetadataMap[manifestDigest] = manifestMeta } - - manifestMetadataMap[manifestDigest] = manifestMeta case ispec.MediaTypeImageIndex: 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) } - manifestHasBeenMatched := false + matchedManifests := []ispec.Descriptor{} for _, manifest := range indexContent.Manifests { 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) } - manifestMetadataMap[manifestDigest] = manifestMeta - if filter(repoMeta, manifestMeta) { - manifestHasBeenMatched = true + matchedManifests = append(matchedManifests, manifest) + manifestMetadataMap[manifestDigest] = manifestMeta } } - if !manifestHasBeenMatched { - delete(matchedTags, tag) + if len(matchedManifests) > 0 { + indexContent.Manifests = matchedManifests - for _, manifest := range indexContent.Manifests { - delete(manifestMetadataMap, manifest.Digest.String()) + indexBlob, err := json.Marshal(indexContent) + 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: dwr.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type") diff --git a/pkg/test/common.go b/pkg/test/common.go index a4bab756..409eb0d0 100644 --- a/pkg/test/common.go +++ b/pkg/test/common.go @@ -20,6 +20,7 @@ import ( "os" "path" "path/filepath" + "runtime" "strings" "sync" "time" @@ -45,6 +46,7 @@ import ( "oras.land/oras-go/v2/registry/remote" "oras.land/oras-go/v2/registry/remote/auth" + zerr "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/meta/repodb" "zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/test/inject" @@ -56,6 +58,8 @@ const ( SleepTime = 100 * time.Millisecond ) +var vulnerableLayer []byte //nolint: gochecknoglobals + var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals // which: manifest, config, layer @@ -604,15 +608,8 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif configDigest := godigest.FromBytes(configBlob) - layer := make([]byte, layerSize) - - _, err = rand.Read(layer) - if err != nil { - return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err - } - layers := [][]byte{ - layer, + GetRandomLayer(layerSize), } schemaVersion := 2 @@ -639,6 +636,138 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif 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) { const layerSize = 20 diff --git a/pkg/test/mocks/cve_mock.go b/pkg/test/mocks/cve_mock.go index cc4368c8..83e01cfa 100644 --- a/pkg/test/mocks/cve_mock.go +++ b/pkg/test/mocks/cve_mock.go @@ -2,17 +2,18 @@ package mocks import ( "zotregistry.io/zot/pkg/common" - cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" ) type CveInfoMock struct { GetImageListForCVEFn 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) 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 UpdateDBFn func() error } @@ -34,7 +35,7 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo } func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string, - searchedCVE string, pageInput cveinfo.PageInput, + searchedCVE string, pageInput cvemodel.PageInput, ) ( []cvemodel.CVE, common.PageInfo, @@ -48,12 +49,21 @@ func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string, } func (cveInfo CveInfoMock) GetCVESummaryForImage(repo string, reference string, -) (cveinfo.ImageCVESummary, error) { +) (cvemodel.ImageCVESummary, error) { if cveInfo.GetCVESummaryForImageFn != nil { 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 { @@ -74,6 +84,7 @@ func (cveInfo CveInfoMock) UpdateDB() error { type CveScannerMock struct { 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) CompareSeveritiesFn func(severity1, severity2 string) int UpdateDBFn func() error @@ -87,6 +98,14 @@ func (scanner CveScannerMock) IsImageFormatScannable(repo string, reference stri 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) { if scanner.ScanImageFn != nil { return scanner.ScanImageFn(image) diff --git a/pkg/test/oci-layout/oci_layout.go b/pkg/test/oci-layout/oci_layout.go index fedf8eff..31ec17a4 100644 --- a/pkg/test/oci-layout/oci_layout.go +++ b/pkg/test/oci-layout/oci_layout.go @@ -216,7 +216,7 @@ func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]cvemodel tagsInfo = append(tagsInfo, cvemodel.TagInfo{ - Name: val, + Tag: val, Timestamp: timeStamp, Descriptor: cvemodel.Descriptor{ Digest: digest, diff --git a/pkg/test/oci-layout/oci_layout_test.go b/pkg/test/oci-layout/oci_layout_test.go index d7755a54..472a2a19 100644 --- a/pkg/test/oci-layout/oci_layout_test.go +++ b/pkg/test/oci-layout/oci_layout_test.go @@ -450,7 +450,7 @@ func TestTagsInfo(t *testing.T) { allTags := make([]cvemodel.TagInfo, 0) firstTag := cvemodel.TagInfo{ - Name: "1.0.0", + Tag: "1.0.0", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -458,7 +458,7 @@ func TestTagsInfo(t *testing.T) { Timestamp: time.Now(), } secondTag := cvemodel.TagInfo{ - Name: "1.0.1", + Tag: "1.0.1", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -466,7 +466,7 @@ func TestTagsInfo(t *testing.T) { Timestamp: time.Now(), } thirdTag := cvemodel.TagInfo{ - Name: "1.0.2", + Tag: "1.0.2", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -474,7 +474,7 @@ func TestTagsInfo(t *testing.T) { Timestamp: time.Now(), } fourthTag := cvemodel.TagInfo{ - Name: "1.0.3", + Tag: "1.0.3", Descriptor: cvemodel.Descriptor{ Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb", MediaType: ispec.MediaTypeImageManifest, @@ -485,6 +485,6 @@ func TestTagsInfo(t *testing.T) { allTags = append(allTags, firstTag, secondTag, thirdTag, fourthTag) latestTag := ocilayout.GetLatestTag(allTags) - So(latestTag.Name, ShouldEqual, "1.0.3") + So(latestTag.Tag, ShouldEqual, "1.0.3") }) }