0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

feat(cve): implemented trivy image scan for multiarch images (#1510)

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2023-07-06 11:36:26 +03:00 committed by GitHub
parent 96d9d318df
commit 0a04b2a4ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1617 additions and 370 deletions

View file

@ -129,6 +129,7 @@ $(TESTDATA): check-skopeo
skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:7 oci:${TESTDATA}/zot-test:0.0.1; \
skopeo --insecure-policy copy -q docker://public.ecr.aws/t0x7q1g8/centos:8 oci:${TESTDATA}/zot-cve-test:0.0.1; \
skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/java:0.0.1 oci:${TESTDATA}/zot-cve-java-test:0.0.1; \
skopeo --insecure-policy copy -q docker://ghcr.io/project-zot/test-images/alpine:3.17.3 oci:${TESTDATA}/alpine:3.17.3; \
chmod -R a=rwx ${TESTDATA}
.PHONY: run-bench

View file

@ -73,7 +73,7 @@ var (
ErrEmptyRepoName = errors.New("repodb: repo name can't be empty string")
ErrEmptyTag = errors.New("repodb: tag can't be empty string")
ErrEmptyDigest = errors.New("repodb: digest can't be empty string")
ErrInvalidRepoTagFormat = errors.New("invalid format for tag search, not following repo:tag")
ErrInvalidRepoRefFormat = errors.New("invalid image reference format")
ErrLimitIsNegative = errors.New("pageturner: limit has negative value")
ErrOffsetIsNegative = errors.New("pageturner: offset has negative value")
ErrSortCriteriaNotSupported = errors.New("pageturner: the sort criteria is not supported")
@ -96,4 +96,5 @@ var (
ErrSyncPingRegistry = errors.New("sync: unable to ping any registry URLs")
ErrSyncImageNotSigned = errors.New("sync: image is not signed")
ErrSyncImageFilteredOut = errors.New("sync: image is filtered out by sync config")
ErrCallerInfo = errors.New("runtime: failed to get info regarding the current runtime")
)

View file

@ -29,6 +29,7 @@ import (
zotErrors "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
@ -1035,7 +1036,7 @@ func TestServerCVEResponse(t *testing.T) {
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
So(err, ShouldBeNil)
So(strings.TrimSpace(str), ShouldEqual,
So(strings.TrimSpace(str), ShouldResemble,
"IMAGE NAME TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 40d1f749 false 605B")
})
@ -1172,7 +1173,8 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
if image == "zot-cve-test:0.0.1" {
if image == "zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495" ||
image == "zot-cve-test:0.0.1" {
return map[string]cvemodel.CVE{
"CVE-1": {
ID: "CVE-1",
@ -1223,12 +1225,20 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
return false, err
}
manifestDigestStr, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zotErrors.ErrTagMetaNotFound
manifestDigestStr := reference
if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zotErrors.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
}
manifestDigest, err := godigest.Parse(manifestDigestStr.Digest)
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return false, err
}

View file

@ -1891,8 +1891,8 @@ func (service mockService) getTagsForCVEGQL(ctx context.Context, config searchCo
func (service mockService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password,
imageName, cveID string,
) (*common.FixedTags, error) {
fixedTags := &common.FixedTags{
) (*common.ImageListWithCVEFixedResponse, error) {
fixedTags := &common.ImageListWithCVEFixedResponse{
Errors: nil,
ImageListWithCVEFixed: struct {
common.PaginatedImagesResult `json:"ImageListWithCVEFixed"` //nolint:tagliatelle // graphQL schema

View file

@ -43,7 +43,7 @@ type SearchService interface { //nolint:interfacebloat
getTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*common.ImagesForCve, error)
getFixedTagsForCVEGQL(ctx context.Context, config searchConfig, username, password, imageName,
cveID string) (*common.FixedTags, error)
cveID string) (*common.ImageListWithCVEFixedResponse, error)
getDerivedImageListGQL(ctx context.Context, config searchConfig, username, password string,
derivedImage string) (*common.DerivedImageListResponse, error)
getBaseImageListGQL(ctx context.Context, config searchConfig, username, password string,
@ -377,7 +377,7 @@ func (service searchService) getTagsForCVEGQL(ctx context.Context, config search
func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config searchConfig,
username, password, imageName, cveID string,
) (*common.FixedTags, error) {
) (*common.ImageListWithCVEFixedResponse, error) {
query := fmt.Sprintf(`
{
ImageListWithCVEFixed(id: "%s", image: "%s") {
@ -398,7 +398,7 @@ func (service searchService) getFixedTagsForCVEGQL(ctx context.Context, config s
}`,
cveID, imageName)
result := &common.FixedTags{}
result := &common.ImageListWithCVEFixedResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
@ -847,7 +847,7 @@ func (service searchService) getFixedTagsForCVE(ctx context.Context, config sear
}
}`, cvid, imageName)
result := &common.FixedTags{}
result := &common.ImageListWithCVEFixedResponse{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if err != nil {

View file

@ -112,7 +112,7 @@ type Annotation struct {
Value string `json:"value"`
}
type FixedTags struct {
type ImageListWithCVEFixedResponse struct {
Errors []ErrorGQL `json:"errors"`
ImageListWithCVEFixed `json:"data"`
}

View file

@ -4,6 +4,7 @@ import (
"strings"
"time"
"github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors"
@ -101,7 +102,7 @@ func GetRepoRefference(repo string) (string, string, bool, error) {
repoName, tag, found := strings.Cut(repo, ":")
if !found {
return "", "", false, zerr.ErrInvalidRepoTagFormat
return "", "", false, zerr.ErrInvalidRepoRefFormat
}
return repoName, tag, true, nil
@ -109,3 +110,22 @@ func GetRepoRefference(repo string) (string, string, bool, error) {
return repoName, digest, false, nil
}
// GetFullImageName returns the formated string for the given repo/tag or repo/digest.
func GetFullImageName(repo, ref string) string {
if IsTag(ref) {
return repo + ":" + ref
}
return repo + "@" + ref
}
func IsDigest(ref string) bool {
_, err := digest.Parse(ref)
return err == nil
}
func IsTag(ref string) bool {
return !IsDigest(ref)
}

View file

@ -13,7 +13,7 @@ import (
. "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/extensions/search/convert"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt"
@ -74,9 +74,9 @@ func TestConvertErrors(t *testing.T) {
map[string]repodb.IndexData{},
convert.SkipQGLField{},
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo string, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
GetCVESummaryForImageMediaFn: func(repo string, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) {
return cvemodel.ImageCVESummary{}, ErrTestError
},
},
)
@ -120,9 +120,8 @@ func TestConvertErrors(t *testing.T) {
},
map[string]repodb.ManifestMetadata{},
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
return cvemodel.ImageCVESummary{}, ErrTestError
},
},
)
@ -153,9 +152,8 @@ func TestConvertErrors(t *testing.T) {
ConfigBlob: configBlob,
},
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
return cvemodel.ImageCVESummary{}, ErrTestError
},
},
)
@ -187,12 +185,7 @@ func TestConvertErrors(t *testing.T) {
ConfigBlob: []byte("bad json"),
},
nil,
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
},
},
mocks.CveInfoMock{},
)
So(err, ShouldNotBeNil)
@ -227,9 +220,8 @@ func TestConvertErrors(t *testing.T) {
},
nil,
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
return cvemodel.ImageCVESummary{}, ErrTestError
},
},
)
@ -259,9 +251,8 @@ func TestConvertErrors(t *testing.T) {
Vulnerabilities: false,
},
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
return cvemodel.ImageCVESummary{}, ErrTestError
},
}, log.NewLogger("debug", ""),
)
@ -286,9 +277,8 @@ func TestConvertErrors(t *testing.T) {
Vulnerabilities: false,
},
mocks.CveInfoMock{
GetCVESummaryForImageFn: func(repo, reference string,
) (cveinfo.ImageCVESummary, error) {
return cveinfo.ImageCVESummary{}, ErrTestError
GetCVESummaryForImageMediaFn: func(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) {
return cvemodel.ImageCVESummary{}, ErrTestError
},
}, log.NewLogger("debug", ""),
)

View file

@ -103,7 +103,8 @@ func RepoMeta2RepoSummary(ctx context.Context, repoMeta repodb.RepoMetadata,
// We only scan the latest image on the repo for performance reasons
// Check if vulnerability scanning is disabled
if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities {
imageCveSummary, err := cveInfo.GetCVESummaryForImage(repoMeta.Name, *lastUpdatedImageSummary.Tag)
imageCveSummary, err := cveInfo.GetCVESummaryForImageMedia(repoMeta.Name, *lastUpdatedImageSummary.Digest,
*lastUpdatedImageSummary.MediaType)
if err != nil {
// Log the error, but we should still include the image in results
graphql.AddError(
@ -227,10 +228,10 @@ func ImageIndex2ImageSummary(ctx context.Context, repo, tag string, indexDigest
}
}
imageCveSummary := cveinfo.ImageCVESummary{}
imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag)
imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, indexDigestStr, ispec.MediaTypeImageIndex)
if err != nil {
// Log the error, but we should still include the manifest in results
@ -345,10 +346,10 @@ func ImageManifest2ImageSummary(ctx context.Context, repo, tag string, digest go
"manifest digest: %s, error: %s", tag, repo, manifestDigest, err.Error()))
}
imageCveSummary := cveinfo.ImageCVESummary{}
imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag)
imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigest, ispec.MediaTypeImageManifest)
if err != nil {
// Log the error, but we should still include the manifest in results
@ -500,10 +501,10 @@ func ImageManifest2ManifestSummary(ctx context.Context, repo, tag string, descri
"manifest digest: %s, error: %s", tag, repo, manifestDigestStr, err.Error()))
}
imageCveSummary := cveinfo.ImageCVESummary{}
imageCveSummary := cvemodel.ImageCVESummary{}
if cveInfo != nil && !skipCVE {
imageCveSummary, err = cveInfo.GetCVESummaryForImage(repo, tag)
imageCveSummary, err = cveInfo.GetCVESummaryForImageMedia(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
if err != nil {
// Log the error, but we should still include the manifest in results
@ -662,7 +663,8 @@ func RepoMeta2ExpandedRepoInfo(ctx context.Context, repoMeta repodb.RepoMetadata
// We only scan the latest image on the repo for performance reasons
// Check if vulnerability scanning is disabled
if cveInfo != nil && lastUpdatedImageSummary != nil && !skip.Vulnerabilities {
imageCveSummary, err := cveInfo.GetCVESummaryForImage(repoMeta.Name, *lastUpdatedImageSummary.Tag)
imageCveSummary, err := cveInfo.GetCVESummaryForImageMedia(repoMeta.Name, *lastUpdatedImageSummary.Digest,
*lastUpdatedImageSummary.MediaType)
if err != nil {
// Log the error, but we should still include the image in results
graphql.AddError(

View file

@ -2,15 +2,14 @@ package cveinfo
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common"
zcommon "zotregistry.io/zot/pkg/common"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
"zotregistry.io/zot/pkg/log"
@ -21,24 +20,22 @@ import (
type CveInfo interface {
GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error)
GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, error)
GetCVEListForImage(repo, tag string, searchedCVE string, pageinput PageInput) ([]cvemodel.CVE, common.PageInfo, error)
GetCVESummaryForImage(repo, tag string) (ImageCVESummary, error)
GetCVEListForImage(repo, tag string, searchedCVE string, pageinput cvemodel.PageInput,
) ([]cvemodel.CVE, zcommon.PageInfo, error)
GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error)
GetCVESummaryForImageMedia(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error)
CompareSeverities(severity1, severity2 string) int
UpdateDB() error
}
type Scanner interface {
ScanImage(image string) (map[string]cvemodel.CVE, error)
IsImageFormatScannable(repo, tag string) (bool, error)
IsImageFormatScannable(repo, ref string) (bool, error)
IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error)
CompareSeverities(severity1, severity2 string) int
UpdateDB() error
}
type ImageCVESummary struct {
Count int
MaxSeverity string
}
type BaseCveInfo struct {
Log log.Logger
Scanner Scanner
@ -70,19 +67,19 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta
for tag, descriptor := range repoMeta.Tags {
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
case ispec.MediaTypeImageManifest, ispec.MediaTypeImageIndex:
manifestDigestStr := descriptor.Digest
manifestDigest := godigest.Digest(manifestDigestStr)
isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag)
isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr)
if !isScanableImage || err != nil {
cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable")
continue
}
cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag))
cveMap, err := cveinfo.Scanner.ScanImage(zcommon.GetFullImageName(repo, tag))
if err != nil {
cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed")
@ -91,7 +88,7 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta
if _, hasCVE := cveMap[cveID]; hasCVE {
imgList = append(imgList, cvemodel.TagInfo{
Name: tag,
Tag: tag,
Descriptor: cvemodel.Descriptor{
Digest: manifestDigest,
MediaType: descriptor.MediaType,
@ -118,87 +115,81 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
vulnerableTags := make([]cvemodel.TagInfo, 0)
allTags := make([]cvemodel.TagInfo, 0)
var hasCVE bool
for tag, descriptor := range repoMeta.Tags {
manifestDigestStr := descriptor.Digest
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
manifestDigest, err := godigest.Parse(manifestDigestStr)
manifestDigestStr := descriptor.Digest
tagInfo, err := getTagInfoForManifest(tag, manifestDigestStr, cveinfo.RepoDB)
if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Str("digest", manifestDigestStr).Msg("unable to parse digest")
Str("cve-id", cveID).Msg("unable to retrieve manifest and config")
continue
}
manifestMeta, err := cveinfo.RepoDB.GetManifestMeta(repo, manifestDigest)
if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Msg("unable to obtain manifest meta")
continue
}
var configContent ispec.Image
err = json.Unmarshal(manifestMeta.ConfigBlob, &configContent)
if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Msg("unable to unmashal manifest blob")
continue
}
tagInfo := cvemodel.TagInfo{
Name: tag,
Timestamp: common.GetImageLastUpdated(configContent),
Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: descriptor.MediaType},
}
allTags = append(allTags, tagInfo)
image := fmt.Sprintf("%s:%s", repo, tag)
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag)
if !isValidImage || err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("image media type not supported for scanning, adding as a vulnerable image")
if cveinfo.isManifestVulnerable(repo, tag, manifestDigestStr, cveID) {
vulnerableTags = append(vulnerableTags, tagInfo)
continue
}
case ispec.MediaTypeImageIndex:
indexDigestStr := descriptor.Digest
cveMap, err := cveinfo.Scanner.ScanImage(getImageString(repo, tag))
indexContent, err := getIndexContent(cveinfo.RepoDB, indexDigestStr)
if err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("scanning failed, adding as a vulnerable image")
vulnerableTags = append(vulnerableTags, tagInfo)
continue
}
hasCVE = false
vulnerableManifests := []cvemodel.DescriptorInfo{}
allManifests := []cvemodel.DescriptorInfo{}
for id := range cveMap {
if id == cveID {
hasCVE = true
for _, manifest := range indexContent.Manifests {
tagInfo, err := getTagInfoForManifest(tag, manifest.Digest.String(), cveinfo.RepoDB)
if err != nil {
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
Str("cve-id", cveID).Msg("unable to retrieve manifest and config")
break
continue
}
manifestDescriptorInfo := cvemodel.DescriptorInfo{
Descriptor: tagInfo.Descriptor,
Timestamp: tagInfo.Timestamp,
}
allManifests = append(allManifests, manifestDescriptorInfo)
if cveinfo.isManifestVulnerable(repo, tag, manifest.Digest.String(), cveID) {
vulnerableManifests = append(vulnerableManifests, manifestDescriptorInfo)
}
}
if hasCVE {
vulnerableTags = append(vulnerableTags, tagInfo)
if len(allManifests) > 0 {
allTags = append(allTags, cvemodel.TagInfo{
Tag: tag,
Descriptor: cvemodel.Descriptor{
Digest: godigest.Digest(indexDigestStr),
MediaType: ispec.MediaTypeImageIndex,
},
Manifests: allManifests,
Timestamp: mostRecentUpdate(allManifests),
})
}
if len(vulnerableManifests) > 0 {
vulnerableTags = append(vulnerableTags, cvemodel.TagInfo{
Tag: tag,
Descriptor: cvemodel.Descriptor{
Digest: godigest.Digest(indexDigestStr),
MediaType: ispec.MediaTypeImageIndex,
},
Manifests: vulnerableManifests,
Timestamp: mostRecentUpdate(vulnerableManifests),
})
}
default:
cveinfo.Log.Error().Str("mediaType", descriptor.MediaType).Msg("media type not supported")
return []cvemodel.TagInfo{},
fmt.Errorf("media type '%s' is not supported: %w", descriptor.MediaType, errors.ErrNotImplemented)
}
}
@ -219,6 +210,117 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
return fixedTags, nil
}
func mostRecentUpdate(allManifests []cvemodel.DescriptorInfo) time.Time {
if len(allManifests) == 0 {
return time.Time{}
}
timeStamp := allManifests[0].Timestamp
for i := range allManifests {
if timeStamp.Before(allManifests[i].Timestamp) {
timeStamp = allManifests[i].Timestamp
}
}
return timeStamp
}
func getTagInfoForManifest(tag, manifestDigestStr string, repoDB repodb.RepoDB) (cvemodel.TagInfo, error) {
configContent, manifestDigest, err := getConfigAndDigest(repoDB, manifestDigestStr)
if err != nil {
return cvemodel.TagInfo{}, err
}
lastUpdated := zcommon.GetImageLastUpdated(configContent)
return cvemodel.TagInfo{
Tag: tag,
Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
Manifests: []cvemodel.DescriptorInfo{
{
Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
Timestamp: lastUpdated,
},
},
Timestamp: lastUpdated,
}, nil
}
func (cveinfo *BaseCveInfo) isManifestVulnerable(repo, tag, manifestDigestStr, cveID string) bool {
image := zcommon.GetFullImageName(repo, tag)
isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
if !isValidImage || err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("image media type not supported for scanning, adding as a vulnerable image")
return true
}
cveMap, err := cveinfo.Scanner.ScanImage(zcommon.GetFullImageName(repo, manifestDigestStr))
if err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
Msg("scanning failed, adding as a vulnerable image")
return true
}
hasCVE := false
for id := range cveMap {
if id == cveID {
hasCVE = true
break
}
}
return hasCVE
}
func getIndexContent(repoDB repodb.RepoDB, indexDigestStr string) (ispec.Index, error) {
indexDigest, err := godigest.Parse(indexDigestStr)
if err != nil {
return ispec.Index{}, err
}
indexData, err := repoDB.GetIndexData(indexDigest)
if err != nil {
return ispec.Index{}, err
}
var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return ispec.Index{}, err
}
return indexContent, nil
}
func getConfigAndDigest(repoDB repodb.RepoDB, manifestDigestStr string) (ispec.Image, godigest.Digest, error) {
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return ispec.Image{}, "", err
}
manifestData, err := repoDB.GetManifestData(manifestDigest)
if err != nil {
return ispec.Image{}, "", err
}
var configContent ispec.Image
err = json.Unmarshal(manifestData.ConfigBlob, &configContent)
if err != nil {
return ispec.Image{}, "", err
}
return configContent, manifestDigest, nil
}
func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) {
searchedCVE = strings.ToUpper(searchedCVE)
@ -230,26 +332,26 @@ func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinde
}
}
func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, searchedCVE string, pageInput PageInput) (
func (cveinfo BaseCveInfo) GetCVEListForImage(repo, ref string, searchedCVE string, pageInput cvemodel.PageInput) (
[]cvemodel.CVE,
common.PageInfo,
zcommon.PageInfo,
error,
) {
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag)
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
if !isValidImage {
return []cvemodel.CVE{}, common.PageInfo{}, err
return []cvemodel.CVE{}, zcommon.PageInfo{}, err
}
image := getImageString(repo, tag)
image := zcommon.GetFullImageName(repo, ref)
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
return []cvemodel.CVE{}, common.PageInfo{}, err
return []cvemodel.CVE{}, zcommon.PageInfo{}, err
}
pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy, cveinfo)
if err != nil {
return []cvemodel.CVE{}, common.PageInfo{}, err
return []cvemodel.CVE{}, zcommon.PageInfo{}, err
}
filterCVEList(cveMap, searchedCVE, pageFinder)
@ -259,23 +361,22 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(repo, tag string, searchedCVE stri
return cveList, pageInfo, nil
}
func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string,
) (ImageCVESummary, error) {
func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error) {
// There are several cases, expected returned values below:
// not scannable / error during scan - max severity "" - cve count 0 - Errors
// scannable no issues found - max severity "NONE" - cve count 0 - no Errors
// scannable issues found - max severity from Scanner - cve count >0 - no Errors
imageCVESummary := ImageCVESummary{
imageCVESummary := cvemodel.ImageCVESummary{
Count: 0,
MaxSeverity: "",
}
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, tag)
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
if !isValidImage {
return imageCVESummary, err
}
image := getImageString(repo, tag)
image := zcommon.GetFullImageName(repo, ref)
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
@ -300,20 +401,41 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, tag string,
return imageCVESummary, nil
}
func referenceIsDigest(reference string) bool {
_, err := godigest.Parse(reference)
return err == nil
}
func getImageString(repo, reference string) string {
image := repo + ":" + reference
if referenceIsDigest(reference) {
image = repo + "@" + reference
func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) {
imageCVESummary := cvemodel.ImageCVESummary{
Count: 0,
MaxSeverity: "",
}
return image
isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType)
if !isValidImage {
return imageCVESummary, err
}
image := repo + "@" + digest
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
return imageCVESummary, err
}
imageCVESummary.Count = len(cveMap)
if imageCVESummary.Count == 0 {
imageCVESummary.MaxSeverity = "NONE"
return imageCVESummary, nil
}
imageCVESummary.MaxSeverity = "UNKNOWN"
for _, cve := range cveMap {
if cveinfo.Scanner.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
imageCVESummary.MaxSeverity = cve.Severity
}
}
return imageCVESummary, nil
}
func (cveinfo BaseCveInfo) UpdateDB() error {
@ -333,10 +455,21 @@ func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo
vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags))
for _, tag := range vulnerableTags {
vulnerableTagMap[tag.Name] = tag
vulnerableTagMap[tag.Tag] = tag
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
earliestVulnerable = tag
switch tag.Descriptor.MediaType {
case ispec.MediaTypeImageManifest:
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
earliestVulnerable = tag
}
case ispec.MediaTypeImageIndex:
for _, manifestDesc := range tag.Manifests {
if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) {
earliestVulnerable = tag
}
}
default:
continue
}
}
@ -348,18 +481,62 @@ func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo
// There may be older images which have a fix or
// newer images which don't
for _, tag := range allTags {
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
// The vulnerability did not exist at the time this
// image was built
switch tag.Descriptor.MediaType {
case ispec.MediaTypeImageManifest:
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
// The vulnerability did not exist at the time this
// image was built
continue
}
// If the image is old enough for the vulnerability to
// exist, but it was not detected, it means it contains
// the fix
if _, ok := vulnerableTagMap[tag.Tag]; !ok {
fixedTags = append(fixedTags, tag)
}
case ispec.MediaTypeImageIndex:
fixedManifests := []cvemodel.DescriptorInfo{}
// If the latest update inside the index is before the earliest vulnerability found then
// the index can't contain a fix
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
continue
}
vulnTagInfo, indexHasVulnerableManifest := vulnerableTagMap[tag.Tag]
for _, manifestDesc := range tag.Manifests {
if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) {
// The vulnerability did not exist at the time this image was built
continue
}
// check if the current manifest doesn't have the vulnerability
if !indexHasVulnerableManifest || !containsDescriptorInfo(vulnTagInfo.Manifests, manifestDesc) {
fixedManifests = append(fixedManifests, manifestDesc)
}
}
if len(fixedManifests) > 0 {
fixedTag := tag
fixedTag.Manifests = fixedManifests
fixedTags = append(fixedTags, fixedTag)
}
default:
continue
}
// If the image is old enough for the vulnerability to
// exist, but it was not detected, it means it contains
// the fix
if _, ok := vulnerableTagMap[tag.Name]; !ok {
fixedTags = append(fixedTags, tag)
}
}
return fixedTags
}
func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvemodel.DescriptorInfo) bool {
for _, di := range slice {
if di.Digest == descriptorInfo.Digest {
return true
}
}
return false
}

View file

@ -0,0 +1,65 @@
package cveinfo
import (
"testing"
"time"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
)
func TestUtils(t *testing.T) {
Convey("Utils", t, func() {
Convey("mostRecentUpdate", func() {
// empty
timestamp := mostRecentUpdate([]cvemodel.DescriptorInfo{})
So(timestamp, ShouldResemble, time.Time{})
timestamp = mostRecentUpdate([]cvemodel.DescriptorInfo{
{
Timestamp: time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
Timestamp: time.Date(2005, 1, 1, 1, 1, 1, 1, time.UTC),
},
})
So(timestamp, ShouldResemble, time.Date(2005, 1, 1, 1, 1, 1, 1, time.UTC))
})
Convey("GetFixedTags", func() {
tags := GetFixedTags(
[]cvemodel.TagInfo{
{},
},
[]cvemodel.TagInfo{
{
Descriptor: cvemodel.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
},
Timestamp: time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
Descriptor: cvemodel.Descriptor{
MediaType: ispec.MediaTypeImageIndex,
},
Manifests: []cvemodel.DescriptorInfo{
{
Timestamp: time.Date(2002, 1, 1, 1, 1, 1, 1, time.UTC),
},
{
Timestamp: time.Date(2000, 1, 1, 1, 1, 1, 1, time.UTC),
},
},
},
{
Descriptor: cvemodel.Descriptor{
MediaType: "bad Type",
},
},
})
So(tags, ShouldBeEmpty)
})
})
}

View file

@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
@ -25,10 +26,12 @@ import (
"zotregistry.io/zot/pkg/api/config"
"zotregistry.io/zot/pkg/api/constants"
apiErr "zotregistry.io/zot/pkg/api/errors"
zcommon "zotregistry.io/zot/pkg/common"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb"
@ -382,10 +385,16 @@ func TestImageFormat(t *testing.T) {
GetRepoMetaFn: func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {MediaType: ispec.MediaTypeImageIndex},
"tag": {
MediaType: ispec.MediaTypeImageIndex,
Digest: godigest.FromString("digest").String(),
},
},
}, nil
},
GetIndexDataFn: func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`{}`)}, nil
},
}
storeController := storage.StoreController{
DefaultStore: mocks.MockedImageStore{},
@ -395,7 +404,7 @@ func TestImageFormat(t *testing.T) {
isScanable, err := cveInfo.Scanner.IsImageFormatScannable("repo", "tag")
So(err, ShouldBeNil)
So(isScanable, ShouldBeFalse)
So(isScanable, ShouldBeTrue)
})
}
@ -1024,8 +1033,11 @@ func TestCVEStruct(t *testing.T) {
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
repo1 := "repo1"
repo, ref, _ := zcommon.GetImageDirAndReference(image)
// Images in chronological order
if image == "repo1:0.1.0" {
if image == "repo1:0.1.0" || ref == digest11.String() {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
@ -1036,7 +1048,8 @@ func TestCVEStruct(t *testing.T) {
}, nil
}
if image == "repo1:1.0.0" {
if image == "repo1:1.0.0" || (repo == repo1 &&
zcommon.Contains([]string{digest12.String(), digest21.String()}, ref)) {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
@ -1059,7 +1072,7 @@ func TestCVEStruct(t *testing.T) {
}, nil
}
if image == "repo1:1.1.0" {
if image == "repo1:1.1.0" || (repo == repo1 && ref == digest13.String()) {
return map[string]cvemodel.CVE{
"CVE3": {
ID: "CVE3",
@ -1072,7 +1085,7 @@ func TestCVEStruct(t *testing.T) {
// As a minor release on 1.0.0 banch
// does not include all fixes published in 1.1.0
if image == "repo1:1.0.1" {
if image == "repo1:1.0.1" || (repo == repo1 && ref == digest14.String()) {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
@ -1089,7 +1102,7 @@ func TestCVEStruct(t *testing.T) {
}, nil
}
if image == "repoIndex:tagIndex" {
if image == "repoIndex:tagIndex" || (repo == "repoIndex" && ref == indexDigest.String()) {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
@ -1119,12 +1132,20 @@ func TestCVEStruct(t *testing.T) {
return false, err
}
manifestDigestStr, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
manifestDigestStr := reference
if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
}
manifestDigest, err := godigest.Parse(manifestDigestStr.Digest)
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return false, err
}
@ -1154,6 +1175,15 @@ func TestCVEStruct(t *testing.T) {
return false, nil
},
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
if repo == "repo2" {
if digest == digest21.String() {
return false, nil
}
}
return true, nil
},
}
log := log.NewLogger("debug", "")
@ -1213,7 +1243,7 @@ func TestCVEStruct(t *testing.T) {
t.Log("Test GetCVEListForImage")
pageInput := cveinfo.PageInput{
pageInput := cvemodel.PageInput{
SortBy: cveinfo.SeverityDsc,
}
@ -1289,14 +1319,14 @@ func TestCVEStruct(t *testing.T) {
tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1")
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 1)
So(tagList[0].Name, ShouldEqual, "1.1.0")
So(tagList[0].Tag, ShouldEqual, "1.1.0")
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE2")
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 2)
expectedTags := []string{"1.0.1", "1.1.0"}
So(expectedTags, ShouldContain, tagList[0].Name)
So(expectedTags, ShouldContain, tagList[1].Name)
So(expectedTags, ShouldContain, tagList[0].Tag)
So(expectedTags, ShouldContain, tagList[1].Tag)
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE3")
So(err, ShouldBeNil)
@ -1309,7 +1339,7 @@ func TestCVEStruct(t *testing.T) {
tagList, err = cveInfo.GetImageListWithCVEFixed("repo6", "CVE1")
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 1)
So(tagList[0].Name, ShouldEqual, "1.0.0")
So(tagList[0].Tag, ShouldEqual, "1.0.0")
// Image is not scannable
tagList, err = cveInfo.GetImageListWithCVEFixed("repo2", "CVE100")
@ -1341,22 +1371,22 @@ func TestCVEStruct(t *testing.T) {
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 3)
expectedTags = []string{"0.1.0", "1.0.0", "1.0.1"}
So(expectedTags, ShouldContain, tagList[0].Name)
So(expectedTags, ShouldContain, tagList[1].Name)
So(expectedTags, ShouldContain, tagList[2].Name)
So(expectedTags, ShouldContain, tagList[0].Tag)
So(expectedTags, ShouldContain, tagList[1].Tag)
So(expectedTags, ShouldContain, tagList[2].Tag)
tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE2")
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 1)
So(tagList[0].Name, ShouldEqual, "1.0.0")
So(tagList[0].Tag, ShouldEqual, "1.0.0")
tagList, err = cveInfo.GetImageListForCVE("repo1", "CVE3")
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 3)
expectedTags = []string{"1.0.0", "1.0.1", "1.1.0"}
So(expectedTags, ShouldContain, tagList[0].Name)
So(expectedTags, ShouldContain, tagList[1].Name)
So(expectedTags, ShouldContain, tagList[2].Name)
So(expectedTags, ShouldContain, tagList[0].Tag)
So(expectedTags, ShouldContain, tagList[1].Tag)
So(expectedTags, ShouldContain, tagList[2].Tag)
// Image/repo doesn't have the CVE at all
tagList, err = cveInfo.GetImageListForCVE("repo6", "CVE1")
@ -1419,7 +1449,7 @@ func TestCVEStruct(t *testing.T) {
tagList, err = cveInfo.GetImageListForCVE("repoIndex", "CVE1")
So(err, ShouldBeNil)
So(len(tagList), ShouldEqual, 0)
So(len(tagList), ShouldEqual, 1)
cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{
IsImageFormatScannableFn: func(repo, reference string) (bool, error) {
@ -1448,7 +1478,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
tags := make([]cvemodel.TagInfo, 0)
firstTag := cvemodel.TagInfo{
Name: "1.0.0",
Tag: "1.0.0",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -1456,7 +1486,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
Timestamp: time.Now(),
}
secondTag := cvemodel.TagInfo{
Name: "1.0.1",
Tag: "1.0.1",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -1464,7 +1494,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
Timestamp: time.Now(),
}
thirdTag := cvemodel.TagInfo{
Name: "1.0.2",
Tag: "1.0.2",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -1472,7 +1502,7 @@ func getTags() ([]cvemodel.TagInfo, []cvemodel.TagInfo) {
Timestamp: time.Now(),
}
fourthTag := cvemodel.TagInfo{
Name: "1.0.3",
Tag: "1.0.3",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -1496,10 +1526,298 @@ func TestFixedTags(t *testing.T) {
So(len(fixedTags), ShouldEqual, 2)
fixedTags = cveinfo.GetFixedTags(allTags, append(vulnerableTags, cvemodel.TagInfo{
Name: "taginfo",
Descriptor: cvemodel.Descriptor{},
Timestamp: time.Date(2000, time.July, 20, 10, 10, 10, 10, time.UTC),
Tag: "taginfo",
Descriptor: cvemodel.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
},
Timestamp: time.Date(2000, time.July, 20, 10, 10, 10, 10, time.UTC),
}))
So(len(fixedTags), ShouldEqual, 3)
})
}
func TestFixedTagsWithIndex(t *testing.T) {
Convey("Test fixed tags", t, func() {
tempDir := t.TempDir()
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Storage.RootDirectory = tempDir
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
CVE: &extconf.CVEConfig{
UpdateInterval: 24 * time.Hour,
Trivy: &extconf.TrivyConfig{},
},
},
}
ctlr := api.NewController(conf)
So(ctlr, ShouldNotBeNil)
cm := NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
// push index with 2 manifests: one with vulns and one without
vulnManifestCreated := time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)
vulnManifest, err := GetVulnImageWithConfig("", ispec.Image{
Created: &vulnManifestCreated,
Platform: ispec.Platform{OS: "linux", Architecture: "amd64"},
})
So(err, ShouldBeNil)
vulnDigest, err := vulnManifest.Digest()
So(err, ShouldBeNil)
fixedManifestCreated := time.Date(2010, 1, 1, 1, 1, 1, 1, time.UTC)
fixedManifest, err := GetImageWithConfig(ispec.Image{
Created: &fixedManifestCreated,
Platform: ispec.Platform{OS: "windows", Architecture: "amd64"},
})
So(err, ShouldBeNil)
fixedDigest, err := fixedManifest.Digest()
So(err, ShouldBeNil)
multiArch := GetMultiarchImageForImages("multi-arch-tag", []Image{fixedManifest, vulnManifest})
multiArchDigest, err := multiArch.Digest()
So(err, ShouldBeNil)
err = UploadMultiarchImage(multiArch, baseURL, "repo")
So(err, ShouldBeNil)
// oldest vulnerability
simpleVulnCreated := time.Date(2005, 1, 1, 1, 1, 1, 1, time.UTC)
simpleVulnImg, err := GetVulnImageWithConfig("vuln-img", ispec.Image{
Created: &simpleVulnCreated,
Platform: ispec.Platform{OS: "windows", Architecture: "amd64"},
})
So(err, ShouldBeNil)
err = UploadImage(simpleVulnImg, baseURL, "repo")
So(err, ShouldBeNil)
scanner := trivy.NewScanner(ctlr.StoreController, ctlr.RepoDB, "ghcr.io/project-zot/trivy-db", "", ctlr.Log)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
cveInfo := cveinfo.NewCVEInfo(ctlr.StoreController, ctlr.RepoDB, "ghcr.io/project-zot/trivy-db", "", ctlr.Log)
tagsInfo, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
So(len(tagsInfo), ShouldEqual, 1)
So(len(tagsInfo[0].Manifests), ShouldEqual, 1)
So(tagsInfo[0].Manifests[0].Digest, ShouldResemble, fixedDigest)
_ = tagsInfo
_ = vulnDigest
_ = multiArchDigest
const query = `
{
ImageListWithCVEFixed(id:"%s",image:"%s"){
Results{
RepoName
Manifests {Digest}
}
}
}`
resp, _ := resty.R().Get(baseURL + constants.FullSearchPrefix + "?query=" +
url.QueryEscape(fmt.Sprintf(query, Vulnerability1ID, "repo")))
So(resp, ShouldNotBeNil)
So(resp.StatusCode(), ShouldEqual, 200)
responseStruct := &zcommon.ImageListWithCVEFixedResponse{}
err = json.Unmarshal(resp.Body(), &responseStruct)
So(err, ShouldBeNil)
So(len(responseStruct.Results), ShouldEqual, 1)
So(len(responseStruct.Results[0].Manifests), ShouldEqual, 1)
fixedManifestResp := responseStruct.Results[0].Manifests[0]
So(fixedManifestResp.Digest, ShouldResemble, fixedDigest.String())
})
}
func TestImageListWithCVEFixedErrors(t *testing.T) {
indexDigest := godigest.FromString("index")
manifestDigest := "sha256:1111111111111111111111111111111111111111111111111111111111111111"
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("getIndexContent errors", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: indexDigest.String(),
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrIndexDataNotFount
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getIndexContent bad indexDigest", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: "bad digest",
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrIndexDataNotFount
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getIndexContent bad index content", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: indexDigest.String(),
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`bad index`)}, nil
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getTagInfoForManifest bad manifest digest", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: "bad digest",
MediaType: ispec.MediaTypeImageManifest,
},
},
}, nil
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
_, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
})
Convey("getTagInfoForManifest fails for index", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: indexDigest.String(),
MediaType: ispec.MediaTypeImageIndex,
},
},
}, nil
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{
IndexBlob: []byte(fmt.Sprintf(`{
"manifests": [
{
"digest": "%s",
"mediaType": "application/vnd.oci.image.manifest.v1+json"
}
]}`, manifestDigest)),
}, nil
}
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
return repodb.ManifestData{}, zerr.ErrManifestDataNotFound
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
tagsInfo, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
So(tagsInfo, ShouldBeEmpty)
})
Convey("media type not supported", func() {
repoDB.GetRepoMetaFn = func(repo string) (repodb.RepoMetadata, error) {
return repodb.RepoMetadata{
Tags: map[string]repodb.Descriptor{
"tag": {
Digest: godigest.FromString("media type").String(),
MediaType: "bad media type",
},
},
}, nil
}
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
tagsInfo, err := cveInfo.GetImageListWithCVEFixed("repo", Vulnerability1ID)
So(err, ShouldBeNil)
So(tagsInfo, ShouldBeEmpty)
})
})
}
func TestGetCVESummaryForImageMediaErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("IsImageMediaScannable returns false", func() {
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
cveInfo.Scanner = mocks.CveScannerMock{
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
return false, zerr.ErrScanNotSupported
},
}
_, err := cveInfo.GetCVESummaryForImageMedia("repo", "digest", ispec.MediaTypeImageManifest)
So(err, ShouldNotBeNil)
})
Convey("Scan fails", func() {
cveInfo := cveinfo.NewCVEInfo(storeController, repoDB, "", "", log)
cveInfo.Scanner = mocks.CveScannerMock{
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
return true, nil
},
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
return nil, zerr.ErrScanNotSupported
},
}
_, err := cveInfo.GetCVESummaryForImageMedia("repo", "digest", ispec.MediaTypeImageManifest)
So(err, ShouldNotBeNil)
})
})
}

View file

@ -6,6 +6,11 @@ import (
godigest "github.com/opencontainers/go-digest"
)
type ImageCVESummary struct {
Count int
MaxSeverity string
}
//nolint:tagliatelle // graphQL schema
type CVE struct {
ID string `json:"Id"`
@ -47,8 +52,15 @@ type Descriptor struct {
MediaType string
}
type DescriptorInfo struct {
Descriptor
Timestamp time.Time
}
type TagInfo struct {
Name string
Tag string
Descriptor Descriptor
Manifests []DescriptorInfo
Timestamp time.Time
}

View file

@ -0,0 +1,9 @@
package model
type SortCriteria string
type PageInput struct {
Limit int
Offset int
SortBy SortCriteria
}

View file

@ -9,16 +9,14 @@ import (
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
)
type SortCriteria string
const (
AlphabeticAsc = SortCriteria("ALPHABETIC_ASC")
AlphabeticDsc = SortCriteria("ALPHABETIC_DSC")
SeverityDsc = SortCriteria("SEVERITY")
AlphabeticAsc = cvemodel.SortCriteria("ALPHABETIC_ASC")
AlphabeticDsc = cvemodel.SortCriteria("ALPHABETIC_DSC")
SeverityDsc = cvemodel.SortCriteria("SEVERITY")
)
func SortFunctions() map[SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return map[SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool{
func SortFunctions() map[cvemodel.SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return map[cvemodel.SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool{
AlphabeticAsc: SortByAlphabeticAsc,
AlphabeticDsc: SortByAlphabeticDsc,
SeverityDsc: SortBySeverity,
@ -56,12 +54,12 @@ type PageFinder interface {
type CvePageFinder struct {
limit int
offset int
sortBy SortCriteria
sortBy cvemodel.SortCriteria
pageBuffer []cvemodel.CVE
cveInfo CveInfo
}
func NewCvePageFinder(limit, offset int, sortBy SortCriteria, cveInfo CveInfo) (*CvePageFinder, error) {
func NewCvePageFinder(limit, offset int, sortBy cvemodel.SortCriteria, cveInfo CveInfo) (*CvePageFinder, error) {
if sortBy == "" {
sortBy = SeverityDsc
}
@ -131,9 +129,3 @@ func (bpt *CvePageFinder) Page() ([]cvemodel.CVE, common.PageInfo) {
return cves, *pageInfo
}
type PageInput struct {
Limit int
Offset int
SortBy SortCriteria
}

View file

@ -187,7 +187,7 @@ func TestCVEPagination(t *testing.T) {
Convey("Page", func() {
Convey("defaults", func() {
// By default expect unlimitted results sorted by severity
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{})
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5)
@ -198,7 +198,7 @@ func TestCVEPagination(t *testing.T) {
previousSeverity = severityToInt[cve.Severity]
}
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cveinfo.PageInput{})
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cvemodel.PageInput{})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
@ -217,7 +217,7 @@ func TestCVEPagination(t *testing.T) {
}
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "",
cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc})
cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5)
@ -228,7 +228,7 @@ func TestCVEPagination(t *testing.T) {
sort.Strings(cveIds)
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "",
cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc})
cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
@ -239,7 +239,7 @@ func TestCVEPagination(t *testing.T) {
sort.Sort(sort.Reverse(sort.StringSlice(cveIds)))
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "",
cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc})
cvemodel.PageInput{SortBy: cveinfo.AlphabeticDsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
@ -249,7 +249,7 @@ func TestCVEPagination(t *testing.T) {
}
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "",
cveinfo.PageInput{SortBy: cveinfo.SeverityDsc})
cvemodel.PageInput{SortBy: cveinfo.SeverityDsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
@ -267,7 +267,7 @@ func TestCVEPagination(t *testing.T) {
cveIds = append(cveIds, fmt.Sprintf("CVE%d", i))
}
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 3,
Offset: 1,
SortBy: cveinfo.AlphabeticAsc,
@ -281,7 +281,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[1].ID, ShouldEqual, "CVE2")
So(cves[2].ID, ShouldEqual, "CVE3")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 2,
Offset: 1,
SortBy: cveinfo.AlphabeticDsc,
@ -294,7 +294,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[0].ID, ShouldEqual, "CVE3")
So(cves[1].ID, ShouldEqual, "CVE2")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 3,
Offset: 1,
SortBy: cveinfo.SeverityDsc,
@ -311,7 +311,7 @@ func TestCVEPagination(t *testing.T) {
}
sort.Strings(cveIds)
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cveinfo.PageInput{
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "1.0.0", "", cvemodel.PageInput{
Limit: 5,
Offset: 20,
SortBy: cveinfo.AlphabeticAsc,
@ -327,7 +327,7 @@ func TestCVEPagination(t *testing.T) {
})
Convey("limit > len(cves)", func() {
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 6,
Offset: 3,
SortBy: cveinfo.AlphabeticAsc,
@ -340,7 +340,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[0].ID, ShouldEqual, "CVE3")
So(cves[1].ID, ShouldEqual, "CVE4")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 6,
Offset: 3,
SortBy: cveinfo.AlphabeticDsc,
@ -353,7 +353,7 @@ func TestCVEPagination(t *testing.T) {
So(cves[0].ID, ShouldEqual, "CVE1")
So(cves[1].ID, ShouldEqual, "CVE0")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cveinfo.PageInput{
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1", "0.1.0", "", cvemodel.PageInput{
Limit: 6,
Offset: 3,
SortBy: cveinfo.SeverityDsc,

View file

@ -22,6 +22,7 @@ import (
_ "modernc.org/sqlite"
zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
@ -181,54 +182,61 @@ func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) {
return report, nil
}
func (scanner Scanner) IsImageFormatScannable(repo, tag string) (bool, error) {
image := repo + ":" + tag
func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) {
var (
digestStr = ref
mediaType string
)
if scanner.cache.Get(image) != nil {
return true, nil
if zcommon.IsTag(ref) {
imgDescriptor, err := repodb.GetImageDescriptor(scanner.repoDB, repo, ref)
if err != nil {
return false, err
}
digestStr = imgDescriptor.Digest
mediaType = imgDescriptor.MediaType
} else {
var found bool
found, mediaType = repodb.FindMediaTypeForDigest(scanner.repoDB, godigest.Digest(ref))
if !found {
return false, zerr.ErrManifestNotFound
}
}
repoMeta, err := scanner.repoDB.GetRepoMeta(repo)
if err != nil {
return false, err
}
return scanner.IsImageMediaScannable(repo, digestStr, mediaType)
}
var ok bool
func (scanner Scanner) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) {
image := repo + "@" + digestStr
imageDescriptor, ok := repoMeta.Tags[tag]
if !ok {
return false, zerr.ErrTagMetaNotFound
}
switch imageDescriptor.MediaType {
switch mediaType {
case ispec.MediaTypeImageManifest:
ok, err := scanner.isManifestScanable(imageDescriptor)
ok, err := scanner.isManifestScanable(digestStr)
if err != nil {
return ok, fmt.Errorf("image '%s' %w", image, err)
}
return ok, nil
case ispec.MediaTypeImageIndex:
ok, err := scanner.isIndexScanable(imageDescriptor)
ok, err := scanner.isIndexScanable(digestStr)
if err != nil {
return ok, fmt.Errorf("image '%s' %w", image, err)
}
return ok, nil
default:
return false, nil
}
return false, nil
}
func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, error) {
manifestDigestStr := descriptor.Digest
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return false, err
func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) {
if scanner.cache.Get(digestStr) != nil {
return true, nil
}
manifestData, err := scanner.repoDB.GetManifestData(manifestDigest)
manifestData, err := scanner.repoDB.GetManifestData(godigest.Digest(digestStr))
if err != nil {
return false, err
}
@ -257,18 +265,98 @@ func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, e
return true, nil
}
func (scanner Scanner) isIndexScanable(descriptor repodb.Descriptor) (bool, error) {
func (scanner Scanner) isIndexScanable(digestStr string) (bool, error) {
if scanner.cache.Get(digestStr) != nil {
return true, nil
}
indexData, err := scanner.repoDB.GetIndexData(godigest.Digest(digestStr))
if err != nil {
return false, err
}
var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return false, err
}
if len(indexContent.Manifests) == 0 {
return true, nil
}
for _, manifest := range indexContent.Manifests {
isScannable, err := scanner.isManifestScanable(manifest.Digest.String())
if err != nil {
continue
}
// if at least 1 manifest is scanable, the whole index is scanable
if isScannable {
return true, nil
}
}
return false, nil
}
func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) {
if scanner.cache.Get(image) != nil {
return scanner.cache.Get(image), nil
var (
originalImageInput = image
digest string
mediaType string
)
repo, ref, isTag := zcommon.GetImageDirAndReference(image)
digest = ref
if isTag {
imgDescriptor, err := repodb.GetImageDescriptor(scanner.repoDB, repo, ref)
if err != nil {
return map[string]cvemodel.CVE{}, err
}
digest = imgDescriptor.Digest
mediaType = imgDescriptor.MediaType
} else {
var found bool
found, mediaType = repodb.FindMediaTypeForDigest(scanner.repoDB, godigest.Digest(ref))
if !found {
return map[string]cvemodel.CVE{}, zerr.ErrManifestNotFound
}
}
cveidMap := make(map[string]cvemodel.CVE)
var (
cveIDMap map[string]cvemodel.CVE
err error
)
scanner.log.Debug().Str("image", image).Msg("scanning image")
switch mediaType {
case ispec.MediaTypeImageIndex:
cveIDMap, err = scanner.scanIndex(repo, digest)
default:
cveIDMap, err = scanner.scanManifest(repo, digest)
}
if err != nil {
scanner.log.Error().Err(err).Str("image", originalImageInput).Msg("unable to scan image")
return map[string]cvemodel.CVE{}, err
}
return cveIDMap, nil
}
func (scanner Scanner) scanManifest(repo, digest string) (map[string]cvemodel.CVE, error) {
if cachedMap := scanner.cache.Get(digest); cachedMap != nil {
return cachedMap, nil
}
cveidMap := map[string]cvemodel.CVE{}
image := repo + "@" + digest
scanner.dbLock.Lock()
opts := scanner.getTrivyOptions(image)
@ -276,8 +364,6 @@ func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error)
scanner.dbLock.Unlock()
if err != nil { //nolint: wsl
scanner.log.Error().Err(err).Str("image", image).Msg("unable to scan image")
return cveidMap, err
}
@ -335,11 +421,42 @@ func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error)
}
}
scanner.cache.Add(image, cveidMap)
scanner.cache.Add(digest, cveidMap)
return cveidMap, nil
}
func (scanner Scanner) scanIndex(repo, digest string) (map[string]cvemodel.CVE, error) {
indexData, err := scanner.repoDB.GetIndexData(godigest.Digest(digest))
if err != nil {
return map[string]cvemodel.CVE{}, err
}
var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil {
return map[string]cvemodel.CVE{}, err
}
indexCveIDMap := map[string]cvemodel.CVE{}
for _, manifest := range indexContent.Manifests {
if isScannable, err := scanner.isManifestScanable(manifest.Digest.String()); isScannable && err == nil {
manifestCveIDMap, err := scanner.scanManifest(repo, manifest.Digest.String())
if err != nil {
return nil, err
}
for vulnerabilityID, CVE := range manifestCveIDMap {
indexCveIDMap[vulnerabilityID] = CVE
}
}
}
return indexCveIDMap, nil
}
// UpdateDB downloads the Trivy DB / Cache under the store root directory.
func (scanner Scanner) UpdateDB() error {
// We need a lock as using multiple substores each with it's own DB

View file

@ -18,6 +18,7 @@ import (
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb"
@ -27,6 +28,7 @@ import (
"zotregistry.io/zot/pkg/storage/local"
storageTypes "zotregistry.io/zot/pkg/storage/types"
"zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
)
func generateTestImage(storeController storage.StoreController, image string) {
@ -100,9 +102,6 @@ func TestMultipleStoragePath(t *testing.T) {
repoDB, err := boltdb_wrapper.NewBoltDBWrapper(boltDriver, log)
So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil)
scanner := NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", "", log)
So(scanner.storeController.DefaultStore, ShouldNotBeNil)
@ -125,6 +124,9 @@ func TestMultipleStoragePath(t *testing.T) {
generateTestImage(storeController, img1)
generateTestImage(storeController, img2)
err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil)
// Try to scan without the DB being downloaded
_, err = scanner.ScanImage(img0)
So(err, ShouldNotBeNil)
@ -508,3 +510,138 @@ func TestDefaultTrivyDBUrl(t *testing.T) {
So(err, ShouldBeNil)
})
}
func TestIsIndexScanable(t *testing.T) {
Convey("IsIndexScanable", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = &local.ImageStoreLocal{}
repoDB := &boltdb_wrapper.DBWrapper{}
log := log.NewLogger("debug", "")
Convey("Find index in cache", func() {
scanner := NewScanner(storeController, repoDB, "", "", log)
scanner.cache.Add("digest", make(map[string]model.CVE))
found, err := scanner.isIndexScanable("digest")
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
})
}
func TestScanIndexErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("GetIndexData fails", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, godigest.ErrDigestUnsupported
}
scanner := NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.scanIndex("repo", "digest")
So(err, ShouldNotBeNil)
})
Convey("Bad Index Blob, Unamrshal fails", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{
IndexBlob: []byte(`bad-blob`),
}, nil
}
scanner := NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.scanIndex("repo", "digest")
So(err, ShouldNotBeNil)
})
})
}
func TestIsIndexScanableErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("GetIndexData errors", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrManifestDataNotFound
}
scanner := NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.isIndexScanable("digest")
So(err, ShouldNotBeNil)
})
Convey("bad index data, can't unmarshal", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`bad`)}, nil
}
scanner := NewScanner(storeController, repoDB, "", "", log)
ok, err := scanner.isIndexScanable("digest")
So(err, ShouldNotBeNil)
So(ok, ShouldBeFalse)
})
Convey("is Manifest Scanable errors", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`{
"manifests": [{
"digest": "digest2"
},
{
"digest": "digest1"
}
]
}`)}, nil
}
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
switch manifestDigest {
case "digest1":
return repodb.ManifestData{
ManifestBlob: []byte("{}"),
}, nil
case "digest2":
return repodb.ManifestData{}, zerr.ErrBadBlob
}
return repodb.ManifestData{}, nil
}
scanner := NewScanner(storeController, repoDB, "", "", log)
ok, err := scanner.isIndexScanable("digest")
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
})
Convey("is Manifest Scanable returns false because no manifest is scanable", func() {
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{IndexBlob: []byte(`{
"manifests": [{
"digest": "digest2"
}
]
}`)}, nil
}
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
return repodb.ManifestData{}, zerr.ErrBadBlob
}
scanner := NewScanner(storeController, repoDB, "", "", log)
ok, err := scanner.isIndexScanable("digest")
So(err, ShouldBeNil)
So(ok, ShouldBeFalse)
})
})
}

View file

@ -0,0 +1,186 @@
package trivy_test
import (
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api"
"zotregistry.io/zot/pkg/api/config"
extconf "zotregistry.io/zot/pkg/extensions/config"
"zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/bolt"
"zotregistry.io/zot/pkg/meta/repodb"
boltdb_wrapper "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
"zotregistry.io/zot/pkg/test"
"zotregistry.io/zot/pkg/test/mocks"
)
func TestScanningByDigest(t *testing.T) {
Convey("Scan the individual manifests inside an index", t, func() {
// start server
tempDir := t.TempDir()
port := test.GetFreePort()
baseURL := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Storage.RootDirectory = tempDir
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
So(ctlr, ShouldNotBeNil)
cm := test.NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
// push index with 2 manifests: one with vulns and one without
vulnImage, err := test.GetVulnImage("")
So(err, ShouldBeNil)
vulnDigest, err := vulnImage.Digest()
So(err, ShouldBeNil)
simpleImage, err := test.GetRandomImage("")
So(err, ShouldBeNil)
simpleDigest, err := simpleImage.Digest()
So(err, ShouldBeNil)
multiArch := test.GetMultiarchImageForImages("multi-arch-tag", []test.Image{simpleImage, vulnImage})
multiArchDigest, err := multiArch.Digest()
So(err, ShouldBeNil)
err = test.UploadMultiarchImage(multiArch, baseURL, "multi-arch")
So(err, ShouldBeNil)
// scan
scanner := trivy.NewScanner(ctlr.StoreController, ctlr.RepoDB, "ghcr.io/project-zot/trivy-db", "", ctlr.Log)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage("multi-arch@" + vulnDigest.String())
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, test.Vulnerability1ID)
So(cveMap, ShouldContainKey, test.Vulnerability2ID)
cveMap, err = scanner.ScanImage("multi-arch@" + simpleDigest.String())
So(err, ShouldBeNil)
So(cveMap, ShouldBeEmpty)
cveMap, err = scanner.ScanImage("multi-arch@" + multiArchDigest.String())
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, test.Vulnerability1ID)
So(cveMap, ShouldContainKey, test.Vulnerability2ID)
cveMap, err = scanner.ScanImage("multi-arch:multi-arch-tag")
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, test.Vulnerability1ID)
So(cveMap, ShouldContainKey, test.Vulnerability2ID)
})
}
func TestScannerErrors(t *testing.T) {
digest := godigest.FromString("dig")
Convey("Errors", t, func() {
storeController := storage.StoreController{}
storeController.DefaultStore = mocks.MockedImageStore{}
repoDB := mocks.RepoDBMock{}
log := log.NewLogger("debug", "")
Convey("IsImageFormatSanable", func() {
repoDB.GetManifestDataFn = func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
return repodb.ManifestData{}, zerr.ErrManifestDataNotFound
}
repoDB.GetIndexDataFn = func(indexDigest godigest.Digest) (repodb.IndexData, error) {
return repodb.IndexData{}, zerr.ErrManifestDataNotFound
}
scanner := trivy.NewScanner(storeController, repoDB, "", "", log)
_, err := scanner.ScanImage("repo@" + digest.String())
So(err, ShouldNotBeNil)
})
})
}
func TestVulnerableLayer(t *testing.T) {
Convey("Vulnerable layer", t, func() {
vulnerableLayer, err := test.GetLayerWithVulnerability(1)
So(err, ShouldBeNil)
created, err := time.Parse(time.RFC3339, "2023-03-29T18:19:24Z")
So(err, ShouldBeNil)
config := ispec.Image{
Created: &created,
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
}
img, err := test.GetImageWithComponents(
config,
[][]byte{
vulnerableLayer,
},
)
So(err, ShouldBeNil)
imgDigest, err := img.Digest()
So(err, ShouldBeNil)
tempDir := t.TempDir()
log := log.NewLogger("debug", "")
imageStore := local.NewImageStore(tempDir, false, 0, false, false,
log, monitoring.NewMetricsServer(false, log), nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
err = test.WriteImageToFileSystem(img, "repo", storeController)
So(err, ShouldBeNil)
params := bolt.DBParameters{
RootDir: tempDir,
}
boltDriver, err := bolt.GetBoltDriver(params)
So(err, ShouldBeNil)
repoDB, err := boltdb_wrapper.NewBoltDBWrapper(boltDriver, log)
So(err, ShouldBeNil)
err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil)
scanner := trivy.NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", "", log)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage("repo@" + imgDigest.String())
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 2)
})
}

View file

@ -1679,7 +1679,7 @@ type Query {
Returns a CVE list for the image specified in the argument
"""
CVEListForImage(
"Image name in format ` + "`" + `repository:tag` + "`" + `"
"Image name in format ` + "`" + `repository:tag` + "`" + ` or ` + "`" + `repository@digest` + "`" + `"
image: String!,
"Sets the parameters of the requested page"
requestedPage: PageInput

View file

@ -125,10 +125,10 @@ func getImageListForDigest(ctx context.Context, digest string, repoDB repodb.Rep
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}
@ -294,24 +294,20 @@ func getCVEListForImage(
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),
pageInput := cvemodel.PageInput{
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: cvemodel.SortCriteria(
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity),
),
}
repo, ref, isTag := zcommon.GetImageDirAndReference(image)
repo, ref, _ := zcommon.GetImageDirAndReference(image)
if ref == "" {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided")
}
if !isTag {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("reference by digest not supported")
}
cveList, pageInfo, err := cveInfo.GetCVEListForImage(repo, ref, searchedCVE, pageInput)
if err != nil {
return &gql_generated.CVEResultForImage{}, err
@ -365,8 +361,17 @@ func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) repodb.FilterFunc {
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String()
for _, tagInfo := range tagsInfo {
if tagInfo.Descriptor.Digest.String() == manifestDigest {
return true
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
}
}
}
}
@ -423,10 +428,10 @@ func getImageListForCVE(
// Actual page requested by user
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
@ -485,10 +490,10 @@ func getImageListWithCVEFixed(
// Actual page requested by user
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
@ -535,10 +540,10 @@ func repoListWithNewestImage(
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
@ -620,10 +625,10 @@ func getFilteredPaginatedRepos(
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
@ -678,10 +683,10 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}
@ -709,10 +714,10 @@ func globalSearch(ctx context.Context, query string, repoDB repodb.RepoDB, filte
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}
@ -753,10 +758,10 @@ func derivedImageList(ctx context.Context, image string, digest *string, repoDB
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
@ -866,10 +871,10 @@ func baseImageList(ctx context.Context, image string, digest *string, repoDB rep
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaUpdateTime),
),
}
@ -1117,7 +1122,7 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
continue
}
manifestMeta, err := repoDB.GetManifestMeta(repo, godigest.Digest(digest))
manifestData, err := repoDB.GetManifestData(godigest.Digest(digest))
if err != nil {
graphql.AddError(ctx, fmt.Errorf("resolver: failed to get manifest meta for image %s:%s with manifest digest %s %w",
repo, tag, digest, err))
@ -1125,7 +1130,10 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
continue
}
manifestMetaMap[digest] = manifestMeta
manifestMetaMap[digest] = repodb.ManifestMetadata{
ManifestBlob: manifestData.ManifestBlob,
ConfigBlob: manifestData.ConfigBlob,
}
case ispec.MediaTypeImageIndex:
digest := descriptor.Digest
@ -1154,7 +1162,7 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
var errorOccured bool
for _, descriptor := range indexContent.Manifests {
manifestMeta, err := repoDB.GetManifestMeta(repo, descriptor.Digest)
manifestData, err := repoDB.GetManifestData(descriptor.Digest)
if err != nil {
graphql.AddError(ctx,
fmt.Errorf("resolver: failed to get manifest meta with digest '%s' for multiarch image %s:%s %w",
@ -1166,7 +1174,10 @@ func expandedRepoInfo(ctx context.Context, repo string, repoDB repodb.RepoDB, cv
break
}
manifestMetaMap[descriptor.Digest.String()] = manifestMeta
manifestMetaMap[descriptor.Digest.String()] = repodb.ManifestMetadata{
ManifestBlob: manifestData.ManifestBlob,
ConfigBlob: manifestData.ConfigBlob,
}
}
if errorOccured {
@ -1210,7 +1221,7 @@ func (p timeSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
func safeDerefferencing[T any](pointer *T, defaultVal T) T {
func safeDereferencing[T any](pointer *T, defaultVal T) T {
if pointer != nil {
return *pointer
}
@ -1236,10 +1247,10 @@ func getImageList(ctx context.Context, repo string, repoDB repodb.RepoDB, cveInf
}
pageInput := repodb.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
Limit: safeDereferencing(requestedPage.Limit, 0),
Offset: safeDereferencing(requestedPage.Offset, 0),
SortBy: repodb.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
safeDereferencing(requestedPage.SortBy, gql_generated.SortCriteriaRelevance),
),
}

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
@ -1987,7 +1988,12 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
digest, ok := tagsMap[image]
if !ok {
return map[string]cvemodel.CVE{}, nil
if !strings.Contains(image, "@") {
return map[string]cvemodel.CVE{}, nil
}
_, digestStr := common.GetImageDirAndDigest(image)
digest = godigest.Digest(digestStr)
}
if digest.String() == digest1.String() {
@ -2075,7 +2081,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
repoWithDigestRef := fmt.Sprintf("repo@%s", dig)
_, err := getCVEListForImage(responseContext, repoWithDigestRef, cveInfo, pageInput, "", log)
So(err.Error(), ShouldContainSubstring, "reference by digest not supported")
So(err, ShouldBeNil)
cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, "", log)
So(err, ShouldBeNil)
@ -3304,12 +3310,12 @@ func TestExpandedRepoInfo(t *testing.T) {
},
}, nil
},
GetManifestMetaFn: func(repo string, manifestDigest godigest.Digest) (repodb.ManifestMetadata, error) {
GetManifestDataFn: func(manifestDigest godigest.Digest) (repodb.ManifestData, error) {
switch manifestDigest {
case "errorDigest":
return repodb.ManifestMetadata{}, ErrTestError
return repodb.ManifestData{}, ErrTestError
default:
return repodb.ManifestMetadata{
return repodb.ManifestData{
ManifestBlob: []byte("{}"),
ConfigBlob: []byte("{}"),
}, nil

View file

@ -592,7 +592,7 @@ type Query {
Returns a CVE list for the image specified in the argument
"""
CVEListForImage(
"Image name in format `repository:tag`"
"Image name in format `repository:tag` or `repository@digest`"
image: String!,
"Sets the parameters of the requested page"
requestedPage: PageInput

View file

@ -235,7 +235,9 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" {
if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" ||
strings.Contains(image, "zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") ||
strings.Contains(image, "a/zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
@ -258,7 +260,9 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
}, nil
}
if image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" {
if image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" ||
strings.Contains(image, "a/zot-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") ||
strings.Contains(image, "zot-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") {
return map[string]cvemodel.CVE{
"CVE3": {
ID: "CVE3",
@ -275,7 +279,8 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
}, nil
}
if image == "test-repo:latest" {
if image == "test-repo:latest" ||
image == "test-repo@sha256:9f8e1a125c4fb03a0f157d75999b73284ccc5cba18eb772e4643e3499343607e" {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
@ -320,12 +325,20 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
return false, err
}
manifestDigestStr, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
manifestDigestStr := reference
if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
}
manifestDigest, err := godigest.Parse(manifestDigestStr.Digest)
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return false, err
}
@ -758,6 +771,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
Name
NewestImage{
Tag
Digest
Vulnerabilities{
MaxSeverity
Count

View file

@ -176,7 +176,7 @@ func GetRepoTag(searchText string) (string, string, error) {
splitSlice := strings.Split(searchText, ":")
if len(splitSlice) != repoTagCount {
return "", "", zerr.ErrInvalidRepoTagFormat
return "", "", zerr.ErrInvalidRepoRefFormat
}
repo := strings.TrimSpace(splitSlice[0])
@ -329,6 +329,7 @@ func FilterDataByRepo(foundRepos []repodb.RepoMetadata, manifestMetadataMap map[
foundindexDataMap[descriptor.Digest] = indexData
default:
continue
}
}
}

View file

@ -1339,7 +1339,6 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
matchedTags := make(map[string]repodb.Descriptor)
// take all manifestMetas
for tag, descriptor := range repoMeta.Tags {
matchedTags[tag] = descriptor
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
manifestDigest := descriptor.Digest
@ -1349,13 +1348,10 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
return fmt.Errorf("repodb: error while unmashaling manifest metadata for digest %s %w", manifestDigest, err)
}
if !filter(repoMeta, manifestMeta) {
delete(matchedTags, tag)
continue
if filter(repoMeta, manifestMeta) {
matchedTags[tag] = descriptor
manifestMetadataMap[manifestDigest] = manifestMeta
}
manifestMetadataMap[manifestDigest] = manifestMeta
case ispec.MediaTypeImageIndex:
indexDigest := descriptor.Digest
@ -1371,7 +1367,7 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
return fmt.Errorf("repodb: error while unmashaling index content for digest %s %w", indexDigest, err)
}
manifestHasBeenMatched := false
matchedManifests := []ispec.Descriptor{}
for _, manifest := range indexContent.Manifests {
manifestDigest := manifest.Digest.String()
@ -1381,24 +1377,25 @@ func (bdw *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
return fmt.Errorf("repodb: error while getting manifest data for digest %s %w", manifestDigest, err)
}
manifestMetadataMap[manifestDigest] = manifestMeta
if filter(repoMeta, manifestMeta) {
manifestHasBeenMatched = true
matchedManifests = append(matchedManifests, manifest)
manifestMetadataMap[manifestDigest] = manifestMeta
}
}
if !manifestHasBeenMatched {
delete(matchedTags, tag)
if len(matchedManifests) > 0 {
indexContent.Manifests = matchedManifests
for _, manifest := range indexContent.Manifests {
delete(manifestMetadataMap, manifest.Digest.String())
indexBlob, err := json.Marshal(indexContent)
if err != nil {
return err
}
continue
}
indexData.IndexBlob = indexBlob
indexDataMap[indexDigest] = indexData
indexDataMap[indexDigest] = indexData
matchedTags[tag] = descriptor
}
default:
bdw.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type")

View file

@ -2,6 +2,11 @@ package repodb
import (
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors"
)
// DetailedRepoMeta is a auxiliary structure used for sorting RepoMeta arrays by information
@ -55,3 +60,33 @@ func SortByDownloads(pageBuffer []DetailedRepoMeta) func(i, j int) bool {
return pageBuffer[i].Downloads > pageBuffer[j].Downloads
}
}
// FindMediaTypeForDigest will look into the buckets for a certain digest. Depending on which bucket that
// digest is found the corresponding mediatype is returned.
func FindMediaTypeForDigest(repoDB RepoDB, digest godigest.Digest) (bool, string) {
_, err := repoDB.GetManifestData(digest)
if err == nil {
return true, ispec.MediaTypeImageManifest
}
_, err = repoDB.GetIndexData(digest)
if err == nil {
return true, ispec.MediaTypeImageIndex
}
return false, ""
}
func GetImageDescriptor(repoDB RepoDB, repo, tag string) (Descriptor, error) {
repoMeta, err := repoDB.GetRepoMeta(repo)
if err != nil {
return Descriptor{}, err
}
imageDescriptor, ok := repoMeta.Tags[tag]
if !ok {
return Descriptor{}, zerr.ErrTagMetaNotFound
}
return imageDescriptor, nil
}

View file

@ -1122,9 +1122,8 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
repoMeta.IsStarred = zcommon.Contains(userStars, repoMeta.Name)
matchedTags := make(map[string]repodb.Descriptor)
for tag, descriptor := range repoMeta.Tags {
matchedTags[tag] = descriptor
for tag, descriptor := range repoMeta.Tags {
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
manifestDigest := descriptor.Digest
@ -1137,13 +1136,10 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
fmt.Errorf("repodb: error while unmashaling manifest metadata for digest %s \n%w", manifestDigest, err)
}
if !filter(repoMeta, manifestMeta) {
delete(matchedTags, tag)
continue
if filter(repoMeta, manifestMeta) {
matchedTags[tag] = descriptor
manifestMetadataMap[manifestDigest] = manifestMeta
}
manifestMetadataMap[manifestDigest] = manifestMeta
case ispec.MediaTypeImageIndex:
indexDigest := descriptor.Digest
@ -1163,7 +1159,7 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
fmt.Errorf("repodb: error while unmashaling index content for digest %s %w", indexDigest, err)
}
manifestHasBeenMatched := false
matchedManifests := []ispec.Descriptor{}
for _, manifest := range indexContent.Manifests {
manifestDigest := manifest.Digest.String()
@ -1176,24 +1172,26 @@ func (dwr *DBWrapper) FilterTags(ctx context.Context, filter repodb.FilterFunc,
fmt.Errorf("%w repodb: error while getting manifest data for digest %s", err, manifestDigest)
}
manifestMetadataMap[manifestDigest] = manifestMeta
if filter(repoMeta, manifestMeta) {
manifestHasBeenMatched = true
matchedManifests = append(matchedManifests, manifest)
manifestMetadataMap[manifestDigest] = manifestMeta
}
}
if !manifestHasBeenMatched {
delete(matchedTags, tag)
if len(matchedManifests) > 0 {
indexContent.Manifests = matchedManifests
for _, manifest := range indexContent.Manifests {
delete(manifestMetadataMap, manifest.Digest.String())
indexBlob, err := json.Marshal(indexContent)
if err != nil {
return []repodb.RepoMetadata{}, map[string]repodb.ManifestMetadata{}, map[string]repodb.IndexData{},
pageInfo, err
}
continue
}
indexData.IndexBlob = indexBlob
indexDataMap[indexDigest] = indexData
indexDataMap[indexDigest] = indexData
matchedTags[tag] = descriptor
}
default:
dwr.Log.Error().Str("mediaType", descriptor.MediaType).Msg("Unsupported media type")

View file

@ -20,6 +20,7 @@ import (
"os"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
@ -45,6 +46,7 @@ import (
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/meta/repodb"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/test/inject"
@ -56,6 +58,8 @@ const (
SleepTime = 100 * time.Millisecond
)
var vulnerableLayer []byte //nolint: gochecknoglobals
var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
// which: manifest, config, layer
@ -604,15 +608,8 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif
configDigest := godigest.FromBytes(configBlob)
layer := make([]byte, layerSize)
_, err = rand.Read(layer)
if err != nil {
return ispec.Image{}, [][]byte{}, ispec.Manifest{}, err
}
layers := [][]byte{
layer,
GetRandomLayer(layerSize),
}
schemaVersion := 2
@ -639,6 +636,138 @@ func GetRandomImageComponents(layerSize int) (ispec.Image, [][]byte, ispec.Manif
return config, layers, manifest, nil
}
// These are the 2 vulnerabilities found for the returned image by the GetVulnImage function.
const (
Vulnerability1ID = "CVE-2023-2650"
Vulnerability2ID = "CVE-2023-1255"
)
func GetVulnImage(ref string) (Image, error) {
const skipStackFrame = 2
vulnerableLayer, err := GetLayerWithVulnerability(skipStackFrame)
if err != nil {
return Image{}, err
}
vulnerableConfig := ispec.Image{
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
}
img, err := GetImageWithComponents(
vulnerableConfig,
[][]byte{
vulnerableLayer,
})
if err != nil {
return Image{}, err
}
img.Reference = ref
return img, err
}
func GetVulnImageWithConfig(ref string, config ispec.Image) (Image, error) {
const skipStackFrame = 2
vulnerableLayer, err := GetLayerWithVulnerability(skipStackFrame)
if err != nil {
return Image{}, err
}
vulnerableConfig := ispec.Image{
Platform: config.Platform,
Config: config.Config,
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
Created: config.Created,
History: config.History,
}
img, err := GetImageWithComponents(
vulnerableConfig,
[][]byte{
vulnerableLayer,
})
if err != nil {
return Image{}, err
}
img.Reference = ref
return img, err
}
func GetLayerWithVulnerability(skip int) ([]byte, error) {
if vulnerableLayer != nil {
return vulnerableLayer, nil
}
_, b, _, ok := runtime.Caller(skip)
if !ok {
return []byte{}, zerr.ErrCallerInfo
}
absoluteCallerpath := filepath.Dir(b)
fmt.Println(absoluteCallerpath)
// we know pkg folder inside zot must exist, and since all tests are called from within pkg we'll use it as reference
relCallerPath := absoluteCallerpath[strings.LastIndex(absoluteCallerpath, "pkg"):]
relCallerSlice := strings.Split(relCallerPath, string(os.PathSeparator))
fmt.Println(relCallerPath, relCallerSlice)
// we'll calculate how many folder we should go back to reach the root of the zot folder relative
// to the callers position
backPathSlice := make([]string, len(relCallerSlice))
for i := 0; i < len(backPathSlice); i++ {
backPathSlice[i] = ".."
}
backPath := filepath.Join(backPathSlice...)
// this is the path of the blob relative to the root of the zot folder
vulnBlobPath := "test/data/alpine/blobs/sha256/f56be85fc22e46face30e2c3de3f7fe7c15f8fd7c4e5add29d7f64b87abdaa09"
var err error
x, _ := filepath.Abs(filepath.Join(backPath, vulnBlobPath))
_ = x
vulnerableLayer, err = os.ReadFile(filepath.Join(backPath, vulnBlobPath)) //nolint: lll
if err != nil {
return nil, err
}
return vulnerableLayer, nil
}
func GetRandomLayer(size int) []byte {
layer := make([]byte, size)
_, err := rand.Read(layer)
if err != nil {
return layer
}
return layer
}
func GetRandomImage(reference string) (Image, error) {
const layerSize = 20

View file

@ -2,17 +2,18 @@ package mocks
import (
"zotregistry.io/zot/pkg/common"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
)
type CveInfoMock struct {
GetImageListForCVEFn func(repo, cveID string) ([]cvemodel.TagInfo, error)
GetImageListWithCVEFixedFn func(repo, cveID string) ([]cvemodel.TagInfo, error)
GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cveinfo.PageInput,
GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, common.PageInfo, error)
GetCVESummaryForImageFn func(repo string, reference string,
) (cveinfo.ImageCVESummary, error)
) (cvemodel.ImageCVESummary, error)
GetCVESummaryForImageMediaFn func(repo string, digest, mediaType string,
) (cvemodel.ImageCVESummary, error)
CompareSeveritiesFn func(severity1, severity2 string) int
UpdateDBFn func() error
}
@ -34,7 +35,7 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
}
func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string,
searchedCVE string, pageInput cveinfo.PageInput,
searchedCVE string, pageInput cvemodel.PageInput,
) (
[]cvemodel.CVE,
common.PageInfo,
@ -48,12 +49,21 @@ func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string,
}
func (cveInfo CveInfoMock) GetCVESummaryForImage(repo string, reference string,
) (cveinfo.ImageCVESummary, error) {
) (cvemodel.ImageCVESummary, error) {
if cveInfo.GetCVESummaryForImageFn != nil {
return cveInfo.GetCVESummaryForImageFn(repo, reference)
}
return cveinfo.ImageCVESummary{}, nil
return cvemodel.ImageCVESummary{}, nil
}
func (cveInfo CveInfoMock) GetCVESummaryForImageMedia(repo, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) {
if cveInfo.GetCVESummaryForImageMediaFn != nil {
return cveInfo.GetCVESummaryForImageMediaFn(repo, digest, mediaType)
}
return cvemodel.ImageCVESummary{}, nil
}
func (cveInfo CveInfoMock) CompareSeverities(severity1, severity2 string) int {
@ -74,6 +84,7 @@ func (cveInfo CveInfoMock) UpdateDB() error {
type CveScannerMock struct {
IsImageFormatScannableFn func(repo string, reference string) (bool, error)
IsImageMediaScannableFn func(repo string, digest, mediaType string) (bool, error)
ScanImageFn func(image string) (map[string]cvemodel.CVE, error)
CompareSeveritiesFn func(severity1, severity2 string) int
UpdateDBFn func() error
@ -87,6 +98,14 @@ func (scanner CveScannerMock) IsImageFormatScannable(repo string, reference stri
return true, nil
}
func (scanner CveScannerMock) IsImageMediaScannable(repo string, digest, mediaType string) (bool, error) {
if scanner.IsImageMediaScannableFn != nil {
return scanner.IsImageMediaScannableFn(repo, digest, mediaType)
}
return true, nil
}
func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) {
if scanner.ScanImageFn != nil {
return scanner.ScanImageFn(image)

View file

@ -216,7 +216,7 @@ func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]cvemodel
tagsInfo = append(tagsInfo,
cvemodel.TagInfo{
Name: val,
Tag: val,
Timestamp: timeStamp,
Descriptor: cvemodel.Descriptor{
Digest: digest,

View file

@ -450,7 +450,7 @@ func TestTagsInfo(t *testing.T) {
allTags := make([]cvemodel.TagInfo, 0)
firstTag := cvemodel.TagInfo{
Name: "1.0.0",
Tag: "1.0.0",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a178362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -458,7 +458,7 @@ func TestTagsInfo(t *testing.T) {
Timestamp: time.Now(),
}
secondTag := cvemodel.TagInfo{
Name: "1.0.1",
Tag: "1.0.1",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a179362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -466,7 +466,7 @@ func TestTagsInfo(t *testing.T) {
Timestamp: time.Now(),
}
thirdTag := cvemodel.TagInfo{
Name: "1.0.2",
Tag: "1.0.2",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a170362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -474,7 +474,7 @@ func TestTagsInfo(t *testing.T) {
Timestamp: time.Now(),
}
fourthTag := cvemodel.TagInfo{
Name: "1.0.3",
Tag: "1.0.3",
Descriptor: cvemodel.Descriptor{
Digest: "sha256:eca04f027f414362596f2632746d8a171362170b9ac9af772011fedcc3877ebb",
MediaType: ispec.MediaTypeImageManifest,
@ -485,6 +485,6 @@ func TestTagsInfo(t *testing.T) {
allTags = append(allTags, firstTag, secondTag, thirdTag, fourthTag)
latestTag := ocilayout.GetLatestTag(allTags)
So(latestTag.Name, ShouldEqual, "1.0.3")
So(latestTag.Tag, ShouldEqual, "1.0.3")
})
}