diff --git a/errors/errors.go b/errors/errors.go index 44191b1d..09207d9e 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -168,4 +168,6 @@ var ( ErrAPINotSupported = errors.New("registry at the given address doesn't implement the correct API") ErrURLNotFound = errors.New("url not found") ErrInvalidSearchQuery = errors.New("invalid search query") + ErrImageNotFound = errors.New("image not found") + ErrAmbiguousInput = errors.New("input is not specific enough") ) diff --git a/pkg/cli/client/cve_cmd_test.go b/pkg/cli/client/cve_cmd_test.go index 518cecfb..552fa388 100644 --- a/pkg/cli/client/cve_cmd_test.go +++ b/pkg/cli/client/cve_cmd_test.go @@ -198,6 +198,276 @@ func TestNegativeServerResponse(t *testing.T) { }) } +func TestCVEDiffList(t *testing.T) { + port := test.GetFreePort() + url := test.GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + + dir := t.TempDir() + + conf.Storage.RootDirectory = dir + trivyConfig := &extconf.TrivyConfig{ + DBRepository: "ghcr.io/project-zot/trivy-db", + } + cveConfig := &extconf.CVEConfig{ + UpdateInterval: 2, + Trivy: trivyConfig, + } + defaultVal := true + searchConfig := &extconf.SearchConfig{ + BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, + CVE: cveConfig, + } + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") + if err != nil { + panic(err) + } + + logPath := logFile.Name() + defer os.Remove(logPath) + + writers := io.MultiWriter(os.Stdout, logFile) + + ctlr := api.NewController(conf) + ctlr.Log.Logger = ctlr.Log.Output(writers) + + if err := ctlr.Init(); 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, _, _ := zcommon.GetRepoReference(image) + + if zcommon.IsDigest(ref) { + return getCveResults(ref), nil + } + + repoMeta, _ := ctlr.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 + }, + } + + ctlr.CveScanner = scanner + + go func() { + if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { + panic(err) + } + }() + + defer ctlr.Shutdown() + + test.WaitTillServerReady(url) + + ctx := context.Background() + _, err = ociutils.InitializeTestMetaDB(ctx, ctlr.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"}, + }, + }, + ) + + Convey("Test CVE by image name - GQL - positive", t, func() { + args := []string{"diff", "repo:image", "repo:base-image", "--config", "cvetest"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cveCmd := client.NewCVECommand(client.NewSearchService()) + buff := bytes.NewBufferString("") + cveCmd.SetOut(buff) + cveCmd.SetErr(buff) + cveCmd.SetArgs(args) + err = cveCmd.Execute() + fmt.Println(buff.String()) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + str = strings.TrimSpace(str) + So(str, ShouldContainSubstring, "CVE3") + So(str, ShouldNotContainSubstring, "CVE1") + So(str, ShouldNotContainSubstring, "CVE2") + }) + + Convey("Errors", t, func() { + // args := []string{"diff", "repo:image", "repo:base-image", "--config", "cvetest"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cveCmd := client.NewCVECommand(client.NewSearchService()) + + Convey("Set wrong number of params", func() { + args := []string{"diff", "repo:image", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("First input is not a repo:tag", func() { + args := []string{"diff", "bad-input", "repo:base-image", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("Second input is arch but not enough args", func() { + args := []string{"diff", "repo:base-image", "linux/amd64", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("Second input is arch 3rd is repo:tag", func() { + args := []string{"diff", "repo:base-image", "linux/amd64", "repo:base-image", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldBeNil) + }) + Convey("Second input is repo:tag 3rd is repo:tag", func() { + args := []string{"diff", "repo:base-image", "repo:base-image", "repo:base-image", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("Second input is arch 3rd is arch as well", func() { + args := []string{"diff", "repo:base-image", "linux/amd64", "linux/amd64", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("Second input is repo:tag 3rd is arch", func() { + args := []string{"diff", "repo:base-image", "repo:base-image", "linux/amd64", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldBeNil) + }) + Convey("Second input is repo:tag 3rd is arch, 4th is repo:tag", func() { + args := []string{ + "diff", "repo:base-image", "repo:base-image", "linux/amd64", "repo:base-image", + "--config", "cvetest", + } + + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("Second input is arch 3rd is repo:tag, 4th is arch", func() { + args := []string{"diff", "repo:base-image", "linux/amd64", "repo:base-image", "linux/amd64", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldBeNil) + }) + Convey("input is with digest ref", func() { + args := []string{"diff", "repo@sha256:123123", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + Convey("input is with just repo no ref", func() { + args := []string{"diff", "repo", "--config", "cvetest"} + cveCmd.SetArgs(args) + So(cveCmd.Execute(), ShouldNotBeNil) + }) + }) +} + //nolint:dupl func TestServerCVEResponse(t *testing.T) { port := test.GetFreePort() diff --git a/pkg/cli/client/cves_cmd.go b/pkg/cli/client/cves_cmd.go index eaa56928..4892e002 100644 --- a/pkg/cli/client/cves_cmd.go +++ b/pkg/cli/client/cves_cmd.go @@ -30,6 +30,7 @@ func NewCVECommand(searchService SearchService) *cobra.Command { cvesCmd.AddCommand(NewCveForImageCommand(searchService)) cvesCmd.AddCommand(NewImagesByCVEIDCommand(searchService)) cvesCmd.AddCommand(NewFixedTagsCommand(searchService)) + cvesCmd.AddCommand(NewCVEDiffCommand(searchService)) return cvesCmd } diff --git a/pkg/cli/client/cves_sub_cmd.go b/pkg/cli/client/cves_sub_cmd.go index 75e8b8af..171d0dd1 100644 --- a/pkg/cli/client/cves_sub_cmd.go +++ b/pkg/cli/client/cves_sub_cmd.go @@ -140,3 +140,139 @@ func NewFixedTagsCommand(searchService SearchService) *cobra.Command { return fixedTagsCmd } + +func NewCVEDiffCommand(searchService SearchService) *cobra.Command { + var ( + minuendStr, minuendArch string + subtrahendStr, subtrahendArch string + ) + imagesByCVEIDCmd := &cobra.Command{ + Use: "diff [minuend] ([minuend-platform]) [subtrahend] ([subtrahend-platform])", + Short: "List the CVE's present in minuend that are not present in subtrahend", + Long: `List the CVE's present in minuend that are not present in subtrahend`, + Args: func(cmd *cobra.Command, args []string) error { + const ( + twoArgs = 2 + threeArgs = 3 + fourArgs = 4 + ) + + if err := cobra.RangeArgs(twoArgs, fourArgs)(cmd, args); err != nil { + return err + } + + if !isRepoTag(args[0]) { + return fmt.Errorf("%w: first parameter should be a repo:tag", zerr.ErrInvalidArgs) + } + + minuendStr = args[0] + + if isRepoTag(args[1]) { + subtrahendStr = args[1] + } else { + minuendArch = args[1] + + if len(args) == twoArgs { + return fmt.Errorf("%w: not enough arguments, specified only 1 image with arch", zerr.ErrInvalidArgs) + } + } + + if len(args) == twoArgs { + return nil + } + + if isRepoTag(args[2]) { + if subtrahendStr == "" { + subtrahendStr = args[2] + } else { + return fmt.Errorf("%w: too many repo:tag inputs", zerr.ErrInvalidArgs) + } + } else { + if subtrahendStr == "" { + return fmt.Errorf("%w: 3rd argument should be a repo:tag", zerr.ErrInvalidArgs) + } else { + subtrahendArch = args[2] + } + } + + if len(args) == threeArgs { + return nil + } + + if isRepoTag(args[3]) { + return fmt.Errorf("%w: 4th argument should not be a repo:tag but an arch", zerr.ErrInvalidArgs) + } else { + subtrahendArch = args[3] + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + searchConfig, err := GetSearchConfigFromFlags(cmd, searchService) + if err != nil { + return err + } + + err = CheckExtEndPointQuery(searchConfig, CVEDiffListForImagesQuery()) + if err != nil { + return fmt.Errorf("%w: '%s'", err, CVEDiffListForImagesQuery().Name) + } + + // parse the args and determine the input + minuend := getImageIdentifier(minuendStr, minuendArch) + subtrahend := getImageIdentifier(subtrahendStr, subtrahendArch) + + return SearchCVEDiffList(searchConfig, minuend, subtrahend) + }, + } + + return imagesByCVEIDCmd +} + +func isRepoTag(arg string) bool { + _, _, _, err := zcommon.GetRepoReference(arg) //nolint:dogsled + + return err == nil +} + +type osArch struct { + Os string + Arch string +} + +type ImageIdentifier struct { + Repo string `json:"repo"` + Tag string `json:"tag"` + Digest string `json:"digest"` + Platform *osArch `json:"platform"` +} + +func getImageIdentifier(repoTagStr, platformStr string) ImageIdentifier { + var tag, digest string + + repo, ref, isTag, err := zcommon.GetRepoReference(repoTagStr) + if err != nil { + return ImageIdentifier{} + } + + if isTag { + tag = ref + } else { + digest = ref + } + + // check if the following input is a repo:tag or repo@digest, if not then it's a platform + var platform *osArch + + if platformStr != "" { + os, arch, _ := strings.Cut(platformStr, "/") + platform = &osArch{Os: os, Arch: arch} + } + + return ImageIdentifier{ + Repo: repo, + Tag: tag, + Digest: digest, + Platform: platform, + } +} diff --git a/pkg/cli/client/flags.go b/pkg/cli/client/flags.go index 64f804ae..639aa121 100644 --- a/pkg/cli/client/flags.go +++ b/pkg/cli/client/flags.go @@ -22,6 +22,7 @@ const ( DebugFlag = "debug" SearchedCVEID = "cve-id" SortByFlag = "sort-by" + PlatformFlag = "platform" ) const ( diff --git a/pkg/cli/client/gql_queries.go b/pkg/cli/client/gql_queries.go index de861d5f..3f90d01a 100644 --- a/pkg/cli/client/gql_queries.go +++ b/pkg/cli/client/gql_queries.go @@ -25,6 +25,12 @@ func CVEResultForImage() GQLType { } } +func CVEDiffResult() GQLType { + return GQLType{ + Name: "CVEDiffResult", + } +} + func PaginatedImagesResult() GQLType { return GQLType{ Name: "PaginatedImagesResult", @@ -51,6 +57,14 @@ func ImageListQuery() GQLQuery { } } +func CVEDiffListForImagesQuery() GQLQuery { + return GQLQuery{ + Name: "CVEDiffListForImages", + Args: []string{"minuend", "subtrahend", "requestedPage", "searchedCVE", "excludedCVE"}, + ReturnType: CVEDiffResult(), + } +} + func ImageListForDigestQuery() GQLQuery { return GQLQuery{ Name: "ImageListForDigest", diff --git a/pkg/cli/client/image_cmd_internal_test.go b/pkg/cli/client/image_cmd_internal_test.go index 793a02a2..242835a4 100644 --- a/pkg/cli/client/image_cmd_internal_test.go +++ b/pkg/cli/client/image_cmd_internal_test.go @@ -1077,6 +1077,20 @@ type mockService struct { getFixedTagsForCVEGQLFn func(ctx context.Context, config SearchConfig, username, password, imageName, cveID string, ) (*common.ImageListWithCVEFixedResponse, error) + + getCVEDiffListGQLFn func(ctx context.Context, config SearchConfig, username, password string, + minuend, subtrahend ImageIdentifier, + ) (*cveDiffListResp, error) +} + +func (service mockService) getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string, + minuend, subtrahend ImageIdentifier, +) (*cveDiffListResp, error) { + if service.getCVEDiffListGQLFn != nil { + return service.getCVEDiffListGQLFn(ctx, config, username, password, minuend, subtrahend) + } + + return &cveDiffListResp{}, nil } func (service mockService) getRepos(ctx context.Context, config SearchConfig, username, diff --git a/pkg/cli/client/search_functions.go b/pkg/cli/client/search_functions.go index 3d453697..a4f2fca9 100644 --- a/pkg/cli/client/search_functions.go +++ b/pkg/cli/client/search_functions.go @@ -267,6 +267,52 @@ func SearchCVEForImageGQL(config SearchConfig, image, searchedCveID string) erro return nil } +func SearchCVEDiffList(config SearchConfig, minuend, subtrahend ImageIdentifier) error { + username, password := getUsernameAndPassword(config.User) + + response, err := config.SearchService.getCVEDiffListGQL(context.Background(), config, username, password, + minuend, subtrahend) + if err != nil { + return err + } + + cveDiffResult := response.Data.CveDiffResult + + result := cveResult{ + Data: cveData{ + CVEListForImage: cveListForImage{ + Tag: cveDiffResult.Minuend.Tag, + CVEList: cveDiffResult.CVEList, + Summary: cveDiffResult.Summary, + }, + }, + } + + var builder strings.Builder + + if config.OutputFormat == defaultOutputFormat || config.OutputFormat == "" { + imageCVESummary := result.Data.CVEListForImage.Summary + + statsStr := fmt.Sprintf("CRITICAL %d, HIGH %d, MEDIUM %d, LOW %d, UNKNOWN %d, TOTAL %d\n\n", + imageCVESummary.CriticalCount, imageCVESummary.HighCount, imageCVESummary.MediumCount, + imageCVESummary.LowCount, imageCVESummary.UnknownCount, imageCVESummary.Count) + + fmt.Fprint(config.ResultWriter, statsStr) + + printCVETableHeader(&builder) + fmt.Fprint(config.ResultWriter, builder.String()) + } + + out, err := result.string(config.OutputFormat) + if err != nil { + return err + } + + fmt.Fprint(config.ResultWriter, out) + + return nil +} + func SearchImagesByCVEIDGQL(config SearchConfig, repo, cveid string) error { username, password := getUsernameAndPassword(config.User) ctx, cancel := context.WithCancel(context.Background()) diff --git a/pkg/cli/client/service.go b/pkg/cli/client/service.go index 8b9f8576..2b406a02 100644 --- a/pkg/cli/client/service.go +++ b/pkg/cli/client/service.go @@ -48,6 +48,9 @@ type SearchService interface { //nolint:interfacebloat baseImage string) (*common.BaseImageListResponse, error) getReferrersGQL(ctx context.Context, config SearchConfig, username, password string, repo, digest string) (*common.ReferrersResp, error) + getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string, + minuend, subtrahend ImageIdentifier, + ) (*cveDiffListResp, error) globalSearchGQL(ctx context.Context, config SearchConfig, username, password string, query string) (*common.GlobalSearch, error) @@ -146,6 +149,46 @@ func (service searchService) getReferrersGQL(ctx context.Context, config SearchC return result, nil } +func (service searchService) getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string, + minuend, subtrahend ImageIdentifier, +) (*cveDiffListResp, error) { + minuendInput := getImageInput(minuend) + subtrahendInput := getImageInput(subtrahend) + query := fmt.Sprintf(` + { + CVEDiffListForImages( minuend: %s, subtrahend: %s ) { + Minuend {Repo Tag} + Subtrahend {Repo Tag} + CVEList { + Id Title Description Severity Reference + PackageList {Name InstalledVersion FixedVersion} + } + Summary { + Count UnknownCount LowCount MediumCount HighCount CriticalCount + } + Page {TotalCount ItemCount} + } + }`, minuendInput, subtrahendInput) + + result := &cveDiffListResp{} + + err := service.makeGraphQLQuery(ctx, config, username, password, query, result) + if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { + return nil, errResult + } + + return result, nil +} + +func getImageInput(img ImageIdentifier) string { + platform := "" + if img.Platform != nil { + platform = fmt.Sprintf(`, Platform: {Os: "%s", Arch: "%s"}`, img.Platform.Os, img.Platform.Arch) + } + + return fmt.Sprintf(`{Repo: "%s", Tag: "%s", Digest: "%s"%s}`, img.Repo, img.Tag, img.Digest, platform) +} + func (service searchService) globalSearchGQL(ctx context.Context, config SearchConfig, username, password string, query string, ) (*common.GlobalSearch, error) { @@ -746,6 +789,22 @@ type cve struct { PackageList []packageList `json:"PackageList"` } +type cveDiffListResp struct { + Data cveDiffResultsForImages `json:"data"` + Errors []common.ErrorGQL `json:"errors"` +} + +type cveDiffResultsForImages struct { + CveDiffResult cveDiffResult `json:"cveDiffListForImages"` +} + +type cveDiffResult struct { + Minuend ImageIdentifier `json:"minuend"` + Subtrahend ImageIdentifier `json:"subtrahend"` + CVEList []cve `json:"cveList"` + Summary common.ImageVulnerabilitySummary `json:"summary"` +} + //nolint:tagliatelle // graphQL schema type cveListForImage struct { Tag string `json:"Tag"` @@ -755,7 +814,7 @@ type cveListForImage struct { //nolint:tagliatelle // graphQL schema type cveData struct { - CVEListForImage cveListForImage `json:"CVEListForImage"` + CVEListForImage cveListForImage `json:"cveListForImage"` } func (cve cveResult) string(format string) (string, error) { diff --git a/pkg/common/model.go b/pkg/common/model.go index 6c0a7aef..596e9967 100644 --- a/pkg/common/model.go +++ b/pkg/common/model.go @@ -112,6 +112,17 @@ type HistoryDescription struct { EmptyLayer bool `json:"emptyLayer"` } +type OsArch struct { + Os, Arch string +} + +type ImageIdentifier struct { + Repo string + Tag string + Digest string + Platform OsArch +} + type Referrer struct { MediaType string `json:"mediatype"` ArtifactType string `json:"artifacttype"` diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index cbdd4454..1ebf530e 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -21,8 +21,10 @@ 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, excludedCVE string, severity string, + GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE, excludedCVE string, severity string, pageinput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) + GetCVEDiffListForImages(ctx context.Context, minuend, subtrahend, searchedCVE, excludedCVE string, + pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string) (cvemodel.ImageCVESummary, error) } @@ -329,8 +331,8 @@ func getConfigAndDigest(metaDB mTypes.MetaDB, manifestDigestStr string) (ispec.I return manifestData.Manifests[0].Config, manifestDigest, err } -func filterCVEList( - cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE, severity string, pageFinder *CvePageFinder, +func filterCVEMap(cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE, severity string, + pageFinder *CvePageFinder, ) { searchedCVE = strings.ToUpper(searchedCVE) @@ -349,8 +351,26 @@ func filterCVEList( } } +func filterCVEList(cveList []cvemodel.CVE, searchedCVE, excludedCVE, severity string, pageFinder *CvePageFinder) { + searchedCVE = strings.ToUpper(searchedCVE) + + for _, cve := range cveList { + if severity != "" && (cvemodel.CompareSeverities(cve.Severity, severity) != 0) { + continue + } + + 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, - excludedCVE string, severity string, pageInput cvemodel.PageInput, + excludedCVE, severity string, pageInput cvemodel.PageInput, ) ( []cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error, ) { @@ -379,13 +399,98 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref str return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err } - filterCVEList(cveMap, searchedCVE, excludedCVE, severity, pageFinder) + filterCVEMap(cveMap, searchedCVE, excludedCVE, severity, pageFinder) cveList, pageInfo := pageFinder.Page() return cveList, imageCVESummary, pageInfo, nil } +func (cveinfo BaseCveInfo) GetCVEDiffListForImages(ctx context.Context, minuend, subtrahend, searchedCVE string, + excludedCVE string, pageInput cvemodel.PageInput, +) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) { + minuendRepo, minuendRef, _ := zcommon.GetImageDirAndReference(minuend) + subtrahendRepo, subtrahendRef, _ := zcommon.GetImageDirAndReference(subtrahend) + + // get the CVEs of image and comparedImage + minuendCVEList, _, _, err := cveinfo.GetCVEListForImage(ctx, minuendRepo, minuendRef, searchedCVE, excludedCVE, + "", cvemodel.PageInput{}) + if err != nil { + return nil, cvemodel.ImageCVESummary{}, zcommon.PageInfo{}, err + } + + subtrahendCVEList, _, _, err := cveinfo.GetCVEListForImage(ctx, subtrahendRepo, subtrahendRef, + searchedCVE, excludedCVE, "", cvemodel.PageInput{}) + if err != nil { + return nil, cvemodel.ImageCVESummary{}, zcommon.PageInfo{}, err + } + + subtrahendCVEMap := map[string]cvemodel.CVE{} + + for _, cve := range subtrahendCVEList { + cve := cve + subtrahendCVEMap[cve.ID] = cve + } + + var ( + count int + unknownCount int + lowCount int + mediumCount int + highCount int + criticalCount int + maxSeverity string + + diffCVEs = []cvemodel.CVE{} + ) + + for i := range minuendCVEList { + if _, ok := subtrahendCVEMap[minuendCVEList[i].ID]; !ok { + diffCVEs = append(diffCVEs, minuendCVEList[i]) + + switch minuendCVEList[i].Severity { + case cvemodel.SeverityUnknown: + unknownCount++ + case cvemodel.SeverityLow: + lowCount++ + case cvemodel.SeverityMedium: + mediumCount++ + case cvemodel.SeverityHigh: + highCount++ + case cvemodel.SeverityCritical: + criticalCount++ + } + + if cvemodel.CompareSeverities(maxSeverity, minuendCVEList[i].Severity) > 0 { + maxSeverity = minuendCVEList[i].Severity + } + } + } + + pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy) + if err != nil { + return nil, cvemodel.ImageCVESummary{}, zcommon.PageInfo{}, err + } + + filterCVEList(diffCVEs, "", "", "", pageFinder) + + cveList, pageInfo := pageFinder.Page() + + count = unknownCount + lowCount + mediumCount + highCount + criticalCount + + diffCVESummary := cvemodel.ImageCVESummary{ + Count: count, + UnknownCount: unknownCount, + LowCount: lowCount, + MediumCount: mediumCount, + HighCount: highCount, + CriticalCount: criticalCount, + MaxSeverity: maxSeverity, + } + + return cveList, diffCVESummary, pageInfo, nil +} + func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string, ) (cvemodel.ImageCVESummary, error) { // There are several cases, expected returned values below: diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index f13eef24..8eee4953 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -1277,6 +1277,11 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveSummary.CriticalCount, ShouldEqual, 2) So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") + _, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo1@"+image13Digest, "", "", pageInput) + So(err, ShouldBeNil) + _, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo1:0.1.0", "", "", pageInput) + So(err, ShouldBeNil) + // Image is multiarch cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", "", "", pageInput) So(err, ShouldBeNil) @@ -1625,6 +1630,35 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo _, err = cveInfo.GetImageListForCVE(ctx, repoMultiarch, "CVE1") So(err, ShouldBeNil) + + cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{ + IsImageFormatScannableFn: func(repo, reference string) (bool, error) { + return true, nil + }, + ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { + return nil, zerr.ErrTypeAssertionFailed + }, + }, MetaDB: metaDB} + _, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo1:0.1.0", "", "", pageInput) + So(err, ShouldNotBeNil) + + try := 0 + cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{ + IsImageFormatScannableFn: func(repo, reference string) (bool, error) { + return true, nil + }, + ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { + if try == 1 { + return nil, zerr.ErrTypeAssertionFailed + } + + try++ + + return make(map[string]cvemodel.CVE), nil + }, + }, MetaDB: metaDB} + _, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo6:0.1.0", "", "", pageInput) + So(err, ShouldNotBeNil) }) } diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 2e97e8ba..b7f30e5e 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -59,6 +59,14 @@ type ComplexityRoot struct { Title func(childComplexity int) int } + CVEDiffResult struct { + CVEList func(childComplexity int) int + Minuend func(childComplexity int) int + Page func(childComplexity int) int + Subtrahend func(childComplexity int) int + Summary func(childComplexity int) int + } + CVEResultForImage struct { CVEList func(childComplexity int) int Page func(childComplexity int) int @@ -81,6 +89,13 @@ type ComplexityRoot struct { EmptyLayer func(childComplexity int) int } + ImageIdentifier struct { + Digest func(childComplexity int) int + Platform func(childComplexity int) int + Repo func(childComplexity int) int + Tag func(childComplexity int) int + } + ImageSummary struct { Authors func(childComplexity int) int Description func(childComplexity int) int @@ -171,6 +186,7 @@ type ComplexityRoot struct { Query struct { BaseImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int BookmarkedRepos func(childComplexity int, requestedPage *PageInput) int + CVEDiffListForImages func(childComplexity int, minuend ImageInput, subtrahend ImageInput, requestedPage *PageInput, searchedCve *string, excludedCve *string) int CVEListForImage func(childComplexity int, image string, requestedPage *PageInput, searchedCve *string, excludedCve *string, severity *string) int DerivedImageList func(childComplexity int, image string, digest *string, requestedPage *PageInput) int ExpandedRepoInfo func(childComplexity int, repo string) int @@ -221,6 +237,7 @@ type ComplexityRoot struct { type QueryResolver interface { CVEListForImage(ctx context.Context, image string, requestedPage *PageInput, searchedCve *string, excludedCve *string, severity *string) (*CVEResultForImage, error) + CVEDiffListForImages(ctx context.Context, minuend ImageInput, subtrahend ImageInput, requestedPage *PageInput, searchedCve *string, excludedCve *string) (*CVEDiffResult, 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) @@ -311,6 +328,41 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.CVE.Title(childComplexity), true + case "CVEDiffResult.CVEList": + if e.complexity.CVEDiffResult.CVEList == nil { + break + } + + return e.complexity.CVEDiffResult.CVEList(childComplexity), true + + case "CVEDiffResult.Minuend": + if e.complexity.CVEDiffResult.Minuend == nil { + break + } + + return e.complexity.CVEDiffResult.Minuend(childComplexity), true + + case "CVEDiffResult.Page": + if e.complexity.CVEDiffResult.Page == nil { + break + } + + return e.complexity.CVEDiffResult.Page(childComplexity), true + + case "CVEDiffResult.Subtrahend": + if e.complexity.CVEDiffResult.Subtrahend == nil { + break + } + + return e.complexity.CVEDiffResult.Subtrahend(childComplexity), true + + case "CVEDiffResult.Summary": + if e.complexity.CVEDiffResult.Summary == nil { + break + } + + return e.complexity.CVEDiffResult.Summary(childComplexity), true + case "CVEResultForImage.CVEList": if e.complexity.CVEResultForImage.CVEList == nil { break @@ -402,6 +454,34 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HistoryDescription.EmptyLayer(childComplexity), true + case "ImageIdentifier.Digest": + if e.complexity.ImageIdentifier.Digest == nil { + break + } + + return e.complexity.ImageIdentifier.Digest(childComplexity), true + + case "ImageIdentifier.Platform": + if e.complexity.ImageIdentifier.Platform == nil { + break + } + + return e.complexity.ImageIdentifier.Platform(childComplexity), true + + case "ImageIdentifier.Repo": + if e.complexity.ImageIdentifier.Repo == nil { + break + } + + return e.complexity.ImageIdentifier.Repo(childComplexity), true + + case "ImageIdentifier.Tag": + if e.complexity.ImageIdentifier.Tag == nil { + break + } + + return e.complexity.ImageIdentifier.Tag(childComplexity), true + case "ImageSummary.Authors": if e.complexity.ImageSummary.Authors == nil { break @@ -825,6 +905,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.BookmarkedRepos(childComplexity, args["requestedPage"].(*PageInput)), true + case "Query.CVEDiffListForImages": + if e.complexity.Query.CVEDiffListForImages == nil { + break + } + + args, err := ec.field_Query_CVEDiffListForImages_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.CVEDiffListForImages(childComplexity, args["minuend"].(ImageInput), args["subtrahend"].(ImageInput), args["requestedPage"].(*PageInput), args["searchedCVE"].(*string), args["excludedCVE"].(*string)), true + case "Query.CVEListForImage": if e.complexity.Query.CVEListForImage == nil { break @@ -1125,7 +1217,9 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec := executionContext{rc, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap( ec.unmarshalInputFilter, + ec.unmarshalInputImageInput, ec.unmarshalInputPageInput, + ec.unmarshalInputPlatformInput, ) first := true @@ -1243,6 +1337,54 @@ type CVEResultForImage { Page: PageInfo } +""" +ImageIdentifier +""" +type ImageIdentifier { + """ + Repo name of the image + """ + Repo: String! + """ + The tag of the image + """ + Tag: String! + """ + The digest of the image + """ + Digest: String + """ + The platform of the image + """ + Platform: Platform +} + +""" +Contains the diff results of subtracting Subtrahend's CVEs from Minuend's CVEs +""" +type CVEDiffResult { + """ + Minuend is the image from which CVE's we subtract + """ + Minuend: ImageIdentifier! + """ + Subtrahend is the image which CVE's are subtracted + """ + Subtrahend: ImageIdentifier! + """ + List of CVE objects which are present in minuend but not in subtrahend + """ + CVEList: [CVE] + """ + Summary of the findings for this image + """ + Summary: ImageVulnerabilitySummary + """ + The CVE pagination information, see PageInfo object for more details + """ + Page: PageInfo +} + """ Contains various details about the CVE (Common Vulnerabilities and Exposures) and a list of PackageInfo about the affected packages @@ -1777,6 +1919,42 @@ input PageInput { sortBy: SortCriteria } +""" +PlatformInput contains the Os and the Arch of the input image +""" +input PlatformInput { + """ + The os of the image + """ + Os: String + """ + The arch of the image + """ + Arch: String +} + +""" +ImageInput +""" +input ImageInput { + """ + Repo name of the image + """ + Repo: String! + """ + The tag of the image + """ + Tag: String! + """ + The digest of the image + """ + Digest: String + """ + The platform of the image + """ + Platform: PlatformInput +} + """ Paginated list of RepoSummary objects """ @@ -1855,6 +2033,22 @@ type Query { severity: String ): CVEResultForImage! + """ + Returns a list with CVE's that are present in ` + "`" + `image` + "`" + ` but not in ` + "`" + `comparedImage` + "`" + ` + """ + CVEDiffListForImages( + "Image name in format ` + "`" + `repository:tag` + "`" + ` or ` + "`" + `repository@digest` + "`" + `" + minuend: ImageInput!, + "Compared image name in format ` + "`" + `repository:tag` + "`" + ` or ` + "`" + `repository@digest` + "`" + `" + subtrahend: ImageInput!, + "Sets the parameters of the requested page" + 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 + ): CVEDiffResult! + """ Returns a list of images vulnerable to the CVE of the specified ID """ @@ -2050,6 +2244,57 @@ func (ec *executionContext) field_Query_BookmarkedRepos_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Query_CVEDiffListForImages_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 ImageInput + if tmp, ok := rawArgs["minuend"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("minuend")) + arg0, err = ec.unmarshalNImageInput2zotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["minuend"] = arg0 + var arg1 ImageInput + if tmp, ok := rawArgs["subtrahend"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("subtrahend")) + arg1, err = ec.unmarshalNImageInput2zotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["subtrahend"] = arg1 + var arg2 *PageInput + if tmp, ok := rawArgs["requestedPage"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage")) + arg2, err = ec.unmarshalOPageInput2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["requestedPage"] = arg2 + var arg3 *string + if tmp, ok := rawArgs["searchedCVE"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("searchedCVE")) + arg3, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["searchedCVE"] = arg3 + var arg4 *string + if tmp, ok := rawArgs["excludedCVE"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("excludedCVE")) + arg4, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["excludedCVE"] = arg4 + return args, nil +} + func (ec *executionContext) field_Query_CVEListForImage_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2774,6 +3019,273 @@ func (ec *executionContext) fieldContext_CVE_PackageList(ctx context.Context, fi return fc, nil } +func (ec *executionContext) _CVEDiffResult_Minuend(ctx context.Context, field graphql.CollectedField, obj *CVEDiffResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CVEDiffResult_Minuend(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Minuend, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*ImageIdentifier) + fc.Result = res + return ec.marshalNImageIdentifier2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageIdentifier(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CVEDiffResult_Minuend(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CVEDiffResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Repo": + return ec.fieldContext_ImageIdentifier_Repo(ctx, field) + case "Tag": + return ec.fieldContext_ImageIdentifier_Tag(ctx, field) + case "Digest": + return ec.fieldContext_ImageIdentifier_Digest(ctx, field) + case "Platform": + return ec.fieldContext_ImageIdentifier_Platform(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageIdentifier", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _CVEDiffResult_Subtrahend(ctx context.Context, field graphql.CollectedField, obj *CVEDiffResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CVEDiffResult_Subtrahend(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Subtrahend, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*ImageIdentifier) + fc.Result = res + return ec.marshalNImageIdentifier2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageIdentifier(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CVEDiffResult_Subtrahend(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CVEDiffResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Repo": + return ec.fieldContext_ImageIdentifier_Repo(ctx, field) + case "Tag": + return ec.fieldContext_ImageIdentifier_Tag(ctx, field) + case "Digest": + return ec.fieldContext_ImageIdentifier_Digest(ctx, field) + case "Platform": + return ec.fieldContext_ImageIdentifier_Platform(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageIdentifier", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _CVEDiffResult_CVEList(ctx context.Context, field graphql.CollectedField, obj *CVEDiffResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CVEDiffResult_CVEList(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CVEList, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*Cve) + fc.Result = res + return ec.marshalOCVE2ᚕᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐCve(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CVEDiffResult_CVEList(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CVEDiffResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Id": + return ec.fieldContext_CVE_Id(ctx, field) + case "Title": + return ec.fieldContext_CVE_Title(ctx, field) + case "Description": + return ec.fieldContext_CVE_Description(ctx, field) + case "Reference": + return ec.fieldContext_CVE_Reference(ctx, field) + case "Severity": + return ec.fieldContext_CVE_Severity(ctx, field) + case "PackageList": + return ec.fieldContext_CVE_PackageList(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CVE", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _CVEDiffResult_Summary(ctx context.Context, field graphql.CollectedField, obj *CVEDiffResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CVEDiffResult_Summary(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Summary, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*ImageVulnerabilitySummary) + fc.Result = res + return ec.marshalOImageVulnerabilitySummary2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CVEDiffResult_Summary(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CVEDiffResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "MaxSeverity": + return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) + case "Count": + return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + case "UnknownCount": + return ec.fieldContext_ImageVulnerabilitySummary_UnknownCount(ctx, field) + case "LowCount": + return ec.fieldContext_ImageVulnerabilitySummary_LowCount(ctx, field) + case "MediumCount": + return ec.fieldContext_ImageVulnerabilitySummary_MediumCount(ctx, field) + case "HighCount": + return ec.fieldContext_ImageVulnerabilitySummary_HighCount(ctx, field) + case "CriticalCount": + return ec.fieldContext_ImageVulnerabilitySummary_CriticalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _CVEDiffResult_Page(ctx context.Context, field graphql.CollectedField, obj *CVEDiffResult) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CVEDiffResult_Page(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Page, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*PageInfo) + fc.Result = res + return ec.marshalOPageInfo2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInfo(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CVEDiffResult_Page(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CVEDiffResult", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "TotalCount": + return ec.fieldContext_PageInfo_TotalCount(ctx, field) + case "ItemCount": + return ec.fieldContext_PageInfo_ItemCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _CVEResultForImage_Tag(ctx context.Context, field graphql.CollectedField, obj *CVEResultForImage) (ret graphql.Marshaler) { fc, err := ec.fieldContext_CVEResultForImage_Tag(ctx, field) if err != nil { @@ -3423,6 +3935,182 @@ func (ec *executionContext) fieldContext_HistoryDescription_EmptyLayer(ctx conte return fc, nil } +func (ec *executionContext) _ImageIdentifier_Repo(ctx context.Context, field graphql.CollectedField, obj *ImageIdentifier) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageIdentifier_Repo(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Repo, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageIdentifier_Repo(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageIdentifier", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageIdentifier_Tag(ctx context.Context, field graphql.CollectedField, obj *ImageIdentifier) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageIdentifier_Tag(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Tag, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageIdentifier_Tag(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageIdentifier", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageIdentifier_Digest(ctx context.Context, field graphql.CollectedField, obj *ImageIdentifier) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageIdentifier_Digest(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Digest, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageIdentifier_Digest(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageIdentifier", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageIdentifier_Platform(ctx context.Context, field graphql.CollectedField, obj *ImageIdentifier) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageIdentifier_Platform(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Platform, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*Platform) + fc.Result = res + return ec.marshalOPlatform2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatform(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageIdentifier_Platform(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageIdentifier", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Os": + return ec.fieldContext_Platform_Os(ctx, field) + case "Arch": + return ec.fieldContext_Platform_Arch(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Platform", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _ImageSummary_RepoName(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { fc, err := ec.fieldContext_ImageSummary_RepoName(ctx, field) if err != nil { @@ -6053,6 +6741,73 @@ func (ec *executionContext) fieldContext_Query_CVEListForImage(ctx context.Conte return fc, nil } +func (ec *executionContext) _Query_CVEDiffListForImages(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query_CVEDiffListForImages(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().CVEDiffListForImages(rctx, fc.Args["minuend"].(ImageInput), fc.Args["subtrahend"].(ImageInput), fc.Args["requestedPage"].(*PageInput), fc.Args["searchedCVE"].(*string), fc.Args["excludedCVE"].(*string)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*CVEDiffResult) + fc.Result = res + return ec.marshalNCVEDiffResult2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐCVEDiffResult(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query_CVEDiffListForImages(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "Minuend": + return ec.fieldContext_CVEDiffResult_Minuend(ctx, field) + case "Subtrahend": + return ec.fieldContext_CVEDiffResult_Subtrahend(ctx, field) + case "CVEList": + return ec.fieldContext_CVEDiffResult_CVEList(ctx, field) + case "Summary": + return ec.fieldContext_CVEDiffResult_Summary(ctx, field) + case "Page": + return ec.fieldContext_CVEDiffResult_Page(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type CVEDiffResult", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query_CVEDiffListForImages_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_ImageListForCVE(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_ImageListForCVE(ctx, field) if err != nil { @@ -9839,6 +10594,54 @@ func (ec *executionContext) unmarshalInputFilter(ctx context.Context, obj interf return it, nil } +func (ec *executionContext) unmarshalInputImageInput(ctx context.Context, obj interface{}) (ImageInput, error) { + var it ImageInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"Repo", "Tag", "Digest", "Platform"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "Repo": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Repo")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Repo = data + case "Tag": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Tag")) + data, err := ec.unmarshalNString2string(ctx, v) + if err != nil { + return it, err + } + it.Tag = data + case "Digest": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Digest")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Digest = data + case "Platform": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Platform")) + data, err := ec.unmarshalOPlatformInput2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatformInput(ctx, v) + if err != nil { + return it, err + } + it.Platform = data + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputPageInput(ctx context.Context, obj interface{}) (PageInput, error) { var it PageInput asMap := map[string]interface{}{} @@ -9880,6 +10683,40 @@ func (ec *executionContext) unmarshalInputPageInput(ctx context.Context, obj int return it, nil } +func (ec *executionContext) unmarshalInputPlatformInput(ctx context.Context, obj interface{}) (PlatformInput, error) { + var it PlatformInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + fieldsInOrder := [...]string{"Os", "Arch"} + for _, k := range fieldsInOrder { + v, ok := asMap[k] + if !ok { + continue + } + switch k { + case "Os": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Os")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Os = data + case "Arch": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("Arch")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.Arch = data + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -9972,6 +10809,56 @@ func (ec *executionContext) _CVE(ctx context.Context, sel ast.SelectionSet, obj return out } +var cVEDiffResultImplementors = []string{"CVEDiffResult"} + +func (ec *executionContext) _CVEDiffResult(ctx context.Context, sel ast.SelectionSet, obj *CVEDiffResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, cVEDiffResultImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("CVEDiffResult") + case "Minuend": + out.Values[i] = ec._CVEDiffResult_Minuend(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "Subtrahend": + out.Values[i] = ec._CVEDiffResult_Subtrahend(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "CVEList": + out.Values[i] = ec._CVEDiffResult_CVEList(ctx, field, obj) + case "Summary": + out.Values[i] = ec._CVEDiffResult_Summary(ctx, field, obj) + case "Page": + out.Values[i] = ec._CVEDiffResult_Page(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var cVEResultForImageImplementors = []string{"CVEResultForImage"} func (ec *executionContext) _CVEResultForImage(ctx context.Context, sel ast.SelectionSet, obj *CVEResultForImage) graphql.Marshaler { @@ -10100,6 +10987,54 @@ func (ec *executionContext) _HistoryDescription(ctx context.Context, sel ast.Sel return out } +var imageIdentifierImplementors = []string{"ImageIdentifier"} + +func (ec *executionContext) _ImageIdentifier(ctx context.Context, sel ast.SelectionSet, obj *ImageIdentifier) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, imageIdentifierImplementors) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ImageIdentifier") + case "Repo": + out.Values[i] = ec._ImageIdentifier_Repo(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "Tag": + out.Values[i] = ec._ImageIdentifier_Tag(ctx, field, obj) + if out.Values[i] == graphql.Null { + out.Invalids++ + } + case "Digest": + out.Values[i] = ec._ImageIdentifier_Digest(ctx, field, obj) + case "Platform": + out.Values[i] = ec._ImageIdentifier_Platform(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var imageSummaryImplementors = []string{"ImageSummary"} func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.SelectionSet, obj *ImageSummary) graphql.Marshaler { @@ -10606,6 +11541,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "CVEDiffListForImages": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_CVEDiffListForImages(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "ImageListForCVE": field := field @@ -11484,6 +12441,20 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNCVEDiffResult2zotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐCVEDiffResult(ctx context.Context, sel ast.SelectionSet, v CVEDiffResult) graphql.Marshaler { + return ec._CVEDiffResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNCVEDiffResult2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐCVEDiffResult(ctx context.Context, sel ast.SelectionSet, v *CVEDiffResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._CVEDiffResult(ctx, sel, v) +} + func (ec *executionContext) marshalNCVEResultForImage2zotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐCVEResultForImage(ctx context.Context, sel ast.SelectionSet, v CVEResultForImage) graphql.Marshaler { return ec._CVEResultForImage(ctx, sel, &v) } @@ -11512,6 +12483,21 @@ func (ec *executionContext) marshalNGlobalSearchResult2ᚖzotregistryᚗdevᚋzo return ec._GlobalSearchResult(ctx, sel, v) } +func (ec *executionContext) marshalNImageIdentifier2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageIdentifier(ctx context.Context, sel ast.SelectionSet, v *ImageIdentifier) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._ImageIdentifier(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNImageInput2zotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageInput(ctx context.Context, v interface{}) (ImageInput, error) { + res, err := ec.unmarshalInputImageInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalNImageSummary2zotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageSummary(ctx context.Context, sel ast.SelectionSet, v ImageSummary) graphql.Marshaler { return ec._ImageSummary(ctx, sel, &v) } @@ -12409,6 +13395,14 @@ func (ec *executionContext) marshalOPlatform2ᚖzotregistryᚗdevᚋzotᚋpkgᚋ return ec._Platform(ctx, sel, v) } +func (ec *executionContext) unmarshalOPlatformInput2ᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPlatformInput(ctx context.Context, v interface{}) (*PlatformInput, error) { + if v == nil { + return nil, nil + } + res, err := ec.unmarshalInputPlatformInput(ctx, v) + return &res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalOReferrer2ᚕᚖzotregistryᚗdevᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐReferrer(ctx context.Context, sel ast.SelectionSet, v []*Referrer) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 9700c0bd..be9fb835 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -35,6 +35,20 @@ type Cve struct { PackageList []*PackageInfo `json:"PackageList,omitempty"` } +// Contains the diff results of subtracting Subtrahend's CVEs from Minuend's CVEs +type CVEDiffResult struct { + // Minuend is the image from which CVE's we subtract + Minuend *ImageIdentifier `json:"Minuend"` + // Subtrahend is the image which CVE's are subtracted + Subtrahend *ImageIdentifier `json:"Subtrahend"` + // List of CVE objects which are present in minuend but not in subtrahend + CVEList []*Cve `json:"CVEList,omitempty"` + // Summary of the findings for this image + Summary *ImageVulnerabilitySummary `json:"Summary,omitempty"` + // The CVE pagination information, see PageInfo object for more details + Page *PageInfo `json:"Page,omitempty"` +} + // Contains the tag of the image and a list of CVEs type CVEResultForImage struct { // Tag affected by the CVEs @@ -92,6 +106,30 @@ type HistoryDescription struct { EmptyLayer *bool `json:"EmptyLayer,omitempty"` } +// ImageIdentifier +type ImageIdentifier struct { + // Repo name of the image + Repo string `json:"Repo"` + // The tag of the image + Tag string `json:"Tag"` + // The digest of the image + Digest *string `json:"Digest,omitempty"` + // The platform of the image + Platform *Platform `json:"Platform,omitempty"` +} + +// ImageInput +type ImageInput struct { + // Repo name of the image + Repo string `json:"Repo"` + // The tag of the image + Tag string `json:"Tag"` + // The digest of the image + Digest *string `json:"Digest,omitempty"` + // The platform of the image + Platform *PlatformInput `json:"Platform,omitempty"` +} + // Details about a specific image, it is used by queries returning a list of images // We define an image as a pairing or a repository and a tag belonging to that repository type ImageSummary struct { @@ -264,6 +302,14 @@ type Platform struct { Arch *string `json:"Arch,omitempty"` } +// PlatformInput contains the Os and the Arch of the input image +type PlatformInput struct { + // The os of the image + Os *string `json:"Os,omitempty"` + // The arch of the image + Arch *string `json:"Arch,omitempty"` +} + // Queries supported by the zot server type Query struct { } diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 77dd1713..638cebb5 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -279,6 +279,260 @@ func getCVEListForImage( }, nil } +func getCVEDiffListForImages( + ctx context.Context, //nolint:unparam // may be used in the future to filter by permissions + minuend gql_generated.ImageInput, + subtrahend gql_generated.ImageInput, + metaDB mTypes.MetaDB, + 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.CVEDiffResult, error) { + minuend, err := resolveImageData(ctx, minuend, metaDB) + if err != nil { + return nil, err + } + + resultMinuend := getImageIdentifier(minuend) + resultSubtrahend := gql_generated.ImageIdentifier{} + + if subtrahend.Repo != "" { + subtrahend, err = resolveImageData(ctx, subtrahend, metaDB) + if err != nil { + return nil, err + } + resultSubtrahend = getImageIdentifier(subtrahend) + } else { + // search for base images + // get minuend image meta + minuendSummary, err := metaDB.GetImageMeta(godigest.Digest(deref(minuend.Digest, ""))) + if err != nil { + return &gql_generated.CVEDiffResult{}, err + } + + // get the base images for the minuend + minuendBaseImages, err := metaDB.FilterTags(ctx, mTypes.AcceptOnlyRepo(minuend.Repo), + filterBaseImagesForMeta(minuendSummary)) + if err != nil { + return &gql_generated.CVEDiffResult{}, err + } + + // get the best base image as subtrahend + // get the one with most layers in common + imgLayers := map[string]struct{}{} + + for _, layer := range minuendSummary.Manifests[0].Manifest.Layers { + imgLayers[layer.Digest.String()] = struct{}{} + } + + bestMatchingScore := 0 + + for _, baseImage := range minuendBaseImages { + for _, baseManifest := range baseImage.Manifests { + currentMatchingScore := 0 + + for _, layer := range baseManifest.Manifest.Layers { + if _, ok := imgLayers[layer.Digest.String()]; ok { + currentMatchingScore++ + } + } + + if currentMatchingScore > bestMatchingScore { + bestMatchingScore = currentMatchingScore + + resultSubtrahend = gql_generated.ImageIdentifier{ + Repo: baseImage.Repo, + Tag: baseImage.Tag, + Digest: ref(baseImage.Manifests[0].Digest.String()), + Platform: &gql_generated.Platform{ + Os: ref(baseImage.Manifests[0].Config.OS), + Arch: ref(getArch(baseImage.Manifests[0].Config.Platform)), + }, + } + subtrahend.Repo = baseImage.Repo + subtrahend.Tag = baseImage.Tag + subtrahend.Digest = ref(baseImage.Manifests[0].Digest.String()) + } + } + } + } + + minuendRepoRef := minuend.Repo + "@" + deref(minuend.Digest, "") + subtrahendRepoRef := subtrahend.Repo + "@" + deref(subtrahend.Digest, "") + page := dderef(requestedPage) + + diffCVEs, diffSummary, _, err := cveInfo.GetCVEDiffListForImages(ctx, minuendRepoRef, subtrahendRepoRef, searchedCVE, + excludedCVE, cvemodel.PageInput{ + Limit: deref(page.Limit, 0), + Offset: deref(page.Offset, 0), + SortBy: cvemodel.SortCriteria(deref(page.SortBy, gql_generated.SortCriteriaSeverity)), + }) + if err != nil { + return nil, err + } + + cveids := []*gql_generated.Cve{} + + for _, cveDetail := range diffCVEs { + vulID := cveDetail.ID + desc := cveDetail.Description + title := cveDetail.Title + severity := cveDetail.Severity + referenceURL := cveDetail.Reference + + pkgList := make([]*gql_generated.PackageInfo, 0) + + for _, pkg := range cveDetail.PackageList { + pkg := pkg + + pkgList = append(pkgList, + &gql_generated.PackageInfo{ + Name: &pkg.Name, + InstalledVersion: &pkg.InstalledVersion, + FixedVersion: &pkg.FixedVersion, + }, + ) + } + + cveids = append(cveids, + &gql_generated.Cve{ + ID: &vulID, + Title: &title, + Description: &desc, + Severity: &severity, + Reference: &referenceURL, + PackageList: pkgList, + }, + ) + } + + return &gql_generated.CVEDiffResult{ + Minuend: &resultMinuend, + Subtrahend: &resultSubtrahend, + Summary: &gql_generated.ImageVulnerabilitySummary{ + Count: &diffSummary.Count, + UnknownCount: &diffSummary.UnknownCount, + LowCount: &diffSummary.LowCount, + MediumCount: &diffSummary.MediumCount, + HighCount: &diffSummary.HighCount, + CriticalCount: &diffSummary.CriticalCount, + MaxSeverity: &diffSummary.MaxSeverity, + }, + CVEList: cveids, + Page: &gql_generated.PageInfo{}, + }, nil +} + +func getImageIdentifier(img gql_generated.ImageInput) gql_generated.ImageIdentifier { + return gql_generated.ImageIdentifier{ + Repo: img.Repo, + Tag: img.Tag, + Digest: img.Digest, + Platform: getIdentifierPlatform(img.Platform), + } +} + +func getIdentifierPlatform(platform *gql_generated.PlatformInput) *gql_generated.Platform { + if platform == nil { + return nil + } + + return &gql_generated.Platform{ + Os: platform.Os, + Arch: platform.Arch, + } +} + +// rename idea: identify image from input. +func resolveImageData(ctx context.Context, imageInput gql_generated.ImageInput, metaDB mTypes.MetaDB, +) (gql_generated.ImageInput, error) { + if imageInput.Repo == "" { + return gql_generated.ImageInput{}, zerr.ErrEmptyRepoName + } + + if imageInput.Tag == "" { + return gql_generated.ImageInput{}, zerr.ErrEmptyTag + } + + // try checking if the tag is a simple image first + repoMeta, err := metaDB.GetRepoMeta(ctx, imageInput.Repo) + if err != nil { + return gql_generated.ImageInput{}, err + } + + descriptor, ok := repoMeta.Tags[imageInput.Tag] + if !ok { + return gql_generated.ImageInput{}, zerr.ErrImageNotFound + } + + switch descriptor.MediaType { + case ispec.MediaTypeImageManifest: + imageInput.Digest = ref(descriptor.Digest) + + return imageInput, nil + case ispec.MediaTypeImageIndex: + if dderef(imageInput.Digest) == "" && !isPlatformSpecified(imageInput.Platform) { + return gql_generated.ImageInput{}, + fmt.Errorf("%w: platform or specific manifest digest needed", zerr.ErrAmbiguousInput) + } + + imageMeta, err := metaDB.GetImageMeta(godigest.Digest(descriptor.Digest)) + if err != nil { + return gql_generated.ImageInput{}, err + } + + for _, manifest := range imageMeta.Manifests { + if manifest.Digest.String() == dderef(imageInput.Digest) || + isMatchingPlatform(manifest.Config.Platform, dderef(imageInput.Platform)) { + imageInput.Digest = ref(manifest.Digest.String()) + imageInput.Platform = &gql_generated.PlatformInput{ + Os: ref(manifest.Config.OS), + Arch: ref(getArch(manifest.Config.Platform)), + } + + return imageInput, nil + } + } + + return imageInput, zerr.ErrImageNotFound + } + + return imageInput, nil +} + +func isPlatformSpecified(platformInput *gql_generated.PlatformInput) bool { + if platformInput == nil { + return false + } + + if dderef(platformInput.Os) == "" || dderef(platformInput.Arch) == "" { + return false + } + + return true +} + +func isMatchingPlatform(platform ispec.Platform, platformInput gql_generated.PlatformInput) bool { + if platform.OS != deref(platformInput.Os, "") { + return false + } + + arch := getArch(platform) + + return arch == deref(platformInput.Arch, "") +} + +func getArch(platform ispec.Platform) string { + arch := platform.Architecture + if arch != "" && platform.Variant != "" { + arch += "/" + platform.Variant + } + + return arch +} + func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc { return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { manifestDigest := imageMeta.Manifests[0].Digest.String() @@ -1001,6 +1255,47 @@ func filterBaseImages(image *gql_generated.ImageSummary) mTypes.FilterFunc { } } +func filterBaseImagesForMeta(image mTypes.ImageMeta) mTypes.FilterFunc { + return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { + var addImageToList bool + + manifest := imageMeta.Manifests[0] + + for i := range image.Manifests { + manifestDigest := manifest.Digest.String() + if manifestDigest == image.Manifests[i].Digest.String() { + return false + } + + addImageToList = true + + for _, l := range manifest.Manifest.Layers { + foundLayer := false + + for _, k := range image.Manifests[i].Manifest.Layers { + if l.Digest.String() == k.Digest.String() { + foundLayer = true + + break + } + } + + if !foundLayer { + addImageToList = false + + break + } + } + + if addImageToList { + return true + } + } + + return false + } +} + func validateGlobalSearchInput(query string, filter *gql_generated.Filter, requestedPage *gql_generated.PageInput, ) error { @@ -1187,6 +1482,17 @@ func (p timeSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +// dderef is a default deref. +func dderef[T any](pointer *T) T { + var defValue T + + if pointer != nil { + return *pointer + } + + return defValue +} + func deref[T any](pointer *T, defaultVal T) T { if pointer != nil { return *pointer @@ -1195,6 +1501,12 @@ func deref[T any](pointer *T, defaultVal T) T { return defaultVal } +func ref[T any](input T) *T { + ref := input + + return &ref +} + func getImageList(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam ) (*gql_generated.PaginatedImagesResult, error) { diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index efe30a7e..16651604 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -14,6 +14,7 @@ import ( 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" @@ -730,6 +731,50 @@ func TestQueryResolverErrors(t *testing.T) { 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, @@ -1883,6 +1928,272 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo ) 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) { @@ -2352,10 +2663,67 @@ func TestExpandedRepoInfoErrors(t *testing.T) { }) } -func ref[T any](input T) *T { - ref := input +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") + }) - return &ref + 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 { diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index e0cbe2f5..00cff376 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -33,6 +33,54 @@ type CVEResultForImage { Page: PageInfo } +""" +ImageIdentifier +""" +type ImageIdentifier { + """ + Repo name of the image + """ + Repo: String! + """ + The tag of the image + """ + Tag: String! + """ + The digest of the image + """ + Digest: String + """ + The platform of the image + """ + Platform: Platform +} + +""" +Contains the diff results of subtracting Subtrahend's CVEs from Minuend's CVEs +""" +type CVEDiffResult { + """ + Minuend is the image from which CVE's we subtract + """ + Minuend: ImageIdentifier! + """ + Subtrahend is the image which CVE's are subtracted + """ + Subtrahend: ImageIdentifier! + """ + List of CVE objects which are present in minuend but not in subtrahend + """ + CVEList: [CVE] + """ + Summary of the findings for this image + """ + Summary: ImageVulnerabilitySummary + """ + The CVE pagination information, see PageInfo object for more details + """ + Page: PageInfo +} + """ Contains various details about the CVE (Common Vulnerabilities and Exposures) and a list of PackageInfo about the affected packages @@ -567,6 +615,42 @@ input PageInput { sortBy: SortCriteria } +""" +PlatformInput contains the Os and the Arch of the input image +""" +input PlatformInput { + """ + The os of the image + """ + Os: String + """ + The arch of the image + """ + Arch: String +} + +""" +ImageInput +""" +input ImageInput { + """ + Repo name of the image + """ + Repo: String! + """ + The tag of the image + """ + Tag: String! + """ + The digest of the image + """ + Digest: String + """ + The platform of the image + """ + Platform: PlatformInput +} + """ Paginated list of RepoSummary objects """ @@ -645,6 +729,22 @@ type Query { severity: String ): CVEResultForImage! + """ + Returns a list with CVE's that are present in `image` but not in `comparedImage` + """ + CVEDiffListForImages( + "Image name in format `repository:tag` or `repository@digest`" + minuend: ImageInput!, + "Compared image name in format `repository:tag` or `repository@digest`" + subtrahend: ImageInput!, + "Sets the parameters of the requested page" + 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 + ): CVEDiffResult! + """ Returns a list of images vulnerable to the CVE of the specified ID """ diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index 4e732a78..65efc1a2 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -23,6 +23,16 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string, reque return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, deref(searchedCve, ""), deref(excludedCve, ""), deref(severity, ""), r.log) } +// CVEDiffListForImages is the resolver for the CVEDiffListForImages field. +func (r *queryResolver) CVEDiffListForImages(ctx context.Context, minuend gql_generated.ImageInput, subtrahend gql_generated.ImageInput, requestedPage *gql_generated.PageInput, searchedCve *string, excludedCve *string) (*gql_generated.CVEDiffResult, error) { + if r.cveInfo == nil { + return &gql_generated.CVEDiffResult{}, zerr.ErrCVESearchDisabled + } + + return getCVEDiffListForImages(ctx, minuend, subtrahend, r.metaDB, r.cveInfo, requestedPage, + dderef(searchedCve), dderef(excludedCve), r.log) +} + // ImageListForCve is the resolver for the ImageListForCVE field. func (r *queryResolver) ImageListForCve(ctx context.Context, id string, filter *gql_generated.Filter, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) { if r.cveInfo == nil { diff --git a/pkg/meta/types/types.go b/pkg/meta/types/types.go index c39b79fc..e6374e91 100644 --- a/pkg/meta/types/types.go +++ b/pkg/meta/types/types.go @@ -38,6 +38,10 @@ func AcceptAllRepoTag(repo, tag string) bool { return true } +func AcceptOnlyRepo(repo string) func(repo, tag string) bool { + return func(r, t string) bool { return repo == r } +} + func AcceptAllImageMeta(repoMeta RepoMeta, imageMeta ImageMeta) bool { return true } diff --git a/pkg/test/mocks/cve_mock.go b/pkg/test/mocks/cve_mock.go index f7f66c4d..478ea14b 100644 --- a/pkg/test/mocks/cve_mock.go +++ b/pkg/test/mocks/cve_mock.go @@ -8,12 +8,29 @@ import ( ) type CveInfoMock struct { - GetImageListForCVEFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) + 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, reference, searchedCVE, excludedCVE, severity string, + + GetCVEListForImageFn func(ctx context.Context, repo, reference, searchedCVE, excludedCVE, severity string, pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) + GetCVESummaryForImageMediaFn func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) + + GetCVEDiffListForImagesFn func(ctx context.Context, minuend, subtrahend, searchedCVE string, + excludedCVE string, pageInput cvemodel.PageInput, + ) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) +} + +func (cveInfo CveInfoMock) GetCVEDiffListForImages(ctx context.Context, minuend, subtrahend, searchedCVE string, + excludedCVE string, pageInput cvemodel.PageInput, +) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) { + if cveInfo.GetCVEDiffListForImagesFn != nil { + return cveInfo.GetCVEDiffListForImagesFn(ctx, minuend, subtrahend, searchedCVE, excludedCVE, pageInput) + } + + return []cvemodel.CVE{}, cvemodel.ImageCVESummary{}, common.PageInfo{}, nil } func (cveInfo CveInfoMock) GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) {