mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 04:48:26 +08:00
bcdd9988f5
1. Only scan CVEs for images returned by graphql calls Since pagination was refactored to account for image indexes, we had started to run the CVE scanner before pagination was applied, resulting in decreased ZOT performance if CVE information was requested 2. Increase in medory-cache of cve results to 1m, from 10k digests. 3. Update CVE model to use CVSS severity values in our code. Previously we relied upon the strings returned by trivy directly, and the sorting they implemented. Since CVE severities are standardized, we don't need to pass around an adapter object just for pagination and sorting purposes anymore. This also improves our testing since we don't mock the sorting functions anymore. 4. Fix a flaky CLI test not waiting for the zot service to start. 5. Add the search build label on search/cve tests which were missing it. 6. The boltdb update method was used in a few places where view was supposed to be called. 7. Add logs for start and finish of parsing MetaDB. 8. Avoid unmarshalling twice to obtain annotations for multiarch images. Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
539 lines
15 KiB
Go
539 lines
15 KiB
Go
package cveinfo
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
|
|
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"
|
|
mTypes "zotregistry.io/zot/pkg/meta/types"
|
|
"zotregistry.io/zot/pkg/storage"
|
|
)
|
|
|
|
type CveInfo interface {
|
|
GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error)
|
|
GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, 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)
|
|
UpdateDB() error
|
|
}
|
|
|
|
type Scanner interface {
|
|
ScanImage(image string) (map[string]cvemodel.CVE, error)
|
|
IsImageFormatScannable(repo, ref string) (bool, error)
|
|
IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error)
|
|
UpdateDB() error
|
|
}
|
|
|
|
type BaseCveInfo struct {
|
|
Log log.Logger
|
|
Scanner Scanner
|
|
MetaDB mTypes.MetaDB
|
|
}
|
|
|
|
func NewCVEInfo(storeController storage.StoreController, metaDB mTypes.MetaDB,
|
|
dbRepository, javaDBRepository string, log log.Logger,
|
|
) *BaseCveInfo {
|
|
scanner := trivy.NewScanner(storeController, metaDB, dbRepository, javaDBRepository, log)
|
|
|
|
return &BaseCveInfo{
|
|
Log: log,
|
|
Scanner: scanner,
|
|
MetaDB: metaDB,
|
|
}
|
|
}
|
|
|
|
func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error) {
|
|
imgList := make([]cvemodel.TagInfo, 0)
|
|
|
|
repoMeta, err := cveinfo.MetaDB.GetRepoMeta(repo)
|
|
if err != nil {
|
|
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("cve-id", cveID).
|
|
Msg("unable to get list of tags from repo")
|
|
|
|
return imgList, err
|
|
}
|
|
|
|
for tag, descriptor := range repoMeta.Tags {
|
|
switch descriptor.MediaType {
|
|
case ispec.MediaTypeImageManifest, ispec.MediaTypeImageIndex:
|
|
manifestDigestStr := descriptor.Digest
|
|
|
|
manifestDigest := godigest.Digest(manifestDigestStr)
|
|
|
|
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(zcommon.GetFullImageName(repo, tag))
|
|
if err != nil {
|
|
cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed")
|
|
|
|
continue
|
|
}
|
|
|
|
if _, hasCVE := cveMap[cveID]; hasCVE {
|
|
imgList = append(imgList, cvemodel.TagInfo{
|
|
Tag: tag,
|
|
Descriptor: cvemodel.Descriptor{
|
|
Digest: manifestDigest,
|
|
MediaType: descriptor.MediaType,
|
|
},
|
|
})
|
|
}
|
|
default:
|
|
cveinfo.Log.Error().Str("mediaType", descriptor.MediaType).Msg("media type not supported for scanning")
|
|
}
|
|
}
|
|
|
|
return imgList, nil
|
|
}
|
|
|
|
func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, error) {
|
|
repoMeta, err := cveinfo.MetaDB.GetRepoMeta(repo)
|
|
if err != nil {
|
|
cveinfo.Log.Error().Err(err).Str("repository", repo).Str("cve-id", cveID).
|
|
Msg("unable to get list of tags from repo")
|
|
|
|
return []cvemodel.TagInfo{}, err
|
|
}
|
|
|
|
vulnerableTags := make([]cvemodel.TagInfo, 0)
|
|
allTags := make([]cvemodel.TagInfo, 0)
|
|
|
|
for tag, descriptor := range repoMeta.Tags {
|
|
switch descriptor.MediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
manifestDigestStr := descriptor.Digest
|
|
|
|
tagInfo, err := getTagInfoForManifest(tag, manifestDigestStr, cveinfo.MetaDB)
|
|
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")
|
|
|
|
continue
|
|
}
|
|
|
|
allTags = append(allTags, tagInfo)
|
|
|
|
if cveinfo.isManifestVulnerable(repo, tag, manifestDigestStr, cveID) {
|
|
vulnerableTags = append(vulnerableTags, tagInfo)
|
|
}
|
|
case ispec.MediaTypeImageIndex:
|
|
indexDigestStr := descriptor.Digest
|
|
|
|
indexContent, err := getIndexContent(cveinfo.MetaDB, indexDigestStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
vulnerableManifests := []cvemodel.DescriptorInfo{}
|
|
allManifests := []cvemodel.DescriptorInfo{}
|
|
|
|
for _, manifest := range indexContent.Manifests {
|
|
tagInfo, err := getTagInfoForManifest(tag, manifest.Digest.String(), cveinfo.MetaDB)
|
|
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")
|
|
|
|
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 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")
|
|
}
|
|
}
|
|
|
|
var fixedTags []cvemodel.TagInfo
|
|
|
|
if len(vulnerableTags) != 0 {
|
|
cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID).
|
|
Interface("vulnerableTags", vulnerableTags).Msg("Vulnerable tags")
|
|
fixedTags = GetFixedTags(allTags, vulnerableTags)
|
|
cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID).
|
|
Interface("fixedTags", fixedTags).Msg("Fixed tags")
|
|
} else {
|
|
cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID).
|
|
Msg("image does not contain any tag that have given cve")
|
|
fixedTags = allTags
|
|
}
|
|
|
|
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, metaDB mTypes.MetaDB) (cvemodel.TagInfo, error) {
|
|
configContent, manifestDigest, err := getConfigAndDigest(metaDB, 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(metaDB mTypes.MetaDB, indexDigestStr string) (ispec.Index, error) {
|
|
indexDigest, err := godigest.Parse(indexDigestStr)
|
|
if err != nil {
|
|
return ispec.Index{}, err
|
|
}
|
|
|
|
indexData, err := metaDB.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(metaDB mTypes.MetaDB, manifestDigestStr string) (ispec.Image, godigest.Digest, error) {
|
|
manifestDigest, err := godigest.Parse(manifestDigestStr)
|
|
if err != nil {
|
|
return ispec.Image{}, "", err
|
|
}
|
|
|
|
manifestData, err := metaDB.GetManifestData(manifestDigest)
|
|
if err != nil {
|
|
return ispec.Image{}, "", err
|
|
}
|
|
|
|
var configContent ispec.Image
|
|
|
|
// we'll fail the execution if the config is not compatibe with ispec.Image because we can't scan this type of images.
|
|
err = json.Unmarshal(manifestData.ConfigBlob, &configContent)
|
|
|
|
return configContent, manifestDigest, err
|
|
}
|
|
|
|
func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) {
|
|
searchedCVE = strings.ToUpper(searchedCVE)
|
|
|
|
for _, cve := range cveMap {
|
|
if strings.Contains(strings.ToUpper(cve.Title), searchedCVE) ||
|
|
strings.Contains(strings.ToUpper(cve.ID), searchedCVE) {
|
|
pageFinder.Add(cve)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cveinfo BaseCveInfo) GetCVEListForImage(repo, ref string, searchedCVE string, pageInput cvemodel.PageInput) (
|
|
[]cvemodel.CVE,
|
|
zcommon.PageInfo,
|
|
error,
|
|
) {
|
|
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
|
|
if !isValidImage {
|
|
return []cvemodel.CVE{}, zcommon.PageInfo{}, err
|
|
}
|
|
|
|
image := zcommon.GetFullImageName(repo, ref)
|
|
|
|
cveMap, err := cveinfo.Scanner.ScanImage(image)
|
|
if err != nil {
|
|
return []cvemodel.CVE{}, zcommon.PageInfo{}, err
|
|
}
|
|
|
|
pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy)
|
|
if err != nil {
|
|
return []cvemodel.CVE{}, zcommon.PageInfo{}, err
|
|
}
|
|
|
|
filterCVEList(cveMap, searchedCVE, pageFinder)
|
|
|
|
cveList, pageInfo := pageFinder.Page()
|
|
|
|
return cveList, pageInfo, nil
|
|
}
|
|
|
|
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 := cvemodel.ImageCVESummary{
|
|
Count: 0,
|
|
MaxSeverity: cvemodel.SeverityNotScanned,
|
|
}
|
|
|
|
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
|
|
if !isValidImage {
|
|
return imageCVESummary, err
|
|
}
|
|
|
|
image := zcommon.GetFullImageName(repo, ref)
|
|
|
|
cveMap, err := cveinfo.Scanner.ScanImage(image)
|
|
if err != nil {
|
|
return imageCVESummary, err
|
|
}
|
|
|
|
imageCVESummary.Count = len(cveMap)
|
|
|
|
if imageCVESummary.Count == 0 {
|
|
imageCVESummary.MaxSeverity = cvemodel.SeverityNone
|
|
|
|
return imageCVESummary, nil
|
|
}
|
|
|
|
imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown
|
|
for _, cve := range cveMap {
|
|
if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
|
|
imageCVESummary.MaxSeverity = cve.Severity
|
|
}
|
|
}
|
|
|
|
return imageCVESummary, nil
|
|
}
|
|
|
|
func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType 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 := cvemodel.ImageCVESummary{
|
|
Count: 0,
|
|
MaxSeverity: cvemodel.SeverityNotScanned,
|
|
}
|
|
|
|
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 = cvemodel.SeverityNone
|
|
|
|
return imageCVESummary, nil
|
|
}
|
|
|
|
imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown
|
|
for _, cve := range cveMap {
|
|
if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
|
|
imageCVESummary.MaxSeverity = cve.Severity
|
|
}
|
|
}
|
|
|
|
return imageCVESummary, nil
|
|
}
|
|
|
|
func (cveinfo BaseCveInfo) UpdateDB() error {
|
|
return cveinfo.Scanner.UpdateDB()
|
|
}
|
|
|
|
func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo {
|
|
sort.Slice(allTags, func(i, j int) bool {
|
|
return allTags[i].Timestamp.Before(allTags[j].Timestamp)
|
|
})
|
|
|
|
earliestVulnerable := vulnerableTags[0]
|
|
vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags))
|
|
|
|
for _, tag := range vulnerableTags {
|
|
vulnerableTagMap[tag.Tag] = 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
|
|
}
|
|
}
|
|
|
|
var fixedTags []cvemodel.TagInfo
|
|
|
|
// There are some downsides to this logic
|
|
// We assume there can't be multiple "branches" of the same
|
|
// image built at different times containing different fixes
|
|
// There may be older images which have a fix or
|
|
// newer images which don't
|
|
for _, tag := range allTags {
|
|
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
|
|
}
|
|
}
|
|
|
|
return fixedTags
|
|
}
|
|
|
|
func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvemodel.DescriptorInfo) bool {
|
|
for _, di := range slice {
|
|
if di.Digest == descriptorInfo.Digest {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|