From eea6f3f85a0b530260e4b69fcecdbfbb04fc050b Mon Sep 17 00:00:00 2001 From: Lisca Ana-Roberta <55219463+aokirisaki@users.noreply.github.com> Date: Thu, 16 Mar 2023 21:13:07 +0200 Subject: [PATCH] fix(cve): Search by CVE title/id (full or partial) when listing an image's CVEs (#1264) Signed-off-by: Ana-Roberta Lisca --- pkg/cli/cve_cmd.go | 1 + pkg/cli/cve_cmd_test.go | 55 ++++++++++++++++++ pkg/cli/image_cmd_test.go | 4 +- pkg/cli/searcher.go | 12 ++-- pkg/cli/service.go | 16 ++--- pkg/extensions/search/cve/cve.go | 20 +++++-- pkg/extensions/search/cve/cve_test.go | 20 +++---- pkg/extensions/search/cve/pagination_test.go | 50 +++++++++------- .../search/gql_generated/generated.go | 19 ++++-- pkg/extensions/search/resolver.go | 3 +- pkg/extensions/search/resolver_test.go | 58 +++++++++++++++++-- pkg/extensions/search/schema.graphql | 2 + pkg/extensions/search/schema.resolvers.go | 8 ++- pkg/test/mocks/cve_mock.go | 8 ++- 14 files changed, 212 insertions(+), 64 deletions(-) diff --git a/pkg/cli/cve_cmd.go b/pkg/cli/cve_cmd.go index c103d67d..7a2f9622 100644 --- a/pkg/cli/cve_cmd.go +++ b/pkg/cli/cve_cmd.go @@ -117,6 +117,7 @@ func NewCveCommand(searchService SearchService) *cobra.Command { func setupCveFlags(cveCmd *cobra.Command, variables cveFlagVariables) { variables.searchCveParams["imageName"] = cveCmd.Flags().StringP("image", "I", "", "List CVEs by IMAGENAME[:TAG]") variables.searchCveParams["cveID"] = cveCmd.Flags().StringP("cve-id", "i", "", "List images affected by a CVE") + variables.searchCveParams["searchedCVE"] = cveCmd.Flags().StringP("search", "s", "", "Search specific CVEs by name/id") cveCmd.Flags().StringVar(variables.servURL, "url", "", "Specify zot server URL if config-name is not mentioned") cveCmd.Flags().StringVarP(variables.user, "user", "u", "", `User Credentials of `+ diff --git a/pkg/cli/cve_cmd_test.go b/pkg/cli/cve_cmd_test.go index ef4911c7..c7bbc6f1 100644 --- a/pkg/cli/cve_cmd_test.go +++ b/pkg/cli/cve_cmd_test.go @@ -604,6 +604,61 @@ func TestServerCVEResponse(t *testing.T) { So(str, ShouldContainSubstring, "CVE") }) + Convey("Test CVE by image name - GQL - search CVE by title in results", t, func() { + args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "--search", "CVE-C1"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cveCmd := NewCveCommand(new(searchService)) + buff := bytes.NewBufferString("") + cveCmd.SetOut(buff) + cveCmd.SetErr(buff) + cveCmd.SetArgs(args) + err = cveCmd.Execute() + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + str = strings.TrimSpace(str) + So(err, ShouldBeNil) + So(str, ShouldContainSubstring, "ID SEVERITY TITLE") + So(str, ShouldContainSubstring, "CVE-C1") + So(str, ShouldNotContainSubstring, "CVE-2") + }) + + Convey("Test CVE by image name - GQL - search CVE by id in results", t, func() { + args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "--search", "CVE-2"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cveCmd := NewCveCommand(new(searchService)) + buff := bytes.NewBufferString("") + cveCmd.SetOut(buff) + cveCmd.SetErr(buff) + cveCmd.SetArgs(args) + err = cveCmd.Execute() + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + str = strings.TrimSpace(str) + So(err, ShouldBeNil) + So(str, ShouldContainSubstring, "ID SEVERITY TITLE") + So(str, ShouldContainSubstring, "CVE-2") + So(str, ShouldNotContainSubstring, "CVE-1") + }) + + Convey("Test CVE by image name - GQL - search nonexistent CVE", t, func() { + args := []string{"cvetest", "--image", "zot-cve-test:0.0.1", "--search", "CVE-100"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cveCmd := NewCveCommand(new(searchService)) + buff := bytes.NewBufferString("") + cveCmd.SetOut(buff) + cveCmd.SetErr(buff) + cveCmd.SetArgs(args) + err = cveCmd.Execute() + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + str = strings.TrimSpace(str) + So(err, ShouldBeNil) + So(str, ShouldContainSubstring, "No CVEs found for image") + }) + Convey("Test CVE by image name - GQL - invalid image", t, func() { args := []string{"cvetest", "--image", "invalid:0.0.1"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index dc4b4c59..39658c52 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -1691,7 +1691,7 @@ func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config sea } func (service mockService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, - imageName string, + imageName, searchedCVE string, ) (*cveResult, error) { cveRes := &cveResult{} cveRes.Data = cveData{ @@ -1797,7 +1797,7 @@ func (service mockService) getImageByName(ctx context.Context, config searchConf } func (service mockService) getCveByImage(ctx context.Context, config searchConfig, username, password, - imageName string, rch chan stringResult, wtgrp *sync.WaitGroup, + imageName, searchedCVE string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index 3a556625..8bb0a302 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -302,7 +302,8 @@ func (search imagesByDigestSearcherGQL) search(config searchConfig) (bool, error type cveByImageSearcher struct{} func (search cveByImageSearcher) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("imageName")) || *config.fixedFlag { + if (!canSearch(config.params, newSet("imageName")) && + !canSearch(config.params, newSet("imageName", "searchedCVE"))) || *config.fixedFlag { return false, nil } @@ -318,7 +319,8 @@ func (search cveByImageSearcher) search(config searchConfig) (bool, error) { wg.Add(1) - go config.searchService.getCveByImage(ctx, config, username, password, *config.params["imageName"], strErr, &wg) + go config.searchService.getCveByImage(ctx, config, username, password, *config.params["imageName"], + *config.params["searchedCVE"], strErr, &wg) wg.Add(1) errCh := make(chan error, 1) @@ -337,7 +339,8 @@ func (search cveByImageSearcher) search(config searchConfig) (bool, error) { type cveByImageSearcherGQL struct{} func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) { - if !canSearch(config.params, newSet("imageName")) || *config.fixedFlag { + if (!canSearch(config.params, newSet("imageName")) && + !canSearch(config.params, newSet("imageName", "searchedCVE"))) || *config.fixedFlag { return false, nil } @@ -352,7 +355,8 @@ func (search cveByImageSearcherGQL) search(config searchConfig) (bool, error) { defer cancel() - cveList, err := config.searchService.getCveByImageGQL(ctx, config, username, password, *config.params["imageName"]) + cveList, err := config.searchService.getCveByImageGQL(ctx, config, username, password, + *config.params["imageName"], *config.params["searchedCVE"]) if err != nil { return true, err } diff --git a/pkg/cli/service.go b/pkg/cli/service.go index 1c6a0c7b..1241b588 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -29,7 +29,7 @@ type SearchService interface { //nolint:interfacebloat getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string) (*imageListStructForDigestGQL, error) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, - imageName string) (*cveResult, error) + imageName string, searchedCVE string) (*cveResult, error) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string, digest string) (*imagesForCve, error) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, @@ -43,7 +43,7 @@ type SearchService interface { //nolint:interfacebloat getAllImages(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup) - getCveByImage(ctx context.Context, config searchConfig, username, password, imageName string, + getCveByImage(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, channel chan stringResult, wtgrp *sync.WaitGroup) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string, channel chan stringResult, wtgrp *sync.WaitGroup) @@ -226,11 +226,11 @@ func (service searchService) getImagesByCveIDGQL(ctx context.Context, config sea } func (service searchService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, - imageName string, + imageName, searchedCVE string, ) (*cveResult, error) { - query := fmt.Sprintf(`{ CVEListForImage (image:"%s")`+ + query := fmt.Sprintf(`{ CVEListForImage (image:"%s", searchedCVE:"%s")`+ ` { Tag CVEList { Id Title Severity Description `+ - `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName) + `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName, searchedCVE) result := &cveResult{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) @@ -618,14 +618,14 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config } func (service searchService) getCveByImage(ctx context.Context, config searchConfig, username, password, - imageName string, rch chan stringResult, wtgrp *sync.WaitGroup, + imageName, searchedCVE string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) - query := fmt.Sprintf(`{ CVEListForImage (image:"%s")`+ + query := fmt.Sprintf(`{ CVEListForImage (image:"%s", searchedCVE:"%s")`+ ` { Tag CVEList { Id Title Severity Description `+ - `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName) + `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName, searchedCVE) result := &cveResult{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index e46cb004..5ea3a811 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -3,6 +3,7 @@ package cveinfo import ( "encoding/json" "fmt" + "strings" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -19,7 +20,7 @@ import ( type CveInfo interface { GetImageListForCVE(repo, cveID string) ([]common.TagInfo, error) GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) - GetCVEListForImage(repo, tag string, pageinput PageInput) ([]cvemodel.CVE, PageInfo, error) + GetCVEListForImage(repo, tag string, searchedCVE string, pageinput PageInput) ([]cvemodel.CVE, PageInfo, error) GetCVESummaryForImage(repo, tag string) (ImageCVESummary, error) CompareSeverities(severity1, severity2 string) int UpdateDB() error @@ -215,7 +216,18 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]commo return fixedTags, nil } -func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, pageInput PageInput) ( +func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE 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) { + pageFinder.Add(cve) + } + } +} + +func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, searchedCVE string, pageInput PageInput) ( []cvemodel.CVE, PageInfo, error, @@ -237,9 +249,7 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, pageInput PageIn return []cvemodel.CVE{}, PageInfo{}, err } - for _, cve := range cveMap { - pageFinder.Add(cve) - } + filterCVEList(cveMap, searchedCVE, pageFinder) cveList, pageInfo := pageFinder.Page() diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index 097535cc..2ca57651 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -1206,14 +1206,14 @@ func TestCVEStruct(t *testing.T) { } // Image is found - cveList, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", pageInput) + cveList, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE1") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("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) { So(pageInfo.ItemCount, ShouldEqual, 3) So(pageInfo.TotalCount, ShouldEqual, 3) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.1", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.1", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 2) So(cveList[0].ID, ShouldEqual, "CVE1") @@ -1230,42 +1230,42 @@ func TestCVEStruct(t *testing.T) { So(pageInfo.ItemCount, ShouldEqual, 2) So(pageInfo.TotalCount, ShouldEqual, 2) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.1.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.1.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE3") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo6", "1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo6", "1.0.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Image is not scannable - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo2", "1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo2", "1.0.0", "", pageInput) So(err, ShouldEqual, zerr.ErrScanNotSupported) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Tag is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo3", "1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo3", "1.0.0", "", pageInput) So(err, ShouldEqual, zerr.ErrTagMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Manifest is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo5", "nonexitent-manifest", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo5", "nonexitent-manifest", "", pageInput) So(err, ShouldEqual, zerr.ErrManifestDataNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) // Repo is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo100", "1.0.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo100", "1.0.0", "", pageInput) So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) @@ -1385,7 +1385,7 @@ func TestCVEStruct(t *testing.T) { So(cveSummary.Count, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") - cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", pageInput) + cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", pageInput) So(err, ShouldNotBeNil) So(cveList, ShouldBeEmpty) So(pageInfo.ItemCount, ShouldEqual, 0) diff --git a/pkg/extensions/search/cve/pagination_test.go b/pkg/extensions/search/cve/pagination_test.go index 7206eb33..172b326f 100644 --- a/pkg/extensions/search/cve/pagination_test.go +++ b/pkg/extensions/search/cve/pagination_test.go @@ -182,7 +182,7 @@ func TestCVEPagination(t *testing.T) { Convey("Page", func() { Convey("defaults", func() { // By default expect unlimitted results sorted by severity - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{}) + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -193,7 +193,7 @@ func TestCVEPagination(t *testing.T) { previousSeverity = severityToInt[cve.Severity] } - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cveinfo.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -211,7 +211,7 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) @@ -222,7 +222,8 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", + cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -232,7 +233,8 @@ func TestCVEPagination(t *testing.T) { } sort.Sort(sort.Reverse(sort.StringSlice(cveIds))) - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", + cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -241,7 +243,8 @@ func TestCVEPagination(t *testing.T) { So(cve.ID, ShouldEqual, cveIds[i]) } - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{SortBy: cveinfo.SeverityDsc}) + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", + cveinfo.PageInput{SortBy: cveinfo.SeverityDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) @@ -259,11 +262,12 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.AlphabeticAsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 3) So(pageInfo.ItemCount, ShouldEqual, 3) @@ -272,11 +276,12 @@ func TestCVEPagination(t *testing.T) { So(cves[1].ID, ShouldEqual, "CVE2") So(cves[2].ID, ShouldEqual, "CVE3") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ Limit: 2, Offset: 1, SortBy: cveinfo.AlphabeticDsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 2) So(pageInfo.ItemCount, ShouldEqual, 2) @@ -284,11 +289,12 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE3") So(cves[1].ID, ShouldEqual, "CVE2") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.SeverityDsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 3) So(pageInfo.ItemCount, ShouldEqual, 3) @@ -300,11 +306,12 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cveinfo.PageInput{ Limit: 5, Offset: 20, SortBy: cveinfo.AlphabeticAsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) @@ -315,11 +322,12 @@ func TestCVEPagination(t *testing.T) { }) Convey("limit > len(cves)", func() { - cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ + cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticAsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 2) So(pageInfo.ItemCount, ShouldEqual, 2) @@ -327,11 +335,12 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE3") So(cves[1].ID, ShouldEqual, "CVE4") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticDsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 2) So(pageInfo.ItemCount, ShouldEqual, 2) @@ -339,11 +348,12 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE1") So(cves[1].ID, ShouldEqual, "CVE0") - cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", cveinfo.PageInput{ + cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.SeverityDsc, - }) + }, + ) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 2) So(pageInfo.ItemCount, ShouldEqual, 2) diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 8013e01a..1a7e7456 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -152,7 +152,7 @@ type ComplexityRoot struct { Query struct { BaseImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int - CVEListForImage func(childComplexity int, image string, requestedPage *PageInput) int + CVEListForImage func(childComplexity int, image string, requestedPage *PageInput, searchedCve *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 @@ -194,7 +194,7 @@ type ComplexityRoot struct { } type QueryResolver interface { - CVEListForImage(ctx context.Context, image string, requestedPage *PageInput) (*CVEResultForImage, error) + CVEListForImage(ctx context.Context, image string, requestedPage *PageInput, searchedCve *string) (*CVEResultForImage, error) ImageListForCve(ctx context.Context, id string, requestedPage *PageInput) (*PaginatedImagesResult, error) ImageListWithCVEFixed(ctx context.Context, id string, image string, requestedPage *PageInput) (*PaginatedImagesResult, error) ImageListForDigest(ctx context.Context, id string, requestedPage *PageInput) (*PaginatedImagesResult, error) @@ -686,7 +686,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)), true + return e.complexity.Query.CVEListForImage(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput), args["searchedCVE"].(*string)), true case "Query.DerivedImageList": if e.complexity.Query.DerivedImageList == nil { @@ -1541,6 +1541,8 @@ type Query { image: String!, "Sets the parameters of the requested page" requestedPage: PageInput + "Search term for specific CVE by title/id" + searchedCVE: String ): CVEResultForImage! """ @@ -1724,6 +1726,15 @@ func (ec *executionContext) field_Query_CVEListForImage_args(ctx context.Context } } args["requestedPage"] = arg1 + var arg2 *string + if tmp, ok := rawArgs["searchedCVE"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("searchedCVE")) + arg2, err = ec.unmarshalOString2áš–string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["searchedCVE"] = arg2 return args, nil } @@ -4882,7 +4893,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)) + return ec.resolvers.Query().CVEListForImage(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput), fc.Args["searchedCVE"].(*string)) }) if err != nil { ec.Error(ctx, err) diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index d0335898..f32c390c 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -292,6 +292,7 @@ func getCVEListForImage( image string, cveInfo cveinfo.CveInfo, requestedPage *gql_generated.PageInput, + searchedCVE string, log log.Logger, //nolint:unparam // may be used by devs for debugging ) (*gql_generated.CVEResultForImage, error) { if requestedPage == nil { @@ -316,7 +317,7 @@ func getCVEListForImage( return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("reference by digest not supported") } - cveList, pageInfo, err := cveInfo.GetCVEListForImage(repo, ref, pageInput) + cveList, pageInfo, err := cveInfo.GetCVEListForImage(repo, ref, searchedCVE, 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 8b7d0a2d..5fce2676 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -2080,6 +2080,12 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo Title: "Title CVE3", Description: "Description CVE3", }, + "CVE34": { + ID: "CVE34", + Severity: "LOW", + Title: "Title for CVE34", + Description: "Description CVE34", + }, }, nil } @@ -2140,21 +2146,63 @@ 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.Error(), ShouldContainSubstring, "reference by digest not supported") - 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") - expectedCves := []string{"CVE1", "CVE2", "CVE3"} + expectedCves := []string{"CVE1", "CVE2", "CVE3", "CVE34"} 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) + // 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") @@ -2165,7 +2213,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") diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 28357ea2..60ad01a7 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -550,6 +550,8 @@ type Query { image: String!, "Sets the parameters of the requested page" requestedPage: PageInput + "Search term for specific CVE by title/id" + searchedCVE: String ): CVEResultForImage! """ diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 3e8fb880..7271d68c 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -14,12 +14,16 @@ import ( ) // CVEListForImage is the resolver for the CVEListForImage field. -func (r *queryResolver) CVEListForImage(ctx context.Context, image string, requestedPage *gql_generated.PageInput) (*gql_generated.CVEResultForImage, error) { +func (r *queryResolver) CVEListForImage(ctx context.Context, image string, requestedPage *gql_generated.PageInput, searchedCve *string) (*gql_generated.CVEResultForImage, error) { if r.cveInfo == nil { return &gql_generated.CVEResultForImage{}, zerr.ErrCVESearchDisabled } - return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, r.log) + if searchedCve == nil { + return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, "", r.log) + } + + return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, *searchedCve, 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 8223648f..d32fa57e 100644 --- a/pkg/test/mocks/cve_mock.go +++ b/pkg/test/mocks/cve_mock.go @@ -9,7 +9,7 @@ import ( type CveInfoMock struct { GetImageListForCVEFn func(repo, cveID string) ([]common.TagInfo, error) GetImageListWithCVEFixedFn func(repo, cveID string) ([]common.TagInfo, error) - GetCVEListForImageFn func(repo string, reference string, pageInput cveinfo.PageInput, + GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cveinfo.PageInput, ) ([]cvemodel.CVE, cveinfo.PageInfo, error) GetCVESummaryForImageFn func(repo string, reference string, ) (cveinfo.ImageCVESummary, error) @@ -33,13 +33,15 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]commo return []common.TagInfo{}, nil } -func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string, pageInput cveinfo.PageInput) ( +func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string, + searchedCVE string, pageInput cveinfo.PageInput, +) ( []cvemodel.CVE, cveinfo.PageInfo, error, ) { if cveInfo.GetCVEListForImageFn != nil { - return cveInfo.GetCVEListForImageFn(repo, reference, pageInput) + return cveInfo.GetCVEListForImageFn(repo, reference, searchedCVE, pageInput) } return []cvemodel.CVE{}, cveinfo.PageInfo{}, nil