Include image vulnerability information in ImageSummary (#798)
Return this data as part of GlobalSearch and RepoListWithNewestImage query results. This commit also includes refactoring of the CVE scanning logic in order to better encapsulate trivy specific logic, remove CVE scanning logic from the graphql resolver. Signed-off-by: Andrei Aaron <andaaron@cisco.com>
This commit is contained in:
parent
69753aa39a
commit
e0d808b196
@ -455,7 +455,7 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
|
||||
// Enable extensions if extension config is provided for DefaultStore
|
||||
if c.Config != nil && c.Config.Extensions != nil {
|
||||
ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory)
|
||||
ext.EnableSearchExtension(c.Config, c.Log, c.Config.Storage.RootDirectory)
|
||||
ext.EnableSearchExtension(c.Config, c.Log, c.StoreController)
|
||||
}
|
||||
|
||||
if c.Config.Storage.SubPaths != nil {
|
||||
@ -468,7 +468,6 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) {
|
||||
// Enable extensions if extension config is provided for subImageStore
|
||||
if c.Config != nil && c.Config.Extensions != nil {
|
||||
ext.EnableMetricsExtension(c.Config, c.Log, storageConfig.RootDirectory)
|
||||
ext.EnableSearchExtension(c.Config, c.Log, storageConfig.RootDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,12 @@ import (
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) {
|
||||
// We need this object to be a singleton as read/writes in the CVE DB may
|
||||
// occur at any time via DB downloads as well as during scanning.
|
||||
// The library doesn't seem to handle concurrency very well internally.
|
||||
var cveInfo cveinfo.CveInfo // nolint:gochecknoglobals
|
||||
|
||||
func EnableSearchExtension(config *config.Config, log log.Logger, storeController storage.StoreController) {
|
||||
if config.Extensions.Search != nil && *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil {
|
||||
defaultUpdateInterval, _ := time.ParseDuration("2h")
|
||||
|
||||
@ -28,9 +33,10 @@ func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string
|
||||
log.Warn().Msg("CVE update interval set to too-short interval < 2h, changing update duration to 2 hours and continuing.") //nolint:lll // gofumpt conflicts with lll
|
||||
}
|
||||
|
||||
cveInfo = cveinfo.NewCVEInfo(storeController, log)
|
||||
|
||||
go func() {
|
||||
err := downloadTrivyDB(rootDir, log,
|
||||
config.Extensions.Search.CVE.UpdateInterval)
|
||||
err := downloadTrivyDB(log, config.Extensions.Search.CVE.UpdateInterval)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error while downloading TrivyDB")
|
||||
}
|
||||
@ -40,11 +46,11 @@ func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string
|
||||
}
|
||||
}
|
||||
|
||||
func downloadTrivyDB(dbDir string, log log.Logger, updateInterval time.Duration) error {
|
||||
func downloadTrivyDB(log log.Logger, updateInterval time.Duration) error {
|
||||
for {
|
||||
log.Info().Msg("updating the CVE database")
|
||||
|
||||
err := cveinfo.UpdateCVEDb(dbDir, log)
|
||||
err := cveInfo.UpdateDB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -66,9 +72,15 @@ func SetupSearchRoutes(config *config.Config, router *mux.Router, storeControlle
|
||||
var resConfig gql_generated.Config
|
||||
|
||||
if config.Extensions.Search.CVE != nil {
|
||||
resConfig = search.GetResolverConfig(log, storeController, true)
|
||||
// cveinfo should already be initialized by this time
|
||||
// as EnableSearchExtension is supposed to be called earlier, but let's be sure
|
||||
if cveInfo == nil {
|
||||
cveInfo = cveinfo.NewCVEInfo(storeController, log)
|
||||
}
|
||||
|
||||
resConfig = search.GetResolverConfig(log, storeController, cveInfo)
|
||||
} else {
|
||||
resConfig = search.GetResolverConfig(log, storeController, false)
|
||||
resConfig = search.GetResolverConfig(log, storeController, nil)
|
||||
}
|
||||
|
||||
graphqlPrefix := router.PathPrefix(constants.ExtSearchPrefix).Methods("OPTIONS", "GET", "POST")
|
||||
|
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// EnableSearchExtension ...
|
||||
func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) {
|
||||
func EnableSearchExtension(config *config.Config, log log.Logger, storeController storage.StoreController) {
|
||||
log.Warn().Msg("skipping enabling search extension because given zot binary doesn't include this feature," +
|
||||
"please build a binary that does so")
|
||||
}
|
||||
|
@ -61,23 +61,39 @@ func GetRepo(image string) string {
|
||||
return image
|
||||
}
|
||||
|
||||
func GetFixedTags(allTags, infectedTags []TagInfo) []TagInfo {
|
||||
func GetFixedTags(allTags, vulnerableTags []TagInfo) []TagInfo {
|
||||
sort.Slice(allTags, func(i, j int) bool {
|
||||
return allTags[i].Timestamp.Before(allTags[j].Timestamp)
|
||||
})
|
||||
|
||||
latestInfected := TagInfo{}
|
||||
earliestVulnerable := vulnerableTags[0]
|
||||
vulnerableTagMap := make(map[string]TagInfo, len(vulnerableTags))
|
||||
|
||||
for _, tag := range infectedTags {
|
||||
if !tag.Timestamp.Before(latestInfected.Timestamp) {
|
||||
latestInfected = tag
|
||||
for _, tag := range vulnerableTags {
|
||||
vulnerableTagMap[tag.Name] = tag
|
||||
|
||||
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
|
||||
earliestVulnerable = tag
|
||||
}
|
||||
}
|
||||
|
||||
var fixedTags []TagInfo
|
||||
|
||||
// There are some downsides to this logic
|
||||
// We assume there can't be multiple "branches" of the same
|
||||
// image built at different times containing different fixes
|
||||
// There may be older images which have a fix or
|
||||
// newer images which don't
|
||||
for _, tag := range allTags {
|
||||
if tag.Timestamp.After(latestInfected.Timestamp) {
|
||||
if tag.Timestamp.Before(earliestVulnerable.Timestamp) {
|
||||
// The vulnerability did not exist at the time this
|
||||
// image was built
|
||||
continue
|
||||
}
|
||||
// If the image is old enough for the vulnerability to
|
||||
// exist, but it was not detected, it means it contains
|
||||
// the fix
|
||||
if _, ok := vulnerableTagMap[tag.Name]; !ok {
|
||||
fixedTags = append(fixedTags, tag)
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/specs-go"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@ -88,16 +89,17 @@ type GlobalSearch struct {
|
||||
}
|
||||
|
||||
type ImageSummary struct {
|
||||
RepoName string `json:"repoName"`
|
||||
Tag string `json:"tag"`
|
||||
LastUpdated time.Time `json:"lastUpdated"`
|
||||
Size string `json:"size"`
|
||||
Platform OsArch `json:"platform"`
|
||||
Vendor string `json:"vendor"`
|
||||
Score int `json:"score"`
|
||||
IsSigned bool `json:"isSigned"`
|
||||
History []LayerHistory `json:"history"`
|
||||
Layers []LayerSummary `json:"layers"`
|
||||
RepoName string `json:"repoName"`
|
||||
Tag string `json:"tag"`
|
||||
LastUpdated time.Time `json:"lastUpdated"`
|
||||
Size string `json:"size"`
|
||||
Platform OsArch `json:"platform"`
|
||||
Vendor string `json:"vendor"`
|
||||
Score int `json:"score"`
|
||||
IsSigned bool `json:"isSigned"`
|
||||
History []LayerHistory `json:"history"`
|
||||
Layers []LayerSummary `json:"layers"`
|
||||
Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"`
|
||||
}
|
||||
|
||||
type LayerHistory struct {
|
||||
@ -113,6 +115,11 @@ type HistoryDescription struct {
|
||||
EmptyLayer bool `json:"emptyLayer"`
|
||||
}
|
||||
|
||||
type ImageVulnerabilitySummary struct {
|
||||
MaxSeverity string `json:"maxSeverity"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type RepoSummary struct {
|
||||
Name string `json:"name"`
|
||||
LastUpdated time.Time `json:"lastUpdated"`
|
||||
@ -278,71 +285,31 @@ func getTags() ([]common.TagInfo, []common.TagInfo) {
|
||||
|
||||
tags = append(tags, firstTag, secondTag, thirdTag, fourthTag)
|
||||
|
||||
infectedTags := make([]common.TagInfo, 0)
|
||||
infectedTags = append(infectedTags, secondTag)
|
||||
vulnerableTags := make([]common.TagInfo, 0)
|
||||
vulnerableTags = append(vulnerableTags, secondTag)
|
||||
|
||||
return tags, infectedTags
|
||||
return tags, vulnerableTags
|
||||
}
|
||||
|
||||
func TestImageFormat(t *testing.T) {
|
||||
Convey("Test valid image", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
dbDir := "../../../../test/data"
|
||||
func readFileAndSearchString(filePath string, stringToMatch string, timeout time.Duration) (bool, error) {
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancelFunc()
|
||||
|
||||
conf := config.New()
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Lint = &extconf.LintConfig{}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false, nil
|
||||
default:
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
metrics := monitoring.NewMetricsServer(false, log)
|
||||
defaultStore := storage.NewImageStore(dbDir, false, storage.DefaultGCDelay,
|
||||
false, false, log, metrics, nil)
|
||||
storeController := storage.StoreController{DefaultStore: defaultStore}
|
||||
olu := common.NewBaseOciLayoutUtils(storeController, log)
|
||||
|
||||
isValidImage, err := olu.IsValidImageFormat("zot-test")
|
||||
So(err, ShouldBeNil)
|
||||
So(isValidImage, ShouldEqual, true)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-test:0.0.1")
|
||||
So(err, ShouldBeNil)
|
||||
So(isValidImage, ShouldEqual, true)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-test:0.0.")
|
||||
So(err, ShouldBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-noindex-test")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot--tet")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-noindex-test")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-noblobs")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-invalid-index")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-invalid-blob")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-squashfs-test:0.3.22-squashfs")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = olu.IsValidImageFormat("zot-nonreadable-test")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
})
|
||||
if strings.Contains(string(content), stringToMatch) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoListWithNewestImage(t *testing.T) {
|
||||
@ -536,6 +503,22 @@ func TestRepoListWithNewestImage(t *testing.T) {
|
||||
images := responseStruct.RepoListWithNewestImage.Repos
|
||||
So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
// Verify we don't return any vulnerabilities if CVE scanning is disabled
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix +
|
||||
"?query={RepoListWithNewestImage{Name%20NewestImage{Tag%20Vulnerabilities{MaxSeverity%20Count}}}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
err = json.Unmarshal(resp.Body(), &responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.RepoListWithNewestImage.Repos), ShouldEqual, 4)
|
||||
|
||||
images = responseStruct.RepoListWithNewestImage.Repos
|
||||
So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1")
|
||||
So(images[0].NewestImage.Vulnerabilities.Count, ShouldEqual, 0)
|
||||
So(images[0].NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "")
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix +
|
||||
"?query={RepoListWithNewestImage{Name%20NewestImage{Tag}}}")
|
||||
So(resp, ShouldNotBeNil)
|
||||
@ -612,6 +595,120 @@ func TestRepoListWithNewestImage(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("Test repoListWithNewestImage with vulnerability scan enabled", t, func() {
|
||||
subpath := "/a"
|
||||
err := testSetup(t, subpath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = rootDir
|
||||
conf.Storage.SubPaths = make(map[string]config.StorageConfig)
|
||||
conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir}
|
||||
defaultVal := true
|
||||
|
||||
updateDuration, _ := time.ParseDuration("1h")
|
||||
cveConfig := &extconf.CVEConfig{
|
||||
UpdateInterval: updateDuration,
|
||||
}
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
Enable: &defaultVal,
|
||||
CVE: cveConfig,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
// we won't use the logging config feature as we want logs in both
|
||||
// stdout and a file
|
||||
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
logPath := logFile.Name()
|
||||
defer os.Remove(logPath)
|
||||
|
||||
writers := io.MultiWriter(os.Stdout, logFile)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Log.Logger = ctlr.Log.Output(writers)
|
||||
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := ctlr.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(baseURL)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// shut down server
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
_ = ctlr.Server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":3600000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll
|
||||
found, err := readFileAndSearchString(logPath, substring, 2*time.Minute)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
found, err = readFileAndSearchString(logPath, "updating the CVE database", 2*time.Minute)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
found, err = readFileAndSearchString(logPath, "DB update completed, next update scheduled", 4*time.Minute)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err := resty.R().Get(baseURL + "/v2/")
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 422)
|
||||
|
||||
query := "?query={RepoListWithNewestImage{Name%20NewestImage{Tag%20Vulnerabilities{MaxSeverity%20Count}}}}"
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + query)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
var responseStruct RepoWithNewestImageResponse
|
||||
err = json.Unmarshal(resp.Body(), &responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(responseStruct.RepoListWithNewestImage.Repos), ShouldEqual, 4)
|
||||
|
||||
repos := responseStruct.RepoListWithNewestImage.Repos
|
||||
So(repos[0].NewestImage.Tag, ShouldEqual, "0.0.1")
|
||||
|
||||
for _, repo := range repos {
|
||||
vulnerabilities := repo.NewestImage.Vulnerabilities
|
||||
So(vulnerabilities, ShouldNotBeNil)
|
||||
t.Logf("Found vulnerability summary %v", vulnerabilities)
|
||||
// Depends on test data, but current tested images contain hundreds
|
||||
So(vulnerabilities.Count, ShouldBeGreaterThan, 1)
|
||||
So(
|
||||
dbTypes.CompareSeverityString(dbTypes.SeverityUnknown.String(), vulnerabilities.MaxSeverity),
|
||||
ShouldBeGreaterThan,
|
||||
0,
|
||||
)
|
||||
// This really depends on the test data, but with the current test images it's CRITICAL
|
||||
So(vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpandedRepoInfo(t *testing.T) {
|
||||
@ -965,12 +1062,12 @@ func TestUtilsMethod(t *testing.T) {
|
||||
routePrefix = common.GetRoutePrefix("a/b/test:latest")
|
||||
So(routePrefix, ShouldEqual, "/a")
|
||||
|
||||
allTags, infectedTags := getTags()
|
||||
allTags, vulnerableTags := getTags()
|
||||
|
||||
latestTag := common.GetLatestTag(allTags)
|
||||
So(latestTag.Name, ShouldEqual, "1.0.3")
|
||||
|
||||
fixedTags := common.GetFixedTags(allTags, infectedTags)
|
||||
fixedTags := common.GetFixedTags(allTags, vulnerableTags)
|
||||
So(len(fixedTags), ShouldEqual, 2)
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
@ -1937,7 +2034,7 @@ func TestGetRepositories(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGlobalSearch(t *testing.T) {
|
||||
Convey("Test utils", t, func() {
|
||||
Convey("Test global search", t, func() {
|
||||
subpath := "/a"
|
||||
|
||||
err := testSetup(t, subpath)
|
||||
@ -2000,16 +2097,20 @@ func TestGlobalSearch(t *testing.T) {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vulnerabilities {
|
||||
Count
|
||||
MaxSeverity
|
||||
}
|
||||
}
|
||||
Repos {
|
||||
Name
|
||||
LastUpdated
|
||||
Size
|
||||
Platforms {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vendors
|
||||
LastUpdated
|
||||
Size
|
||||
Platforms {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vendors
|
||||
Score
|
||||
NewestImage {
|
||||
RepoName
|
||||
@ -2023,6 +2124,10 @@ func TestGlobalSearch(t *testing.T) {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vulnerabilities {
|
||||
Count
|
||||
MaxSeverity
|
||||
}
|
||||
}
|
||||
}
|
||||
Layers {
|
||||
@ -2098,6 +2203,235 @@ func TestGlobalSearch(t *testing.T) {
|
||||
So(repo.NewestImage.Vendor, ShouldEqual, image.Vendor)
|
||||
So(repo.NewestImage.Platform.Os, ShouldEqual, image.Platform.Os)
|
||||
So(repo.NewestImage.Platform.Arch, ShouldEqual, image.Platform.Arch)
|
||||
So(repo.NewestImage.Vulnerabilities.Count, ShouldEqual, 0)
|
||||
So(repo.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "")
|
||||
}
|
||||
|
||||
// GetRepositories fail
|
||||
|
||||
err = os.Chmod(rootDir, 0o333)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
responseStruct = &GlobalSearchResultResp{}
|
||||
err = json.Unmarshal(resp.Body(), responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(responseStruct.Errors, ShouldNotBeEmpty)
|
||||
err = os.Chmod(rootDir, 0o777)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Test global search with vulnerabitity scanning enabled", t, func() {
|
||||
subpath := "/a"
|
||||
|
||||
err := testSetup(t, subpath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
port := GetFreePort()
|
||||
baseURL := GetBaseURL(port)
|
||||
conf := config.New()
|
||||
conf.HTTP.Port = port
|
||||
conf.Storage.RootDirectory = rootDir
|
||||
conf.Storage.SubPaths = make(map[string]config.StorageConfig)
|
||||
conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir}
|
||||
defaultVal := true
|
||||
|
||||
updateDuration, _ := time.ParseDuration("1h")
|
||||
cveConfig := &extconf.CVEConfig{
|
||||
UpdateInterval: updateDuration,
|
||||
}
|
||||
searchConfig := &extconf.SearchConfig{
|
||||
Enable: &defaultVal,
|
||||
CVE: cveConfig,
|
||||
}
|
||||
conf.Extensions = &extconf.ExtensionConfig{
|
||||
Search: searchConfig,
|
||||
}
|
||||
|
||||
// we won't use the logging config feature as we want logs in both
|
||||
// stdout and a file
|
||||
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
|
||||
So(err, ShouldBeNil)
|
||||
logPath := logFile.Name()
|
||||
defer os.Remove(logPath)
|
||||
|
||||
writers := io.MultiWriter(os.Stdout, logFile)
|
||||
|
||||
ctlr := api.NewController(conf)
|
||||
ctlr.Log.Logger = ctlr.Log.Output(writers)
|
||||
|
||||
go func() {
|
||||
// this blocks
|
||||
if err := ctlr.Run(context.Background()); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// wait till ready
|
||||
for {
|
||||
_, err := resty.R().Get(baseURL)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// shut down server
|
||||
|
||||
defer func() {
|
||||
ctx := context.Background()
|
||||
_ = ctlr.Server.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
// Wait for trivy db to download
|
||||
substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":3600000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll
|
||||
found, err := readFileAndSearchString(logPath, substring, 2*time.Minute)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
found, err = readFileAndSearchString(logPath, "updating the CVE database", 2*time.Minute)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
found, err = readFileAndSearchString(logPath, "DB update completed, next update scheduled", 4*time.Minute)
|
||||
So(found, ShouldBeTrue)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := `
|
||||
{
|
||||
GlobalSearch(query:""){
|
||||
Images {
|
||||
RepoName
|
||||
Tag
|
||||
LastUpdated
|
||||
Size
|
||||
IsSigned
|
||||
Vendor
|
||||
Score
|
||||
Platform {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vulnerabilities {
|
||||
Count
|
||||
MaxSeverity
|
||||
}
|
||||
}
|
||||
Repos {
|
||||
Name
|
||||
LastUpdated
|
||||
Size
|
||||
Platforms {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vendors
|
||||
Score
|
||||
NewestImage {
|
||||
RepoName
|
||||
Tag
|
||||
LastUpdated
|
||||
Size
|
||||
IsSigned
|
||||
Vendor
|
||||
Score
|
||||
Platform {
|
||||
Os
|
||||
Arch
|
||||
}
|
||||
Vulnerabilities {
|
||||
Count
|
||||
MaxSeverity
|
||||
}
|
||||
}
|
||||
}
|
||||
Layers {
|
||||
Digest
|
||||
Size
|
||||
}
|
||||
}
|
||||
}`
|
||||
resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query))
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.StatusCode(), ShouldEqual, 200)
|
||||
|
||||
responseStruct := &GlobalSearchResultResp{}
|
||||
|
||||
err = json.Unmarshal(resp.Body(), responseStruct)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// There are 2 repos: zot-cve-test and zot-test, each having an image with tag 0.0.1
|
||||
imageStore := ctlr.StoreController.DefaultStore
|
||||
|
||||
repos, err := imageStore.GetRepositories()
|
||||
So(err, ShouldBeNil)
|
||||
expectedRepoCount := len(repos)
|
||||
|
||||
allExpectedTagMap := make(map[string][]string, expectedRepoCount)
|
||||
expectedImageCount := 0
|
||||
for _, repo := range repos {
|
||||
tags, err := imageStore.GetImageTags(repo)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
allExpectedTagMap[repo] = tags
|
||||
expectedImageCount += len(tags)
|
||||
}
|
||||
|
||||
// Make sure the repo/image counts match before comparing actual content
|
||||
So(responseStruct.GlobalSearchResult.GlobalSearch.Images, ShouldNotBeNil)
|
||||
t.Logf("returned images: %v", responseStruct.GlobalSearchResult.GlobalSearch.Images)
|
||||
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Images), ShouldEqual, expectedImageCount)
|
||||
t.Logf("returned repos: %v", responseStruct.GlobalSearchResult.GlobalSearch.Repos)
|
||||
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Repos), ShouldEqual, expectedRepoCount)
|
||||
t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers)
|
||||
So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty)
|
||||
|
||||
newestImageMap := make(map[string]ImageSummary)
|
||||
for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images {
|
||||
// Make sure all returned results are supposed to be in the repo
|
||||
So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag)
|
||||
// Identify the newest image in each repo
|
||||
if newestImage, ok := newestImageMap[image.RepoName]; ok {
|
||||
if newestImage.LastUpdated.Before(image.LastUpdated) {
|
||||
newestImageMap[image.RepoName] = image
|
||||
}
|
||||
} else {
|
||||
newestImageMap[image.RepoName] = image
|
||||
}
|
||||
}
|
||||
t.Logf("expected results for newest images in repos: %v", newestImageMap)
|
||||
|
||||
for _, repo := range responseStruct.GlobalSearchResult.GlobalSearch.Repos {
|
||||
image := newestImageMap[repo.Name]
|
||||
So(repo.Name, ShouldEqual, image.RepoName)
|
||||
So(repo.LastUpdated, ShouldEqual, image.LastUpdated)
|
||||
So(repo.Size, ShouldEqual, image.Size)
|
||||
So(repo.Vendors[0], ShouldEqual, image.Vendor)
|
||||
So(repo.Platforms[0].Os, ShouldEqual, image.Platform.Os)
|
||||
So(repo.Platforms[0].Arch, ShouldEqual, image.Platform.Arch)
|
||||
So(repo.NewestImage.RepoName, ShouldEqual, image.RepoName)
|
||||
So(repo.NewestImage.Tag, ShouldEqual, image.Tag)
|
||||
So(repo.NewestImage.LastUpdated, ShouldEqual, image.LastUpdated)
|
||||
So(repo.NewestImage.Size, ShouldEqual, image.Size)
|
||||
So(repo.NewestImage.IsSigned, ShouldEqual, image.IsSigned)
|
||||
So(repo.NewestImage.Vendor, ShouldEqual, image.Vendor)
|
||||
So(repo.NewestImage.Platform.Os, ShouldEqual, image.Platform.Os)
|
||||
So(repo.NewestImage.Platform.Arch, ShouldEqual, image.Platform.Arch)
|
||||
t.Logf("Found vulnerability summary %v", repo.NewestImage.Vulnerabilities)
|
||||
So(repo.NewestImage.Vulnerabilities.Count, ShouldEqual, image.Vulnerabilities.Count)
|
||||
So(repo.NewestImage.Vulnerabilities.Count, ShouldBeGreaterThan, 1)
|
||||
So(repo.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, image.Vulnerabilities.MaxSeverity)
|
||||
// This really depends on the test data, but with the current test images it's CRITICAL
|
||||
So(repo.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL")
|
||||
}
|
||||
|
||||
// GetRepositories fail
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
notreg "github.com/notaryproject/notation-go/registry"
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@ -24,7 +23,6 @@ type OciLayoutUtils interface {
|
||||
GetImageManifests(image string) ([]ispec.Descriptor, error)
|
||||
GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
||||
GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error)
|
||||
IsValidImageFormat(image string) (bool, error)
|
||||
GetImageTagsWithTimestamp(repo string) ([]TagInfo, error)
|
||||
GetImageLastUpdated(imageInfo ispec.Image) time.Time
|
||||
GetImagePlatform(imageInfo ispec.Image) (string, string)
|
||||
@ -115,6 +113,7 @@ func (olu BaseOciLayoutUtils) GetImageManifest(repo string, reference string) (i
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// Provide a list of repositories from all the available image stores.
|
||||
func (olu BaseOciLayoutUtils) GetRepositories() ([]string, error) {
|
||||
defaultStore := olu.StoreController.DefaultStore
|
||||
substores := olu.StoreController.SubStore
|
||||
@ -208,44 +207,6 @@ func (olu BaseOciLayoutUtils) GetImageInfo(imageDir string, hash v1.Hash) (ispec
|
||||
return imageInfo, err
|
||||
}
|
||||
|
||||
func (olu BaseOciLayoutUtils) IsValidImageFormat(image string) (bool, error) {
|
||||
imageDir, inputTag := GetImageDirAndTag(image)
|
||||
|
||||
manifests, err := olu.GetImageManifests(imageDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
tag, ok := manifest.Annotations[ispec.AnnotationRefName]
|
||||
|
||||
if ok && inputTag != "" && tag != inputTag {
|
||||
continue
|
||||
}
|
||||
|
||||
blobManifest, err := olu.GetImageBlobManifest(imageDir, manifest.Digest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
imageLayers := blobManifest.Layers
|
||||
|
||||
for _, imageLayer := range imageLayers {
|
||||
switch imageLayer.MediaType {
|
||||
case types.OCILayer, types.DockerLayer:
|
||||
return true, nil
|
||||
|
||||
default:
|
||||
olu.Log.Debug().Msg("image media type not supported for scanning")
|
||||
|
||||
return false, errors.ErrScanNotSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GetImageTagsWithTimestamp returns a list of image tags with timestamp available in the specified repository.
|
||||
func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) {
|
||||
tagsInfo := make([]TagInfo, 0)
|
||||
|
@ -1,155 +1,63 @@
|
||||
package cveinfo
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/artifact"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
||||
"github.com/aquasecurity/trivy/pkg/report"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/urfave/cli/v2"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
|
||||
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
func getRoutePrefix(name string) string {
|
||||
names := strings.SplitN(name, "/", 2) //nolint:gomnd
|
||||
|
||||
if len(names) != 2 { // nolint: gomnd
|
||||
// it means route is of global storage e.g "centos:latest"
|
||||
if len(names) == 1 {
|
||||
return "/"
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("/%s", names[0])
|
||||
type CveInfo interface {
|
||||
GetImageListForCVE(repo, cveID string) ([]ImageInfoByCVE, error)
|
||||
GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error)
|
||||
GetCVEListForImage(image string) (map[string]cvemodel.CVE, error)
|
||||
GetCVESummaryForImage(image string) (ImageCVESummary, error)
|
||||
UpdateDB() error
|
||||
}
|
||||
|
||||
// UpdateCVEDb ...
|
||||
func UpdateCVEDb(dbDir string, log log.Logger) error {
|
||||
return operation.DownloadDB("dev", dbDir, false, false, false)
|
||||
type Scanner interface {
|
||||
ScanImage(image string) (map[string]cvemodel.CVE, error)
|
||||
IsImageFormatScannable(image string) (bool, error)
|
||||
CompareSeverities(severity1, severity2 string) int
|
||||
UpdateDB() error
|
||||
}
|
||||
|
||||
// NewTrivyContext set some trivy configuration value and return a context.
|
||||
func NewTrivyContext(dir string) *TrivyCtx {
|
||||
trivyCtx := &TrivyCtx{}
|
||||
|
||||
app := &cli.App{}
|
||||
|
||||
flagSet := &flag.FlagSet{}
|
||||
|
||||
var cacheDir string
|
||||
|
||||
flagSet.StringVar(&cacheDir, "cache-dir", dir, "")
|
||||
|
||||
var vuln string
|
||||
|
||||
flagSet.StringVar(&vuln, "vuln-type", strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","), "")
|
||||
|
||||
var severity string
|
||||
|
||||
flagSet.StringVar(&severity, "severity", strings.Join(dbTypes.SeverityNames, ","), "")
|
||||
|
||||
flagSet.StringVar(&trivyCtx.Input, "input", "", "")
|
||||
|
||||
var securityCheck string
|
||||
|
||||
flagSet.StringVar(&securityCheck, "security-checks", types.SecurityCheckVulnerability, "")
|
||||
|
||||
var reportFormat string
|
||||
|
||||
flagSet.StringVar(&reportFormat, "format", "table", "")
|
||||
|
||||
ctx := cli.NewContext(app, flagSet, nil)
|
||||
|
||||
trivyCtx.Ctx = ctx
|
||||
|
||||
return trivyCtx
|
||||
type ImageInfoByCVE struct {
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
Manifest v1.Manifest
|
||||
}
|
||||
|
||||
func ScanImage(ctx *cli.Context) (report.Report, error) {
|
||||
return artifact.TrivyImageRun(ctx)
|
||||
type ImageCVESummary struct {
|
||||
Count int
|
||||
MaxSeverity string
|
||||
}
|
||||
|
||||
func GetCVEInfo(storeController storage.StoreController, log log.Logger) (*CveInfo, error) {
|
||||
cveController := CveTrivyController{}
|
||||
type BaseCveInfo struct {
|
||||
Log log.Logger
|
||||
Scanner Scanner
|
||||
LayoutUtils common.OciLayoutUtils
|
||||
}
|
||||
|
||||
func NewCVEInfo(storeController storage.StoreController, log log.Logger) *BaseCveInfo {
|
||||
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
|
||||
scanner := trivy.NewScanner(storeController, layoutUtils, log)
|
||||
|
||||
subCveConfig := make(map[string]*TrivyCtx)
|
||||
|
||||
if storeController.DefaultStore != nil {
|
||||
imageStore := storeController.DefaultStore
|
||||
|
||||
rootDir := imageStore.RootDir()
|
||||
|
||||
ctx := NewTrivyContext(rootDir)
|
||||
|
||||
cveController.DefaultCveConfig = ctx
|
||||
}
|
||||
|
||||
if storeController.SubStore != nil {
|
||||
for route, storage := range storeController.SubStore {
|
||||
rootDir := storage.RootDir()
|
||||
|
||||
ctx := NewTrivyContext(rootDir)
|
||||
|
||||
subCveConfig[route] = ctx
|
||||
}
|
||||
}
|
||||
|
||||
cveController.SubCveConfig = subCveConfig
|
||||
|
||||
return &CveInfo{
|
||||
Log: log, CveTrivyController: cveController, StoreController: storeController,
|
||||
LayoutUtils: layoutUtils,
|
||||
}, nil
|
||||
return &BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils}
|
||||
}
|
||||
|
||||
func (cveinfo CveInfo) GetTrivyContext(image string) *TrivyCtx {
|
||||
// Split image to get route prefix
|
||||
prefixName := getRoutePrefix(image)
|
||||
|
||||
var trivyCtx *TrivyCtx
|
||||
|
||||
var ok bool
|
||||
|
||||
var rootDir string
|
||||
|
||||
// Get corresponding CVE trivy config, if no sub cve config present that means its default
|
||||
trivyCtx, ok = cveinfo.CveTrivyController.SubCveConfig[prefixName]
|
||||
if ok {
|
||||
imgStore := cveinfo.StoreController.SubStore[prefixName]
|
||||
|
||||
rootDir = imgStore.RootDir()
|
||||
} else {
|
||||
trivyCtx = cveinfo.CveTrivyController.DefaultCveConfig
|
||||
|
||||
imgStore := cveinfo.StoreController.DefaultStore
|
||||
|
||||
rootDir = imgStore.RootDir()
|
||||
}
|
||||
|
||||
trivyCtx.Input = path.Join(rootDir, image)
|
||||
|
||||
return trivyCtx
|
||||
}
|
||||
|
||||
func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.ImageStore,
|
||||
trivyCtx *TrivyCtx,
|
||||
) ([]ImageInfoByCVE, error) {
|
||||
func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]ImageInfoByCVE, error) {
|
||||
imgList := make([]ImageInfoByCVE, 0)
|
||||
|
||||
rootDir := imgStore.RootDir()
|
||||
|
||||
manifests, err := cveinfo.LayoutUtils.GetImageManifests(repo)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag")
|
||||
cveinfo.Log.Error().Err(err).Str("repo", repo).Msg("unable to get list of tags from repo")
|
||||
|
||||
return imgList, err
|
||||
}
|
||||
@ -159,47 +67,141 @@ func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.Im
|
||||
|
||||
image := fmt.Sprintf("%s:%s", repo, tag)
|
||||
|
||||
trivyCtx.Input = path.Join(rootDir, image)
|
||||
|
||||
isValidImage, _ := cveinfo.LayoutUtils.IsValidImageFormat(image)
|
||||
isValidImage, _ := cveinfo.Scanner.IsImageFormatScannable(image)
|
||||
if !isValidImage {
|
||||
cveinfo.Log.Debug().Str("image", repo+":"+tag).Msg("image media type not supported for scanning")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
cveinfo.Log.Info().Str("image", repo+":"+tag).Msg("scanning image")
|
||||
|
||||
report, err := ScanImage(trivyCtx.Ctx)
|
||||
cveMap, err := cveinfo.Scanner.ScanImage(image)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Str("image", repo+":"+tag).Msg("unable to scan image")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
for _, result := range report.Results {
|
||||
for _, vulnerability := range result.Vulnerabilities {
|
||||
if vulnerability.VulnerabilityID == cvid {
|
||||
digest := manifest.Digest
|
||||
for id := range cveMap {
|
||||
if id == cveID {
|
||||
digest := manifest.Digest
|
||||
|
||||
imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(repo, digest)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
|
||||
imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(repo, digest)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest")
|
||||
|
||||
return []ImageInfoByCVE{}, err
|
||||
}
|
||||
|
||||
imgList = append(imgList, ImageInfoByCVE{
|
||||
Tag: tag,
|
||||
Digest: digest,
|
||||
Manifest: imageBlobManifest,
|
||||
})
|
||||
|
||||
break
|
||||
return []ImageInfoByCVE{}, err
|
||||
}
|
||||
|
||||
imgList = append(imgList, ImageInfoByCVE{
|
||||
Tag: tag,
|
||||
Digest: digest,
|
||||
Manifest: imageBlobManifest,
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return imgList, nil
|
||||
}
|
||||
|
||||
func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) {
|
||||
tagsInfo, err := cveinfo.LayoutUtils.GetImageTagsWithTimestamp(repo)
|
||||
if err != nil {
|
||||
cveinfo.Log.Error().Err(err).Str("repo", repo).Msg("unable to get list of tags from repo")
|
||||
|
||||
return []common.TagInfo{}, err
|
||||
}
|
||||
|
||||
vulnerableTags := make([]common.TagInfo, 0)
|
||||
|
||||
var hasCVE bool
|
||||
|
||||
for _, tag := range tagsInfo {
|
||||
image := fmt.Sprintf("%s:%s", repo, tag.Name)
|
||||
tagInfo := common.TagInfo{Name: tag.Name, Timestamp: tag.Timestamp, Digest: tag.Digest}
|
||||
|
||||
isValidImage, _ := cveinfo.Scanner.IsImageFormatScannable(image)
|
||||
if !isValidImage {
|
||||
cveinfo.Log.Debug().Str("image", image).
|
||||
Msg("image media type not supported for scanning, adding as a vulnerable image")
|
||||
|
||||
vulnerableTags = append(vulnerableTags, tagInfo)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
cveMap, err := cveinfo.Scanner.ScanImage(image)
|
||||
if err != nil {
|
||||
cveinfo.Log.Debug().Str("image", image).
|
||||
Msg("scanning failed, adding as a vulnerable image")
|
||||
|
||||
vulnerableTags = append(vulnerableTags, tagInfo)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
hasCVE = false
|
||||
|
||||
for id := range cveMap {
|
||||
if id == cveID {
|
||||
hasCVE = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasCVE {
|
||||
vulnerableTags = append(vulnerableTags, tagInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if len(vulnerableTags) != 0 {
|
||||
cveinfo.Log.Info().Str("repo", repo).Msg("comparing fixed tags timestamp")
|
||||
|
||||
tagsInfo = common.GetFixedTags(tagsInfo, vulnerableTags)
|
||||
} else {
|
||||
cveinfo.Log.Info().Str("repo", repo).Str("cve-id", cveID).
|
||||
Msg("image does not contain any tag that have given cve")
|
||||
}
|
||||
|
||||
return tagsInfo, nil
|
||||
}
|
||||
|
||||
func (cveinfo BaseCveInfo) GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) {
|
||||
cveMap := make(map[string]cvemodel.CVE)
|
||||
|
||||
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image)
|
||||
if !isValidImage {
|
||||
return cveMap, err
|
||||
}
|
||||
|
||||
return cveinfo.Scanner.ScanImage(image)
|
||||
}
|
||||
|
||||
func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary, error) {
|
||||
imageCVESummary := ImageCVESummary{
|
||||
Count: 0,
|
||||
MaxSeverity: "UNKNOWN",
|
||||
}
|
||||
|
||||
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image)
|
||||
if !isValidImage {
|
||||
return imageCVESummary, err
|
||||
}
|
||||
|
||||
cveMap, err := cveinfo.Scanner.ScanImage(image)
|
||||
if err != nil {
|
||||
return imageCVESummary, err
|
||||
}
|
||||
|
||||
for _, cve := range cveMap {
|
||||
if cveinfo.Scanner.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 {
|
||||
imageCVESummary.MaxSeverity = cve.Severity
|
||||
}
|
||||
}
|
||||
imageCVESummary.Count = len(cveMap)
|
||||
|
||||
return imageCVESummary, nil
|
||||
}
|
||||
|
||||
func (cveinfo BaseCveInfo) UpdateDB() error {
|
||||
return cveinfo.Scanner.UpdateDB()
|
||||
}
|
||||
|
@ -14,9 +14,13 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
regTypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"gopkg.in/resty.v1"
|
||||
"zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/api"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
"zotregistry.io/zot/pkg/api/constants"
|
||||
@ -24,14 +28,17 @@ import (
|
||||
"zotregistry.io/zot/pkg/extensions/monitoring"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
||||
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
|
||||
"zotregistry.io/zot/pkg/extensions/search/cve/trivy"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
. "zotregistry.io/zot/pkg/test"
|
||||
"zotregistry.io/zot/pkg/test/mocks"
|
||||
)
|
||||
|
||||
// nolint:gochecknoglobals
|
||||
var (
|
||||
cve *cveinfo.CveInfo
|
||||
cve cveinfo.CveInfo
|
||||
dbDir string
|
||||
updateDuration time.Duration
|
||||
)
|
||||
@ -62,15 +69,8 @@ type ImgList struct {
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type CVEResultForImage struct {
|
||||
Tag string `json:"Tag"`
|
||||
CVEList []CVE `json:"CVEList"`
|
||||
}
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type CVE struct {
|
||||
ID string `json:"Id"`
|
||||
Description string `json:"Description"`
|
||||
Severity string `json:"Severity"`
|
||||
Tag string `json:"Tag"`
|
||||
CVEList []cvemodel.CVE `json:"CVEList"`
|
||||
}
|
||||
|
||||
func testSetup() error {
|
||||
@ -89,8 +89,9 @@ func testSetup() error {
|
||||
storeController := storage.StoreController{DefaultStore: storage.NewImageStore(dir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)}
|
||||
|
||||
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
|
||||
scanner := trivy.NewScanner(storeController, layoutUtils, log)
|
||||
|
||||
cve = &cveinfo.CveInfo{Log: log, StoreController: storeController, LayoutUtils: layoutUtils}
|
||||
cve = &cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils}
|
||||
|
||||
dbDir = dir
|
||||
|
||||
@ -318,43 +319,65 @@ func makeTestFile(fileName, content string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMultipleStoragePath(t *testing.T) {
|
||||
Convey("Test multiple storage path", t, func() {
|
||||
// Create temporary directory
|
||||
firstRootDir := t.TempDir()
|
||||
secondRootDir := t.TempDir()
|
||||
thirdRootDir := t.TempDir()
|
||||
|
||||
func TestImageFormat(t *testing.T) {
|
||||
Convey("Test valid image", t, func() {
|
||||
log := log.NewLogger("debug", "")
|
||||
metrics := monitoring.NewMetricsServer(false, log)
|
||||
dbDir := "../../../../test/data"
|
||||
|
||||
conf := config.New()
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Lint = &extconf.LintConfig{}
|
||||
|
||||
// Create ImageStore
|
||||
firstStore := storage.NewImageStore(firstRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)
|
||||
metrics := monitoring.NewMetricsServer(false, log)
|
||||
defaultStore := storage.NewImageStore(dbDir, false, storage.DefaultGCDelay,
|
||||
false, false, log, metrics, nil)
|
||||
storeController := storage.StoreController{DefaultStore: defaultStore}
|
||||
|
||||
secondStore := storage.NewImageStore(secondRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)
|
||||
|
||||
thirdStore := storage.NewImageStore(thirdRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)
|
||||
|
||||
storeController := storage.StoreController{}
|
||||
|
||||
storeController.DefaultStore = firstStore
|
||||
|
||||
subStore := make(map[string]storage.ImageStore)
|
||||
|
||||
subStore["/a"] = secondStore
|
||||
subStore["/b"] = thirdStore
|
||||
|
||||
storeController.SubStore = subStore
|
||||
|
||||
cveInfo, err := cveinfo.GetCVEInfo(storeController, log)
|
||||
cveInfo := cveinfo.NewCVEInfo(storeController, log)
|
||||
|
||||
isValidImage, err := cveInfo.Scanner.IsImageFormatScannable("zot-test")
|
||||
So(err, ShouldBeNil)
|
||||
So(cveInfo.StoreController.DefaultStore, ShouldNotBeNil)
|
||||
So(cveInfo.StoreController.SubStore, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, true)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test:0.0.1")
|
||||
So(err, ShouldBeNil)
|
||||
So(isValidImage, ShouldEqual, true)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test:0.0.")
|
||||
So(err, ShouldBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot--tet")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-noblobs")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-index")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-blob")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-test:0.3.22-squashfs")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
|
||||
isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-nonreadable-test")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(isValidImage, ShouldEqual, false)
|
||||
})
|
||||
}
|
||||
|
||||
@ -730,3 +753,457 @@ func TestHTTPOptionsResponse(t *testing.T) {
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
func TestCVEStruct(t *testing.T) {
|
||||
Convey("Unit test the CVE struct", t, func() {
|
||||
// Setup test image data in mock storage
|
||||
layoutUtils := mocks.OciLayoutUtilsMock{
|
||||
GetImageManifestsFn: func(repo string) ([]ispec.Descriptor, error) {
|
||||
// Valid image for scanning
|
||||
if repo == "repo1" { //nolint: goconst
|
||||
return []ispec.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: int64(0),
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "0.1.0",
|
||||
},
|
||||
Digest: "abcc",
|
||||
},
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: int64(0),
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "1.0.0",
|
||||
},
|
||||
Digest: "abcd",
|
||||
},
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: int64(0),
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "1.1.0",
|
||||
},
|
||||
Digest: "abce",
|
||||
},
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: int64(0),
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "1.0.1",
|
||||
},
|
||||
Digest: "abcf",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Image with non-scannable blob
|
||||
if repo == "repo2" { //nolint: goconst
|
||||
return []ispec.Descriptor{
|
||||
{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: int64(0),
|
||||
Annotations: map[string]string{
|
||||
ispec.AnnotationRefName: "1.0.0",
|
||||
},
|
||||
Digest: "abcd",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// By default the image is not found
|
||||
return nil, errors.ErrRepoNotFound
|
||||
},
|
||||
GetImageTagsWithTimestampFn: func(repo string) ([]common.TagInfo, error) {
|
||||
// Valid image for scanning
|
||||
if repo == "repo1" { //nolint: goconst
|
||||
return []common.TagInfo{
|
||||
{
|
||||
Name: "0.1.0",
|
||||
Digest: "abcc",
|
||||
Timestamp: time.Date(2008, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "1.0.0",
|
||||
Digest: "abcd",
|
||||
Timestamp: time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "1.1.0",
|
||||
Digest: "abce",
|
||||
Timestamp: time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "1.0.1",
|
||||
Digest: "abcf",
|
||||
Timestamp: time.Date(2011, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Image with non-scannable blob
|
||||
if repo == "repo2" { //nolint: goconst
|
||||
return []common.TagInfo{
|
||||
{
|
||||
Name: "1.0.0",
|
||||
Digest: "abcd",
|
||||
Timestamp: time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// By default do not return any tags
|
||||
return []common.TagInfo{}, errors.ErrRepoNotFound
|
||||
},
|
||||
GetImageBlobManifestFn: func(imageDir string, digest digest.Digest) (v1.Manifest, error) {
|
||||
// Valid image for scanning
|
||||
if imageDir == "repo1" { //nolint: goconst
|
||||
return v1.Manifest{
|
||||
Layers: []v1.Descriptor{
|
||||
{
|
||||
MediaType: regTypes.OCILayer,
|
||||
Size: 0,
|
||||
Digest: v1.Hash{},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Image with non-scannable blob
|
||||
if imageDir == "repo2" { //nolint: goconst
|
||||
return v1.Manifest{
|
||||
Layers: []v1.Descriptor{
|
||||
{
|
||||
MediaType: regTypes.OCIRestrictedLayer,
|
||||
Size: 0,
|
||||
Digest: v1.Hash{},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return v1.Manifest{}, errors.ErrBlobNotFound
|
||||
},
|
||||
}
|
||||
|
||||
severities := map[string]int{
|
||||
"UNKNOWN": 0,
|
||||
"LOW": 1,
|
||||
"MEDIUM": 2,
|
||||
"HIGH": 3,
|
||||
"CRITICAL": 4,
|
||||
}
|
||||
|
||||
// Setup test CVE data in mock scanner
|
||||
scanner := mocks.CveScannerMock{
|
||||
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
|
||||
// Images in chronological order
|
||||
if image == "repo1:0.1.0" {
|
||||
return map[string]cvemodel.CVE{
|
||||
"CVE1": {
|
||||
ID: "CVE1",
|
||||
Severity: "MEDIUM",
|
||||
Title: "Title CVE1",
|
||||
Description: "Description CVE1",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if image == "repo1:1.0.0" {
|
||||
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",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if image == "repo1:1.1.0" {
|
||||
return map[string]cvemodel.CVE{
|
||||
"CVE3": {
|
||||
ID: "CVE3",
|
||||
Severity: "LOW",
|
||||
Title: "Title CVE3",
|
||||
Description: "Description CVE3",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// As a minor release on 1.0.0 banch
|
||||
// does not include all fixes published in 1.1.0
|
||||
if image == "repo1:1.0.1" {
|
||||
return 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",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// By default the image has no vulnerabilities
|
||||
return map[string]cvemodel.CVE{}, nil
|
||||
},
|
||||
CompareSeveritiesFn: func(severity1, severity2 string) int {
|
||||
return severities[severity2] - severities[severity1]
|
||||
},
|
||||
IsImageFormatScannableFn: func(image string) (bool, error) {
|
||||
// Almost same logic compared to actual Trivy specific implementation
|
||||
imageDir, inputTag := common.GetImageDirAndTag(image)
|
||||
|
||||
manifests, err := layoutUtils.GetImageManifests(imageDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
tag, ok := manifest.Annotations[ispec.AnnotationRefName]
|
||||
|
||||
if ok && inputTag != "" && tag != inputTag {
|
||||
continue
|
||||
}
|
||||
|
||||
blobManifest, err := layoutUtils.GetImageBlobManifest(imageDir, manifest.Digest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
imageLayers := blobManifest.Layers
|
||||
|
||||
for _, imageLayer := range imageLayers {
|
||||
switch imageLayer.MediaType {
|
||||
case regTypes.OCILayer, regTypes.DockerLayer:
|
||||
return true, nil
|
||||
|
||||
default:
|
||||
return false, errors.ErrScanNotSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
|
||||
Convey("Test GetCVESummaryForImage", func() {
|
||||
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils}
|
||||
|
||||
// Image is found
|
||||
cveSummary, err := cveInfo.GetCVESummaryForImage("repo1:0.1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(cveSummary.Count, ShouldEqual, 1)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM")
|
||||
|
||||
cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.0.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(cveSummary.Count, ShouldEqual, 3)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "HIGH")
|
||||
|
||||
cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.0.1")
|
||||
So(err, ShouldBeNil)
|
||||
So(cveSummary.Count, ShouldEqual, 2)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM")
|
||||
|
||||
cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(cveSummary.Count, ShouldEqual, 1)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "LOW")
|
||||
|
||||
// Image is not scannable
|
||||
cveSummary, err = cveInfo.GetCVESummaryForImage("repo2:1.0.0")
|
||||
So(err, ShouldEqual, errors.ErrScanNotSupported)
|
||||
So(cveSummary.Count, ShouldEqual, 0)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "UNKNOWN")
|
||||
|
||||
// Image is not found
|
||||
cveSummary, err = cveInfo.GetCVESummaryForImage("repo3:1.0.0")
|
||||
So(err, ShouldEqual, errors.ErrRepoNotFound)
|
||||
So(cveSummary.Count, ShouldEqual, 0)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "UNKNOWN")
|
||||
})
|
||||
|
||||
Convey("Test GetCVEListForImage", func() {
|
||||
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils}
|
||||
|
||||
// Image is found
|
||||
cveMap, err := cveInfo.GetCVEListForImage("repo1:0.1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(cveMap), ShouldEqual, 1)
|
||||
So(cveMap, ShouldContainKey, "CVE1")
|
||||
So(cveMap, ShouldNotContainKey, "CVE2")
|
||||
So(cveMap, ShouldNotContainKey, "CVE3")
|
||||
|
||||
cveMap, err = cveInfo.GetCVEListForImage("repo1:1.0.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(cveMap), ShouldEqual, 3)
|
||||
So(cveMap, ShouldContainKey, "CVE1")
|
||||
So(cveMap, ShouldContainKey, "CVE2")
|
||||
So(cveMap, ShouldContainKey, "CVE3")
|
||||
|
||||
cveMap, err = cveInfo.GetCVEListForImage("repo1:1.0.1")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(cveMap), ShouldEqual, 2)
|
||||
So(cveMap, ShouldContainKey, "CVE1")
|
||||
So(cveMap, ShouldNotContainKey, "CVE2")
|
||||
So(cveMap, ShouldContainKey, "CVE3")
|
||||
|
||||
cveMap, err = cveInfo.GetCVEListForImage("repo1:1.1.0")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(cveMap), ShouldEqual, 1)
|
||||
So(cveMap, ShouldNotContainKey, "CVE1")
|
||||
So(cveMap, ShouldNotContainKey, "CVE2")
|
||||
So(cveMap, ShouldContainKey, "CVE3")
|
||||
|
||||
// Image is not scannable
|
||||
cveMap, err = cveInfo.GetCVEListForImage("repo2:1.0.0")
|
||||
So(err, ShouldEqual, errors.ErrScanNotSupported)
|
||||
So(len(cveMap), ShouldEqual, 0)
|
||||
|
||||
// Image is not found
|
||||
cveMap, err = cveInfo.GetCVEListForImage("repo3:1.0.0")
|
||||
So(err, ShouldEqual, errors.ErrRepoNotFound)
|
||||
So(len(cveMap), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Test GetImageListWithCVEFixed", func() {
|
||||
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils}
|
||||
|
||||
// Image is found
|
||||
tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(tagList), ShouldEqual, 1)
|
||||
So(tagList[0].Name, ShouldEqual, "1.1.0")
|
||||
|
||||
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE2")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(tagList), ShouldEqual, 2)
|
||||
So(tagList[0].Name, ShouldEqual, "1.1.0")
|
||||
So(tagList[1].Name, ShouldEqual, "1.0.1")
|
||||
|
||||
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE3")
|
||||
So(err, ShouldBeNil)
|
||||
// CVE3 is not present in 0.1.0, but that is older than all other
|
||||
// images where it is present. The rest of the images explicitly have it.
|
||||
// This means we consider it not fixed in any image.
|
||||
So(len(tagList), ShouldEqual, 0)
|
||||
|
||||
// Image is not scannable
|
||||
tagList, err = cveInfo.GetImageListWithCVEFixed("repo2", "CVE100")
|
||||
// CVE is not considered fixed as scan is not possible
|
||||
// but do not return an error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(tagList), ShouldEqual, 0)
|
||||
|
||||
// Image is not found
|
||||
tagList, err = cveInfo.GetImageListWithCVEFixed("repo3", "CVE101")
|
||||
So(err, ShouldEqual, errors.ErrRepoNotFound)
|
||||
So(len(tagList), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Test GetImageListForCVE", func() {
|
||||
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils}
|
||||
|
||||
// Image is found
|
||||
imageInfoByCveList, err := cveInfo.GetImageListForCVE("repo1", "CVE1")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 3)
|
||||
So(imageInfoByCveList[0].Tag, ShouldEqual, "0.1.0")
|
||||
So(imageInfoByCveList[1].Tag, ShouldEqual, "1.0.0")
|
||||
So(imageInfoByCveList[2].Tag, ShouldEqual, "1.0.1")
|
||||
|
||||
imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo1", "CVE2")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 1)
|
||||
So(imageInfoByCveList[0].Tag, ShouldEqual, "1.0.0")
|
||||
|
||||
imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo1", "CVE3")
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 3)
|
||||
So(imageInfoByCveList[0].Tag, ShouldEqual, "1.0.0")
|
||||
So(imageInfoByCveList[1].Tag, ShouldEqual, "1.1.0")
|
||||
So(imageInfoByCveList[2].Tag, ShouldEqual, "1.0.1")
|
||||
|
||||
// Image is not scannable
|
||||
imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo2", "CVE100")
|
||||
// Image is not considered affected with CVE as scan is not possible
|
||||
// but do not return an error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 0)
|
||||
|
||||
// Image is not found
|
||||
imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo3", "CVE101")
|
||||
So(err, ShouldEqual, errors.ErrRepoNotFound)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Test errors while scanning", func() {
|
||||
localScanner := scanner
|
||||
|
||||
localScanner.ScanImageFn = func(image string) (map[string]cvemodel.CVE, error) {
|
||||
// Could be any type of error, let's reuse this one
|
||||
return nil, errors.ErrScanNotSupported
|
||||
}
|
||||
|
||||
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: localScanner, LayoutUtils: layoutUtils}
|
||||
|
||||
cveSummary, err := cveInfo.GetCVESummaryForImage("repo1:0.1.0")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(cveSummary.Count, ShouldEqual, 0)
|
||||
So(cveSummary.MaxSeverity, ShouldEqual, "UNKNOWN")
|
||||
|
||||
cveMap, err := cveInfo.GetCVEListForImage("repo1:0.1.0")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(cveMap, ShouldBeNil)
|
||||
|
||||
tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1")
|
||||
// CVE is not considered fixed as scan is not possible
|
||||
// but do not return an error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(tagList), ShouldEqual, 0)
|
||||
|
||||
imageInfoByCveList, err := cveInfo.GetImageListForCVE("repo1", "CVE1")
|
||||
// Image is not considered affected with CVE as scan is not possible
|
||||
// but do not return an error
|
||||
So(err, ShouldBeNil)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 0)
|
||||
})
|
||||
|
||||
Convey("Test error while reading blob manifest", func() {
|
||||
localLayoutUtils := layoutUtils
|
||||
localLayoutUtils.GetImageBlobManifestFn = func(imageDir string,
|
||||
digest digest.Digest,
|
||||
) (v1.Manifest, error) {
|
||||
return v1.Manifest{}, errors.ErrBlobNotFound
|
||||
}
|
||||
|
||||
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: localLayoutUtils}
|
||||
|
||||
imageInfoByCveList, err := cveInfo.GetImageListForCVE("repo1", "CVE1")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(len(imageInfoByCveList), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
17
pkg/extensions/search/cve/model/models.go
Normal file
17
pkg/extensions/search/cve/model/models.go
Normal file
@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type CVE struct {
|
||||
ID string `json:"Id"`
|
||||
Description string `json:"Description"`
|
||||
Severity string `json:"Severity"`
|
||||
Title string `json:"Title"`
|
||||
PackageList []Package `json:"PackageList"`
|
||||
}
|
||||
|
||||
//nolint:tagliatelle // graphQL schema
|
||||
type Package struct {
|
||||
Name string `json:"Name"`
|
||||
InstalledVersion string `json:"InstalledVersion"`
|
||||
FixedVersion string `json:"FixedVersion"`
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
// Package cveinfo ...
|
||||
package cveinfo
|
||||
|
||||
import (
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/urfave/cli/v2"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
// CveInfo ...
|
||||
type CveInfo struct {
|
||||
Log log.Logger
|
||||
CveTrivyController CveTrivyController
|
||||
StoreController storage.StoreController
|
||||
LayoutUtils *common.BaseOciLayoutUtils
|
||||
}
|
||||
|
||||
type CveTrivyController struct {
|
||||
DefaultCveConfig *TrivyCtx
|
||||
SubCveConfig map[string]*TrivyCtx
|
||||
}
|
||||
|
||||
type TrivyCtx struct {
|
||||
Input string
|
||||
Ctx *cli.Context
|
||||
}
|
||||
|
||||
type ImageInfoByCVE struct {
|
||||
Tag string
|
||||
Digest digest.Digest
|
||||
Manifest v1.Manifest
|
||||
}
|
304
pkg/extensions/search/cve/trivy/scanner.go
Normal file
304
pkg/extensions/search/cve/trivy/scanner.go
Normal file
@ -0,0 +1,304 @@
|
||||
package trivy
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/artifact"
|
||||
"github.com/aquasecurity/trivy/pkg/commands/operation"
|
||||
"github.com/aquasecurity/trivy/pkg/types"
|
||||
regTypes "github.com/google/go-containerregistry/pkg/v1/types"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/urfave/cli/v2"
|
||||
"zotregistry.io/zot/errors"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
)
|
||||
|
||||
type trivyCtx struct {
|
||||
Input string
|
||||
Ctx *cli.Context
|
||||
}
|
||||
|
||||
// newTrivyContext set some trivy configuration value and return a context.
|
||||
func newTrivyContext(dir string) *trivyCtx {
|
||||
tCtx := &trivyCtx{}
|
||||
|
||||
app := &cli.App{}
|
||||
|
||||
flagSet := &flag.FlagSet{}
|
||||
|
||||
var cacheDir string
|
||||
|
||||
flagSet.StringVar(&cacheDir, "cache-dir", dir, "")
|
||||
|
||||
var vuln string
|
||||
|
||||
flagSet.StringVar(&vuln, "vuln-type", strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","), "")
|
||||
|
||||
var severity string
|
||||
|
||||
flagSet.StringVar(&severity, "severity", strings.Join(dbTypes.SeverityNames, ","), "")
|
||||
|
||||
flagSet.StringVar(&tCtx.Input, "input", "", "")
|
||||
|
||||
var securityCheck string
|
||||
|
||||
flagSet.StringVar(&securityCheck, "security-checks", types.SecurityCheckVulnerability, "")
|
||||
|
||||
var reportFormat string
|
||||
|
||||
flagSet.StringVar(&reportFormat, "format", "table", "")
|
||||
|
||||
ctx := cli.NewContext(app, flagSet, nil)
|
||||
|
||||
tCtx.Ctx = ctx
|
||||
|
||||
return tCtx
|
||||
}
|
||||
|
||||
type cveTrivyController struct {
|
||||
DefaultCveConfig *trivyCtx
|
||||
SubCveConfig map[string]*trivyCtx
|
||||
}
|
||||
|
||||
type Scanner struct {
|
||||
layoutUtils common.OciLayoutUtils
|
||||
cveController cveTrivyController
|
||||
storeController storage.StoreController
|
||||
log log.Logger
|
||||
dbLock *sync.Mutex
|
||||
}
|
||||
|
||||
func NewScanner(storeController storage.StoreController,
|
||||
layoutUtils common.OciLayoutUtils, log log.Logger,
|
||||
) *Scanner {
|
||||
cveController := cveTrivyController{}
|
||||
|
||||
subCveConfig := make(map[string]*trivyCtx)
|
||||
|
||||
if storeController.DefaultStore != nil {
|
||||
imageStore := storeController.DefaultStore
|
||||
|
||||
rootDir := imageStore.RootDir()
|
||||
|
||||
ctx := newTrivyContext(rootDir)
|
||||
|
||||
cveController.DefaultCveConfig = ctx
|
||||
}
|
||||
|
||||
if storeController.SubStore != nil {
|
||||
for route, storage := range storeController.SubStore {
|
||||
rootDir := storage.RootDir()
|
||||
|
||||
ctx := newTrivyContext(rootDir)
|
||||
|
||||
subCveConfig[route] = ctx
|
||||
}
|
||||
}
|
||||
|
||||
cveController.SubCveConfig = subCveConfig
|
||||
|
||||
return &Scanner{
|
||||
log: log,
|
||||
layoutUtils: layoutUtils,
|
||||
cveController: cveController,
|
||||
storeController: storeController,
|
||||
dbLock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (scanner Scanner) getTrivyContext(image string) *trivyCtx {
|
||||
// Split image to get route prefix
|
||||
prefixName := common.GetRoutePrefix(image)
|
||||
|
||||
var tCtx *trivyCtx
|
||||
|
||||
var ok bool
|
||||
|
||||
var rootDir string
|
||||
|
||||
// Get corresponding CVE trivy config, if no sub cve config present that means its default
|
||||
tCtx, ok = scanner.cveController.SubCveConfig[prefixName]
|
||||
if ok {
|
||||
imgStore := scanner.storeController.SubStore[prefixName]
|
||||
|
||||
rootDir = imgStore.RootDir()
|
||||
} else {
|
||||
tCtx = scanner.cveController.DefaultCveConfig
|
||||
|
||||
imgStore := scanner.storeController.DefaultStore
|
||||
|
||||
rootDir = imgStore.RootDir()
|
||||
}
|
||||
|
||||
tCtx.Input = path.Join(rootDir, image)
|
||||
|
||||
return tCtx
|
||||
}
|
||||
|
||||
func (scanner Scanner) IsImageFormatScannable(image string) (bool, error) {
|
||||
imageDir, inputTag := common.GetImageDirAndTag(image)
|
||||
|
||||
manifests, err := scanner.layoutUtils.GetImageManifests(imageDir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
tag, ok := manifest.Annotations[ispec.AnnotationRefName]
|
||||
|
||||
if ok && inputTag != "" && tag != inputTag {
|
||||
continue
|
||||
}
|
||||
|
||||
blobManifest, err := scanner.layoutUtils.GetImageBlobManifest(imageDir, manifest.Digest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
imageLayers := blobManifest.Layers
|
||||
|
||||
for _, imageLayer := range imageLayers {
|
||||
switch imageLayer.MediaType {
|
||||
case regTypes.OCILayer, regTypes.DockerLayer:
|
||||
return true, nil
|
||||
|
||||
default:
|
||||
scanner.log.Debug().Str("image",
|
||||
image).Msgf("image media type %s not supported for scanning", imageLayer.MediaType)
|
||||
|
||||
return false, errors.ErrScanNotSupported
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) {
|
||||
cveidMap := make(map[string]cvemodel.CVE)
|
||||
|
||||
scanner.log.Info().Str("image", image).Msg("scanning image")
|
||||
|
||||
tCtx := scanner.getTrivyContext(image)
|
||||
|
||||
scanner.dbLock.Lock()
|
||||
report, err := artifact.TrivyImageRun(tCtx.Ctx)
|
||||
scanner.dbLock.Unlock()
|
||||
|
||||
if err != nil { // nolint: wsl
|
||||
scanner.log.Error().Err(err).Str("image", image).Msg("unable to scan image")
|
||||
|
||||
return cveidMap, err
|
||||
}
|
||||
|
||||
for _, result := range report.Results {
|
||||
for _, vulnerability := range result.Vulnerabilities {
|
||||
pkgName := vulnerability.PkgName
|
||||
|
||||
installedVersion := vulnerability.InstalledVersion
|
||||
|
||||
var fixedVersion string
|
||||
if vulnerability.FixedVersion != "" {
|
||||
fixedVersion = vulnerability.FixedVersion
|
||||
} else {
|
||||
fixedVersion = "Not Specified"
|
||||
}
|
||||
|
||||
_, ok := cveidMap[vulnerability.VulnerabilityID]
|
||||
if ok {
|
||||
cveDetailStruct := cveidMap[vulnerability.VulnerabilityID]
|
||||
|
||||
pkgList := cveDetailStruct.PackageList
|
||||
|
||||
pkgList = append(
|
||||
pkgList,
|
||||
cvemodel.Package{
|
||||
Name: pkgName,
|
||||
InstalledVersion: installedVersion,
|
||||
FixedVersion: fixedVersion,
|
||||
},
|
||||
)
|
||||
|
||||
cveDetailStruct.PackageList = pkgList
|
||||
|
||||
cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct
|
||||
} else {
|
||||
newPkgList := make([]cvemodel.Package, 0)
|
||||
|
||||
newPkgList = append(
|
||||
newPkgList,
|
||||
cvemodel.Package{
|
||||
Name: pkgName,
|
||||
InstalledVersion: installedVersion,
|
||||
FixedVersion: fixedVersion,
|
||||
},
|
||||
)
|
||||
|
||||
cveidMap[vulnerability.VulnerabilityID] = cvemodel.CVE{
|
||||
ID: vulnerability.VulnerabilityID,
|
||||
Title: vulnerability.Title,
|
||||
Description: vulnerability.Description,
|
||||
Severity: vulnerability.Severity,
|
||||
PackageList: newPkgList,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cveidMap, nil
|
||||
}
|
||||
|
||||
// UpdateDb download the Trivy DB / Cache under the store root directory.
|
||||
func (scanner Scanner) UpdateDB() error {
|
||||
// We need a lock as using multiple substores each with it's own DB
|
||||
// can result in a DATARACE because some varibles in trivy-db are global
|
||||
// https://github.com/project-zot/trivy-db/blob/main/pkg/db/db.go#L23
|
||||
scanner.dbLock.Lock()
|
||||
defer scanner.dbLock.Unlock()
|
||||
|
||||
if scanner.storeController.DefaultStore != nil {
|
||||
dbDir := scanner.storeController.DefaultStore.RootDir()
|
||||
|
||||
err := scanner.updateDB(dbDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if scanner.storeController.SubStore != nil {
|
||||
for _, storage := range scanner.storeController.SubStore {
|
||||
err := scanner.updateDB(storage.RootDir())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (scanner Scanner) updateDB(dbDir string) error {
|
||||
scanner.log.Debug().Msgf("Download Trivy DB to destination dir: %s", dbDir)
|
||||
|
||||
err := operation.DownloadDB("dev", dbDir, false, false, false)
|
||||
if err != nil {
|
||||
scanner.log.Error().Err(err).Msgf("Error downloading Trivy DB to destination dir: %s", dbDir)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
scanner.log.Debug().Msgf("Finished downloading Trivy DB to destination dir: %s", dbDir)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (scanner Scanner) CompareSeverities(severity1, severity2 string) int {
|
||||
return dbTypes.CompareSeverityString(severity1, severity2)
|
||||
}
|
144
pkg/extensions/search/cve/trivy/scanner_internal_test.go
Normal file
144
pkg/extensions/search/cve/trivy/scanner_internal_test.go
Normal file
@ -0,0 +1,144 @@
|
||||
package trivy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"zotregistry.io/zot/pkg/api/config"
|
||||
extconf "zotregistry.io/zot/pkg/extensions/config"
|
||||
"zotregistry.io/zot/pkg/extensions/monitoring"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
"zotregistry.io/zot/pkg/log"
|
||||
"zotregistry.io/zot/pkg/storage"
|
||||
"zotregistry.io/zot/pkg/test"
|
||||
)
|
||||
|
||||
func generateTestImage(storeController storage.StoreController, image string) {
|
||||
repoName, tag := common.GetImageDirAndTag(image)
|
||||
|
||||
config, layers, manifest, err := test.GetImageComponents(10)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
store := storeController.GetImageStore(repoName)
|
||||
err = store.InitRepo(repoName)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
for _, layerBlob := range layers {
|
||||
layerReader := bytes.NewReader(layerBlob)
|
||||
layerDigest := godigest.FromBytes(layerBlob)
|
||||
_, _, err = store.FullBlobUpload(repoName, layerReader, layerDigest.String())
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
configBlob, err := json.Marshal(config)
|
||||
So(err, ShouldBeNil)
|
||||
configReader := bytes.NewReader(configBlob)
|
||||
configDigest := godigest.FromBytes(configBlob)
|
||||
_, _, err = store.FullBlobUpload(repoName, configReader, configDigest.String())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
manifestBlob, err := json.Marshal(manifest)
|
||||
So(err, ShouldBeNil)
|
||||
_, err = store.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBlob)
|
||||
So(err, ShouldBeNil)
|
||||
}
|
||||
|
||||
func TestMultipleStoragePath(t *testing.T) {
|
||||
Convey("Test multiple storage path", t, func() {
|
||||
// Create temporary directory
|
||||
firstRootDir := t.TempDir()
|
||||
secondRootDir := t.TempDir()
|
||||
thirdRootDir := t.TempDir()
|
||||
|
||||
log := log.NewLogger("debug", "")
|
||||
metrics := monitoring.NewMetricsServer(false, log)
|
||||
|
||||
conf := config.New()
|
||||
conf.Extensions = &extconf.ExtensionConfig{}
|
||||
conf.Extensions.Lint = &extconf.LintConfig{}
|
||||
|
||||
// Create ImageStore
|
||||
firstStore := storage.NewImageStore(firstRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)
|
||||
|
||||
secondStore := storage.NewImageStore(secondRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)
|
||||
|
||||
thirdStore := storage.NewImageStore(thirdRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)
|
||||
|
||||
storeController := storage.StoreController{}
|
||||
|
||||
storeController.DefaultStore = firstStore
|
||||
|
||||
subStore := make(map[string]storage.ImageStore)
|
||||
|
||||
subStore["/a"] = secondStore
|
||||
subStore["/b"] = thirdStore
|
||||
|
||||
storeController.SubStore = subStore
|
||||
|
||||
layoutUtils := common.NewBaseOciLayoutUtils(storeController, log)
|
||||
|
||||
scanner := NewScanner(storeController, layoutUtils, log)
|
||||
|
||||
So(scanner.storeController.DefaultStore, ShouldNotBeNil)
|
||||
So(scanner.storeController.SubStore, ShouldNotBeNil)
|
||||
|
||||
img0 := "test/image0:tag0"
|
||||
img1 := "a/test/image1:tag1"
|
||||
img2 := "b/test/image2:tag2"
|
||||
|
||||
ctx := scanner.getTrivyContext(img0)
|
||||
So(ctx.Input, ShouldEqual, path.Join(firstStore.RootDir(), img0))
|
||||
|
||||
ctx = scanner.getTrivyContext(img1)
|
||||
So(ctx.Input, ShouldEqual, path.Join(secondStore.RootDir(), img1))
|
||||
|
||||
ctx = scanner.getTrivyContext(img2)
|
||||
So(ctx.Input, ShouldEqual, path.Join(thirdStore.RootDir(), img2))
|
||||
|
||||
generateTestImage(storeController, img0)
|
||||
generateTestImage(storeController, img1)
|
||||
generateTestImage(storeController, img2)
|
||||
|
||||
// Scanning image in default store
|
||||
cveMap, err := scanner.ScanImage(img0)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(cveMap), ShouldEqual, 0)
|
||||
|
||||
// Scanning image in substore
|
||||
cveMap, err = scanner.ScanImage(img1)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(cveMap), ShouldEqual, 0)
|
||||
|
||||
// Scanning image which does not exist
|
||||
cveMap, err = scanner.ScanImage("a/test/image2:tag100")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(len(cveMap), ShouldEqual, 0)
|
||||
|
||||
// Download the DB to a default store location without permissions
|
||||
err = os.Chmod(firstRootDir, 0o000)
|
||||
So(err, ShouldBeNil)
|
||||
err = scanner.UpdateDB()
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
// Check the download works correctly when permissions allow
|
||||
err = os.Chmod(firstRootDir, 0o777)
|
||||
So(err, ShouldBeNil)
|
||||
err = scanner.UpdateDB()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Download the DB to a substore location without permissions
|
||||
err = os.Chmod(secondRootDir, 0o000)
|
||||
So(err, ShouldBeNil)
|
||||
err = scanner.UpdateDB()
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
err = os.Chmod(secondRootDir, 0o777)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
}
|
@ -71,25 +71,31 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
ImageSummary struct {
|
||||
ConfigDigest func(childComplexity int) int
|
||||
Description func(childComplexity int) int
|
||||
Digest func(childComplexity int) int
|
||||
Documentation func(childComplexity int) int
|
||||
DownloadCount func(childComplexity int) int
|
||||
History func(childComplexity int) int
|
||||
IsSigned func(childComplexity int) int
|
||||
Labels func(childComplexity int) int
|
||||
LastUpdated func(childComplexity int) int
|
||||
Layers func(childComplexity int) int
|
||||
Licenses func(childComplexity int) int
|
||||
Platform func(childComplexity int) int
|
||||
RepoName func(childComplexity int) int
|
||||
Score func(childComplexity int) int
|
||||
Size func(childComplexity int) int
|
||||
Source func(childComplexity int) int
|
||||
Tag func(childComplexity int) int
|
||||
Title func(childComplexity int) int
|
||||
Vendor func(childComplexity int) int
|
||||
ConfigDigest func(childComplexity int) int
|
||||
Description func(childComplexity int) int
|
||||
Digest func(childComplexity int) int
|
||||
Documentation func(childComplexity int) int
|
||||
DownloadCount func(childComplexity int) int
|
||||
History func(childComplexity int) int
|
||||
IsSigned func(childComplexity int) int
|
||||
Labels func(childComplexity int) int
|
||||
LastUpdated func(childComplexity int) int
|
||||
Layers func(childComplexity int) int
|
||||
Licenses func(childComplexity int) int
|
||||
Platform func(childComplexity int) int
|
||||
RepoName func(childComplexity int) int
|
||||
Score func(childComplexity int) int
|
||||
Size func(childComplexity int) int
|
||||
Source func(childComplexity int) int
|
||||
Tag func(childComplexity int) int
|
||||
Title func(childComplexity int) int
|
||||
Vendor func(childComplexity int) int
|
||||
Vulnerabilities func(childComplexity int) int
|
||||
}
|
||||
|
||||
ImageVulnerabilitySummary struct {
|
||||
Count func(childComplexity int) int
|
||||
MaxSeverity func(childComplexity int) int
|
||||
}
|
||||
|
||||
LayerHistory struct {
|
||||
@ -412,6 +418,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.ImageSummary.Vendor(childComplexity), true
|
||||
|
||||
case "ImageSummary.Vulnerabilities":
|
||||
if e.complexity.ImageSummary.Vulnerabilities == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.ImageSummary.Vulnerabilities(childComplexity), true
|
||||
|
||||
case "ImageVulnerabilitySummary.Count":
|
||||
if e.complexity.ImageVulnerabilitySummary.Count == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.ImageVulnerabilitySummary.Count(childComplexity), true
|
||||
|
||||
case "ImageVulnerabilitySummary.MaxSeverity":
|
||||
if e.complexity.ImageVulnerabilitySummary.MaxSeverity == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.ImageVulnerabilitySummary.MaxSeverity(childComplexity), true
|
||||
|
||||
case "LayerHistory.HistoryDescription":
|
||||
if e.complexity.LayerHistory.HistoryDescription == nil {
|
||||
break
|
||||
@ -789,6 +816,12 @@ type ImageSummary {
|
||||
Source: String
|
||||
Documentation: String
|
||||
History: [LayerHistory]
|
||||
Vulnerabilities: ImageVulnerabilitySummary
|
||||
}
|
||||
|
||||
type ImageVulnerabilitySummary {
|
||||
MaxSeverity: String
|
||||
Count: Int
|
||||
}
|
||||
|
||||
# Brief on a specific repo to be used in queries returning a list of repos
|
||||
@ -1440,6 +1473,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -2563,6 +2598,135 @@ func (ec *executionContext) fieldContext_ImageSummary_History(ctx context.Contex
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _ImageSummary_Vulnerabilities(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Vulnerabilities, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*ImageVulnerabilitySummary)
|
||||
fc.Result = res
|
||||
return ec.marshalOImageVulnerabilitySummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_ImageSummary_Vulnerabilities(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "ImageSummary",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
switch field.Name {
|
||||
case "MaxSeverity":
|
||||
return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field)
|
||||
case "Count":
|
||||
return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name)
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.MaxSeverity, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "ImageVulnerabilitySummary",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type String does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _ImageVulnerabilitySummary_Count(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field)
|
||||
if err != nil {
|
||||
return graphql.Null
|
||||
}
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Count, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*int)
|
||||
fc.Result = res
|
||||
return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_Count(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
|
||||
fc = &graphql.FieldContext{
|
||||
Object: "ImageVulnerabilitySummary",
|
||||
Field: field,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
|
||||
return nil, errors.New("field of type Int does not have child fields")
|
||||
},
|
||||
}
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) _LayerHistory_Layer(ctx context.Context, field graphql.CollectedField, obj *LayerHistory) (ret graphql.Marshaler) {
|
||||
fc, err := ec.fieldContext_LayerHistory_Layer(ctx, field)
|
||||
if err != nil {
|
||||
@ -3128,6 +3292,8 @@ func (ec *executionContext) fieldContext_Query_ImageListForCVE(ctx context.Conte
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -3220,6 +3386,8 @@ func (ec *executionContext) fieldContext_Query_ImageListWithCVEFixed(ctx context
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -3312,6 +3480,8 @@ func (ec *executionContext) fieldContext_Query_ImageListForDigest(ctx context.Co
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -3470,6 +3640,8 @@ func (ec *executionContext) fieldContext_Query_ImageList(ctx context.Context, fi
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -3686,6 +3858,8 @@ func (ec *executionContext) fieldContext_Query_DerivedImageList(ctx context.Cont
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -3778,6 +3952,8 @@ func (ec *executionContext) fieldContext_Query_BaseImageList(ctx context.Context
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -3999,6 +4175,8 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -4395,6 +4573,8 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con
|
||||
return ec.fieldContext_ImageSummary_Documentation(ctx, field)
|
||||
case "History":
|
||||
return ec.fieldContext_ImageSummary_History(ctx, field)
|
||||
case "Vulnerabilities":
|
||||
return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field)
|
||||
}
|
||||
return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name)
|
||||
},
|
||||
@ -6536,6 +6716,39 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection
|
||||
|
||||
out.Values[i] = ec._ImageSummary_History(ctx, field, obj)
|
||||
|
||||
case "Vulnerabilities":
|
||||
|
||||
out.Values[i] = ec._ImageSummary_Vulnerabilities(ctx, field, obj)
|
||||
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
}
|
||||
out.Dispatch()
|
||||
if invalids > 0 {
|
||||
return graphql.Null
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var imageVulnerabilitySummaryImplementors = []string{"ImageVulnerabilitySummary"}
|
||||
|
||||
func (ec *executionContext) _ImageVulnerabilitySummary(ctx context.Context, sel ast.SelectionSet, obj *ImageVulnerabilitySummary) graphql.Marshaler {
|
||||
fields := graphql.CollectFields(ec.OperationContext, sel, imageVulnerabilitySummaryImplementors)
|
||||
out := graphql.NewFieldSet(fields)
|
||||
var invalids uint32
|
||||
for i, field := range fields {
|
||||
switch field.Name {
|
||||
case "__typename":
|
||||
out.Values[i] = graphql.MarshalString("ImageVulnerabilitySummary")
|
||||
case "MaxSeverity":
|
||||
|
||||
out.Values[i] = ec._ImageVulnerabilitySummary_MaxSeverity(ctx, field, obj)
|
||||
|
||||
case "Count":
|
||||
|
||||
out.Values[i] = ec._ImageVulnerabilitySummary_Count(ctx, field, obj)
|
||||
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@ -7898,6 +8111,13 @@ func (ec *executionContext) marshalOImageSummary2ᚖzotregistryᚗioᚋzotᚋpkg
|
||||
return ec._ImageSummary(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalOImageVulnerabilitySummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx context.Context, sel ast.SelectionSet, v *ImageVulnerabilitySummary) graphql.Marshaler {
|
||||
if v == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
return ec._ImageVulnerabilitySummary(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) {
|
||||
if v == nil {
|
||||
return nil, nil
|
||||
|
@ -38,25 +38,31 @@ type HistoryDescription struct {
|
||||
}
|
||||
|
||||
type ImageSummary struct {
|
||||
RepoName *string `json:"RepoName"`
|
||||
Tag *string `json:"Tag"`
|
||||
Digest *string `json:"Digest"`
|
||||
ConfigDigest *string `json:"ConfigDigest"`
|
||||
LastUpdated *time.Time `json:"LastUpdated"`
|
||||
IsSigned *bool `json:"IsSigned"`
|
||||
Size *string `json:"Size"`
|
||||
Platform *OsArch `json:"Platform"`
|
||||
Vendor *string `json:"Vendor"`
|
||||
Score *int `json:"Score"`
|
||||
DownloadCount *int `json:"DownloadCount"`
|
||||
Layers []*LayerSummary `json:"Layers"`
|
||||
Description *string `json:"Description"`
|
||||
Licenses *string `json:"Licenses"`
|
||||
Labels *string `json:"Labels"`
|
||||
Title *string `json:"Title"`
|
||||
Source *string `json:"Source"`
|
||||
Documentation *string `json:"Documentation"`
|
||||
History []*LayerHistory `json:"History"`
|
||||
RepoName *string `json:"RepoName"`
|
||||
Tag *string `json:"Tag"`
|
||||
Digest *string `json:"Digest"`
|
||||
ConfigDigest *string `json:"ConfigDigest"`
|
||||
LastUpdated *time.Time `json:"LastUpdated"`
|
||||
IsSigned *bool `json:"IsSigned"`
|
||||
Size *string `json:"Size"`
|
||||
Platform *OsArch `json:"Platform"`
|
||||
Vendor *string `json:"Vendor"`
|
||||
Score *int `json:"Score"`
|
||||
DownloadCount *int `json:"DownloadCount"`
|
||||
Layers []*LayerSummary `json:"Layers"`
|
||||
Description *string `json:"Description"`
|
||||
Licenses *string `json:"Licenses"`
|
||||
Labels *string `json:"Labels"`
|
||||
Title *string `json:"Title"`
|
||||
Source *string `json:"Source"`
|
||||
Documentation *string `json:"Documentation"`
|
||||
History []*LayerHistory `json:"History"`
|
||||
Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities"`
|
||||
}
|
||||
|
||||
type ImageVulnerabilitySummary struct {
|
||||
MaxSeverity *string `json:"MaxSeverity"`
|
||||
Count *int `json:"Count"`
|
||||
}
|
||||
|
||||
type LayerHistory struct {
|
||||
|
@ -29,37 +29,20 @@ import (
|
||||
|
||||
// Resolver ...
|
||||
type Resolver struct {
|
||||
cveInfo *cveinfo.CveInfo
|
||||
cveInfo cveinfo.CveInfo
|
||||
storeController storage.StoreController
|
||||
digestInfo *digestinfo.DigestInfo
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
type cveDetail struct {
|
||||
Title string
|
||||
Description string
|
||||
Severity string
|
||||
PackageList []*gql_generated.PackageInfo
|
||||
}
|
||||
|
||||
var (
|
||||
ErrBadCtxFormat = errors.New("type assertion failed")
|
||||
ErrBadLayerCount = errors.New("manifest: layers count doesn't correspond to config history")
|
||||
)
|
||||
|
||||
// GetResolverConfig ...
|
||||
func GetResolverConfig(log log.Logger, storeController storage.StoreController, enableCVE bool) gql_generated.Config {
|
||||
var cveInfo *cveinfo.CveInfo
|
||||
|
||||
var err error
|
||||
|
||||
if enableCVE {
|
||||
cveInfo, err = cveinfo.GetCVEInfo(storeController, log)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetResolverConfig(log log.Logger, storeController storage.StoreController, cveInfo cveinfo.CveInfo,
|
||||
) gql_generated.Config {
|
||||
digestInfo := digestinfo.NewDigestInfo(storeController, log)
|
||||
|
||||
resConfig := &Resolver{cveInfo: cveInfo, storeController: storeController, digestInfo: digestInfo, log: log}
|
||||
@ -70,39 +53,6 @@ func GetResolverConfig(log log.Logger, storeController storage.StoreController,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgStore storage.ImageStore,
|
||||
trivyCtx *cveinfo.TrivyCtx,
|
||||
) ([]*gql_generated.ImageSummary, error) {
|
||||
cveResult := []*gql_generated.ImageSummary{}
|
||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
|
||||
for _, repo := range repoList {
|
||||
r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
|
||||
|
||||
imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("error getting tag")
|
||||
|
||||
return cveResult, err
|
||||
}
|
||||
|
||||
for _, imageByCVE := range imageListByCVE {
|
||||
imageConfig, err := olu.GetImageConfigInfo(repo, imageByCVE.Digest)
|
||||
if err != nil {
|
||||
return []*gql_generated.ImageSummary{}, err
|
||||
}
|
||||
|
||||
imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig)
|
||||
|
||||
cveResult = append(
|
||||
cveResult, imageInfo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return cveResult, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) {
|
||||
imgResultForDigest := []*gql_generated.ImageSummary{}
|
||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
@ -138,6 +88,7 @@ func repoListWithNewestImage(
|
||||
ctx context.Context,
|
||||
repoList []string,
|
||||
olu common.OciLayoutUtils,
|
||||
cveInfo cveinfo.CveInfo,
|
||||
log log.Logger,
|
||||
) ([]*gql_generated.RepoSummary, error) {
|
||||
reposSummary := []*gql_generated.RepoSummary{}
|
||||
@ -227,7 +178,8 @@ func repoListWithNewestImage(
|
||||
|
||||
manifestTag, ok := manifest.Annotations[ispec.AnnotationRefName]
|
||||
if !ok {
|
||||
msg := fmt.Sprintf("reference not found for manifest %s", manifest.Digest.String())
|
||||
msg := fmt.Sprintf("reference not found for manifest %s in repo %s",
|
||||
manifest.Digest.String(), repoName)
|
||||
|
||||
log.Error().Msg(msg)
|
||||
graphql.AddError(ctx, gqlerror.Errorf(msg))
|
||||
@ -237,6 +189,25 @@ func repoListWithNewestImage(
|
||||
break
|
||||
}
|
||||
|
||||
imageCveSummary := cveinfo.ImageCVESummary{}
|
||||
// Check if vulnerability scanning is disabled
|
||||
if cveInfo != nil {
|
||||
imageName := fmt.Sprintf("%s:%s", repoName, manifestTag)
|
||||
imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName)
|
||||
|
||||
if err != nil {
|
||||
// Log the error, but we should still include the manifest in results
|
||||
msg := fmt.Sprintf(
|
||||
"unable to run vulnerability scan on tag %s in repo %s",
|
||||
manifestTag,
|
||||
repoName,
|
||||
)
|
||||
|
||||
log.Error().Msg(msg)
|
||||
graphql.AddError(ctx, gqlerror.Errorf(msg))
|
||||
}
|
||||
}
|
||||
|
||||
tag := manifestTag
|
||||
size := strconv.Itoa(int(imageSize))
|
||||
manifestDigest := manifest.Digest.Hex()
|
||||
@ -262,6 +233,10 @@ func repoListWithNewestImage(
|
||||
Licenses: &annotations.Licenses,
|
||||
Labels: &annotations.Labels,
|
||||
Source: &annotations.Source,
|
||||
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
|
||||
MaxSeverity: &imageCveSummary.MaxSeverity,
|
||||
Count: &imageCveSummary.Count,
|
||||
},
|
||||
}
|
||||
|
||||
if manifest.Digest.String() == lastUpdatedTag.Digest {
|
||||
@ -301,7 +276,8 @@ func cleanQuerry(query string) string {
|
||||
return query
|
||||
}
|
||||
|
||||
func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils, log log.Logger) (
|
||||
func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils,
|
||||
cveInfo cveinfo.CveInfo, log log.Logger) (
|
||||
[]*gql_generated.RepoSummary, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary,
|
||||
) {
|
||||
repos := []*gql_generated.RepoSummary{}
|
||||
@ -413,6 +389,22 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils
|
||||
repoPlatforms = append(repoPlatforms, osArch)
|
||||
repoVendors = append(repoVendors, &annotations.Vendor)
|
||||
|
||||
imageCveSummary := cveinfo.ImageCVESummary{}
|
||||
// Check if vulnerability scanning is disabled
|
||||
if cveInfo != nil {
|
||||
imageName := fmt.Sprintf("%s:%s", repo, manifestTag)
|
||||
imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName)
|
||||
|
||||
if err != nil {
|
||||
// Log the error, but we should still include the manifest in results
|
||||
log.Error().Err(err).Msgf(
|
||||
"unable to run vulnerability scan on tag %s in repo %s",
|
||||
manifestTag,
|
||||
repo,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
imageSummary := gql_generated.ImageSummary{
|
||||
RepoName: &repo,
|
||||
Tag: &manifestTag,
|
||||
@ -430,6 +422,10 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils
|
||||
Licenses: &annotations.Licenses,
|
||||
Labels: &annotations.Labels,
|
||||
Source: &annotations.Source,
|
||||
Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{
|
||||
MaxSeverity: &imageCveSummary.MaxSeverity,
|
||||
Count: &imageCveSummary.Count,
|
||||
},
|
||||
}
|
||||
|
||||
if manifest.Digest.String() == lastUpdatedTag.Digest {
|
||||
|
@ -31,8 +31,9 @@ func TestGlobalSearch(t *testing.T) {
|
||||
return common.TagInfo{}, ErrTestError
|
||||
},
|
||||
}
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("GetImageTagsWithTimestamp fail", func() {
|
||||
@ -41,8 +42,9 @@ func TestGlobalSearch(t *testing.T) {
|
||||
return []common.TagInfo{}, ErrTestError
|
||||
},
|
||||
}
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("GetImageManifests fail", func() {
|
||||
@ -51,8 +53,9 @@ func TestGlobalSearch(t *testing.T) {
|
||||
return []ispec.Descriptor{}, ErrTestError
|
||||
},
|
||||
}
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("Manifests given, bad image blob manifest", func() {
|
||||
@ -72,7 +75,9 @@ func TestGlobalSearch(t *testing.T) {
|
||||
return v1.Manifest{}, ErrTestError
|
||||
},
|
||||
}
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("Manifests given, no manifest tag", func() {
|
||||
@ -86,8 +91,9 @@ func TestGlobalSearch(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
|
||||
globalSearch([]string{"repo1"}, "test", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
globalSearch([]string{"repo1"}, "test", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("Global search success, no tag", func() {
|
||||
@ -119,7 +125,8 @@ func TestGlobalSearch(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("Manifests given, bad image config info", func() {
|
||||
@ -139,7 +146,8 @@ func TestGlobalSearch(t *testing.T) {
|
||||
return ispec.Image{}, ErrTestError
|
||||
},
|
||||
}
|
||||
globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
|
||||
Convey("Tag given, no layer match", func() {
|
||||
@ -174,7 +182,8 @@ func TestGlobalSearch(t *testing.T) {
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -189,7 +198,8 @@ func TestRepoListWithNewestImage(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover)
|
||||
_, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
_, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
errs := graphql.GetErrors(ctx)
|
||||
@ -212,7 +222,8 @@ func TestRepoListWithNewestImage(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover)
|
||||
_, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
_, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
errs := graphql.GetErrors(ctx)
|
||||
@ -237,7 +248,8 @@ func TestRepoListWithNewestImage(t *testing.T) {
|
||||
}
|
||||
|
||||
ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover)
|
||||
_, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", ""))
|
||||
mockCve := mocks.CveInfoMock{}
|
||||
_, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, mockCve, log.NewLogger("debug", ""))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
errs := graphql.GetErrors(ctx)
|
||||
|
@ -54,6 +54,12 @@ type ImageSummary {
|
||||
Source: String
|
||||
Documentation: String
|
||||
History: [LayerHistory]
|
||||
Vulnerabilities: ImageVulnerabilitySummary
|
||||
}
|
||||
|
||||
type ImageVulnerabilitySummary {
|
||||
MaxSeverity: String
|
||||
Count: Int
|
||||
}
|
||||
|
||||
# Brief on a specific repo to be used in queries returning a list of repos
|
||||
|
@ -6,96 +6,52 @@ package search
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
godigest "github.com/opencontainers/go-digest"
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
||||
"zotregistry.io/zot/pkg/extensions/search/gql_generated"
|
||||
)
|
||||
|
||||
// CVEListForImage is the resolver for the CVEListForImage field.
|
||||
func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql_generated.CVEResultForImage, error) {
|
||||
trivyCtx := r.cveInfo.GetTrivyContext(image)
|
||||
|
||||
r.log.Info().Str("image", image).Msg("scanning image")
|
||||
|
||||
isValidImage, err := r.cveInfo.LayoutUtils.IsValidImageFormat(image)
|
||||
if !isValidImage {
|
||||
r.log.Debug().Str("image", image).Msg("image media type not supported for scanning")
|
||||
|
||||
return &gql_generated.CVEResultForImage{}, err
|
||||
}
|
||||
|
||||
report, err := cveinfo.ScanImage(trivyCtx.Ctx)
|
||||
cveidMap, err := r.cveInfo.GetCVEListForImage(image)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("unable to scan image repository")
|
||||
|
||||
return &gql_generated.CVEResultForImage{}, err
|
||||
}
|
||||
|
||||
var copyImgTag string
|
||||
|
||||
if strings.Contains(image, ":") {
|
||||
copyImgTag = strings.Split(image, ":")[1]
|
||||
}
|
||||
|
||||
cveidMap := make(map[string]cveDetail)
|
||||
|
||||
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,
|
||||
&gql_generated.PackageInfo{Name: &pkgName, InstalledVersion: &installedVersion, FixedVersion: &fixedVersion})
|
||||
|
||||
cveDetailStruct.PackageList = pkgList
|
||||
|
||||
cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct
|
||||
} else {
|
||||
newPkgList := make([]*gql_generated.PackageInfo, 0)
|
||||
|
||||
newPkgList = append(newPkgList,
|
||||
&gql_generated.PackageInfo{Name: &pkgName, InstalledVersion: &installedVersion, FixedVersion: &fixedVersion})
|
||||
|
||||
cveidMap[vulnerability.VulnerabilityID] = cveDetail{
|
||||
Title: vulnerability.Title,
|
||||
Description: vulnerability.Description, Severity: vulnerability.Severity, PackageList: newPkgList,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_, copyImgTag := common.GetImageDirAndTag(image)
|
||||
|
||||
cveids := []*gql_generated.Cve{}
|
||||
|
||||
for id, cveDetail := range cveidMap {
|
||||
vulID := id
|
||||
|
||||
desc := cveDetail.Description
|
||||
|
||||
title := cveDetail.Title
|
||||
|
||||
severity := cveDetail.Severity
|
||||
|
||||
pkgList := cveDetail.PackageList
|
||||
pkgList := make([]*gql_generated.PackageInfo, 0)
|
||||
|
||||
for _, pkg := range cveDetail.PackageList {
|
||||
pkg := pkg
|
||||
|
||||
pkgList = append(pkgList,
|
||||
&gql_generated.PackageInfo{
|
||||
Name: &pkg.Name,
|
||||
InstalledVersion: &pkg.InstalledVersion,
|
||||
FixedVersion: &pkg.FixedVersion,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
cveids = append(cveids,
|
||||
&gql_generated.Cve{ID: &vulID, Title: &title, Description: &desc, Severity: &severity, PackageList: pkgList})
|
||||
&gql_generated.Cve{
|
||||
ID: &vulID,
|
||||
Title: &title,
|
||||
Description: &desc,
|
||||
Severity: &severity,
|
||||
PackageList: pkgList,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return &gql_generated.CVEResultForImage{Tag: ©ImgTag, CVEList: cveids}, nil
|
||||
@ -103,146 +59,82 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql
|
||||
|
||||
// ImageListForCve is the resolver for the ImageListForCVE field.
|
||||
func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) {
|
||||
finalCveResult := []*gql_generated.ImageSummary{}
|
||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
|
||||
affectedImages := []*gql_generated.ImageSummary{}
|
||||
|
||||
r.log.Info().Msg("extracting repositories")
|
||||
|
||||
defaultStore := r.storeController.DefaultStore
|
||||
|
||||
defaultTrivyCtx := r.cveInfo.CveTrivyController.DefaultCveConfig
|
||||
|
||||
repoList, err := defaultStore.GetRepositories()
|
||||
if err != nil {
|
||||
repoList, err := olu.GetRepositories()
|
||||
if err != nil { // nolint: wsl
|
||||
r.log.Error().Err(err).Msg("unable to search repositories")
|
||||
|
||||
return finalCveResult, err
|
||||
return affectedImages, err
|
||||
}
|
||||
|
||||
r.cveInfo.Log.Info().Msg("scanning each global repository")
|
||||
r.log.Info().Msg("scanning each repository")
|
||||
|
||||
cveResult, err := r.getImageListForCVE(repoList, id, defaultStore, defaultTrivyCtx)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("error getting cve list for global repositories")
|
||||
for _, repo := range repoList {
|
||||
r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo")
|
||||
|
||||
return finalCveResult, err
|
||||
}
|
||||
|
||||
finalCveResult = append(finalCveResult, cveResult...)
|
||||
|
||||
subStore := r.storeController.SubStore
|
||||
|
||||
for route, store := range subStore {
|
||||
subRepoList, err := store.GetRepositories()
|
||||
imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, id)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("unable to search repositories")
|
||||
r.log.Error().Str("repo", repo).Str("CVE", id).Err(err).
|
||||
Msg("error getting image list for CVE from repo")
|
||||
|
||||
return cveResult, err
|
||||
return affectedImages, err
|
||||
}
|
||||
|
||||
subTrivyCtx := r.cveInfo.CveTrivyController.SubCveConfig[route]
|
||||
for _, imageByCVE := range imageListByCVE {
|
||||
imageConfig, err := olu.GetImageConfigInfo(repo, imageByCVE.Digest)
|
||||
if err != nil {
|
||||
return affectedImages, err
|
||||
}
|
||||
|
||||
subCveResult, err := r.getImageListForCVE(subRepoList, id, store, subTrivyCtx)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("unable to get cve result for sub repositories")
|
||||
imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig)
|
||||
|
||||
return finalCveResult, err
|
||||
affectedImages = append(
|
||||
affectedImages,
|
||||
imageInfo,
|
||||
)
|
||||
}
|
||||
|
||||
finalCveResult = append(finalCveResult, subCveResult...)
|
||||
}
|
||||
|
||||
return finalCveResult, nil
|
||||
return affectedImages, nil
|
||||
}
|
||||
|
||||
// ImageListWithCVEFixed is the resolver for the ImageListWithCVEFixed field.
|
||||
func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) ([]*gql_generated.ImageSummary, error) {
|
||||
tagListForCVE := []*gql_generated.ImageSummary{}
|
||||
olu := common.NewBaseOciLayoutUtils(r.storeController, r.log)
|
||||
|
||||
r.log.Info().Str("image", image).Msg("extracting list of tags available in repo")
|
||||
unaffectedImages := []*gql_generated.ImageSummary{}
|
||||
|
||||
tagsInfo, err := r.cveInfo.LayoutUtils.GetImageTagsWithTimestamp(image)
|
||||
tagsInfo, err := r.cveInfo.GetImageListWithCVEFixed(image, id)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("unable to read image tags")
|
||||
|
||||
return tagListForCVE, err
|
||||
}
|
||||
|
||||
infectedTags := make([]common.TagInfo, 0)
|
||||
|
||||
var hasCVE bool
|
||||
|
||||
for _, tag := range tagsInfo {
|
||||
image := fmt.Sprintf("%s:%s", image, tag.Name)
|
||||
|
||||
isValidImage, _ := r.cveInfo.LayoutUtils.IsValidImageFormat(image)
|
||||
if !isValidImage {
|
||||
r.log.Debug().Str("image",
|
||||
fmt.Sprintf("%s:%s", image, tag.Name)).
|
||||
Msg("image media type not supported for scanning, adding as an infected image")
|
||||
|
||||
infectedTags = append(infectedTags, common.TagInfo{Name: tag.Name, Timestamp: tag.Timestamp})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
trivyCtx := r.cveInfo.GetTrivyContext(image)
|
||||
|
||||
r.cveInfo.Log.Info().Str("image", fmt.Sprintf("%s:%s", image, tag.Name)).Msg("scanning image")
|
||||
|
||||
report, err := cveinfo.ScanImage(trivyCtx.Ctx)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).
|
||||
Str("image", fmt.Sprintf("%s:%s", image, tag.Name)).Msg("unable to scan image")
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
hasCVE = false
|
||||
|
||||
for _, result := range report.Results {
|
||||
for _, vulnerability := range result.Vulnerabilities {
|
||||
if vulnerability.VulnerabilityID == id {
|
||||
hasCVE = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasCVE {
|
||||
infectedTags = append(infectedTags, common.TagInfo{Name: tag.Name, Timestamp: tag.Timestamp, Digest: tag.Digest})
|
||||
}
|
||||
}
|
||||
|
||||
if len(infectedTags) != 0 {
|
||||
r.log.Info().Msg("comparing fixed tags timestamp")
|
||||
|
||||
tagsInfo = common.GetFixedTags(tagsInfo, infectedTags)
|
||||
} else {
|
||||
r.log.Info().Str("image", image).Str("cve-id", id).Msg("image does not contain any tag that have given cve")
|
||||
return unaffectedImages, err
|
||||
}
|
||||
|
||||
for _, tag := range tagsInfo {
|
||||
digest := godigest.Digest(tag.Digest)
|
||||
|
||||
manifest, err := r.cveInfo.LayoutUtils.GetImageBlobManifest(image, digest)
|
||||
manifest, err := olu.GetImageBlobManifest(image, digest)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error reading manifest")
|
||||
r.log.Error().Err(err).Str("repo", image).Str("digest", tag.Digest).
|
||||
Msg("extension api: error reading manifest")
|
||||
|
||||
return []*gql_generated.ImageSummary{}, err
|
||||
return unaffectedImages, err
|
||||
}
|
||||
|
||||
imageConfig, err := r.cveInfo.LayoutUtils.GetImageConfigInfo(image, digest)
|
||||
imageConfig, err := olu.GetImageConfigInfo(image, digest)
|
||||
if err != nil {
|
||||
return []*gql_generated.ImageSummary{}, err
|
||||
}
|
||||
|
||||
imageInfo := BuildImageInfo(image, tag.Name, digest, manifest, imageConfig)
|
||||
|
||||
tagListForCVE = append(tagListForCVE, imageInfo)
|
||||
unaffectedImages = append(unaffectedImages, imageInfo)
|
||||
}
|
||||
|
||||
return tagListForCVE, nil
|
||||
return unaffectedImages, nil
|
||||
}
|
||||
|
||||
// ImageListForDigest is the resolver for the ImageListForDigest field.
|
||||
@ -326,7 +218,7 @@ func (r *queryResolver) RepoListWithNewestImage(ctx context.Context) ([]*gql_gen
|
||||
repoList = append(repoList, subRepoList...)
|
||||
}
|
||||
|
||||
reposSummary, err = repoListWithNewestImage(ctx, repoList, olu, r.log)
|
||||
reposSummary, err = repoListWithNewestImage(ctx, repoList, olu, r.cveInfo, r.log)
|
||||
if err != nil {
|
||||
r.log.Error().Err(err).Msg("extension api: error extracting substore image list")
|
||||
|
||||
@ -492,7 +384,7 @@ func (r *queryResolver) GlobalSearch(ctx context.Context, query string) (*gql_ge
|
||||
return &gql_generated.GlobalSearchResult{}, err
|
||||
}
|
||||
|
||||
repos, images, layers := globalSearch(availableRepos, name, tag, olu, r.log)
|
||||
repos, images, layers := globalSearch(availableRepos, name, tag, olu, r.cveInfo, r.log)
|
||||
|
||||
return &gql_generated.GlobalSearchResult{
|
||||
Images: images,
|
||||
|
94
pkg/test/mocks/cve_mock.go
Normal file
94
pkg/test/mocks/cve_mock.go
Normal file
@ -0,0 +1,94 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"zotregistry.io/zot/pkg/extensions/search/common"
|
||||
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
|
||||
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
|
||||
)
|
||||
|
||||
type CveInfoMock struct {
|
||||
GetImageListForCVEFn func(repo, cveID string) ([]cveinfo.ImageInfoByCVE, error)
|
||||
GetImageListWithCVEFixedFn func(repo, cveID string) ([]common.TagInfo, error)
|
||||
GetCVEListForImageFn func(image string) (map[string]cvemodel.CVE, error)
|
||||
GetCVESummaryForImageFn func(image string) (cveinfo.ImageCVESummary, error)
|
||||
UpdateDBFn func() error
|
||||
}
|
||||
|
||||
func (cveInfo CveInfoMock) GetImageListForCVE(repo, cveID string) ([]cveinfo.ImageInfoByCVE, error) {
|
||||
if cveInfo.GetImageListForCVEFn != nil {
|
||||
return cveInfo.GetImageListForCVEFn(repo, cveID)
|
||||
}
|
||||
|
||||
return []cveinfo.ImageInfoByCVE{}, nil
|
||||
}
|
||||
|
||||
func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) {
|
||||
if cveInfo.GetImageListWithCVEFixedFn != nil {
|
||||
return cveInfo.GetImageListWithCVEFixedFn(repo, cveID)
|
||||
}
|
||||
|
||||
return []common.TagInfo{}, nil
|
||||
}
|
||||
|
||||
func (cveInfo CveInfoMock) GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) {
|
||||
if cveInfo.GetCVEListForImageFn != nil {
|
||||
return cveInfo.GetCVEListForImageFn(image)
|
||||
}
|
||||
|
||||
return map[string]cvemodel.CVE{}, nil
|
||||
}
|
||||
|
||||
func (cveInfo CveInfoMock) GetCVESummaryForImage(image string) (cveinfo.ImageCVESummary, error) {
|
||||
if cveInfo.GetCVESummaryForImageFn != nil {
|
||||
return cveInfo.GetCVESummaryForImageFn(image)
|
||||
}
|
||||
|
||||
return cveinfo.ImageCVESummary{}, nil
|
||||
}
|
||||
|
||||
func (cveInfo CveInfoMock) UpdateDB() error {
|
||||
if cveInfo.UpdateDBFn != nil {
|
||||
return cveInfo.UpdateDBFn()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CveScannerMock struct {
|
||||
IsImageFormatScannableFn func(image string) (bool, error)
|
||||
ScanImageFn func(image string) (map[string]cvemodel.CVE, error)
|
||||
CompareSeveritiesFn func(severity1, severity2 string) int
|
||||
UpdateDBFn func() error
|
||||
}
|
||||
|
||||
func (scanner CveScannerMock) IsImageFormatScannable(image string) (bool, error) {
|
||||
if scanner.IsImageFormatScannableFn != nil {
|
||||
return scanner.IsImageFormatScannableFn(image)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) {
|
||||
if scanner.ScanImageFn != nil {
|
||||
return scanner.ScanImageFn(image)
|
||||
}
|
||||
|
||||
return map[string]cvemodel.CVE{}, nil
|
||||
}
|
||||
|
||||
func (scanner CveScannerMock) CompareSeverities(severity1, severity2 string) int {
|
||||
if scanner.CompareSeveritiesFn != nil {
|
||||
return scanner.CompareSeveritiesFn(severity1, severity2)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (scanner CveScannerMock) UpdateDB() error {
|
||||
if scanner.UpdateDBFn != nil {
|
||||
return scanner.UpdateDBFn()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -13,7 +13,6 @@ type OciLayoutUtilsMock struct {
|
||||
GetImageManifestsFn func(image string) ([]ispec.Descriptor, error)
|
||||
GetImageBlobManifestFn func(imageDir string, digest godigest.Digest) (v1.Manifest, error)
|
||||
GetImageInfoFn func(imageDir string, hash v1.Hash) (ispec.Image, error)
|
||||
IsValidImageFormatFn func(image string) (bool, error)
|
||||
GetImageTagsWithTimestampFn func(repo string) ([]common.TagInfo, error)
|
||||
GetImageLastUpdatedFn func(imageInfo ispec.Image) time.Time
|
||||
GetImagePlatformFn func(imageInfo ispec.Image) (string, string)
|
||||
@ -28,7 +27,7 @@ type OciLayoutUtilsMock struct {
|
||||
}
|
||||
|
||||
func (olum OciLayoutUtilsMock) GetRepositories() ([]string, error) {
|
||||
if olum.GetImageManifestsFn != nil {
|
||||
if olum.GetRepositoriesFn != nil {
|
||||
return olum.GetRepositoriesFn()
|
||||
}
|
||||
|
||||
@ -59,14 +58,6 @@ func (olum OciLayoutUtilsMock) GetImageInfo(imageDir string, hash v1.Hash) (ispe
|
||||
return ispec.Image{}, nil
|
||||
}
|
||||
|
||||
func (olum OciLayoutUtilsMock) IsValidImageFormat(image string) (bool, error) {
|
||||
if olum.IsValidImageFormatFn != nil {
|
||||
return olum.IsValidImageFormatFn(image)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (olum OciLayoutUtilsMock) GetImageTagsWithTimestamp(repo string) ([]common.TagInfo, error) {
|
||||
if olum.GetImageTagsWithTimestampFn != nil {
|
||||
return olum.GetImageTagsWithTimestampFn(repo)
|
||||
|
Loading…
x
Reference in New Issue
Block a user