0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -05:00

feat(cve): implement CVE scanning as background tasks (#1833)

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>
This commit is contained in:
Andrei Aaron 2023-09-22 21:49:17 +03:00 committed by GitHub
parent 4e04be420e
commit 7c78f80a96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1812 additions and 807 deletions

View file

@ -348,6 +348,10 @@ func (c *Config) IsSearchEnabled() bool {
return c.Extensions != nil && c.Extensions.Search != nil && *c.Extensions.Search.Enable return c.Extensions != nil && c.Extensions.Search != nil && *c.Extensions.Search.Enable
} }
func (c *Config) IsCveScanningEnabled() bool {
return c.IsSearchEnabled() && c.Extensions.Search.CVE != nil
}
func (c *Config) IsUIEnabled() bool { func (c *Config) IsUIEnabled() bool {
return c.Extensions != nil && c.Extensions.UI != nil && *c.Extensions.UI.Enable return c.Extensions != nil && c.Extensions.UI != nil && *c.Extensions.UI.Enable
} }

View file

@ -46,7 +46,7 @@ type Controller struct {
Audit *log.Logger Audit *log.Logger
Server *http.Server Server *http.Server
Metrics monitoring.MetricServer Metrics monitoring.MetricServer
CveInfo ext.CveInfo CveScanner ext.CveScanner
SyncOnDemand SyncOnDemand SyncOnDemand SyncOnDemand
RelyingParties map[string]rp.RelyingParty RelyingParties map[string]rp.RelyingParty
CookieStore sessions.Store CookieStore sessions.Store
@ -241,7 +241,7 @@ func (c *Controller) Init(reloadCtx context.Context) error {
func (c *Controller) InitCVEInfo() { func (c *Controller) InitCVEInfo() {
// Enable CVE extension if extension config is provided // Enable CVE extension if extension config is provided
if c.Config != nil && c.Config.Extensions != nil { if c.Config != nil && c.Config.Extensions != nil {
c.CveInfo = ext.GetCVEInfo(c.Config, c.StoreController, c.MetaDB, c.Log) c.CveScanner = ext.GetCveScanner(c.Config, c.StoreController, c.MetaDB, c.Log)
} }
} }
@ -347,7 +347,7 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
// Enable extensions if extension config is provided for DefaultStore // Enable extensions if extension config is provided for DefaultStore
if c.Config != nil && c.Config.Extensions != nil { if c.Config != nil && c.Config.Extensions != nil {
ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory) ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory)
ext.EnableSearchExtension(c.Config, c.StoreController, c.MetaDB, taskScheduler, c.CveInfo, c.Log) ext.EnableSearchExtension(c.Config, c.StoreController, c.MetaDB, taskScheduler, c.CveScanner, c.Log)
} }
if c.Config.Storage.SubPaths != nil { if c.Config.Storage.SubPaths != nil {

View file

@ -184,7 +184,7 @@ func (rh *RouteHandler) SetupRoutes() {
// Preconditions for enabling the actual extension routes are part of extensions themselves // Preconditions for enabling the actual extension routes are part of extensions themselves
ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log, rh.c.Metrics) ext.SetupMetricsRoutes(rh.c.Config, rh.c.Router, authHandler, rh.c.Log, rh.c.Metrics)
ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveInfo, ext.SetupSearchRoutes(rh.c.Config, prefixedRouter, rh.c.StoreController, rh.c.MetaDB, rh.c.CveScanner,
rh.c.Log) rh.c.Log)
ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log) ext.SetupImageTrustRoutes(rh.c.Config, prefixedRouter, rh.c.MetaDB, rh.c.Log)
ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log) ext.SetupMgmtRoutes(rh.c.Config, prefixedRouter, rh.c.Log)

View file

