0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00
zot/pkg/extensions/search/resolver.go
Andrei Aaron da6bd56a21
fix: issues with nested index processing in CVE and metaDB code (#2732)
Also fix an issue with searching tags, which should work with case insensitive searches.

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
2024-10-21 09:57:43 -07:00

1597 lines
43 KiB
Go

package search
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
import (
"context"
"errors"
"fmt"
"sort"
"strings"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/vektah/gqlparser/v2/gqlerror"
zerr "zotregistry.dev/zot/errors"
zcommon "zotregistry.dev/zot/pkg/common"
"zotregistry.dev/zot/pkg/extensions/search/convert"
cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model"
"zotregistry.dev/zot/pkg/extensions/search/gql_generated"
"zotregistry.dev/zot/pkg/extensions/search/pagination"
"zotregistry.dev/zot/pkg/log"
mTypes "zotregistry.dev/zot/pkg/meta/types"
reqCtx "zotregistry.dev/zot/pkg/requestcontext"
"zotregistry.dev/zot/pkg/storage"
)
// THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
const (
querySizeLimit = 256
)
// Resolver ...
type Resolver struct {
cveInfo cveinfo.CveInfo
metaDB mTypes.MetaDB
storeController storage.StoreController
log log.Logger
}
// GetResolverConfig ...
func GetResolverConfig(log log.Logger, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo,
) gql_generated.Config {
resConfig := &Resolver{
cveInfo: cveInfo,
metaDB: metaDB,
storeController: storeController,
log: log,
}
return gql_generated.Config{
Resolvers: resConfig, Directives: gql_generated.DirectiveRoot{},
Complexity: gql_generated.ComplexityRoot{},
}
}
func NewResolver(log log.Logger, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo,
) *Resolver {
resolver := &Resolver{
cveInfo: cveInfo,
metaDB: metaDB,
storeController: storeController,
log: log,
}
return resolver
}
func FilterByDigest(digest string) mTypes.FilterFunc {
// imageMeta will always contain 1 manifest
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
lookupDigest := digest
// Check in case of an index if the index digest matches the search digest
// For Manifests, this is equivalent to imageMeta.Manifests[0]
if imageMeta.Digest.String() == lookupDigest {
return true
}
manifest := imageMeta.Manifests[0]
manifestDigest := manifest.Digest.String()
// Check the image manifest in index.json matches the search digest
// This is a blob with mediaType application/vnd.oci.image.manifest.v1+json
if strings.Contains(manifestDigest, lookupDigest) {
return true
}
// Check the image config matches the search digest
// This is a blob with mediaType application/vnd.oci.image.config.v1+json
if strings.Contains(manifest.Manifest.Config.Digest.String(), lookupDigest) {
return true
}
// Check to see if the individual layers in the oci image manifest match the digest
// These are blobs with mediaType application/vnd.oci.image.layer.v1.tar+gzip
for _, layer := range manifest.Manifest.Layers {
if strings.Contains(layer.Digest.String(), lookupDigest) {
return true
}
}
return false
}
}
func getImageListForDigest(ctx context.Context, digest string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput,
) (*gql_generated.PaginatedImagesResult, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"),
}
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}
fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByDigest(digest))
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip,
cveInfo, mTypes.Filter{}, pageInput)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
return &gql_generated.PaginatedImagesResult{
Results: imageSummaries,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func getImageSummary(ctx context.Context, repo, tag string, digest *string, skipCVE convert.SkipQGLField,
metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam
) (
*gql_generated.ImageSummary, error,
) {
repoMeta, err := metaDB.GetRepoMeta(ctx, repo)
if err != nil {
return nil, err
}
manifestDescriptor, ok := repoMeta.Tags[tag]
if !ok {
return nil, gqlerror.Errorf("can't find image: %s:%s", repo, tag)
}
repoMeta.Tags = map[mTypes.Tag]mTypes.Descriptor{tag: manifestDescriptor}
imageDigest := manifestDescriptor.Digest
if digest != nil {
imageDigest = *digest
repoMeta.Tags[tag] = mTypes.Descriptor{
Digest: imageDigest,
MediaType: ispec.MediaTypeImageManifest,
}
}
imageMetaMap, err := metaDB.FilterImageMeta(ctx, []string{imageDigest})
if err != nil {
return &gql_generated.ImageSummary{}, err
}
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, imageMetaMap, skipCVE, cveInfo)
if len(imageSummaries) == 0 {
return &gql_generated.ImageSummary{}, nil
}
return imageSummaries[0], nil
}
func getCVEListForImage(
ctx context.Context, //nolint:unparam // may be used in the future to filter by permissions
image string,
cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput,
searchedCVE string,
excludedCVE string,
severity string,
log log.Logger, //nolint:unparam // may be used by devs for debugging
) (*gql_generated.CVEResultForImage, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
pageInput := cvemodel.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: cvemodel.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaSeverity),
),
}
repo, ref, _ := zcommon.GetImageDirAndReference(image)
if ref == "" {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided")
}
cveList, imageCveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo, ref,
searchedCVE, excludedCVE, severity, pageInput)
if err != nil {
return &gql_generated.CVEResultForImage{}, err
}
cveids := []*gql_generated.Cve{}
for _, cveDetail := range cveList {
vulID := cveDetail.ID
desc := cveDetail.Description
title := cveDetail.Title
severity := cveDetail.Severity
referenceURL := cveDetail.Reference
pkgList := make([]*gql_generated.PackageInfo, 0)
for _, pkg := range cveDetail.PackageList {
pkg := pkg
pkgList = append(pkgList,
&gql_generated.PackageInfo{
Name: &pkg.Name,
PackagePath: &pkg.PackagePath,
InstalledVersion: &pkg.InstalledVersion,
FixedVersion: &pkg.FixedVersion,
},
)
}
cveids = append(cveids,
&gql_generated.Cve{
ID: &vulID,
Title: &title,
Description: &desc,
Severity: &severity,
Reference: &referenceURL,
PackageList: pkgList,
},
)
}
return &gql_generated.CVEResultForImage{
Tag: &ref,
CVEList: cveids,
Summary: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
UnknownCount: &imageCveSummary.UnknownCount,
LowCount: &imageCveSummary.LowCount,
MediumCount: &imageCveSummary.MediumCount,
HighCount: &imageCveSummary.HighCount,
CriticalCount: &imageCveSummary.CriticalCount,
Count: &imageCveSummary.Count,
},
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func getCVEDiffListForImages(
ctx context.Context, //nolint:unparam // may be used in the future to filter by permissions
minuend gql_generated.ImageInput,
subtrahend gql_generated.ImageInput,
metaDB mTypes.MetaDB,
cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput,
searchedCVE string,
excludedCVE string,
log log.Logger, //nolint:unparam // may be used by devs for debugging
) (*gql_generated.CVEDiffResult, error) {
minuend, err := resolveImageData(ctx, minuend, metaDB)
if err != nil {
return nil, err
}
resultMinuend := getImageIdentifier(minuend)
resultSubtrahend := gql_generated.ImageIdentifier{}
if subtrahend.Repo != "" {
subtrahend, err = resolveImageData(ctx, subtrahend, metaDB)
if err != nil {
return nil, err
}
resultSubtrahend = getImageIdentifier(subtrahend)
} else {
// search for base images
// get minuend image meta
minuendSummary, err := metaDB.GetImageMeta(godigest.Digest(deref(minuend.Digest, "")))
if err != nil {
return &gql_generated.CVEDiffResult{}, err
}
// get the base images for the minuend
minuendBaseImages, err := metaDB.FilterTags(ctx, mTypes.AcceptOnlyRepo(minuend.Repo),
filterBaseImagesForMeta(minuendSummary))
if err != nil {
return &gql_generated.CVEDiffResult{}, err
}
// get the best base image as subtrahend
// get the one with most layers in common
imgLayers := map[string]struct{}{}
for _, layer := range minuendSummary.Manifests[0].Manifest.Layers {
imgLayers[layer.Digest.String()] = struct{}{}
}
bestMatchingScore := 0
for _, baseImage := range minuendBaseImages {
for _, baseManifest := range baseImage.Manifests {
currentMatchingScore := 0
for _, layer := range baseManifest.Manifest.Layers {
if _, ok := imgLayers[layer.Digest.String()]; ok {
currentMatchingScore++
}
}
if currentMatchingScore > bestMatchingScore {
bestMatchingScore = currentMatchingScore
resultSubtrahend = gql_generated.ImageIdentifier{
Repo: baseImage.Repo,
Tag: baseImage.Tag,
Digest: ref(baseImage.Manifests[0].Digest.String()),
Platform: &gql_generated.Platform{
Os: ref(baseImage.Manifests[0].Config.OS),
Arch: ref(getArch(baseImage.Manifests[0].Config.Platform)),
},
}
subtrahend.Repo = baseImage.Repo
subtrahend.Tag = baseImage.Tag
subtrahend.Digest = ref(baseImage.Manifests[0].Digest.String())
}
}
}
}
minuendRepoRef := minuend.Repo + "@" + deref(minuend.Digest, "")
subtrahendRepoRef := subtrahend.Repo + "@" + deref(subtrahend.Digest, "")
page := dderef(requestedPage)
diffCVEs, diffSummary, _, err := cveInfo.GetCVEDiffListForImages(ctx, minuendRepoRef, subtrahendRepoRef, searchedCVE,
excludedCVE, cvemodel.PageInput{
Limit: deref(page.Limit, 0),
Offset: deref(page.Offset, 0),
SortBy: cvemodel.SortCriteria(deref(page.SortBy, gql_generated.SortCriteriaSeverity)),
})
if err != nil {
return nil, err
}
cveids := []*gql_generated.Cve{}
for _, cveDetail := range diffCVEs {
vulID := cveDetail.ID
desc := cveDetail.Description
title := cveDetail.Title
severity := cveDetail.Severity
referenceURL := cveDetail.Reference
pkgList := make([]*gql_generated.PackageInfo, 0)
for _, pkg := range cveDetail.PackageList {
pkg := pkg
pkgList = append(pkgList,
&gql_generated.PackageInfo{
Name: &pkg.Name,
InstalledVersion: &pkg.InstalledVersion,
FixedVersion: &pkg.FixedVersion,
},
)
}
cveids = append(cveids,
&gql_generated.Cve{
ID: &vulID,
Title: &title,
Description: &desc,
Severity: &severity,
Reference: &referenceURL,
PackageList: pkgList,
},
)
}
return &gql_generated.CVEDiffResult{
Minuend: &resultMinuend,
Subtrahend: &resultSubtrahend,
Summary: &gql_generated.ImageVulnerabilitySummary{
Count: &diffSummary.Count,
UnknownCount: &diffSummary.UnknownCount,
LowCount: &diffSummary.LowCount,
MediumCount: &diffSummary.MediumCount,
HighCount: &diffSummary.HighCount,
CriticalCount: &diffSummary.CriticalCount,
MaxSeverity: &diffSummary.MaxSeverity,
},
CVEList: cveids,
Page: &gql_generated.PageInfo{},
}, nil
}
func getImageIdentifier(img gql_generated.ImageInput) gql_generated.ImageIdentifier {
return gql_generated.ImageIdentifier{
Repo: img.Repo,
Tag: img.Tag,
Digest: img.Digest,
Platform: getIdentifierPlatform(img.Platform),
}
}
func getIdentifierPlatform(platform *gql_generated.PlatformInput) *gql_generated.Platform {
if platform == nil {
return nil
}
return &gql_generated.Platform{
Os: platform.Os,
Arch: platform.Arch,
}
}
// rename idea: identify image from input.
func resolveImageData(ctx context.Context, imageInput gql_generated.ImageInput, metaDB mTypes.MetaDB,
) (gql_generated.ImageInput, error) {
if imageInput.Repo == "" {
return gql_generated.ImageInput{}, zerr.ErrEmptyRepoName
}
if imageInput.Tag == "" {
return gql_generated.ImageInput{}, zerr.ErrEmptyTag
}
// try checking if the tag is a simple image first
repoMeta, err := metaDB.GetRepoMeta(ctx, imageInput.Repo)
if err != nil {
return gql_generated.ImageInput{}, err
}
descriptor, ok := repoMeta.Tags[imageInput.Tag]
if !ok {
return gql_generated.ImageInput{}, zerr.ErrImageNotFound
}
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
imageInput.Digest = ref(descriptor.Digest)
return imageInput, nil
case ispec.MediaTypeImageIndex:
if dderef(imageInput.Digest) == "" && !isPlatformSpecified(imageInput.Platform) {
return gql_generated.ImageInput{},
fmt.Errorf("%w: platform or specific manifest digest needed", zerr.ErrAmbiguousInput)
}
imageMeta, err := metaDB.GetImageMeta(godigest.Digest(descriptor.Digest))
if err != nil {
return gql_generated.ImageInput{}, err
}
for _, manifest := range imageMeta.Manifests {
if manifest.Digest.String() == dderef(imageInput.Digest) ||
isMatchingPlatform(manifest.Config.Platform, dderef(imageInput.Platform)) {
imageInput.Digest = ref(manifest.Digest.String())
imageInput.Platform = &gql_generated.PlatformInput{
Os: ref(manifest.Config.OS),
Arch: ref(getArch(manifest.Config.Platform)),
}
return imageInput, nil
}
}
return imageInput, zerr.ErrImageNotFound
}
return imageInput, nil
}
func isPlatformSpecified(platformInput *gql_generated.PlatformInput) bool {
if platformInput == nil {
return false
}
if dderef(platformInput.Os) == "" || dderef(platformInput.Arch) == "" {
return false
}
return true
}
func isMatchingPlatform(platform ispec.Platform, platformInput gql_generated.PlatformInput) bool {
if platform.OS != deref(platformInput.Os, "") {
return false
}
arch := getArch(platform)
return arch == deref(platformInput.Arch, "")
}
func getArch(platform ispec.Platform) string {
arch := platform.Architecture
if arch != "" && platform.Variant != "" {
arch += "/" + platform.Variant
}
return arch
}
func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
manifestDigest := imageMeta.Manifests[0].Digest.String()
for _, tagInfo := range tagsInfo {
switch tagInfo.Descriptor.MediaType {
case ispec.MediaTypeImageManifest:
if tagInfo.Descriptor.Digest.String() == manifestDigest {
return true
}
case ispec.MediaTypeImageIndex:
for _, manifestDesc := range tagInfo.Manifests {
if manifestDesc.Digest.String() == manifestDigest {
return true
}
}
}
}
return false
}
}
func FilterByRepoAndTagInfo(repo string, tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
if repoMeta.Name != repo {
return false
}
manifestDigest := imageMeta.Manifests[0].Digest.String()
for _, tagInfo := range tagsInfo {
switch tagInfo.Descriptor.MediaType {
case ispec.MediaTypeImageManifest:
if tagInfo.Descriptor.Digest.String() == manifestDigest {
return true
}
case ispec.MediaTypeImageIndex:
for _, manifestDesc := range tagInfo.Manifests {
if manifestDesc.Digest.String() == manifestDigest {
return true
}
}
}
}
return false
}
}
func getImageListForCVE(
ctx context.Context,
cveID string,
cveInfo cveinfo.CveInfo,
filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput,
metaDB mTypes.MetaDB,
log log.Logger,
) (*gql_generated.PaginatedImagesResult, error) {
// Obtain all repos and tags
// Infinite page to make sure we scan all repos in advance, before filtering results
// The CVE scan logic is called from here, not in the actual filter,
// this is because we shouldn't keep the DB locked while we wait on scan results
reposMeta, err := metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMeta) bool { return true })
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
affectedImages := []cvemodel.TagInfo{}
for _, repoMeta := range reposMeta {
repo := repoMeta.Name
log.Info().Str("repository", repo).Str("CVE", cveID).Msg("extracting list of tags affected by this cve")
tagsInfo, err := cveInfo.GetImageListForCVE(ctx, repo, cveID)
if err != nil {
log.Error().Str("repository", repo).Str("CVE", cveID).Err(err).
Msg("failed to get image list for this cve from repository")
return &gql_generated.PaginatedImagesResult{}, err
}
affectedImages = append(affectedImages, tagsInfo...)
}
// We're not interested in other vulnerabilities
skip := convert.SkipQGLField{Vulnerabilities: true}
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
localFilter := mTypes.Filter{}
if filter != nil {
localFilter = mTypes.Filter{
Os: filter.Os,
Arch: filter.Arch,
HasToBeSigned: filter.HasToBeSigned,
IsBookmarked: filter.IsBookmarked,
IsStarred: filter.IsStarred,
}
}
// Actual page requested by user
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
// get all repos
fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByTagInfo(affectedImages))
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList,
skip, cveInfo, localFilter, pageInput)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
return &gql_generated.PaginatedImagesResult{
Results: imageSummaries,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func getImageListWithCVEFixed(
ctx context.Context,
cveID string,
repo string,
cveInfo cveinfo.CveInfo,
filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput,
metaDB mTypes.MetaDB,
log log.Logger,
) (*gql_generated.PaginatedImagesResult, error) {
imageList := make([]*gql_generated.ImageSummary, 0)
log.Info().Str("repository", repo).Str("CVE", cveID).Msg("extracting list of tags where this cve is fixed")
tagsInfo, err := cveInfo.GetImageListWithCVEFixed(ctx, repo, cveID)
if err != nil {
log.Error().Str("repository", repo).Str("CVE", cveID).Err(err).
Msg("failed to get image list with this cve fixed from repository")
return &gql_generated.PaginatedImagesResult{
Page: &gql_generated.PageInfo{},
Results: imageList,
}, err
}
// We're not interested in other vulnerabilities
skip := convert.SkipQGLField{Vulnerabilities: true}
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
localFilter := mTypes.Filter{}
if filter != nil {
localFilter = mTypes.Filter{
Os: filter.Os,
Arch: filter.Arch,
HasToBeSigned: filter.HasToBeSigned,
IsBookmarked: filter.IsBookmarked,
IsStarred: filter.IsStarred,
}
}
// Actual page requested by user
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
// get all repos
fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByRepoAndTagInfo(repo, tagsInfo))
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList,
skip, cveInfo, localFilter, pageInput)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
return &gql_generated.PaginatedImagesResult{
Results: imageSummaries,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func repoListWithNewestImage(
ctx context.Context,
cveInfo cveinfo.CveInfo,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
metaDB mTypes.MetaDB,
) (*gql_generated.PaginatedReposResult, error) {
paginatedRepos := &gql_generated.PaginatedReposResult{}
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Results.NewestImage.Vulnerabilities"),
}
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
repoMetaList, err := metaDB.SearchRepos(ctx, "")
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
imageMetaMap, err := metaDB.FilterImageMeta(ctx, mTypes.GetLatestImageDigests(repoMetaList))
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
repos, pageInfo, err := convert.PaginatedRepoMeta2RepoSummaries(ctx, repoMetaList, imageMetaMap,
mTypes.Filter{}, pageInput, cveInfo, skip)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
paginatedRepos.Results = repos
return paginatedRepos, nil
}
func getBookmarkedRepos(
ctx context.Context,
cveInfo cveinfo.CveInfo,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
metaDB mTypes.MetaDB,
) (*gql_generated.PaginatedReposResult, error) {
bookmarkedRepos, err := metaDB.GetBookmarkedRepos(ctx)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
filterByName := func(repo string) bool {
return zcommon.Contains(bookmarkedRepos, repo)
}
return getFilteredPaginatedRepos(ctx, cveInfo, filterByName, log, requestedPage, metaDB)
}
func getStarredRepos(
ctx context.Context,
cveInfo cveinfo.CveInfo,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
metaDB mTypes.MetaDB,
) (*gql_generated.PaginatedReposResult, error) {
starredRepos, err := metaDB.GetStarredRepos(ctx)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
filterFn := func(repo string) bool {
return zcommon.Contains(starredRepos, repo)
}
return getFilteredPaginatedRepos(ctx, cveInfo, filterFn, log, requestedPage, metaDB)
}
func getFilteredPaginatedRepos(
ctx context.Context,
cveInfo cveinfo.CveInfo,
filterFn mTypes.FilterRepoNameFunc,
log log.Logger, //nolint:unparam // may be used by devs for debugging
requestedPage *gql_generated.PageInput,
metaDB mTypes.MetaDB,
) (*gql_generated.PaginatedReposResult, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Results.NewestImage.Vulnerabilities"),
}
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
repoMetaList, err := metaDB.FilterRepos(ctx, filterFn, mTypes.AcceptAllRepoMeta)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
latestImageMeta, err := metaDB.FilterImageMeta(ctx, mTypes.GetLatestImageDigests(repoMetaList))
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
repos, pageInfo, err := convert.PaginatedRepoMeta2RepoSummaries(ctx, repoMetaList, latestImageMeta,
mTypes.Filter{}, pageInput, cveInfo, skip)
if err != nil {
return &gql_generated.PaginatedReposResult{}, err
}
return &gql_generated.PaginatedReposResult{
Results: repos,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func globalSearch(ctx context.Context, query string, metaDB mTypes.MetaDB, filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput, cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam
) (*gql_generated.PaginatedReposResult, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary, error,
) {
preloads := convert.GetPreloads(ctx)
paginatedRepos := gql_generated.PaginatedReposResult{}
images := []*gql_generated.ImageSummary{}
layers := []*gql_generated.LayerSummary{}
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
localFilter := mTypes.Filter{}
if filter != nil {
localFilter = mTypes.Filter{
Os: filter.Os,
Arch: filter.Arch,
HasToBeSigned: filter.HasToBeSigned,
IsBookmarked: filter.IsBookmarked,
IsStarred: filter.IsStarred,
}
}
switch getSearchTarget(query) {
case RepoTarget:
skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Repos.NewestImage.Vulnerabilities")}
pageInput := getPageInput(requestedPage)
repoMetaList, err := metaDB.SearchRepos(ctx, query)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{},
[]*gql_generated.LayerSummary{}, err
}
imageMetaMap, err := metaDB.FilterImageMeta(ctx, mTypes.GetLatestImageDigests(repoMetaList))
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{},
[]*gql_generated.LayerSummary{}, err
}
repos, pageInfo, err := convert.PaginatedRepoMeta2RepoSummaries(ctx, repoMetaList, imageMetaMap, localFilter,
pageInput, cveInfo,
skip)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{},
[]*gql_generated.LayerSummary{}, err
}
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
paginatedRepos.Results = repos
case ImageTarget:
skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities")}
pageInput := getPageInput(requestedPage)
fullImageMetaList, err := metaDB.SearchTags(ctx, query)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
}
imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo,
localFilter, pageInput)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
}
images = imageSummaries
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
case TagTarget:
skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities")}
pageInput := getPageInput(requestedPage)
expectedTag := strings.TrimPrefix(query, `:`)
matchTagName := func(repoName, actualTag string) bool {
return strings.Contains(strings.ToLower(actualTag), expectedTag)
}
fullImageMetaList, err := metaDB.FilterTags(ctx, matchTagName, mTypes.AcceptAllImageMeta)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
}
imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo,
localFilter, pageInput)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
}
images = imageSummaries
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
case DigestTarget:
skip := convert.SkipQGLField{Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities")}
pageInput := getPageInput(requestedPage)
searchedDigest := query
fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, FilterByDigest(searchedDigest))
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
}
imageSummaries, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo,
localFilter, pageInput)
if err != nil {
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
}
images = imageSummaries
paginatedRepos.Page = &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
}
default:
return &paginatedRepos, images, layers, zerr.ErrInvalidSearchQuery
}
return &paginatedRepos, images, layers, nil
}
func getPageInput(requestedPage *gql_generated.PageInput) pagination.PageInput {
return pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}
}
type SearchTarget int
const (
RepoTarget = iota
ImageTarget
DigestTarget
InvalidTarget
TagTarget
)
func getSearchTarget(query string) SearchTarget {
if !strings.ContainsAny(query, ":@") {
return RepoTarget
}
if strings.HasPrefix(query, string(godigest.SHA256)+":") {
return DigestTarget
}
if before, after, found := strings.Cut(query, ":"); found {
if before != "" {
return ImageTarget
} else if after != "" {
return TagTarget
}
}
return InvalidTarget
}
func canSkipField(preloads map[string]bool, s string) bool {
fieldIsPresent := preloads[s]
return !fieldIsPresent
}
func derivedImageList(ctx context.Context, image string, digest *string, metaDB mTypes.MetaDB,
requestedPage *gql_generated.PageInput,
cveInfo cveinfo.CveInfo, log log.Logger,
) (*gql_generated.PaginatedImagesResult, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
}
imageRepo, imageTag := zcommon.GetImageDirAndTag(image)
if imageTag == "" {
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided")
}
skipReferenceImage := convert.SkipQGLField{
Vulnerabilities: true,
}
searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, digest, skipReferenceImage, metaDB, cveInfo, log)
if err != nil {
if errors.Is(err, zerr.ErrRepoMetaNotFound) {
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository not found")
}
return &gql_generated.PaginatedImagesResult{}, err
}
// we need all available tags
fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, filterDerivedImages(searchedImage))
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
derivedList, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList, skip, cveInfo,
mTypes.Filter{}, pageInput)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
return &gql_generated.PaginatedImagesResult{
Results: derivedList,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func filterDerivedImages(image *gql_generated.ImageSummary) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
var addImageToList bool
imageManifest := imageMeta.Manifests[0]
for i := range image.Manifests {
manifestDigest := imageManifest.Digest.String()
if manifestDigest == *image.Manifests[i].Digest {
return false
}
imageLayers := image.Manifests[i].Layers
addImageToList = false
layers := imageManifest.Manifest.Layers
sameLayer := 0
for _, l := range imageLayers {
for _, k := range layers {
if k.Digest.String() == *l.Digest {
sameLayer++
}
}
}
// if all layers are the same
if sameLayer == len(imageLayers) {
// it's a derived image
addImageToList = true
}
if addImageToList {
return true
}
}
return false
}
}
func baseImageList(ctx context.Context, image string, digest *string, metaDB mTypes.MetaDB,
requestedPage *gql_generated.PageInput,
cveInfo cveinfo.CveInfo, log log.Logger,
) (*gql_generated.PaginatedImagesResult, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
}
imageRepo, imageTag := zcommon.GetImageDirAndTag(image)
if imageTag == "" {
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided")
}
skipReferenceImage := convert.SkipQGLField{
Vulnerabilities: true,
}
searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, digest, skipReferenceImage, metaDB, cveInfo, log)
if err != nil {
if errors.Is(err, zerr.ErrRepoMetaNotFound) {
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("repository not found")
}
return &gql_generated.PaginatedImagesResult{}, err
}
// we need all available tags
fullImageMetaList, err := metaDB.FilterTags(ctx, mTypes.AcceptAllRepoTag, filterBaseImages(searchedImage))
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
baseList, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, fullImageMetaList,
skip, cveInfo, mTypes.Filter{}, pageInput)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
return &gql_generated.PaginatedImagesResult{
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
Results: baseList,
}, nil
}
func filterBaseImages(image *gql_generated.ImageSummary) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
var addImageToList bool
manifest := imageMeta.Manifests[0]
for i := range image.Manifests {
manifestDigest := manifest.Digest.String()
if manifestDigest == *image.Manifests[i].Digest {
return false
}
addImageToList = true
for _, l := range manifest.Manifest.Layers {
foundLayer := false
for _, k := range image.Manifests[i].Layers {
if l.Digest.String() == *k.Digest {
foundLayer = true
break
}
}
if !foundLayer {
addImageToList = false
break
}
}
if addImageToList {
return true
}
}
return false
}
}
func filterBaseImagesForMeta(image mTypes.ImageMeta) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
var addImageToList bool
manifest := imageMeta.Manifests[0]
for i := range image.Manifests {
manifestDigest := manifest.Digest.String()
if manifestDigest == image.Manifests[i].Digest.String() {
return false
}
addImageToList = true
for _, l := range manifest.Manifest.Layers {
foundLayer := false
for _, k := range image.Manifests[i].Manifest.Layers {
if l.Digest.String() == k.Digest.String() {
foundLayer = true
break
}
}
if !foundLayer {
addImageToList = false
break
}
}
if addImageToList {
return true
}
}
return false
}
}
func validateGlobalSearchInput(query string, filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput,
) error {
if len(query) > querySizeLimit {
return fmt.Errorf("max string size limit exceeded for query parameter. max=%d current=%d %w",
querySizeLimit, len(query), zerr.ErrInvalidRequestParams)
}
err := checkFilter(filter)
if err != nil {
return err
}
err = checkRequestedPage(requestedPage)
if err != nil {
return err
}
return nil
}
func checkFilter(filter *gql_generated.Filter) error {
if filter == nil {
return nil
}
for _, arch := range filter.Arch {
if len(*arch) > querySizeLimit {
return fmt.Errorf("max string size limit exceeded for arch parameter. max=%d current=%d %w",
querySizeLimit, len(*arch), zerr.ErrInvalidRequestParams)
}
}
for _, osSys := range filter.Os {
if len(*osSys) > querySizeLimit {
return fmt.Errorf("max string size limit exceeded for os parameter. max=%d current=%d %w",
querySizeLimit, len(*osSys), zerr.ErrInvalidRequestParams)
}
}
return nil
}
func checkRequestedPage(requestedPage *gql_generated.PageInput) error {
if requestedPage == nil {
return nil
}
if requestedPage.Limit != nil && *requestedPage.Limit < 0 {
return fmt.Errorf("requested page limit parameter can't be negative %w",
zerr.ErrInvalidRequestParams)
}
if requestedPage.Offset != nil && *requestedPage.Offset < 0 {
return fmt.Errorf("requested page offset parameter can't be negative %w",
zerr.ErrInvalidRequestParams)
}
return nil
}
func cleanQuery(query string) string {
query = strings.TrimSpace(query)
query = strings.Trim(query, "/")
query = strings.ToLower(query)
return query
}
func cleanFilter(filter *gql_generated.Filter) *gql_generated.Filter {
if filter == nil {
return nil
}
if filter.Arch != nil {
for i := range filter.Arch {
*filter.Arch[i] = strings.ToLower(*filter.Arch[i])
*filter.Arch[i] = strings.TrimSpace(*filter.Arch[i])
}
filter.Arch = deleteEmptyElements(filter.Arch)
}
if filter.Os != nil {
for i := range filter.Os {
*filter.Os[i] = strings.ToLower(*filter.Os[i])
*filter.Os[i] = strings.TrimSpace(*filter.Os[i])
}
filter.Os = deleteEmptyElements(filter.Os)
}
return filter
}
func deleteEmptyElements(slice []*string) []*string {
i := 0
for i < len(slice) {
if elementIsEmpty(*slice[i]) {
slice = deleteElementAt(slice, i)
} else {
i++
}
}
return slice
}
func elementIsEmpty(s string) bool {
return s == ""
}
func deleteElementAt(slice []*string, i int) []*string {
slice[i] = slice[len(slice)-1]
slice = slice[:len(slice)-1]
return slice
}
func expandedRepoInfo(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo, log log.Logger,
) (*gql_generated.RepoInfo, error) {
if ok, err := reqCtx.RepoIsUserAvailable(ctx, repo); !ok || err != nil {
log.Info().Err(err).Str("repository", repo).Bool("availability", ok).Str("component", "graphql").
Msg("repo user availability")
return &gql_generated.RepoInfo{}, nil //nolint:nilerr // don't give details to a potential attacker
}
repoMeta, err := metaDB.GetRepoMeta(ctx, repo)
if err != nil {
log.Error().Err(err).Str("repository", repo).Str("component", "graphql").
Msg("can't retrieve repoMeta for repository")
return &gql_generated.RepoInfo{}, err
}
tagsDigests := []string{}
for i := range repoMeta.Tags {
if i == "" {
continue
}
digest := repoMeta.Tags[i].Digest
if !zcommon.Contains(tagsDigests, digest) {
tagsDigests = append(tagsDigests, digest)
}
}
imageMetaMap, err := metaDB.FilterImageMeta(ctx, tagsDigests)
if err != nil {
log.Error().Err(err).Str("repository", repo).Str("component", "graphql").
Msg("can't retrieve imageMeta for repo")
return &gql_generated.RepoInfo{}, err
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Summary.NewestImage.Vulnerabilities") &&
canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"),
}
repoSummary, imageSummaries := convert.RepoMeta2ExpandedRepoInfo(ctx, repoMeta, imageMetaMap,
skip, cveInfo, log)
dateSortedImages := make(timeSlice, 0, len(imageSummaries))
for _, imgSummary := range imageSummaries {
dateSortedImages = append(dateSortedImages, imgSummary)
}
sort.Sort(dateSortedImages)
return &gql_generated.RepoInfo{Summary: repoSummary, Images: dateSortedImages}, nil
}
type timeSlice []*gql_generated.ImageSummary
func (p timeSlice) Len() int {
return len(p)
}
func (p timeSlice) Less(i, j int) bool {
return p[i].LastUpdated.After(*p[j].LastUpdated)
}
func (p timeSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
// dderef is a default deref.
func dderef[T any](pointer *T) T {
var defValue T
if pointer != nil {
return *pointer
}
return defValue
}
func deref[T any](pointer *T, defaultVal T) T {
if pointer != nil {
return *pointer
}
return defaultVal
}
func ref[T any](input T) *T {
ref := input
return &ref
}
func getImageList(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam
) (*gql_generated.PaginatedImagesResult, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
skip := convert.SkipQGLField{
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"),
}
pageInput := pagination.PageInput{
Limit: deref(requestedPage.Limit, 0),
Offset: deref(requestedPage.Offset, 0),
SortBy: pagination.SortCriteria(
deref(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}
var matchRepoName mTypes.FilterRepoTagFunc
if repo == "" {
matchRepoName = mTypes.AcceptAllRepoTag
} else {
matchRepoName = func(repoName, tag string) bool { return repoName == repo }
}
imageMeta, err := metaDB.FilterTags(ctx, matchRepoName, mTypes.AcceptAllImageMeta)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
imageList, pageInfo, err := convert.PaginatedFullImageMeta2ImageSummaries(ctx, imageMeta, skip,
cveInfo, mTypes.Filter{}, pageInput)
if err != nil {
return &gql_generated.PaginatedImagesResult{}, err
}
return &gql_generated.PaginatedImagesResult{
Results: imageList,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func getReferrers(metaDB mTypes.MetaDB, repo string, referredDigest string, artifactTypes []string,
log log.Logger,
) ([]*gql_generated.Referrer, error) {
refDigest := godigest.Digest(referredDigest)
if err := refDigest.Validate(); err != nil {
log.Error().Err(err).Str("digest", referredDigest).Str("component", "graphql").
Msg("bad referenced digest string from request")
return []*gql_generated.Referrer{}, fmt.Errorf("bad digest string from request '%s' %w",
referredDigest, err)
}
referrers, err := metaDB.GetReferrersInfo(repo, refDigest, artifactTypes)
if err != nil {
return nil, err
}
results := make([]*gql_generated.Referrer, 0, len(referrers))
for _, referrer := range referrers {
referrer := referrer
results = append(results, &gql_generated.Referrer{
MediaType: &referrer.MediaType,
ArtifactType: &referrer.ArtifactType,
Digest: &referrer.Digest,
Size: &referrer.Size,
Annotations: convert.StringMap2Annotations(referrer.Annotations),
})
}
return results, nil
}