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:
Andrei Aaron 2022-09-28 21:39:54 +03:00 committed by GitHub
parent 69753aa39a
commit e0d808b196
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 2092 additions and 644 deletions

View File

@ -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)
}
}
}

View File

@ -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")

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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()
}

View File

@ -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)
})
})
}

View 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"`
}

View File

@ -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
}

View 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)
}

View 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)
})
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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: &copyImgTag, 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,

View 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
}

View File

@ -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)