@ -531,7 +531,7 @@ func TestNegativeServerResponse(t *testing.T) {
panic(err) panic(err)
} }
ctlr.CveInfo = getMockCveInfo(ctlr.MetaDB, ctlr.Log) ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() { go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) { if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
@ -606,7 +606,7 @@ func TestServerCVEResponse(t *testing.T) {
panic(err) panic(err)
} }
ctlr.CveInfo = getMockCveInfo(ctlr.MetaDB, ctlr.Log) ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() { go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) { if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
@ -947,39 +947,35 @@ func TestCVESort(t *testing.T) {
panic(err) panic(err)
} }
ctlr.CveInfo = cveinfo.BaseCveInfo{ ctlr.CveScanner = mocks.CveScannerMock{
Log: ctlr.Log, ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
MetaDB: mocks.MetaDBMock{}, return map[string]cvemodel.CVE{
Scanner: mocks.CveScannerMock{ "CVE-2023-1255": {
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { ID: "CVE-2023-1255",
return map[string]cvemodel.CVE{ Severity: "LOW",
"CVE-2023-1255": { Title: "Input buffer over-read in AES-XTS implementation and testing",
ID: "CVE-2023-1255", },
Severity: "LOW", "CVE-2023-2650": {
Title: "Input buffer over-read in AES-XTS implementation and testing", ID: "CVE-2023-2650",
}, Severity: "MEDIUM",
"CVE-2023-2650": { Title: "Possible DoS translating ASN.1 object identifier and executer",
ID: "CVE-2023-2650", },
Severity: "MEDIUM", "CVE-2023-2975": {
Title: "Possible DoS translating ASN.1 object identifier and executer", ID: "CVE-2023-2975",
}, Severity: "HIGH",
"CVE-2023-2975": { Title: "AES-SIV cipher implementation contains a bug that can break",
ID: "CVE-2023-2975", },
Severity: "HIGH", "CVE-2023-3446": {
Title: "AES-SIV cipher implementation contains a bug that can break", ID: "CVE-2023-3446",
}, Severity: "CRITICAL",
"CVE-2023-3446": { Title: "Excessive time spent checking DH keys and parenthesis",
ID: "CVE-2023-3446", },
Severity: "CRITICAL", "CVE-2023-3817": {
Title: "Excessive time spent checking DH keys and parenthesis", ID: "CVE-2023-3817",
}, Severity: "MEDIUM",
"CVE-2023-3817": { Title: "Excessive time spent checking DH q parameter and arguments",
ID: "CVE-2023-3817", },
Severity: "MEDIUM", }, nil
Title: "Excessive time spent checking DH q parameter and arguments",
},
}, nil
},
}, },
} }
@ -1373,7 +1369,7 @@ func TestCVECommandErrors(t *testing.T) {
}) })
} }
func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { func getMockCveScanner(metaDB mTypes.MetaDB) cveinfo.Scanner {
// MetaDB loaded with initial data now mock the scanner // MetaDB loaded with initial data now mock the scanner
// Setup test CVE data in mock scanner // Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{ scanner := mocks.CveScannerMock{
@ -1472,11 +1468,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
}, },
} }
return &cveinfo.BaseCveInfo{ return &scanner
Log: log,
Scanner: scanner,
MetaDB: metaDB,
}
} }
type mockServiceForRetry struct { type mockServiceForRetry struct {

View file

@ -4,9 +4,7 @@
package extensions package extensions
import ( import (
"context"
"net/http" "net/http"
"sync"
"time" "time"
gqlHandler "github.com/99designs/gqlgen/graphql/handler" gqlHandler "github.com/99designs/gqlgen/graphql/handler"
@ -24,145 +22,58 @@ import (
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
) )
type ( const scanInterval = 15 * time.Minute
CveInfo cveinfo.CveInfo
state int
)
const ( type CveScanner cveinfo.Scanner
pending state = iota
running
done
)
func IsBuiltWithSearchExtension() bool { func IsBuiltWithSearchExtension() bool {
return true return true
} }
func GetCVEInfo(config *config.Config, storeController storage.StoreController, func GetCveScanner(conf *config.Config, storeController storage.StoreController,
metaDB mTypes.MetaDB, log log.Logger, metaDB mTypes.MetaDB, log log.Logger,
) CveInfo { ) CveScanner {
if config.Extensions.Search == nil || !*config.Extensions.Search.Enable || config.Extensions.Search.CVE == nil { if !conf.IsCveScanningEnabled() {
return nil return nil
} }
dbRepository := config.Extensions.Search.CVE.Trivy.DBRepository dbRepository := conf.Extensions.Search.CVE.Trivy.DBRepository
javaDBRepository := config.Extensions.Search.CVE.Trivy.JavaDBRepository javaDBRepository := conf.Extensions.Search.CVE.Trivy.JavaDBRepository
return cveinfo.NewCVEInfo(storeController, metaDB, dbRepository, javaDBRepository, log) return cveinfo.NewScanner(storeController, metaDB, dbRepository, javaDBRepository, log)
} }
func EnableSearchExtension(config *config.Config, storeController storage.StoreController, func EnableSearchExtension(conf *config.Config, storeController storage.StoreController,
metaDB mTypes.MetaDB, taskScheduler *scheduler.Scheduler, cveInfo CveInfo, log log.Logger, metaDB mTypes.MetaDB, taskScheduler *scheduler.Scheduler, cveScanner CveScanner, log log.Logger,
) { ) {
if config.Extensions.Search != nil && *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil { if conf.IsCveScanningEnabled() {
updateInterval := config.Extensions.Search.CVE.UpdateInterval updateInterval := conf.Extensions.Search.CVE.UpdateInterval
downloadTrivyDB(updateInterval, taskScheduler, cveInfo, log) downloadTrivyDB(updateInterval, taskScheduler, cveScanner, log)
startScanner(scanInterval, metaDB, taskScheduler, cveScanner, log)
} else { } else {
log.Info().Msg("CVE config not provided, skipping CVE update") log.Info().Msg("CVE config not provided, skipping CVE update")
} }
} }
func downloadTrivyDB(interval time.Duration, sch *scheduler.Scheduler, cveInfo CveInfo, log log.Logger) { func downloadTrivyDB(interval time.Duration, sch *scheduler.Scheduler, cveScanner CveScanner, log log.Logger) {
generator := NewTrivyTaskGenerator(interval, cveInfo, log) generator := cveinfo.NewDBUpdateTaskGenerator(interval, cveScanner, log)
log.Info().Msg("Submitting CVE DB update scheduler") log.Info().Msg("Submitting CVE DB update scheduler")
sch.SubmitGenerator(generator, interval, scheduler.HighPriority) sch.SubmitGenerator(generator, interval, scheduler.HighPriority)
} }
func NewTrivyTaskGenerator(interval time.Duration, cveInfo CveInfo, log log.Logger) *TrivyTaskGenerator { func startScanner(interval time.Duration, metaDB mTypes.MetaDB, sch *scheduler.Scheduler,
generator := &TrivyTaskGenerator{interval, cveInfo, log, pending, 0, time.Now(), &sync.Mutex{}} cveScanner CveScanner, log log.Logger,
) {
generator := cveinfo.NewScanTaskGenerator(metaDB, cveScanner, log)
return generator log.Info().Msg("Submitting CVE scan scheduler")
} sch.SubmitGenerator(generator, interval, scheduler.MediumPriority)
type TrivyTaskGenerator struct {
interval time.Duration
cveInfo CveInfo
log log.Logger
status state
waitTime time.Duration
lastTaskTime time.Time
lock *sync.Mutex
}
func (gen *TrivyTaskGenerator) Next() (scheduler.Task, error) {
var newTask scheduler.Task
gen.lock.Lock()
if gen.status == pending && time.Since(gen.lastTaskTime) >= gen.waitTime {
newTask = newTrivyTask(gen.interval, gen.cveInfo, gen, gen.log)
gen.status = running
}
gen.lock.Unlock()
return newTask, nil
}
func (gen *TrivyTaskGenerator) IsDone() bool {
gen.lock.Lock()
status := gen.status
gen.lock.Unlock()
return status == done
}
func (gen *TrivyTaskGenerator) IsReady() bool {
return true
}
func (gen *TrivyTaskGenerator) Reset() {
gen.lock.Lock()
gen.status = pending
gen.waitTime = 0
gen.lock.Unlock()
}
type trivyTask struct {
interval time.Duration
cveInfo cveinfo.CveInfo
generator *TrivyTaskGenerator
log log.Logger
}
func newTrivyTask(interval time.Duration, cveInfo cveinfo.CveInfo,
generator *TrivyTaskGenerator, log log.Logger,
) *trivyTask {
return &trivyTask{interval, cveInfo, generator, log}
}
func (trivyT *trivyTask) DoWork(ctx context.Context) error {
trivyT.log.Info().Msg("updating the CVE database")
err := trivyT.cveInfo.UpdateDB()
if err != nil {
trivyT.generator.lock.Lock()
trivyT.generator.status = pending
if trivyT.generator.waitTime == 0 {
trivyT.generator.waitTime = time.Second
}
trivyT.generator.waitTime *= 2
trivyT.generator.lastTaskTime = time.Now()
trivyT.generator.lock.Unlock()
return err
}
trivyT.generator.lock.Lock()
trivyT.generator.lastTaskTime = time.Now()
trivyT.generator.status = done
trivyT.generator.lock.Unlock()
trivyT.log.Info().Str("DB update completed, next update scheduled after", trivyT.interval.String()).Msg("")
return nil
} }
func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController, func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger, metaDB mTypes.MetaDB, cveScanner CveScanner, log log.Logger,
) { ) {
if !conf.IsSearchEnabled() { if !conf.IsSearchEnabled() {
log.Info().Msg("skip enabling the search route as the config prerequisites are not met") log.Info().Msg("skip enabling the search route as the config prerequisites are not met")
@ -172,6 +83,13 @@ func SetupSearchRoutes(conf *config.Config, router *mux.Router, storeController
log.Info().Msg("setting up search routes") log.Info().Msg("setting up search routes")
var cveInfo cveinfo.CveInfo
if conf.IsCveScanningEnabled() {
cveInfo = cveinfo.NewCVEInfo(cveScanner, metaDB, log)
} else {
cveInfo = nil
}
resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo) resConfig := search.GetResolverConfig(log, storeController, metaDB, cveInfo)
allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost) allowedMethods := zcommon.AllowedMethods(http.MethodGet, http.MethodPost)

View file

@ -13,11 +13,11 @@ import (
"zotregistry.io/zot/pkg/storage" "zotregistry.io/zot/pkg/storage"
) )
type CveInfo interface{} type CveScanner interface{}
func GetCVEInfo(config *config.Config, storeController storage.StoreController, func GetCveScanner(config *config.Config, storeController storage.StoreController,
metaDB mTypes.MetaDB, log log.Logger, metaDB mTypes.MetaDB, log log.Logger,
) CveInfo { ) CveScanner {
return nil return nil
} }
@ -27,7 +27,7 @@ func IsBuiltWithSearchExtension() bool {
// EnableSearchExtension ... // EnableSearchExtension ...
func EnableSearchExtension(config *config.Config, storeController storage.StoreController, func EnableSearchExtension(config *config.Config, storeController storage.StoreController,
metaDB mTypes.MetaDB, scheduler *scheduler.Scheduler, cveInfo CveInfo, log log.Logger, metaDB mTypes.MetaDB, scheduler *scheduler.Scheduler, cveScanner CveScanner, log log.Logger,
) { ) {
log.Warn().Msg("skipping enabling search extension because given zot binary doesn't include this feature," + log.Warn().Msg("skipping enabling search extension because given zot binary doesn't include this feature," +
"please build a binary that does so") "please build a binary that does so")
@ -35,7 +35,7 @@ func EnableSearchExtension(config *config.Config, storeController storage.StoreC
// SetupSearchRoutes ... // SetupSearchRoutes ...
func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController, func SetupSearchRoutes(config *config.Config, router *mux.Router, storeController storage.StoreController,
metaDB mTypes.MetaDB, cveInfo CveInfo, log log.Logger, metaDB mTypes.MetaDB, cveScanner CveScanner, log log.Logger,
) { ) {
log.Warn().Msg("skipping setting up search routes because given zot binary doesn't include this feature," + log.Warn().Msg("skipping setting up search routes because given zot binary doesn't include this feature," +
"please build a binary that does so") "please build a binary that does so")

View file

@ -1,4 +1,4 @@
package trivy package cache
import ( import (
lru "github.com/hashicorp/golang-lru/v2" lru "github.com/hashicorp/golang-lru/v2"
@ -22,6 +22,10 @@ func (cveCache *CveCache) Add(image string, cveMap map[string]cvemodel.CVE) {
cveCache.cache.Add(image, cveMap) cveCache.cache.Add(image, cveMap)
} }
func (cveCache *CveCache) Contains(image string) bool {
return cveCache.cache.Contains(image)
}
func (cveCache *CveCache) Get(image string) map[string]cvemodel.CVE { func (cveCache *CveCache) Get(image string) map[string]cvemodel.CVE {
cveMap, ok := cveCache.cache.Get(image) cveMap, ok := cveCache.cache.Get(image)
if !ok { if !ok {

View file

@ -22,15 +22,15 @@ type CveInfo interface {
GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixed(repo, cveID string) ([]cvemodel.TagInfo, error)
GetCVEListForImage(repo, tag string, searchedCVE string, pageinput cvemodel.PageInput, GetCVEListForImage(repo, tag string, searchedCVE string, pageinput cvemodel.PageInput,
) ([]cvemodel.CVE, zcommon.PageInfo, error) ) ([]cvemodel.CVE, zcommon.PageInfo, error)
GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error)
GetCVESummaryForImageMedia(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) GetCVESummaryForImageMedia(repo, digest, mediaType string) (cvemodel.ImageCVESummary, error)
UpdateDB() error
} }
type Scanner interface { type Scanner interface {
ScanImage(image string) (map[string]cvemodel.CVE, error) ScanImage(image string) (map[string]cvemodel.CVE, error)
IsImageFormatScannable(repo, ref string) (bool, error) IsImageFormatScannable(repo, ref string) (bool, error)
IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error) IsImageMediaScannable(repo, digestStr, mediaType string) (bool, error)
IsResultCached(digestStr string) bool
GetCachedResult(digestStr string) map[string]cvemodel.CVE
UpdateDB() error UpdateDB() error
} }
@ -40,11 +40,13 @@ type BaseCveInfo struct {
MetaDB mTypes.MetaDB MetaDB mTypes.MetaDB
} }
func NewCVEInfo(storeController storage.StoreController, metaDB mTypes.MetaDB, func NewScanner(storeController storage.StoreController, metaDB mTypes.MetaDB,
dbRepository, javaDBRepository string, log log.Logger, dbRepository, javaDBRepository string, log log.Logger,
) *BaseCveInfo { ) Scanner {
scanner := trivy.NewScanner(storeController, metaDB, dbRepository, javaDBRepository, log) return trivy.NewScanner(storeController, metaDB, dbRepository, javaDBRepository, log)
}
func NewCVEInfo(scanner Scanner, metaDB mTypes.MetaDB, log log.Logger) *BaseCveInfo {
return &BaseCveInfo{ return &BaseCveInfo{
Log: log, Log: log,
Scanner: scanner, Scanner: scanner,
@ -72,7 +74,7 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta
isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr) isScanableImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, manifestDigestStr)
if !isScanableImage || err != nil { if !isScanableImage || err != nil {
cveinfo.Log.Info().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable") cveinfo.Log.Debug().Str("image", repo+":"+tag).Err(err).Msg("image is not scanable")
continue continue
} }
@ -94,7 +96,8 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]cvemodel.Ta
}) })
} }
default: default:
cveinfo.Log.Error().Str("mediaType", descriptor.MediaType).Msg("media type not supported for scanning") cveinfo.Log.Debug().Str("image", repo+":"+tag).Str("mediaType", descriptor.MediaType).
Msg("image media type not supported for scanning")
} }
} }
@ -187,7 +190,8 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]cvemo
}) })
} }
default: default:
cveinfo.Log.Error().Str("mediaType", descriptor.MediaType).Msg("media type not supported") cveinfo.Log.Debug().Str("mediaType", descriptor.MediaType).
Msg("image media type not supported for scanning")
} }
} }
@ -250,7 +254,7 @@ func (cveinfo *BaseCveInfo) isManifestVulnerable(repo, tag, manifestDigestStr, c
isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, manifestDigestStr, ispec.MediaTypeImageManifest) isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, manifestDigestStr, ispec.MediaTypeImageManifest)
if !isValidImage || err != nil { if !isValidImage || err != nil {
cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID). cveinfo.Log.Debug().Str("image", image).Str("cve-id", cveID).Err(err).
Msg("image media type not supported for scanning, adding as a vulnerable image") Msg("image media type not supported for scanning, adding as a vulnerable image")
return true return true
@ -335,6 +339,8 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(repo, ref string, searchedCVE stri
) { ) {
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref) isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
if !isValidImage { if !isValidImage {
cveinfo.Log.Debug().Str("image", repo+":"+ref).Err(err).Msg("image is not scanable")
return []cvemodel.CVE{}, zcommon.PageInfo{}, err return []cvemodel.CVE{}, zcommon.PageInfo{}, err
} }
@ -357,50 +363,11 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(repo, ref string, searchedCVE stri
return cveList, pageInfo, nil return cveList, pageInfo, nil
} }
func (cveinfo BaseCveInfo) GetCVESummaryForImage(repo, ref string) (cvemodel.ImageCVESummary, error) {
// There are several cases, expected returned values below:
// not scannable / error during scan - max severity "" - cve count 0 - Errors
// scannable no issues found - max severity "NONE" - cve count 0 - no Errors
// scannable issues found - max severity from Scanner - cve count >0 - no Errors
imageCVESummary := cvemodel.ImageCVESummary{
Count: 0,
MaxSeverity: cvemodel.SeverityNotScanned,
}
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref)
if !isValidImage {
return imageCVESummary, err
}
image := zcommon.GetFullImageName(repo, ref)
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
return imageCVESummary, err
}
imageCVESummary.Count = len(cveMap)
if imageCVESummary.Count == 0 {
imageCVESummary.MaxSeverity = cvemodel.SeverityNone
return imageCVESummary, nil
}
imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown
for _, cve := range cveMap {
if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
imageCVESummary.MaxSeverity = cve.Severity
}
}
return imageCVESummary, nil
}
func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType string, func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) { ) (cvemodel.ImageCVESummary, error) {
// There are several cases, expected returned values below: // There are several cases, expected returned values below:
// not scannable / error during scan - max severity "" - cve count 0 - Errors // not scanned yet - max severity "" - cve count 0 - no Errors
// not scannable - max severity "" - cve count 0 - has Errors
// scannable no issues found - max severity "NONE" - cve count 0 - no Errors // scannable no issues found - max severity "NONE" - cve count 0 - no Errors
// scannable issues found - max severity from Scanner - cve count >0 - no Errors // scannable issues found - max severity from Scanner - cve count >0 - no Errors
imageCVESummary := cvemodel.ImageCVESummary{ imageCVESummary := cvemodel.ImageCVESummary{
@ -408,20 +375,21 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType st
MaxSeverity: cvemodel.SeverityNotScanned, MaxSeverity: cvemodel.SeverityNotScanned,
} }
isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType) // For this call we only look at the scanner cache, we skip the actual scanning to save time
if !isValidImage { if !cveinfo.Scanner.IsResultCached(digest) {
isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType)
if !isValidImage {
cveinfo.Log.Debug().Str("digest", digest).Str("mediaType", mediaType).
Err(err).Msg("image is not scannable")
}
return imageCVESummary, err return imageCVESummary, err
} }
image := repo + "@" + digest // We will make due with cached results
cveMap := cveinfo.Scanner.GetCachedResult(digest)
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
return imageCVESummary, err
}
imageCVESummary.Count = len(cveMap) imageCVESummary.Count = len(cveMap)
if imageCVESummary.Count == 0 { if imageCVESummary.Count == 0 {
imageCVESummary.MaxSeverity = cvemodel.SeverityNone imageCVESummary.MaxSeverity = cvemodel.SeverityNone
@ -438,10 +406,6 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(repo, digest, mediaType st
return imageCVESummary, nil return imageCVESummary, nil
} }
func (cveinfo BaseCveInfo) UpdateDB() error {
return cveinfo.Scanner.UpdateDB()
}
func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo { func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo {
sort.Slice(allTags, func(i, j int) bool { sort.Slice(allTags, func(i, j int) bool {
return allTags[i].Timestamp.Before(allTags[j].Timestamp) return allTags[i].Timestamp.Before(allTags[j].Timestamp)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,207 @@
package cveinfo
import (
"context"
"sync"
godigest "github.com/opencontainers/go-digest"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
reqCtx "zotregistry.io/zot/pkg/requestcontext"
"zotregistry.io/zot/pkg/scheduler"
)
func NewScanTaskGenerator(
metaDB mTypes.MetaDB,
scanner Scanner,
log log.Logger,
) scheduler.TaskGenerator {
return &scanTaskGenerator{
log: log,
metaDB: metaDB,
scanner: scanner,
lock: &sync.Mutex{},
scanErrors: map[string]error{},
scheduled: map[string]bool{},
done: false,
}
}
// scanTaskGenerator takes all manifests from repodb and runs the CVE scanner on them.
// If the scanner already has results cached for a specific manifests, or it cannot be
// scanned, the manifest will be skipped.
// If there are no manifests missing from the cache, the generator finishes.
type scanTaskGenerator struct {
log log.Logger
metaDB mTypes.MetaDB
scanner Scanner
lock *sync.Mutex
scanErrors map[string]error
scheduled map[string]bool
done bool
}
func (gen *scanTaskGenerator) getMatcherFunc() mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMetadata, manifestMeta mTypes.ManifestMetadata) bool {
// Note this matcher will return information based on scan status of manifests
// An index scan aggregates results of manifest scans
// If at least one of its manifests can be scanned,
// the index and its tag will be returned by the caller function too
repoName := repoMeta.Name
manifestDigest := godigest.FromBytes(manifestMeta.ManifestBlob).String()
if gen.isScheduled(manifestDigest) {
// We skip this manifest as it has already scheduled
return false
}
if gen.hasError(manifestDigest) {
// We skip this manifest as it has already been scanned and errored
// This is to prevent the generator attempting to run a scan
// in a loop of the same image which would consistently fail
return false
}
if gen.scanner.IsResultCached(manifestDigest) {
// We skip this manifest, it was already scanned
return false
}
ok, err := gen.scanner.IsImageFormatScannable(repoName, manifestDigest)
if !ok || err != nil {
// We skip this manifest, we cannot scan it
return false
}
return true
}
}
func (gen *scanTaskGenerator) addError(digest string, err error) {
gen.lock.Lock()
defer gen.lock.Unlock()
gen.scanErrors[digest] = err
}
func (gen *scanTaskGenerator) hasError(digest string) bool {
gen.lock.Lock()
defer gen.lock.Unlock()
_, ok := gen.scanErrors[digest]
return ok
}
func (gen *scanTaskGenerator) setScheduled(digest string, isScheduled bool) {
gen.lock.Lock()
defer gen.lock.Unlock()
if _, ok := gen.scheduled[digest]; ok && !isScheduled {
delete(gen.scheduled, digest)
} else if isScheduled {
gen.scheduled[digest] = true
}
}
func (gen *scanTaskGenerator) isScheduled(digest string) bool {
gen.lock.Lock()
defer gen.lock.Unlock()
_, ok := gen.scheduled[digest]
return ok
}
func (gen *scanTaskGenerator) Next() (scheduler.Task, error) {
// metaRB requires us to use a context for authorization
userAc := reqCtx.NewUserAccessControl()
userAc.SetUsername("scheduler")
userAc.SetIsAdmin(true)
ctx := userAc.DeriveContext(context.Background())
// Obtain a list of repos with unscanned scannable manifests
// We may implement a method to return just 1 match at some point
reposMeta, _, _, err := gen.metaDB.FilterTags(ctx, gen.getMatcherFunc())
if err != nil {
// Do not crash the generator for potential repodb inconistencies
// as there may be scannable images not yet scanned
gen.log.Warn().Err(err).Msg("Scheduled CVE scan: error while obtaining repo metadata")
}
// no reposMeta are returned, all results are in already in cache
// or manifests cannot be scanned
if len(reposMeta) == 0 {
gen.log.Info().Msg("Scheduled CVE scan: finished for available images")
gen.done = true
return nil, nil
}
// Since reposMeta will always contain just unscanned images we can pick
// any repo and any tag out of the resulting matches
repoMeta := reposMeta[0]
var digest string
// Pick any tag
for _, descriptor := range repoMeta.Tags {
digest = descriptor.Digest
break
}
// Mark the digest as scheduled so it is skipped on next generator run
gen.setScheduled(digest, true)
return newScanTask(gen, repoMeta.Name, digest), nil
}
func (gen *scanTaskGenerator) IsDone() bool {
return gen.done
}
func (gen *scanTaskGenerator) IsReady() bool {
return true
}
func (gen *scanTaskGenerator) Reset() {
gen.lock.Lock()
defer gen.lock.Unlock()
gen.scheduled = map[string]bool{}
gen.scanErrors = map[string]error{}
gen.done = false
}
type scanTask struct {
generator *scanTaskGenerator
repo string
digest string
}
func newScanTask(generator *scanTaskGenerator, repo string, digest string) *scanTask {
return &scanTask{generator, repo, digest}
}
func (st *scanTask) DoWork(ctx context.Context) error {
// When work finished clean this entry from the generator
defer st.generator.setScheduled(st.digest, false)
image := st.repo + "@" + st.digest
// We cache the results internally in the scanner
// so we can discard the actual results for now
if _, err := st.generator.scanner.ScanImage(image); err != nil {
st.generator.log.Error().Err(err).Str("image", image).Msg("Scheduled CVE scan errored for image")
st.generator.addError(st.digest, err)
return err
}
st.generator.log.Debug().Str("image", image).Msg("Scheduled CVE scan completed successfully for image")
return nil
}

View file

@ -0,0 +1,670 @@
//go:build search
// +build search
package cveinfo_test
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"testing"
"time"
regTypes "github.com/google/go-containerregistry/pkg/v1/types"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/api/config"
zcommon "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions/monitoring"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvecache "zotregistry.io/zot/pkg/extensions/search/cve/cache"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta"
"zotregistry.io/zot/pkg/meta/boltdb"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
"zotregistry.io/zot/pkg/storage"
"zotregistry.io/zot/pkg/storage/local"
. "zotregistry.io/zot/pkg/test"
. "zotregistry.io/zot/pkg/test/image-utils"
"zotregistry.io/zot/pkg/test/mocks"
)
var (
ErrBadTest = errors.New("there is a bug in the test")
ErrFailedScan = errors.New("scan has failed intentionally")
)
func TestScanGeneratorWithMockedData(t *testing.T) { //nolint: gocyclo
Convey("Test CVE scanning task scheduler with diverse mocked data", t, func() {
repo1 := "repo1"
repoIndex := "repoIndex"
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
logPath := logFile.Name()
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logPath)
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
cfg := config.New()
cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3}
sch := scheduler.NewScheduler(cfg, logger)
params := boltdb.DBParameters{
RootDir: t.TempDir(),
}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log.NewLogger("debug", ""))
So(err, ShouldBeNil)
// Create metadb data for scannable image with vulnerabilities
image11 := CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: DateRef(2008, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta11 := mTypes.ManifestMetadata{
ManifestBlob: image11.ManifestDescriptor.Data,
ConfigBlob: image11.ConfigDescriptor.Data,
DownloadCount: 0,
Signatures: mTypes.ManifestSignatures{},
}
err = metaDB.SetManifestMeta("repo1", image11.ManifestDescriptor.Digest, repoMeta11)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo1", "0.1.0", image11.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
image12 := CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta12 := mTypes.ManifestMetadata{
ManifestBlob: image12.ManifestDescriptor.Data,
ConfigBlob: image12.ConfigDescriptor.Data,
DownloadCount: 0,
Signatures: mTypes.ManifestSignatures{},
}
err = metaDB.SetManifestMeta("repo1", image12.ManifestDescriptor.Digest, repoMeta12)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo1", "1.0.0", image12.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
image13 := CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: DateRef(2010, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta13 := mTypes.ManifestMetadata{
ManifestBlob: image13.ManifestDescriptor.Data,
ConfigBlob: image13.ConfigDescriptor.Data,
DownloadCount: 0,
Signatures: mTypes.ManifestSignatures{},
}
err = metaDB.SetManifestMeta("repo1", image13.ManifestDescriptor.Digest, repoMeta13)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo1", "1.1.0", image13.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
image14 := CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: DateRef(2011, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta14 := mTypes.ManifestMetadata{
ManifestBlob: image14.ManifestDescriptor.Data,
ConfigBlob: image14.ConfigDescriptor.Data,
}
err = metaDB.SetManifestMeta("repo1", image14.ManifestDescriptor.Digest, repoMeta14)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo1", "1.0.1", image14.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
// Create metadb data for scannable image with no vulnerabilities
image61 := CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: DateRef(2016, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta61 := mTypes.ManifestMetadata{
ManifestBlob: image61.ManifestDescriptor.Data,
ConfigBlob: image61.ConfigDescriptor.Data,
}
err = metaDB.SetManifestMeta("repo6", image61.ManifestDescriptor.Digest, repoMeta61)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo6", "1.0.0", image61.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
// Create metadb data for image not supporting scanning
image21 := CreateImageWith().Layers([]Layer{{
MediaType: ispec.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck
Blob: []byte{10, 10, 10},
Digest: godigest.FromBytes([]byte{10, 10, 10}),
}}).ImageConfig(ispec.Image{Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta21 := mTypes.ManifestMetadata{
ManifestBlob: image21.ManifestDescriptor.Data,
ConfigBlob: image21.ConfigDescriptor.Data,
}
err = metaDB.SetManifestMeta("repo2", image21.ManifestDescriptor.Digest, repoMeta21)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo2", "1.0.0", image21.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
// Create metadb data for invalid images/negative tests
manifestBlob31 := []byte("invalid manifest blob")
So(err, ShouldBeNil)
repoMeta31 := mTypes.ManifestMetadata{
ManifestBlob: manifestBlob31,
}
digest31 := godigest.FromBytes(manifestBlob31)
err = metaDB.SetManifestMeta("repo3", digest31, repoMeta31)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo3", "invalid-manifest", digest31, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
image41 := CreateImageWith().DefaultLayers().
CustomConfigBlob([]byte("invalid config blob"), ispec.MediaTypeImageConfig).Build()
repoMeta41 := mTypes.ManifestMetadata{
ManifestBlob: image41.ManifestDescriptor.Data,
ConfigBlob: image41.ConfigDescriptor.Data,
}
err = metaDB.SetManifestMeta("repo4", image41.ManifestDescriptor.Digest, repoMeta41)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo4", "invalid-config", image41.ManifestDescriptor.Digest,
ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
digest51 := godigest.FromString("abc8")
err = metaDB.SetRepoReference("repo5", "nonexitent-manifest", digest51, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
// Create metadb data for scannable image which errors during scan
image71 := CreateImageWith().DefaultLayers().
ImageConfig(ispec.Image{Created: DateRef(2000, 1, 1, 12, 0, 0, 0, time.UTC)}).Build()
repoMeta71 := mTypes.ManifestMetadata{
ManifestBlob: image71.ManifestDescriptor.Data,
ConfigBlob: image71.ConfigDescriptor.Data,
}
err = metaDB.SetManifestMeta("repo7", image71.ManifestDescriptor.Digest, repoMeta71)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference("repo7", "1.0.0", image71.ManifestDescriptor.Digest, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
// Create multiarch image with vulnerabilities
multiarchImage := CreateRandomMultiarch()
err = metaDB.SetIndexData(
multiarchImage.IndexDescriptor.Digest,
mTypes.IndexData{IndexBlob: multiarchImage.IndexDescriptor.Data},
)
So(err, ShouldBeNil)
err = metaDB.SetManifestData(
multiarchImage.Images[0].ManifestDescriptor.Digest,
mTypes.ManifestData{
ManifestBlob: multiarchImage.Images[0].ManifestDescriptor.Data,
ConfigBlob: multiarchImage.Images[0].ConfigDescriptor.Data,
},
)
So(err, ShouldBeNil)
err = metaDB.SetManifestData(
multiarchImage.Images[1].ManifestDescriptor.Digest,
mTypes.ManifestData{
ManifestBlob: multiarchImage.Images[1].ManifestDescriptor.Data,
ConfigBlob: multiarchImage.Images[1].ConfigDescriptor.Data,
},
)
So(err, ShouldBeNil)
err = metaDB.SetManifestData(
multiarchImage.Images[2].ManifestDescriptor.Digest,
mTypes.ManifestData{
ManifestBlob: multiarchImage.Images[2].ManifestDescriptor.Data,
ConfigBlob: multiarchImage.Images[2].ConfigDescriptor.Data,
},
)
So(err, ShouldBeNil)
err = metaDB.SetRepoReference(
repoIndex,
"tagIndex",
multiarchImage.IndexDescriptor.Digest,
ispec.MediaTypeImageIndex,
)
So(err, ShouldBeNil)
// Keep a record of all the image references / digest pairings
// This is normally done in MetaDB, but we want to verify
// the whole flow, including MetaDB
imageMap := map[string]string{}
image11Digest := image11.ManifestDescriptor.Digest.String()
image11Name := "repo1:0.1.0"
imageMap[image11Name] = image11Digest
image12Digest := image12.ManifestDescriptor.Digest.String()
image12Name := "repo1:1.0.0"
imageMap[image12Name] = image12Digest
image13Digest := image13.ManifestDescriptor.Digest.String()
image13Name := "repo1:1.1.0"
imageMap[image13Name] = image13Digest
image14Digest := image14.ManifestDescriptor.Digest.String()
image14Name := "repo1:1.0.1"
imageMap[image14Name] = image14Digest
image21Digest := image21.ManifestDescriptor.Digest.String()
image21Name := "repo2:1.0.0"
imageMap[image21Name] = image21Digest
image31Name := "repo3:invalid-manifest"
imageMap[image31Name] = digest31.String()
image41Digest := image41.ManifestDescriptor.Digest.String()
image41Name := "repo4:invalid-config"
imageMap[image41Name] = image41Digest
image51Name := "repo5:nonexitent-manifest"
imageMap[image51Name] = digest51.String()
image61Digest := image61.ManifestDescriptor.Digest.String()
image61Name := "repo6:1.0.0"
imageMap[image61Name] = image61Digest
image71Digest := image71.ManifestDescriptor.Digest.String()
image71Name := "repo7:1.0.0"
imageMap[image71Name] = image71Digest
indexDigest := multiarchImage.IndexDescriptor.Digest.String()
indexName := "repoIndex:tagIndex"
imageMap[indexName] = indexDigest
indexM1Digest := multiarchImage.Images[0].ManifestDescriptor.Digest.String()
indexM1Name := "repoIndex@" + indexM1Digest
imageMap[indexM1Name] = indexM1Digest
indexM2Digest := multiarchImage.Images[1].ManifestDescriptor.Digest.String()
indexM2Name := "repoIndex@" + indexM2Digest
imageMap[indexM2Name] = indexM2Digest
indexM3Digest := multiarchImage.Images[2].ManifestDescriptor.Digest.String()
indexM3Name := "repoIndex@" + indexM3Digest
imageMap[indexM3Name] = indexM3Digest
// Initialize a test CVE cache
cache := cvecache.NewCveCache(10, logger)
// MetaDB loaded with initial data, now mock the scanner
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
result := cache.Get(image)
// Will not match sending the repo:tag as a parameter, but we don't care
if result != nil {
return result, nil
}
repo, ref, isTag := zcommon.GetImageDirAndReference(image)
if isTag {
foundRef, ok := imageMap[image]
if !ok {
return nil, ErrBadTest
}
ref = foundRef
}
// Images in chronological order
if repo == repo1 && ref == image11Digest {
result := map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
}
cache.Add(ref, result)
return result, nil
}
if repo == repo1 && zcommon.Contains([]string{image12Digest, image21Digest}, ref) {
result := map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE2": {
ID: "CVE2",
Severity: "HIGH",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}
cache.Add(ref, result)
return result, nil
}
if repo == repo1 && ref == image13Digest {
result := map[string]cvemodel.CVE{
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}
cache.Add(ref, result)
return result, nil
}
// As a minor release on 1.0.0 banch
// does not include all fixes published in 1.1.0
if repo == repo1 && ref == image14Digest {
result := map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}
cache.Add(ref, result)
return result, nil
}
// Unexpected error while scanning
if repo == "repo7" {
return map[string]cvemodel.CVE{}, ErrFailedScan
}
if (repo == repoIndex && ref == indexDigest) ||
(repo == repoIndex && ref == indexM1Digest) {
result := map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
}
// Simulate scanning an index results in scanning its manifests
if ref == indexDigest {
cache.Add(indexM1Digest, result)
cache.Add(indexM2Digest, map[string]cvemodel.CVE{})
cache.Add(indexM3Digest, map[string]cvemodel.CVE{})
}
cache.Add(ref, result)
return result, nil
}
// By default the image has no vulnerabilities
result = map[string]cvemodel.CVE{}
cache.Add(ref, result)
return result, nil
},
IsImageFormatScannableFn: func(repo string, reference string) (bool, error) {
if repo == repoIndex {
return true, nil
}
// Almost same logic compared to actual Trivy specific implementation
imageDir, inputTag := repo, reference
repoMeta, err := metaDB.GetRepoMeta(imageDir)
if err != nil {
return false, err
}
manifestDigestStr := reference
if zcommon.IsTag(reference) {
var ok bool
descriptor, ok := repoMeta.Tags[inputTag]
if !ok {
return false, zerr.ErrTagMetaNotFound
}
manifestDigestStr = descriptor.Digest
}
manifestDigest, err := godigest.Parse(manifestDigestStr)
if err != nil {
return false, err
}
manifestData, err := metaDB.GetManifestData(manifestDigest)
if err != nil {
return false, err
}
var manifestContent ispec.Manifest
err = json.Unmarshal(manifestData.ManifestBlob, &manifestContent)
if err != nil {
return false, zerr.ErrScanNotSupported
}
for _, imageLayer := range manifestContent.Layers {
switch imageLayer.MediaType {
case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):
return true, nil
default:
return false, zerr.ErrScanNotSupported
}
}
return false, nil
},
IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) {
if repo == "repo2" {
if digest == image21Digest {
return false, nil
}
}
return true, nil
},
IsResultCachedFn: func(digest string) bool {
return cache.Contains(digest)
},
UpdateDBFn: func() error {
cache.Purge()
return nil
},
}
// Purge scan, it should not be needed
So(scanner.UpdateDB(), ShouldBeNil)
// Verify none of the entries are cached to begin with
t.Log("verify cache is initially empty")
for image, digestStr := range imageMap {
t.Log("expecting " + image + " " + digestStr + " to be absent from cache")
So(scanner.IsResultCached(digestStr), ShouldBeFalse)
}
// Start the generator
generator := cveinfo.NewScanTaskGenerator(metaDB, scanner, logger)
sch.SubmitGenerator(generator, 10*time.Second, scheduler.MediumPriority)
ctx, cancel := context.WithCancel(context.Background())
sch.RunScheduler(ctx)
defer cancel()
// Make sure the scanner generator has completed despite errors
found, err := ReadLogFileAndSearchString(logPath,
"Scheduled CVE scan: finished for available images", 20*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
t.Log("verify cache is up to date after scanner generator ran")
// Verify all of the entries are cached
for image, digestStr := range imageMap {
repo, _, _ := zcommon.GetImageDirAndReference(image)
ok, err := scanner.IsImageFormatScannable(repo, digestStr)
if ok && err == nil && repo != "repo7" {
t.Log("expecting " + image + " " + digestStr + " to be present in cache")
So(scanner.IsResultCached(digestStr), ShouldBeTrue)
} else {
// We don't cache results for unscannable manifests
t.Log("expecting " + image + " " + digestStr + " to be absent from cache")
So(scanner.IsResultCached(digestStr), ShouldBeFalse)
}
}
// Make sure the scanner generator is catching the metadb error for repo5:nonexitent-manifest
found, err = ReadLogFileAndSearchString(logPath,
"Scheduled CVE scan: error while obtaining repo metadata", 20*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// Make sure the scanner generator is catching the scanning error for repo7
found, err = ReadLogFileAndSearchString(logPath,
"Scheduled CVE scan errored for image", 20*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
// Make sure the scanner generator is triggered at least twice
found, err = ReadLogFileAndCountStringOccurence(logPath,
"Scheduled CVE scan: finished for available images", 30*time.Second, 2)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
})
}
func TestScanGeneratorWithRealData(t *testing.T) {
Convey("Test CVE scanning task scheduler real data", t, func() {
rootDir := t.TempDir()
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
logPath := logFile.Name()
So(err, ShouldBeNil)
defer os.Remove(logFile.Name()) // clean up
logger := log.NewLogger("debug", logPath)
writers := io.MultiWriter(os.Stdout, logFile)
logger.Logger = logger.Output(writers)
cfg := config.New()
cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3}
boltDriver, err := boltdb.GetBoltDriver(boltdb.DBParameters{RootDir: rootDir})
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, logger)
So(err, ShouldBeNil)
imageStore := local.NewImageStore(rootDir, false, false, 0, 0, false, false,
logger, monitoring.NewMetricsServer(false, logger), nil, nil)
storeController := storage.StoreController{DefaultStore: imageStore}
image := CreateRandomVulnerableImage()
err = WriteImageToFileSystem(image, "zot-test", "0.0.1", storeController)
So(err, ShouldBeNil)
err = meta.ParseStorage(metaDB, storeController, logger)
So(err, ShouldBeNil)
scanner := cveinfo.NewScanner(storeController, metaDB, "ghcr.io/project-zot/trivy-db", "", logger)
err = scanner.UpdateDB()
So(err, ShouldBeNil)
So(scanner.IsResultCached(image.DigestStr()), ShouldBeFalse)
sch := scheduler.NewScheduler(cfg, logger)
generator := cveinfo.NewScanTaskGenerator(metaDB, scanner, logger)
// Start the generator
sch.SubmitGenerator(generator, 120*time.Second, scheduler.MediumPriority)
ctx, cancel := context.WithCancel(context.Background())
sch.RunScheduler(ctx)
defer cancel()
// Make sure the scanner generator has completed
found, err := ReadLogFileAndSearchString(logPath,
"Scheduled CVE scan: finished for available images", 120*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = ReadLogFileAndSearchString(logPath,
image.ManifestDescriptor.Digest.String(), 120*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
found, err = ReadLogFileAndSearchString(logPath,
"Scheduled CVE scan completed successfully for image", 120*time.Second)
So(err, ShouldBeNil)
So(found, ShouldBeTrue)
So(scanner.IsResultCached(image.DigestStr()), ShouldBeTrue)
cveMap, err := scanner.ScanImage("zot-test:0.0.1")
So(err, ShouldBeNil)
t.Logf("cveMap: %v", cveMap)
// As of September 22 2023 there are 5 CVEs:
// CVE-2023-1255, CVE-2023-2650, CVE-2023-2975, CVE-2023-3817, CVE-2023-3446
// There may be more discovered in the future
So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 5)
So(cveMap, ShouldContainKey, "CVE-2023-1255")
So(cveMap, ShouldContainKey, "CVE-2023-2650")
So(cveMap, ShouldContainKey, "CVE-2023-2975")
So(cveMap, ShouldContainKey, "CVE-2023-3817")
So(cveMap, ShouldContainKey, "CVE-2023-3446")
cveInfo := cveinfo.NewCVEInfo(scanner, metaDB, logger)
// Based on cache population only, no extra scanning
cveSummary, err := cveInfo.GetCVESummaryForImageMedia("zot-test", image.DigestStr(),
image.ManifestDescriptor.MediaType)
So(err, ShouldBeNil)
So(cveSummary.Count, ShouldBeGreaterThanOrEqualTo, 5)
// As of September 22 the max severity is MEDIUM, but new CVEs could appear in the future
So([]string{"MEDIUM", "HIGH", "CRITICAL"}, ShouldContain, cveSummary.MaxSeverity)
})
}

View file

@ -23,6 +23,7 @@ import (
zerr "zotregistry.io/zot/errors" zerr "zotregistry.io/zot/errors"
zcommon "zotregistry.io/zot/pkg/common" zcommon "zotregistry.io/zot/pkg/common"
cvecache "zotregistry.io/zot/pkg/extensions/search/cve/cache"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
mcommon "zotregistry.io/zot/pkg/meta/common" mcommon "zotregistry.io/zot/pkg/meta/common"
@ -78,7 +79,7 @@ type Scanner struct {
storeController storage.StoreController storeController storage.StoreController
log log.Logger log log.Logger
dbLock *sync.Mutex dbLock *sync.Mutex
cache *CveCache cache *cvecache.CveCache
dbRepository string dbRepository string
javaDBRepository string javaDBRepository string
} }
@ -120,7 +121,7 @@ func NewScanner(storeController storage.StoreController,
cveController: cveController, cveController: cveController,
storeController: storeController, storeController: storeController,
dbLock: &sync.Mutex{}, dbLock: &sync.Mutex{},
cache: NewCveCache(cacheSize, log), cache: cvecache.NewCveCache(cacheSize, log),
dbRepository: dbRepository, dbRepository: dbRepository,
javaDBRepository: javaDBRepository, javaDBRepository: javaDBRepository,
} }
@ -258,9 +259,6 @@ func (scanner Scanner) isManifestScanable(digestStr string) (bool, error) {
case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer):
continue continue
default: default:
scanner.log.Debug().Str("mediaType", imageLayer.MediaType).
Msg("image media type not supported for scanning")
return false, zerr.ErrScanNotSupported return false, zerr.ErrScanNotSupported
} }
} }
@ -304,6 +302,15 @@ func (scanner Scanner) isIndexScanable(digestStr string) (bool, error) {
return false, 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) { func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) {
var ( var (
originalImageInput = image originalImageInput = image
@ -430,6 +437,10 @@ func (scanner Scanner) scanManifest(repo, digest string) (map[string]cvemodel.CV
} }
func (scanner Scanner) scanIndex(repo, digest string) (map[string]cvemodel.CVE, error) { 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)) indexData, err := scanner.metaDB.GetIndexData(godigest.Digest(digest))
if err != nil { if err != nil {
return map[string]cvemodel.CVE{}, err return map[string]cvemodel.CVE{}, err
@ -457,12 +468,14 @@ func (scanner Scanner) scanIndex(repo, digest string) (map[string]cvemodel.CVE,
} }
} }
scanner.cache.Add(digest, indexCveIDMap)
return indexCveIDMap, nil return indexCveIDMap, nil
} }
// UpdateDB downloads the Trivy DB / Cache under the store root directory. // UpdateDB downloads the Trivy DB / Cache under the store root directory.
func (scanner Scanner) UpdateDB() error { func (scanner Scanner) UpdateDB() error {
// We need a lock as using multiple substores each with it's own DB // 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 // 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 // https://github.com/project-zot/trivy-db/blob/main/pkg/db/db.go#L23
scanner.dbLock.Lock() scanner.dbLock.Lock()

View file

@ -216,8 +216,14 @@ func TestVulnerableLayer(t *testing.T) {
cveMap, err := scanner.ScanImage("repo@" + img.DigestStr()) cveMap, err := scanner.ScanImage("repo@" + img.DigestStr())
So(err, ShouldBeNil) So(err, ShouldBeNil)
t.Logf("cveMap: %v", cveMap) t.Logf("cveMap: %v", cveMap)
// As of July 15 2023 there are 3 CVEs: CVE-2023-1255, CVE-2023-2650, CVE-2023-2975 // As of September 17 2023 there are 5 CVEs:
// CVE-2023-1255, CVE-2023-2650, CVE-2023-2975, CVE-2023-3817, CVE-2023-3446
// There may be more discovered in the future // There may be more discovered in the future
So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 3) So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 5)
So(cveMap, ShouldContainKey, "CVE-2023-1255")
So(cveMap, ShouldContainKey, "CVE-2023-2650")
So(cveMap, ShouldContainKey, "CVE-2023-2975")
So(cveMap, ShouldContainKey, "CVE-2023-3817")
So(cveMap, ShouldContainKey, "CVE-2023-3446")
}) })
} }

