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

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