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 )