View file

@ -0,0 +1,120 @@
package cveinfo
import (
"context"
"sync"
"time"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/scheduler"
)
type state int
const (
pending state = iota
running
done
)
func NewDBUpdateTaskGenerator(
interval time.Duration,
scanner Scanner,
log log.Logger,
) scheduler.TaskGenerator {
generator := &DBUpdateTaskGenerator{
interval,
scanner,
log,
pending,
0,
time.Now(),
&sync.Mutex{},
}
return generator
}
type DBUpdateTaskGenerator struct {
interval time.Duration
scanner Scanner
log log.Logger
status state
waitTime time.Duration
lastTaskTime time.Time
lock *sync.Mutex
}
func (gen *DBUpdateTaskGenerator) Next() (scheduler.Task, error) {
var newTask scheduler.Task
gen.lock.Lock()
if gen.status == pending && time.Since(gen.lastTaskTime) >= gen.waitTime {
newTask = newDBUpdadeTask(gen.interval, gen.scanner, gen, gen.log)
gen.status = running
}
gen.lock.Unlock()
return newTask, nil
}
func (gen *DBUpdateTaskGenerator) IsDone() bool {
gen.lock.Lock()
status := gen.status
gen.lock.Unlock()
return status == done
}
func (gen *DBUpdateTaskGenerator) IsReady() bool {
return true
}
func (gen *DBUpdateTaskGenerator) Reset() {
gen.lock.Lock()
gen.status = pending
gen.waitTime = 0
gen.lock.Unlock()
}
type dbUpdateTask struct {
interval time.Duration
scanner Scanner
generator *DBUpdateTaskGenerator
log log.Logger
}
func newDBUpdadeTask(interval time.Duration, scanner Scanner,
generator *DBUpdateTaskGenerator, log log.Logger,
) *dbUpdateTask {
return &dbUpdateTask{interval, scanner, generator, log}
}
func (dbt *dbUpdateTask) DoWork(ctx context.Context) error {
dbt.log.Info().Msg("updating the CVE database")
err := dbt.scanner.UpdateDB()
if err != nil {
dbt.generator.lock.Lock()
dbt.generator.status = pending
if dbt.generator.waitTime == 0 {
dbt.generator.waitTime = time.Second
}
dbt.generator.waitTime *= 2
dbt.generator.lastTaskTime = time.Now()
dbt.generator.lock.Unlock()
return err
}
dbt.generator.lock.Lock()
dbt.generator.lastTaskTime = time.Now()
dbt.generator.status = done
dbt.generator.lock.Unlock()
dbt.log.Info().Str("DB update completed, next update scheduled after", dbt.interval.String()).Msg("")
return nil
}

