Files
zot/pkg/extensions/search/cve/cve.go
T
Andrei Aaron bcdd9988f5 fix(cve): cummulative fixes and improvements for CVE scanning logic (#1810)
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>
2023-09-17 15:12:20 -07:00

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
}