mirror of
https://github.com/project-zot/zot.git
synced 2025-01-20 22:52:51 -05:00
7c78f80a96
1. Move existing CVE DB download generator/task login under the cve package 2. Add a new CVE scanner task generator and task type to run in the background, as well as tests for it 3. Move the CVE cache in its own package 4. Add a CVE scanner methods to check if an entry is present in the cache, and to retreive the results 5. Modify the FilterTags MetaDB method to not exit on first error This is needed in order to pass all tags to the generator, instead of the generator stopping at the first set of invalid data 6. Integrate the new scanning task generator with the existing zot code. 7. Fix an issue where the CVE scan results for multiarch images was not cached 8. Rewrite some of the older CVE tests to use the new image-utils test package 9. Use the CVE scanner as attribute of the controller instead of CveInfo. Remove functionality of CVE DB update from CveInfo, it is now responsible, as the name states, only for providing CVE information. 10. The logic to get maximum severity and cve count for image sumaries now uses only the scanner cache. 11. Removed the GetCVESummaryForImage method from CveInfo as it was only used in tests Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
582 lines
14 KiB
Go
582 lines
14 KiB
Go
package trivy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"sync"
|
|
|
|
"github.com/aquasecurity/trivy-db/pkg/metadata"
|
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
|
"github.com/aquasecurity/trivy/pkg/commands/artifact"
|
|
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
|
fanalTypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
|
"github.com/aquasecurity/trivy/pkg/flag"
|
|
"github.com/aquasecurity/trivy/pkg/javadb"
|
|
"github.com/aquasecurity/trivy/pkg/types"
|
|
regTypes "github.com/google/go-containerregistry/pkg/v1/types"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
_ "modernc.org/sqlite"
|
|
|
|
zerr "zotregistry.io/zot/errors"
|
|
zcommon "zotregistry.io/zot/pkg/common"
|
|
cvecache "zotregistry.io/zot/pkg/extensions/search/cve/cache"
|
|
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
|
|
"zotregistry.io/zot/pkg/log"
|
|
mcommon "zotregistry.io/zot/pkg/meta/common"
|
|
mTypes "zotregistry.io/zot/pkg/meta/types"
|
|
"zotregistry.io/zot/pkg/storage"
|
|
)
|
|
|
|
const cacheSize = 1000000
|
|
|
|
// getNewScanOptions sets trivy configuration values for our scans and returns them as
|
|
// a trivy Options structure.
|
|
func getNewScanOptions(dir, dbRepository, javaDBRepository string) *flag.Options {
|
|
scanOptions := flag.Options{
|
|
GlobalOptions: flag.GlobalOptions{
|
|
CacheDir: dir,
|
|
},
|
|
ScanOptions: flag.ScanOptions{
|
|
Scanners: types.Scanners{types.VulnerabilityScanner},
|
|
OfflineScan: true,
|
|
},
|
|
VulnerabilityOptions: flag.VulnerabilityOptions{
|
|
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
|
|
},
|
|
DBOptions: flag.DBOptions{
|
|
DBRepository: dbRepository,
|
|
JavaDBRepository: javaDBRepository,
|
|
SkipDBUpdate: true,
|
|
SkipJavaDBUpdate: true,
|
|
},
|
|
ReportOptions: flag.ReportOptions{
|
|
Format: "table",
|
|
Severities: []dbTypes.Severity{
|
|
dbTypes.SeverityUnknown,
|
|
dbTypes.SeverityLow,
|
|
dbTypes.SeverityMedium,
|
|
dbTypes.SeverityHigh,
|
|
dbTypes.SeverityCritical,
|
|
},
|
|
},
|
|
}
|
|
|
|
return &scanOptions
|
|
}
|
|
|
|
type cveTrivyController struct {
|
|
DefaultCveConfig *flag.Options
|
|
SubCveConfig map[string]*flag.Options
|
|
}
|
|
|
|
type Scanner struct {
|
|
metaDB mTypes.MetaDB
|
|
cveController cveTrivyController
|
|
storeController storage.StoreController
|
|
log log.Logger
|
|
dbLock *sync.Mutex
|
|
cache *cvecache.CveCache
|
|
dbRepository string
|
|
javaDBRepository string
|
|
}
|
|
|
|
func NewScanner(storeController storage.StoreController,
|
|
metaDB mTypes.MetaDB, dbRepository, javaDBRepository string, log log.Logger,
|
|
) *Scanner {
|
|
cveController := cveTrivyController{}
|
|
|
|
subCveConfig := make(map[string]*flag.Options)
|
|
|
|
if storeController.DefaultStore != nil {
|
|
imageStore := storeController.DefaultStore
|
|
|
|
rootDir := imageStore.RootDir()
|
|
|
|
cacheDir := path.Join(rootDir, "_trivy")
|
|
opts := getNewScanOptions(cacheDir, dbRepository, javaDBRepository)
|
|
|
|
cveController.DefaultCveConfig = opts
|
|
}
|
|
|
|
if storeController.SubStore != nil {
|
|
for route, storage := range storeController.SubStore {
|
|
rootDir := storage.RootDir()
|
|
|
|
cacheDir := path.Join(rootDir, "_trivy")
|
|
opts := getNewScanOptions(cacheDir, dbRepository, javaDBRepository)
|
|
|
|
subCveConfig[route] = opts
|
|
}
|
|
}
|
|
|
|
cveController.SubCveConfig = subCveConfig
|
|
|
|
return &Scanner{
|
|
log: log,
|
|
metaDB: metaDB,
|
|
cveController: cveController,
|
|
storeController: storeController,
|
|
dbLock: &sync.Mutex{},
|
|
cache: cvecache.NewCveCache(cacheSize, log),
|
|
dbRepository: dbRepository,
|
|
javaDBRepository: javaDBRepository,
|
|
}
|
|
}
|
|
|
|
func (scanner Scanner) getTrivyOptions(image string) flag.Options {
|
|
// Split image to get route prefix
|
|
prefixName := storage.GetRoutePrefix(image)
|
|
|
|
var opts flag.Options
|
|
|
|
var ok bool
|
|
|
|
var rootDir string
|
|
|
|
// Get corresponding CVE trivy config, if no sub cve config present that means its default
|
|
_, ok = scanner.cveController.SubCveConfig[prefixName]
|
|
if ok {
|
|
opts = *scanner.cveController.SubCveConfig[prefixName]
|
|
|
|
imgStore := scanner.storeController.SubStore[prefixName]
|
|
|
|
rootDir = imgStore.RootDir()
|
|
} else {
|
|
opts = *scanner.cveController.DefaultCveConfig
|
|
|
|
imgStore := scanner.storeController.DefaultStore
|
|
|
|
rootDir = imgStore.RootDir()
|
|
}
|
|
|
|
opts.ScanOptions.Target = path.Join(rootDir, image)
|
|
opts.ImageOptions.Input = path.Join(rootDir, image)
|
|
|
|
return opts
|
|
}
|
|
|
|
func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) {
|
|
ctx := context.Background()
|
|
|
|
err := scanner.checkDBPresence()
|
|
if err != nil {
|
|
return types.Report{}, err
|
|
}
|
|
|
|
runner, err := artifact.NewRunner(ctx, opts)
|
|
if err != nil {
|
|
return types.Report{}, err
|
|
}
|
|
defer runner.Close(ctx)
|
|
|
|
report, err := runner.ScanImage(ctx, opts)
|
|
if err != nil {
|
|
return types.Report{}, err
|
|
}
|
|
|
|
report, err = runner.Filter(ctx, opts, report)
|
|
if err != nil {
|
|
return types.Report{}, err
|
|
}
|
|
|
|
return report, nil
|
|
}
|
|
|
|
func (scanner Scanner) IsImageFormatScannable(repo, ref string) (bool, error) {
|
|
var (
|
|
digestStr = ref
|
|
mediaType string
|
|
)
|
|
|
|
if zcommon.IsTag(ref) {
|
|
imgDescriptor, err := mcommon.GetImageDescriptor(scanner.metaDB, repo, ref)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
digestStr = imgDescriptor.Digest
|
|
mediaType = imgDescriptor.MediaType
|
|
} else {
|
|
var found bool
|
|
|
|
found, mediaType = mcommon.FindMediaTypeForDigest(scanner.metaDB, godigest.Digest(ref))
|
|
if !found {
|
|
return false, zerr.ErrManifestNotFound
|
|
}
|
|
}
|
|
|
|
return scanner.IsImageMediaScannable(repo, digestStr, mediaType)
|
|
}
|
|
|
|
func (scanner Scanner) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) {
|
|
image := repo + "@" + digestStr
|
|
|
|
switch mediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
ok, err := scanner.isManifestScanable(digestStr)
|
|
if err != nil {
|
|
return ok, fmt.Errorf("image '%s' %w", image, err)
|
|
}
|
|
|
|
return ok, nil
|
|
case ispec.MediaTypeImageIndex:
|
|
ok, err := scanner.isIndexScanable(digestStr)
|
|
if err != nil {
|
|
return ok, fmt.Errorf("image '%s' %w", image, err)
|
|
}
|
|
|
|
return ok, nil
|
|
default:
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) {
|
|
if scanner.cache.Get(digestStr) != nil {
|
|
return true, nil
|
|
}
|
|
|
|
manifestData, err := scanner.metaDB.GetManifestData(godigest.Digest(digestStr))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var manifestContent ispec.Manifest
|
|
|
|
err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent)
|
|
if err != nil {
|
|
scanner.log.Error().Err(err).Msg("unable to unmashal manifest blob")
|
|
|
|
return false, zerr.ErrScanNotSupported
|
|
}
|
|
|
|
for _, imageLayer := range manifestContent.Layers {
|
|
switch imageLayer.MediaType {
|
|
case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):
|
|
continue
|
|
default:
|
|
return false, zerr.ErrScanNotSupported
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (scanner Scanner) isIndexScanable(digestStr string) (bool, error) {
|
|
if scanner.cache.Get(digestStr) != nil {
|
|
return true, nil
|
|
}
|
|
|
|
indexData, err := scanner.metaDB.GetIndexData(godigest.Digest(digestStr))
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var indexContent ispec.Index
|
|
|
|
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if len(indexContent.Manifests) == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
for _, manifest := range indexContent.Manifests {
|
|
isScannable, err := scanner.isManifestScanable(manifest.Digest.String())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
// if at least 1 manifest is scanable, the whole index is scanable
|
|
if isScannable {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (scanner Scanner) IsResultCached(digest string) bool {
|
|
// Check if the entry exists in cache without updating the recent-ness
|
|
return scanner.cache.Contains(digest)
|
|
}
|
|
|
|
func (scanner Scanner) GetCachedResult(digest string) map[string]cvemodel.CVE {
|
|
return scanner.cache.Get(digest)
|
|
}
|
|
|
|
func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) {
|
|
var (
|
|
originalImageInput = image
|
|
digest string
|
|
mediaType string
|
|
)
|
|
|
|
repo, ref, isTag := zcommon.GetImageDirAndReference(image)
|
|
|
|
digest = ref
|
|
|
|
if isTag {
|
|
imgDescriptor, err := mcommon.GetImageDescriptor(scanner.metaDB, repo, ref)
|
|
if err != nil {
|
|
return map[string]cvemodel.CVE{}, err
|
|
}
|
|
|
|
digest = imgDescriptor.Digest
|
|
mediaType = imgDescriptor.MediaType
|
|
} else {
|
|
var found bool
|
|
|
|
found, mediaType = mcommon.FindMediaTypeForDigest(scanner.metaDB, godigest.Digest(ref))
|
|
if !found {
|
|
return map[string]cvemodel.CVE{}, zerr.ErrManifestNotFound
|
|
}
|
|
}
|
|
|
|
var (
|
|
cveIDMap map[string]cvemodel.CVE
|
|
err error
|
|
)
|
|
|
|
switch mediaType {
|
|
case ispec.MediaTypeImageIndex:
|
|
cveIDMap, err = scanner.scanIndex(repo, digest)
|
|
default:
|
|
cveIDMap, err = scanner.scanManifest(repo, digest)
|
|
}
|
|
|
|
if err != nil {
|
|
scanner.log.Error().Err(err).Str("image", originalImageInput).Msg("unable to scan image")
|
|
|
|
return map[string]cvemodel.CVE{}, err
|
|
}
|
|
|
|
return cveIDMap, nil
|
|
}
|
|
|
|
func (scanner Scanner) scanManifest(repo, digest string) (map[string]cvemodel.CVE, error) {
|
|
if cachedMap := scanner.cache.Get(digest); cachedMap != nil {
|
|
return cachedMap, nil
|
|
}
|
|
|
|
cveidMap := map[string]cvemodel.CVE{}
|
|
image := repo + "@" + digest
|
|
|
|
scanner.dbLock.Lock()
|
|
opts := scanner.getTrivyOptions(image)
|
|
report, err := scanner.runTrivy(opts)
|
|
scanner.dbLock.Unlock()
|
|
|
|
if err != nil { //nolint: wsl
|
|
return cveidMap, err
|
|
}
|
|
|
|
for _, result := range report.Results {
|
|
for _, vulnerability := range result.Vulnerabilities {
|
|
pkgName := vulnerability.PkgName
|
|
|
|
installedVersion := vulnerability.InstalledVersion
|
|
|
|
var fixedVersion string
|
|
if vulnerability.FixedVersion != "" {
|
|
fixedVersion = vulnerability.FixedVersion
|
|
} else {
|
|
fixedVersion = "Not Specified"
|
|
}
|
|
|
|
_, ok := cveidMap[vulnerability.VulnerabilityID]
|
|
if ok {
|
|
cveDetailStruct := cveidMap[vulnerability.VulnerabilityID]
|
|
|
|
pkgList := cveDetailStruct.PackageList
|
|
|
|
pkgList = append(
|
|
pkgList,
|
|
cvemodel.Package{
|
|
Name: pkgName,
|
|
InstalledVersion: installedVersion,
|
|
FixedVersion: fixedVersion,
|
|
},
|
|
)
|
|
|
|
cveDetailStruct.PackageList = pkgList
|
|
|
|
cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct
|
|
} else {
|
|
newPkgList := make([]cvemodel.Package, 0)
|
|
|
|
newPkgList = append(
|
|
newPkgList,
|
|
cvemodel.Package{
|
|
Name: pkgName,
|
|
InstalledVersion: installedVersion,
|
|
FixedVersion: fixedVersion,
|
|
},
|
|
)
|
|
|
|
cveidMap[vulnerability.VulnerabilityID] = cvemodel.CVE{
|
|
ID: vulnerability.VulnerabilityID,
|
|
Title: vulnerability.Title,
|
|
Description: vulnerability.Description,
|
|
Severity: convertSeverity(vulnerability.Severity),
|
|
PackageList: newPkgList,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
scanner.cache.Add(digest, cveidMap)
|
|
|
|
return cveidMap, nil
|
|
}
|
|
|
|
func (scanner Scanner) scanIndex(repo, digest string) (map[string]cvemodel.CVE, error) {
|
|
if cachedMap := scanner.cache.Get(digest); cachedMap != nil {
|
|
return cachedMap, nil
|
|
}
|
|
|
|
indexData, err := scanner.metaDB.GetIndexData(godigest.Digest(digest))
|
|
if err != nil {
|
|
return map[string]cvemodel.CVE{}, err
|
|
}
|
|
|
|
var indexContent ispec.Index
|
|
|
|
err = json.Unmarshal(indexData.IndexBlob, &indexContent)
|
|
if err != nil {
|
|
return map[string]cvemodel.CVE{}, err
|
|
}
|
|
|
|
indexCveIDMap := map[string]cvemodel.CVE{}
|
|
|
|
for _, manifest := range indexContent.Manifests {
|
|
if isScannable, err := scanner.isManifestScanable(manifest.Digest.String()); isScannable && err == nil {
|
|
manifestCveIDMap, err := scanner.scanManifest(repo, manifest.Digest.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for vulnerabilityID, CVE := range manifestCveIDMap {
|
|
indexCveIDMap[vulnerabilityID] = CVE
|
|
}
|
|
}
|
|
}
|
|
|
|
scanner.cache.Add(digest, indexCveIDMap)
|
|
|
|
return indexCveIDMap, nil
|
|
}
|
|
|
|
// UpdateDB downloads the Trivy DB / Cache under the store root directory.
|
|
func (scanner Scanner) UpdateDB() error {
|
|
// We need a lock as using multiple substores each with its own DB
|
|
// can result in a DATARACE because some varibles in trivy-db are global
|
|
// https://github.com/project-zot/trivy-db/blob/main/pkg/db/db.go#L23
|
|
scanner.dbLock.Lock()
|
|
defer scanner.dbLock.Unlock()
|
|
|
|
if scanner.storeController.DefaultStore != nil {
|
|
dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy")
|
|
|
|
err := scanner.updateDB(dbDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if scanner.storeController.SubStore != nil {
|
|
for _, storage := range scanner.storeController.SubStore {
|
|
dbDir := path.Join(storage.RootDir(), "_trivy")
|
|
|
|
err := scanner.updateDB(dbDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
scanner.cache.Purge()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (scanner Scanner) updateDB(dbDir string) error {
|
|
scanner.log.Debug().Str("dbDir", dbDir).Msg("Download Trivy DB to destination dir")
|
|
|
|
ctx := context.Background()
|
|
|
|
registryOpts := fanalTypes.RegistryOptions{Insecure: false}
|
|
|
|
scanner.log.Debug().Str("dbDir", dbDir).Msg("Started downloading Trivy DB to destination dir")
|
|
|
|
err := operation.DownloadDB(ctx, "dev", dbDir, scanner.dbRepository, false, false, registryOpts)
|
|
if err != nil {
|
|
scanner.log.Error().Err(err).Str("dbDir", dbDir).
|
|
Str("dbRepository", scanner.dbRepository).Msg("Error downloading Trivy DB to destination dir")
|
|
|
|
return err
|
|
}
|
|
|
|
if scanner.javaDBRepository != "" {
|
|
javadb.Init(dbDir, scanner.javaDBRepository, false, false, registryOpts.Insecure)
|
|
|
|
if err := javadb.Update(); err != nil {
|
|
scanner.log.Error().Err(err).Str("dbDir", dbDir).
|
|
Str("javaDBRepository", scanner.javaDBRepository).Msg("Error downloading Trivy Java DB to destination dir")
|
|
|
|
return err
|
|
}
|
|
}
|
|
|
|
scanner.log.Debug().Str("dbDir", dbDir).Msg("Finished downloading Trivy DB to destination dir")
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkDBPresence errors if the DB metadata files cannot be accessed.
|
|
func (scanner Scanner) checkDBPresence() error {
|
|
result := true
|
|
|
|
if scanner.storeController.DefaultStore != nil {
|
|
dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy")
|
|
if _, err := os.Stat(metadata.Path(dbDir)); err != nil {
|
|
result = false
|
|
}
|
|
}
|
|
|
|
if scanner.storeController.SubStore != nil {
|
|
for _, storage := range scanner.storeController.SubStore {
|
|
dbDir := path.Join(storage.RootDir(), "_trivy")
|
|
|
|
if _, err := os.Stat(metadata.Path(dbDir)); err != nil {
|
|
result = false
|
|
}
|
|
}
|
|
}
|
|
|
|
if !result {
|
|
return zerr.ErrCVEDBNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func convertSeverity(detectedSeverity string) string {
|
|
trivySeverity, _ := dbTypes.NewSeverity(detectedSeverity)
|
|
|
|
sevMap := map[dbTypes.Severity]string{
|
|
dbTypes.SeverityUnknown: cvemodel.SeverityUnknown,
|
|
dbTypes.SeverityLow: cvemodel.SeverityLow,
|
|
dbTypes.SeverityMedium: cvemodel.SeverityMedium,
|
|
dbTypes.SeverityHigh: cvemodel.SeverityHigh,
|
|
dbTypes.SeverityCritical: cvemodel.SeverityCritical,
|
|
}
|
|
|
|
return sevMap[trivySeverity]
|
|
}
|