mirror of
https://github.com/project-zot/zot.git
synced 2024-12-23 22:27:35 -05:00
6f0c37079c
Signed-off-by: Nicol Draghici <idraghic@cisco.com>
386 lines
9.4 KiB
Go
386 lines
9.4 KiB
Go
package trivy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"sync"
|
|
|
|
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
|
"github.com/aquasecurity/trivy/pkg/commands/artifact"
|
|
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
|
"github.com/aquasecurity/trivy/pkg/flag"
|
|
"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"
|
|
|
|
zerr "zotregistry.io/zot/errors"
|
|
"zotregistry.io/zot/pkg/extensions/search/common"
|
|
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
|
|
"zotregistry.io/zot/pkg/log"
|
|
"zotregistry.io/zot/pkg/meta/repodb"
|
|
"zotregistry.io/zot/pkg/storage"
|
|
)
|
|
|
|
const defaultDBRepository = "ghcr.io/aquasecurity/trivy-db"
|
|
|
|
// getNewScanOptions sets trivy configuration values for our scans and returns them as
|
|
// a trivy Options structure.
|
|
func getNewScanOptions(dir, dbRepository string) *flag.Options {
|
|
scanOptions := flag.Options{
|
|
GlobalOptions: flag.GlobalOptions{
|
|
CacheDir: dir,
|
|
},
|
|
ScanOptions: flag.ScanOptions{
|
|
SecurityChecks: []string{types.SecurityCheckVulnerability},
|
|
OfflineScan: true,
|
|
},
|
|
VulnerabilityOptions: flag.VulnerabilityOptions{
|
|
VulnType: []string{types.VulnTypeOS, types.VulnTypeLibrary},
|
|
},
|
|
DBOptions: flag.DBOptions{
|
|
DBRepository: dbRepository,
|
|
SkipDBUpdate: 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 {
|
|
repoDB repodb.RepoDB
|
|
cveController cveTrivyController
|
|
storeController storage.StoreController
|
|
log log.Logger
|
|
dbLock *sync.Mutex
|
|
cache *CveCache
|
|
dbRepository string
|
|
}
|
|
|
|
func NewScanner(storeController storage.StoreController,
|
|
repoDB repodb.RepoDB, dbRepository string, log log.Logger,
|
|
) *Scanner {
|
|
cveController := cveTrivyController{}
|
|
|
|
subCveConfig := make(map[string]*flag.Options)
|
|
|
|
if dbRepository == "" {
|
|
dbRepository = defaultDBRepository
|
|
}
|
|
|
|
if storeController.DefaultStore != nil {
|
|
imageStore := storeController.DefaultStore
|
|
|
|
rootDir := imageStore.RootDir()
|
|
|
|
cacheDir := path.Join(rootDir, "_trivy")
|
|
opts := getNewScanOptions(cacheDir, dbRepository)
|
|
|
|
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)
|
|
|
|
subCveConfig[route] = opts
|
|
}
|
|
}
|
|
|
|
cveController.SubCveConfig = subCveConfig
|
|
|
|
return &Scanner{
|
|
log: log,
|
|
repoDB: repoDB,
|
|
cveController: cveController,
|
|
storeController: storeController,
|
|
dbLock: &sync.Mutex{},
|
|
cache: NewCveCache(10000, log), //nolint:gomnd
|
|
dbRepository: dbRepository,
|
|
}
|
|
}
|
|
|
|
func (scanner Scanner) getTrivyOptions(image string) flag.Options {
|
|
// Split image to get route prefix
|
|
prefixName := common.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()
|
|
|
|
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, tag string) (bool, error) {
|
|
image := repo + ":" + tag
|
|
|
|
if scanner.cache.Get(image) != nil {
|
|
return true, nil
|
|
}
|
|
|
|
repoMeta, err := scanner.repoDB.GetRepoMeta(repo)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var ok bool
|
|
|
|
imageDescriptor, ok := repoMeta.Tags[tag]
|
|
if !ok {
|
|
return false, zerr.ErrTagMetaNotFound
|
|
}
|
|
|
|
switch imageDescriptor.MediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
ok, err := scanner.isManifestScanable(imageDescriptor)
|
|
if err != nil {
|
|
return ok, fmt.Errorf("image '%s' %w", image, err)
|
|
}
|
|
|
|
return ok, nil
|
|
case ispec.MediaTypeImageIndex:
|
|
ok, err := scanner.isIndexScanable(imageDescriptor)
|
|
if err != nil {
|
|
return ok, fmt.Errorf("image '%s' %w", image, err)
|
|
}
|
|
|
|
return ok, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (scanner Scanner) isManifestScanable(descriptor repodb.Descriptor) (bool, error) {
|
|
manifestDigestStr := descriptor.Digest
|
|
|
|
manifestDigest, err := godigest.Parse(manifestDigestStr)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
manifestData, err := scanner.repoDB.GetManifestData(manifestDigest)
|
|
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:
|
|
scanner.log.Debug().
|
|
Msgf("image media type %s not supported for scanning", imageLayer.MediaType)
|
|
|
|
return false, zerr.ErrScanNotSupported
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (scanner Scanner) isIndexScanable(descriptor repodb.Descriptor) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) {
|
|
if scanner.cache.Get(image) != nil {
|
|
return scanner.cache.Get(image), nil
|
|
}
|
|
|
|
cveidMap := make(map[string]cvemodel.CVE)
|
|
|
|
scanner.log.Debug().Str("image", image).Msg("scanning image")
|
|
|
|
scanner.dbLock.Lock()
|
|
opts := scanner.getTrivyOptions(image)
|
|
report, err := scanner.runTrivy(opts)
|
|
scanner.dbLock.Unlock()
|
|
|
|
if err != nil { //nolint: wsl
|
|
scanner.log.Error().Err(err).Str("image", image).Msg("unable to scan image")
|
|
|
|
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: vulnerability.Severity,
|
|
PackageList: newPkgList,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
scanner.cache.Add(image, cveidMap)
|
|
|
|
return cveidMap, nil
|
|
}
|
|
|
|
// UpdateDb download the Trivy DB / Cache under the store root directory.
|
|
func (scanner Scanner) UpdateDB() error {
|
|
// We need a lock as using multiple substores each with it's 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().Msgf("Download Trivy DB to destination dir: %s", dbDir)
|
|
|
|
err := operation.DownloadDB("dev", dbDir, scanner.dbRepository, false, false, false)
|
|
if err != nil {
|
|
scanner.log.Error().Err(err).Msgf("Error downloading Trivy DB to destination dir: %s", dbDir)
|
|
|
|
return err
|
|
}
|
|
|
|
scanner.log.Debug().Msgf("Finished downloading Trivy DB to destination dir: %s", dbDir)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (scanner Scanner) CompareSeverities(severity1, severity2 string) int {
|
|
return dbTypes.CompareSeverityString(severity1, severity2)
|
|
}
|