From 519ea75d9a799ccf6c7864ef3368f19f89824e72 Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Wed, 26 May 2021 20:22:31 +0300 Subject: [PATCH] Implement a way to search for an image by manifest, config or layer digest ``` Usage: zot images [config-name] [flags] Flags: -d, --digest string List images containing a specific manifest, config, or layer digest [...] ``` --- pkg/cli/image_cmd.go | 2 + pkg/cli/image_cmd_test.go | 50 ++++ pkg/cli/searcher.go | 33 +++ pkg/cli/service.go | 129 ++++++--- pkg/extensions/search/common/oci_layout.go | 152 ++++++++++ pkg/extensions/search/cve/cve.go | 138 +--------- pkg/extensions/search/cve/cve_test.go | 14 +- pkg/extensions/search/cve/models.go | 2 + pkg/extensions/search/digest/digest.go | 88 ++++++ pkg/extensions/search/digest/digest_test.go | 290 ++++++++++++++++++++ pkg/extensions/search/generated.go | 229 ++++++++++++++++ pkg/extensions/search/models_gen.go | 5 + pkg/extensions/search/resolver.go | 80 +++++- pkg/extensions/search/schema.graphql | 6 + 14 files changed, 1043 insertions(+), 175 deletions(-) create mode 100644 pkg/extensions/search/common/oci_layout.go create mode 100644 pkg/extensions/search/digest/digest.go create mode 100644 pkg/extensions/search/digest/digest_test.go diff --git a/pkg/cli/image_cmd.go b/pkg/cli/image_cmd.go index b7bd13ea..dead9746 100644 --- a/pkg/cli/image_cmd.go +++ b/pkg/cli/image_cmd.go @@ -109,6 +109,8 @@ func parseBooleanConfig(configPath, configName, configParam string) (bool, error func setupImageFlags(imageCmd *cobra.Command, searchImageParams map[string]*string, servURL, user, outputFormat *string) { searchImageParams["imageName"] = imageCmd.Flags().StringP("name", "n", "", "List image details by name") + searchImageParams["digest"] = imageCmd.Flags().StringP("digest", "d", "", + "List images containing a specific manifest, config, or layer digest") imageCmd.Flags().StringVar(servURL, "url", "", "Specify zot server URL if config-name is not mentioned") imageCmd.Flags().StringVarP(user, "user", "u", "", `User Credentials of zot server in "username:password" format`) diff --git a/pkg/cli/image_cmd_test.go b/pkg/cli/image_cmd_test.go index f4b63be4..b8d9d2c2 100644 --- a/pkg/cli/image_cmd_test.go +++ b/pkg/cli/image_cmd_test.go @@ -22,6 +22,7 @@ import ( zotErrors "github.com/anuvu/zot/errors" "github.com/anuvu/zot/pkg/api" "github.com/anuvu/zot/pkg/compliance/v1_0_0" + "github.com/anuvu/zot/pkg/extensions" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" @@ -285,6 +286,9 @@ func TestServerResponse(t *testing.T) { url := "http://127.0.0.1:8080" config := api.NewConfig() config.HTTP.Port = port + config.Extensions = &extensions.ExtensionConfig{ + Search: &extensions.SearchConfig{}, + } c := api.NewController(config) dir, err := ioutil.TempDir("", "oci-repo-test") if err != nil { @@ -372,6 +376,47 @@ func TestServerResponse(t *testing.T) { }) }) + Convey("Test image by digest", func() { + args := []string{"imagetest", "--digest", "a0ca253b"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cmd := NewImageCommand(new(searchService)) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + // Actual cli output should be something similar to (order of images may differ): + // IMAGE NAME TAG DIGEST SIZE + // repo7 test:2.0 a0ca253b 15B + // repo7 test:1.0 a0ca253b 15B + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") + Convey("with shorthand", func() { + args := []string{"imagetest", "-d", "a0ca253b"} + configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) + defer os.Remove(configPath) + cmd := NewImageCommand(new(searchService)) + buff := bytes.NewBufferString("") + cmd.SetOut(buff) + cmd.SetErr(buff) + cmd.SetArgs(args) + err = cmd.Execute() + So(err, ShouldBeNil) + space := regexp.MustCompile(`\s+`) + str := space.ReplaceAllString(buff.String(), " ") + actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "IMAGE NAME TAG DIGEST SIZE") + So(actual, ShouldContainSubstring, "repo7 test:2.0 a0ca253b 15B") + So(actual, ShouldContainSubstring, "repo7 test:1.0 a0ca253b 15B") + }) + }) + Convey("Test image by name invalid name", func() { args := []string{"imagetest", "--name", "repo777"} configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"imagetest","url":"%s","showspinner":false}]}`, url)) @@ -528,6 +573,11 @@ func (service mockService) getImagesByCveID(ctx context.Context, config searchCo service.getImageByName(ctx, config, username, password, "anImage", c, wg) } +func (service mockService) getImagesByDigest(ctx context.Context, config searchConfig, username, + password, digest string, c chan stringResult, wg *sync.WaitGroup) { + service.getImageByName(ctx, config, username, password, "anImage", c, wg) +} + func (service mockService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) { service.getImageByName(ctx, config, username, password, imageName, c, wg) diff --git a/pkg/cli/searcher.go b/pkg/cli/searcher.go index e18f424e..f23e1811 100644 --- a/pkg/cli/searcher.go +++ b/pkg/cli/searcher.go @@ -19,6 +19,7 @@ func getImageSearchers() []searcher { searchers := []searcher{ new(allImagesSearcher), new(imageByNameSearcher), + new(imagesByDigestSearcher), } return searchers @@ -125,6 +126,38 @@ func (search imageByNameSearcher) search(config searchConfig) (bool, error) { } } +type imagesByDigestSearcher struct{} + +func (search imagesByDigestSearcher) search(config searchConfig) (bool, error) { + if !canSearch(config.params, newSet("digest")) { + return false, nil + } + + username, password := getUsernameAndPassword(*config.user) + imageErr := make(chan stringResult) + ctx, cancel := context.WithCancel(context.Background()) + + var wg sync.WaitGroup + + wg.Add(1) + + go config.searchService.getImagesByDigest(ctx, config, username, password, + *config.params["digest"], imageErr, &wg) + wg.Add(1) + + var errCh chan error = make(chan error, 1) + go collectResults(config, &wg, imageErr, cancel, printImageTableHeader, errCh) + + wg.Wait() + + select { + case err := <-errCh: + return true, err + default: + return true, nil + } +} + type cveByImageSearcher struct{} func (search cveByImageSearcher) search(config searchConfig) (bool, error) { diff --git a/pkg/cli/service.go b/pkg/cli/service.go index d9fe1aef..d7106bb2 100644 --- a/pkg/cli/service.go +++ b/pkg/cli/service.go @@ -29,6 +29,8 @@ type SearchService interface { channel chan stringResult, wg *sync.WaitGroup) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cveID string, channel chan stringResult, wg *sync.WaitGroup) + getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string, + channel chan stringResult, wg *sync.WaitGroup) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveID string, channel chan stringResult, wg *sync.WaitGroup) getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cveID string, @@ -147,7 +149,8 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search cveID) result := &imagesForCve{} - endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query") + err := service.makeGraphQLQuery(config, username, password, query, result) + if err != nil { if isContextDone(ctx) { return @@ -157,17 +160,7 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search return } - err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result) - if err != nil { - if isContextDone(ctx) { - return - } - c <- stringResult{"", err} - - return - } - - if result.Errors != nil { + if result.Errors != nil || err != nil { var errBuilder strings.Builder for _, err := range result.Errors { @@ -200,6 +193,61 @@ func (service searchService) getImagesByCveID(ctx context.Context, config search localWg.Wait() } +func (service searchService) getImagesByDigest(ctx context.Context, config searchConfig, username, + password string, digest string, c chan stringResult, wg *sync.WaitGroup) { + defer wg.Done() + defer close(c) + + query := fmt.Sprintf(`{ImageListForDigest(id: "%s") {`+` + Name Tags } + }`, + digest) + result := &imagesForDigest{} + + err := service.makeGraphQLQuery(config, username, password, query, result) + + if err != nil { + if isContextDone(ctx) { + return + } + c <- stringResult{"", err} + + return + } + + if result.Errors != nil { + var errBuilder strings.Builder + + for _, err := range result.Errors { + fmt.Fprintln(&errBuilder, err.Message) + } + + if isContextDone(ctx) { + return + } + c <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 + + return + } + + var localWg sync.WaitGroup + + p := newSmoothRateLimiter(ctx, &localWg, c) + localWg.Add(1) + + go p.startRateLimiter() + + for _, image := range result.Data.ImageListForDigest { + for _, tag := range image.Tags { + localWg.Add(1) + + go addManifestCallToPool(ctx, config, p, username, password, image.Name, tag, c, &localWg) + } + } + + localWg.Wait() +} + func (service searchService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cveID string, c chan stringResult, wg *sync.WaitGroup) { defer wg.Done() @@ -211,17 +259,8 @@ func (service searchService) getImageByNameAndCVEID(ctx context.Context, config cveID) result := &imagesForCve{} - endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query") - if err != nil { - if isContextDone(ctx) { - return - } - c <- stringResult{"", err} + err := service.makeGraphQLQuery(config, username, password, query, result) - return - } - - err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result) if err != nil { if isContextDone(ctx) { return @@ -278,17 +317,8 @@ func (service searchService) getCveByImage(ctx context.Context, config searchCon `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName) result := &cveResult{} - endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query") - if err != nil { - if isContextDone(ctx) { - return - } - c <- stringResult{"", err} + err := service.makeGraphQLQuery(config, username, password, query, result) - return - } - - err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result) if err != nil { if isContextDone(ctx) { return @@ -372,17 +402,8 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear cveID, imageName) result := &fixedTags{} - endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query") - if err != nil { - if isContextDone(ctx) { - return - } - c <- stringResult{"", err} + err := service.makeGraphQLQuery(config, username, password, query, result) - return - } - - err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, result) if err != nil { if isContextDone(ctx) { return @@ -423,6 +444,23 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear localWg.Wait() } +// Query using JQL, the query string is passed as a parameter +// errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr. +func (service searchService) makeGraphQLQuery(config searchConfig, username, password, query string, + resultPtr interface{}) error { + endPoint, err := combineServerAndEndpointURL(*config.servURL, "/query") + if err != nil { + return err + } + + err = makeGraphQLRequest(endPoint, query, username, password, *config.verifyTLS, resultPtr) + if err != nil { + return err + } + + return nil +} + func addManifestCallToPool(ctx context.Context, config searchConfig, p *requestsPool, username, password, imageName, tagName string, c chan stringResult, wg *sync.WaitGroup) { defer wg.Done() @@ -555,6 +593,13 @@ type imagesForCve struct { } `json:"data"` } +type imagesForDigest struct { + Errors []errorGraphQL `json:"errors"` + Data struct { + ImageListForDigest []tagListResp `json:"ImageListForDigest"` + } `json:"data"` +} + type tagListResp struct { Name string `json:"name"` Tags []string `json:"tags"` diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go new file mode 100644 index 00000000..123853b6 --- /dev/null +++ b/pkg/extensions/search/common/oci_layout.go @@ -0,0 +1,152 @@ +// Package common ... +package common + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path" + "strings" + + "github.com/anuvu/zot/errors" + "github.com/anuvu/zot/pkg/log" + "github.com/anuvu/zot/pkg/storage" + v1 "github.com/google/go-containerregistry/pkg/v1" + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// CveInfo ... +type OciLayoutUtils struct { + Log log.Logger + StoreController storage.StoreController +} + +// NewOciLayoutUtils initializes a new OciLayoutUtils object. +func NewOciLayoutUtils(storeController storage.StoreController, log log.Logger) *OciLayoutUtils { + return &OciLayoutUtils{Log: log, StoreController: storeController} +} + +// Below method will return image path including root dir, root dir is determined by splitting. +func (olu OciLayoutUtils) GetImageRepoPath(image string) string { + var rootDir string + + prefixName := GetRoutePrefix(image) + + subStore := olu.StoreController.SubStore + + if subStore != nil { + imgStore, ok := olu.StoreController.SubStore[prefixName] + if ok { + rootDir = imgStore.RootDir() + } else { + rootDir = olu.StoreController.DefaultStore.RootDir() + } + } else { + rootDir = olu.StoreController.DefaultStore.RootDir() + } + + return path.Join(rootDir, image) +} + +func (olu OciLayoutUtils) GetImageManifests(imagePath string) ([]ispec.Descriptor, error) { + buf, err := ioutil.ReadFile(path.Join(imagePath, "index.json")) + + if err != nil { + if os.IsNotExist(err) { + olu.Log.Error().Err(err).Msg("index.json doesn't exist") + + return nil, errors.ErrRepoNotFound + } + + olu.Log.Error().Err(err).Msg("unable to open index.json") + + return nil, errors.ErrRepoNotFound + } + + var index ispec.Index + + if err := json.Unmarshal(buf, &index); err != nil { + olu.Log.Error().Err(err).Str("dir", imagePath).Msg("invalid JSON") + return nil, errors.ErrRepoNotFound + } + + return index.Manifests, nil +} + +func (olu OciLayoutUtils) GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error) { + var blobIndex v1.Manifest + + blobBuf, err := ioutil.ReadFile(path.Join(imageDir, "blobs", digest.Algorithm().String(), digest.Encoded())) + if err != nil { + olu.Log.Error().Err(err).Msg("unable to open image metadata file") + + return blobIndex, err + } + + if err := json.Unmarshal(blobBuf, &blobIndex); err != nil { + olu.Log.Error().Err(err).Msg("unable to marshal blob index") + + return blobIndex, err + } + + return blobIndex, nil +} + +func (olu OciLayoutUtils) GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error) { + var imageInfo ispec.Image + + blobBuf, err := ioutil.ReadFile(path.Join(imageDir, "blobs", hash.Algorithm, hash.Hex)) + if err != nil { + olu.Log.Error().Err(err).Msg("unable to open image layers file") + + return imageInfo, err + } + + if err := json.Unmarshal(blobBuf, &imageInfo); err != nil { + olu.Log.Error().Err(err).Msg("unable to marshal blob index") + + return imageInfo, err + } + + return imageInfo, err +} + +func GetRoutePrefix(name string) string { + names := strings.SplitN(name, "/", 2) + + if len(names) != 2 { // nolint: gomnd + // it means route is of global storage e.g "centos:latest" + if len(names) == 1 { + return "/" + } + } + + return fmt.Sprintf("/%s", names[0]) +} + +func DirExists(d string) bool { + fi, err := os.Stat(d) + if err != nil && os.IsNotExist(err) { + return false + } + + return fi.IsDir() +} + +func GetImageDirAndTag(imageName string) (string, string) { + var imageDir string + + var imageTag string + + if strings.Contains(imageName, ":") { + splitImageName := strings.Split(imageName, ":") + imageDir = splitImageName[0] + imageTag = splitImageName[1] + } else { + imageDir = imageName + } + + return imageDir, imageTag +} diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index bd42df63..b1bc2ca3 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -1,23 +1,19 @@ package cveinfo import ( - "encoding/json" "fmt" - "io/ioutil" - "os" "path" "sort" "strings" "github.com/anuvu/zot/errors" + "github.com/anuvu/zot/pkg/extensions/search/common" "github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/storage" integration "github.com/aquasecurity/trivy/integration" config "github.com/aquasecurity/trivy/integration/config" "github.com/aquasecurity/trivy/pkg/report" - v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/types" - godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -48,6 +44,7 @@ func ScanImage(config *config.Config) (report.Results, error) { func GetCVEInfo(storeController storage.StoreController, log log.Logger) (*CveInfo, error) { cveController := CveTrivyController{} + layoutUtils := common.NewOciLayoutUtils(storeController, log) subCveConfig := make(map[string]*config.Config) @@ -79,7 +76,8 @@ func GetCVEInfo(storeController storage.StoreController, log log.Logger) (*CveIn cveController.SubCveConfig = subCveConfig - return &CveInfo{Log: log, CveTrivyController: cveController, StoreController: storeController}, nil + return &CveInfo{Log: log, CveTrivyController: cveController, StoreController: storeController, + LayoutUtils: layoutUtils}, nil } func getRoutePrefix(name string) string { @@ -125,15 +123,15 @@ func (cveinfo CveInfo) GetTrivyConfig(image string) *config.Config { } func (cveinfo CveInfo) IsValidImageFormat(imagePath string) (bool, error) { - imageDir, inputTag := getImageDirAndTag(imagePath) + imageDir, inputTag := common.GetImageDirAndTag(imagePath) - if !dirExists(imageDir) { + if !common.DirExists(imageDir) { cveinfo.Log.Error().Msg("image directory doesn't exist") return false, errors.ErrRepoNotFound } - manifests, err := cveinfo.getImageManifests(imageDir) + manifests, err := cveinfo.LayoutUtils.GetImageManifests(imageDir) if err != nil { return false, err @@ -146,7 +144,7 @@ func (cveinfo CveInfo) IsValidImageFormat(imagePath string) (bool, error) { continue } - blobManifest, err := cveinfo.getImageBlobManifest(imageDir, m.Digest) + blobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(imageDir, m.Digest) if err != nil { return false, err } @@ -168,53 +166,6 @@ func (cveinfo CveInfo) IsValidImageFormat(imagePath string) (bool, error) { return false, nil } -func dirExists(d string) bool { - fi, err := os.Stat(d) - if err != nil && os.IsNotExist(err) { - return false - } - - return fi.IsDir() -} - -func getImageDirAndTag(imageName string) (string, string) { - var imageDir string - - var imageTag string - - if strings.Contains(imageName, ":") { - splitImageName := strings.Split(imageName, ":") - imageDir = splitImageName[0] - imageTag = splitImageName[1] - } else { - imageDir = imageName - } - - return imageDir, imageTag -} - -// Below method will return image path including root dir, root dir is determined by splitting. -func (cveinfo CveInfo) GetImageRepoPath(image string) string { - var rootDir string - - prefixName := getRoutePrefix(image) - - subStore := cveinfo.StoreController.SubStore - - if subStore != nil { - imgStore, ok := cveinfo.StoreController.SubStore[prefixName] - if ok { - rootDir = imgStore.RootDir() - } else { - rootDir = cveinfo.StoreController.DefaultStore.RootDir() - } - } else { - rootDir = cveinfo.StoreController.DefaultStore.RootDir() - } - - return path.Join(rootDir, image) -} - func (cveinfo CveInfo) GetImageListForCVE(repo string, id string, imgStore *storage.ImageStore, trivyConfig *config.Config) ([]*string, error) { tags := make([]*string, 0) @@ -266,12 +217,12 @@ func (cveinfo CveInfo) GetImageListForCVE(repo string, id string, imgStore *stor func (cveinfo CveInfo) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) { tagsInfo := make([]TagInfo, 0) - imagePath := cveinfo.GetImageRepoPath(repo) - if !dirExists(imagePath) { + imagePath := cveinfo.LayoutUtils.GetImageRepoPath(repo) + if !common.DirExists(imagePath) { return nil, errors.ErrRepoNotFound } - manifests, err := cveinfo.getImageManifests(imagePath) + manifests, err := cveinfo.LayoutUtils.GetImageManifests(imagePath) if err != nil { cveinfo.Log.Error().Err(err).Msg("unable to read image manifests") @@ -284,7 +235,7 @@ func (cveinfo CveInfo) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) v, ok := manifest.Annotations[ispec.AnnotationRefName] if ok { - imageBlobManifest, err := cveinfo.getImageBlobManifest(imagePath, digest) + imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(imagePath, digest) if err != nil { cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest") @@ -292,7 +243,7 @@ func (cveinfo CveInfo) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) return tagsInfo, err } - imageInfo, err := cveinfo.getImageInfo(imagePath, imageBlobManifest.Config.Digest) + imageInfo, err := cveinfo.LayoutUtils.GetImageInfo(imagePath, imageBlobManifest.Config.Digest) if err != nil { cveinfo.Log.Error().Err(err).Msg("unable to read image info") @@ -331,66 +282,3 @@ func GetFixedTags(allTags []TagInfo, infectedTags []TagInfo) []TagInfo { return fixedTags } - -func (cveinfo CveInfo) getImageManifests(imagePath string) ([]ispec.Descriptor, error) { - buf, err := ioutil.ReadFile(path.Join(imagePath, "index.json")) - - if err != nil { - if os.IsNotExist(err) { - cveinfo.Log.Error().Err(err).Msg("index.json doesn't exist") - - return nil, errors.ErrRepoNotFound - } - - cveinfo.Log.Error().Err(err).Msg("unable to open index.json") - - return nil, errors.ErrRepoNotFound - } - - var index ispec.Index - - if err := json.Unmarshal(buf, &index); err != nil { - cveinfo.Log.Error().Err(err).Str("dir", imagePath).Msg("invalid JSON") - return nil, errors.ErrRepoNotFound - } - - return index.Manifests, nil -} - -func (cveinfo CveInfo) getImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error) { - var blobIndex v1.Manifest - - blobBuf, err := ioutil.ReadFile(path.Join(imageDir, "blobs", digest.Algorithm().String(), digest.Encoded())) - if err != nil { - cveinfo.Log.Error().Err(err).Msg("unable to open image metadata file") - - return blobIndex, err - } - - if err := json.Unmarshal(blobBuf, &blobIndex); err != nil { - cveinfo.Log.Error().Err(err).Msg("unable to marshal blob index") - - return blobIndex, err - } - - return blobIndex, nil -} - -func (cveinfo CveInfo) getImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error) { - var imageInfo ispec.Image - - blobBuf, err := ioutil.ReadFile(path.Join(imageDir, "blobs", hash.Algorithm, hash.Hex)) - if err != nil { - cveinfo.Log.Error().Err(err).Msg("unable to open image layers file") - - return imageInfo, err - } - - if err := json.Unmarshal(blobBuf, &imageInfo); err != nil { - cveinfo.Log.Error().Err(err).Msg("unable to marshal blob index") - - return imageInfo, err - } - - return imageInfo, err -} diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index 8669228e..50f8e4c8 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -14,6 +14,7 @@ import ( "github.com/anuvu/zot/pkg/api" ext "github.com/anuvu/zot/pkg/extensions" + "github.com/anuvu/zot/pkg/extensions/search/common" cveinfo "github.com/anuvu/zot/pkg/extensions/search/cve" "github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/storage" @@ -80,9 +81,10 @@ func testSetup() error { log := log.NewLogger("debug", "") - cve = &cveinfo.CveInfo{Log: log} + storeController := storage.StoreController{DefaultStore: storage.NewImageStore(dir, false, false, log)} + layoutUtils := common.NewOciLayoutUtils(storeController, log) - cve.StoreController = storage.StoreController{DefaultStore: storage.NewImageStore(dir, false, false, log)} + cve = &cveinfo.CveInfo{Log: log, StoreController: storeController, LayoutUtils: layoutUtils} dbDir = dir @@ -418,16 +420,16 @@ func TestMultipleStoragePath(t *testing.T) { So(cveInfo.StoreController.DefaultStore, ShouldNotBeNil) So(cveInfo.StoreController.SubStore, ShouldNotBeNil) - imagePath := cveInfo.GetImageRepoPath("zot-test") + imagePath := cveInfo.LayoutUtils.GetImageRepoPath("zot-test") So(imagePath, ShouldEqual, path.Join(firstRootDir, "zot-test")) - imagePath = cveInfo.GetImageRepoPath("a/zot-a-test") + imagePath = cveInfo.LayoutUtils.GetImageRepoPath("a/zot-a-test") So(imagePath, ShouldEqual, path.Join(secondRootDir, "a/zot-a-test")) - imagePath = cveInfo.GetImageRepoPath("b/zot-b-test") + imagePath = cveInfo.LayoutUtils.GetImageRepoPath("b/zot-b-test") So(imagePath, ShouldEqual, path.Join(thirdRootDir, "b/zot-b-test")) - imagePath = cveInfo.GetImageRepoPath("c/zot-c-test") + imagePath = cveInfo.LayoutUtils.GetImageRepoPath("c/zot-c-test") So(imagePath, ShouldEqual, path.Join(firstRootDir, "c/zot-c-test")) }) } diff --git a/pkg/extensions/search/cve/models.go b/pkg/extensions/search/cve/models.go index e6cdd6d9..cd3733ac 100644 --- a/pkg/extensions/search/cve/models.go +++ b/pkg/extensions/search/cve/models.go @@ -4,6 +4,7 @@ package cveinfo import ( "time" + "github.com/anuvu/zot/pkg/extensions/search/common" "github.com/anuvu/zot/pkg/log" "github.com/anuvu/zot/pkg/storage" config "github.com/aquasecurity/trivy/integration/config" @@ -14,6 +15,7 @@ type CveInfo struct { Log log.Logger CveTrivyController CveTrivyController StoreController storage.StoreController + LayoutUtils *common.OciLayoutUtils } type CveTrivyController struct { diff --git a/pkg/extensions/search/digest/digest.go b/pkg/extensions/search/digest/digest.go new file mode 100644 index 00000000..16ad10d1 --- /dev/null +++ b/pkg/extensions/search/digest/digest.go @@ -0,0 +1,88 @@ +package digestinfo + +import ( + "strings" + + "github.com/anuvu/zot/errors" + "github.com/anuvu/zot/pkg/extensions/search/common" + "github.com/anuvu/zot/pkg/log" + "github.com/anuvu/zot/pkg/storage" + ispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// DigestInfo implements searching by manifes/config/layer digest. +type DigestInfo struct { + Log log.Logger + LayoutUtils *common.OciLayoutUtils +} + +// NewDigestInfo initializes a new DigestInfo object. +func NewDigestInfo(storeController storage.StoreController, log log.Logger) *DigestInfo { + layoutUtils := common.NewOciLayoutUtils(storeController, log) + + return &DigestInfo{Log: log, LayoutUtils: layoutUtils} +} + +// FilterImagesByDigest returns a list of image tags in a repository matching a specific divest. +func (digestinfo DigestInfo) GetImageTagsByDigest(repo string, digest string) ([]*string, error) { + uniqueTags := []*string{} + + imagePath := digestinfo.LayoutUtils.GetImageRepoPath(repo) + if !common.DirExists(imagePath) { + return nil, errors.ErrRepoNotFound + } + + manifests, err := digestinfo.LayoutUtils.GetImageManifests(imagePath) + + if err != nil { + digestinfo.Log.Error().Err(err).Msg("unable to read image manifests") + return uniqueTags, err + } + + for _, manifest := range manifests { + imageDigest := manifest.Digest + + v, ok := manifest.Annotations[ispec.AnnotationRefName] + if ok { + imageBlobManifest, err := digestinfo.LayoutUtils.GetImageBlobManifest(imagePath, imageDigest) + + if err != nil { + digestinfo.Log.Error().Err(err).Msg("unable to read image blob manifest") + return uniqueTags, err + } + + tags := []*string{} + + // Check the image manigest in index.json matches the search digest + // This is a blob with mediaType application/vnd.oci.image.manifest.v1+json + if strings.Contains(manifest.Digest.String(), digest) { + tags = append(tags, &v) + } + + // Check the image config matches the search digest + // This is a blob with mediaType application/vnd.oci.image.config.v1+json + if strings.Contains(imageBlobManifest.Config.Digest.Algorithm+":"+imageBlobManifest.Config.Digest.Hex, digest) { + tags = append(tags, &v) + } + + // Check to see if the individual layers in the oci image manifest match the digest + // These are blobs with mediaType application/vnd.oci.image.layer.v1.tar+gzip + for _, layer := range imageBlobManifest.Layers { + if strings.Contains(layer.Digest.Algorithm+":"+layer.Digest.Hex, digest) { + tags = append(tags, &v) + } + } + + keys := make(map[string]bool) + + for _, entry := range tags { + if _, value := keys[*entry]; !value { + uniqueTags = append(uniqueTags, entry) + keys[*entry] = true + } + } + } + } + + return uniqueTags, nil +} diff --git a/pkg/extensions/search/digest/digest_test.go b/pkg/extensions/search/digest/digest_test.go new file mode 100644 index 00000000..5198efea --- /dev/null +++ b/pkg/extensions/search/digest/digest_test.go @@ -0,0 +1,290 @@ +// nolint: gochecknoinits +package digestinfo_test + +import ( + "context" + "encoding/json" + "io" + "io/ioutil" + "os" + "path" + "testing" + "time" + + "github.com/anuvu/zot/pkg/api" + ext "github.com/anuvu/zot/pkg/extensions" + digestinfo "github.com/anuvu/zot/pkg/extensions/search/digest" + "github.com/anuvu/zot/pkg/log" + "github.com/anuvu/zot/pkg/storage" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/resty.v1" +) + +// nolint:gochecknoglobals +var ( + digestInfo *digestinfo.DigestInfo + rootDir string +) + +const ( + BaseURL1 = "http://127.0.0.1:8085" + Port1 = "8085" +) + +type ImgResponseForDigest struct { + ImgListForDigest ImgListForDigest `json:"data"` + Errors []ErrorGQL `json:"errors"` +} + +type ImgListForDigest struct { + Images []ImgInfo `json:"ImageListForDigest"` +} + +type ImgInfo struct { + Name string `json:"Name"` + Tags []string `json:"Tags"` +} + +type ErrorGQL struct { + Message string `json:"message"` + Path []string `json:"path"` +} + +func init() { + err := testSetup() + if err != nil { + panic(err) + } +} + +func testSetup() error { + dir, err := ioutil.TempDir("", "digest_test") + if err != nil { + return err + } + + rootDir = dir + + // Test images used/copied: + // IMAGE NAME TAG DIGEST CONFIG LAYERS SIZE + // zot-test 0.0.1 2bacca16 adf3bb6c 76MB + // 2d473b07 76MB + // zot-cve-test 0.0.1 63a795ca 8dd57e17 75MB + // 7a0437f0 75MB + + err = copyFiles("../../../../test/data", rootDir) + if err != nil { + return err + } + + log := log.NewLogger("debug", "") + + storeController := storage.StoreController{DefaultStore: storage.NewImageStore(rootDir, false, false, log)} + + digestInfo = digestinfo.NewDigestInfo(storeController, log) + + return nil +} + +func copyFiles(sourceDir string, destDir string) error { + sourceMeta, err := os.Stat(sourceDir) + if err != nil { + return err + } + + if err := os.MkdirAll(destDir, sourceMeta.Mode()); err != nil { + return err + } + + files, err := ioutil.ReadDir(sourceDir) + if err != nil { + return err + } + + for _, file := range files { + sourceFilePath := path.Join(sourceDir, file.Name()) + destFilePath := path.Join(destDir, file.Name()) + + if file.IsDir() { + if err = copyFiles(sourceFilePath, destFilePath); err != nil { + return err + } + } else { + sourceFile, err := os.Open(sourceFilePath) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(destFilePath) + if err != nil { + return err + } + defer destFile.Close() + + if _, err = io.Copy(destFile, sourceFile); err != nil { + return err + } + } + } + + return nil +} + +func TestDigestInfo(t *testing.T) { + Convey("Test image tag", t, func() { + // Search by manifest digest + imageTags, err := digestInfo.GetImageTagsByDigest("zot-cve-test", "63a795ca") + So(err, ShouldBeNil) + So(len(imageTags), ShouldEqual, 1) + So(*imageTags[0], ShouldEqual, "0.0.1") + + // Search by config digest + imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", "adf3bb6c") + So(err, ShouldBeNil) + So(len(imageTags), ShouldEqual, 1) + So(*imageTags[0], ShouldEqual, "0.0.1") + + // Search by layer digest + imageTags, err = digestInfo.GetImageTagsByDigest("zot-cve-test", "7a0437f0") + So(err, ShouldBeNil) + So(len(imageTags), ShouldEqual, 1) + So(*imageTags[0], ShouldEqual, "0.0.1") + + // Search by non-existent image + imageTags, err = digestInfo.GetImageTagsByDigest("zot-tes", "63a795ca") + So(err, ShouldNotBeNil) + So(len(imageTags), ShouldEqual, 0) + + // Search by non-existent digest + imageTags, err = digestInfo.GetImageTagsByDigest("zot-test", "111") + So(err, ShouldBeNil) + So(len(imageTags), ShouldEqual, 0) + }) +} + +func TestDigestSearchHTTP(t *testing.T) { + Convey("Test image search by digest scanning", t, func() { + config := api.NewConfig() + config.HTTP.Port = Port1 + config.Storage.RootDirectory = rootDir + config.Extensions = &ext.ExtensionConfig{ + Search: &ext.SearchConfig{}, + } + + c := api.NewController(config) + + go func() { + // this blocks + if err := c.Run(); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(BaseURL1) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + // shut down server + defer func() { + ctx := context.Background() + _ = c.Server.Shutdown(ctx) + }() + + resp, err := resty.R().Get(BaseURL1 + "/v2/") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + resp, err = resty.R().Get(BaseURL1 + "/query") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + // "sha" should match all digests in all images + resp, err = resty.R().Get(BaseURL1 + "/query?query={ImageListForDigest(id:\"sha\"){Name%20Tags}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + var responseStruct ImgResponseForDigest + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.Errors), ShouldEqual, 0) + So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 2) + So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1) + So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1) + + // Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}} + // "2bacca16" should match the manifest of 1 image + resp, err = resty.R().Get(BaseURL1 + "/query?query={ImageListForDigest(id:\"2bacca16\"){Name%20Tags}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.Errors), ShouldEqual, 0) + So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1) + So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-test") + So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1) + So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1") + + // Call should return {"data":{"ImageListForDigest":[{"Name":"zot-test","Tags":["0.0.1"]}]}} + // "adf3bb6c" should match the config of 1 image + resp, err = resty.R().Get(BaseURL1 + "/query?query={ImageListForDigest(id:\"adf3bb6c\"){Name%20Tags}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.Errors), ShouldEqual, 0) + So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1) + So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-test") + So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1) + So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1") + + // Call should return {"data":{"ImageListForDigest":[{"Name":"zot-cve-test","Tags":["0.0.1"]}]}} + // "7a0437f0" should match the layer of 1 image + resp, err = resty.R().Get(BaseURL1 + "/query?query={ImageListForDigest(id:\"7a0437f0\"){Name%20Tags}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.Errors), ShouldEqual, 0) + So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 1) + So(responseStruct.ImgListForDigest.Images[0].Name, ShouldEqual, "zot-cve-test") + So(len(responseStruct.ImgListForDigest.Images[0].Tags), ShouldEqual, 1) + So(responseStruct.ImgListForDigest.Images[0].Tags[0], ShouldEqual, "0.0.1") + + // Call should return {"data":{"ImageListForDigest":[]}} + // "1111111" should match 0 images + resp, err = resty.R().Get(BaseURL1 + "/query?query={ImageListForDigest(id:\"1111111\"){Name%20Tags}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.Errors), ShouldEqual, 0) + So(len(responseStruct.ImgListForDigest.Images), ShouldEqual, 0) + + // Call should return {"errors": [{....}]", data":null}} + resp, err = resty.R().Get(BaseURL1 + "/query?query={ImageListForDigest(id:\"1111111\"){Name%20Tag343s}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 422) + + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.Errors), ShouldEqual, 1) + }) +} diff --git a/pkg/extensions/search/generated.go b/pkg/extensions/search/generated.go index fae6ebc8..92838ddb 100644 --- a/pkg/extensions/search/generated.go +++ b/pkg/extensions/search/generated.go @@ -62,6 +62,11 @@ type ComplexityRoot struct { Tags func(childComplexity int) int } + ImgResultForDigest struct { + Name func(childComplexity int) int + Tags func(childComplexity int) int + } + ImgResultForFixedCve struct { Tags func(childComplexity int) int } @@ -75,6 +80,7 @@ type ComplexityRoot struct { Query struct { CVEListForImage func(childComplexity int, image string) int ImageListForCve func(childComplexity int, id string) int + ImageListForDigest func(childComplexity int, id string) int ImageListWithCVEFixed func(childComplexity int, id string, image string) int } @@ -88,6 +94,7 @@ type QueryResolver interface { CVEListForImage(ctx context.Context, image string) (*CVEResultForImage, error) ImageListForCve(ctx context.Context, id string) ([]*ImgResultForCve, error) ImageListWithCVEFixed(ctx context.Context, id string, image string) (*ImgResultForFixedCve, error) + ImageListForDigest(ctx context.Context, id string) ([]*ImgResultForDigest, error) } type executableSchema struct { @@ -168,6 +175,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImgResultForCve.Tags(childComplexity), true + case "ImgResultForDigest.Name": + if e.complexity.ImgResultForDigest.Name == nil { + break + } + + return e.complexity.ImgResultForDigest.Name(childComplexity), true + + case "ImgResultForDigest.Tags": + if e.complexity.ImgResultForDigest.Tags == nil { + break + } + + return e.complexity.ImgResultForDigest.Tags(childComplexity), true + case "ImgResultForFixedCVE.Tags": if e.complexity.ImgResultForFixedCve.Tags == nil { break @@ -220,6 +241,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.ImageListForCve(childComplexity, args["id"].(string)), true + case "Query.ImageListForDigest": + if e.complexity.Query.ImageListForDigest == nil { + break + } + + args, err := ec.field_Query_ImageListForDigest_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.ImageListForDigest(childComplexity, args["id"].(string)), true + case "Query.ImageListWithCVEFixed": if e.complexity.Query.ImageListWithCVEFixed == nil { break @@ -326,6 +359,11 @@ type ImgResultForFixedCVE { Tags: [TagInfo] } +type ImgResultForDigest { + Name: String + Tags: [String] +} + type TagInfo { Name: String Timestamp: Time @@ -335,6 +373,7 @@ type Query { CVEListForImage(image: String!) :CVEResultForImage ImageListForCVE(id: String!) :[ImgResultForCVE] ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE + ImageListForDigest(id: String!) :[ImgResultForDigest] }`, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -418,6 +457,21 @@ func (ec *executionContext) field_Query_ImageListForCVE_args(ctx context.Context return args, nil } +func (ec *executionContext) field_Query_ImageListForDigest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 string + if tmp, ok := rawArgs["id"]; ok { + ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithField("id")) + arg0, err = ec.unmarshalNString2string(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_ImageListWithCVEFixed_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -789,6 +843,62 @@ func (ec *executionContext) _ImgResultForCVE_Tags(ctx context.Context, field gra return ec.marshalOString2ᚕᚖstring(ctx, field.Selections, res) } +func (ec *executionContext) _ImgResultForDigest_Name(ctx context.Context, field graphql.CollectedField, obj *ImgResultForDigest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ImgResultForDigest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp := ec._fieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Name, nil + }) + + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _ImgResultForDigest_Tags(ctx context.Context, field graphql.CollectedField, obj *ImgResultForDigest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ImgResultForDigest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp := ec._fieldMiddleware(ctx, obj, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Tags, nil + }) + + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*string) + fc.Result = res + return ec.marshalOString2ᚕᚖstring(ctx, field.Selections, res) +} + func (ec *executionContext) _ImgResultForFixedCVE_Tags(ctx context.Context, field graphql.CollectedField, obj *ImgResultForFixedCve) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1006,6 +1116,41 @@ func (ec *executionContext) _Query_ImageListWithCVEFixed(ctx context.Context, fi return ec.marshalOImgResultForFixedCVE2ᚖgithubᚗcomᚋanuvuᚋzotᚋpkgᚋextensionsᚋsearchᚐImgResultForFixedCve(ctx, field.Selections, res) } +func (ec *executionContext) _Query_ImageListForDigest(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Query", + Field: field, + Args: nil, + IsMethod: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_ImageListForDigest_args(ctx, rawArgs) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + fc.Args = args + resTmp := ec._fieldMiddleware(ctx, nil, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Query().ImageListForDigest(rctx, args["id"].(string)) + }) + + if resTmp == nil { + return graphql.Null + } + res := resTmp.([]*ImgResultForDigest) + fc.Result = res + return ec.marshalOImgResultForDigest2ᚕᚖgithubᚗcomᚋanuvuᚋzotᚋpkgᚋextensionsᚋsearchᚐImgResultForDigest(ctx, field.Selections, res) +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2176,6 +2321,32 @@ func (ec *executionContext) _ImgResultForCVE(ctx context.Context, sel ast.Select return out } +var imgResultForDigestImplementors = []string{"ImgResultForDigest"} + +func (ec *executionContext) _ImgResultForDigest(ctx context.Context, sel ast.SelectionSet, obj *ImgResultForDigest) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, imgResultForDigestImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ImgResultForDigest") + case "Name": + out.Values[i] = ec._ImgResultForDigest_Name(ctx, field, obj) + case "Tags": + out.Values[i] = ec._ImgResultForDigest_Tags(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var imgResultForFixedCVEImplementors = []string{"ImgResultForFixedCVE"} func (ec *executionContext) _ImgResultForFixedCVE(ctx context.Context, sel ast.SelectionSet, obj *ImgResultForFixedCve) graphql.Marshaler { @@ -2276,6 +2447,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr res = ec._Query_ImageListWithCVEFixed(ctx, field) return res }) + case "ImageListForDigest": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_ImageListForDigest(ctx, field) + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": @@ -2946,6 +3128,53 @@ func (ec *executionContext) marshalOImgResultForCVE2ᚖgithubᚗcomᚋanuvuᚋzo return ec._ImgResultForCVE(ctx, sel, v) } +func (ec *executionContext) marshalOImgResultForDigest2ᚕᚖgithubᚗcomᚋanuvuᚋzotᚋpkgᚋextensionsᚋsearchᚐImgResultForDigest(ctx context.Context, sel ast.SelectionSet, v []*ImgResultForDigest) graphql.Marshaler { + if v == nil { + return graphql.Null + } + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalOImgResultForDigest2ᚖgithubᚗcomᚋanuvuᚋzotᚋpkgᚋextensionsᚋsearchᚐImgResultForDigest(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + return ret +} + +func (ec *executionContext) marshalOImgResultForDigest2ᚖgithubᚗcomᚋanuvuᚋzotᚋpkgᚋextensionsᚋsearchᚐImgResultForDigest(ctx context.Context, sel ast.SelectionSet, v *ImgResultForDigest) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ImgResultForDigest(ctx, sel, v) +} + func (ec *executionContext) marshalOImgResultForFixedCVE2ᚖgithubᚗcomᚋanuvuᚋzotᚋpkgᚋextensionsᚋsearchᚐImgResultForFixedCve(ctx context.Context, sel ast.SelectionSet, v *ImgResultForFixedCve) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/extensions/search/models_gen.go b/pkg/extensions/search/models_gen.go index 4dd079a6..54e2fdcd 100644 --- a/pkg/extensions/search/models_gen.go +++ b/pkg/extensions/search/models_gen.go @@ -24,6 +24,11 @@ type ImgResultForCve struct { Tags []*string `json:"Tags"` } +type ImgResultForDigest struct { + Name *string `json:"Name"` + Tags []*string `json:"Tags"` +} + type ImgResultForFixedCve struct { Tags []*TagInfo `json:"Tags"` } diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 0518f459..cf2fc360 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -11,6 +11,7 @@ import ( "github.com/aquasecurity/trivy/integration/config" cveinfo "github.com/anuvu/zot/pkg/extensions/search/cve" + digestinfo "github.com/anuvu/zot/pkg/extensions/search/digest" "github.com/anuvu/zot/pkg/storage" ) // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. @@ -18,6 +19,7 @@ import ( type Resolver struct { cveInfo *cveinfo.CveInfo storeController storage.StoreController + digestInfo *digestinfo.DigestInfo } // Query ... @@ -41,7 +43,8 @@ func GetResolverConfig(log log.Logger, storeController storage.StoreController) panic(err) } - resConfig := &Resolver{cveInfo: cveInfo, storeController: storeController} + digestInfo := digestinfo.NewDigestInfo(storeController, log) + resConfig := &Resolver{cveInfo: cveInfo, storeController: storeController, digestInfo: digestInfo} return Config{Resolvers: resConfig, Directives: DirectiveRoot{}, Complexity: ComplexityRoot{}} @@ -211,7 +214,7 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im r.cveInfo.Log.Info().Str("image", image).Msg("retrieving image path") - imagePath := r.cveInfo.GetImageRepoPath(image) + imagePath := r.cveInfo.LayoutUtils.GetImageRepoPath(image) r.cveInfo.Log.Info().Str("image", image).Msg("retrieving trivy config") @@ -288,6 +291,79 @@ func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, im return imgResultForFixedCVE, nil } +func (r *queryResolver) ImageListForDigest(ctx context.Context, id string) ([]*ImgResultForDigest, error) { + imgResultForDigest := []*ImgResultForDigest{} + + r.digestInfo.Log.Info().Msg("extracting repositories") + + defaultStore := r.storeController.DefaultStore + + repoList, err := defaultStore.GetRepositories() + if err != nil { + r.digestInfo.Log.Error().Err(err).Msg("unable to search repositories") + + return imgResultForDigest, err + } + + r.digestInfo.Log.Info().Msg("scanning each global repository") + + partialImgResultForDigest, err := r.getImageListForDigest(repoList, id) + if err != nil { + r.cveInfo.Log.Error().Err(err).Msg("unable to get image and tag list for global repositories") + + return imgResultForDigest, err + } + + imgResultForDigest = append(imgResultForDigest, partialImgResultForDigest...) + + subStore := r.storeController.SubStore + for _, store := range subStore { + subRepoList, err := store.GetRepositories() + if err != nil { + r.cveInfo.Log.Error().Err(err).Msg("unable to search sub-repositories") + + return imgResultForDigest, err + } + + partialImgResultForDigest, err = r.getImageListForDigest(subRepoList, id) + if err != nil { + r.cveInfo.Log.Error().Err(err).Msg("unable to get image and tag list for sub-repositories") + + return imgResultForDigest, err + } + + imgResultForDigest = append(imgResultForDigest, partialImgResultForDigest...) + } + + return imgResultForDigest, nil +} + +func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*ImgResultForDigest, error) { + imgResultForDigest := []*ImgResultForDigest{} + + var errResult error + + for _, repo := range repoList { + r.digestInfo.Log.Info().Str("repo", repo).Msg("filtering list of tags in image repo by digest") + + tags, err := r.digestInfo.GetImageTagsByDigest(repo, digest) + if err != nil { + r.digestInfo.Log.Error().Err(err).Msg("unable to get filtered list of image tags") + errResult = err + + continue + } + + if len(tags) != 0 { + name := repo + + imgResultForDigest = append(imgResultForDigest, &ImgResultForDigest{Name: &name, Tags: tags}) + } + } + + return imgResultForDigest, errResult +} + func getGraphqlCompatibleTags(fixedTags []cveinfo.TagInfo) []*TagInfo { finalTagList := make([]*TagInfo, 0) diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index 71e1eccc..6dfe3b18 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -28,6 +28,11 @@ type ImgResultForFixedCVE { Tags: [TagInfo] } +type ImgResultForDigest { + Name: String + Tags: [String] +} + type TagInfo { Name: String Timestamp: Time @@ -37,4 +42,5 @@ type Query { CVEListForImage(image: String!) :CVEResultForImage ImageListForCVE(id: String!) :[ImgResultForCVE] ImageListWithCVEFixed(id: String!, image: String!) :ImgResultForFixedCVE + ImageListForDigest(id: String!) :[ImgResultForDigest] } \ No newline at end of file