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