View file

@ -1,7 +1,7 @@
//go:build search //go:build search
// +build search // +build search
package extensions_test package cveinfo_test
import ( import (
"context" "context"
@ -14,7 +14,6 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/config"
. "zotregistry.io/zot/pkg/extensions"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types" mTypes "zotregistry.io/zot/pkg/meta/types"
@ -24,8 +23,8 @@ import (
"zotregistry.io/zot/pkg/test/mocks" "zotregistry.io/zot/pkg/test/mocks"
) )
func TestTrivyDBGenerator(t *testing.T) { func TestCVEDBGenerator(t *testing.T) {
Convey("Test trivy task scheduler reset", t, func() { Convey("Test CVE DB task scheduler reset", t, func() {
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
logPath := logFile.Name() logPath := logFile.Name()
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -57,8 +56,8 @@ func TestTrivyDBGenerator(t *testing.T) {
}, },
} }
cveInfo := cveinfo.NewCVEInfo(storeController, metaDB, "ghcr.io/project-zot/trivy-db", "", logger) cveScanner := cveinfo.NewScanner(storeController, metaDB, "ghcr.io/project-zot/trivy-db", "", logger)
generator := NewTrivyTaskGenerator(time.Minute, cveInfo, logger) generator := cveinfo.NewDBUpdateTaskGenerator(time.Minute, cveScanner, logger)
sch.SubmitGenerator(generator, 12000*time.Millisecond, scheduler.HighPriority) sch.SubmitGenerator(generator, 12000*time.Millisecond, scheduler.HighPriority)

View file

@ -2078,6 +2078,68 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
} }
} }
getCveResults := func(digestStr string) map[string]cvemodel.CVE {
if digestStr == digest1.String() {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
"CVE34": {
ID: "CVE34",
Severity: "LOW",
Title: "Title for CVE34",
Description: "Description CVE34",
},
}
}
if digestStr == digest2.String() {
return map[string]cvemodel.CVE{
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}
}
if digestStr == digest3.String() {
return map[string]cvemodel.CVE{
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}
}
// By default the image has no vulnerabilities
return map[string]cvemodel.CVE{}
}
// MetaDB loaded with initial data, now mock the scanner // MetaDB loaded with initial data, now mock the scanner
// Setup test CVE data in mock scanner // Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{ scanner := mocks.CveScannerMock{
@ -2092,65 +2154,13 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
digest = godigest.Digest(digestStr) digest = godigest.Digest(digestStr)
} }
if digest.String() == digest1.String() { return getCveResults(digest.String()), nil
return map[string]cvemodel.CVE{ },
"CVE1": { GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE {
ID: "CVE1", return getCveResults(digestStr)
Severity: "HIGH", },
Title: "Title CVE1", IsResultCachedFn: func(digestStr string) bool {
Description: "Description CVE1", return true
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
"CVE34": {
ID: "CVE34",
Severity: "LOW",
Title: "Title for CVE34",
Description: "Description CVE34",
},
}, nil
}
if digest.String() == digest2.String() {
return map[string]cvemodel.CVE{
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}, nil
}
if digest.String() == digest3.String() {
return map[string]cvemodel.CVE{
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}, nil
}
// By default the image has no vulnerabilities
return map[string]cvemodel.CVE{}, nil
}, },
} }

