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
6 changed files with 159 additions and 10 deletions
+38 -4
View File
@@ -7,6 +7,8 @@ import (
"fmt"
"os"
"path"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/spf13/cobra"
@@ -14,6 +16,10 @@ import (
zotErrors "zotregistry.io/zot/errors"
)
const (
cveDBRetryInterval = 3
)
func NewCveCommand(searchService SearchService) *cobra.Command {
searchCveParams := make(map[string]*string)
@@ -148,13 +154,41 @@ func searchCve(searchConfig searchConfig) error {
}
for _, searcher := range searchers {
found, err := searcher.search(searchConfig)
if found {
if err != nil {
// there can be CVE DB readyness issues on the server side
// we need a retry mechanism for that specific type of errors
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 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
}
}
+67
View File
@@ -14,7 +14,9 @@ import (
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"testing"
"time"
@@ -287,6 +289,48 @@ func TestSearchCVECmd(t *testing.T) {
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() {
args := []string{"cvetest", "--cve-id", "invalidCVEID"}
configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`)
@@ -1222,3 +1266,26 @@ func getMockCveInfo(repoDB repodb.RepoDB, log log.Logger) cveinfo.CveInfo {
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)
}
+1 -1
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.
func (service searchService) makeGraphQLQuery(ctx context.Context,
config searchConfig, username, password, query string,