From 3f97f878fdad586216bca028827485c3289f471a Mon Sep 17 00:00:00 2001 From: LaurentiuNiculae Date: Fri, 19 Jan 2024 22:59:42 +0200 Subject: [PATCH] feat(cve): add option to exclude string from cve search (#2163) Signed-off-by: Laurentiu Niculae --- pkg/cli/client/gql_queries.go | 2 +- pkg/extensions/search/cve/cve.go | 26 ++++------ .../search/cve/cve_internal_test.go | 15 ++++++ pkg/extensions/search/cve/cve_test.go | 26 +++++----- pkg/extensions/search/cve/model/models.go | 17 +++++++ pkg/extensions/search/cve/pagination_test.go | 26 +++++----- .../search/gql_generated/generated.go | 19 +++++-- pkg/extensions/search/resolver.go | 3 +- pkg/extensions/search/resolver_test.go | 51 +++++++++++++++---- pkg/extensions/search/schema.graphql | 2 + pkg/extensions/search/schema.resolvers.go | 4 +- pkg/test/mocks/cve_mock.go | 6 +-- 12 files changed, 135 insertions(+), 62 deletions(-) diff --git a/pkg/cli/client/gql_queries.go b/pkg/cli/client/gql_queries.go index bfa84e1e..1d38349b 100644 --- a/pkg/cli/client/gql_queries.go +++ b/pkg/cli/client/gql_queries.go @@ -78,7 +78,7 @@ func DerivedImageListQuery() GQLQuery { func CVEListForImageQuery() GQLQuery { return GQLQuery{ Name: "CVEListForImage", - Args: []string{"image", "requestedPage", "searchedCVE"}, + Args: []string{"image", "requestedPage", "searchedCVE", "excludedCVE"}, ReturnType: CVEResultForImage(), } } diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index c2899587..e91a16fe 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -8,7 +8,6 @@ import ( godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - "golang.org/x/exp/slices" zerr "zotregistry.io/zot/errors" zcommon "zotregistry.io/zot/pkg/common" @@ -22,8 +21,8 @@ import ( type CveInfo interface { GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixed(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) - GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE string, pageinput cvemodel.PageInput, - ) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) + GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE string, excludedCVE string, + pageinput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string) (cvemodel.ImageCVESummary, error) } @@ -330,27 +329,22 @@ func getConfigAndDigest(metaDB mTypes.MetaDB, manifestDigestStr string) (ispec.I return manifestData.Manifests[0].Config, manifestDigest, err } -func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) { +func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE string, pageFinder *CvePageFinder) { searchedCVE = strings.ToUpper(searchedCVE) for _, cve := range cveMap { - if strings.Contains(strings.ToUpper(cve.Title), searchedCVE) || - strings.Contains(strings.ToUpper(cve.ID), searchedCVE) || - strings.Contains(strings.ToUpper(cve.Description), searchedCVE) || - strings.Contains(strings.ToUpper(cve.Reference), searchedCVE) || - strings.Contains(strings.ToUpper(cve.Severity), searchedCVE) || - slices.ContainsFunc(cve.PackageList, func(pack cvemodel.Package) bool { - return strings.Contains(strings.ToUpper(pack.Name), searchedCVE) || - strings.Contains(strings.ToUpper(pack.FixedVersion), searchedCVE) || - strings.Contains(strings.ToUpper(pack.InstalledVersion), searchedCVE) - }) { + if excludedCVE != "" && cve.ContainsStr(excludedCVE) { + continue + } + + if cve.ContainsStr(searchedCVE) { pageFinder.Add(cve) } } } func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref string, searchedCVE string, - pageInput cvemodel.PageInput, + excludedCVE string, pageInput cvemodel.PageInput, ) ( []cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error, ) { @@ -379,7 +373,7 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref str return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err } - filterCVEList(cveMap, searchedCVE, pageFinder) + filterCVEList(cveMap, searchedCVE, excludedCVE, pageFinder) cveList, pageInfo := pageFinder.Page() diff --git a/pkg/extensions/search/cve/cve_internal_test.go b/pkg/extensions/search/cve/cve_internal_test.go index d242efc3..16519222 100644 --- a/pkg/extensions/search/cve/cve_internal_test.go +++ b/pkg/extensions/search/cve/cve_internal_test.go @@ -20,6 +20,21 @@ var ErrTestError = errors.New("test error") func TestUtils(t *testing.T) { Convey("Utils", t, func() { + Convey("cve.ContainsStr for package list", func() { + cve := cvemodel.CVE{ + PackageList: []cvemodel.Package{ + { + Name: "NameTest", + FixedVersion: "FixedVersionTest", + InstalledVersion: "InstalledVersionTest", + }, + }, + } + + So(cve.ContainsStr("NameTest"), ShouldBeTrue) + So(cve.ContainsStr("FixedVersionTest"), ShouldBeTrue) + So(cve.ContainsStr("InstalledVersionTest"), ShouldBeTrue) + }) Convey("getConfigAndDigest", func() { _, _, err := getConfigAndDigest(mocks.MetaDBMock{}, "bad-digest") So(err, ShouldNotBeNil) diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index 0347af72..e7721cb7 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -1192,7 +1192,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo ctx := context.Background() // Image is found - cveList, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", pageInput) + cveList, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE1") @@ -1206,7 +1206,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.0", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 3) So(cveList[0].ID, ShouldEqual, "CVE2") @@ -1222,7 +1222,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "HIGH") - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.1", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.1", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 2) So(cveList[0].ID, ShouldEqual, "CVE1") @@ -1237,7 +1237,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.1.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.1.0", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE3") @@ -1251,7 +1251,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "LOW") - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo6, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo6, "1.0.0", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1264,7 +1264,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "NONE") - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo8, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo8, "1.0.0", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 7) So(pageInfo.ItemCount, ShouldEqual, 7) @@ -1278,7 +1278,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") // Image is multiarch - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE1") @@ -1293,7 +1293,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") // Image is not scannable - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo2, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo2, "1.0.0", "", "", pageInput) So(err, ShouldEqual, zerr.ErrScanNotSupported) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1307,7 +1307,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.MaxSeverity, ShouldEqual, "") // Tag is not found - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo3, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo3, "1.0.0", "", "", pageInput) So(err, ShouldEqual, zerr.ErrTagMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1321,7 +1321,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.MaxSeverity, ShouldEqual, "") // Scan failed - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo7, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo7, "1.0.0", "", "", pageInput) So(err, ShouldEqual, ErrFailedScan) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1335,7 +1335,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.MaxSeverity, ShouldEqual, "") // Tag is not found - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo-with-bad-tag-digest", "tag", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo-with-bad-tag-digest", "tag", "", "", pageInput) So(err, ShouldEqual, zerr.ErrImageMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1349,7 +1349,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.MaxSeverity, ShouldEqual, "") // Repo is not found - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo100, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo100, "1.0.0", "", "", pageInput) So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1580,7 +1580,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") - cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", "", pageInput) So(err, ShouldNotBeNil) So(cveList, ShouldBeEmpty) So(pageInfo.ItemCount, ShouldEqual, 0) diff --git a/pkg/extensions/search/cve/model/models.go b/pkg/extensions/search/cve/model/models.go index e85de88c..7a0c8ef1 100644 --- a/pkg/extensions/search/cve/model/models.go +++ b/pkg/extensions/search/cve/model/models.go @@ -1,9 +1,11 @@ package model import ( + "strings" "time" godigest "github.com/opencontainers/go-digest" + "golang.org/x/exp/slices" ) type ImageCVESummary struct { @@ -26,6 +28,21 @@ type CVE struct { PackageList []Package `json:"PackageList"` } +func (cve *CVE) ContainsStr(str string) bool { + str = strings.ToUpper(str) + + return strings.Contains(strings.ToUpper(cve.Title), str) || + strings.Contains(strings.ToUpper(cve.ID), str) || + strings.Contains(strings.ToUpper(cve.Severity), str) || + strings.Contains(strings.ToUpper(cve.Reference), str) || + strings.Contains(strings.ToUpper(cve.Description), str) || + slices.ContainsFunc(cve.PackageList, func(pack Package) bool { + return strings.Contains(strings.ToUpper(pack.Name), str) || + strings.Contains(strings.ToUpper(pack.FixedVersion), str) || + strings.Contains(strings.ToUpper(pack.InstalledVersion), str) + }) +} + //nolint:tagliatelle // graphQL schema type Package struct { Name string `json:"Name"` diff --git a/pkg/extensions/search/cve/pagination_test.go b/pkg/extensions/search/cve/pagination_test.go index 08f3b899..137b8e0d 100644 --- a/pkg/extensions/search/cve/pagination_test.go +++ b/pkg/extensions/search/cve/pagination_test.go @@ -140,7 +140,7 @@ func TestCVEPagination(t *testing.T) { Convey("Page", func() { Convey("defaults", func() { // By default expect unlimitted results sorted by severity - cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{}) + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -158,7 +158,7 @@ func TestCVEPagination(t *testing.T) { previousSeverity = severityToInt[cve.Severity] } - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{}) + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", "", cvemodel.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -183,7 +183,7 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) @@ -201,7 +201,7 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", "", cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) @@ -219,7 +219,7 @@ func TestCVEPagination(t *testing.T) { } sort.Sort(sort.Reverse(sort.StringSlice(cveIds))) - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", "", cvemodel.PageInput{SortBy: cveinfo.AlphabeticDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) @@ -236,7 +236,7 @@ func TestCVEPagination(t *testing.T) { So(cve.ID, ShouldEqual, cveIds[i]) } - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", "", cvemodel.PageInput{SortBy: cveinfo.SeverityDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) @@ -262,7 +262,7 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.AlphabeticAsc, @@ -283,7 +283,7 @@ func TestCVEPagination(t *testing.T) { So(cveSummary.CriticalCount, ShouldEqual, 1) So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{ Limit: 2, Offset: 1, SortBy: cveinfo.AlphabeticDsc, @@ -303,7 +303,7 @@ func TestCVEPagination(t *testing.T) { So(cveSummary.CriticalCount, ShouldEqual, 1) So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.SeverityDsc, @@ -327,7 +327,7 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", "", cvemodel.PageInput{ Limit: 5, Offset: 20, SortBy: cveinfo.AlphabeticAsc, @@ -350,7 +350,7 @@ func TestCVEPagination(t *testing.T) { }) Convey("limit > len(cves)", func() { - cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticAsc, @@ -370,7 +370,7 @@ func TestCVEPagination(t *testing.T) { So(cveSummary.CriticalCount, ShouldEqual, 1) So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticDsc, @@ -390,7 +390,7 @@ func TestCVEPagination(t *testing.T) { So(cveSummary.CriticalCount, ShouldEqual, 1) So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", "", cvemodel.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.SeverityDsc, diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 9585fc77..88512069 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -170,7 +170,7 @@ type ComplexityRoot struct { Query struct { BaseImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int BookmarkedRepos func(childComplexity int, requestedPage *PageInput) int - CVEListForImage func(childComplexity int, image string, requestedPage *PageInput, searchedCve *string) int + CVEListForImage func(childComplexity int, image string, requestedPage *PageInput, searchedCve *string, excludedCve *string) int DerivedImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int ExpandedRepoInfo func(childComplexity int, repo string) int GlobalSearch func(childComplexity int, query string, filter *Filter, requestedPage *PageInput) int @@ -219,7 +219,7 @@ type ComplexityRoot struct { } type QueryResolver interface { - CVEListForImage(ctx context.Context, image string, requestedPage *PageInput, searchedCve *string) (*CVEResultForImage, error) + CVEListForImage(ctx context.Context, image string, requestedPage *PageInput, searchedCve *string, excludedCve *string) (*CVEResultForImage, error) ImageListForCve(ctx context.Context, id string, filter *Filter, requestedPage *PageInput) (*PaginatedImagesResult, error) ImageListWithCVEFixed(ctx context.Context, id string, image string, filter *Filter, requestedPage *PageInput) (*PaginatedImagesResult, error) ImageListForDigest(ctx context.Context, id string, requestedPage *PageInput) (*PaginatedImagesResult, error) @@ -827,7 +827,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Query.CVEListForImage(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput), args["searchedCVE"].(*string)), true + return e.complexity.Query.CVEListForImage(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput), args["searchedCVE"].(*string), args["excludedCVE"].(*string)), true case "Query.DerivedImageList": if e.complexity.Query.DerivedImageList == nil { @@ -1837,6 +1837,8 @@ type Query { requestedPage: PageInput "Search term for specific CVE by title/id" searchedCVE: String + "Search term that must not be present in the returned results" + excludedCVE: String ): CVEResultForImage! """ @@ -2064,6 +2066,15 @@ func (ec *executionContext) field_Query_CVEListForImage_args(ctx context.Context } } args["searchedCVE"] = arg2 + var arg3 *string + if tmp, ok := rawArgs["excludedCVE"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("excludedCVE")) + arg3, err = ec.unmarshalOString2áš–string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["excludedCVE"] = arg3 return args, nil } @@ -5925,7 +5936,7 @@ func (ec *executionContext) _Query_CVEListForImage(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().CVEListForImage(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput), fc.Args["searchedCVE"].(*string)) + return ec.resolvers.Query().CVEListForImage(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput), fc.Args["searchedCVE"].(*string), fc.Args["excludedCVE"].(*string)) }) if err != nil { ec.Error(ctx, err) diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 220663f3..0534cefd 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -196,6 +196,7 @@ func getCVEListForImage( cveInfo cveinfo.CveInfo, requestedPage *gql_generated.PageInput, searchedCVE string, + excludedCVE string, log log.Logger, //nolint:unparam // may be used by devs for debugging ) (*gql_generated.CVEResultForImage, error) { if requestedPage == nil { @@ -217,7 +218,7 @@ func getCVEListForImage( } cveList, imageCveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo, ref, - searchedCVE, pageInput) + searchedCVE, excludedCVE, pageInput) if err != nil { return &gql_generated.CVEResultForImage{}, err } diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 2412a4b5..bc9b4379 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -1133,10 +1133,10 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo dig := godigest.FromString("dig") repoWithDigestRef := fmt.Sprintf("repo@%s", dig) - _, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, "", log) + _, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) - cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", log) + cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") @@ -1148,7 +1148,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo } // test searching CVE by id in results - cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "CVE3", log) + cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "CVE3", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") @@ -1160,13 +1160,13 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo } // test searching CVE by id in results - no matches - cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "CVE100", log) + 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) + cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "VE3", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") @@ -1178,7 +1178,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo } // test searching CVE by title in results - cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "Title CVE", log) + cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "Title CVE", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.0") @@ -1189,7 +1189,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(expectedCves, ShouldContain, *cve.ID) } - cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.1", cveInfo, pageInput, "", log) + cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.1", cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.0.1") @@ -1200,7 +1200,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo So(expectedCves, ShouldContain, *cve.ID) } - cveResult, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, "", log) + cveResult, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, "", "", log) So(err, ShouldBeNil) So(*cveResult.Tag, ShouldEqual, "1.1.0") @@ -1212,6 +1212,39 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo } }) + 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("paginated fail", func() { pageInput := &gql_generated.PageInput{ Limit: ref(-1), @@ -1220,7 +1253,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo responseContext := graphql.WithResponseContext(ctx, graphql.DefaultErrorPresenter, graphql.DefaultRecover) - _, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, "", log) + _, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, "", "", log) So(err, ShouldNotBeNil) }) }) diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 9f7f19c0..45ca4291 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -635,6 +635,8 @@ type Query { requestedPage: PageInput "Search term for specific CVE by title/id" searchedCVE: String + "Search term that must not be present in the returned results" + excludedCVE: String ): CVEResultForImage! """ diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 434209a7..72b9f6c9 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -15,12 +15,12 @@ import ( ) // CVEListForImage is the resolver for the CVEListForImage field. -func (r *queryResolver) CVEListForImage(ctx context.Context, image string, requestedPage *gql_generated.PageInput, searchedCve *string) (*gql_generated.CVEResultForImage, error) { +func (r *queryResolver) CVEListForImage(ctx context.Context, image string, requestedPage *gql_generated.PageInput, searchedCve *string, excludedCve *string) (*gql_generated.CVEResultForImage, error) { if r.cveInfo == nil { return &gql_generated.CVEResultForImage{}, zerr.ErrCVESearchDisabled } - return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, deref(searchedCve, ""), r.log) + return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, deref(searchedCve, ""), deref(excludedCve, ""), r.log) } // ImageListForCve is the resolver for the ImageListForCVE field. diff --git a/pkg/test/mocks/cve_mock.go b/pkg/test/mocks/cve_mock.go index ad11abf5..40e350eb 100644 --- a/pkg/test/mocks/cve_mock.go +++ b/pkg/test/mocks/cve_mock.go @@ -10,7 +10,7 @@ import ( type CveInfoMock struct { GetImageListForCVEFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixedFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) - GetCVEListForImageFn func(ctx context.Context, repo string, reference string, searchedCVE string, + GetCVEListForImageFn func(ctx context.Context, repo, reference, searchedCVE, excludedCVE string, pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) GetCVESummaryForImageMediaFn func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) @@ -34,7 +34,7 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(ctx context.Context, repo, c } func (cveInfo CveInfoMock) GetCVEListForImage(ctx context.Context, repo string, reference string, - searchedCVE string, pageInput cvemodel.PageInput, + searchedCVE string, excludedCVE string, pageInput cvemodel.PageInput, ) ( []cvemodel.CVE, cvemodel.ImageCVESummary, @@ -42,7 +42,7 @@ func (cveInfo CveInfoMock) GetCVEListForImage(ctx context.Context, repo string, error, ) { if cveInfo.GetCVEListForImageFn != nil { - return cveInfo.GetCVEListForImageFn(ctx, repo, reference, searchedCVE, pageInput) + return cveInfo.GetCVEListForImageFn(ctx, repo, reference, searchedCVE, excludedCVE, pageInput) } return []cvemodel.CVE{}, cvemodel.ImageCVESummary{}, common.PageInfo{}, nil