0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-01-06 22:40:28 -05:00
zot/pkg/extensions/search/cve/trivy/scanner.go
LaurentiuNiculae 28de980319
feat(refator): refactoring repodb into meta (#1626)
Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
2023-07-18 10:27:26 -07:00

557 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"
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"
)
// 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
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: NewCveCache(10000, log), //nolint:gomnd
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:
scanner.log.Debug().Str("mediaType", imageLayer.MediaType).
Msg("image media type not supported for scanning")
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) 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: vulnerability.Severity,
PackageList: newPkgList,
}
}
}
}
scanner.cache.Add(digest, cveidMap)
return cveidMap, nil
}
func (scanner Scanner) scanIndex(repo, digest string) (map[string]cvemodel.CVE, error) {
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
}
}
}
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 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().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 (scanner Scanner) CompareSeverities(severity1, severity2 string) int {
return dbTypes.CompareSeverityString(severity1, severity2)
}