zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/search/cve/trivy/scanner.go (about)

     1  package trivy
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"path"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/aquasecurity/trivy-db/pkg/metadata"
    12  	dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
    13  	"github.com/aquasecurity/trivy/pkg/commands/artifact"
    14  	"github.com/aquasecurity/trivy/pkg/commands/operation"
    15  	fanalTypes "github.com/aquasecurity/trivy/pkg/fanal/types"
    16  	"github.com/aquasecurity/trivy/pkg/flag"
    17  	"github.com/aquasecurity/trivy/pkg/javadb"
    18  	"github.com/aquasecurity/trivy/pkg/types"
    19  	regTypes "github.com/google/go-containerregistry/pkg/v1/types"
    20  	godigest "github.com/opencontainers/go-digest"
    21  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    22  	_ "modernc.org/sqlite"
    23  
    24  	zerr "zotregistry.dev/zot/errors"
    25  	zcommon "zotregistry.dev/zot/pkg/common"
    26  	cvecache "zotregistry.dev/zot/pkg/extensions/search/cve/cache"
    27  	cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model"
    28  	"zotregistry.dev/zot/pkg/log"
    29  	mTypes "zotregistry.dev/zot/pkg/meta/types"
    30  	"zotregistry.dev/zot/pkg/storage"
    31  )
    32  
    33  const cacheSize = 1000000
    34  
    35  // getNewScanOptions sets trivy configuration values for our scans and returns them as
    36  // a trivy Options structure.
    37  func getNewScanOptions(dir, dbRepository, javaDBRepository string) *flag.Options {
    38  	scanOptions := flag.Options{
    39  		GlobalOptions: flag.GlobalOptions{
    40  			CacheDir: dir,
    41  		},
    42  		ScanOptions: flag.ScanOptions{
    43  			Scanners:    types.Scanners{types.VulnerabilityScanner},
    44  			OfflineScan: true,
    45  		},
    46  		VulnerabilityOptions: flag.VulnerabilityOptions{
    47  			VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
    48  		},
    49  		DBOptions: flag.DBOptions{
    50  			DBRepository:     dbRepository,
    51  			JavaDBRepository: javaDBRepository,
    52  			SkipDBUpdate:     true,
    53  			SkipJavaDBUpdate: true,
    54  		},
    55  		ReportOptions: flag.ReportOptions{
    56  			Format: "table",
    57  			Severities: []dbTypes.Severity{
    58  				dbTypes.SeverityUnknown,
    59  				dbTypes.SeverityLow,
    60  				dbTypes.SeverityMedium,
    61  				dbTypes.SeverityHigh,
    62  				dbTypes.SeverityCritical,
    63  			},
    64  		},
    65  	}
    66  
    67  	return &scanOptions
    68  }
    69  
    70  type cveTrivyController struct {
    71  	DefaultCveConfig *flag.Options
    72  	SubCveConfig     map[string]*flag.Options
    73  }
    74  
    75  type Scanner struct {
    76  	metaDB           mTypes.MetaDB
    77  	cveController    cveTrivyController
    78  	storeController  storage.StoreController
    79  	log              log.Logger
    80  	dbLock           *sync.Mutex
    81  	cache            *cvecache.CveCache
    82  	dbRepository     string
    83  	javaDBRepository string
    84  }
    85  
    86  func NewScanner(storeController storage.StoreController,
    87  	metaDB mTypes.MetaDB, dbRepository, javaDBRepository string, log log.Logger,
    88  ) *Scanner {
    89  	cveController := cveTrivyController{}
    90  
    91  	subCveConfig := make(map[string]*flag.Options)
    92  
    93  	if storeController.DefaultStore != nil {
    94  		imageStore := storeController.DefaultStore
    95  
    96  		rootDir := imageStore.RootDir()
    97  
    98  		cacheDir := path.Join(rootDir, "_trivy")
    99  		opts := getNewScanOptions(cacheDir, dbRepository, javaDBRepository)
   100  
   101  		cveController.DefaultCveConfig = opts
   102  	}
   103  
   104  	if storeController.SubStore != nil {
   105  		for route, storage := range storeController.SubStore {
   106  			rootDir := storage.RootDir()
   107  
   108  			cacheDir := path.Join(rootDir, "_trivy")
   109  			opts := getNewScanOptions(cacheDir, dbRepository, javaDBRepository)
   110  
   111  			subCveConfig[route] = opts
   112  		}
   113  	}
   114  
   115  	cveController.SubCveConfig = subCveConfig
   116  
   117  	return &Scanner{
   118  		log:              log,
   119  		metaDB:           metaDB,
   120  		cveController:    cveController,
   121  		storeController:  storeController,
   122  		dbLock:           &sync.Mutex{},
   123  		cache:            cvecache.NewCveCache(cacheSize, log),
   124  		dbRepository:     dbRepository,
   125  		javaDBRepository: javaDBRepository,
   126  	}
   127  }
   128  
   129  func (scanner Scanner) getTrivyOptions(image string) flag.Options {
   130  	// Split image to get route prefix
   131  	prefixName := storage.GetRoutePrefix(image)
   132  
   133  	var opts flag.Options
   134  
   135  	var ok bool
   136  
   137  	var rootDir string
   138  
   139  	// Get corresponding CVE trivy config, if no sub cve config present that means its default
   140  	_, ok = scanner.cveController.SubCveConfig[prefixName]
   141  	if ok {
   142  		opts = *scanner.cveController.SubCveConfig[prefixName]
   143  
   144  		imgStore := scanner.storeController.SubStore[prefixName]
   145  
   146  		rootDir = imgStore.RootDir()
   147  	} else {
   148  		opts = *scanner.cveController.DefaultCveConfig
   149  
   150  		imgStore := scanner.storeController.DefaultStore
   151  
   152  		rootDir = imgStore.RootDir()
   153  	}
   154  
   155  	opts.ScanOptions.Target = path.Join(rootDir, image)
   156  	opts.ImageOptions.Input = path.Join(rootDir, image)
   157  
   158  	return opts
   159  }
   160  
   161  func (scanner Scanner) runTrivy(ctx context.Context, opts flag.Options) (types.Report, error) {
   162  	err := scanner.checkDBPresence()
   163  	if err != nil {
   164  		return types.Report{}, err
   165  	}
   166  
   167  	runner, err := artifact.NewRunner(ctx, opts)
   168  	if err != nil {
   169  		return types.Report{}, err
   170  	}
   171  	defer runner.Close(ctx)
   172  
   173  	report, err := runner.ScanImage(ctx, opts)
   174  	if err != nil {
   175  		return types.Report{}, err
   176  	}
   177  
   178  	report, err = runner.Filter(ctx, opts, report)
   179  	if err != nil {
   180  		return types.Report{}, err
   181  	}
   182  
   183  	return report, nil
   184  }
   185  
   186  func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) {
   187  	var (
   188  		digestStr = ref
   189  		mediaType string
   190  	)
   191  
   192  	if zcommon.IsTag(ref) {
   193  		imgDescriptor, err := getImageDescriptor(context.Background(), scanner.metaDB, repo, ref)
   194  		if err != nil {
   195  			return false, err
   196  		}
   197  
   198  		digestStr = imgDescriptor.Digest
   199  		mediaType = imgDescriptor.MediaType
   200  	} else {
   201  		var found bool
   202  
   203  		found, mediaType = findMediaTypeForDigest(scanner.metaDB, godigest.Digest(ref))
   204  		if !found {
   205  			return false, zerr.ErrManifestNotFound
   206  		}
   207  	}
   208  
   209  	return scanner.IsImageMediaScannable(repo, digestStr, mediaType)
   210  }
   211  
   212  func (scanner Scanner) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) {
   213  	image := repo + "@" + digestStr
   214  
   215  	switch mediaType {
   216  	case ispec.MediaTypeImageManifest:
   217  		ok, err := scanner.isManifestScanable(digestStr)
   218  		if err != nil {
   219  			return ok, fmt.Errorf("image '%s' %w", image, err)
   220  		}
   221  
   222  		return ok, nil
   223  	case ispec.MediaTypeImageIndex:
   224  		ok, err := scanner.isIndexScannable(digestStr)
   225  		if err != nil {
   226  			return ok, fmt.Errorf("image '%s' %w", image, err)
   227  		}
   228  
   229  		return ok, nil
   230  	default:
   231  		return false, nil
   232  	}
   233  }
   234  
   235  func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) {
   236  	if scanner.cache.Get(digestStr) != nil {
   237  		return true, nil
   238  	}
   239  
   240  	manifestData, err := scanner.metaDB.GetImageMeta(godigest.Digest(digestStr))
   241  	if err != nil {
   242  		return false, err
   243  	}
   244  
   245  	for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers {
   246  		switch imageLayer.MediaType {
   247  		case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):
   248  			continue
   249  		default:
   250  			return false, zerr.ErrScanNotSupported
   251  		}
   252  	}
   253  
   254  	return true, nil
   255  }
   256  
   257  func (scanner Scanner) isManifestDataScannable(manifestData mTypes.ManifestMeta) (bool, error) {
   258  	if scanner.cache.Get(manifestData.Digest.String()) != nil {
   259  		return true, nil
   260  	}
   261  
   262  	for _, imageLayer := range manifestData.Manifest.Layers {
   263  		switch imageLayer.MediaType {
   264  		case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):
   265  			continue
   266  		default:
   267  			return false, zerr.ErrScanNotSupported
   268  		}
   269  	}
   270  
   271  	return true, nil
   272  }
   273  
   274  func (scanner Scanner) isIndexScannable(digestStr string) (bool, error) {
   275  	if scanner.cache.Get(digestStr) != nil {
   276  		return true, nil
   277  	}
   278  
   279  	indexData, err := scanner.metaDB.GetImageMeta(godigest.Digest(digestStr))
   280  	if err != nil {
   281  		return false, err
   282  	}
   283  
   284  	if indexData.Index == nil {
   285  		return false, zerr.ErrUnexpectedMediaType
   286  	}
   287  
   288  	indexContent := *indexData.Index
   289  
   290  	if len(indexContent.Manifests) == 0 {
   291  		return true, nil
   292  	}
   293  
   294  	for _, manifest := range indexData.Manifests {
   295  		isScannable, err := scanner.isManifestDataScannable(manifest)
   296  		if err != nil {
   297  			continue
   298  		}
   299  
   300  		// if at least 1 manifest is scannable, the whole index is scannable
   301  		if isScannable {
   302  			return true, nil
   303  		}
   304  	}
   305  
   306  	return false, nil
   307  }
   308  
   309  func (scanner Scanner) IsResultCached(digest string) bool {
   310  	// Check if the entry exists in cache without updating the recent-ness
   311  	return scanner.cache.Contains(digest)
   312  }
   313  
   314  func (scanner Scanner) GetCachedResult(digest string) map[string]cvemodel.CVE {
   315  	return scanner.cache.Get(digest)
   316  }
   317  
   318  func (scanner Scanner) ScanImage(ctx context.Context, image string) (map[string]cvemodel.CVE, error) {
   319  	var (
   320  		originalImageInput = image
   321  		digest             string
   322  		mediaType          string
   323  	)
   324  
   325  	repo, ref, isTag := zcommon.GetImageDirAndReference(image)
   326  
   327  	digest = ref
   328  
   329  	if isTag {
   330  		imgDescriptor, err := getImageDescriptor(ctx, scanner.metaDB, repo, ref)
   331  		if err != nil {
   332  			return map[string]cvemodel.CVE{}, err
   333  		}
   334  
   335  		digest = imgDescriptor.Digest
   336  		mediaType = imgDescriptor.MediaType
   337  	} else {
   338  		var found bool
   339  
   340  		found, mediaType = findMediaTypeForDigest(scanner.metaDB, godigest.Digest(ref))
   341  		if !found {
   342  			return map[string]cvemodel.CVE{}, zerr.ErrManifestNotFound
   343  		}
   344  	}
   345  
   346  	var (
   347  		cveIDMap map[string]cvemodel.CVE
   348  		err      error
   349  	)
   350  
   351  	switch mediaType {
   352  	case ispec.MediaTypeImageIndex:
   353  		cveIDMap, err = scanner.scanIndex(ctx, repo, digest)
   354  	default:
   355  		cveIDMap, err = scanner.scanManifest(ctx, repo, digest)
   356  	}
   357  
   358  	if err != nil {
   359  		scanner.log.Error().Err(err).Str("image", originalImageInput).Msg("failed to scan image")
   360  
   361  		return map[string]cvemodel.CVE{}, err
   362  	}
   363  
   364  	return cveIDMap, nil
   365  }
   366  
   367  func (scanner Scanner) scanManifest(ctx context.Context, repo, digest string) (map[string]cvemodel.CVE, error) {
   368  	if cachedMap := scanner.cache.Get(digest); cachedMap != nil {
   369  		return cachedMap, nil
   370  	}
   371  
   372  	cveidMap := map[string]cvemodel.CVE{}
   373  	image := repo + "@" + digest
   374  
   375  	scanner.dbLock.Lock()
   376  	opts := scanner.getTrivyOptions(image)
   377  	report, err := scanner.runTrivy(ctx, opts)
   378  	scanner.dbLock.Unlock()
   379  
   380  	if err != nil { //nolint: wsl
   381  		return cveidMap, err
   382  	}
   383  
   384  	for _, result := range report.Results {
   385  		for _, vulnerability := range result.Vulnerabilities {
   386  			pkgName := vulnerability.PkgName
   387  
   388  			installedVersion := vulnerability.InstalledVersion
   389  
   390  			var fixedVersion string
   391  			if vulnerability.FixedVersion != "" {
   392  				fixedVersion = vulnerability.FixedVersion
   393  			} else {
   394  				fixedVersion = "Not Specified"
   395  			}
   396  
   397  			var packagePath string
   398  			if vulnerability.PkgPath != "" {
   399  				packagePath = vulnerability.PkgPath
   400  			} else {
   401  				packagePath = "Not Specified"
   402  			}
   403  
   404  			_, ok := cveidMap[vulnerability.VulnerabilityID]
   405  			if ok {
   406  				cveDetailStruct := cveidMap[vulnerability.VulnerabilityID]
   407  
   408  				pkgList := cveDetailStruct.PackageList
   409  
   410  				pkgList = append(
   411  					pkgList,
   412  					cvemodel.Package{
   413  						Name:             pkgName,
   414  						PackagePath:      packagePath,
   415  						InstalledVersion: installedVersion,
   416  						FixedVersion:     fixedVersion,
   417  					},
   418  				)
   419  
   420  				cveDetailStruct.PackageList = pkgList
   421  
   422  				cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct
   423  			} else {
   424  				newPkgList := make([]cvemodel.Package, 0)
   425  
   426  				newPkgList = append(
   427  					newPkgList,
   428  					cvemodel.Package{
   429  						Name:             pkgName,
   430  						PackagePath:      packagePath,
   431  						InstalledVersion: installedVersion,
   432  						FixedVersion:     fixedVersion,
   433  					},
   434  				)
   435  
   436  				cveidMap[vulnerability.VulnerabilityID] = cvemodel.CVE{
   437  					ID:          vulnerability.VulnerabilityID,
   438  					Title:       vulnerability.Title,
   439  					Description: vulnerability.Description,
   440  					Reference:   getCVEReference(vulnerability.PrimaryURL, vulnerability.References),
   441  					Severity:    convertSeverity(vulnerability.Severity),
   442  					PackageList: newPkgList,
   443  				}
   444  			}
   445  		}
   446  	}
   447  
   448  	scanner.cache.Add(digest, cveidMap)
   449  
   450  	return cveidMap, nil
   451  }
   452  
   453  func getCVEReference(primaryURL string, references []string) string {
   454  	if primaryURL != "" {
   455  		return primaryURL
   456  	}
   457  
   458  	if len(references) > 0 {
   459  		nvdReference, found := getNVDReference(references)
   460  
   461  		if found {
   462  			return nvdReference
   463  		}
   464  
   465  		return references[0]
   466  	}
   467  
   468  	return ""
   469  }
   470  
   471  func getNVDReference(references []string) (string, bool) {
   472  	for i := range references {
   473  		if strings.Contains(references[i], "nvd.nist.gov") {
   474  			return references[i], true
   475  		}
   476  	}
   477  
   478  	return "", false
   479  }
   480  
   481  func (scanner Scanner) scanIndex(ctx context.Context, repo, digest string) (map[string]cvemodel.CVE, error) {
   482  	if cachedMap := scanner.cache.Get(digest); cachedMap != nil {
   483  		return cachedMap, nil
   484  	}
   485  
   486  	indexData, err := scanner.metaDB.GetImageMeta(godigest.Digest(digest))
   487  	if err != nil {
   488  		return map[string]cvemodel.CVE{}, err
   489  	}
   490  
   491  	if indexData.Index == nil {
   492  		return map[string]cvemodel.CVE{}, zerr.ErrUnexpectedMediaType
   493  	}
   494  
   495  	indexCveIDMap := map[string]cvemodel.CVE{}
   496  
   497  	for _, manifest := range indexData.Index.Manifests {
   498  		if isScannable, err := scanner.isManifestScanable(manifest.Digest.String()); isScannable && err == nil {
   499  			manifestCveIDMap, err := scanner.scanManifest(ctx, repo, manifest.Digest.String())
   500  			if err != nil {
   501  				return nil, err
   502  			}
   503  
   504  			for vulnerabilityID, CVE := range manifestCveIDMap {
   505  				indexCveIDMap[vulnerabilityID] = CVE
   506  			}
   507  		}
   508  	}
   509  
   510  	scanner.cache.Add(digest, indexCveIDMap)
   511  
   512  	return indexCveIDMap, nil
   513  }
   514  
   515  // UpdateDB downloads the Trivy DB / Cache under the store root directory.
   516  func (scanner Scanner) UpdateDB(ctx context.Context) error {
   517  	// We need a lock as using multiple substores each with its own DB
   518  	// can result in a DATARACE because some varibles in trivy-db are global
   519  	// https://github.com/project-zot/trivy-db/blob/main/pkg/db/db.go#L23
   520  	scanner.dbLock.Lock()
   521  	defer scanner.dbLock.Unlock()
   522  
   523  	if scanner.storeController.DefaultStore != nil {
   524  		dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy")
   525  
   526  		err := scanner.updateDB(ctx, dbDir)
   527  		if err != nil {
   528  			return err
   529  		}
   530  	}
   531  
   532  	if scanner.storeController.SubStore != nil {
   533  		for _, storage := range scanner.storeController.SubStore {
   534  			dbDir := path.Join(storage.RootDir(), "_trivy")
   535  
   536  			err := scanner.updateDB(ctx, dbDir)
   537  			if err != nil {
   538  				return err
   539  			}
   540  		}
   541  	}
   542  
   543  	scanner.cache.Purge()
   544  
   545  	return nil
   546  }
   547  
   548  func (scanner Scanner) updateDB(ctx context.Context, dbDir string) error {
   549  	scanner.log.Debug().Str("dbDir", dbDir).Msg("download Trivy DB to destination dir")
   550  
   551  	registryOpts := fanalTypes.RegistryOptions{Insecure: false}
   552  
   553  	scanner.log.Debug().Str("dbDir", dbDir).Msg("started downloading trivy-db to destination dir")
   554  
   555  	err := operation.DownloadDB(ctx, "dev", dbDir, scanner.dbRepository, false, false, registryOpts)
   556  	if err != nil {
   557  		scanner.log.Error().Err(err).Str("dbDir", dbDir).
   558  			Str("dbRepository", scanner.dbRepository).Msg("failed to download trivy-db to destination dir")
   559  
   560  		return err
   561  	}
   562  
   563  	if scanner.javaDBRepository != "" {
   564  		javadb.Init(dbDir, scanner.javaDBRepository, false, false, registryOpts)
   565  
   566  		if err := javadb.Update(); err != nil {
   567  			scanner.log.Error().Err(err).Str("dbDir", dbDir).
   568  				Str("javaDBRepository", scanner.javaDBRepository).Msg("failed to download trivy-java-db to destination dir")
   569  
   570  			return err
   571  		}
   572  	}
   573  
   574  	scanner.log.Debug().Str("dbDir", dbDir).Msg("finished downloading trivy-db to destination dir")
   575  
   576  	return nil
   577  }
   578  
   579  // checkDBPresence errors if the DB metadata files cannot be accessed.
   580  func (scanner Scanner) checkDBPresence() error {
   581  	result := true
   582  
   583  	if scanner.storeController.DefaultStore != nil {
   584  		dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy")
   585  		if _, err := os.Stat(metadata.Path(dbDir)); err != nil {
   586  			result = false
   587  		}
   588  	}
   589  
   590  	if scanner.storeController.SubStore != nil {
   591  		for _, storage := range scanner.storeController.SubStore {
   592  			dbDir := path.Join(storage.RootDir(), "_trivy")
   593  
   594  			if _, err := os.Stat(metadata.Path(dbDir)); err != nil {
   595  				result = false
   596  			}
   597  		}
   598  	}
   599  
   600  	if !result {
   601  		return zerr.ErrCVEDBNotFound
   602  	}
   603  
   604  	return nil
   605  }
   606  
   607  func getImageDescriptor(ctx context.Context, metaDB mTypes.MetaDB, repo, tag string) (mTypes.Descriptor, error) {
   608  	repoMeta, err := metaDB.GetRepoMeta(ctx, repo)
   609  	if err != nil {
   610  		return mTypes.Descriptor{}, err
   611  	}
   612  
   613  	imageDescriptor, ok := repoMeta.Tags[tag]
   614  	if !ok {
   615  		return mTypes.Descriptor{}, zerr.ErrTagMetaNotFound
   616  	}
   617  
   618  	return imageDescriptor, nil
   619  }
   620  
   621  // findMediaTypeForDigest will look into the buckets for a certain digest. Depending on which bucket that
   622  // digest is found the corresponding mediatype is returned.
   623  func findMediaTypeForDigest(metaDB mTypes.MetaDB, digest godigest.Digest) (bool, string) {
   624  	imageMeta, err := metaDB.GetImageMeta(digest)
   625  	if err == nil {
   626  		return true, imageMeta.MediaType
   627  	}
   628  
   629  	return false, ""
   630  }
   631  
   632  func convertSeverity(detectedSeverity string) string {
   633  	trivySeverity, _ := dbTypes.NewSeverity(detectedSeverity)
   634  
   635  	sevMap := map[dbTypes.Severity]string{
   636  		dbTypes.SeverityUnknown:  cvemodel.SeverityUnknown,
   637  		dbTypes.SeverityLow:      cvemodel.SeverityLow,
   638  		dbTypes.SeverityMedium:   cvemodel.SeverityMedium,
   639  		dbTypes.SeverityHigh:     cvemodel.SeverityHigh,
   640  		dbTypes.SeverityCritical: cvemodel.SeverityCritical,
   641  	}
   642  
   643  	return sevMap[trivySeverity]
   644  }