View file

@ -220,87 +220,84 @@ func uploadNewRepoTag(tag string, repoName string, baseURL string, layers [][]by
return err return err
} }
func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo { func getMockCveScanner(metaDB mTypes.MetaDB) cveinfo.Scanner {
// MetaDB loaded with initial data, mock the scanner // MetaDB loaded with initial data, mock the scanner
// Setup test CVE data in mock scanner // Setup test CVE data in mock scanner
getCveResults := func(image string) map[string]cvemodel.CVE {
if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" ||
image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" ||
strings.Contains(image, "sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE2": {
ID: "CVE2",
Severity: "HIGH",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
"CVE4": {
ID: "CVE4",
Severity: "CRITICAL",
Title: "Title CVE4",
Description: "Description CVE4",
},
}
}
if image == "test-repo:latest" ||
strings.Contains(image, "sha256:9f8e1a125c4fb03a0f157d75999b73284ccc5cba18eb772e4643e3499343607e") {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE2": {
ID: "CVE2",
Severity: "HIGH",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
"CVE4": {
ID: "CVE4",
Severity: "CRITICAL",
Title: "Title CVE4",
Description: "Description CVE4",
},
}
}
// By default the image has no vulnerabilities
return map[string]cvemodel.CVE{}
}
scanner := mocks.CveScannerMock{ scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
if image == "zot-cve-test:0.0.1" || image == "a/zot-cve-test:0.0.1" || return getCveResults(image), nil
strings.Contains(image, "zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") || },
strings.Contains(image, "a/zot-cve-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") { GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE {
return map[string]cvemodel.CVE{ return getCveResults(digestStr)
"CVE1": { },
ID: "CVE1", IsResultCachedFn: func(digestStr string) bool {
Severity: "MEDIUM", return true
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE2": {
ID: "CVE2",
Severity: "HIGH",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
}, nil
}
if image == "zot-test:0.0.1" || image == "a/zot-test:0.0.1" ||
strings.Contains(image, "a/zot-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") ||
strings.Contains(image, "zot-test@sha256:40d1f74918aefed733c590f798d7eafde8fc0a7ec63bb8bc52eaae133cf92495") {
return map[string]cvemodel.CVE{
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
"CVE4": {
ID: "CVE4",
Severity: "CRITICAL",
Title: "Title CVE4",
Description: "Description CVE4",
},
}, nil
}
if image == "test-repo:latest" ||
image == "test-repo@sha256:9f8e1a125c4fb03a0f157d75999b73284ccc5cba18eb772e4643e3499343607e" {
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "MEDIUM",
Title: "Title CVE1",
Description: "Description CVE1",
},
"CVE2": {
ID: "CVE2",
Severity: "HIGH",
Title: "Title CVE2",
Description: "Description CVE2",
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
},
"CVE4": {
ID: "CVE4",
Severity: "CRITICAL",
Title: "Title CVE4",
Description: "Description CVE4",
},
}, nil
}
// By default the image has no vulnerabilities
return map[string]cvemodel.CVE{}, nil
}, },
IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { IsImageFormatScannableFn: func(repo string, reference string) (bool, error) {
// Almost same logic compared to actual Trivy specific implementation // Almost same logic compared to actual Trivy specific implementation
@ -357,11 +354,7 @@ func getMockCveInfo(metaDB mTypes.MetaDB, log log.Logger) cveinfo.CveInfo {
}, },
} }
return &cveinfo.BaseCveInfo{ return &scanner
Log: log,
Scanner: scanner,
MetaDB: metaDB,
}
} }
func TestRepoListWithNewestImage(t *testing.T) { func TestRepoListWithNewestImage(t *testing.T) {
@ -698,7 +691,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
panic(err) panic(err)
} }
ctlr.CveInfo = getMockCveInfo(ctlr.MetaDB, ctlr.Log) ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() { go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) { if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
@ -783,13 +776,7 @@ func TestRepoListWithNewestImage(t *testing.T) {
ShouldBeGreaterThan, ShouldBeGreaterThan,
0, 0,
) )
if repo.Name == "zot-cve-test" { So(vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
// This really depends on the test data, but with the current test image it's HIGH
So(vulnerabilities.MaxSeverity, ShouldEqual, "HIGH")
} else if repo.Name == "zot-test" {
// This really depends on the test data, but with the current test image it's CRITICAL
So(vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
}
} }
}) })
} }
@ -3396,7 +3383,7 @@ func TestGlobalSearch(t *testing.T) {
panic(err) panic(err)
} }
ctlr.CveInfo = getMockCveInfo(ctlr.MetaDB, ctlr.Log) ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() { go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) { if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {
@ -3592,9 +3579,15 @@ func TestGlobalSearch(t *testing.T) {
// RepoInfo object does not provide vulnerability information so we need to check differently // RepoInfo object does not provide vulnerability information so we need to check differently
t.Logf("Found vulnerability summary %v", repoSummary.NewestImage.Vulnerabilities) t.Logf("Found vulnerability summary %v", repoSummary.NewestImage.Vulnerabilities)
So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0) if repoName == "repo1" { //nolint:goconst
// There are 0 vulnerabilities this data used in tests So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 4)
So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "NONE") // There are 4 vulnerabilities in the data used in tests
So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
} else {
So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0)
// There are 0 vulnerabilities this data used in tests
So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "NONE")
}
} }
query = ` query = `
@ -3659,9 +3652,9 @@ func TestGlobalSearch(t *testing.T) {
// RepoInfo object does not provide vulnerability information so we need to check differently // RepoInfo object does not provide vulnerability information so we need to check differently
t.Logf("Found vulnerability summary %v", actualImageSummary.Vulnerabilities) t.Logf("Found vulnerability summary %v", actualImageSummary.Vulnerabilities)
// There are 0 vulnerabilities this data used in tests // There are 4 vulnerabilities in the data used in tests
So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 4)
So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "NONE") So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
}) })
} }
@ -6204,7 +6197,7 @@ func TestImageSummary(t *testing.T) {
panic(err) panic(err)
} }
ctlr.CveInfo = getMockCveInfo(ctlr.MetaDB, ctlr.Log) ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB)
go func() { go func() {
if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) { if err := ctlr.Run(ctx); !errors.Is(err, http.ErrServerClosed) {

View file

@ -1152,6 +1152,7 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
cursor = repoBuck.Cursor() cursor = repoBuck.Cursor()
userBookmarks = getUserBookmarks(ctx, transaction) userBookmarks = getUserBookmarks(ctx, transaction)
userStars = getUserStars(ctx, transaction) userStars = getUserStars(ctx, transaction)
viewError error
) )
repoName, repoMetaBlob := cursor.First() repoName, repoMetaBlob := cursor.First()
@ -1163,9 +1164,10 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
repoMeta := mTypes.RepoMetadata{} repoMeta := mTypes.RepoMetadata{}
err := json.Unmarshal(repoMetaBlob, &repoMeta) if err := json.Unmarshal(repoMetaBlob, &repoMeta); err != nil {
if err != nil { viewError = errors.Join(viewError, err)
return err
continue
} }
repoMeta.IsBookmarked = zcommon.Contains(userBookmarks, repoMeta.Name) repoMeta.IsBookmarked = zcommon.Contains(userBookmarks, repoMeta.Name)
@ -1180,7 +1182,10 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck) manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck)
if err != nil { if err != nil {
return fmt.Errorf("metadb: error while unmashaling manifest metadata for digest %s %w", manifestDigest, err) err = fmt.Errorf("metadb: error while unmashaling manifest metadata for digest %s %w", manifestDigest, err)
viewError = errors.Join(viewError, err)
continue
} }
if filterFunc(repoMeta, manifestMeta) { if filterFunc(repoMeta, manifestMeta) {
@ -1192,14 +1197,20 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
indexData, err := fetchIndexDataWithCheck(indexDigest, indexDataMap, indexBuck) indexData, err := fetchIndexDataWithCheck(indexDigest, indexDataMap, indexBuck)
if err != nil { if err != nil {
return fmt.Errorf("metadb: error while getting index data for digest %s %w", indexDigest, err) err = fmt.Errorf("metadb: error while getting index data for digest %s %w", indexDigest, err)
viewError = errors.Join(viewError, err)
continue
} }
var indexContent ispec.Index var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent) err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil { if err != nil {
return fmt.Errorf("metadb: error while unmashaling index content for digest %s %w", indexDigest, err) err = fmt.Errorf("metadb: error while unmashaling index content for digest %s %w", indexDigest, err)
viewError = errors.Join(viewError, err)
continue
} }
matchedManifests := []ispec.Descriptor{} matchedManifests := []ispec.Descriptor{}
@ -1209,7 +1220,10 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck) manifestMeta, err := fetchManifestMetaWithCheck(repoMeta, manifestDigest, manifestMetadataMap, manifestBuck)
if err != nil { if err != nil {
return fmt.Errorf("metadb: error while getting manifest data for digest %s %w", manifestDigest, err) err = fmt.Errorf("metadb: error while getting manifest data for digest %s %w", manifestDigest, err)
viewError = errors.Join(viewError, err)
continue
} }
if filterFunc(repoMeta, manifestMeta) { if filterFunc(repoMeta, manifestMeta) {
@ -1223,7 +1237,9 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
indexBlob, err := json.Marshal(indexContent) indexBlob, err := json.Marshal(indexContent)
if err != nil { if err != nil {
return err viewError = errors.Join(viewError, err)
continue
} }
indexData.IndexBlob = indexBlob indexData.IndexBlob = indexBlob
@ -1247,7 +1263,7 @@ func (bdw *BoltDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFunc,
foundRepos = append(foundRepos, repoMeta) foundRepos = append(foundRepos, repoMeta)
} }
return nil return viewError
}) })
return foundRepos, manifestMetadataMap, indexDataMap, err return foundRepos, manifestMetadataMap, indexDataMap, err

