0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-23 22:27:35 -05:00
zot/pkg/extensions/search/resolver.go
Andrei Aaron e0d808b196
Include image vulnerability information in ImageSummary (#798)
Return this data as part of GlobalSearch and RepoListWithNewestImage
query results.
This commit also includes refactoring of the CVE scanning logic in
order to better encapsulate trivy specific logic, remove CVE scanning
logic from the graphql resolver.

Signed-off-by: Andrei Aaron <andaaron@cisco.com>
2022-09-28 11:39:54 -07:00

721 lines
20 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"
"strconv"
"strings"
"github.com/99designs/gqlgen/graphql"
glob "github.com/bmatcuk/doublestar/v4" // nolint:gci
v1 "github.com/google/go-containerregistry/pkg/v1" // nolint:gci
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/vektah/gqlparser/v2/gqlerror"
"zotregistry.io/zot/pkg/extensions/search/common"
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" // nolint: gci
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.
// Resolver ...
type Resolver struct {
cveInfo cveinfo.CveInfo
storeController storage.StoreController
digestInfo *digestinfo.DigestInfo
log log.Logger
}
var (
ErrBadCtxFormat = errors.New("type assertion failed")
ErrBadLayerCount = errors.New("manifest: layers count doesn't correspond to config history")
)
// GetResolverConfig ...
func GetResolverConfig(log log.Logger, storeController storage.StoreController, cveInfo cveinfo.CveInfo,
) gql_generated.Config {
digestInfo := digestinfo.NewDigestInfo(storeController, log)
resConfig := &Resolver{cveInfo: cveInfo, storeController: storeController, digestInfo: digestInfo, log: log}
return gql_generated.Config{
Resolvers: resConfig, Directives: gql_generated.DirectiveRoot{},
Complexity: gql_generated.ComplexityRoot{},
}
}
func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) {
imgResultForDigest := []*gql_generated.ImageSummary{}
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
var errResult error
for _, repo := range repoList {
r.log.Info().Str("repo", repo).Msg("filtering list of tags in image repo by digest")
imgTags, err := r.digestInfo.GetImageTagsByDigest(repo, digest)
if err != nil {
r.log.Error().Err(err).Msg("unable to get filtered list of image tags")
return []*gql_generated.ImageSummary{}, err
}
for _, imageInfo := range imgTags {
imageConfig, err := olu.GetImageConfigInfo(repo, imageInfo.Digest)
if err != nil {
return []*gql_generated.ImageSummary{}, err
}
imageInfo := BuildImageInfo(repo, imageInfo.Tag, imageInfo.Digest, imageInfo.Manifest, imageConfig)
imgResultForDigest = append(imgResultForDigest, imageInfo)
}
}
return imgResultForDigest, errResult
}
func repoListWithNewestImage(
ctx context.Context,
repoList []string,
olu common.OciLayoutUtils,
cveInfo cveinfo.CveInfo,
log log.Logger,
) ([]*gql_generated.RepoSummary, error) {
reposSummary := []*gql_generated.RepoSummary{}
for _, repo := range repoList {
lastUpdatedTag, err := olu.GetRepoLastUpdated(repo)
if err != nil {
msg := fmt.Sprintf("can't get last updated manifest for repo: %s", repo)
log.Error().Err(err).Msg(msg)
graphql.AddError(ctx, gqlerror.Errorf(msg))
continue
}
repoSize := int64(0)
repoBlob2Size := make(map[string]int64, 10)
manifests, err := olu.GetImageManifests(repo)
if err != nil {
msg := fmt.Sprintf("can't get manifests for repo: %s", repo)
log.Error().Err(err).Msg(msg)
graphql.AddError(ctx, gqlerror.Errorf(msg))
continue
}
repoPlatforms := make([]*gql_generated.OsArch, 0)
repoVendors := make([]*string, 0, len(manifests))
repoName := repo
var lastUpdatedImageSummary gql_generated.ImageSummary
var brokenManifest bool
for _, manifest := range manifests {
imageLayersSize := int64(0)
manifestSize := olu.GetImageManifestSize(repo, manifest.Digest)
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifest.Digest)
if err != nil {
msg := fmt.Sprintf("reference not found for manifest %s", manifest.Digest)
log.Error().Err(err).Msg(msg)
graphql.AddError(ctx, gqlerror.Errorf(msg))
brokenManifest = true
continue
}
configSize := imageBlobManifest.Config.Size
repoBlob2Size[manifest.Digest.String()] = manifestSize
repoBlob2Size[imageBlobManifest.Config.Digest.Hex] = configSize
for _, layer := range imageBlobManifest.Layers {
repoBlob2Size[layer.Digest.String()] = layer.Size
imageLayersSize += layer.Size
}
imageSize := imageLayersSize + manifestSize + configSize
imageConfigInfo, err := olu.GetImageConfigInfo(repo, manifest.Digest)
if err != nil {
msg := fmt.Sprintf("can't get image config for manifest %s", manifest.Digest)
log.Error().Err(err).Msg(msg)
graphql.AddError(ctx, gqlerror.Errorf(msg))
brokenManifest = true
continue
}
os, arch := olu.GetImagePlatform(imageConfigInfo)
osArch := &gql_generated.OsArch{
Os: &os,
Arch: &arch,
}
repoPlatforms = append(repoPlatforms, osArch)
// get image info from manifest annotation, if not found get from image config labels.
annotations := common.GetAnnotations(imageBlobManifest.Annotations, imageConfigInfo.Config.Labels)
repoVendors = append(repoVendors, &annotations.Vendor)
manifestTag, ok := manifest.Annotations[ispec.AnnotationRefName]
if !ok {
msg := fmt.Sprintf("reference not found for manifest %s in repo %s",
manifest.Digest.String(), repoName)
log.Error().Msg(msg)
graphql.AddError(ctx, gqlerror.Errorf(msg))
brokenManifest = true
break
}
imageCveSummary := cveinfo.ImageCVESummary{}
// Check if vulnerability scanning is disabled
if cveInfo != nil {
imageName := fmt.Sprintf("%s:%s", repoName, manifestTag)
imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName)
if err != nil {
// Log the error, but we should still include the manifest in results
msg := fmt.Sprintf(
"unable to run vulnerability scan on tag %s in repo %s",
manifestTag,
repoName,
)
log.Error().Msg(msg)
graphql.AddError(ctx, gqlerror.Errorf(msg))
}
}
tag := manifestTag
size := strconv.Itoa(int(imageSize))
manifestDigest := manifest.Digest.Hex()
configDigest := imageBlobManifest.Config.Digest.Hex
isSigned := olu.CheckManifestSignature(repo, manifest.Digest)
lastUpdated := olu.GetImageLastUpdated(imageConfigInfo)
score := 0
imageSummary := gql_generated.ImageSummary{
RepoName: &repoName,
Tag: &tag,
LastUpdated: &lastUpdated,
Digest: &manifestDigest,
ConfigDigest: &configDigest,
IsSigned: &isSigned,
Size: &size,
Platform: osArch,
Vendor: &annotations.Vendor,
Score: &score,
Description: &annotations.Description,
Title: &annotations.Title,
Documentation: &annotations.Documentation,
Licenses: &annotations.Licenses,
Labels: &annotations.Labels,
Source: &annotations.Source,
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
},
}
if manifest.Digest.String() == lastUpdatedTag.Digest {
lastUpdatedImageSummary = imageSummary
}
}
if brokenManifest {
continue
}
for blob := range repoBlob2Size {
repoSize += repoBlob2Size[blob]
}
repoSizeStr := strconv.FormatInt(repoSize, 10)
index := 0
reposSummary = append(reposSummary, &gql_generated.RepoSummary{
Name: &repoName,
LastUpdated: &lastUpdatedTag.Timestamp,
Size: &repoSizeStr,
Platforms: repoPlatforms,
Vendors: repoVendors,
Score: &index,
NewestImage: &lastUpdatedImageSummary,
})
}
return reposSummary, nil
}
func cleanQuerry(query string) string {
query = strings.ToLower(query)
query = strings.Replace(query, ":", " ", 1)
return query
}
func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils,
cveInfo cveinfo.CveInfo, log log.Logger) (
[]*gql_generated.RepoSummary, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary,
) {
repos := []*gql_generated.RepoSummary{}
images := []*gql_generated.ImageSummary{}
layers := []*gql_generated.LayerSummary{}
for _, repo := range repoList {
repo := repo
// map used for dedube if 2 images reference the same blob
repoBlob2Size := make(map[string]int64, 10)
// made up of all manifests, configs and image layers
repoSize := int64(0)
lastUpdatedTag, err := olu.GetRepoLastUpdated(repo)
if err != nil {
log.Error().Err(err).Msgf("can't find latest updated tag for repo: %s", repo)
}
manifests, err := olu.GetImageManifests(repo)
if err != nil {
log.Error().Err(err).Msgf("can't get manifests for repo: %s", repo)
continue
}
var lastUpdatedImageSummary gql_generated.ImageSummary
repoPlatforms := make([]*gql_generated.OsArch, 0, len(manifests))
repoVendors := make([]*string, 0, len(manifests))
for i, manifest := range manifests {
imageLayersSize := int64(0)
manifestTag, ok := manifest.Annotations[ispec.AnnotationRefName]
if !ok {
log.Error().Str("digest", manifest.Digest.String()).Msg("reference not found for this manifest")
continue
}
imageBlobManifest, err := olu.GetImageBlobManifest(repo, manifests[i].Digest)
if err != nil {
log.Error().Err(err).Msgf("can't read manifest for repo %s %s", repo, manifestTag)
continue
}
manifestSize := olu.GetImageManifestSize(repo, manifest.Digest)
configSize := imageBlobManifest.Config.Size
repoBlob2Size[manifest.Digest.String()] = manifestSize
repoBlob2Size[imageBlobManifest.Config.Digest.Hex] = configSize
for _, layer := range imageBlobManifest.Layers {
layer := layer
layerDigest := layer.Digest.String()
layerSizeStr := strconv.Itoa(int(layer.Size))
repoBlob2Size[layer.Digest.String()] = layer.Size
imageLayersSize += layer.Size
// if we have a tag we won't match a layer
if tag != "" {
continue
}
if index := strings.Index(layerDigest, name); index != -1 {
layers = append(layers, &gql_generated.LayerSummary{
Digest: &layerDigest,
Size: &layerSizeStr,
Score: &index,
})
}
}
imageSize := imageLayersSize + manifestSize + configSize
index := strings.Index(repo, name)
matchesTag := strings.HasPrefix(manifestTag, tag)
if index != -1 {
imageConfigInfo, err := olu.GetImageConfigInfo(repo, manifests[i].Digest)
if err != nil {
log.Error().Err(err).Msgf("can't retrieve config info for the image %s %s", repo, manifestTag)
continue
}
size := strconv.Itoa(int(imageSize))
isSigned := olu.CheckManifestSignature(repo, manifests[i].Digest)
// update matching score
score := calculateImageMatchingScore(repo, index, matchesTag)
lastUpdated := olu.GetImageLastUpdated(imageConfigInfo)
os, arch := olu.GetImagePlatform(imageConfigInfo)
osArch := &gql_generated.OsArch{
Os: &os,
Arch: &arch,
}
// get image info from manifest annotation, if not found get from image config labels.
annotations := common.GetAnnotations(imageBlobManifest.Annotations, imageConfigInfo.Config.Labels)
manifestDigest := manifest.Digest.Hex()
configDigest := imageBlobManifest.Config.Digest.Hex
repoPlatforms = append(repoPlatforms, osArch)
repoVendors = append(repoVendors, &annotations.Vendor)
imageCveSummary := cveinfo.ImageCVESummary{}
// Check if vulnerability scanning is disabled
if cveInfo != nil {
imageName := fmt.Sprintf("%s:%s", repo, manifestTag)
imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName)
if err != nil {
// Log the error, but we should still include the manifest in results
log.Error().Err(err).Msgf(
"unable to run vulnerability scan on tag %s in repo %s",
manifestTag,
repo,
)
}
}
imageSummary := gql_generated.ImageSummary{
RepoName: &repo,
Tag: &manifestTag,
LastUpdated: &lastUpdated,
Digest: &manifestDigest,
ConfigDigest: &configDigest,
IsSigned: &isSigned,
Size: &size,
Platform: osArch,
Vendor: &annotations.Vendor,
Score: &score,
Description: &annotations.Description,
Title: &annotations.Title,
Documentation: &annotations.Documentation,
Licenses: &annotations.Licenses,
Labels: &annotations.Labels,
Source: &annotations.Source,
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
MaxSeverity: &imageCveSummary.MaxSeverity,
Count: &imageCveSummary.Count,
},
}
if manifest.Digest.String() == lastUpdatedTag.Digest {
lastUpdatedImageSummary = imageSummary
}
images = append(images, &imageSummary)
}
}
for blob := range repoBlob2Size {
repoSize += repoBlob2Size[blob]
}
if index := strings.Index(repo, name); index != -1 {
repoSize := strconv.FormatInt(repoSize, 10)
repos = append(repos, &gql_generated.RepoSummary{
Name: &repo,
LastUpdated: &lastUpdatedTag.Timestamp,
Size: &repoSize,
Platforms: repoPlatforms,
Vendors: repoVendors,
Score: &index,
NewestImage: &lastUpdatedImageSummary,
})
}
}
sort.Slice(repos, func(i, j int) bool {
return *repos[i].Score < *repos[j].Score
})
sort.Slice(images, func(i, j int) bool {
return *images[i].Score < *images[j].Score
})
sort.Slice(layers, func(i, j int) bool {
return *layers[i].Score < *layers[j].Score
})
return repos, images, layers
}
// calcalculateImageMatchingScore iterated from the index of the matched string in the
// artifact name until the beginning of the string or until delimitator "/".
// The distance represents the score of the match.
//
// Example:
// query: image
// repos: repo/test/myimage
// Score will be 2.
func calculateImageMatchingScore(artefactName string, index int, matchesTag bool) int {
score := 0
for index >= 1 {
if artefactName[index-1] == '/' {
break
}
index--
score++
}
if !matchesTag {
score += 10
}
return score
}
func (r *queryResolver) getImageList(store storage.ImageStore, imageName string) (
[]*gql_generated.ImageSummary, error,
) {
results := make([]*gql_generated.ImageSummary, 0)
repoList, err := store.GetRepositories()
if err != nil {
r.log.Error().Err(err).Msg("extension api: error extracting repositories list")
return results, err
}
layoutUtils := common.NewBaseOciLayoutUtils(r.storeController, r.log)
for _, repo := range repoList {
if (imageName != "" && repo == imageName) || imageName == "" {
tagsInfo, err := layoutUtils.GetImageTagsWithTimestamp(repo)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error getting tag timestamp info")
return results, nil
}
if len(tagsInfo) == 0 {
r.log.Info().Str("no tagsinfo found for repo", repo).Msg(" continuing traversing")
continue
}
for i := range tagsInfo {
// using a loop variable called tag would be reassigned after each iteration, using the same memory address
// directly access the value at the current index in the slice as ImageInfo requires pointers to tag fields
tag := tagsInfo[i]
digest := godigest.Digest(tag.Digest)
manifest, err := layoutUtils.GetImageBlobManifest(repo, digest)
if err != nil {
r.log.Error().Err(err).Msg("extension api: error reading manifest")
return results, err
}
imageConfig, err := layoutUtils.GetImageConfigInfo(repo, digest)
if err != nil {
return results, err
}
imageInfo := BuildImageInfo(repo, tag.Name, digest, manifest, imageConfig)
results = append(results, imageInfo)
}
}
}
if len(results) == 0 {
r.log.Info().Msg("no repositories found")
}
return results, nil
}
func BuildImageInfo(repo string, tag string, manifestDigest godigest.Digest,
manifest v1.Manifest, imageConfig ispec.Image,
) *gql_generated.ImageSummary {
layers := []*gql_generated.LayerSummary{}
size := int64(0)
log := log.NewLogger("debug", "")
allHistory := []*gql_generated.LayerHistory{}
formattedManifestDigest := manifestDigest.Hex()
history := imageConfig.History
if len(history) == 0 {
for _, layer := range manifest.Layers {
size += layer.Size
digest := layer.Digest.Hex
layerSize := strconv.FormatInt(layer.Size, 10)
layer := &gql_generated.LayerSummary{
Size: &layerSize,
Digest: &digest,
}
layers = append(
layers,
layer,
)
allHistory = append(allHistory, &gql_generated.LayerHistory{
Layer: layer,
HistoryDescription: &gql_generated.HistoryDescription{},
})
}
formattedSize := strconv.FormatInt(size, 10)
imageInfo := &gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
Digest: &formattedManifestDigest,
ConfigDigest: &manifest.Config.Digest.Hex,
Size: &formattedSize,
Layers: layers,
History: []*gql_generated.LayerHistory{},
}
return imageInfo
}
// iterator over manifest layers
var layersIterator int
// since we are appending pointers, it is important to iterate with an index over slice
for i := range history {
allHistory = append(allHistory, &gql_generated.LayerHistory{
HistoryDescription: &gql_generated.HistoryDescription{
Created: history[i].Created,
CreatedBy: &history[i].CreatedBy,
Author: &history[i].Author,
Comment: &history[i].Comment,
EmptyLayer: &history[i].EmptyLayer,
},
})
if history[i].EmptyLayer {
continue
}
if layersIterator+1 > len(manifest.Layers) {
formattedSize := strconv.FormatInt(size, 10)
log.Error().Err(ErrBadLayerCount).Msg("error on creating layer history for ImageSummary")
return &gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
Digest: &formattedManifestDigest,
ConfigDigest: &manifest.Config.Digest.Hex,
Size: &formattedSize,
Layers: layers,
History: allHistory,
}
}
size += manifest.Layers[layersIterator].Size
digest := manifest.Layers[layersIterator].Digest.Hex
layerSize := strconv.FormatInt(manifest.Layers[layersIterator].Size, 10)
layer := &gql_generated.LayerSummary{
Size: &layerSize,
Digest: &digest,
}
layers = append(
layers,
layer,
)
allHistory[i].Layer = layer
layersIterator++
}
formattedSize := strconv.FormatInt(size, 10)
imageInfo := &gql_generated.ImageSummary{
RepoName: &repo,
Tag: &tag,
Digest: &formattedManifestDigest,
ConfigDigest: &manifest.Config.Digest.Hex,
Size: &formattedSize,
Layers: layers,
History: allHistory,
}
return imageInfo
}
// returns either a user has or not rights on 'repository'.
func matchesRepo(globPatterns map[string]bool, repository string) bool {
var longestMatchedPattern string
// because of the longest path matching rule, we need to check all patterns from config
for pattern := range globPatterns {
matched, err := glob.Match(pattern, repository)
if err == nil {
if matched && len(pattern) > len(longestMatchedPattern) {
longestMatchedPattern = pattern
}
}
}
allowed := globPatterns[longestMatchedPattern]
return allowed
}
// get passed context from authzHandler and filter out repos based on permissions.
func userAvailableRepos(ctx context.Context, repoList []string) ([]string, error) {
var availableRepos []string
authzCtxKey := localCtx.GetContextKey()
if authCtx := ctx.Value(authzCtxKey); authCtx != nil {
acCtx, ok := authCtx.(localCtx.AccessControlContext)
if !ok {
err := ErrBadCtxFormat
return []string{}, err
}
for _, r := range repoList {
if acCtx.IsAdmin || matchesRepo(acCtx.GlobPatterns, r) {
availableRepos = append(availableRepos, r)
}
}
} else {
availableRepos = repoList
}
return availableRepos, nil
}