zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/cli/client/service.go (about)

     1  //go:build search
     2  // +build search
     3  
     4  package client
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"net/url"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/dustin/go-humanize"
    17  	jsoniter "github.com/json-iterator/go"
    18  	"github.com/olekukonko/tablewriter"
    19  	godigest "github.com/opencontainers/go-digest"
    20  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    21  	"gopkg.in/yaml.v2"
    22  
    23  	zerr "zotregistry.dev/zot/errors"
    24  	"zotregistry.dev/zot/pkg/api/constants"
    25  	"zotregistry.dev/zot/pkg/common"
    26  )
    27  
    28  const (
    29  	jsonFormat = "json"
    30  	yamlFormat = "yaml"
    31  	ymlFormat  = "yml"
    32  )
    33  
    34  type SearchService interface { //nolint:interfacebloat
    35  	getImagesGQL(ctx context.Context, config SearchConfig, username, password string,
    36  		imageName string) (*common.ImageListResponse, error)
    37  	getImagesForDigestGQL(ctx context.Context, config SearchConfig, username, password string,
    38  		digest string) (*common.ImagesForDigest, error)
    39  	getCveByImageGQL(ctx context.Context, config SearchConfig, username, password,
    40  		imageName string, searchedCVE string) (*cveResult, error)
    41  	getTagsForCVEGQL(ctx context.Context, config SearchConfig, username, password, repo,
    42  		cveID string) (*common.ImagesForCve, error)
    43  	getFixedTagsForCVEGQL(ctx context.Context, config SearchConfig, username, password, imageName,
    44  		cveID string) (*common.ImageListWithCVEFixedResponse, error)
    45  	getDerivedImageListGQL(ctx context.Context, config SearchConfig, username, password string,
    46  		derivedImage string) (*common.DerivedImageListResponse, error)
    47  	getBaseImageListGQL(ctx context.Context, config SearchConfig, username, password string,
    48  		baseImage string) (*common.BaseImageListResponse, error)
    49  	getReferrersGQL(ctx context.Context, config SearchConfig, username, password string,
    50  		repo, digest string) (*common.ReferrersResp, error)
    51  	getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string,
    52  		minuend, subtrahend ImageIdentifier,
    53  	) (*cveDiffListResp, error)
    54  	globalSearchGQL(ctx context.Context, config SearchConfig, username, password string,
    55  		query string) (*common.GlobalSearch, error)
    56  
    57  	getAllImages(ctx context.Context, config SearchConfig, username, password string,
    58  		channel chan stringResult, wtgrp *sync.WaitGroup)
    59  	getImagesByDigest(ctx context.Context, config SearchConfig, username, password, digest string,
    60  		channel chan stringResult, wtgrp *sync.WaitGroup)
    61  	getRepos(ctx context.Context, config SearchConfig, username, password string,
    62  		channel chan stringResult, wtgrp *sync.WaitGroup)
    63  	getImageByName(ctx context.Context, config SearchConfig, username, password, imageName string,
    64  		channel chan stringResult, wtgrp *sync.WaitGroup)
    65  	getReferrers(ctx context.Context, config SearchConfig, username, password string, repo, digest string,
    66  	) (referrersResult, error)
    67  }
    68  
    69  type SearchConfig struct {
    70  	SearchService SearchService
    71  	ServURL       string
    72  	User          string
    73  	OutputFormat  string
    74  	SortBy        string
    75  	VerifyTLS     bool
    76  	FixedFlag     bool
    77  	Verbose       bool
    78  	Debug         bool
    79  	ResultWriter  io.Writer
    80  	Spinner       spinnerState
    81  }
    82  
    83  type searchService struct{}
    84  
    85  func NewSearchService() SearchService {
    86  	return searchService{}
    87  }
    88  
    89  func (service searchService) getDerivedImageListGQL(ctx context.Context, config SearchConfig, username, password string,
    90  	derivedImage string,
    91  ) (*common.DerivedImageListResponse, error) {
    92  	query := fmt.Sprintf(`
    93  		{
    94  			DerivedImageList(image:"%s", requestedPage: {sortBy: %s}){
    95  				Results{
    96  					RepoName Tag
    97  					Digest
    98  					MediaType
    99  					Manifests {
   100  						Digest
   101  						ConfigDigest
   102  						Size
   103  						Platform {Os Arch}
   104  						IsSigned
   105  						Layers {Size Digest}
   106  						LastUpdated
   107  					}
   108  					LastUpdated
   109  					Size
   110  					IsSigned
   111  				}
   112  			}
   113  		}`, derivedImage, Flag2SortCriteria(config.SortBy))
   114  
   115  	result := &common.DerivedImageListResponse{}
   116  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   117  
   118  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   119  		return nil, errResult
   120  	}
   121  
   122  	return result, nil
   123  }
   124  
   125  func (service searchService) getReferrersGQL(ctx context.Context, config SearchConfig, username, password string,
   126  	repo, digest string,
   127  ) (*common.ReferrersResp, error) {
   128  	query := fmt.Sprintf(`
   129  		{
   130  			Referrers( repo: "%s", digest: "%s" ){
   131  				ArtifactType,
   132  				Digest,
   133  				MediaType,
   134  				Size,
   135  				Annotations{
   136  					Key
   137  					Value
   138  				}
   139  			}
   140  		}`, repo, digest)
   141  
   142  	result := &common.ReferrersResp{}
   143  
   144  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   145  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   146  		return nil, errResult
   147  	}
   148  
   149  	return result, nil
   150  }
   151  
   152  func (service searchService) getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string,
   153  	minuend, subtrahend ImageIdentifier,
   154  ) (*cveDiffListResp, error) {
   155  	minuendInput := getImageInput(minuend)
   156  	subtrahendInput := getImageInput(subtrahend)
   157  	query := fmt.Sprintf(`
   158  		{
   159  			CVEDiffListForImages( minuend: %s, subtrahend: %s ) {
   160  				Minuend {Repo Tag}
   161  				Subtrahend {Repo Tag}
   162  				CVEList {
   163  					Id Title Description Severity Reference 
   164  					PackageList {Name InstalledVersion FixedVersion}
   165  				} 
   166  				Summary {
   167  					Count UnknownCount LowCount MediumCount HighCount CriticalCount
   168  				} 
   169  				Page {TotalCount ItemCount}
   170  			}
   171  		}`, minuendInput, subtrahendInput)
   172  
   173  	result := &cveDiffListResp{}
   174  
   175  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   176  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   177  		return nil, errResult
   178  	}
   179  
   180  	return result, nil
   181  }
   182  
   183  func getImageInput(img ImageIdentifier) string {
   184  	platform := ""
   185  	if img.Platform != nil {
   186  		platform = fmt.Sprintf(`, Platform: {Os: "%s", Arch: "%s"}`, img.Platform.Os, img.Platform.Arch)
   187  	}
   188  
   189  	return fmt.Sprintf(`{Repo: "%s", Tag: "%s", Digest: "%s"%s}`, img.Repo, img.Tag, img.Digest, platform)
   190  }
   191  
   192  func (service searchService) globalSearchGQL(ctx context.Context, config SearchConfig, username, password string,
   193  	query string,
   194  ) (*common.GlobalSearch, error) {
   195  	GQLQuery := fmt.Sprintf(`
   196  		{
   197  			GlobalSearch(query:"%s", requestedPage: {sortBy: %s}){
   198  				Images {
   199  					RepoName
   200  					Tag
   201  					MediaType
   202  					Digest
   203  					Size
   204  					IsSigned
   205  					LastUpdated
   206  					Manifests {
   207  						Digest
   208  						ConfigDigest
   209  						Platform {Os Arch}
   210  						Size
   211  						IsSigned
   212  						Layers {Size Digest}
   213  						LastUpdated
   214  					}
   215  				}
   216  				Repos {
   217  					Name
   218  					Platforms { Os Arch }
   219  					LastUpdated
   220  					Size
   221  					DownloadCount
   222  					StarCount
   223  				}
   224  			}
   225  		}`, query, Flag2SortCriteria(config.SortBy))
   226  
   227  	result := &common.GlobalSearchResultResp{}
   228  
   229  	err := service.makeGraphQLQuery(ctx, config, username, password, GQLQuery, result)
   230  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   231  		return nil, errResult
   232  	}
   233  
   234  	return &result.GlobalSearch, nil
   235  }
   236  
   237  func (service searchService) getBaseImageListGQL(ctx context.Context, config SearchConfig, username, password string,
   238  	baseImage string,
   239  ) (*common.BaseImageListResponse, error) {
   240  	query := fmt.Sprintf(`
   241  		{
   242  			BaseImageList(image:"%s", requestedPage: {sortBy: %s}){
   243  				Results{
   244  					RepoName Tag
   245  					Digest
   246  					MediaType
   247  					Manifests {
   248  						Digest
   249  						ConfigDigest
   250  						Size
   251  						Platform {Os Arch}
   252  						IsSigned
   253  						Layers {Size Digest}
   254  						LastUpdated
   255  					}
   256  					LastUpdated
   257  					Size
   258  					IsSigned
   259  				}
   260  			}
   261  		}`, baseImage, Flag2SortCriteria(config.SortBy))
   262  
   263  	result := &common.BaseImageListResponse{}
   264  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   265  
   266  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   267  		return nil, errResult
   268  	}
   269  
   270  	return result, nil
   271  }
   272  
   273  func (service searchService) getImagesGQL(ctx context.Context, config SearchConfig, username, password string,
   274  	imageName string,
   275  ) (*common.ImageListResponse, error) {
   276  	query := fmt.Sprintf(`
   277  	{
   278  		ImageList(repo: "%s", requestedPage: {sortBy: %s}) {
   279  			Results {
   280  				RepoName Tag
   281  				Digest
   282  				MediaType
   283  				Manifests {
   284  					Digest
   285  					ConfigDigest
   286  					Size
   287  					Platform {Os Arch}
   288  					IsSigned
   289  					Layers {Size Digest}
   290  					LastUpdated
   291  				}
   292  				LastUpdated
   293  				Size
   294  				IsSigned
   295  			}
   296  		}
   297  	}`, imageName, Flag2SortCriteria(config.SortBy))
   298  	result := &common.ImageListResponse{}
   299  
   300  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   301  
   302  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   303  		return nil, errResult
   304  	}
   305  
   306  	return result, nil
   307  }
   308  
   309  func (service searchService) getImagesForDigestGQL(ctx context.Context, config SearchConfig, username, password string,
   310  	digest string,
   311  ) (*common.ImagesForDigest, error) {
   312  	query := fmt.Sprintf(`
   313  	{
   314  		ImageListForDigest(id: "%s", requestedPage: {sortBy: %s}) {
   315  			Results {
   316  				RepoName Tag
   317  				Digest
   318  				MediaType
   319  				Manifests {
   320  					Digest
   321  					ConfigDigest
   322  					Size
   323  					Platform {Os Arch}
   324  					IsSigned
   325  					Layers {Size Digest}
   326  					LastUpdated
   327  				}
   328  				LastUpdated
   329  				Size
   330  				IsSigned
   331  			}
   332  		}
   333  	}`, digest, Flag2SortCriteria(config.SortBy))
   334  	result := &common.ImagesForDigest{}
   335  
   336  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   337  
   338  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   339  		return nil, errResult
   340  	}
   341  
   342  	return result, nil
   343  }
   344  
   345  func (service searchService) getCveByImageGQL(ctx context.Context, config SearchConfig, username, password,
   346  	imageName, searchedCVE string,
   347  ) (*cveResult, error) {
   348  	query := fmt.Sprintf(`
   349  	{
   350  		CVEListForImage (image:"%s", searchedCVE:"%s", requestedPage: {sortBy: %s}) {
   351  			Tag
   352  			CVEList {
   353  				Id Title Severity Description
   354  				PackageList {Name PackagePath InstalledVersion FixedVersion}
   355  			}
   356  			Summary {
   357  				Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity
   358  			}
   359  		}
   360  	}`, imageName, searchedCVE, Flag2SortCriteria(config.SortBy))
   361  	result := &cveResult{}
   362  
   363  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   364  
   365  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   366  		return nil, errResult
   367  	}
   368  
   369  	return result, nil
   370  }
   371  
   372  func (service searchService) getTagsForCVEGQL(ctx context.Context, config SearchConfig,
   373  	username, password, repo, cveID string,
   374  ) (*common.ImagesForCve, error) {
   375  	query := fmt.Sprintf(`
   376  		{
   377  			ImageListForCVE(id: "%s", requestedPage: {sortBy: %s}) {
   378  				Results {
   379  					RepoName Tag
   380  					Digest
   381  					MediaType
   382  					Manifests {
   383  						Digest
   384  						ConfigDigest
   385  						Size
   386  						Platform {Os Arch}
   387  						IsSigned
   388  						Layers {Size Digest}
   389  						LastUpdated
   390  					}
   391  					LastUpdated
   392  					Size
   393  					IsSigned
   394  				}
   395  			}
   396  		}`,
   397  		cveID, Flag2SortCriteria(config.SortBy))
   398  	result := &common.ImagesForCve{}
   399  
   400  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   401  
   402  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   403  		return nil, errResult
   404  	}
   405  
   406  	if repo == "" {
   407  		return result, nil
   408  	}
   409  
   410  	filteredResults := &common.ImagesForCve{}
   411  
   412  	for _, image := range result.Results {
   413  		if image.RepoName == repo {
   414  			filteredResults.Results = append(filteredResults.Results, image)
   415  		}
   416  	}
   417  
   418  	return filteredResults, nil
   419  }
   420  
   421  func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config SearchConfig,
   422  	username, password, imageName, cveID string,
   423  ) (*common.ImageListWithCVEFixedResponse, error) {
   424  	query := fmt.Sprintf(`
   425  		{
   426  			ImageListWithCVEFixed(id: "%s", image: "%s") {
   427  				Results {
   428  					RepoName Tag
   429  					Digest
   430  					MediaType
   431  					Manifests {
   432  						Digest
   433  						ConfigDigest
   434  						Size
   435  						Platform {Os Arch}
   436  						IsSigned
   437  						Layers {Size Digest}
   438  						LastUpdated
   439  					}
   440  					LastUpdated
   441  					Size
   442  					IsSigned
   443  				}
   444  			}
   445  		}`,
   446  		cveID, imageName)
   447  
   448  	result := &common.ImageListWithCVEFixedResponse{}
   449  
   450  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   451  
   452  	if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
   453  		return nil, errResult
   454  	}
   455  
   456  	return result, nil
   457  }
   458  
   459  func (service searchService) getReferrers(ctx context.Context, config SearchConfig, username, password string,
   460  	repo, digest string,
   461  ) (referrersResult, error) {
   462  	referrersEndpoint, err := combineServerAndEndpointURL(config.ServURL,
   463  		fmt.Sprintf("/v2/%s/referrers/%s", repo, digest))
   464  	if err != nil {
   465  		if common.IsContextDone(ctx) {
   466  			return referrersResult{}, nil
   467  		}
   468  
   469  		return referrersResult{}, err
   470  	}
   471  
   472  	referrerResp := &ispec.Index{}
   473  	_, err = makeGETRequest(ctx, referrersEndpoint, username, password, config.VerifyTLS,
   474  		config.Debug, &referrerResp, config.ResultWriter)
   475  
   476  	if err != nil {
   477  		if common.IsContextDone(ctx) {
   478  			return referrersResult{}, nil
   479  		}
   480  
   481  		return referrersResult{}, err
   482  	}
   483  
   484  	referrersList := referrersResult{}
   485  
   486  	for _, referrer := range referrerResp.Manifests {
   487  		referrersList = append(referrersList, common.Referrer{
   488  			ArtifactType: referrer.ArtifactType,
   489  			Digest:       referrer.Digest.String(),
   490  			Size:         int(referrer.Size),
   491  		})
   492  	}
   493  
   494  	return referrersList, nil
   495  }
   496  
   497  func (service searchService) getImageByName(ctx context.Context, config SearchConfig,
   498  	username, password, imageName string, rch chan stringResult, wtgrp *sync.WaitGroup,
   499  ) {
   500  	defer wtgrp.Done()
   501  	defer close(rch)
   502  
   503  	var localWg sync.WaitGroup
   504  	rlim := newSmoothRateLimiter(&localWg, rch)
   505  
   506  	localWg.Add(1)
   507  
   508  	go rlim.startRateLimiter(ctx)
   509  	localWg.Add(1)
   510  
   511  	go getImage(ctx, config, username, password, imageName, rch, &localWg, rlim)
   512  
   513  	localWg.Wait()
   514  }
   515  
   516  func (service searchService) getAllImages(ctx context.Context, config SearchConfig, username, password string,
   517  	rch chan stringResult, wtgrp *sync.WaitGroup,
   518  ) {
   519  	defer wtgrp.Done()
   520  	defer close(rch)
   521  
   522  	catalog := &catalogResponse{}
   523  
   524  	catalogEndPoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("%s%s",
   525  		constants.RoutePrefix, constants.ExtCatalogPrefix))
   526  	if err != nil {
   527  		if common.IsContextDone(ctx) {
   528  			return
   529  		}
   530  		rch <- stringResult{"", err}
   531  
   532  		return
   533  	}
   534  
   535  	_, err = makeGETRequest(ctx, catalogEndPoint, username, password, config.VerifyTLS,
   536  		config.Debug, catalog, config.ResultWriter)
   537  	if err != nil {
   538  		if common.IsContextDone(ctx) {
   539  			return
   540  		}
   541  		rch <- stringResult{"", err}
   542  
   543  		return
   544  	}
   545  
   546  	var localWg sync.WaitGroup
   547  
   548  	rlim := newSmoothRateLimiter(&localWg, rch)
   549  
   550  	localWg.Add(1)
   551  
   552  	go rlim.startRateLimiter(ctx)
   553  
   554  	for _, repo := range catalog.Repositories {
   555  		localWg.Add(1)
   556  
   557  		go getImage(ctx, config, username, password, repo, rch, &localWg, rlim)
   558  	}
   559  
   560  	localWg.Wait()
   561  }
   562  
   563  func getImage(ctx context.Context, config SearchConfig, username, password, imageName string,
   564  	rch chan stringResult, wtgrp *sync.WaitGroup, pool *requestsPool,
   565  ) {
   566  	defer wtgrp.Done()
   567  
   568  	repo, imageTag := common.GetImageDirAndTag(imageName)
   569  
   570  	tagListEndpoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("/v2/%s/tags/list", repo))
   571  	if err != nil {
   572  		if common.IsContextDone(ctx) {
   573  			return
   574  		}
   575  		rch <- stringResult{"", err}
   576  
   577  		return
   578  	}
   579  
   580  	tagList := &tagListResp{}
   581  	_, err = makeGETRequest(ctx, tagListEndpoint, username, password, config.VerifyTLS,
   582  		config.Debug, &tagList, config.ResultWriter)
   583  
   584  	if err != nil {
   585  		if common.IsContextDone(ctx) {
   586  			return
   587  		}
   588  		rch <- stringResult{"", err}
   589  
   590  		return
   591  	}
   592  
   593  	for _, tag := range tagList.Tags {
   594  		hasTagPrefix := strings.HasPrefix(tag, "sha256-")
   595  		hasTagSuffix := strings.HasSuffix(tag, ".sig")
   596  
   597  		// check if it's an image or a signature
   598  		// we don't want to show signatures in cli responses
   599  		if hasTagPrefix && hasTagSuffix {
   600  			continue
   601  		}
   602  
   603  		shouldMatchTag := imageTag != ""
   604  		matchesTag := tag == imageTag
   605  
   606  		// when the tag is empty we match everything
   607  		if shouldMatchTag && !matchesTag {
   608  			continue
   609  		}
   610  
   611  		wtgrp.Add(1)
   612  
   613  		go addManifestCallToPool(ctx, config, pool, username, password, repo, tag, rch, wtgrp)
   614  	}
   615  }
   616  
   617  func (service searchService) getImagesByDigest(ctx context.Context, config SearchConfig, username,
   618  	password string, digest string, rch chan stringResult, wtgrp *sync.WaitGroup,
   619  ) {
   620  	defer wtgrp.Done()
   621  	defer close(rch)
   622  
   623  	query := fmt.Sprintf(
   624  		`{
   625  			ImageListForDigest(id: "%s") {
   626  				Results {
   627  					RepoName Tag
   628  					Digest
   629  					MediaType
   630  					Manifests {
   631  						Digest
   632  						ConfigDigest
   633  						Size
   634  						Platform {Os Arch}
   635  						IsSigned
   636  						Layers {Size Digest}
   637  						LastUpdated
   638  					}
   639  					LastUpdated
   640  					Size
   641  					IsSigned
   642  				}
   643  			}
   644  		}`,
   645  		digest)
   646  
   647  	result := &common.ImagesForDigest{}
   648  
   649  	err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
   650  	if err != nil {
   651  		if common.IsContextDone(ctx) {
   652  			return
   653  		}
   654  		rch <- stringResult{"", err}
   655  
   656  		return
   657  	}
   658  
   659  	if result.Errors != nil {
   660  		var errBuilder strings.Builder
   661  
   662  		for _, err := range result.Errors {
   663  			fmt.Fprintln(&errBuilder, err.Message)
   664  		}
   665  
   666  		if common.IsContextDone(ctx) {
   667  			return
   668  		}
   669  		rch <- stringResult{"", errors.New(errBuilder.String())} //nolint: goerr113
   670  
   671  		return
   672  	}
   673  
   674  	var localWg sync.WaitGroup
   675  
   676  	rlim := newSmoothRateLimiter(&localWg, rch)
   677  	localWg.Add(1)
   678  
   679  	go rlim.startRateLimiter(ctx)
   680  
   681  	for _, image := range result.Results {
   682  		localWg.Add(1)
   683  
   684  		go addManifestCallToPool(ctx, config, rlim, username, password, image.RepoName, image.Tag, rch, &localWg)
   685  	}
   686  
   687  	localWg.Wait()
   688  }
   689  
   690  // Query using GQL, the query string is passed as a parameter
   691  // errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr.
   692  func (service searchService) makeGraphQLQuery(ctx context.Context,
   693  	config SearchConfig, username, password, query string,
   694  	resultPtr interface{},
   695  ) error {
   696  	endPoint, err := combineServerAndEndpointURL(config.ServURL, constants.FullSearchPrefix)
   697  	if err != nil {
   698  		return err
   699  	}
   700  
   701  	err = makeGraphQLRequest(ctx, endPoint, query, username, password, config.VerifyTLS,
   702  		config.Debug, resultPtr, config.ResultWriter)
   703  	if err != nil {
   704  		return err
   705  	}
   706  
   707  	return nil
   708  }
   709  
   710  func checkResultGraphQLQuery(ctx context.Context, err error, resultErrors []common.ErrorGQL,
   711  ) error {
   712  	if err != nil {
   713  		if common.IsContextDone(ctx) {
   714  			return nil //nolint:nilnil
   715  		}
   716  
   717  		return err
   718  	}
   719  
   720  	if resultErrors != nil {
   721  		var errBuilder strings.Builder
   722  
   723  		for _, error := range resultErrors {
   724  			fmt.Fprintln(&errBuilder, error.Message)
   725  		}
   726  
   727  		if common.IsContextDone(ctx) {
   728  			return nil
   729  		}
   730  
   731  		//nolint: goerr113
   732  		return errors.New(errBuilder.String())
   733  	}
   734  
   735  	return nil
   736  }
   737  
   738  func addManifestCallToPool(ctx context.Context, config SearchConfig, pool *requestsPool,
   739  	username, password, imageName, tagName string, rch chan stringResult, wtgrp *sync.WaitGroup,
   740  ) {
   741  	defer wtgrp.Done()
   742  
   743  	manifestEndpoint, err := combineServerAndEndpointURL(config.ServURL,
   744  		fmt.Sprintf("/v2/%s/manifests/%s", imageName, tagName))
   745  	if err != nil {
   746  		if common.IsContextDone(ctx) {
   747  			return
   748  		}
   749  		rch <- stringResult{"", err}
   750  	}
   751  
   752  	job := httpJob{
   753  		url:       manifestEndpoint,
   754  		username:  username,
   755  		imageName: imageName,
   756  		password:  password,
   757  		tagName:   tagName,
   758  		config:    config,
   759  	}
   760  
   761  	wtgrp.Add(1)
   762  	pool.submitJob(&job)
   763  }
   764  
   765  type cveResult struct {
   766  	Errors []common.ErrorGQL `json:"errors"`
   767  	Data   cveData           `json:"data"`
   768  }
   769  
   770  type tagListResp struct {
   771  	Name string   `json:"name"`
   772  	Tags []string `json:"tags"`
   773  }
   774  
   775  //nolint:tagliatelle // graphQL schema
   776  type packageList struct {
   777  	Name             string `json:"Name"`
   778  	PackagePath      string `json:"PackagePath"`
   779  	InstalledVersion string `json:"InstalledVersion"`
   780  	FixedVersion     string `json:"FixedVersion"`
   781  }
   782  
   783  //nolint:tagliatelle // graphQL schema
   784  type cve struct {
   785  	ID          string        `json:"Id"`
   786  	Severity    string        `json:"Severity"`
   787  	Title       string        `json:"Title"`
   788  	Description string        `json:"Description"`
   789  	PackageList []packageList `json:"PackageList"`
   790  }
   791  
   792  type cveDiffListResp struct {
   793  	Data   cveDiffResultsForImages `json:"data"`
   794  	Errors []common.ErrorGQL       `json:"errors"`
   795  }
   796  
   797  type cveDiffResultsForImages struct {
   798  	CveDiffResult cveDiffResult `json:"cveDiffListForImages"`
   799  }
   800  
   801  type cveDiffResult struct {
   802  	Minuend    ImageIdentifier                  `json:"minuend"`
   803  	Subtrahend ImageIdentifier                  `json:"subtrahend"`
   804  	CVEList    []cve                            `json:"cveList"`
   805  	Summary    common.ImageVulnerabilitySummary `json:"summary"`
   806  }
   807  
   808  //nolint:tagliatelle // graphQL schema
   809  type cveListForImage struct {
   810  	Tag     string                           `json:"Tag"`
   811  	CVEList []cve                            `json:"CVEList"`
   812  	Summary common.ImageVulnerabilitySummary `json:"Summary"`
   813  }
   814  
   815  //nolint:tagliatelle // graphQL schema
   816  type cveData struct {
   817  	CVEListForImage cveListForImage `json:"cveListForImage"`
   818  }
   819  
   820  func (cve cveResult) string(format string, verbose bool) (string, error) {
   821  	switch strings.ToLower(format) {
   822  	case "", defaultOutputFormat:
   823  		{
   824  			var out string
   825  			if verbose {
   826  				out = cve.stringPlainTextDetailed()
   827  			} else {
   828  				out = cve.stringPlainText()
   829  			}
   830  
   831  			return out, nil
   832  		}
   833  	case jsonFormat:
   834  		return cve.stringJSON()
   835  	case ymlFormat, yamlFormat:
   836  		return cve.stringYAML()
   837  	default:
   838  		return "", zerr.ErrInvalidOutputFormat
   839  	}
   840  }
   841  
   842  func (cve cveResult) stringPlainTextDetailed() string {
   843  	var builder strings.Builder
   844  
   845  	for _, cveListItem := range cve.Data.CVEListForImage.CVEList {
   846  		cveDesc := strings.TrimSpace(cveListItem.Description)
   847  		if len(cveDesc) == 0 {
   848  			cveDesc = "Not Specified"
   849  		}
   850  		cveMetaData := fmt.Sprintf(
   851  			"%s\nSeverity: %s\nTitle: %s\nDescription:\n%s\n\n",
   852  			cveListItem.ID, cveListItem.Severity, cveListItem.Title, cveDesc,
   853  		)
   854  		fmt.Fprint(&builder, cveMetaData)
   855  		fmt.Fprint(&builder, "Vulnerable Packages:\n")
   856  
   857  		for _, pkg := range cveListItem.PackageList {
   858  			pkgMetaData := fmt.Sprintf(
   859  				" Package Name: %s\n Package Path: %s\n Installed Version: %s\n Fixed Version: %s\n\n",
   860  				pkg.Name, pkg.PackagePath, pkg.InstalledVersion, pkg.FixedVersion,
   861  			)
   862  			fmt.Fprint(&builder, pkgMetaData)
   863  		}
   864  
   865  		if len(cveListItem.PackageList) == 0 {
   866  			fmt.Fprintf(&builder, "No Vulnerable Packages\n\n")
   867  		}
   868  
   869  		fmt.Fprint(&builder, "\n")
   870  	}
   871  
   872  	return builder.String()
   873  }
   874  
   875  func (cve cveResult) stringPlainText() string {
   876  	var builder strings.Builder
   877  
   878  	table := getCVETableWriter(&builder)
   879  
   880  	for _, c := range cve.Data.CVEListForImage.CVEList {
   881  		id := ellipsize(c.ID, cveIDWidth, ellipsis)
   882  		title := ellipsize(c.Title, cveTitleWidth, ellipsis)
   883  		severity := ellipsize(c.Severity, cveSeverityWidth, ellipsis)
   884  		row := make([]string, 3) //nolint:gomnd
   885  		row[colCVEIDIndex] = id
   886  		row[colCVESeverityIndex] = severity
   887  		row[colCVETitleIndex] = title
   888  
   889  		table.Append(row)
   890  	}
   891  
   892  	table.Render()
   893  
   894  	return builder.String()
   895  }
   896  
   897  func (cve cveResult) stringJSON() (string, error) {
   898  	// Output is in json lines format - do not indent, append new line after json
   899  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   900  
   901  	body, err := json.Marshal(cve.Data.CVEListForImage)
   902  	if err != nil {
   903  		return "", err
   904  	}
   905  
   906  	return string(body) + "\n", nil
   907  }
   908  
   909  func (cve cveResult) stringYAML() (string, error) {
   910  	// Output will be a multidoc yaml - use triple-dash to indicate a new document
   911  	body, err := yaml.Marshal(&cve.Data.CVEListForImage)
   912  	if err != nil {
   913  		return "", err
   914  	}
   915  
   916  	return "---\n" + string(body), nil
   917  }
   918  
   919  type referrersResult []common.Referrer
   920  
   921  func (ref referrersResult) string(format string, maxArtifactTypeLen int) (string, error) {
   922  	switch strings.ToLower(format) {
   923  	case "", defaultOutputFormat:
   924  		return ref.stringPlainText(maxArtifactTypeLen)
   925  	case jsonFormat:
   926  		return ref.stringJSON()
   927  	case ymlFormat, yamlFormat:
   928  		return ref.stringYAML()
   929  	default:
   930  		return "", zerr.ErrInvalidOutputFormat
   931  	}
   932  }
   933  
   934  func (ref referrersResult) stringPlainText(maxArtifactTypeLen int) (string, error) {
   935  	var builder strings.Builder
   936  
   937  	table := getImageTableWriter(&builder)
   938  
   939  	table.SetColMinWidth(refArtifactTypeIndex, maxArtifactTypeLen)
   940  	table.SetColMinWidth(refDigestIndex, digestWidth)
   941  	table.SetColMinWidth(refSizeIndex, sizeWidth)
   942  
   943  	for _, referrer := range ref {
   944  		artifactType := ellipsize(referrer.ArtifactType, maxArtifactTypeLen, ellipsis)
   945  		// digest := ellipsize(godigest.Digest(referrer.Digest).Encoded(), digestWidth, "")
   946  		size := ellipsize(humanize.Bytes(uint64(referrer.Size)), sizeWidth, ellipsis)
   947  
   948  		row := make([]string, refRowWidth)
   949  		row[refArtifactTypeIndex] = artifactType
   950  		row[refDigestIndex] = referrer.Digest
   951  		row[refSizeIndex] = size
   952  
   953  		table.Append(row)
   954  	}
   955  
   956  	table.Render()
   957  
   958  	return builder.String(), nil
   959  }
   960  
   961  func (ref referrersResult) stringJSON() (string, error) {
   962  	// Output is in json lines format - do not indent, append new line after json
   963  	json := jsoniter.ConfigCompatibleWithStandardLibrary
   964  
   965  	body, err := json.Marshal(ref)
   966  	if err != nil {
   967  		return "", err
   968  	}
   969  
   970  	return string(body) + "\n", nil
   971  }
   972  
   973  func (ref referrersResult) stringYAML() (string, error) {
   974  	// Output will be a multidoc yaml - use triple-dash to indicate a new document
   975  	body, err := yaml.Marshal(ref)
   976  	if err != nil {
   977  		return "", err
   978  	}
   979  
   980  	return "---\n" + string(body), nil
   981  }
   982  
   983  type repoStruct common.RepoSummary
   984  
   985  func (repo repoStruct) string(format string, maxImgNameLen, maxTimeLen int, verbose bool) (string, error) { //nolint: lll
   986  	switch strings.ToLower(format) {
   987  	case "", defaultOutputFormat:
   988  		return repo.stringPlainText(maxImgNameLen, maxTimeLen, verbose)
   989  	case jsonFormat:
   990  		return repo.stringJSON()
   991  	case ymlFormat, yamlFormat:
   992  		return repo.stringYAML()
   993  	default:
   994  		return "", zerr.ErrInvalidOutputFormat
   995  	}
   996  }
   997  
   998  func (repo repoStruct) stringPlainText(repoMaxLen, maxTimeLen int, verbose bool) (string, error) {
   999  	var builder strings.Builder
  1000  
  1001  	table := getImageTableWriter(&builder)
  1002  
  1003  	table.SetColMinWidth(repoNameIndex, repoMaxLen)
  1004  	table.SetColMinWidth(repoSizeIndex, sizeWidth)
  1005  	table.SetColMinWidth(repoLastUpdatedIndex, maxTimeLen)
  1006  	table.SetColMinWidth(repoDownloadsIndex, downloadsWidth)
  1007  	table.SetColMinWidth(repoStarsIndex, signedWidth)
  1008  
  1009  	if verbose {
  1010  		table.SetColMinWidth(repoPlatformsIndex, platformWidth)
  1011  	}
  1012  
  1013  	repoSize, err := strconv.Atoi(repo.Size)
  1014  	if err != nil {
  1015  		return "", err
  1016  	}
  1017  
  1018  	repoName := repo.Name
  1019  	repoLastUpdated := repo.LastUpdated
  1020  	repoDownloads := repo.DownloadCount
  1021  	repoStars := repo.StarCount
  1022  	repoPlatforms := repo.Platforms
  1023  
  1024  	row := make([]string, repoRowWidth)
  1025  	row[repoNameIndex] = repoName
  1026  	row[repoSizeIndex] = ellipsize(strings.ReplaceAll(humanize.Bytes(uint64(repoSize)), " ", ""), sizeWidth, ellipsis)
  1027  	row[repoLastUpdatedIndex] = repoLastUpdated.String()
  1028  	row[repoDownloadsIndex] = strconv.Itoa(repoDownloads)
  1029  	row[repoStarsIndex] = strconv.Itoa(repoStars)
  1030  
  1031  	if verbose && len(repoPlatforms) > 0 {
  1032  		row[repoPlatformsIndex] = getPlatformStr(repoPlatforms[0])
  1033  		repoPlatforms = repoPlatforms[1:]
  1034  	}
  1035  
  1036  	table.Append(row)
  1037  
  1038  	if verbose {
  1039  		for _, platform := range repoPlatforms {
  1040  			row := make([]string, repoRowWidth)
  1041  
  1042  			row[repoPlatformsIndex] = getPlatformStr(platform)
  1043  
  1044  			table.Append(row)
  1045  		}
  1046  	}
  1047  
  1048  	table.Render()
  1049  
  1050  	return builder.String(), nil
  1051  }
  1052  
  1053  func (repo repoStruct) stringJSON() (string, error) {
  1054  	// Output is in json lines format - do not indent, append new line after json
  1055  	json := jsoniter.ConfigCompatibleWithStandardLibrary
  1056  
  1057  	body, err := json.Marshal(repo)
  1058  	if err != nil {
  1059  		return "", err
  1060  	}
  1061  
  1062  	return string(body) + "\n", nil
  1063  }
  1064  
  1065  func (repo repoStruct) stringYAML() (string, error) {
  1066  	// Output will be a multidoc yaml - use triple-dash to indicate a new document
  1067  	body, err := yaml.Marshal(&repo)
  1068  	if err != nil {
  1069  		return "", err
  1070  	}
  1071  
  1072  	return "---\n" + string(body), nil
  1073  }
  1074  
  1075  type imageStruct common.ImageSummary
  1076  
  1077  func (img imageStruct) string(format string, maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) { //nolint: lll
  1078  	switch strings.ToLower(format) {
  1079  	case "", defaultOutputFormat:
  1080  		return img.stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen, verbose)
  1081  	case jsonFormat:
  1082  		return img.stringJSON()
  1083  	case ymlFormat, yamlFormat:
  1084  		return img.stringYAML()
  1085  	default:
  1086  		return "", zerr.ErrInvalidOutputFormat
  1087  	}
  1088  }
  1089  
  1090  func (img imageStruct) stringPlainText(maxImgNameLen, maxTagLen, maxPlatformLen int, verbose bool) (string, error) {
  1091  	var builder strings.Builder
  1092  
  1093  	table := getImageTableWriter(&builder)
  1094  
  1095  	table.SetColMinWidth(colImageNameIndex, maxImgNameLen)
  1096  	table.SetColMinWidth(colTagIndex, maxTagLen)
  1097  	table.SetColMinWidth(colPlatformIndex, platformWidth)
  1098  	table.SetColMinWidth(colDigestIndex, digestWidth)
  1099  	table.SetColMinWidth(colSizeIndex, sizeWidth)
  1100  	table.SetColMinWidth(colIsSignedIndex, isSignedWidth)
  1101  
  1102  	if verbose {
  1103  		table.SetColMinWidth(colConfigIndex, configWidth)
  1104  		table.SetColMinWidth(colLayersIndex, layersWidth)
  1105  	}
  1106  
  1107  	var imageName, tagName string
  1108  
  1109  	imageName = img.RepoName
  1110  	tagName = img.Tag
  1111  
  1112  	if imageNameWidth > maxImgNameLen {
  1113  		maxImgNameLen = imageNameWidth
  1114  	}
  1115  
  1116  	if tagWidth > maxTagLen {
  1117  		maxTagLen = tagWidth
  1118  	}
  1119  
  1120  	// adding spaces so that image name and tag columns are aligned
  1121  	// in case the name/tag are fully shown and too long
  1122  	var offset string
  1123  	if maxImgNameLen > len(imageName) {
  1124  		offset = strings.Repeat(" ", maxImgNameLen-len(imageName))
  1125  		imageName += offset
  1126  	}
  1127  
  1128  	if maxTagLen > len(tagName) {
  1129  		offset = strings.Repeat(" ", maxTagLen-len(tagName))
  1130  		tagName += offset
  1131  	}
  1132  
  1133  	err := addImageToTable(table, &img, maxPlatformLen, imageName, tagName, verbose)
  1134  	if err != nil {
  1135  		return "", err
  1136  	}
  1137  
  1138  	table.Render()
  1139  
  1140  	return builder.String(), nil
  1141  }
  1142  
  1143  func addImageToTable(table *tablewriter.Table, img *imageStruct, maxPlatformLen int,
  1144  	imageName, tagName string, verbose bool,
  1145  ) error {
  1146  	switch img.MediaType {
  1147  	case ispec.MediaTypeImageManifest:
  1148  		return addManifestToTable(table, imageName, tagName, &img.Manifests[0], maxPlatformLen, verbose)
  1149  	case ispec.MediaTypeImageIndex:
  1150  		return addImageIndexToTable(table, img, maxPlatformLen, imageName, tagName, verbose)
  1151  	}
  1152  
  1153  	return nil
  1154  }
  1155  
  1156  func addImageIndexToTable(table *tablewriter.Table, img *imageStruct, maxPlatformLen int,
  1157  	imageName, tagName string, verbose bool,
  1158  ) error {
  1159  	indexDigest, err := godigest.Parse(img.Digest)
  1160  	if err != nil {
  1161  		return fmt.Errorf("error parsing index digest %s: %w", indexDigest, err)
  1162  	}
  1163  	row := make([]string, rowWidth)
  1164  	row[colImageNameIndex] = imageName
  1165  	row[colTagIndex] = tagName
  1166  	row[colDigestIndex] = ellipsize(indexDigest.Encoded(), digestWidth, "")
  1167  	row[colPlatformIndex] = "*"
  1168  
  1169  	imgSize, _ := strconv.ParseUint(img.Size, 10, 64)
  1170  	row[colSizeIndex] = ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis)
  1171  	row[colIsSignedIndex] = strconv.FormatBool(img.IsSigned)
  1172  
  1173  	if verbose {
  1174  		row[colConfigIndex] = ""
  1175  		row[colLayersIndex] = ""
  1176  	}
  1177  
  1178  	table.Append(row)
  1179  
  1180  	for i := range img.Manifests {
  1181  		err := addManifestToTable(table, "", "", &img.Manifests[i], maxPlatformLen, verbose)
  1182  		if err != nil {
  1183  			return err
  1184  		}
  1185  	}
  1186  
  1187  	return nil
  1188  }
  1189  
  1190  func addManifestToTable(table *tablewriter.Table, imageName, tagName string, manifest *common.ManifestSummary,
  1191  	maxPlatformLen int, verbose bool,
  1192  ) error {
  1193  	manifestDigest, err := godigest.Parse(manifest.Digest)
  1194  	if err != nil {
  1195  		return fmt.Errorf("error parsing manifest digest %s: %w", manifest.Digest, err)
  1196  	}
  1197  
  1198  	configDigest, err := godigest.Parse(manifest.ConfigDigest)
  1199  	if err != nil {
  1200  		return fmt.Errorf("error parsing config digest %s: %w", manifest.ConfigDigest, err)
  1201  	}
  1202  
  1203  	platform := getPlatformStr(manifest.Platform)
  1204  
  1205  	if maxPlatformLen > len(platform) {
  1206  		offset := strings.Repeat(" ", maxPlatformLen-len(platform))
  1207  		platform += offset
  1208  	}
  1209  
  1210  	manifestDigestStr := ellipsize(manifestDigest.Encoded(), digestWidth, "")
  1211  	configDigestStr := ellipsize(configDigest.Encoded(), configWidth, "")
  1212  	imgSize, _ := strconv.ParseUint(manifest.Size, 10, 64)
  1213  	size := ellipsize(strings.ReplaceAll(humanize.Bytes(imgSize), " ", ""), sizeWidth, ellipsis)
  1214  	isSigned := manifest.IsSigned
  1215  	row := make([]string, 8) //nolint:gomnd
  1216  
  1217  	row[colImageNameIndex] = imageName
  1218  	row[colTagIndex] = tagName
  1219  	row[colDigestIndex] = manifestDigestStr
  1220  	row[colPlatformIndex] = platform
  1221  	row[colSizeIndex] = size
  1222  	row[colIsSignedIndex] = strconv.FormatBool(isSigned)
  1223  
  1224  	if verbose {
  1225  		row[colConfigIndex] = configDigestStr
  1226  		row[colLayersIndex] = ""
  1227  	}
  1228  
  1229  	table.Append(row)
  1230  
  1231  	if verbose {
  1232  		for _, entry := range manifest.Layers {
  1233  			layerSize, _ := strconv.ParseUint(entry.Size, 10, 64)
  1234  			size := ellipsize(strings.ReplaceAll(humanize.Bytes(layerSize), " ", ""), sizeWidth, ellipsis)
  1235  
  1236  			layerDigest, err := godigest.Parse(entry.Digest)
  1237  			if err != nil {
  1238  				return fmt.Errorf("error parsing layer digest %s: %w", entry.Digest, err)
  1239  			}
  1240  
  1241  			layerDigestStr := ellipsize(layerDigest.Encoded(), digestWidth, "")
  1242  
  1243  			layerRow := make([]string, 8) //nolint:gomnd
  1244  			layerRow[colImageNameIndex] = ""
  1245  			layerRow[colTagIndex] = ""
  1246  			layerRow[colDigestIndex] = ""
  1247  			layerRow[colPlatformIndex] = ""
  1248  			layerRow[colSizeIndex] = size
  1249  			layerRow[colConfigIndex] = ""
  1250  			layerRow[colLayersIndex] = layerDigestStr
  1251  
  1252  			table.Append(layerRow)
  1253  		}
  1254  	}
  1255  
  1256  	return nil
  1257  }
  1258  
  1259  func getPlatformStr(platform common.Platform) string {
  1260  	if platform.Arch == "" && platform.Os == "" {
  1261  		return ""
  1262  	}
  1263  
  1264  	fullPlatform := platform.Os
  1265  
  1266  	if platform.Arch != "" {
  1267  		fullPlatform = fullPlatform + "/" + platform.Arch
  1268  		fullPlatform = strings.Trim(fullPlatform, "/")
  1269  
  1270  		if platform.Variant != "" {
  1271  			fullPlatform = fullPlatform + "/" + platform.Variant
  1272  		}
  1273  	}
  1274  
  1275  	return fullPlatform
  1276  }
  1277  
  1278  func (img imageStruct) stringJSON() (string, error) {
  1279  	// Output is in json lines format - do not indent, append new line after json
  1280  	json := jsoniter.ConfigCompatibleWithStandardLibrary
  1281  
  1282  	body, err := json.Marshal(img)
  1283  	if err != nil {
  1284  		return "", err
  1285  	}
  1286  
  1287  	return string(body) + "\n", nil
  1288  }
  1289  
  1290  func (img imageStruct) stringYAML() (string, error) {
  1291  	// Output will be a multidoc yaml - use triple-dash to indicate a new document
  1292  	body, err := yaml.Marshal(&img)
  1293  	if err != nil {
  1294  		return "", err
  1295  	}
  1296  
  1297  	return "---\n" + string(body), nil
  1298  }
  1299  
  1300  type catalogResponse struct {
  1301  	Repositories []string `json:"repositories"`
  1302  }
  1303  
  1304  func combineServerAndEndpointURL(serverURL, endPoint string) (string, error) {
  1305  	if err := validateURL(serverURL); err != nil {
  1306  		return "", err
  1307  	}
  1308  
  1309  	newURL, err := url.Parse(serverURL)
  1310  	if err != nil {
  1311  		return "", zerr.ErrInvalidURL
  1312  	}
  1313  
  1314  	newURL, _ = newURL.Parse(endPoint)
  1315  
  1316  	return newURL.String(), nil
  1317  }
  1318  
  1319  func ellipsize(text string, max int, trailing string) string {
  1320  	text = strings.TrimSpace(text)
  1321  	if len(text) <= max {
  1322  		return text
  1323  	}
  1324  
  1325  	chopLength := len(trailing)
  1326  
  1327  	return text[:max-chopLength] + trailing
  1328  }
  1329  
  1330  func getImageTableWriter(writer io.Writer) *tablewriter.Table {
  1331  	table := tablewriter.NewWriter(writer)
  1332  
  1333  	table.SetAutoWrapText(false)
  1334  	table.SetAutoFormatHeaders(true)
  1335  	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
  1336  	table.SetAlignment(tablewriter.ALIGN_LEFT)
  1337  	table.SetCenterSeparator("")
  1338  	table.SetColumnSeparator("")
  1339  	table.SetRowSeparator("")
  1340  	table.SetHeaderLine(false)
  1341  	table.SetBorder(false)
  1342  	table.SetTablePadding("  ")
  1343  	table.SetNoWhiteSpace(true)
  1344  
  1345  	return table
  1346  }
  1347  
  1348  func getCVETableWriter(writer io.Writer) *tablewriter.Table {
  1349  	table := tablewriter.NewWriter(writer)
  1350  
  1351  	table.SetAutoWrapText(false)
  1352  	table.SetAutoFormatHeaders(true)
  1353  	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
  1354  	table.SetAlignment(tablewriter.ALIGN_LEFT)
  1355  	table.SetCenterSeparator("")
  1356  	table.SetColumnSeparator("")
  1357  	table.SetRowSeparator("")
  1358  	table.SetHeaderLine(false)
  1359  	table.SetBorder(false)
  1360  	table.SetTablePadding("  ")
  1361  	table.SetNoWhiteSpace(true)
  1362  	table.SetColMinWidth(colCVEIDIndex, cveIDWidth)
  1363  	table.SetColMinWidth(colCVESeverityIndex, cveSeverityWidth)
  1364  	table.SetColMinWidth(colCVETitleIndex, cveTitleWidth)
  1365  
  1366  	return table
  1367  }
  1368  
  1369  func getReferrersTableWriter(writer io.Writer) *tablewriter.Table {
  1370  	table := tablewriter.NewWriter(writer)
  1371  
  1372  	table.SetAutoWrapText(false)
  1373  	table.SetAutoFormatHeaders(true)
  1374  	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
  1375  	table.SetAlignment(tablewriter.ALIGN_LEFT)
  1376  	table.SetCenterSeparator("")
  1377  	table.SetColumnSeparator("")
  1378  	table.SetRowSeparator("")
  1379  	table.SetHeaderLine(false)
  1380  	table.SetBorder(false)
  1381  	table.SetTablePadding("  ")
  1382  	table.SetNoWhiteSpace(true)
  1383  
  1384  	return table
  1385  }
  1386  
  1387  func getRepoTableWriter(writer io.Writer) *tablewriter.Table {
  1388  	table := tablewriter.NewWriter(writer)
  1389  
  1390  	table.SetAutoWrapText(false)
  1391  	table.SetAutoFormatHeaders(true)
  1392  	table.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
  1393  	table.SetAlignment(tablewriter.ALIGN_LEFT)
  1394  	table.SetCenterSeparator("")
  1395  	table.SetColumnSeparator("")
  1396  	table.SetRowSeparator("")
  1397  	table.SetHeaderLine(false)
  1398  	table.SetBorder(false)
  1399  	table.SetTablePadding("  ")
  1400  	table.SetNoWhiteSpace(true)
  1401  
  1402  	return table
  1403  }
  1404  
  1405  func (service searchService) getRepos(ctx context.Context, config SearchConfig, username, password string,
  1406  	rch chan stringResult, wtgrp *sync.WaitGroup,
  1407  ) {
  1408  	defer wtgrp.Done()
  1409  	defer close(rch)
  1410  
  1411  	catalog := &catalogResponse{}
  1412  
  1413  	catalogEndPoint, err := combineServerAndEndpointURL(config.ServURL, fmt.Sprintf("%s%s",
  1414  		constants.RoutePrefix, constants.ExtCatalogPrefix))
  1415  	if err != nil {
  1416  		if common.IsContextDone(ctx) {
  1417  			return
  1418  		}
  1419  		rch <- stringResult{"", err}
  1420  
  1421  		return
  1422  	}
  1423  
  1424  	_, err = makeGETRequest(ctx, catalogEndPoint, username, password, config.VerifyTLS,
  1425  		config.Debug, catalog, config.ResultWriter)
  1426  	if err != nil {
  1427  		if common.IsContextDone(ctx) {
  1428  			return
  1429  		}
  1430  		rch <- stringResult{"", err}
  1431  
  1432  		return
  1433  	}
  1434  
  1435  	fmt.Fprintln(config.ResultWriter, "\nREPOSITORY NAME")
  1436  
  1437  	if config.SortBy == SortByAlphabeticAsc {
  1438  		for i := 0; i < len(catalog.Repositories); i++ {
  1439  			fmt.Fprintln(config.ResultWriter, catalog.Repositories[i])
  1440  		}
  1441  	} else {
  1442  		for i := len(catalog.Repositories) - 1; i >= 0; i-- {
  1443  			fmt.Fprintln(config.ResultWriter, catalog.Repositories[i])
  1444  		}
  1445  	}
  1446  }
  1447  
  1448  const (
  1449  	imageNameWidth   = 10
  1450  	tagWidth         = 8
  1451  	digestWidth      = 8
  1452  	platformWidth    = 14
  1453  	sizeWidth        = 10
  1454  	isSignedWidth    = 8
  1455  	downloadsWidth   = 10
  1456  	signedWidth      = 10
  1457  	lastUpdatedWidth = 14
  1458  	configWidth      = 8
  1459  	layersWidth      = 8
  1460  	ellipsis         = "..."
  1461  
  1462  	cveIDWidth       = 16
  1463  	cveSeverityWidth = 8
  1464  	cveTitleWidth    = 48
  1465  
  1466  	colCVEIDIndex       = 0
  1467  	colCVESeverityIndex = 1
  1468  	colCVETitleIndex    = 2
  1469  
  1470  	defaultOutputFormat = "text"
  1471  )
  1472  
  1473  const (
  1474  	colImageNameIndex = iota
  1475  	colTagIndex
  1476  	colPlatformIndex
  1477  	colDigestIndex
  1478  	colConfigIndex
  1479  	colIsSignedIndex
  1480  	colLayersIndex
  1481  	colSizeIndex
  1482  
  1483  	rowWidth
  1484  )
  1485  
  1486  const (
  1487  	repoNameIndex = iota
  1488  	repoSizeIndex
  1489  	repoLastUpdatedIndex
  1490  	repoDownloadsIndex
  1491  	repoStarsIndex
  1492  	repoPlatformsIndex
  1493  
  1494  	repoRowWidth
  1495  )
  1496  
  1497  const (
  1498  	refArtifactTypeIndex = iota
  1499  	refSizeIndex
  1500  	refDigestIndex
  1501  
  1502  	refRowWidth
  1503  )