zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/search/resolver.go (about) 1 package search 2 3 // This file will not be regenerated automatically. 4 // 5 // It serves as dependency injection for your app, add any dependencies you require here. 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "sort" 12 "strings" 13 14 godigest "github.com/opencontainers/go-digest" 15 ispec "github.com/opencontainers/image-spec/specs-go/v1" 16 "github.com/vektah/gqlparser/v2/gqlerror" 17 18 zerr "zotregistry.io/zot/errors" 19 zcommon "zotregistry.io/zot/pkg/common" 20 "zotregistry.io/zot/pkg/extensions/search/convert" 21 cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" 22 cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" 23 "zotregistry.io/zot/pkg/extensions/search/gql_generated" 24 "zotregistry.io/zot/pkg/extensions/search/pagination" 25 "zotregistry.io/zot/pkg/log" 26 mTypes "zotregistry.io/zot/pkg/meta/types" 27 reqCtx "zotregistry.io/zot/pkg/requestcontext" 28 "zotregistry.io/zot/pkg/storage" 29 ) 30 31 // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES. 32 33 const ( 34 querySizeLimit = 256 35 ) 36 37 // Resolver ... 38 type Resolver struct { 39 cveInfo cveinfo.CveInfo 40 metaDB mTypes.MetaDB 41 storeController storage.StoreController 42 log log.Logger 43 } 44 45 // GetResolverConfig ... 46 func GetResolverConfig(log log.Logger, storeController storage.StoreController, 47 metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, 48 ) gql_generated.Config { 49 resConfig := &Resolver{ 50 cveInfo: cveInfo, 51 metaDB: metaDB, 52 storeController: storeController, 53 log: log, 54 } 55 56 return gql_generated.Config{ 57 Resolvers: resConfig, Directives: gql_generated.DirectiveRoot{}, 58 Complexity: gql_generated.ComplexityRoot{}, 59 } 60 } 61 62 func NewResolver(log log.Logger, storeController storage.StoreController, 63 metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, 64 ) *Resolver { 65 resolver := &Resolver{ 66 cveInfo: cveInfo, 67 metaDB: metaDB, 68 storeController: storeController, 69 log: log, 70 } 71 72 return resolver 73 } 74 75 func FilterByDigest(digest string) mTypes.FilterFunc { 76 // imageMeta will always contain 1 manifest 77 return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { 78 lookupDigest := digest 79 contains := false 80 81 manifest := imageMeta.Manifests[0] 82 83 manifestDigest := manifest.Digest.String() 84 85 // Check the image manifest in index.json matches the search digest 86 // This is a blob with mediaType application/vnd.oci.image.manifest.v1+json 87 if strings.Contains(manifestDigest, lookupDigest) { 88 contains = true 89 } 90 91 // Check the image config matches the search digest 92 // This is a blob with mediaType application/vnd.oci.image.config.v1+json 93 if strings.Contains(manifest.Manifest.Config.Digest.String(), lookupDigest) { 94 contains = true 95 } 96 97 // Check to see if the individual layers in the oci image manifest match the digest 98 // These are blobs with mediaType application/vnd.oci.image.layer.v1.tar+gzip 99 for _, layer := range manifest.Manifest.Layers { 100 if strings.Contains(layer.Digest.String(), lookupDigest) { 101 contains = true 102 } 103 } 104 105 return contains 106 } 107 } 108 109 func getImageListForDigest(ctx context.Context, digest string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, 110 requestedPage *gql_generated.PageInput, 111 ) (*gql_generated.PaginatedImagesResult, error) { 112 if requestedPage == nil { 113 requestedPage = &gql_generated.PageInput{} 114 } 115 116 skip := convert.SkipQGLField{ 117 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"), 118 } 119 120 pageInput := pagination.PageInput{ 121 Limit: deref(requestedPage.Limit, 0), 122 Offset: deref(requestedPage.Offset, 0), 123 SortBy: pagination.SortCriteria( 124 deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), 125 ), 126 } 127 128 fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByDigest(digest)) 129 if err != nil { 130 return &gql_generated.PaginatedImagesResult{}, err 131 } 132 133 imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, 134 cveInfo, mTypes.Filter{}, pageInput) 135 if err != nil { 136 return &gql_generated.PaginatedImagesResult{}, err 137 } 138 139 return &gql_generated.PaginatedImagesResult{ 140 Results: imageSummaries, 141 Page: &gql_generated.PageInfo{ 142 TotalCount: pageInfo.TotalCount, 143 ItemCount: pageInfo.ItemCount, 144 }, 145 }, nil 146 } 147 148 func getImageSummary(ctx context.Context, repo, tag string, digest *string, skipCVE convert.SkipQGLField, 149 metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam 150 ) ( 151 *gql_generated.ImageSummary, error, 152 ) { 153 repoMeta, err := metaDB.GetRepoMeta(ctx, repo) 154 if err != nil { 155 return nil, err 156 } 157 158 manifestDescriptor, ok := repoMeta.Tags[tag] 159 if !ok { 160 return nil, gqlerror.Errorf("can't find image: %s:%s", repo, tag) 161 } 162 163 repoMeta.Tags = map[string]mTypes.Descriptor{tag: manifestDescriptor} 164 165 imageDigest := manifestDescriptor.Digest 166 if digest != nil { 167 imageDigest = *digest 168 repoMeta.Tags[tag] = mTypes.Descriptor{ 169 Digest: imageDigest, 170 MediaType: ispec.MediaTypeImageManifest, 171 } 172 } 173 174 imageMetaMap, err := metaDB.FilterImageMeta(ctx, []string{imageDigest}) 175 if err != nil { 176 return &gql_generated.ImageSummary{}, err 177 } 178 179 imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, imageMetaMap, skipCVE, cveInfo) 180 181 if len(imageSummaries) == 0 { 182 return &gql_generated.ImageSummary{}, nil 183 } 184 185 return imageSummaries[0], nil 186 } 187 188 func getCVEListForImage( 189 ctx context.Context, //nolint:unparam // may be used in the future to filter by permissions 190 image string, 191 cveInfo cveinfo.CveInfo, 192 requestedPage *gql_generated.PageInput, 193 searchedCVE string, 194 log log.Logger, //nolint:unparam // may be used by devs for debugging 195 ) (*gql_generated.CVEResultForImage, error) { 196 if requestedPage == nil { 197 requestedPage = &gql_generated.PageInput{} 198 } 199 200 pageInput := cvemodel.PageInput{ 201 Limit: deref(requestedPage.Limit, 0), 202 Offset: deref(requestedPage.Offset, 0), 203 SortBy: cvemodel.SortCriteria( 204 deref(requestedPage.SortBy, gql_generated.SortCriteriaSeverity), 205 ), 206 } 207 208 repo, ref, _ := zcommon.GetImageDirAndReference(image) 209 210 if ref == "" { 211 return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided") 212 } 213 214 cveList, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo, ref, searchedCVE, pageInput) 215 if err != nil { 216 return &gql_generated.CVEResultForImage{}, err 217 } 218 219 cveids := []*gql_generated.Cve{} 220 221 for _, cveDetail := range cveList { 222 vulID := cveDetail.ID 223 desc := cveDetail.Description 224 title := cveDetail.Title 225 severity := cveDetail.Severity 226 227 pkgList := make([]*gql_generated.PackageInfo, 0) 228 229 for _, pkg := range cveDetail.PackageList { 230 pkg := pkg 231 232 pkgList = append(pkgList, 233 &gql_generated.PackageInfo{ 234 Name: &pkg.Name, 235 InstalledVersion: &pkg.InstalledVersion, 236 FixedVersion: &pkg.FixedVersion, 237 }, 238 ) 239 } 240 241 cveids = append(cveids, 242 &gql_generated.Cve{ 243 ID: &vulID, 244 Title: &title, 245 Description: &desc, 246 Severity: &severity, 247 PackageList: pkgList, 248 }, 249 ) 250 } 251 252 return &gql_generated.CVEResultForImage{ 253 Tag: &ref, 254 CVEList: cveids, 255 Page: &gql_generated.PageInfo{ 256 TotalCount: pageInfo.TotalCount, 257 ItemCount: pageInfo.ItemCount, 258 }, 259 }, nil 260 } 261 262 func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc { 263 return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { 264 manifestDigest := imageMeta.Manifests[0].Digest.String() 265 266 for _, tagInfo := range tagsInfo { 267 switch tagInfo.Descriptor.MediaType { 268 case ispec.MediaTypeImageManifest: 269 if tagInfo.Descriptor.Digest.String() == manifestDigest { 270 return true 271 } 272 case ispec.MediaTypeImageIndex: 273 for _, manifestDesc := range tagInfo.Manifests { 274 if manifestDesc.Digest.String() == manifestDigest { 275 return true 276 } 277 } 278 } 279 } 280 281 return false 282 } 283 } 284 285 func FilterByRepoAndTagInfo(repo string, tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc { 286 return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { 287 if repoMeta.Name != repo { 288 return false 289 } 290 291 manifestDigest := imageMeta.Manifests[0].Digest.String() 292 293 for _, tagInfo := range tagsInfo { 294 switch tagInfo.Descriptor.MediaType { 295 case ispec.MediaTypeImageManifest: 296 if tagInfo.Descriptor.Digest.String() == manifestDigest { 297 return true 298 } 299 case ispec.MediaTypeImageIndex: 300 for _, manifestDesc := range tagInfo.Manifests { 301 if manifestDesc.Digest.String() == manifestDigest { 302 return true 303 } 304 } 305 } 306 } 307 308 return false 309 } 310 } 311 312 func getImageListForCVE( 313 ctx context.Context, 314 cveID string, 315 cveInfo cveinfo.CveInfo, 316 filter *gql_generated.Filter, 317 requestedPage *gql_generated.PageInput, 318 metaDB mTypes.MetaDB, 319 log log.Logger, 320 ) (*gql_generated.PaginatedImagesResult, error) { 321 // Obtain all repos and tags 322 // Infinite page to make sure we scan all repos in advance, before filtering results 323 // The CVE scan logic is called from here, not in the actual filter, 324 // this is because we shouldn't keep the DB locked while we wait on scan results 325 reposMeta, err := metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true }) 326 if err != nil { 327 return &gql_generated.PaginatedImagesResult{}, err 328 } 329 330 affectedImages := []cvemodel.TagInfo{} 331 332 for _, repoMeta := range reposMeta { 333 repo := repoMeta.Name 334 335 log.Info().Str("repository", repo).Str("CVE", cveID).Msg("extracting list of tags affected by CVE") 336 337 tagsInfo, err := cveInfo.GetImageListForCVE(ctx, repo, cveID) 338 if err != nil { 339 log.Error().Str("repository", repo).Str("CVE", cveID).Err(err). 340 Msg("error getting image list for CVE from repo") 341 342 return &gql_generated.PaginatedImagesResult{}, err 343 } 344 345 affectedImages = append(affectedImages, tagsInfo...) 346 } 347 348 // We're not interested in other vulnerabilities 349 skip := convert.SkipQGLField{Vulnerabilities: true} 350 351 if requestedPage == nil { 352 requestedPage = &gql_generated.PageInput{} 353 } 354 355 localFilter := mTypes.Filter{} 356 if filter != nil { 357 localFilter = mTypes.Filter{ 358 Os: filter.Os, 359 Arch: filter.Arch, 360 HasToBeSigned: filter.HasToBeSigned, 361 IsBookmarked: filter.IsBookmarked, 362 IsStarred: filter.IsStarred, 363 } 364 } 365 366 // Actual page requested by user 367 pageInput := pagination.PageInput{ 368 Limit: deref(requestedPage.Limit, 0), 369 Offset: deref(requestedPage.Offset, 0), 370 SortBy: pagination.SortCriteria( 371 deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), 372 ), 373 } 374 375 // get all repos 376 fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByTagInfo(affectedImages)) 377 if err != nil { 378 return &gql_generated.PaginatedImagesResult{}, err 379 } 380 381 imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, 382 skip, cveInfo, localFilter, pageInput) 383 if err != nil { 384 return &gql_generated.PaginatedImagesResult{}, err 385 } 386 387 return &gql_generated.PaginatedImagesResult{ 388 Results: imageSummaries, 389 Page: &gql_generated.PageInfo{ 390 TotalCount: pageInfo.TotalCount, 391 ItemCount: pageInfo.ItemCount, 392 }, 393 }, nil 394 } 395 396 func getImageListWithCVEFixed( 397 ctx context.Context, 398 cveID string, 399 repo string, 400 cveInfo cveinfo.CveInfo, 401 filter *gql_generated.Filter, 402 requestedPage *gql_generated.PageInput, 403 metaDB mTypes.MetaDB, 404 log log.Logger, 405 ) (*gql_generated.PaginatedImagesResult, error) { 406 imageList := make([]*gql_generated.ImageSummary, 0) 407 408 log.Info().Str("repository", repo).Str("CVE", cveID).Msg("extracting list of tags where CVE is fixed") 409 410 tagsInfo, err := cveInfo.GetImageListWithCVEFixed(ctx, repo, cveID) 411 if err != nil { 412 log.Error().Str("repository", repo).Str("CVE", cveID).Err(err). 413 Msg("error getting image list with CVE fixed from repo") 414 415 return &gql_generated.PaginatedImagesResult{ 416 Page: &gql_generated.PageInfo{}, 417 Results: imageList, 418 }, err 419 } 420 421 // We're not interested in other vulnerabilities 422 skip := convert.SkipQGLField{Vulnerabilities: true} 423 424 if requestedPage == nil { 425 requestedPage = &gql_generated.PageInput{} 426 } 427 428 localFilter := mTypes.Filter{} 429 if filter != nil { 430 localFilter = mTypes.Filter{ 431 Os: filter.Os, 432 Arch: filter.Arch, 433 HasToBeSigned: filter.HasToBeSigned, 434 IsBookmarked: filter.IsBookmarked, 435 IsStarred: filter.IsStarred, 436 } 437 } 438 439 // Actual page requested by user 440 pageInput := pagination.PageInput{ 441 Limit: deref(requestedPage.Limit, 0), 442 Offset: deref(requestedPage.Offset, 0), 443 SortBy: pagination.SortCriteria( 444 deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), 445 ), 446 } 447 448 // get all repos 449 fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByRepoAndTagInfo(repo, tagsInfo)) 450 if err != nil { 451 return &gql_generated.PaginatedImagesResult{}, err 452 } 453 454 imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, 455 skip, cveInfo, localFilter, pageInput) 456 if err != nil { 457 return &gql_generated.PaginatedImagesResult{}, err 458 } 459 460 return &gql_generated.PaginatedImagesResult{ 461 Results: imageSummaries, 462 Page: &gql_generated.PageInfo{ 463 TotalCount: pageInfo.TotalCount, 464 ItemCount: pageInfo.ItemCount, 465 }, 466 }, nil 467 } 468 469 func repoListWithNewestImage( 470 ctx context.Context, 471 cveInfo cveinfo.CveInfo, 472 log log.Logger, //nolint:unparam // may be used by devs for debugging 473 requestedPage *gql_generated.PageInput, 474 metaDB mTypes.MetaDB, 475 ) (*gql_generated.PaginatedReposResult, error) { 476 paginatedRepos := &gql_generated.PaginatedReposResult{} 477 478 if requestedPage == nil { 479 requestedPage = &gql_generated.PageInput{} 480 } 481 482 skip := convert.SkipQGLField{ 483 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Results.NewestImage.Vulnerabilities"), 484 } 485 486 pageInput := pagination.PageInput{ 487 Limit: deref(requestedPage.Limit, 0), 488 Offset: deref(requestedPage.Offset, 0), 489 SortBy: pagination.SortCriteria( 490 deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), 491 ), 492 } 493 494 repoMetaList, err := metaDB.SearchRepos(ctx, "") 495 if err != nil { 496 return &gql_generated.PaginatedReposResult{}, err 497 } 498 499 imageMetaMap, err := metaDB.FilterImageMeta(ctx, mTypes.GetLatestImageDigests(repoMetaList)) 500 if err != nil { 501 return &gql_generated.PaginatedReposResult{}, err 502 } 503 504 repos, pageInfo, err := convert.PaginatedRepoMeta2RepoSummaries(ctx, repoMetaList, imageMetaMap, 505 mTypes.Filter{}, pageInput, cveInfo, skip) 506 if err != nil { 507 return &gql_generated.PaginatedReposResult{}, err 508 } 509 510 paginatedRepos.Page = &gql_generated.PageInfo{ 511 TotalCount: pageInfo.TotalCount, 512 ItemCount: pageInfo.ItemCount, 513 } 514 515 paginatedRepos.Results = repos 516 517 return paginatedRepos, nil 518 } 519 520 func getBookmarkedRepos( 521 ctx context.Context, 522 cveInfo cveinfo.CveInfo, 523 log log.Logger, //nolint:unparam // may be used by devs for debugging 524 requestedPage *gql_generated.PageInput, 525 metaDB mTypes.MetaDB, 526 ) (*gql_generated.PaginatedReposResult, error) { 527 bookmarkedRepos, err := metaDB.GetBookmarkedRepos(ctx) 528 if err != nil { 529 return &gql_generated.PaginatedReposResult{}, err 530 } 531 532 filterByName := func(repo string) bool { 533 return zcommon.Contains(bookmarkedRepos, repo) 534 } 535 536 return getFilteredPaginatedRepos(ctx, cveInfo, filterByName, log, requestedPage, metaDB) 537 } 538 539 func getStarredRepos( 540 ctx context.Context, 541 cveInfo cveinfo.CveInfo, 542 log log.Logger, //nolint:unparam // may be used by devs for debugging 543 requestedPage *gql_generated.PageInput, 544 metaDB mTypes.MetaDB, 545 ) (*gql_generated.PaginatedReposResult, error) { 546 starredRepos, err := metaDB.GetStarredRepos(ctx) 547 if err != nil { 548 return &gql_generated.PaginatedReposResult{}, err 549 } 550 551 filterFn := func(repo string) bool { 552 return zcommon.Contains(starredRepos, repo) 553 } 554 555 return getFilteredPaginatedRepos(ctx, cveInfo, filterFn, log, requestedPage, metaDB) 556 } 557 558 func getFilteredPaginatedRepos( 559 ctx context.Context, 560 cveInfo cveinfo.CveInfo, 561 filterFn mTypes.FilterRepoNameFunc, 562 log log.Logger, //nolint:unparam // may be used by devs for debugging 563 requestedPage *gql_generated.PageInput, 564 metaDB mTypes.MetaDB, 565 ) (*gql_generated.PaginatedReposResult, error) { 566 if requestedPage == nil { 567 requestedPage = &gql_generated.PageInput{} 568 } 569 570 skip := convert.SkipQGLField{ 571 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Results.NewestImage.Vulnerabilities"), 572 } 573 574 pageInput := pagination.PageInput{ 575 Limit: deref(requestedPage.Limit, 0), 576 Offset: deref(requestedPage.Offset, 0), 577 SortBy: pagination.SortCriteria( 578 deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), 579 ), 580 } 581 582 repoMetaList, err := metaDB.FilterRepos(ctx, filterFn, mTypes.AcceptAllRepoMeta) 583 if err != nil { 584 return &gql_generated.PaginatedReposResult{}, err 585 } 586 587 latestImageMeta, err := metaDB.FilterImageMeta(ctx, mTypes.GetLatestImageDigests(repoMetaList)) 588 if err != nil { 589 return &gql_generated.PaginatedReposResult{}, err 590 } 591 592 repos, pageInfo, err := convert.PaginatedRepoMeta2RepoSummaries(ctx, repoMetaList, latestImageMeta, 593 mTypes.Filter{}, pageInput, cveInfo, skip) 594 if err != nil { 595 return &gql_generated.PaginatedReposResult{}, err 596 } 597 598 return &gql_generated.PaginatedReposResult{ 599 Results: repos, 600 Page: &gql_generated.PageInfo{ 601 TotalCount: pageInfo.TotalCount, 602 ItemCount: pageInfo.ItemCount, 603 }, 604 }, nil 605 } 606 607 func globalSearch(ctx context.Context, query string, metaDB mTypes.MetaDB, filter *gql_generated.Filter, 608 requestedPage *gql_generated.PageInput, cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam 609 ) (*gql_generated.PaginatedReposResult, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary, error, 610 ) { 611 preloads := convert.GetPreloads(ctx) 612 paginatedRepos := gql_generated.PaginatedReposResult{} 613 images := []*gql_generated.ImageSummary{} 614 layers := []*gql_generated.LayerSummary{} 615 616 if requestedPage == nil { 617 requestedPage = &gql_generated.PageInput{} 618 } 619 620 localFilter := mTypes.Filter{} 621 if filter != nil { 622 localFilter = mTypes.Filter{ 623 Os: filter.Os, 624 Arch: filter.Arch, 625 HasToBeSigned: filter.HasToBeSigned, 626 IsBookmarked: filter.IsBookmarked, 627 IsStarred: filter.IsStarred, 628 } 629 } 630 631 if searchingForRepos(query) { 632 skip := convert.SkipQGLField{ 633 Vulnerabilities: canSkipField(preloads, "Repos.NewestImage.Vulnerabilities"), 634 } 635 636 pageInput := pagination.PageInput{ 637 Limit: deref(requestedPage.Limit, 0), 638 Offset: deref(requestedPage.Offset, 0), 639 SortBy: pagination.SortCriteria( 640 deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), 641 ), 642 } 643 644 repoMetaList, err := metaDB.SearchRepos(ctx, query) 645 if err != nil { 646 return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, 647 []*gql_generated.LayerSummary{}, err 648 } 649 650 imageMetaMap, err := metaDB.FilterImageMeta(ctx, mTypes.GetLatestImageDigests(repoMetaList)) 651 if err != nil { 652 return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, 653 []*gql_generated.LayerSummary{}, err 654 } 655 656 repos, pageInfo, err := convert.PaginatedRepoMeta2RepoSummaries(ctx, repoMetaList, imageMetaMap, localFilter, 657 pageInput, cveInfo, 658 skip) 659 if err != nil { 660 return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, 661 []*gql_generated.LayerSummary{}, err 662 } 663 664 paginatedRepos.Page = &gql_generated.PageInfo{ 665 TotalCount: pageInfo.TotalCount, 666 ItemCount: pageInfo.ItemCount, 667 } 668 669 paginatedRepos.Results = repos 670 } else { // search for images 671 skip := convert.SkipQGLField{ 672 Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities"), 673 } 674 675 pageInput := pagination.PageInput{ 676 Limit: deref(requestedPage.Limit, 0), 677 Offset: deref(requestedPage.Offset, 0), 678 SortBy: pagination.SortCriteria( 679 deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), 680 ), 681 } 682 683 fullImageMetaList, err := metaDB.SearchTags(ctx, query) 684 if err != nil { 685 return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err 686 } 687 688 imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo, 689 localFilter, pageInput) 690 if err != nil { 691 return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err 692 } 693 694 images = imageSummaries 695 696 paginatedRepos.Page = &gql_generated.PageInfo{ 697 TotalCount: pageInfo.TotalCount, 698 ItemCount: pageInfo.ItemCount, 699 } 700 } 701 702 return &paginatedRepos, images, layers, nil 703 } 704 705 func canSkipField(preloads map[string]bool, s string) bool { 706 fieldIsPresent := preloads[s] 707 708 return !fieldIsPresent 709 } 710 711 func derivedImageList(ctx context.Context, image string, digest *string, metaDB mTypes.MetaDB, 712 requestedPage *gql_generated.PageInput, 713 cveInfo cveinfo.CveInfo, log log.Logger, 714 ) (*gql_generated.PaginatedImagesResult, error) { 715 if requestedPage == nil { 716 requestedPage = &gql_generated.PageInput{} 717 } 718 719 pageInput := pagination.PageInput{ 720 Limit: deref(requestedPage.Limit, 0), 721 Offset: deref(requestedPage.Offset, 0), 722 SortBy: pagination.SortCriteria( 723 deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), 724 ), 725 } 726 727 skip := convert.SkipQGLField{ 728 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"), 729 } 730 731 imageRepo, imageTag := zcommon.GetImageDirAndTag(image) 732 if imageTag == "" { 733 return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided") 734 } 735 736 skipReferenceImage := convert.SkipQGLField{ 737 Vulnerabilities: true, 738 } 739 740 searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, digest, skipReferenceImage, metaDB, cveInfo, log) 741 if err != nil { 742 if errors.Is(err, zerr.ErrRepoMetaNotFound) { 743 return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository: not found") 744 } 745 746 return &gql_generated.PaginatedImagesResult{}, err 747 } 748 749 // we need all available tags 750 fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, filterDerivedImages(searchedImage)) 751 if err != nil { 752 return &gql_generated.PaginatedImagesResult{}, err 753 } 754 755 derivedList, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo, 756 mTypes.Filter{}, pageInput) 757 if err != nil { 758 return &gql_generated.PaginatedImagesResult{}, err 759 } 760 761 return &gql_generated.PaginatedImagesResult{ 762 Results: derivedList, 763 Page: &gql_generated.PageInfo{ 764 TotalCount: pageInfo.TotalCount, 765 ItemCount: pageInfo.ItemCount, 766 }, 767 }, nil 768 } 769 770 func filterDerivedImages(image *gql_generated.ImageSummary) mTypes.FilterFunc { 771 return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { 772 var addImageToList bool 773 774 imageManifest := imageMeta.Manifests[0] 775 776 for i := range image.Manifests { 777 manifestDigest := imageManifest.Digest.String() 778 if manifestDigest == *image.Manifests[i].Digest { 779 return false 780 } 781 imageLayers := image.Manifests[i].Layers 782 783 addImageToList = false 784 layers := imageManifest.Manifest.Layers 785 786 sameLayer := 0 787 788 for _, l := range imageLayers { 789 for _, k := range layers { 790 if k.Digest.String() == *l.Digest { 791 sameLayer++ 792 } 793 } 794 } 795 796 // if all layers are the same 797 if sameLayer == len(imageLayers) { 798 // it's a derived image 799 addImageToList = true 800 } 801 802 if addImageToList { 803 return true 804 } 805 } 806 807 return false 808 } 809 } 810 811 func baseImageList(ctx context.Context, image string, digest *string, metaDB mTypes.MetaDB, 812 requestedPage *gql_generated.PageInput, 813 cveInfo cveinfo.CveInfo, log log.Logger, 814 ) (*gql_generated.PaginatedImagesResult, error) { 815 if requestedPage == nil { 816 requestedPage = &gql_generated.PageInput{} 817 } 818 819 pageInput := pagination.PageInput{ 820 Limit: deref(requestedPage.Limit, 0), 821 Offset: deref(requestedPage.Offset, 0), 822 SortBy: pagination.SortCriteria( 823 deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime), 824 ), 825 } 826 827 skip := convert.SkipQGLField{ 828 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"), 829 } 830 831 imageRepo, imageTag := zcommon.GetImageDirAndTag(image) 832 833 if imageTag == "" { 834 return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided") 835 } 836 837 skipReferenceImage := convert.SkipQGLField{ 838 Vulnerabilities: true, 839 } 840 841 searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, digest, skipReferenceImage, metaDB, cveInfo, log) 842 if err != nil { 843 if errors.Is(err, zerr.ErrRepoMetaNotFound) { 844 return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository: not found") 845 } 846 847 return &gql_generated.PaginatedImagesResult{}, err 848 } 849 850 // we need all available tags 851 fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, filterBaseImages(searchedImage)) 852 if err != nil { 853 return &gql_generated.PaginatedImagesResult{}, err 854 } 855 856 baseList, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, 857 skip, cveInfo, mTypes.Filter{}, pageInput) 858 if err != nil { 859 return &gql_generated.PaginatedImagesResult{}, err 860 } 861 862 return &gql_generated.PaginatedImagesResult{ 863 Page: &gql_generated.PageInfo{ 864 TotalCount: pageInfo.TotalCount, 865 ItemCount: pageInfo.ItemCount, 866 }, 867 Results: baseList, 868 }, nil 869 } 870 871 func filterBaseImages(image *gql_generated.ImageSummary) mTypes.FilterFunc { 872 return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { 873 var addImageToList bool 874 875 manifest := imageMeta.Manifests[0] 876 877 for i := range image.Manifests { 878 manifestDigest := manifest.Digest.String() 879 if manifestDigest == *image.Manifests[i].Digest { 880 return false 881 } 882 883 addImageToList = true 884 885 for _, l := range manifest.Manifest.Layers { 886 foundLayer := false 887 888 for _, k := range image.Manifests[i].Layers { 889 if l.Digest.String() == *k.Digest { 890 foundLayer = true 891 892 break 893 } 894 } 895 896 if !foundLayer { 897 addImageToList = false 898 899 break 900 } 901 } 902 903 if addImageToList { 904 return true 905 } 906 } 907 908 return false 909 } 910 } 911 912 func validateGlobalSearchInput(query string, filter *gql_generated.Filter, 913 requestedPage *gql_generated.PageInput, 914 ) error { 915 if len(query) > querySizeLimit { 916 return fmt.Errorf("global-search: max string size limit exceeded for query parameter. max=%d current=%d %w", 917 querySizeLimit, len(query), zerr.ErrInvalidRequestParams) 918 } 919 920 err := checkFilter(filter) 921 if err != nil { 922 return err 923 } 924 925 err = checkRequestedPage(requestedPage) 926 if err != nil { 927 return err 928 } 929 930 return nil 931 } 932 933 func checkFilter(filter *gql_generated.Filter) error { 934 if filter == nil { 935 return nil 936 } 937 938 for _, arch := range filter.Arch { 939 if len(*arch) > querySizeLimit { 940 return fmt.Errorf("global-search: max string size limit exceeded for arch parameter. max=%d current=%d %w", 941 querySizeLimit, len(*arch), zerr.ErrInvalidRequestParams) 942 } 943 } 944 945 for _, osSys := range filter.Os { 946 if len(*osSys) > querySizeLimit { 947 return fmt.Errorf("global-search: max string size limit exceeded for os parameter. max=%d current=%d %w", 948 querySizeLimit, len(*osSys), zerr.ErrInvalidRequestParams) 949 } 950 } 951 952 return nil 953 } 954 955 func checkRequestedPage(requestedPage *gql_generated.PageInput) error { 956 if requestedPage == nil { 957 return nil 958 } 959 960 if requestedPage.Limit != nil && *requestedPage.Limit < 0 { 961 return fmt.Errorf("global-search: requested page limit parameter can't be negative %w", 962 zerr.ErrInvalidRequestParams) 963 } 964 965 if requestedPage.Offset != nil && *requestedPage.Offset < 0 { 966 return fmt.Errorf("global-search: requested page offset parameter can't be negative %w", 967 zerr.ErrInvalidRequestParams) 968 } 969 970 return nil 971 } 972 973 func cleanQuery(query string) string { 974 query = strings.TrimSpace(query) 975 query = strings.Trim(query, "/") 976 query = strings.ToLower(query) 977 978 return query 979 } 980 981 func cleanFilter(filter *gql_generated.Filter) *gql_generated.Filter { 982 if filter == nil { 983 return nil 984 } 985 986 if filter.Arch != nil { 987 for i := range filter.Arch { 988 *filter.Arch[i] = strings.ToLower(*filter.Arch[i]) 989 *filter.Arch[i] = strings.TrimSpace(*filter.Arch[i]) 990 } 991 992 filter.Arch = deleteEmptyElements(filter.Arch) 993 } 994 995 if filter.Os != nil { 996 for i := range filter.Os { 997 *filter.Os[i] = strings.ToLower(*filter.Os[i]) 998 *filter.Os[i] = strings.TrimSpace(*filter.Os[i]) 999 } 1000 1001 filter.Os = deleteEmptyElements(filter.Os) 1002 } 1003 1004 return filter 1005 } 1006 1007 func deleteEmptyElements(slice []*string) []*string { 1008 i := 0 1009 for i < len(slice) { 1010 if elementIsEmpty(*slice[i]) { 1011 slice = deleteElementAt(slice, i) 1012 } else { 1013 i++ 1014 } 1015 } 1016 1017 return slice 1018 } 1019 1020 func elementIsEmpty(s string) bool { 1021 return s == "" 1022 } 1023 1024 func deleteElementAt(slice []*string, i int) []*string { 1025 slice[i] = slice[len(slice)-1] 1026 slice = slice[:len(slice)-1] 1027 1028 return slice 1029 } 1030 1031 func expandedRepoInfo(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, log log.Logger, 1032 ) (*gql_generated.RepoInfo, error) { 1033 if ok, err := reqCtx.RepoIsUserAvailable(ctx, repo); !ok || err != nil { 1034 log.Info().Err(err).Str("repository", repo).Bool("availability", ok).Msg("resolver: repo user availability") 1035 1036 return &gql_generated.RepoInfo{}, nil //nolint:nilerr // don't give details to a potential attacker 1037 } 1038 1039 repoMeta, err := metaDB.GetRepoMeta(ctx, repo) 1040 if err != nil { 1041 log.Error().Err(err).Str("repository", repo).Msg("resolver: can't retrieve repoMeta for repo") 1042 1043 return &gql_generated.RepoInfo{}, err 1044 } 1045 1046 tagsDigests := []string{} 1047 1048 for i := range repoMeta.Tags { 1049 if i == "" { 1050 continue 1051 } 1052 1053 tagsDigests = append(tagsDigests, repoMeta.Tags[i].Digest) 1054 } 1055 1056 imageMetaMap, err := metaDB.FilterImageMeta(ctx, tagsDigests) 1057 if err != nil { 1058 log.Error().Err(err).Str("repository", repo).Msg("resolver: can't retrieve imageMeta for repo") 1059 1060 return &gql_generated.RepoInfo{}, err 1061 } 1062 1063 skip := convert.SkipQGLField{ 1064 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Summary.NewestImage.Vulnerabilities") && 1065 canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"), 1066 } 1067 1068 repoSummary, imageSummaries := convert.RepoMeta2ExpandedRepoInfo(ctx, repoMeta, imageMetaMap, 1069 skip, cveInfo, log) 1070 1071 dateSortedImages := make(timeSlice, 0, len(imageSummaries)) 1072 for _, imgSummary := range imageSummaries { 1073 dateSortedImages = append(dateSortedImages, imgSummary) 1074 } 1075 1076 sort.Sort(dateSortedImages) 1077 1078 return &gql_generated.RepoInfo{Summary: repoSummary, Images: dateSortedImages}, nil 1079 } 1080 1081 type timeSlice []*gql_generated.ImageSummary 1082 1083 func (p timeSlice) Len() int { 1084 return len(p) 1085 } 1086 1087 func (p timeSlice) Less(i, j int) bool { 1088 return p[i].LastUpdated.After(*p[j].LastUpdated) 1089 } 1090 1091 func (p timeSlice) Swap(i, j int) { 1092 p[i], p[j] = p[j], p[i] 1093 } 1094 1095 func deref[T any](pointer *T, defaultVal T) T { 1096 if pointer != nil { 1097 return *pointer 1098 } 1099 1100 return defaultVal 1101 } 1102 1103 func searchingForRepos(query string) bool { 1104 return !strings.Contains(query, ":") 1105 } 1106 1107 func getImageList(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, 1108 requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam 1109 ) (*gql_generated.PaginatedImagesResult, error) { 1110 if requestedPage == nil { 1111 requestedPage = &gql_generated.PageInput{} 1112 } 1113 1114 skip := convert.SkipQGLField{ 1115 Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"), 1116 } 1117 1118 pageInput := pagination.PageInput{ 1119 Limit: deref(requestedPage.Limit, 0), 1120 Offset: deref(requestedPage.Offset, 0), 1121 SortBy: pagination.SortCriteria( 1122 deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance), 1123 ), 1124 } 1125 1126 var matchRepoName mTypes.FilterRepoTagFunc 1127 1128 if repo == "" { 1129 matchRepoName = mTypes.AcceptAllRepoTag 1130 } else { 1131 matchRepoName = func(repoName, tag string) bool { return repoName == repo } 1132 } 1133 1134 imageMeta, err := metaDB.FilterTags(ctx, matchRepoName, mTypes.AcceptAllImageMeta) 1135 if err != nil { 1136 return &gql_generated.PaginatedImagesResult{}, err 1137 } 1138 1139 imageList, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, imageMeta, skip, 1140 cveInfo, mTypes.Filter{}, pageInput) 1141 if err != nil { 1142 return &gql_generated.PaginatedImagesResult{}, err 1143 } 1144 1145 return &gql_generated.PaginatedImagesResult{ 1146 Results: imageList, 1147 Page: &gql_generated.PageInfo{ 1148 TotalCount: pageInfo.TotalCount, 1149 ItemCount: pageInfo.ItemCount, 1150 }, 1151 }, nil 1152 } 1153 1154 func getReferrers(metaDB mTypes.MetaDB, repo string, referredDigest string, artifactTypes []string, 1155 log log.Logger, 1156 ) ([]*gql_generated.Referrer, error) { 1157 refDigest := godigest.Digest(referredDigest) 1158 if err := refDigest.Validate(); err != nil { 1159 log.Error().Err(err).Str("digest", referredDigest).Msg("graphql: bad referenced digest string from request") 1160 1161 return []*gql_generated.Referrer{}, fmt.Errorf("graphql: bad digest string from request '%s' %w", 1162 referredDigest, err) 1163 } 1164 1165 referrers, err := metaDB.GetReferrersInfo(repo, refDigest, artifactTypes) 1166 if err != nil { 1167 return nil, err 1168 } 1169 1170 results := make([]*gql_generated.Referrer, 0, len(referrers)) 1171 1172 for _, referrer := range referrers { 1173 referrer := referrer 1174 1175 results = append(results, &gql_generated.Referrer{ 1176 MediaType: &referrer.MediaType, 1177 ArtifactType: &referrer.ArtifactType, 1178 Digest: &referrer.Digest, 1179 Size: &referrer.Size, 1180 Annotations: convert.StringMap2Annotations(referrer.Annotations), 1181 }) 1182 } 1183 1184 return results, nil 1185 }