//go:build search // +build search package cli import ( "context" "errors" "fmt" "io" "net/url" "strconv" "strings" "sync" "github.com/dustin/go-humanize" jsoniter "github.com/json-iterator/go" "github.com/olekukonko/tablewriter" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v2" zotErrors "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api/constants" "zotregistry.io/zot/pkg/common" ) type SearchService interface { //nolint:interfacebloat getImagesGQL(ctx context.Context, config searchConfig, username, password string, imageName string) (*imageListStructGQL, error) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string) (*imageListStructForDigestGQL, error) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, imageName string, searchedCVE string) (*cveResult, error) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password string, digest string) (*imagesForCve, error) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string) (*imagesForCve, error) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string) (*fixedTags, error) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string, derivedImage string) (*imageListStructForDerivedImagesGQL, error) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, baseImage string) (*imageListStructForBaseImagesGQL, error) getAllImages(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup) getCveByImage(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, channel chan stringResult, wtgrp *sync.WaitGroup) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string, channel chan stringResult, wtgrp *sync.WaitGroup) getImagesByDigest(ctx context.Context, config searchConfig, username, password, digest string, channel chan stringResult, wtgrp *sync.WaitGroup) getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cvid string, channel chan stringResult, wtgrp *sync.WaitGroup) getRepos(ctx context.Context, config searchConfig, username, password string, channel chan stringResult, wtgrp *sync.WaitGroup) getImageByName(ctx context.Context, config searchConfig, username, password, imageName string, channel chan stringResult, wtgrp *sync.WaitGroup) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string, channel chan stringResult, wtgrp *sync.WaitGroup) } type searchService struct{} func NewSearchService() SearchService { return searchService{} } func (service searchService) getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string, derivedImage string, ) (*imageListStructForDerivedImagesGQL, error) { query := fmt.Sprintf(` { DerivedImageList(image:"%s"){ Results{ RepoName, Tag, Digest, MediaType, Manifests { Digest, ConfigDigest, Layers {Size Digest}, LastUpdated, IsSigned, Size }, LastUpdated, IsSigned, Size } } }`, derivedImage) result := &imageListStructForDerivedImagesGQL{} 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 (service searchService) getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string, baseImage string, ) (*imageListStructForBaseImagesGQL, error) { query := fmt.Sprintf(` { BaseImageList(image:"%s"){ Results{ RepoName, Tag, Digest, MediaType, Manifests { Digest, ConfigDigest, Layers {Size Digest}, LastUpdated, IsSigned, Size }, LastUpdated, IsSigned, Size } } }`, baseImage) result := &imageListStructForBaseImagesGQL{} 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 (service searchService) getImagesGQL(ctx context.Context, config searchConfig, username, password string, imageName string, ) (*imageListStructGQL, error) { query := fmt.Sprintf(` { ImageList(repo: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size Platform {Os Arch} IsSigned Layers {Size Digest} } Size IsSigned } } }`, imageName) result := &imageListStructGQL{} 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 (service searchService) getImagesByDigestGQL(ctx context.Context, config searchConfig, username, password string, digest string, ) (*imageListStructForDigestGQL, error) { query := fmt.Sprintf(` { ImageListForDigest(id: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size IsSigned } } }`, digest) result := &imageListStructForDigestGQL{} 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 (service searchService) getImagesByCveIDGQL(ctx context.Context, config searchConfig, username, password, cveID string, ) (*imagesForCve, error) { query := fmt.Sprintf(` { ImageListForCVE(id: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size IsSigned } } }`, cveID) result := &imagesForCve{} 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 (service searchService) getCveByImageGQL(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, ) (*cveResult, error) { query := fmt.Sprintf(`{ CVEListForImage (image:"%s", searchedCVE:"%s")`+ ` { Tag CVEList { Id Title Severity Description `+ `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName, searchedCVE) result := &cveResult{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil { return nil, errResult } result.Data.CVEListForImage.CVEList = groupCVEsBySeverity(result.Data.CVEListForImage.CVEList) return result, nil } func (service searchService) getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*imagesForCve, error) { query := fmt.Sprintf(` { ImageListForCVE(id: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size } } }`, cveID) result := &imagesForCve{} 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 (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName, cveID string, ) (*fixedTags, error) { query := fmt.Sprintf(` { ImageListWithCVEFixed(id: "%s", image: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size } } }`, cveID, imageName) result := &fixedTags{} 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 (service searchService) getImageByName(ctx context.Context, config searchConfig, username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) var localWg sync.WaitGroup rlim := newSmoothRateLimiter(&localWg, rch) localWg.Add(1) go rlim.startRateLimiter(ctx) localWg.Add(1) go getImage(ctx, config, username, password, imageName, rch, &localWg, rlim) localWg.Wait() } func (service searchService) getAllImages(ctx context.Context, config searchConfig, username, password string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) catalog := &catalogResponse{} catalogEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix)) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } _, err = makeGETRequest(ctx, catalogEndPoint, username, password, *config.verifyTLS, *config.debug, catalog, config.resultWriter) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } var localWg sync.WaitGroup rlim := newSmoothRateLimiter(&localWg, rch) localWg.Add(1) go rlim.startRateLimiter(ctx) for _, repo := range catalog.Repositories { localWg.Add(1) go getImage(ctx, config, username, password, repo, rch, &localWg, rlim) } localWg.Wait() } func getImage(ctx context.Context, config searchConfig, username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup, pool *requestsPool, ) { defer wtgrp.Done() tagListEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/tags/list", imageName)) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } tagList := &tagListResp{} _, err = makeGETRequest(ctx, tagListEndpoint, username, password, *config.verifyTLS, *config.debug, &tagList, config.resultWriter) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } for _, tag := range tagList.Tags { hasTagPrefix := strings.HasPrefix(tag, "sha256-") hasTagSuffix := strings.HasSuffix(tag, ".sig") // check if it's an image or a signature // we don't want to show signatures in cli responses if hasTagPrefix && hasTagSuffix { continue } wtgrp.Add(1) go addManifestCallToPool(ctx, config, pool, username, password, imageName, tag, rch, wtgrp) } } func (service searchService) getImagesByCveID(ctx context.Context, config searchConfig, username, password, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) query := fmt.Sprintf( `{ ImageListForCVE(id: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size } } }`, cvid) result := &imagesForCve{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } if result.Errors != nil || err != nil { var errBuilder strings.Builder for _, err := range result.Errors { fmt.Fprintln(&errBuilder, err.Message) } if isContextDone(ctx) { return } rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 return } var localWg sync.WaitGroup rlim := newSmoothRateLimiter(&localWg, rch) localWg.Add(1) go rlim.startRateLimiter(ctx) for _, image := range result.Data.Results { localWg.Add(1) go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg) } localWg.Wait() } func (service searchService) getImagesByDigest(ctx context.Context, config searchConfig, username, password string, digest string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) query := fmt.Sprintf( `{ ImageListForDigest(id: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size } } }`, digest) result := &imagesForDigest{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if err != nil { if isContextDone(ctx) { return } rch <- 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 } rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 return } var localWg sync.WaitGroup rlim := newSmoothRateLimiter(&localWg, rch) localWg.Add(1) go rlim.startRateLimiter(ctx) for _, image := range result.Data.Results { localWg.Add(1) go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg) } localWg.Wait() } func (service searchService) getImageByNameAndCVEID(ctx context.Context, config searchConfig, username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) query := fmt.Sprintf( `{ ImageListForCVE(id: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size } } }`, cvid) result := &imagesForCve{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if err != nil { if isContextDone(ctx) { return } rch <- 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 } rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 return } var localWg sync.WaitGroup rlim := newSmoothRateLimiter(&localWg, rch) localWg.Add(1) go rlim.startRateLimiter(ctx) for _, image := range result.Data.Results { if !strings.EqualFold(imageName, image.RepoName) { continue } localWg.Add(1) go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg) } localWg.Wait() } func (service searchService) getCveByImage(ctx context.Context, config searchConfig, username, password, imageName, searchedCVE string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) query := fmt.Sprintf(`{ CVEListForImage (image:"%s", searchedCVE:"%s")`+ ` { Tag CVEList { Id Title Severity Description `+ `PackageList {Name InstalledVersion FixedVersion}} } }`, imageName, searchedCVE) result := &cveResult{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if err != nil { if isContextDone(ctx) { return } rch <- 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 } rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 return } result.Data.CVEListForImage.CVEList = groupCVEsBySeverity(result.Data.CVEListForImage.CVEList) str, err := result.string(*config.outputFormat) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } if isContextDone(ctx) { return } rch <- stringResult{str, nil} } func (service searchService) getFixedTagsForCVE(ctx context.Context, config searchConfig, username, password, imageName, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) query := fmt.Sprintf(` { ImageListWithCVEFixed (id: "%s", image: "%s") { Results { RepoName Tag Digest MediaType Manifests { Digest ConfigDigest Size IsSigned Layers {Size Digest} } Size } } }`, cvid, imageName) result := &fixedTags{} err := service.makeGraphQLQuery(ctx, config, username, password, query, result) if err != nil { if isContextDone(ctx) { return } rch <- 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 } rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113 return } var localWg sync.WaitGroup rlim := newSmoothRateLimiter(&localWg, rch) localWg.Add(1) go rlim.startRateLimiter(ctx) for _, img := range result.Data.Results { localWg.Add(1) go addManifestCallToPool(ctx, config, rlim, username, password, imageName, img.Tag, rch, &localWg) } localWg.Wait() } func groupCVEsBySeverity(cveList []cve) []cve { var ( unknown = make([]cve, 0) none = make([]cve, 0) high = make([]cve, 0) med = make([]cve, 0) low = make([]cve, 0) critical = make([]cve, 0) ) for _, cve := range cveList { switch cve.Severity { case "NONE": none = append(none, cve) case "LOW": low = append(low, cve) case "MEDIUM": med = append(med, cve) case "HIGH": high = append(high, cve) case "CRITICAL": critical = append(critical, cve) default: unknown = append(unknown, cve) } } vulnsCount := len(unknown) + len(none) + len(high) + len(med) + len(low) + len(critical) vulns := make([]cve, 0, vulnsCount) vulns = append(vulns, critical...) vulns = append(vulns, high...) vulns = append(vulns, med...) vulns = append(vulns, low...) vulns = append(vulns, none...) vulns = append(vulns, unknown...) return vulns } func isContextDone(ctx context.Context) bool { select { case <-ctx.Done(): return true default: return false } } // 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(ctx context.Context, config searchConfig, username, password, query string, resultPtr interface{}, ) error { endPoint, err := combineServerAndEndpointURL(*config.servURL, constants.FullSearchPrefix) if err != nil { return err } err = makeGraphQLRequest(ctx, endPoint, query, username, password, *config.verifyTLS, *config.debug, resultPtr, config.resultWriter) if err != nil { return err } return nil } func checkResultGraphQLQuery(ctx context.Context, err error, resultErrors []common.ErrorGraphQL, ) error { if err != nil { if isContextDone(ctx) { return nil //nolint:nilnil } return err } if resultErrors != nil { var errBuilder strings.Builder for _, error := range resultErrors { fmt.Fprintln(&errBuilder, error.Message) } if isContextDone(ctx) { return nil } //nolint: goerr113 return errors.New(errBuilder.String()) } return nil } func addManifestCallToPool(ctx context.Context, config searchConfig, pool *requestsPool, username, password, imageName, tagName string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() manifestEndpoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName)) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} } job := httpJob{ url: manifestEndpoint, username: username, imageName: imageName, password: password, tagName: tagName, config: config, } wtgrp.Add(1) pool.submitJob(&job) } type cveResult struct { Errors []common.ErrorGraphQL `json:"errors"` Data cveData `json:"data"` } type tagListResp struct { Name string `json:"name"` Tags []string `json:"tags"` } //nolint:tagliatelle // graphQL schema type packageList struct { Name string `json:"Name"` InstalledVersion string `json:"InstalledVersion"` FixedVersion string `json:"FixedVersion"` } //nolint:tagliatelle // graphQL schema type cve struct { ID string `json:"Id"` Severity string `json:"Severity"` Title string `json:"Title"` Description string `json:"Description"` PackageList []packageList `json:"PackageList"` } //nolint:tagliatelle // graphQL schema type cveListForImage struct { Tag string `json:"Tag"` CVEList []cve `json:"CVEList"` } //nolint:tagliatelle // graphQL schema type cveData struct { CVEListForImage cveListForImage `json:"CVEListForImage"` } func (cve cveResult) string(format string) (string, error) { switch strings.ToLower(format) { case "", defaultOutoutFormat: return cve.stringPlainText() case "json": return cve.stringJSON() case "yml", "yaml": return cve.stringYAML() default: return "", ErrInvalidOutputFormat } } func (cve cveResult) stringPlainText() (string, error) { var builder strings.Builder table := getCVETableWriter(&builder) for _, c := range cve.Data.CVEListForImage.CVEList { id := ellipsize(c.ID, cveIDWidth, ellipsis) title := ellipsize(c.Title, cveTitleWidth, ellipsis) severity := ellipsize(c.Severity, cveSeverityWidth, ellipsis) row := make([]string, 3) //nolint:gomnd row[colCVEIDIndex] = id row[colCVESeverityIndex] = severity row[colCVETitleIndex] = title table.Append(row) } table.Render() return builder.String(), nil } func (cve cveResult) stringJSON() (string, error) { json := jsoniter.ConfigCompatibleWithStandardLibrary body, err := json.MarshalIndent(cve.Data.CVEListForImage, "", " ") if err != nil { return "", err } return string(body), nil } func (cve cveResult) stringYAML() (string, error) { body, err := yaml.Marshal(&cve.Data.CVEListForImage) if err != nil { return "", err } return string(body), nil } type fixedTags struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema } `json:"data"` } type imagesForCve struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"ImageListForCVE"` //nolint:tagliatelle // graphQL schema } `json:"data"` } type PaginatedImagesResult struct { Results []imageStruct `json:"results"` } type imageStruct struct { RepoName string `json:"repoName"` Tag string `json:"tag"` Manifests []manifestStruct Size string `json:"size"` Digest string `json:"digest"` MediaType string `json:"mediaType"` IsSigned bool `json:"isSigned"` verbose bool } type manifestStruct struct { ConfigDigest string `json:"configDigest"` Digest string `json:"digest"` Layers []layer `json:"layers"` Platform platform `json:"platform"` Size string `json:"size"` IsSigned bool `json:"isSigned"` } type platform struct { Os string `json:"os"` Arch string `json:"arch"` Variant string `json:"variant"` } type DerivedImageList struct { Results []imageStruct `json:"results"` } type BaseImageList struct { Results []imageStruct `json:"results"` } type imageListStructGQL struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"ImageList"` //nolint:tagliatelle } `json:"data"` } type imageListStructForDigestGQL struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"ImageListForDigest"` //nolint:tagliatelle } `json:"data"` } type imageListStructForDerivedImagesGQL struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"DerivedImageList"` //nolint:tagliatelle } `json:"data"` } type imageListStructForBaseImagesGQL struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"BaseImageList"` //nolint:tagliatelle } `json:"data"` } type imagesForDigest struct { Errors []common.ErrorGraphQL `json:"errors"` Data struct { PaginatedImagesResult `json:"ImageListForDigest"` //nolint:tagliatelle // graphQL schema } `json:"data"` } type layer struct { Size int64 `json:"size,string"` Digest string `json:"digest"` } func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int) (string, error) { switch strings.ToLower(format) { case "", defaultOutoutFormat: return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen) case "json": return img.stringJSON() case "yml", "yaml": return img.stringYAML() default: return "", ErrInvalidOutputFormat } } func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen int) (string, error) { var builder strings.Builder table := getImageTableWriter(&builder) table.SetColMinWidth(colImageNameIndex, maxImgNameLen) table.SetColMinWidth(colTagIndex, maxTagLen) table.SetColMinWidth(colPlatformIndex, platformWidth) table.SetColMinWidth(colDigestIndex, digestWidth) table.SetColMinWidth(colSizeIndex, sizeWidth) table.SetColMinWidth(colIsSignedIndex, isSignedWidth) if img.verbose { table.SetColMinWidth(colConfigIndex, configWidth) table.SetColMinWidth(colLayersIndex, layersWidth) } var imageName, tagName string imageName = img.RepoName tagName = img.Tag if imageNameWidth > maxImgNameLen { maxImgNameLen = imageNameWidth } if tagWidth > maxTagLen { maxTagLen = tagWidth } // adding spaces so that image name and tag columns are aligned // in case the name/tag are fully shown and too long var offset string if maxImgNameLen > len(imageName) { offset = strings.Repeat(" ", maxImgNameLen-len(imageName)) imageName += offset } if maxTagLen > len(tagName) { offset = strings.Repeat(" ", maxTagLen-len(tagName)) tagName += offset } err := addImageToTable(table, &img, maxPlatformLen, imageName, tagName) if err != nil { return "", err } table.Render() return builder.String(), nil } func addImageToTable(table *tablewriter.Table, img *imageStruct, maxPlatformLen int, imageName, tagName string, ) error { switch img.MediaType { case ispec.MediaTypeImageManifest: return addManifestToTable(table, imageName, tagName, &img.Manifests[0], maxPlatformLen, img.verbose) case ispec.MediaTypeImageIndex: return addImageIndexToTable(table, img, maxPlatformLen, imageName, tagName) } return nil } func addImageIndexToTable(table *tablewriter.Table, img *imageStruct, maxPlatformLen int, imageName, tagName string, ) error { indexDigest, err := godigest.Parse(img.Digest) if err != nil { return fmt.Errorf("error parsing index digest %s: %w", indexDigest, err) } row := make([]string, rowWidth) row[colImageNameIndex] = imageName row[colTagIndex] = tagName row[colDigestIndex] = ellipsize(indexDigest.Encoded(), digestWidth, "") row[colPlatformIndex] = "*" imgSize, _ := strconv.ParseUint(img.Size, 10, 64) row[colSizeIndex] = ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) row[colIsSignedIndex] = strconv.FormatBool(img.IsSigned) if img.verbose { row[colConfigIndex] = "" row[colLayersIndex] = "" } table.Append(row) for i := range img.Manifests { err := addManifestToTable(table, "", "", &img.Manifests[i], maxPlatformLen, img.verbose) if err != nil { return err } } return nil } func addManifestToTable(table *tablewriter.Table, imageName, tagName string, manifest *manifestStruct, maxPlatformLen int, verbose bool, ) error { manifestDigest, err := godigest.Parse(manifest.Digest) if err != nil { return fmt.Errorf("error parsing manifest digest %s: %w", manifest.Digest, err) } configDigest, err := godigest.Parse(manifest.ConfigDigest) if err != nil { return fmt.Errorf("error parsing config digest %s: %w", manifest.ConfigDigest, err) } platform := getPlatformStr(manifest.Platform) if maxPlatformLen > len(platform) { offset := strings.Repeat(" ", maxPlatformLen-len(platform)) platform += offset } minifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "") configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "") imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64) size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis) isSigned := manifest.IsSigned row := make([]string, 8) //nolint:gomnd row[colImageNameIndex] = imageName row[colTagIndex] = tagName row[colDigestIndex] = minifestDigestStr row[colPlatformIndex] = platform row[colSizeIndex] = size row[colIsSignedIndex] = strconv.FormatBool(isSigned) if verbose { row[colConfigIndex] = configDigestStr row[colLayersIndex] = "" } table.Append(row) if verbose { for _, entry := range manifest.Layers { layerSize := entry.Size size := ellipsize(strings.ReplaceAll(humanize.Bytes(uint64(layerSize)), " ", ""), sizeWidth, ellipsis) layerDigest, err := godigest.Parse(entry.Digest) if err != nil { return fmt.Errorf("error parsing layer digest %s: %w", entry.Digest, err) } layerDigestStr := ellipsize(layerDigest.Encoded(), digestWidth, "") layerRow := make([]string, 8) //nolint:gomnd layerRow[colImageNameIndex] = "" layerRow[colTagIndex] = "" layerRow[colDigestIndex] = "" layerRow[colPlatformIndex] = "" layerRow[colSizeIndex] = size layerRow[colConfigIndex] = "" layerRow[colLayersIndex] = layerDigestStr table.Append(layerRow) } } return nil } func getPlatformStr(platf platform) string { if platf.Arch == "" && platf.Os == "" { return "" } platform := platf.Os if platf.Arch != "" { platform = platform + "/" + platf.Arch platform = strings.Trim(platform, "/") if platf.Variant != "" { platform = platform + "/" + platf.Variant } } return platform } func (img imageStruct) stringJSON() (string, error) { json := jsoniter.ConfigCompatibleWithStandardLibrary body, err := json.MarshalIndent(img, "", " ") if err != nil { return "", err } return string(body), nil } func (img imageStruct) stringYAML() (string, error) { body, err := yaml.Marshal(&img) if err != nil { return "", err } return string(body), nil } type catalogResponse struct { Repositories []string `json:"repositories"` } func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) { if !isURL(serverURL) { return "", zotErrors.ErrInvalidURL } newURL, err := url.Parse(serverURL) if err != nil { return "", zotErrors.ErrInvalidURL } newURL, _ = newURL.Parse(endPoint) return newURL.String(), nil } func ellipsize(text string, max int, trailing string) string { text = strings.TrimSpace(text) if len(text) <= max { return text } chopLength := len(trailing) return text[:max-chopLength] + trailing } func getImageTableWriter(writer io.Writer) *tablewriter.Table { table := tablewriter.NewWriter(writer) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding(" ") table.SetNoWhiteSpace(true) return table } func getCVETableWriter(writer io.Writer) *tablewriter.Table { table := tablewriter.NewWriter(writer) table.SetAutoWrapText(false) table.SetAutoFormatHeaders(true) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetCenterSeparator("") table.SetColumnSeparator("") table.SetRowSeparator("") table.SetHeaderLine(false) table.SetBorder(false) table.SetTablePadding(" ") table.SetNoWhiteSpace(true) table.SetColMinWidth(colCVEIDIndex, cveIDWidth) table.SetColMinWidth(colCVESeverityIndex, cveSeverityWidth) table.SetColMinWidth(colCVETitleIndex, cveTitleWidth) return table } func (service searchService) getRepos(ctx context.Context, config searchConfig, username, password string, rch chan stringResult, wtgrp *sync.WaitGroup, ) { defer wtgrp.Done() defer close(rch) catalog := &catalogResponse{} catalogEndPoint, err := combineServerAndEndpointURL(*config.servURL, fmt.Sprintf("%s%s", constants.RoutePrefix, constants.ExtCatalogPrefix)) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } _, err = makeGETRequest(ctx, catalogEndPoint, username, password, *config.verifyTLS, *config.debug, catalog, config.resultWriter) if err != nil { if isContextDone(ctx) { return } rch <- stringResult{"", err} return } fmt.Fprintln(config.resultWriter, "\n\nREPOSITORY NAME") for _, repo := range catalog.Repositories { fmt.Fprintln(config.resultWriter, repo) } } const ( imageNameWidth = 10 tagWidth = 8 digestWidth = 8 platformWidth = 14 sizeWidth = 8 isSignedWidth = 8 configWidth = 8 layersWidth = 8 ellipsis = "..." cveIDWidth = 16 cveSeverityWidth = 8 cveTitleWidth = 48 colCVEIDIndex = 0 colCVESeverityIndex = 1 colCVETitleIndex = 2 defaultOutoutFormat = "text" ) const ( colImageNameIndex = iota colTagIndex colPlatformIndex colDigestIndex colConfigIndex colIsSignedIndex colLayersIndex colSizeIndex rowWidth )