View file

@ -1007,6 +1007,7 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
repoMetaAttributeIterator AttributesIterator repoMetaAttributeIterator AttributesIterator
userBookmarks = getUserBookmarks(ctx, dwr) userBookmarks = getUserBookmarks(ctx, dwr)
userStars = getUserStars(ctx, dwr) userStars = getUserStars(ctx, dwr)
aggregateError error
) )
repoMetaAttributeIterator = NewBaseDynamoAttributesIterator( repoMetaAttributeIterator = NewBaseDynamoAttributesIterator(
@ -1014,19 +1015,24 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
) )
repoMetaAttribute, err := repoMetaAttributeIterator.First(ctx) repoMetaAttribute, err := repoMetaAttributeIterator.First(ctx)
if err != nil {
return foundRepos, manifestMetadataMap, indexDataMap, err
}
for ; repoMetaAttribute != nil; repoMetaAttribute, err = repoMetaAttributeIterator.Next(ctx) { for ; repoMetaAttribute != nil; repoMetaAttribute, err = repoMetaAttributeIterator.Next(ctx) {
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, aggregateError = errors.Join(aggregateError, err)
err
continue
} }
var repoMeta mTypes.RepoMetadata var repoMeta mTypes.RepoMetadata
err := attributevalue.Unmarshal(repoMetaAttribute, &repoMeta) err := attributevalue.Unmarshal(repoMetaAttribute, &repoMeta)
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, aggregateError = errors.Join(aggregateError, err)
err
continue
} }
if ok, err := reqCtx.RepoIsUserAvailable(ctx, repoMeta.Name); !ok || err != nil { if ok, err := reqCtx.RepoIsUserAvailable(ctx, repoMeta.Name); !ok || err != nil {
@ -1046,8 +1052,10 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck
manifestMetadataMap) manifestMetadataMap)
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, err = fmt.Errorf("metadb: error while unmashaling manifest metadata for digest %s \n%w", manifestDigest, err)
fmt.Errorf("metadb: error while unmashaling manifest metadata for digest %s \n%w", manifestDigest, err) aggregateError = errors.Join(aggregateError, err)
continue
} }
if filterFunc(repoMeta, manifestMeta) { if filterFunc(repoMeta, manifestMeta) {
@ -1059,16 +1067,20 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
indexData, err := dwr.fetchIndexDataWithCheck(indexDigest, indexDataMap) //nolint:contextcheck indexData, err := dwr.fetchIndexDataWithCheck(indexDigest, indexDataMap) //nolint:contextcheck
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, err = fmt.Errorf("metadb: error while getting index data for digest %s %w", indexDigest, err)
fmt.Errorf("metadb: error while getting index data for digest %s %w", indexDigest, err) aggregateError = errors.Join(aggregateError, err)
continue
} }
var indexContent ispec.Index var indexContent ispec.Index
err = json.Unmarshal(indexData.IndexBlob, &indexContent) err = json.Unmarshal(indexData.IndexBlob, &indexContent)
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, err = fmt.Errorf("metadb: error while unmashaling index content for digest %s %w", indexDigest, err)
fmt.Errorf("metadb: error while unmashaling index content for digest %s %w", indexDigest, err) aggregateError = errors.Join(aggregateError, err)
continue
} }
matchedManifests := []ispec.Descriptor{} matchedManifests := []ispec.Descriptor{}
@ -1079,8 +1091,10 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck manifestMeta, err := dwr.fetchManifestMetaWithCheck(repoMeta.Name, manifestDigest, //nolint:contextcheck
manifestMetadataMap) manifestMetadataMap)
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, err = fmt.Errorf("%w metadb: error while getting manifest data for digest %s", err, manifestDigest)
fmt.Errorf("%w metadb: error while getting manifest data for digest %s", err, manifestDigest) aggregateError = errors.Join(aggregateError, err)
continue
} }
if filterFunc(repoMeta, manifestMeta) { if filterFunc(repoMeta, manifestMeta) {
@ -1094,8 +1108,9 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
indexBlob, err := json.Marshal(indexContent) indexBlob, err := json.Marshal(indexContent)
if err != nil { if err != nil {
return []mTypes.RepoMetadata{}, map[string]mTypes.ManifestMetadata{}, map[string]mTypes.IndexData{}, aggregateError = errors.Join(aggregateError, err)
err
continue
} }
indexData.IndexBlob = indexBlob indexData.IndexBlob = indexBlob
@ -1119,7 +1134,7 @@ func (dwr *DynamoDB) FilterTags(ctx context.Context, filterFunc mTypes.FilterFun
foundRepos = append(foundRepos, repoMeta) foundRepos = append(foundRepos, repoMeta)
} }
return foundRepos, manifestMetadataMap, indexDataMap, err return foundRepos, manifestMetadataMap, indexDataMap, aggregateError
} }
func (dwr *DynamoDB) FilterRepos(ctx context.Context, filter mTypes.FilterRepoFunc, func (dwr *DynamoDB) FilterRepos(ctx context.Context, filter mTypes.FilterRepoFunc,

View file

@ -66,7 +66,7 @@ type MetaDB interface { //nolint:interfacebloat
// SetManifestData sets ManifestData for a given manifest in the database // SetManifestData sets ManifestData for a given manifest in the database
SetManifestData(manifestDigest godigest.Digest, md ManifestData) error SetManifestData(manifestDigest godigest.Digest, md ManifestData) error
// GetManifestData return the manifest and it's related config // GetManifestData return the manifest and its related config
GetManifestData(manifestDigest godigest.Digest) (ManifestData, error) GetManifestData(manifestDigest godigest.Digest) (ManifestData, error)
// GetManifestMeta returns ManifestMetadata for a given manifest from the database // GetManifestMeta returns ManifestMetadata for a given manifest from the database

View file

@ -10,11 +10,8 @@ type CveInfoMock struct {
GetImageListWithCVEFixedFn func(repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixedFn func(repo, cveID string) ([]cvemodel.TagInfo, error)
GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cvemodel.PageInput, GetCVEListForImageFn func(repo string, reference string, searchedCVE string, pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, common.PageInfo, error) ) ([]cvemodel.CVE, common.PageInfo, error)
GetCVESummaryForImageFn func(repo string, reference string,
) (cvemodel.ImageCVESummary, error)
GetCVESummaryForImageMediaFn func(repo string, digest, mediaType string, GetCVESummaryForImageMediaFn func(repo string, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) ) (cvemodel.ImageCVESummary, error)
UpdateDBFn func() error
} }
func (cveInfo CveInfoMock) GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error) { func (cveInfo CveInfoMock) GetImageListForCVE(repo, cveID string) ([]cvemodel.TagInfo, error) {
@ -47,15 +44,6 @@ func (cveInfo CveInfoMock) GetCVEListForImage(repo string, reference string,
return []cvemodel.CVE{}, common.PageInfo{}, nil return []cvemodel.CVE{}, common.PageInfo{}, nil
} }
func (cveInfo CveInfoMock) GetCVESummaryForImage(repo string, reference string,
) (cvemodel.ImageCVESummary, error) {
if cveInfo.GetCVESummaryForImageFn != nil {
return cveInfo.GetCVESummaryForImageFn(repo, reference)
}
return cvemodel.ImageCVESummary{}, nil
}
func (cveInfo CveInfoMock) GetCVESummaryForImageMedia(repo, digest, mediaType string, func (cveInfo CveInfoMock) GetCVESummaryForImageMedia(repo, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) { ) (cvemodel.ImageCVESummary, error) {
if cveInfo.GetCVESummaryForImageMediaFn != nil { if cveInfo.GetCVESummaryForImageMediaFn != nil {
@ -65,17 +53,11 @@ func (cveInfo CveInfoMock) GetCVESummaryForImageMedia(repo, digest, mediaType st
return cvemodel.ImageCVESummary{}, nil return cvemodel.ImageCVESummary{}, nil
} }
func (cveInfo CveInfoMock) UpdateDB() error {
if cveInfo.UpdateDBFn != nil {
return cveInfo.UpdateDBFn()
}
return nil
}
type CveScannerMock struct { type CveScannerMock struct {
IsImageFormatScannableFn func(repo string, reference string) (bool, error) IsImageFormatScannableFn func(repo string, reference string) (bool, error)
IsImageMediaScannableFn func(repo string, digest, mediaType string) (bool, error) IsImageMediaScannableFn func(repo string, digest, mediaType string) (bool, error)
IsResultCachedFn func(digest string) bool
GetCachedResultFn func(digest string) map[string]cvemodel.CVE
ScanImageFn func(image string) (map[string]cvemodel.CVE, error) ScanImageFn func(image string) (map[string]cvemodel.CVE, error)
UpdateDBFn func() error UpdateDBFn func() error
} }
@ -96,6 +78,22 @@ func (scanner CveScannerMock) IsImageMediaScannable(repo string, digest, mediaTy
return true, nil return true, nil
} }
func (scanner CveScannerMock) IsResultCached(digest string) bool {
if scanner.IsResultCachedFn != nil {
return scanner.IsResultCachedFn(digest)
}
return false
}
func (scanner CveScannerMock) GetCachedResult(digest string) map[string]cvemodel.CVE {
if scanner.GetCachedResultFn != nil {
return scanner.GetCachedResultFn(digest)
}
return map[string]cvemodel.CVE{}
}
func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) { func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) {
if scanner.ScanImageFn != nil { if scanner.ScanImageFn != nil {
return scanner.ScanImageFn(image) return scanner.ScanImageFn(image)