mirror of
https://github.com/project-zot/zot.git
synced 2024-12-30 22:34:13 -05:00
da6bd56a21
Also fix an issue with searching tags, which should work with case insensitive searches. Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
1597 lines
43 KiB
Go
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
|
|
}
|