mirror of
https://github.com/project-zot/zot.git
synced 2024-12-23 22:27:35 -05:00
e2c7a3c5ba
Signed-off-by: Ramkumar Chinchani <rchincha@cisco.com>
1111 lines
30 KiB
Go
1111 lines
30 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"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/99designs/gqlgen/graphql"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
|
|
|
zerr "zotregistry.io/zot/errors"
|
|
"zotregistry.io/zot/pkg/extensions/search/common"
|
|
"zotregistry.io/zot/pkg/extensions/search/convert"
|
|
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
|
digestinfo "zotregistry.io/zot/pkg/extensions/search/digest"
|
|
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
|
|
"zotregistry.io/zot/pkg/log"
|
|
"zotregistry.io/zot/pkg/meta/repodb"
|
|
localCtx "zotregistry.io/zot/pkg/requestcontext"
|
|
"zotregistry.io/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
|
|
repoDB repodb.RepoDB
|
|
storeController storage.StoreController
|
|
digestInfo *digestinfo.DigestInfo
|
|
log log.Logger
|
|
}
|
|
|
|
// GetResolverConfig ...
|
|
func GetResolverConfig(log log.Logger, storeController storage.StoreController,
|
|
repoDB repodb.RepoDB, cveInfo cveinfo.CveInfo,
|
|
) gql_generated.Config {
|
|
digestInfo := digestinfo.NewDigestInfo(storeController, log)
|
|
|
|
resConfig := &Resolver{
|
|
cveInfo: cveInfo,
|
|
repoDB: repoDB,
|
|
storeController: storeController,
|
|
digestInfo: digestInfo,
|
|
log: log,
|
|
}
|
|
|
|
return gql_generated.Config{
|
|
Resolvers: resConfig, Directives: gql_generated.DirectiveRoot{},
|
|
Complexity: gql_generated.ComplexityRoot{},
|
|
}
|
|
}
|
|
|
|
func NewResolver(log log.Logger, storeController storage.StoreController,
|
|
repoDB repodb.RepoDB, cveInfo cveinfo.CveInfo,
|
|
) *Resolver {
|
|
digestInfo := digestinfo.NewDigestInfo(storeController, log)
|
|
|
|
resolver := &Resolver{
|
|
cveInfo: cveInfo,
|
|
repoDB: repoDB,
|
|
storeController: storeController,
|
|
digestInfo: digestInfo,
|
|
log: log,
|
|
}
|
|
|
|
return resolver
|
|
}
|
|
|
|
func FilterByDigest(digest string) repodb.FilterFunc {
|
|
return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool {
|
|
lookupDigest := digest
|
|
contains := false
|
|
|
|
var manifest ispec.Manifest
|
|
|
|
err := json.Unmarshal(manifestMeta.ManifestBlob, &manifest)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).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) {
|
|
contains = 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.Config.Digest.String(), lookupDigest) {
|
|
contains = 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.Layers {
|
|
if strings.Contains(layer.Digest.String(), lookupDigest) {
|
|
contains = true
|
|
}
|
|
}
|
|
|
|
return contains
|
|
}
|
|
}
|
|
|
|
func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.RepoDB, cveInfo cveinfo.CveInfo,
|
|
requestedPage *gql_generated.PageInput,
|
|
) ([]*gql_generated.ImageSummary, error) {
|
|
imageList := make([]*gql_generated.ImageSummary, 0)
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"),
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
|
|
),
|
|
}
|
|
|
|
// get all repos
|
|
reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, FilterByDigest(digest), pageInput)
|
|
if err != nil {
|
|
return []*gql_generated.ImageSummary{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
imageList = append(imageList, imageSummaries...)
|
|
}
|
|
|
|
return imageList, nil
|
|
}
|
|
|
|
func getImageSummary(ctx context.Context, repo, tag string, repoDB repodb.RepoDB,
|
|
cveInfo cveinfo.CveInfo, log log.Logger, //nolint:unparam
|
|
) (
|
|
*gql_generated.ImageSummary, error,
|
|
) {
|
|
repoMeta, err := repoDB.GetRepoMeta(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)
|
|
}
|
|
|
|
manifestDigest := manifestDescriptor.Digest
|
|
|
|
for t := range repoMeta.Tags {
|
|
if t != tag {
|
|
delete(repoMeta.Tags, t)
|
|
}
|
|
}
|
|
|
|
manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(manifestDigest))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
manifestMetaMap := map[string]repodb.ManifestMetadata{
|
|
manifestDigest: manifestMeta,
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
|
|
}
|
|
|
|
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
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,
|
|
log log.Logger, //nolint:unparam // may be used by devs for debugging
|
|
) (*gql_generated.CVEResultForImage, error) {
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
pageInput := cveinfo.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: cveinfo.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity),
|
|
),
|
|
}
|
|
|
|
_, copyImgTag := common.GetImageDirAndTag(image)
|
|
|
|
if copyImgTag == "" {
|
|
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided")
|
|
}
|
|
|
|
cveList, pageInfo, err := cveInfo.GetCVEListForImage(image, 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
|
|
|
|
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,
|
|
PackageList: pkgList,
|
|
},
|
|
)
|
|
}
|
|
|
|
return &gql_generated.CVEResultForImage{
|
|
Tag: ©ImgTag,
|
|
CVEList: cveids,
|
|
Page: &gql_generated.PageInfo{
|
|
TotalCount: pageInfo.TotalCount,
|
|
ItemCount: pageInfo.ItemCount,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func FilterByTagInfo(tagsInfo []common.TagInfo) repodb.FilterFunc {
|
|
return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool {
|
|
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String()
|
|
|
|
for _, tagInfo := range tagsInfo {
|
|
if tagInfo.Digest.String() == manifestDigest {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|
|
|
|
func getImageListForCVE(
|
|
ctx context.Context,
|
|
cveID string,
|
|
cveInfo cveinfo.CveInfo,
|
|
requestedPage *gql_generated.PageInput,
|
|
repoDB repodb.RepoDB,
|
|
log log.Logger,
|
|
) ([]*gql_generated.ImageSummary, 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 := repoDB.GetMultipleRepoMeta(ctx,
|
|
func(repoMeta repodb.RepoMetadata) bool { return true },
|
|
repodb.PageInput{Limit: 0, Offset: 0, SortBy: repodb.SortCriteria(gql_generated.SortCriteriaUpdateTime)},
|
|
)
|
|
if err != nil {
|
|
return []*gql_generated.ImageSummary{}, err
|
|
}
|
|
|
|
affectedImages := []common.TagInfo{}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
repo := repoMeta.Name
|
|
|
|
log.Info().Str("repo", repo).Str("CVE", cveID).Msg("extracting list of tags affected by CVE")
|
|
|
|
tagsInfo, err := cveInfo.GetImageListForCVE(repo, cveID)
|
|
if err != nil {
|
|
log.Error().Str("repo", repo).Str("CVE", cveID).Err(err).
|
|
Msg("error getting image list for CVE from repo")
|
|
|
|
return []*gql_generated.ImageSummary{}, err
|
|
}
|
|
|
|
affectedImages = append(affectedImages, tagsInfo...)
|
|
}
|
|
|
|
imageList := make([]*gql_generated.ImageSummary, 0)
|
|
|
|
// We're not interested in other vulnerabilities
|
|
skip := convert.SkipQGLField{Vulnerabilities: true}
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
// Actual page requested by user
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
|
|
),
|
|
}
|
|
|
|
// get all repos
|
|
reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, FilterByTagInfo(affectedImages), pageInput)
|
|
if err != nil {
|
|
return []*gql_generated.ImageSummary{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
imageList = append(imageList, imageSummaries...)
|
|
}
|
|
|
|
return imageList, nil
|
|
}
|
|
|
|
func getImageListWithCVEFixed(
|
|
ctx context.Context,
|
|
cveID string,
|
|
repo string,
|
|
cveInfo cveinfo.CveInfo,
|
|
requestedPage *gql_generated.PageInput,
|
|
repoDB repodb.RepoDB,
|
|
log log.Logger,
|
|
) ([]*gql_generated.ImageSummary, error) {
|
|
imageList := make([]*gql_generated.ImageSummary, 0)
|
|
|
|
log.Info().Str("repo", repo).Str("CVE", cveID).Msg("extracting list of tags where CVE is fixed")
|
|
|
|
tagsInfo, err := cveInfo.GetImageListWithCVEFixed(repo, cveID)
|
|
if err != nil {
|
|
log.Error().Str("repo", repo).Str("CVE", cveID).Err(err).
|
|
Msg("error getting image list with CVE fixed from repo")
|
|
|
|
return imageList, err
|
|
}
|
|
|
|
// We're not interested in other vulnerabilities
|
|
skip := convert.SkipQGLField{Vulnerabilities: true}
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
// Actual page requested by user
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
|
|
),
|
|
}
|
|
|
|
// get all repos
|
|
reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx, FilterByTagInfo(tagsInfo), pageInput)
|
|
if err != nil {
|
|
return []*gql_generated.ImageSummary{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
if repoMeta.Name != repo {
|
|
continue
|
|
}
|
|
|
|
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
imageList = append(imageList, imageSummaries...)
|
|
}
|
|
|
|
return imageList, 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,
|
|
repoDB repodb.RepoDB,
|
|
) (*gql_generated.PaginatedReposResult, error) {
|
|
repos := []*gql_generated.RepoSummary{}
|
|
paginatedRepos := &gql_generated.PaginatedReposResult{}
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Results.NewestImage.Vulnerabilities"),
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
|
|
),
|
|
}
|
|
|
|
reposMeta, manifestMetaMap, pageInfo, err := repoDB.SearchRepos(ctx, "", repodb.Filter{}, pageInput)
|
|
if err != nil {
|
|
return &gql_generated.PaginatedReposResult{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
repos = append(repos, repoSummary)
|
|
}
|
|
|
|
paginatedRepos.Page = &gql_generated.PageInfo{
|
|
TotalCount: pageInfo.TotalCount,
|
|
ItemCount: pageInfo.ItemCount,
|
|
}
|
|
paginatedRepos.Results = repos
|
|
|
|
return paginatedRepos, nil
|
|
}
|
|
|
|
func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, 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)
|
|
repos := []*gql_generated.RepoSummary{}
|
|
paginatedRepos := gql_generated.PaginatedReposResult{}
|
|
images := []*gql_generated.ImageSummary{}
|
|
layers := []*gql_generated.LayerSummary{}
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
localFilter := repodb.Filter{}
|
|
if filter != nil {
|
|
localFilter = repodb.Filter{
|
|
Os: filter.Os,
|
|
Arch: filter.Arch,
|
|
HasToBeSigned: filter.HasToBeSigned,
|
|
}
|
|
}
|
|
|
|
if searchingForRepos(query) {
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(preloads, "Repos.NewestImage.Vulnerabilities"),
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
|
|
),
|
|
}
|
|
|
|
reposMeta, manifestMetaMap, pageInfo, err := repoDB.SearchRepos(ctx, query, localFilter, pageInput)
|
|
if err != nil {
|
|
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
repoSummary := convert.RepoMeta2RepoSummary(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
repos = append(repos, repoSummary)
|
|
}
|
|
|
|
paginatedRepos.Page = &gql_generated.PageInfo{
|
|
TotalCount: pageInfo.TotalCount,
|
|
ItemCount: pageInfo.ItemCount,
|
|
}
|
|
|
|
paginatedRepos.Results = repos
|
|
} else { // search for images
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(preloads, "Images.Vulnerabilities"),
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
|
|
),
|
|
}
|
|
|
|
reposMeta, manifestMetaMap, pageInfo, err := repoDB.SearchTags(ctx, query, localFilter, pageInput)
|
|
if err != nil {
|
|
return &gql_generated.PaginatedReposResult{}, []*gql_generated.ImageSummary{}, []*gql_generated.LayerSummary{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
images = append(images, imageSummaries...)
|
|
}
|
|
|
|
paginatedRepos.Page = &gql_generated.PageInfo{
|
|
TotalCount: pageInfo.TotalCount,
|
|
ItemCount: pageInfo.ItemCount,
|
|
}
|
|
}
|
|
|
|
return &paginatedRepos, images, layers, nil
|
|
}
|
|
|
|
func canSkipField(preloads map[string]bool, s string) bool {
|
|
fieldIsPresent := preloads[s]
|
|
|
|
return !fieldIsPresent
|
|
}
|
|
|
|
func derivedImageList(ctx context.Context, image string, repoDB repodb.RepoDB,
|
|
requestedPage *gql_generated.PageInput,
|
|
cveInfo cveinfo.CveInfo, log log.Logger,
|
|
) (*gql_generated.PaginatedImagesResult, error) {
|
|
derivedList := make([]*gql_generated.ImageSummary, 0)
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
|
|
),
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
|
|
}
|
|
|
|
imageRepo, imageTag := common.GetImageDirAndTag(image)
|
|
if imageTag == "" {
|
|
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided")
|
|
}
|
|
|
|
searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, repoDB, 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
|
|
reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx,
|
|
filterDerivedImages(searchedImage),
|
|
pageInput)
|
|
if err != nil {
|
|
return &gql_generated.PaginatedImagesResult{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
derivedList = append(derivedList, summary...)
|
|
}
|
|
|
|
if len(derivedList) == 0 {
|
|
log.Info().Msg("no images found")
|
|
|
|
return &gql_generated.PaginatedImagesResult{
|
|
Page: &gql_generated.PageInfo{},
|
|
Results: derivedList,
|
|
}, nil
|
|
}
|
|
|
|
return &gql_generated.PaginatedImagesResult{
|
|
Results: derivedList,
|
|
Page: &gql_generated.PageInfo{
|
|
TotalCount: pageInfo.TotalCount,
|
|
ItemCount: pageInfo.ItemCount,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func filterDerivedImages(image *gql_generated.ImageSummary) repodb.FilterFunc {
|
|
return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool {
|
|
var addImageToList bool
|
|
|
|
var imageManifest ispec.Manifest
|
|
|
|
err := json.Unmarshal(manifestMeta.ManifestBlob, &imageManifest)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String()
|
|
if manifestDigest == *image.Digest {
|
|
return false
|
|
}
|
|
|
|
imageLayers := image.Layers
|
|
|
|
addImageToList = false
|
|
layers := imageManifest.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
|
|
}
|
|
|
|
return addImageToList
|
|
}
|
|
}
|
|
|
|
func baseImageList(ctx context.Context, image string, repoDB repodb.RepoDB,
|
|
requestedPage *gql_generated.PageInput,
|
|
cveInfo cveinfo.CveInfo, log log.Logger,
|
|
) (*gql_generated.PaginatedImagesResult, error) {
|
|
imageSummaries := make([]*gql_generated.ImageSummary, 0)
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
|
|
),
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Vulnerabilities"),
|
|
}
|
|
|
|
imageRepo, imageTag := common.GetImageDirAndTag(image)
|
|
|
|
if imageTag == "" {
|
|
return &gql_generated.PaginatedImagesResult{}, gqlerror.Errorf("no reference provided")
|
|
}
|
|
|
|
searchedImage, err := getImageSummary(ctx, imageRepo, imageTag, repoDB, 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
|
|
reposMeta, manifestMetaMap, pageInfo, err := repoDB.FilterTags(ctx,
|
|
filterBaseImages(searchedImage),
|
|
pageInput)
|
|
if err != nil {
|
|
return &gql_generated.PaginatedImagesResult{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
summary := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
imageSummaries = append(imageSummaries, summary...)
|
|
}
|
|
|
|
if len(imageSummaries) == 0 {
|
|
log.Info().Msg("no images found")
|
|
|
|
return &gql_generated.PaginatedImagesResult{
|
|
Results: imageSummaries,
|
|
Page: &gql_generated.PageInfo{},
|
|
}, nil
|
|
}
|
|
|
|
return &gql_generated.PaginatedImagesResult{
|
|
Page: &gql_generated.PageInfo{
|
|
TotalCount: pageInfo.TotalCount,
|
|
ItemCount: pageInfo.ItemCount,
|
|
},
|
|
Results: imageSummaries,
|
|
}, nil
|
|
}
|
|
|
|
func filterBaseImages(image *gql_generated.ImageSummary) repodb.FilterFunc {
|
|
return func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool {
|
|
var addImageToList bool
|
|
|
|
var imageManifest ispec.Manifest
|
|
|
|
err := json.Unmarshal(manifestMeta.ManifestBlob, &imageManifest)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String()
|
|
if manifestDigest == *image.Digest {
|
|
return false
|
|
}
|
|
|
|
imageLayers := image.Layers
|
|
|
|
addImageToList = true
|
|
layers := imageManifest.Layers
|
|
|
|
for _, l := range layers {
|
|
foundLayer := false
|
|
|
|
for _, k := range imageLayers {
|
|
if l.Digest.String() == *k.Digest {
|
|
foundLayer = true
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundLayer {
|
|
addImageToList = false
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return addImageToList
|
|
}
|
|
}
|
|
|
|
func validateGlobalSearchInput(query string, filter *gql_generated.Filter,
|
|
requestedPage *gql_generated.PageInput,
|
|
) error {
|
|
if len(query) > querySizeLimit {
|
|
format := "global-search: max string size limit exeeded for query parameter. max=%d current=%d"
|
|
|
|
return errors.Wrapf(zerr.ErrInvalidRequestParams, format, querySizeLimit, len(query))
|
|
}
|
|
|
|
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 {
|
|
format := "global-search: max string size limit exeeded for arch parameter. max=%d current=%d"
|
|
|
|
return errors.Wrapf(zerr.ErrInvalidRequestParams, format, querySizeLimit, len(*arch))
|
|
}
|
|
}
|
|
|
|
for _, osSys := range filter.Os {
|
|
if len(*osSys) > querySizeLimit {
|
|
format := "global-search: max string size limit exeeded for os parameter. max=%d current=%d"
|
|
|
|
return errors.Wrapf(zerr.ErrInvalidRequestParams, format, querySizeLimit, len(*osSys))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func checkRequestedPage(requestedPage *gql_generated.PageInput) error {
|
|
if requestedPage == nil {
|
|
return nil
|
|
}
|
|
|
|
if requestedPage.Limit != nil && *requestedPage.Limit < 0 {
|
|
format := "global-search: requested page limit parameter can't be negative"
|
|
|
|
return errors.Wrap(zerr.ErrInvalidRequestParams, format)
|
|
}
|
|
|
|
if requestedPage.Offset != nil && *requestedPage.Offset < 0 {
|
|
format := "global-search: requested page offset parameter can't be negative"
|
|
|
|
return errors.Wrap(zerr.ErrInvalidRequestParams, format)
|
|
}
|
|
|
|
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, repoDB repodb.RepoDB, cveInfo cveinfo.CveInfo, log log.Logger,
|
|
) (*gql_generated.RepoInfo, error) {
|
|
if ok, err := localCtx.RepoIsUserAvailable(ctx, repo); !ok || err != nil {
|
|
log.Info().Err(err).Msgf("resolver: 'repo %s is user available' = %v", repo, ok)
|
|
|
|
return &gql_generated.RepoInfo{}, nil //nolint:nilerr // don't give details to a potential attacker
|
|
}
|
|
|
|
repoMeta, err := repoDB.GetRepoMeta(repo)
|
|
if err != nil {
|
|
log.Error().Err(err).Msgf("resolver: can't retrieve repoMeta for repo %s", repo)
|
|
|
|
return &gql_generated.RepoInfo{}, err
|
|
}
|
|
|
|
manifestMetaMap := map[string]repodb.ManifestMetadata{}
|
|
|
|
for tag, descriptor := range repoMeta.Tags {
|
|
digest := descriptor.Digest
|
|
|
|
if _, alreadyDownloaded := manifestMetaMap[digest]; alreadyDownloaded {
|
|
continue
|
|
}
|
|
|
|
manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(digest))
|
|
if err != nil {
|
|
graphql.AddError(ctx, errors.Wrapf(err,
|
|
"resolver: failed to get manifest meta for image %s:%s with manifest digest %s", repo, tag, digest))
|
|
|
|
continue
|
|
}
|
|
|
|
manifestMetaMap[digest] = manifestMeta
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Summary.NewestImage.Vulnerabilities"),
|
|
}
|
|
|
|
repoSummary, imageSummaries := convert.RepoMeta2ExpandedRepoInfo(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
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]
|
|
}
|
|
|
|
func safeDerefferencing[T any](pointer *T, defaultVal T) T {
|
|
if pointer != nil {
|
|
return *pointer
|
|
}
|
|
|
|
return defaultVal
|
|
}
|
|
|
|
func searchingForRepos(query string) bool {
|
|
return !strings.Contains(query, ":")
|
|
}
|
|
|
|
func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInfo cveinfo.CveInfo,
|
|
requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam
|
|
) ([]*gql_generated.ImageSummary, error) {
|
|
imageList := make([]*gql_generated.ImageSummary, 0)
|
|
|
|
if requestedPage == nil {
|
|
requestedPage = &gql_generated.PageInput{}
|
|
}
|
|
|
|
skip := convert.SkipQGLField{
|
|
Vulnerabilities: canSkipField(convert.GetPreloads(ctx), "Images.Vulnerabilities"),
|
|
}
|
|
|
|
pageInput := repodb.PageInput{
|
|
Limit: safeDerefferencing(requestedPage.Limit, 0),
|
|
Offset: safeDerefferencing(requestedPage.Offset, 0),
|
|
SortBy: repodb.SortCriteria(
|
|
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
|
|
),
|
|
}
|
|
|
|
// reposMeta, manifestMetaMap, err := repoDB.SearchRepos(ctx, repo, repodb.Filter{}, pageInput)
|
|
reposMeta, manifestMetaMap, _, err := repoDB.FilterTags(ctx,
|
|
func(repoMeta repodb.RepoMetadata, manifestMeta repodb.ManifestMetadata) bool {
|
|
return true
|
|
},
|
|
pageInput)
|
|
if err != nil {
|
|
return []*gql_generated.ImageSummary{}, err
|
|
}
|
|
|
|
for _, repoMeta := range reposMeta {
|
|
if repoMeta.Name != repo && repo != "" {
|
|
continue
|
|
}
|
|
imageSummaries := convert.RepoMeta2ImageSummaries(ctx, repoMeta, manifestMetaMap, skip, cveInfo)
|
|
|
|
imageList = append(imageList, imageSummaries...)
|
|
}
|
|
|
|
return imageList, nil
|
|
}
|
|
|
|
func getReferrers(store storage.ImageStore, repoName string, digest string, artifactTypes []string, log log.Logger) (
|
|
[]*gql_generated.Referrer, error,
|
|
) {
|
|
results := make([]*gql_generated.Referrer, 0)
|
|
|
|
index, err := store.GetReferrers(repoName, godigest.Digest(digest), artifactTypes)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("error extracting referrers list")
|
|
|
|
return results, err
|
|
}
|
|
|
|
for _, manifest := range index.Manifests {
|
|
size := int(manifest.Size)
|
|
digest := manifest.Digest.String()
|
|
annotations := make([]*gql_generated.Annotation, 0)
|
|
artifactType := manifest.ArtifactType
|
|
mediaType := manifest.MediaType
|
|
|
|
for k, v := range manifest.Annotations {
|
|
key := k
|
|
value := v
|
|
|
|
annotations = append(annotations, &gql_generated.Annotation{
|
|
Key: &key,
|
|
Value: &value,
|
|
})
|
|
}
|
|
|
|
results = append(results, &gql_generated.Referrer{
|
|
MediaType: &mediaType,
|
|
ArtifactType: &artifactType,
|
|
Digest: &digest,
|
|
Size: &size,
|
|
Annotations: annotations,
|
|
})
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
// get passed context from authzHandler and filter out repos based on permissions.
|
|
func userAvailableRepos(ctx context.Context, repoList []string) ([]string, error) {
|
|
var availableRepos []string
|
|
|
|
// authz request context (set in authz middleware)
|
|
acCtx, err := localCtx.GetAccessControlContext(ctx)
|
|
if err != nil {
|
|
err := zerr.ErrBadType
|
|
|
|
return []string{}, err
|
|
}
|
|
|
|
if acCtx != nil {
|
|
for _, r := range repoList {
|
|
if acCtx.IsAdmin || acCtx.CanReadRepo(r) {
|
|
availableRepos = append(availableRepos, r)
|
|
}
|
|
}
|
|
} else {
|
|
availableRepos = repoList
|
|
}
|
|
|
|
return availableRepos, nil
|
|
}
|
|
|
|
func extractImageDetails(
|
|
ctx context.Context,
|
|
layoutUtils common.OciLayoutUtils,
|
|
repo, tag string, //nolint:unparam // function only called in the tests
|
|
log log.Logger) (
|
|
godigest.Digest, *ispec.Manifest, *ispec.Image, error,
|
|
) {
|
|
validRepoList, err := userAvailableRepos(ctx, []string{repo})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("unable to retrieve access token")
|
|
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
if len(validRepoList) == 0 {
|
|
log.Error().Err(err).Msg("user is not authorized")
|
|
|
|
return "", nil, nil, zerr.ErrUnauthorizedAccess
|
|
}
|
|
|
|
manifest, dig, err := layoutUtils.GetImageManifest(repo, tag)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Could not retrieve image ispec manifest")
|
|
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
digest := dig
|
|
|
|
imageConfig, err := layoutUtils.GetImageConfigInfo(repo, digest)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Could not retrieve image config")
|
|
|
|
return "", nil, nil, err
|
|
}
|
|
|
|
return digest, &manifest, &imageConfig, nil
|
|
}
|