//go:build search package search //nolint import ( "context" "errors" "fmt" "testing" "time" "github.com/99designs/gqlgen/graphql" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" zerr "zotregistry.dev/zot/errors" "zotregistry.dev/zot/pkg/common" "zotregistry.dev/zot/pkg/extensions/search/convert" cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve" cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model" "zotregistry.dev/zot/pkg/extensions/search/gql_generated" "zotregistry.dev/zot/pkg/log" "zotregistry.dev/zot/pkg/meta/boltdb" mConvert "zotregistry.dev/zot/pkg/meta/convert" mTypes "zotregistry.dev/zot/pkg/meta/types" reqCtx "zotregistry.dev/zot/pkg/requestcontext" "zotregistry.dev/zot/pkg/storage" . "zotregistry.dev/zot/pkg/test/image-utils" "zotregistry.dev/zot/pkg/test/mocks" ociutils "zotregistry.dev/zot/pkg/test/oci-utils" ) var ErrTestError = errors.New("TestError") func TestResolverGlobalSearch(t *testing.T) { Convey("globalSearch", t, func() { const query = "repo1" Convey("MetaDB SearchRepos error", func() { mockMetaDB := mocks.MetaDBMock{ SearchReposFn: func(ctx context.Context, searchText string, ) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} repos, images, layers, err := globalSearch(responseContext, query, mockMetaDB, &gql_generated.Filter{}, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) Convey("paginated fail", func() { pageInput := &gql_generated.PageInput{ Limit: ref(-1), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, _, _, err := globalSearch(responseContext, "repo", mocks.MetaDBMock{}, &gql_generated.Filter{}, pageInput, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) _, _, _, err = globalSearch(responseContext, "repo:tag", mocks.MetaDBMock{}, &gql_generated.Filter{}, pageInput, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) }) Convey("MetaDB SearchTags gives error", func() { mockMetaDB := mocks.MetaDBMock{ SearchTagsFn: func(ctx context.Context, searchText string) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, } const query = "repo1:1.0.1" mockCve := mocks.CveInfoMock{} responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) repos, images, layers, err := globalSearch(responseContext, query, mockMetaDB, &gql_generated.Filter{}, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) Convey("Searching by digest", func() { ctx := context.Background() query := "sha256:aabb12341baf2" mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) repos, images, layers, err := globalSearch(responseContext, query, mockMetaDB, &gql_generated.Filter{}, &gql_generated.PageInput{}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) Convey("Searching by digest with bad pagination", func() { ctx := context.Background() query := "sha256:aabb12341baf2" responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{}, &gql_generated.PageInput{Limit: ref(-10)}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) Convey("Searching by tag returns a filter error", func() { ctx := context.Background() query := ":test" mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) repos, images, layers, err := globalSearch(responseContext, query, mockMetaDB, &gql_generated.Filter{}, &gql_generated.PageInput{}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) Convey("Searching by tag returns a pagination error", func() { ctx := context.Background() query := ":test" responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{}, &gql_generated.PageInput{Limit: ref(-10)}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) Convey("Searching with a bad query", func() { ctx := context.Background() query := ":" responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) repos, images, layers, err := globalSearch(responseContext, query, mocks.MetaDBMock{}, &gql_generated.Filter{}, &gql_generated.PageInput{}, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images, ShouldBeEmpty) So(layers, ShouldBeEmpty) So(repos.Results, ShouldBeEmpty) }) }) } func TestRepoListWithNewestImage(t *testing.T) { Convey("RepoListWithNewestImage", t, func() { Convey("MetaDB SearchRepos error", func() { mockMetaDB := mocks.MetaDBMock{ SearchReposFn: func(ctx context.Context, searchText string) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} pageInput := gql_generated.PageInput{ Limit: ref(1), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaUpdateTime), } repos, err := repoListWithNewestImage(responseContext, mockCve, log.NewLogger("debug", ""), &pageInput, mockMetaDB) So(err, ShouldNotBeNil) So(repos.Results, ShouldBeEmpty) }) Convey("paginated fail", func() { pageInput := &gql_generated.PageInput{ Limit: ref(-1), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := repoListWithNewestImage(responseContext, mocks.CveInfoMock{}, log.NewLogger("debug", ""), pageInput, mocks.MetaDBMock{}) So(err, ShouldNotBeNil) }) Convey("Working SearchRepo function", func() { createTime := time.Now() createTime2 := createTime.Add(time.Second) img1 := CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{ Config: ispec.ImageConfig{ Labels: map[string]string{}, }, Created: &createTime, }).Build() img2 := CreateImageWith().DefaultLayers(). ImageConfig(ispec.Image{ Config: ispec.ImageConfig{ Labels: map[string]string{}, }, Created: &createTime2, }).Build() mockMetaDB := mocks.MetaDBMock{ SearchReposFn: func(ctx context.Context, searchText string) ([]mTypes.RepoMeta, error) { repos := []mTypes.RepoMeta{ { Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": { Digest: img1.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }, }, Signatures: map[mTypes.ImageDigest]mTypes.ManifestSignatures{ img1.DigestStr(): { "cosign": []mTypes.SignatureInfo{ {SignatureManifestDigest: "testSignature", LayersInfo: []mTypes.LayerInfo{}}, }, }, }, StarCount: 100, LastUpdatedImage: &mTypes.LastUpdatedImage{ Descriptor: mTypes.Descriptor{ Digest: img1.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }, Tag: "1.0.1", LastUpdated: &createTime, }, }, { Name: "repo2", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.2": { Digest: img2.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }, }, Signatures: map[mTypes.ImageDigest]mTypes.ManifestSignatures{ img1.DigestStr(): { "cosign": []mTypes.SignatureInfo{ {SignatureManifestDigest: "testSignature", LayersInfo: []mTypes.LayerInfo{}}, }, }, }, StarCount: 100, LastUpdatedImage: &mTypes.LastUpdatedImage{ Descriptor: mTypes.Descriptor{ Digest: img2.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }, Tag: "1.0.2", LastUpdated: &createTime2, }, }, } return repos, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string, ) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{ img1.DigestStr(): mConvert.GetImageManifestMeta(img1.Manifest, img1.Config, img1.ManifestDescriptor.Size, img1.ManifestDescriptor.Digest), img2.DigestStr(): mConvert.GetImageManifestMeta(img2.Manifest, img2.Config, img2.ManifestDescriptor.Size, img2.ManifestDescriptor.Digest), }, nil }, } Convey("MetaDB missing requestedPage", func() { responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} repos, err := repoListWithNewestImage(responseContext, mockCve, log.NewLogger("debug", ""), nil, mockMetaDB) So(err, ShouldBeNil) So(repos.Results, ShouldNotBeEmpty) }) Convey("MetaDB SearchRepo is successful", func() { pageInput := gql_generated.PageInput{ Limit: ref(2), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaUpdateTime), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} repos, err := repoListWithNewestImage(responseContext, mockCve, log.NewLogger("debug", ""), &pageInput, mockMetaDB) So(err, ShouldBeNil) So(repos, ShouldNotBeEmpty) So(len(repos.Results), ShouldEqual, 2) So(*repos.Results[0].Name, ShouldEqual, "repo2") So(*repos.Results[0].LastUpdated, ShouldEqual, createTime2) }) }) }) } func TestGetFilteredPaginatedRepos(t *testing.T) { ctx := context.Background() log := log.NewLogger("debug", "") Convey("getFilteredPaginatedRepos", t, func() { metaDB := mocks.MetaDBMock{} Convey("FilterRepos", func() { metaDB.FilterReposFn = func(ctx context.Context, rankName mTypes.FilterRepoNameFunc, filterFunc mTypes.FilterFullRepoFunc, ) ([]mTypes.RepoMeta, error) { return nil, ErrTestError } _, err := getFilteredPaginatedRepos(ctx, nil, func(repo string) bool { return true }, log, &gql_generated.PageInput{}, metaDB) So(err, ShouldNotBeNil) }) Convey("FilterImageMeta", func() { metaDB.FilterImageMetaFn = func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return nil, ErrTestError } _, err := getFilteredPaginatedRepos(ctx, nil, func(repo string) bool { return true }, log, &gql_generated.PageInput{}, metaDB) So(err, ShouldNotBeNil) }) Convey("PaginatedRepoMeta2RepoSummaries", func() { _, err := getFilteredPaginatedRepos(ctx, nil, func(repo string) bool { return true }, log, &gql_generated.PageInput{Limit: ref(-10)}, metaDB) So(err, ShouldNotBeNil) }) }) } func TestGetBookmarkedRepos(t *testing.T) { Convey("getBookmarkedRepos", t, func() { responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getBookmarkedRepos( responseContext, mocks.CveInfoMock{}, log.NewLogger("debug", ""), nil, mocks.MetaDBMock{ GetBookmarkedReposFn: func(ctx context.Context) ([]string, error) { return []string{}, ErrTestError }, }, ) So(err, ShouldNotBeNil) }) } func TestGetStarredRepos(t *testing.T) { Convey("getStarredRepos", t, func() { responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getStarredRepos( responseContext, mocks.CveInfoMock{}, log.NewLogger("debug", ""), nil, mocks.MetaDBMock{ GetStarredReposFn: func(ctx context.Context) ([]string, error) { return []string{}, ErrTestError }, }, ) So(err, ShouldNotBeNil) }) } func getTestRepoMetaWithImages(repo string, images []Image) mTypes.RepoMeta { tags := map[mTypes.Tag]mTypes.Descriptor{"": {}} statistics := map[mTypes.Tag]mTypes.DescriptorStatistics{"": {}} signatures := map[mTypes.ImageDigest]mTypes.ManifestSignatures{"": {}} referrers := map[string][]mTypes.ReferrerInfo{"": {}} for i := range images { tags[images[i].DigestStr()] = mTypes.Descriptor{} statistics[images[i].DigestStr()] = mTypes.DescriptorStatistics{} signatures[images[i].DigestStr()] = mTypes.ManifestSignatures{} referrers[images[i].DigestStr()] = []mTypes.ReferrerInfo{} } return mTypes.RepoMeta{ Name: repo, Tags: tags, Statistics: statistics, Signatures: signatures, Referrers: referrers, } } func TestImageListForDigest(t *testing.T) { Convey("getImageList", t, func() { Convey("no page requested, FilterTagsFn returns error", func() { mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getImageListForDigest(responseContext, "invalid", mockMetaDB, mocks.CveInfoMock{}, nil) So(err, ShouldNotBeNil) }) Convey("Paginated convert fails", func() { responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getImageListForDigest(responseContext, "invalid", mocks.MetaDBMock{}, mocks.CveInfoMock{}, &gql_generated.PageInput{Limit: ref(-1)}) So(err, ShouldNotBeNil) }) Convey("valid imageListForDigest returned for matching manifest digest", func() { img1, img2 := CreateRandomImage(), CreateRandomImage() mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { fullImageMetaList := []mTypes.ImageMeta{img1.AsImageMeta(), img2.AsImageMeta()} repoMeta := getTestRepoMetaWithImages("repo", []Image{img1, img2}) tags := []string{"tag1", "tag2"} acceptedImages := []mTypes.FullImageMeta{} for i := range fullImageMetaList { if filterFunc(repoMeta, fullImageMetaList[i]) { acceptedImages = append(acceptedImages, convert.GetFullImageMeta(tags[i], repoMeta, fullImageMetaList[i])) continue } } return acceptedImages, nil }, } pageInput := gql_generated.PageInput{ Limit: ref(1), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) imageSummaries, err := getImageListForDigest(responseContext, img1.DigestStr(), mockMetaDB, mocks.CveInfoMock{}, &pageInput) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 1) imageSummaries, err = getImageListForDigest(responseContext, "invalid", mockMetaDB, mocks.CveInfoMock{}, &pageInput) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 0) imageSummaries, err = getImageListForDigest(responseContext, img1.Manifest.Config.Digest.String(), mockMetaDB, mocks.CveInfoMock{}, &pageInput) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 1) imageSummaries, err = getImageListForDigest(responseContext, img1.Manifest.Layers[0].Digest.String(), mockMetaDB, mocks.CveInfoMock{}, &pageInput) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 1) }) Convey("valid imageListForDigest, multiple matching tags", func() { img1 := CreateRandomImage() mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { fullImageMetaList := []mTypes.ImageMeta{img1.AsImageMeta()} repoMeta := getTestRepoMetaWithImages("repo", []Image{img1, img1}) tags := []string{"tag1", "tag2"} acceptedImages := []mTypes.FullImageMeta{} for i := range fullImageMetaList { if filterFunc(repoMeta, fullImageMetaList[i]) { acceptedImages = append(acceptedImages, convert.GetFullImageMeta(tags[i], repoMeta, fullImageMetaList[i])) continue } } return acceptedImages, nil }, } pageInput := gql_generated.PageInput{ Limit: ref(1), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) imageSummaries, err := getImageListForDigest(responseContext, img1.DigestStr(), mockMetaDB, mocks.CveInfoMock{}, &pageInput) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 1) }) }) } func TestGetImageSummaryError(t *testing.T) { Convey("getImageSummary", t, func() { metaDB := mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{Tags: map[mTypes.Tag]mTypes.Descriptor{"tag": {}}}, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return nil, ErrTestError }, } log := log.NewLogger("debug", "") _, err := getImageSummary(context.Background(), "repo", "tag", nil, convert.SkipQGLField{}, metaDB, nil, log) So(err, ShouldNotBeNil) }) } func TestImageListError(t *testing.T) { Convey("getImageList", t, func() { testLogger := log.NewLogger("debug", "/dev/null") Convey("no page requested, SearchRepoFn returns error", func() { mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getImageList(responseContext, "test", mockMetaDB, mocks.CveInfoMock{}, nil, testLogger) So(err, ShouldNotBeNil) }) Convey("Paginated convert fails", func() { responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getImageList(responseContext, "test", mocks.MetaDBMock{}, mocks.CveInfoMock{}, &gql_generated.PageInput{Limit: ref(-1)}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) }) Convey("valid repoList returned", func() { mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { repoName := "correct-repo" if !filterRepoTag(repoName, "tag") { return []mTypes.FullImageMeta{}, nil } image := CreateDefaultImage() repoMeta := mTypes.RepoMeta{ Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{image.DigestStr(): { Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }}, } return []mTypes.FullImageMeta{convert.GetFullImageMeta("tag", repoMeta, image.AsImageMeta())}, nil }, } pageInput := gql_generated.PageInput{ Limit: ref(1), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) imageSummaries, err := getImageList(responseContext, "correct-repo", mockMetaDB, mocks.CveInfoMock{}, &pageInput, testLogger) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 1) imageSummaries, err = getImageList(responseContext, "invalid", mockMetaDB, mocks.CveInfoMock{}, &pageInput, testLogger) So(err, ShouldBeNil) So(len(imageSummaries.Results), ShouldEqual, 0) }) }) } func TestGetReferrers(t *testing.T) { Convey("getReferrers", t, func() { referredDigest := godigest.FromString("t").String() Convey("referredDigest is empty", func() { testLogger := log.NewLogger("debug", "") _, err := getReferrers(mocks.MetaDBMock{}, "test", "", nil, testLogger) So(err, ShouldNotBeNil) }) Convey("GetReferrers returns error", func() { testLogger := log.NewLogger("debug", "") mockedStore := mocks.MetaDBMock{ GetReferrersInfoFn: func(repo string, referredDigest godigest.Digest, artifactTypes []string, ) ([]mTypes.ReferrerInfo, error) { return nil, ErrTestError }, } _, err := getReferrers(mockedStore, "test", referredDigest, nil, testLogger) So(err, ShouldNotBeNil) }) Convey("GetReferrers return index of descriptors", func() { testLogger := log.NewLogger("debug", "") referrerDescriptor := ispec.Descriptor{ MediaType: ispec.MediaTypeImageManifest, ArtifactType: "com.artifact.test", Size: 403, Digest: godigest.FromString("test"), Annotations: map[string]string{ "key": "value", }, } mockedStore := mocks.MetaDBMock{ GetReferrersInfoFn: func(repo string, referredDigest godigest.Digest, artifactTypes []string, ) ([]mTypes.ReferrerInfo, error) { return []mTypes.ReferrerInfo{ { Digest: referrerDescriptor.Digest.String(), MediaType: referrerDescriptor.MediaType, ArtifactType: referrerDescriptor.ArtifactType, Size: int(referrerDescriptor.Size), Annotations: referrerDescriptor.Annotations, }, }, nil }, } referrers, err := getReferrers(mockedStore, "test", referredDigest, nil, testLogger) So(err, ShouldBeNil) So(*referrers[0].ArtifactType, ShouldEqual, referrerDescriptor.ArtifactType) So(*referrers[0].MediaType, ShouldEqual, referrerDescriptor.MediaType) So(*referrers[0].Size, ShouldEqual, referrerDescriptor.Size) So(*referrers[0].Digest, ShouldEqual, referrerDescriptor.Digest.String()) So(*referrers[0].Annotations[0].Value, ShouldEqual, referrerDescriptor.Annotations["key"]) }) }) } func TestQueryResolverErrors(t *testing.T) { Convey("Errors", t, func() { log := log.NewLogger("debug", "") ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) Convey("GlobalSearch error bad requested page", func() { resolverConfig := NewResolver(log, storage.StoreController{}, mocks.MetaDBMock{}, mocks.CveInfoMock{}) resolver := queryResolver{resolverConfig} pageInput := gql_generated.PageInput{ Limit: ref(-1), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } _, err := resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, &pageInput) So(err, ShouldNotBeNil) pageInput = gql_generated.PageInput{ Limit: ref(0), Offset: ref(-1), SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } _, err = resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, &pageInput) So(err, ShouldNotBeNil) }) Convey("GlobalSearch error filte image meta", func() { resolverConfig := NewResolver(log, storage.StoreController{}, mocks.MetaDBMock{ FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return nil, ErrTestError }, }, mocks.CveInfoMock{}) resolver := queryResolver{resolverConfig} _, err := resolver.GlobalSearch(ctx, "some_string", &gql_generated.Filter{}, getGQLPageInput(1, 1)) So(err, ShouldNotBeNil) }) Convey("CVEDiffListForImages nill cveinfo", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool, ) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{}, ErrTestError }, }, nil, ) qr := queryResolver{resolverConfig} _, err := qr.CVEDiffListForImages(ctx, gql_generated.ImageInput{}, gql_generated.ImageInput{}, &gql_generated.PageInput{}, nil, nil) So(err, ShouldNotBeNil) }) Convey("CVEDiffListForImages error", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool, ) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.CVEDiffListForImages(ctx, gql_generated.ImageInput{}, gql_generated.ImageInput{}, &gql_generated.PageInput{}, nil, nil) So(err, ShouldNotBeNil) }) Convey("ImageListForCve error in GetMultipleRepoMeta", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool, ) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.ImageListForCve(ctx, "cve1", &gql_generated.Filter{}, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("ImageListForCve error in FilterTags", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.ImageListForCve(ctx, "cve1", &gql_generated.Filter{}, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("ImageListWithCVEFixed error in FilterTags", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.ImageListWithCVEFixed(ctx, "cve1", "image", &gql_generated.Filter{}, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("RepoListWithNewestImage repoListWithNewestImage() filter image meta error", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ SearchReposFn: func(ctx context.Context, searchText string, ) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{}, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return nil, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.RepoListWithNewestImage(ctx, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("RepoListWithNewestImage repoListWithNewestImage() errors mocked StoreController", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{}, }, mocks.MetaDBMock{ SearchReposFn: func(ctx context.Context, searchText string, ) ([]mTypes.RepoMeta, error) { return nil, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.RepoListWithNewestImage(ctx, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("RepoListWithNewestImage repoListWithNewestImage() errors valid StoreController", func() { resolverConfig := NewResolver( log, storage.StoreController{}, mocks.MetaDBMock{ SearchReposFn: func(ctx context.Context, searchText string, ) ([]mTypes.RepoMeta, error) { return nil, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.RepoListWithNewestImage(ctx, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("ImageList getImageList() errors", func() { resolverConfig := NewResolver( log, storage.StoreController{}, mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.ImageList(ctx, "repo", &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("DerivedImageList ExpandedRepoInfo() errors", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{ GetRepositoriesFn: func() ([]string, error) { return []string{"sub1/repo"}, nil }, GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte("{}"), "digest", "str", nil }, }, }, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.DerivedImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("BaseImageList ExpandedRepoInfo() errors", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{ GetRepositoriesFn: func() ([]string, error) { return []string{"sub1/repo"}, nil }, GetImageManifestFn: func(repo, reference string) ([]byte, godigest.Digest, string, error) { return []byte("{}"), "digest", "str", nil }, }, }, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{}, ErrTestError }, }, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.BaseImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("DerivedImageList and BaseImage List FilterTags() errors", func() { image := CreateDefaultImage() resolverConfig := NewResolver( log, storage.StoreController{}, mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Name: "repo", Tags: map[mTypes.Tag]mTypes.Descriptor{ "tag": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }, nil }, GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) { return image.AsImageMeta(), nil }, }, mocks.CveInfoMock{}, ) resolver := queryResolver{resolverConfig} _, err := resolver.DerivedImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) _, err = resolver.BaseImageList(ctx, "repo:tag", nil, &gql_generated.PageInput{}) So(err, ShouldNotBeNil) }) Convey("GetReferrers error", func() { resolverConfig := NewResolver( log, storage.StoreController{ DefaultStore: mocks.MockedImageStore{ GetReferrersFn: func(repo string, digest godigest.Digest, artifactTypes []string) (ispec.Index, error) { return ispec.Index{}, ErrTestError }, }, }, mocks.MetaDBMock{}, mocks.CveInfoMock{}, ) qr := queryResolver{resolverConfig} _, err := qr.Referrers(ctx, "repo", "", nil) So(err, ShouldNotBeNil) }) }) } func TestCVEResolvers(t *testing.T) { //nolint:gocyclo ctx := context.Background() log := log.NewLogger("debug", "") LINUX := "linux" AMD := "amd" ARM := "arm64" boltDriver, err := boltdb.GetBoltDriver(boltdb.DBParameters{RootDir: t.TempDir()}) if err != nil { panic(err) } metaDB, err := boltdb.New(boltDriver, log) if err != nil { panic(err) } image1 := CreateImageWith().RandomLayers(5, 2).ImageConfig(ispec.Image{ Created: DateRef(2008, 1, 1, 12, 0, 0, 0, time.UTC), Platform: ispec.Platform{ Architecture: AMD, OS: LINUX, }, }).Build() digest1 := image1.Digest() image2 := CreateImageWith().RandomLayers(5, 2).ImageConfig(ispec.Image{ Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC), Platform: ispec.Platform{ Architecture: AMD, OS: LINUX, }, }).Build() digest2 := image2.Digest() image3 := CreateImageWith().RandomLayers(5, 2).ImageConfig(ispec.Image{ Created: DateRef(2010, 1, 1, 12, 0, 0, 0, time.UTC), Platform: ispec.Platform{ Architecture: ARM, OS: LINUX, }, }).Build() digest3 := image3.Digest() ctx, err = ociutils.InitializeTestMetaDB(ctx, metaDB, ociutils.Repo{ Name: "repo1", Images: []ociutils.RepoImage{ {Image: image1, Reference: "1.0.0"}, {Image: image2, Reference: "1.0.1"}, {Image: image3, Reference: "1.1.0"}, {Image: image3, Reference: "latest"}, }, }, ociutils.Repo{ Name: "repo2", Images: []ociutils.RepoImage{ {Image: image1, Reference: "2.0.0"}, {Image: image2, Reference: "2.0.1"}, {Image: image3, Reference: "2.1.0"}, {Image: image3, Reference: "latest"}, }, }, ociutils.Repo{ Name: "repo3", Images: []ociutils.RepoImage{ {Image: image2, Reference: "3.0.1"}, {Image: image3, Reference: "3.1.0"}, {Image: image3, Reference: "latest"}, }, }, ) if err != nil { panic(err) } getCveResults := func(digestStr string) map[string]cvemodel.CVE { if digestStr == digest1.String() { return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", Severity: "HIGH", Title: "Title CVE1", Description: "Description CVE1", }, "CVE2": { ID: "CVE2", Severity: "MEDIUM", Title: "Title CVE2", Description: "Description CVE2", }, "CVE3": { ID: "CVE3", Severity: "LOW", Title: "Title CVE3", Description: "Description CVE3", }, "CVE34": { ID: "CVE34", Severity: "LOW", Title: "Title for CVE34", Description: "Description CVE34", }, } } if digestStr == digest2.String() { return map[string]cvemodel.CVE{ "CVE2": { ID: "CVE2", Severity: "MEDIUM", Title: "Title CVE2", Description: "Description CVE2", }, "CVE3": { ID: "CVE3", Severity: "LOW", Title: "Title CVE3", Description: "Description CVE3", }, } } if digestStr == digest3.String() { return map[string]cvemodel.CVE{ "CVE3": { ID: "CVE3", Severity: "LOW", Title: "Title CVE3", Description: "Description CVE3", }, } } // By default the image has no vulnerabilities return map[string]cvemodel.CVE{} } // MetaDB loaded with initial data, now mock the scanner // Setup test CVE data in mock scanner scanner := mocks.CveScannerMock{ ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { repo, ref, _, _ := common.GetRepoReference(image) if common.IsDigest(ref) { return getCveResults(ref), nil } repoMeta, _ := metaDB.GetRepoMeta(ctx, repo) if _, ok := repoMeta.Tags[ref]; !ok { panic("unexpected tag '" + ref + "', test might be wrong") } return getCveResults(repoMeta.Tags[ref].Digest), nil }, GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE { return getCveResults(digestStr) }, IsResultCachedFn: func(digestStr string) bool { return true }, } cveInfo := &cveinfo.BaseCveInfo{ Log: log, Scanner: scanner, MetaDB: metaDB, } Convey("Get CVE list for image ", t, func() { Convey("Unpaginated request to get all CVEs in an image", func() { pageInput := &gql_generated.PageInput{ SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) dig := godigest.FromString("dig") repoWithDigestRef := fmt.Sprintf("repo@%s", dig) _, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, "", "", "", log) So(err, ShouldBeNil) cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves := []string{"CVE1", "CVE2", "CVE3", "CVE34"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } // test searching CVE by id in results cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "CVE3", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves = []string{"CVE3", "CVE34"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } // test searching CVE by id in results - no matches cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "CVE100", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") So(len(cveResult.CVEList), ShouldEqual, 0) // test searching CVE by id in results - partial name cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "VE3", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves = []string{"CVE3", "CVE34"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } // test searching CVE by title in results cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "Title CVE", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves = []string{"CVE1", "CVE2", "CVE3"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.1", cveInfo, pageInput, "", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.1") expectedCves = []string{"CVE2", "CVE3"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } cveResult, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, "", "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.1.0") expectedCves = []string{"CVE3"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } }) Convey("Unpaginated request to get all CVEs in an image excluding some", func() { pageInput := &gql_generated.PageInput{ SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "Title CVE", "Title CVE2", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves := []string{"CVE1", "CVE3"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "Description", "Description CVE2", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves = []string{"CVE1", "CVE3", "CVE34"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } }) Convey("Unpaginated request to get all CVEs in an image filtered by severity", func() { pageInput := &gql_generated.PageInput{ SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", "", "HIGH", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves := []string{"CVE1"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "Description", "Description CVE2", "LOW", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") expectedCves = []string{"CVE3", "CVE34"} So(len(cveResult.CVEList), ShouldEqual, len(expectedCves)) for _, cve := range cveResult.CVEList { So(expectedCves, ShouldContain, *cve.ID) } }) Convey("paginated fail", func() { pageInput := &gql_generated.PageInput{ Limit: ref(-1), } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, "", "", "", log) So(err, ShouldNotBeNil) }) }) Convey("Get a list of images affected by a particular CVE ", t, func() { Convey("Unpaginated request", func() { responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) images, err := getImageListForCVE(responseContext, "CVE1", cveInfo, nil, nil, metaDB, log) So(err, ShouldBeNil) expectedImages := []string{ "repo1:1.0.0", "repo2:2.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } images, err = getImageListForCVE(responseContext, "CVE2", cveInfo, nil, nil, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo1:1.0.1", "repo2:2.0.0", "repo2:2.0.1", "repo3:3.0.1", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, nil, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo1:1.0.1", "repo1:1.1.0", "repo1:latest", "repo2:2.0.0", "repo2:2.0.1", "repo2:2.1.0", "repo2:latest", "repo3:3.0.1", "repo3:3.1.0", "repo3:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } }) Convey("paginated fail", func() { pageInput := &gql_generated.PageInput{ Limit: ref(-1), } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err = getImageListForCVE(responseContext, "repo1:1.1.0", cveInfo, &gql_generated.Filter{}, pageInput, mocks.MetaDBMock{}, log) So(err, ShouldNotBeNil) }) Convey("context done", func() { pageInput := getGQLPageInput(1, 0) ctx, cancel := context.WithCancel(ctx) cancel() responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) canceledScanner := scanner canceledScanner.ScanImageFn = func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { return nil, ctx.Err() } cveInfo.Scanner = canceledScanner defer func() { cveInfo.Scanner = scanner }() _, err = getImageListForCVE(responseContext, "repo1:1.1.0", cveInfo, &gql_generated.Filter{}, pageInput, metaDB, log) So(err, ShouldEqual, ctx.Err()) }) Convey("Paginated requests", func() { responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover, ) pageInput := getGQLPageInput(1, 0) images, err := getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages := []string{ "repo1:1.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(1, 1) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo2:2.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(1, 2) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) pageInput = getGQLPageInput(1, 5) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) pageInput = getGQLPageInput(2, 0) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo2:2.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo2:2.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 1) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo2:2.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 2) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) pageInput = getGQLPageInput(5, 5) images, err = getImageListForCVE(responseContext, "CVE1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE2", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo1:1.0.1", "repo2:2.0.0", "repo2:2.0.1", "repo3:3.0.1", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 3) images, err = getImageListForCVE(responseContext, "CVE2", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo2:2.0.1", "repo3:3.0.1", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo1:1.0.1", "repo1:1.1.0", "repo1:latest", "repo2:2.0.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 5) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo2:2.0.1", "repo2:2.1.0", "repo2:latest", "repo3:3.0.1", "repo3:3.1.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 10) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo3:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } amdFilter := &gql_generated.Filter{Arch: []*string{&AMD}} pageInput = getGQLPageInput(5, 0) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, amdFilter, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.0", "repo1:1.0.1", "repo2:2.0.0", "repo2:2.0.1", "repo3:3.0.1", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(2, 2) images, err = getImageListForCVE(responseContext, "CVE3", cveInfo, amdFilter, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo2:2.0.0", "repo2:2.0.1", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } }) }) Convey("Get a list of images where a particular CVE is fixed", t, func() { Convey("Unpaginated request", func() { responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) images, err := getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, nil, metaDB, log) So(err, ShouldBeNil) expectedImages := []string{ "repo1:1.0.1", "repo1:1.1.0", "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } images, err = getImageListWithCVEFixed(responseContext, "CVE2", "repo1", cveInfo, nil, nil, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.1.0", "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } images, err = getImageListWithCVEFixed(responseContext, "CVE3", "repo1", cveInfo, nil, nil, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) }) Convey("paginated fail", func() { pageInput := &gql_generated.PageInput{ Limit: ref(-1), } responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err = getImageListWithCVEFixed(responseContext, "cve", "repo1:1.1.0", cveInfo, &gql_generated.Filter{}, pageInput, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.1.0": { Digest: godigest.FromString("str").String(), MediaType: ispec.MediaTypeImageManifest, }, }, }, nil }, }, log) So(err, ShouldNotBeNil) }) Convey("context done", func() { ctx, cancel := context.WithCancel(ctx) cancel() responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, nil, metaDB, log) So(err, ShouldNotBeNil) }) Convey("Paginated requests", func() { responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover, ) pageInput := getGQLPageInput(1, 0) images, err := getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages := []string{ "repo1:1.0.1", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(1, 1) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.1.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(1, 2) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(1, 3) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) pageInput = getGQLPageInput(1, 10) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) pageInput = getGQLPageInput(2, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.1", "repo1:1.1.0", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(2, 1) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.1.0", "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(2, 2) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.0.1", "repo1:1.1.0", "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE2", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{ "repo1:1.1.0", "repo1:latest", } So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(5, 2) images, err = getImageListWithCVEFixed(responseContext, "CVE2", "repo1", cveInfo, nil, pageInput, metaDB, log) So(err, ShouldBeNil) So(len(images.Results), ShouldEqual, 0) amdFilter := &gql_generated.Filter{Arch: []*string{&AMD}} armFilter := &gql_generated.Filter{Arch: []*string{&ARM}} pageInput = getGQLPageInput(3, 0) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, amdFilter, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{"repo1:1.0.1"} So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, armFilter, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{"repo1:1.1.0", "repo1:latest"} So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } pageInput = getGQLPageInput(1, 1) images, err = getImageListWithCVEFixed(responseContext, "CVE1", "repo1", cveInfo, armFilter, pageInput, metaDB, log) So(err, ShouldBeNil) expectedImages = []string{"repo1:latest"} So(len(images.Results), ShouldEqual, len(expectedImages)) for _, image := range images.Results { So(fmt.Sprintf("%s:%s", *image.RepoName, *image.Tag), ShouldBeIn, expectedImages) } }) }) Convey("Errors for cve resolvers", t, func() { _, err := getImageListForCVE( ctx, "id", mocks.CveInfoMock{ GetImageListForCVEFn: func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) { return []cvemodel.TagInfo{}, ErrTestError }, }, nil, nil, mocks.MetaDBMock{ GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool, ) ([]mTypes.RepoMeta, error) { return []mTypes.RepoMeta{{}}, nil }, }, log, ) So(err, ShouldNotBeNil) }) Convey("CVE Diff between images", t, func() { // image := "image:tag" // baseImage := "base:basetag" ctx := context.Background() pageInput := &gql_generated.PageInput{ SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } boltDriver, err := boltdb.GetBoltDriver(boltdb.DBParameters{RootDir: t.TempDir()}) if err != nil { panic(err) } metaDB, err := boltdb.New(boltDriver, log) if err != nil { panic(err) } layer1 := []byte{10, 20, 30} layer2 := []byte{11, 21, 31} layer3 := []byte{12, 22, 23} otherImage := CreateImageWith().LayerBlobs([][]byte{ layer1, }).DefaultConfig().Build() baseImage := CreateImageWith().LayerBlobs([][]byte{ layer1, layer2, }).PlatformConfig("testArch", "testOs").Build() image := CreateImageWith().LayerBlobs([][]byte{ layer1, layer2, layer3, }).PlatformConfig("testArch", "testOs").Build() multiArchBase := CreateMultiarchWith().Images([]Image{baseImage, CreateRandomImage(), CreateRandomImage()}). Build() multiArchImage := CreateMultiarchWith().Images([]Image{image, CreateRandomImage(), CreateRandomImage()}). Build() getCveResults := func(digestStr string) map[string]cvemodel.CVE { switch digestStr { case image.DigestStr(): return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", Severity: "HIGH", Title: "Title CVE1", Description: "Description CVE1", PackageList: []cvemodel.Package{{}}, }, "CVE2": { ID: "CVE2", Severity: "MEDIUM", Title: "Title CVE2", Description: "Description CVE2", PackageList: []cvemodel.Package{{}}, }, "CVE3": { ID: "CVE3", Severity: "LOW", Title: "Title CVE3", Description: "Description CVE3", PackageList: []cvemodel.Package{{}}, }, } case baseImage.DigestStr(): return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", Severity: "HIGH", Title: "Title CVE1", Description: "Description CVE1", PackageList: []cvemodel.Package{{}}, }, "CVE2": { ID: "CVE2", Severity: "MEDIUM", Title: "Title CVE2", Description: "Description CVE2", PackageList: []cvemodel.Package{{}}, }, } case otherImage.DigestStr(): return map[string]cvemodel.CVE{ "CVE1": { ID: "CVE1", Severity: "HIGH", Title: "Title CVE1", Description: "Description CVE1", PackageList: []cvemodel.Package{{}}, }, } } // By default the image has no vulnerabilities return map[string]cvemodel.CVE{} } // MetaDB loaded with initial data, now mock the scanner // Setup test CVE data in mock scanner scanner := mocks.CveScannerMock{ ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { repo, ref, _, _ := common.GetRepoReference(image) if common.IsDigest(ref) { return getCveResults(ref), nil } repoMeta, _ := metaDB.GetRepoMeta(ctx, repo) if _, ok := repoMeta.Tags[ref]; !ok { panic("unexpected tag '" + ref + "', test might be wrong") } return getCveResults(repoMeta.Tags[ref].Digest), nil }, GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE { return getCveResults(digestStr) }, IsResultCachedFn: func(digestStr string) bool { return true }, } cveInfo := &cveinfo.BaseCveInfo{ Log: log, Scanner: scanner, MetaDB: metaDB, } ctx, err = ociutils.InitializeTestMetaDB(ctx, metaDB, ociutils.Repo{ Name: "repo", Images: []ociutils.RepoImage{ {Image: otherImage, Reference: "other-image"}, {Image: baseImage, Reference: "base-image"}, {Image: image, Reference: "image"}, }, }, ociutils.Repo{ Name: "repo-multi", MultiArchImages: []ociutils.RepoMultiArchImage{ {MultiarchImage: CreateRandomMultiarch(), Reference: "multi-rand"}, {MultiarchImage: multiArchBase, Reference: "multi-base"}, {MultiarchImage: multiArchImage, Reference: "multi-img"}, }, }, ) So(err, ShouldBeNil) minuend := gql_generated.ImageInput{Repo: "repo", Tag: "image"} subtrahend := gql_generated.ImageInput{Repo: "repo", Tag: "image"} diffResult, err := getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(len(diffResult.CVEList), ShouldEqual, 0) minuend = gql_generated.ImageInput{Repo: "repo", Tag: "image"} subtrahend = gql_generated.ImageInput{} diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(len(diffResult.CVEList), ShouldEqual, 1) minuend = gql_generated.ImageInput{Repo: "repo", Tag: "base-image"} subtrahend = gql_generated.ImageInput{Repo: "repo", Tag: "image"} diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(len(diffResult.CVEList), ShouldEqual, 0) minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{}} subtrahend = gql_generated.ImageInput{} _, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldNotBeNil) minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{ Os: ref("testOs"), Arch: ref("testArch"), }} subtrahend = gql_generated.ImageInput{} diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(len(diffResult.CVEList), ShouldEqual, 1) So(diffResult.Subtrahend.Repo, ShouldEqual, "repo-multi") So(diffResult.Subtrahend.Tag, ShouldEqual, "multi-base") So(dderef(diffResult.Subtrahend.Platform.Os), ShouldResemble, "testOs") So(dderef(diffResult.Subtrahend.Platform.Arch), ShouldResemble, "testArch") minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{ Os: ref("testOs"), Arch: ref("testArch"), }} subtrahend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-base", Platform: &gql_generated.PlatformInput{ Os: ref("testOs"), Arch: ref("testArch"), }} diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(len(diffResult.CVEList), ShouldEqual, 1) minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{ Os: ref("testOs"), Arch: ref("testArch"), }} subtrahend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-base", Platform: &gql_generated.PlatformInput{}} _, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log) So(err, ShouldNotBeNil) }) Convey("CVE Diff Errors", t, func() { ctx := context.Background() metaDB := mocks.MetaDBMock{} cveInfo := mocks.CveInfoMock{} emptyImage := gql_generated.ImageInput{} Convey("minuend is empty", func() { _, err := getCVEDiffListForImages(ctx, emptyImage, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log) So(err, ShouldNotBeNil) }) Convey("no ", func() { minuend := gql_generated.ImageInput{Repo: "repo", Tag: "bad-tag"} _, err := getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log) So(err, ShouldNotBeNil) }) Convey("getImageSummary for subtrahend errors", func() { metaDB.GetRepoMetaFn = func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{}, ErrTestError } minuend := gql_generated.ImageInput{Repo: "test", Tag: "img"} _, err := getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log) So(err, ShouldNotBeNil) metaDB.GetRepoMetaFn = func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{}, zerr.ErrRepoMetaNotFound } minuend = gql_generated.ImageInput{Repo: "test", Tag: "img"} _, err = getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log) So(err, ShouldNotBeNil) }) Convey("FilterTags for subtrahend errors", func() { metaDB.FilterTagsFn = func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return nil, ErrTestError } minuend := gql_generated.ImageInput{Repo: "test", Tag: "img"} _, err = getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log) So(err, ShouldNotBeNil) }) Convey("GetCVEDiffListForImages errors", func() { cveInfo.GetCVEDiffListForImagesFn = func(ctx context.Context, minuend, subtrahend, searchedCVE, excluded string, pageInput cvemodel.PageInput, ) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) { return nil, cvemodel.ImageCVESummary{}, common.PageInfo{}, ErrTestError } minuend := gql_generated.ImageInput{Repo: "test", Tag: "img"} subtrahend := gql_generated.ImageInput{Repo: "sub", Tag: "img"} _, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log) So(err, ShouldNotBeNil) }) }) } func TestMockedDerivedImageList(t *testing.T) { Convey("MetaDB FilterTags error", t, func() { log := log.NewLogger("debug", "/dev/null") image := CreateRandomImage() mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{}, ErrTestError }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{image.DigestStr(): image.AsImageMeta()}, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log) So(err, ShouldNotBeNil) So(images.Results, ShouldBeEmpty) }) Convey("paginated fail", t, func() { log := log.NewLogger("debug", "/dev/null") image := CreateRandomImage() pageInput := &gql_generated.PageInput{ Limit: ref(-1), } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": { Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }, }, }, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{image.DigestStr(): image.AsImageMeta()}, nil }, }, pageInput, mocks.CveInfoMock{}, log) So(err, ShouldNotBeNil) }) //nolint: dupl Convey("MetaDB FilterTags no repo available", t, func() { log := log.NewLogger("debug", "/dev/null") image := CreateDefaultImage() mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, nil }, GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{ digests[0]: image.AsImageMeta(), }, nil }, GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) { return image.AsImageMeta(), nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log) So(err, ShouldBeNil) So(images.Results, ShouldBeEmpty) }) //nolint: dupl Convey("derived image list working", t, func() { log := log.NewLogger("debug", "/dev/null") layer1 := []byte{10, 11, 10, 11} layer2 := []byte{11, 11, 11, 11} layer3 := []byte{10, 10, 10, 11} layer4 := []byte{13, 14, 15, 11} image := CreateImageWith(). LayerBlobs([][]byte{ layer1, layer2, layer3, }).DefaultConfig().Build() derivedImage := CreateImageWith(). LayerBlobs([][]byte{ layer1, layer2, layer3, layer4, }).DefaultConfig().Build() imageMetaMap := map[string]mTypes.ImageMeta{ image.DigestStr(): image.AsImageMeta(), derivedImage.DigestStr(): derivedImage.AsImageMeta(), } mockMetaDB := mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }, nil }, GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) { return imageMetaMap[digest.String()], nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { result := map[string]mTypes.ImageMeta{} for _, digest := range digests { result[digest] = imageMetaMap[digest] } return result, nil }, FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { fullImageMetaList := []mTypes.FullImageMeta{} repos := []mTypes.RepoMeta{{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, "1.0.2": {Digest: derivedImage.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, "1.0.3": {Digest: derivedImage.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }} for _, repo := range repos { for tag, descriptor := range repo.Tags { if filterFunc(repo, imageMetaMap[descriptor.Digest]) { fullImageMetaList = append(fullImageMetaList, convert.GetFullImageMeta(tag, repo, imageMetaMap[descriptor.Digest])) } } } return fullImageMetaList, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} Convey("valid derivedImageList, results not affected by pageInput", func() { images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) So(len(images.Results), ShouldEqual, 2) }) Convey("valid derivedImageList, results affected by pageInput", func() { pageInput := gql_generated.PageInput{ Limit: ref(1), Offset: ref(0), SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc), } images, err := derivedImageList(responseContext, "repo1:1.0.1", nil, mockMetaDB, &pageInput, mockCve, log) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) So(len(images.Results), ShouldEqual, 1) }) }) } func TestMockedBaseImageList(t *testing.T) { Convey("MetaDB FilterTags error", t, func() { mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, ErrTestError }, GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{}, ErrTestError }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{}, ErrTestError }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) So(images.Results, ShouldBeEmpty) }) Convey("paginated fail", t, func() { image := CreateDefaultImage() pageInput := &gql_generated.PageInput{Limit: ref(-1)} responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := baseImageList(responseContext, "repo1:1.0.2", nil, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.2": { Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest, }, }, }, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{image.DigestStr(): image.AsImageMeta()}, nil }, }, pageInput, mocks.CveInfoMock{}, log.NewLogger("debug", "")) So(err, ShouldNotBeNil) }) //nolint: dupl Convey("MetaDB FilterTags no repo available", t, func() { image := CreateDefaultImage() mockMetaDB := mocks.MetaDBMock{ FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { return []mTypes.FullImageMeta{}, nil }, GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.2": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return map[string]mTypes.ImageMeta{image.DigestStr(): image.AsImageMeta()}, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldBeEmpty) }) //nolint: dupl Convey("base image list working", t, func() { layer1 := []byte{10, 11, 10, 11} layer2 := []byte{11, 11, 11, 11} layer3 := []byte{10, 10, 10, 11} layer4 := []byte{13, 14, 15, 11} image := CreateImageWith(). LayerBlobs([][]byte{ layer1, layer2, layer3, }).DefaultConfig().Build() derivedImage := CreateImageWith(). LayerBlobs([][]byte{ layer1, layer2, layer3, layer4, }).DefaultConfig().Build() imageMetaMap := map[string]mTypes.ImageMeta{ image.DigestStr(): image.AsImageMeta(), derivedImage.DigestStr(): derivedImage.AsImageMeta(), } mockMetaDB := mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.2": {Digest: derivedImage.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return imageMetaMap, nil }, FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { fullImageMetaList := []mTypes.FullImageMeta{} repos := []mTypes.RepoMeta{{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, "1.0.3": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, "1.0.2": {Digest: derivedImage.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }} for _, repo := range repos { for tag, descriptor := range repo.Tags { if filterFunc(repo, imageMetaMap[descriptor.Digest]) { fullImageMetaList = append(fullImageMetaList, convert.GetFullImageMeta(tag, repo, imageMetaMap[descriptor.Digest])) } } } return fullImageMetaList, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} Convey("valid baseImageList, results not affected by pageInput", func() { images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) So(len(images.Results), ShouldEqual, 2) expectedTags := []string{"1.0.1", "1.0.3"} So(expectedTags, ShouldContain, *images.Results[0].Tag) So(expectedTags, ShouldContain, *images.Results[1].Tag) }) Convey("valid baseImageList, results affected by pageInput", func() { limit := 1 offset := 0 sortCriteria := gql_generated.SortCriteriaAlphabeticAsc pageInput := gql_generated.PageInput{ Limit: &limit, Offset: &offset, SortBy: &sortCriteria, } images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockMetaDB, &pageInput, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldNotBeEmpty) So(len(images.Results), ShouldEqual, limit) So(*images.Results[0].Tag, ShouldEqual, "1.0.1") }) }) //nolint: dupl Convey("filterTags working, no base image list found", t, func() { layer1 := []byte{10, 11, 10, 11} layer2 := []byte{11, 11, 11, 11} layer3 := []byte{10, 10, 10, 11} layer4 := []byte{13, 14, 15, 11} image := CreateImageWith(). LayerBlobs([][]byte{ layer1, layer2, layer3, }).DefaultConfig().Build() derivedImage := CreateImageWith(). LayerBlobs([][]byte{ layer4, }).DefaultConfig().Build() imageMetaMap := map[string]mTypes.ImageMeta{ image.DigestStr(): image.AsImageMeta(), derivedImage.DigestStr(): derivedImage.AsImageMeta(), } mockMetaDB := mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.2": {Digest: derivedImage.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }, nil }, FilterImageMetaFn: func(ctx context.Context, digests []string) (map[string]mTypes.ImageMeta, error) { return imageMetaMap, nil }, FilterTagsFn: func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc, ) ([]mTypes.FullImageMeta, error) { fullImageMetaList := []mTypes.FullImageMeta{} repos := []mTypes.RepoMeta{{ Name: "repo1", Tags: map[mTypes.Tag]mTypes.Descriptor{ "1.0.1": {Digest: image.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, "1.0.2": {Digest: derivedImage.DigestStr(), MediaType: ispec.MediaTypeImageManifest}, }, }} for _, repo := range repos { for tag, descriptor := range repo.Tags { if filterFunc(repo, imageMetaMap[descriptor.Digest]) { fullImageMetaList = append(fullImageMetaList, convert.GetFullImageMeta(tag, repo, imageMetaMap[descriptor.Digest])) } } } return fullImageMetaList, nil }, } responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.DefaultRecover) mockCve := mocks.CveInfoMock{} images, err := baseImageList(responseContext, "repo1:1.0.2", nil, mockMetaDB, &gql_generated.PageInput{}, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) So(images.Results, ShouldBeEmpty) }) } func TestExpandedRepoInfoErrors(t *testing.T) { log := log.NewLogger("debug", "") Convey("Access error", t, func() { userAc := reqCtx.NewUserAccessControl() userAc.SetUsername("user") userAc.SetGlobPatterns("read", map[string]bool{ "repo": false, }) ctx := userAc.DeriveContext(context.Background()) responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) _, err := expandedRepoInfo(responseContext, "repo", mocks.MetaDBMock{}, mocks.CveInfoMock{}, log) So(err, ShouldBeNil) }) } func TestUtils(t *testing.T) { Convey("utils", t, func() { Convey("", func() { So(isMatchingPlatform(ispec.Platform{OS: "test"}, gql_generated.PlatformInput{Os: ref("t")}), ShouldBeFalse) So(getArch(ispec.Platform{OS: "t", Architecture: "e", Variant: "st"}), ShouldResemble, "e/st") }) Convey("checkImageInput", func() { _, err := resolveImageData(context.Background(), gql_generated.ImageInput{Repo: "test"}, mocks.MetaDBMock{}) So(err, ShouldNotBeNil) }) Convey("checkImageInput can't find index data", func() { _, err := resolveImageData(context.Background(), gql_generated.ImageInput{ Repo: "test", Tag: "test", Digest: ref("dig"), }, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{Tags: map[string]mTypes.Descriptor{ "test": {MediaType: ispec.MediaTypeImageIndex}, }}, nil }, GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) { return mTypes.ImageMeta{}, ErrTestError }, }) So(err, ShouldNotBeNil) }) Convey("checkImageInput image meta not found", func() { _, err := resolveImageData(context.Background(), gql_generated.ImageInput{ Repo: "test", Tag: "test", Digest: ref("dig"), }, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{Tags: map[string]mTypes.Descriptor{ "test": {MediaType: ispec.MediaTypeImageIndex}, }}, nil }, GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) { return mTypes.ImageMeta{}, nil }, }) So(err, ShouldNotBeNil) }) Convey("checkImageInput image meta bad media type", func() { _, err := resolveImageData(context.Background(), gql_generated.ImageInput{ Repo: "test", Tag: "test", Digest: ref("dig"), }, mocks.MetaDBMock{ GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) { return mTypes.RepoMeta{Tags: map[string]mTypes.Descriptor{ "test": {MediaType: "bad-type"}, }}, nil }, GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) { return mTypes.ImageMeta{}, nil }, }) So(err, ShouldBeNil) }) }) } func getGQLPageInput(limit int, offset int) *gql_generated.PageInput { sortCriteria := gql_generated.SortCriteriaAlphabeticAsc return &gql_generated.PageInput{ Limit: &limit, Offset: &offset, SortBy: &sortCriteria, } }