0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

fix(CVE): attempt to scan now returns early with an error if trivyDB metadata json is missing (#1548)

Also modify zli to retry in case of such errors,
assuming the trivyDB will eventually be downloaded by the scheduled task.

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
Andrei Aaron 2023-07-05 19:08:16 +03:00 committed by GitHub
parent 137e5bd793
commit 7fee57e7cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 159 additions and 10 deletions

View file

@ -46,6 +46,7 @@ var (
ErrEmptyValue = errors.New("cache: empty value") ErrEmptyValue = errors.New("cache: empty value")
ErrEmptyRepoList = errors.New("search: no repository found") ErrEmptyRepoList = errors.New("search: no repository found")
ErrCVESearchDisabled = errors.New("search: CVE search is disabled") ErrCVESearchDisabled = errors.New("search: CVE search is disabled")
ErrCVEDBNotFound = errors.New("cve: CVE DB is not present")
ErrInvalidRepositoryName = errors.New("repository: not a valid repository name") ErrInvalidRepositoryName = errors.New("repository: not a valid repository name")
ErrSyncMissingCatalog = errors.New("sync: couldn't fetch upstream registry's catalog") ErrSyncMissingCatalog = errors.New("sync: couldn't fetch upstream registry's catalog")
ErrMethodNotSupported = errors.New("storage: method not supported") ErrMethodNotSupported = errors.New("storage: method not supported")

View file

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"strings"
"time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -14,6 +16,10 @@ import (
zotErrors "zotregistry.io/zot/errors" zotErrors "zotregistry.io/zot/errors"
) )
const (
cveDBRetryInterval = 3
)
func NewCveCommand(searchService SearchService) *cobra.Command { func NewCveCommand(searchService SearchService) *cobra.Command {
searchCveParams := make(map[string]*string) searchCveParams := make(map[string]*string)
@ -148,13 +154,41 @@ func searchCve(searchConfig searchConfig) error {
} }
for _, searcher := range searchers { for _, searcher := range searchers {
found, err := searcher.search(searchConfig) // there can be CVE DB readyness issues on the server side
if found { // we need a retry mechanism for that specific type of errors
if err != nil { maxAttempts := 20
for i := 0; i < maxAttempts; i++ {
found, err := searcher.search(searchConfig)
if !found {
// searcher does not support this searchConfig
// exit the attempts loop and try a different searcher
break
}
if err == nil {
// searcher matcher search config and results are already printed
return nil
}
if i+1 >= maxAttempts {
// searcher matches search config but there are errors
// this is the last attempt and we cannot retry
return err return err
} }
return nil if strings.Contains(err.Error(), zotErrors.ErrCVEDBNotFound.Error()) {
// searches matches search config but CVE DB is not ready server side
// wait and retry a few more times
fmt.Fprintln(searchConfig.resultWriter,
"[warning] CVE DB is not ready [", i, "] - retry in ", cveDBRetryInterval, " seconds")
time.Sleep(cveDBRetryInterval * time.Second)
continue
}
// an unrecoverable error occurred
return err
} }
} }

View file

@ -14,7 +14,9 @@ import (
"os" "os"
"path" "path"
"regexp" "regexp"
"strconv"
"strings" "strings"
"sync"
"testing" "testing"
"time" "time"
@ -287,6 +289,48 @@ func TestSearchCVECmd(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
}) })
Convey("Test images by CVE ID - positive with retries", t, func() {
args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "someURL"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
defer os.Remove(configPath)
mockService := mockServiceForRetry{succeedOn: 2} // CVE info will be provided in 2nd attempt
cveCmd := NewCveCommand(&mockService)
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
t.Logf("Output: %s", str)
So(strings.TrimSpace(str), ShouldContainSubstring,
"[warning] CVE DB is not ready [ 0 ] - retry in "+strconv.Itoa(cveDBRetryInterval)+" seconds")
So(strings.TrimSpace(str), ShouldContainSubstring,
"IMAGE NAME TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB")
So(err, ShouldBeNil)
})
Convey("Test images by CVE ID - failed after retries", t, func() {
args := []string{"cvetest", "--cve-id", "aCVEID", "--url", "someURL"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
defer os.Remove(configPath)
mockService := mockServiceForRetry{succeedOn: -1} // CVE info will be unavailable on all retries
cveCmd := NewCveCommand(&mockService)
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err := cveCmd.Execute()
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
t.Logf("Output: %s", str)
So(strings.TrimSpace(str), ShouldContainSubstring,
"[warning] CVE DB is not ready [ 0 ] - retry in "+strconv.Itoa(cveDBRetryInterval)+" seconds")
So(strings.TrimSpace(str), ShouldNotContainSubstring,
"IMAGE NAME TAG OS/ARCH DIGEST SIGNED SIZE anImage tag os/arch 6e2f80bf false 123kB")
So(err, ShouldNotBeNil)
})
Convey("Test images by CVE ID - invalid CVE ID", t, func() { Convey("Test images by CVE ID - invalid CVE ID", t, func() {
args := []string{"cvetest", "--cve-id", "invalidCVEID"} args := []string{"cvetest", "--cve-id", "invalidCVEID"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
@ -1222,3 +1266,26 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
RepoDB: repoDB, RepoDB: repoDB,
} }
} }
type mockServiceForRetry struct {
mockService
retryCounter int
succeedOn int
}
func (service *mockServiceForRetry) getImagesByCveID(ctx context.Context, config searchConfig,
username, password, cvid string, rch chan stringResult, wtgrp *sync.WaitGroup,
) {
service.retryCounter += 1
if service.retryCounter < service.succeedOn || service.succeedOn < 0 {
rch <- stringResult{"", zotErrors.ErrCVEDBNotFound}
close(rch)
wtgrp.Done()
return
}
service.getImageByName(ctx, config, username, password, "anImage", rch, wtgrp)
}

View file

@ -943,7 +943,7 @@ func isContextDone(ctx context.Context) bool {
} }
} }
// Query using JQL, the query string is passed as a parameter // Query using GQL, the query string is passed as a parameter
// errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr. // errors are returned in the stringResult channel, the unmarshalled payload is in resultPtr.
func (service searchService) makeGraphQLQuery(ctx context.Context, func (service searchService) makeGraphQLQuery(ctx context.Context,
config searchConfig, username, password, query string, config searchConfig, username, password, query string,

View file

@ -4,9 +4,11 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"path" "path"
"sync" "sync"
"github.com/aquasecurity/trivy-db/pkg/metadata"
dbTypes "github.com/aquasecurity/trivy-db/pkg/types" dbTypes "github.com/aquasecurity/trivy-db/pkg/types"
"github.com/aquasecurity/trivy/pkg/commands/artifact" "github.com/aquasecurity/trivy/pkg/commands/artifact"
"github.com/aquasecurity/trivy/pkg/commands/operation" "github.com/aquasecurity/trivy/pkg/commands/operation"
@ -155,6 +157,11 @@ func (scanner Scanner) getTrivyOptions(image string) flag.Options {
func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) { func (scanner Scanner) runTrivy(opts flag.Options) (types.Report, error) {
ctx := context.Background() ctx := context.Background()
err := scanner.checkDBPresence()
if err != nil {
return types.Report{}, err
}
runner, err := artifact.NewRunner(ctx, opts) runner, err := artifact.NewRunner(ctx, opts)
if err != nil { if err != nil {
return types.Report{}, err return types.Report{}, err
@ -333,7 +340,7 @@ func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error)
return cveidMap, nil return cveidMap, nil
} }
// UpdateDb download the Trivy DB / Cache under the store root directory. // UpdateDB downloads the Trivy DB / Cache under the store root directory.
func (scanner Scanner) UpdateDB() error { func (scanner Scanner) UpdateDB() error {
// We need a lock as using multiple substores each with it's own DB // We need a lock as using multiple substores each with it's own DB
// can result in a DATARACE because some varibles in trivy-db are global // can result in a DATARACE because some varibles in trivy-db are global
@ -395,6 +402,34 @@ func (scanner Scanner) updateDB(dbDir string) error {
return nil return nil
} }
// checkDBPresence errors if the DB metadata files cannot be accessed.
func (scanner Scanner) checkDBPresence() error {
result := true
if scanner.storeController.DefaultStore != nil {
dbDir := path.Join(scanner.storeController.DefaultStore.RootDir(), "_trivy")
if _, err := os.Stat(metadata.Path(dbDir)); err != nil {
result = false
}
}
if scanner.storeController.SubStore != nil {
for _, storage := range scanner.storeController.SubStore {
dbDir := path.Join(storage.RootDir(), "_trivy")
if _, err := os.Stat(metadata.Path(dbDir)); err != nil {
result = false
}
}
}
if !result {
return zerr.ErrCVEDBNotFound
}
return nil
}
func (scanner Scanner) CompareSeverities(severity1, severity2 string) int { func (scanner Scanner) CompareSeverities(severity1, severity2 string) int {
return dbTypes.CompareSeverityString(severity1, severity2) return dbTypes.CompareSeverityString(severity1, severity2)
} }

View file

@ -15,6 +15,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/common" "zotregistry.io/zot/pkg/common"
"zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/monitoring"
"zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/log"
@ -124,6 +125,11 @@ func TestMultipleStoragePath(t *testing.T) {
generateTestImage(storeController, img1) generateTestImage(storeController, img1)
generateTestImage(storeController, img2) generateTestImage(storeController, img2)
// Try to scan without the DB being downloaded
_, err = scanner.ScanImage(img0)
So(err, ShouldNotBeNil)
So(err, ShouldWrap, zerr.ErrCVEDBNotFound)
// Download DB since DB download on scan is disabled // Download DB since DB download on scan is disabled
err = scanner.UpdateDB() err = scanner.UpdateDB()
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -197,12 +203,20 @@ func TestTrivyLibraryErrors(t *testing.T) {
err = repodb.ParseStorage(repoDB, storeController, log) err = repodb.ParseStorage(repoDB, storeController, log)
So(err, ShouldBeNil) So(err, ShouldBeNil)
img := "zot-test:0.0.1" //nolint:goconst
// Download DB fails for missing DB url // Download DB fails for missing DB url
scanner := NewScanner(storeController, repoDB, "", "", log) scanner := NewScanner(storeController, repoDB, "", "", log)
err = scanner.UpdateDB() err = scanner.UpdateDB()
So(err, ShouldNotBeNil) So(err, ShouldNotBeNil)
// Try to scan without the DB being downloaded
opts := scanner.getTrivyOptions(img)
_, err = scanner.runTrivy(opts)
So(err, ShouldNotBeNil)
So(err, ShouldWrap, zerr.ErrCVEDBNotFound)
// Download DB fails for invalid Java DB // Download DB fails for invalid Java DB
scanner = NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db", scanner = NewScanner(storeController, repoDB, "ghcr.io/project-zot/trivy-db",
"ghcr.io/project-zot/trivy-not-db", log) "ghcr.io/project-zot/trivy-not-db", log)
@ -217,10 +231,8 @@ func TestTrivyLibraryErrors(t *testing.T) {
err = scanner.UpdateDB() err = scanner.UpdateDB()
So(err, ShouldBeNil) So(err, ShouldBeNil)
img := "zot-test:0.0.1"
// Scanning image with correct options // Scanning image with correct options
opts := scanner.getTrivyOptions(img) opts = scanner.getTrivyOptions(img)
_, err = scanner.runTrivy(opts) _, err = scanner.runTrivy(opts)
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -482,7 +494,7 @@ func TestDefaultTrivyDBUrl(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
// Scanning image // Scanning image
img := "zot-test:0.0.1" img := "zot-test:0.0.1" //nolint:goconst
opts := scanner.getTrivyOptions(img) opts := scanner.getTrivyOptions(img)
_, err = scanner.runTrivy(opts) _, err = scanner.runTrivy(opts)