zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/search/cve/cve.go (about)

     1  package cveinfo
     2  
     3  import (
     4  	"context"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	godigest "github.com/opencontainers/go-digest"
    10  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    11  
    12  	zerr "zotregistry.io/zot/errors"
    13  	zcommon "zotregistry.io/zot/pkg/common"
    14  	cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
    15  	"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
    16  	"zotregistry.io/zot/pkg/log"
    17  	mTypes "zotregistry.io/zot/pkg/meta/types"
    18  	"zotregistry.io/zot/pkg/storage"
    19  )
    20  
    21  type CveInfo interface {
    22  	GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error)
    23  	GetImageListWithCVEFixed(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error)
    24  	GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE string, pageinput cvemodel.PageInput,
    25  	) ([]cvemodel.CVE, zcommon.PageInfo, error)
    26  	GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string) (cvemodel.ImageCVESummary, error)
    27  }
    28  
    29  type Scanner interface {
    30  	ScanImage(ctx context.Context, image string) (map[string]cvemodel.CVE, error)
    31  	IsImageFormatScannable(repo, ref string) (bool, error)
    32  	IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error)
    33  	IsResultCached(digestStr string) bool
    34  	GetCachedResult(digestStr string) map[string]cvemodel.CVE
    35  	UpdateDB(ctx context.Context) error
    36  }
    37  
    38  type BaseCveInfo struct {
    39  	Log     log.Logger
    40  	Scanner Scanner
    41  	MetaDB  mTypes.MetaDB
    42  }
    43  
    44  func NewScanner(storeController storage.StoreController, metaDB mTypes.MetaDB,
    45  	dbRepository, javaDBRepository string, log log.Logger,
    46  ) Scanner {
    47  	return trivy.NewScanner(storeController, metaDB, dbRepository, javaDBRepository, log)
    48  }
    49  
    50  func NewCVEInfo(scanner Scanner, metaDB mTypes.MetaDB, log log.Logger) *BaseCveInfo {
    51  	return &BaseCveInfo{
    52  		Log:     log,
    53  		Scanner: scanner,
    54  		MetaDB:  metaDB,
    55  	}
    56  }
    57  
    58  func (cveinfo BaseCveInfo) GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) {
    59  	imgList := make([]cvemodel.TagInfo, 0)
    60  
    61  	repoMeta, err := cveinfo.MetaDB.GetRepoMeta(ctx, repo)
    62  	if err != nil {
    63  		cveinfo.Log.Error().Err(err).Str("repository", repo).Str("cve-id", cveID).
    64  			Msg("unable to get list of tags from repo")
    65  
    66  		return imgList, err
    67  	}
    68  
    69  	for tag, descriptor := range repoMeta.Tags {
    70  		switch descriptor.MediaType {
    71  		case ispec.MediaTypeImageManifest, ispec.MediaTypeImageIndex:
    72  			manifestDigestStr := descriptor.Digest
    73  
    74  			manifestDigest := godigest.Digest(manifestDigestStr)
    75  
    76  			isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr)
    77  			if !isScanableImage || err != nil {
    78  				cveinfo.Log.Debug().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable")
    79  
    80  				continue
    81  			}
    82  
    83  			cveMap, err := cveinfo.Scanner.ScanImage(ctx, zcommon.GetFullImageName(repo, tag))
    84  			if err != nil {
    85  				if zcommon.IsContextDone(ctx) {
    86  					return imgList, err
    87  				}
    88  
    89  				cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image scan failed")
    90  
    91  				continue
    92  			}
    93  
    94  			if _, hasCVE := cveMap[cveID]; hasCVE {
    95  				imgList = append(imgList, cvemodel.TagInfo{
    96  					Tag: tag,
    97  					Descriptor: cvemodel.Descriptor{
    98  						Digest:    manifestDigest,
    99  						MediaType: descriptor.MediaType,
   100  					},
   101  				})
   102  			}
   103  		default:
   104  			cveinfo.Log.Debug().Str("image", repo+":"+tag).Str("mediaType", descriptor.MediaType).
   105  				Msg("image media type not supported for scanning")
   106  		}
   107  	}
   108  
   109  	return imgList, nil
   110  }
   111  
   112  func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(ctx context.Context, repo, cveID string,
   113  ) ([]cvemodel.TagInfo, error) {
   114  	repoMeta, err := cveinfo.MetaDB.GetRepoMeta(ctx, repo)
   115  	if err != nil {
   116  		cveinfo.Log.Error().Err(err).Str("repository", repo).Str("cve-id", cveID).
   117  			Msg("unable to get list of tags from repo")
   118  
   119  		return []cvemodel.TagInfo{}, err
   120  	}
   121  
   122  	vulnerableTags := make([]cvemodel.TagInfo, 0)
   123  	allTags := make([]cvemodel.TagInfo, 0)
   124  
   125  	for tag, descriptor := range repoMeta.Tags {
   126  		if zcommon.IsContextDone(ctx) {
   127  			return []cvemodel.TagInfo{}, ctx.Err()
   128  		}
   129  
   130  		switch descriptor.MediaType {
   131  		case ispec.MediaTypeImageManifest:
   132  			manifestDigestStr := descriptor.Digest
   133  
   134  			tagInfo, err := getTagInfoForManifest(tag, manifestDigestStr, cveinfo.MetaDB)
   135  			if err != nil {
   136  				cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
   137  					Str("cve-id", cveID).Msg("unable to retrieve manifest and config")
   138  
   139  				continue
   140  			}
   141  
   142  			allTags = append(allTags, tagInfo)
   143  
   144  			if cveinfo.isManifestVulnerable(ctx, repo, tag, manifestDigestStr, cveID) {
   145  				vulnerableTags = append(vulnerableTags, tagInfo)
   146  			}
   147  		case ispec.MediaTypeImageIndex:
   148  			indexDigestStr := descriptor.Digest
   149  
   150  			indexContent, err := getIndexContent(cveinfo.MetaDB, indexDigestStr)
   151  			if err != nil {
   152  				continue
   153  			}
   154  
   155  			vulnerableManifests := []cvemodel.DescriptorInfo{}
   156  			allManifests := []cvemodel.DescriptorInfo{}
   157  
   158  			for _, manifest := range indexContent.Manifests {
   159  				tagInfo, err := getTagInfoForManifest(tag, manifest.Digest.String(), cveinfo.MetaDB)
   160  				if err != nil {
   161  					cveinfo.Log.Error().Err(err).Str("repository", repo).Str("tag", tag).
   162  						Str("cve-id", cveID).Msg("unable to retrieve manifest and config")
   163  
   164  					continue
   165  				}
   166  
   167  				manifestDescriptorInfo := cvemodel.DescriptorInfo{
   168  					Descriptor: tagInfo.Descriptor,
   169  					Timestamp:  tagInfo.Timestamp,
   170  				}
   171  
   172  				allManifests = append(allManifests, manifestDescriptorInfo)
   173  
   174  				if cveinfo.isManifestVulnerable(ctx, repo, tag, manifest.Digest.String(), cveID) {
   175  					vulnerableManifests = append(vulnerableManifests, manifestDescriptorInfo)
   176  				}
   177  			}
   178  
   179  			if len(allManifests) > 0 {
   180  				allTags = append(allTags, cvemodel.TagInfo{
   181  					Tag: tag,
   182  					Descriptor: cvemodel.Descriptor{
   183  						Digest:    godigest.Digest(indexDigestStr),
   184  						MediaType: ispec.MediaTypeImageIndex,
   185  					},
   186  					Manifests: allManifests,
   187  					Timestamp: mostRecentUpdate(allManifests),
   188  				})
   189  			}
   190  
   191  			if len(vulnerableManifests) > 0 {
   192  				vulnerableTags = append(vulnerableTags, cvemodel.TagInfo{
   193  					Tag: tag,
   194  					Descriptor: cvemodel.Descriptor{
   195  						Digest:    godigest.Digest(indexDigestStr),
   196  						MediaType: ispec.MediaTypeImageIndex,
   197  					},
   198  					Manifests: vulnerableManifests,
   199  					Timestamp: mostRecentUpdate(vulnerableManifests),
   200  				})
   201  			}
   202  		default:
   203  			cveinfo.Log.Debug().Str("mediaType", descriptor.MediaType).
   204  				Msg("image media type not supported for scanning")
   205  		}
   206  	}
   207  
   208  	var fixedTags []cvemodel.TagInfo
   209  
   210  	if len(vulnerableTags) != 0 {
   211  		cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID).
   212  			Interface("vulnerableTags", vulnerableTags).Msg("Vulnerable tags")
   213  		fixedTags = GetFixedTags(allTags, vulnerableTags)
   214  		cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID).
   215  			Interface("fixedTags", fixedTags).Msg("Fixed tags")
   216  	} else {
   217  		cveinfo.Log.Info().Str("repository", repo).Str("cve-id", cveID).
   218  			Msg("image does not contain any tag that have given cve")
   219  		fixedTags = allTags
   220  	}
   221  
   222  	return fixedTags, nil
   223  }
   224  
   225  func mostRecentUpdate(allManifests []cvemodel.DescriptorInfo) time.Time {
   226  	if len(allManifests) == 0 {
   227  		return time.Time{}
   228  	}
   229  
   230  	timeStamp := allManifests[0].Timestamp
   231  
   232  	for i := range allManifests {
   233  		if timeStamp.Before(allManifests[i].Timestamp) {
   234  			timeStamp = allManifests[i].Timestamp
   235  		}
   236  	}
   237  
   238  	return timeStamp
   239  }
   240  
   241  func getTagInfoForManifest(tag, manifestDigestStr string, metaDB mTypes.MetaDB) (cvemodel.TagInfo, error) {
   242  	configContent, manifestDigest, err := getConfigAndDigest(metaDB, manifestDigestStr)
   243  	if err != nil {
   244  		return cvemodel.TagInfo{}, err
   245  	}
   246  
   247  	lastUpdated := zcommon.GetImageLastUpdated(configContent)
   248  
   249  	return cvemodel.TagInfo{
   250  		Tag:        tag,
   251  		Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
   252  		Manifests: []cvemodel.DescriptorInfo{
   253  			{
   254  				Descriptor: cvemodel.Descriptor{Digest: manifestDigest, MediaType: ispec.MediaTypeImageManifest},
   255  				Timestamp:  lastUpdated,
   256  			},
   257  		},
   258  		Timestamp: lastUpdated,
   259  	}, nil
   260  }
   261  
   262  func (cveinfo *BaseCveInfo) isManifestVulnerable(ctx context.Context, repo, tag, manifestDigestStr, cveID string,
   263  ) bool {
   264  	image := zcommon.GetFullImageName(repo, tag)
   265  
   266  	isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
   267  	if !isValidImage || err != nil {
   268  		cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).Err(err).
   269  			Msg("image media type not supported for scanning, adding as a vulnerable image")
   270  
   271  		return true
   272  	}
   273  
   274  	cveMap, err := cveinfo.Scanner.ScanImage(ctx, zcommon.GetFullImageName(repo, manifestDigestStr))
   275  	if err != nil {
   276  		cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).
   277  			Msg("scanning failed, adding as a vulnerable image")
   278  
   279  		return true
   280  	}
   281  
   282  	hasCVE := false
   283  
   284  	for id := range cveMap {
   285  		if id == cveID {
   286  			hasCVE = true
   287  
   288  			break
   289  		}
   290  	}
   291  
   292  	return hasCVE
   293  }
   294  
   295  func getIndexContent(metaDB mTypes.MetaDB, indexDigestStr string) (ispec.Index, error) {
   296  	indexDigest, err := godigest.Parse(indexDigestStr)
   297  	if err != nil {
   298  		return ispec.Index{}, err
   299  	}
   300  
   301  	indexData, err := metaDB.GetImageMeta(indexDigest)
   302  	if err != nil {
   303  		return ispec.Index{}, err
   304  	}
   305  
   306  	if indexData.Index == nil {
   307  		return ispec.Index{}, zerr.ErrUnexpectedMediaType
   308  	}
   309  
   310  	return *indexData.Index, nil
   311  }
   312  
   313  func getConfigAndDigest(metaDB mTypes.MetaDB, manifestDigestStr string) (ispec.Image, godigest.Digest, error) {
   314  	manifestDigest, err := godigest.Parse(manifestDigestStr)
   315  	if err != nil {
   316  		return ispec.Image{}, "", err
   317  	}
   318  
   319  	manifestData, err := metaDB.GetImageMeta(manifestDigest)
   320  	if err != nil {
   321  		return ispec.Image{}, "", err
   322  	}
   323  
   324  	// we'll fail the execution if the config is not compatible with ispec.Image because we can't scan this type of images.
   325  	if manifestData.Manifests[0].Manifest.Config.MediaType != ispec.MediaTypeImageConfig {
   326  		return ispec.Image{}, "", zerr.ErrUnexpectedMediaType
   327  	}
   328  
   329  	return manifestData.Manifests[0].Config, manifestDigest, err
   330  }
   331  
   332  func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinder *CvePageFinder) {
   333  	searchedCVE = strings.ToUpper(searchedCVE)
   334  
   335  	for _, cve := range cveMap {
   336  		if strings.Contains(strings.ToUpper(cve.Title), searchedCVE) ||
   337  			strings.Contains(strings.ToUpper(cve.ID), searchedCVE) {
   338  			pageFinder.Add(cve)
   339  		}
   340  	}
   341  }
   342  
   343  func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref string, searchedCVE string,
   344  	pageInput cvemodel.PageInput,
   345  ) (
   346  	[]cvemodel.CVE, zcommon.PageInfo, error,
   347  ) {
   348  	isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
   349  	if !isValidImage {
   350  		cveinfo.Log.Debug().Str("image", repo+":"+ref).Err(err).Msg("image is not scanable")
   351  
   352  		return []cvemodel.CVE{}, zcommon.PageInfo{}, err
   353  	}
   354  
   355  	image := zcommon.GetFullImageName(repo, ref)
   356  
   357  	cveMap, err := cveinfo.Scanner.ScanImage(ctx, image)
   358  	if err != nil {
   359  		return []cvemodel.CVE{}, zcommon.PageInfo{}, err
   360  	}
   361  
   362  	pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy)
   363  	if err != nil {
   364  		return []cvemodel.CVE{}, zcommon.PageInfo{}, err
   365  	}
   366  
   367  	filterCVEList(cveMap, searchedCVE, pageFinder)
   368  
   369  	cveList, pageInfo := pageFinder.Page()
   370  
   371  	return cveList, pageInfo, nil
   372  }
   373  
   374  func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string,
   375  ) (cvemodel.ImageCVESummary, error) {
   376  	// There are several cases, expected returned values below:
   377  	// not scanned yet                     - max severity ""            - cve count 0   - no Errors
   378  	// not scannable                       - max severity ""            - cve count 0   - has Errors
   379  	// scannable no issues found           - max severity "NONE"        - cve count 0   - no Errors
   380  	// scannable issues found              - max severity from Scanner  - cve count >0  - no Errors
   381  	imageCVESummary := cvemodel.ImageCVESummary{
   382  		Count:       0,
   383  		MaxSeverity: cvemodel.SeverityNotScanned,
   384  	}
   385  
   386  	// For this call we only look at the scanner cache, we skip the actual scanning to save time
   387  	if !cveinfo.Scanner.IsResultCached(digest) {
   388  		isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType)
   389  		if !isValidImage {
   390  			cveinfo.Log.Debug().Str("digest", digest).Str("mediaType", mediaType).
   391  				Err(err).Msg("image is not scannable")
   392  		}
   393  
   394  		return imageCVESummary, err
   395  	}
   396  
   397  	// We will make due with cached results
   398  	cveMap := cveinfo.Scanner.GetCachedResult(digest)
   399  
   400  	imageCVESummary.Count = len(cveMap)
   401  	if imageCVESummary.Count == 0 {
   402  		imageCVESummary.MaxSeverity = cvemodel.SeverityNone
   403  
   404  		return imageCVESummary, nil
   405  	}
   406  
   407  	imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown
   408  	for _, cve := range cveMap {
   409  		if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
   410  			imageCVESummary.MaxSeverity = cve.Severity
   411  		}
   412  	}
   413  
   414  	return imageCVESummary, nil
   415  }
   416  
   417  func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo {
   418  	sort.Slice(allTags, func(i, j int) bool {
   419  		return allTags[i].Timestamp.Before(allTags[j].Timestamp)
   420  	})
   421  
   422  	earliestVulnerable := vulnerableTags[0]
   423  	vulnerableTagMap := make(map[string]cvemodel.TagInfo, len(vulnerableTags))
   424  
   425  	for _, tag := range vulnerableTags {
   426  		vulnerableTagMap[tag.Tag] = tag
   427  
   428  		switch tag.Descriptor.MediaType {
   429  		case ispec.MediaTypeImageManifest:
   430  			if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
   431  				earliestVulnerable = tag
   432  			}
   433  		case ispec.MediaTypeImageIndex:
   434  			for _, manifestDesc := range tag.Manifests {
   435  				if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) {
   436  					earliestVulnerable = tag
   437  				}
   438  			}
   439  		default:
   440  			continue
   441  		}
   442  	}
   443  
   444  	var fixedTags []cvemodel.TagInfo
   445  
   446  	// There are some downsides to this logic
   447  	// We assume there can't be multiple "branches" of the same
   448  	// image built at different times containing different fixes
   449  	// There may be older images which have a fix or
   450  	// newer images which don't
   451  	for _, tag := range allTags {
   452  		switch tag.Descriptor.MediaType {
   453  		case ispec.MediaTypeImageManifest:
   454  			if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
   455  				// The vulnerability did not exist at the time this
   456  				// image was built
   457  				continue
   458  			}
   459  			// If the image is old enough for the vulnerability to
   460  			// exist, but it was not detected, it means it contains
   461  			// the fix
   462  			if _, ok := vulnerableTagMap[tag.Tag]; !ok {
   463  				fixedTags = append(fixedTags, tag)
   464  			}
   465  		case ispec.MediaTypeImageIndex:
   466  			fixedManifests := []cvemodel.DescriptorInfo{}
   467  
   468  			// If the latest update inside the index is before the earliest vulnerability found then
   469  			// the index can't contain a fix
   470  			if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
   471  				continue
   472  			}
   473  
   474  			vulnTagInfo, indexHasVulnerableManifest := vulnerableTagMap[tag.Tag]
   475  
   476  			for _, manifestDesc := range tag.Manifests {
   477  				if manifestDesc.Timestamp.Before(earliestVulnerable.Timestamp) {
   478  					// The vulnerability did not exist at the time this image was built
   479  					continue
   480  				}
   481  
   482  				// check if the current manifest doesn't have the vulnerability
   483  				if !indexHasVulnerableManifest || !containsDescriptorInfo(vulnTagInfo.Manifests, manifestDesc) {
   484  					fixedManifests = append(fixedManifests, manifestDesc)
   485  				}
   486  			}
   487  
   488  			if len(fixedManifests) > 0 {
   489  				fixedTag := tag
   490  				fixedTag.Manifests = fixedManifests
   491  
   492  				fixedTags = append(fixedTags, fixedTag)
   493  			}
   494  		default:
   495  			continue
   496  		}
   497  	}
   498  
   499  	return fixedTags
   500  }
   501  
   502  func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvemodel.DescriptorInfo) bool {
   503  	for _, di := range slice {
   504  		if di.Digest == descriptorInfo.Digest {
   505  			return true
   506  		}
   507  	}
   508  
   509  	return false
   510  }