mirror of
https://github.com/anchore/grype
synced 2024-11-12 23:37:06 +00:00
fix: fail when grype cant check for db update (#1247)
Signed-off-by: Shane Dell <shanedell100@gmail.com> Signed-off-by: Keith Zantow <kzantow@gmail.com> Co-authored-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
parent
b26f3e29ee
commit
d21c5490e0
4 changed files with 185 additions and 2 deletions
|
@ -12,8 +12,11 @@ type DBOptions struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbOptionsDefault(id clio.Identification) *DBOptions {
|
func dbOptionsDefault(id clio.Identification) *DBOptions {
|
||||||
|
dbDefaults := options.DefaultDatabase(id)
|
||||||
|
// by default, require update check success for db operations which check for updates
|
||||||
|
dbDefaults.RequireUpdateCheck = true
|
||||||
return &DBOptions{
|
return &DBOptions{
|
||||||
DB: options.DefaultDatabase(id),
|
DB: dbDefaults,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ type Database struct {
|
||||||
ValidateByHashOnStart bool `yaml:"validate-by-hash-on-start" json:"validate-by-hash-on-start" mapstructure:"validate-by-hash-on-start"`
|
ValidateByHashOnStart bool `yaml:"validate-by-hash-on-start" json:"validate-by-hash-on-start" mapstructure:"validate-by-hash-on-start"`
|
||||||
ValidateAge bool `yaml:"validate-age" json:"validate-age" mapstructure:"validate-age"`
|
ValidateAge bool `yaml:"validate-age" json:"validate-age" mapstructure:"validate-age"`
|
||||||
MaxAllowedBuiltAge time.Duration `yaml:"max-allowed-built-age" json:"max-allowed-built-age" mapstructure:"max-allowed-built-age"`
|
MaxAllowedBuiltAge time.Duration `yaml:"max-allowed-built-age" json:"max-allowed-built-age" mapstructure:"max-allowed-built-age"`
|
||||||
|
RequireUpdateCheck bool `yaml:"require-update-check" json:"require-update-check" mapstructure:"require-update-check"`
|
||||||
UpdateAvailableTimeout time.Duration `yaml:"update-available-timeout" json:"update-available-timeout" mapstructure:"update-available-timeout"`
|
UpdateAvailableTimeout time.Duration `yaml:"update-available-timeout" json:"update-available-timeout" mapstructure:"update-available-timeout"`
|
||||||
UpdateDownloadTimeout time.Duration `yaml:"update-download-timeout" json:"update-download-timeout" mapstructure:"update-download-timeout"`
|
UpdateDownloadTimeout time.Duration `yaml:"update-download-timeout" json:"update-download-timeout" mapstructure:"update-download-timeout"`
|
||||||
}
|
}
|
||||||
|
@ -41,6 +42,7 @@ func DefaultDatabase(id clio.Identification) Database {
|
||||||
ValidateAge: true,
|
ValidateAge: true,
|
||||||
// After this period (5 days) the db data is considered stale
|
// After this period (5 days) the db data is considered stale
|
||||||
MaxAllowedBuiltAge: defaultMaxDBAge,
|
MaxAllowedBuiltAge: defaultMaxDBAge,
|
||||||
|
RequireUpdateCheck: false,
|
||||||
UpdateAvailableTimeout: defaultUpdateAvailableTimeout,
|
UpdateAvailableTimeout: defaultUpdateAvailableTimeout,
|
||||||
UpdateDownloadTimeout: defaultUpdateDownloadTimeout,
|
UpdateDownloadTimeout: defaultUpdateDownloadTimeout,
|
||||||
}
|
}
|
||||||
|
@ -54,6 +56,7 @@ func (cfg Database) ToCuratorConfig() db.Config {
|
||||||
ValidateByHashOnGet: cfg.ValidateByHashOnStart,
|
ValidateByHashOnGet: cfg.ValidateByHashOnStart,
|
||||||
ValidateAge: cfg.ValidateAge,
|
ValidateAge: cfg.ValidateAge,
|
||||||
MaxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
|
MaxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
|
||||||
|
RequireUpdateCheck: cfg.RequireUpdateCheck,
|
||||||
ListingFileTimeout: cfg.UpdateAvailableTimeout,
|
ListingFileTimeout: cfg.UpdateAvailableTimeout,
|
||||||
UpdateTimeout: cfg.UpdateDownloadTimeout,
|
UpdateTimeout: cfg.UpdateDownloadTimeout,
|
||||||
}
|
}
|
||||||
|
@ -69,6 +72,7 @@ func (cfg *Database) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||||
descriptions.Add(&cfg.MaxAllowedBuiltAge, `Max allowed age for vulnerability database,
|
descriptions.Add(&cfg.MaxAllowedBuiltAge, `Max allowed age for vulnerability database,
|
||||||
age being the time since it was built
|
age being the time since it was built
|
||||||
Default max age is 120h (or five days)`)
|
Default max age is 120h (or five days)`)
|
||||||
|
descriptions.Add(&cfg.RequireUpdateCheck, `fail the scan if unable to check for database updates`)
|
||||||
descriptions.Add(&cfg.UpdateAvailableTimeout, `Timeout for downloading GRYPE_DB_UPDATE_URL to see if the database needs to be downloaded
|
descriptions.Add(&cfg.UpdateAvailableTimeout, `Timeout for downloading GRYPE_DB_UPDATE_URL to see if the database needs to be downloaded
|
||||||
This file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as needed`)
|
This file is ~156KiB as of 2024-04-17 so the download should be quick; adjust as needed`)
|
||||||
descriptions.Add(&cfg.UpdateDownloadTimeout, `Timeout for downloading actual vulnerability DB
|
descriptions.Add(&cfg.UpdateDownloadTimeout, `Timeout for downloading actual vulnerability DB
|
||||||
|
|
|
@ -37,6 +37,7 @@ type Config struct {
|
||||||
ValidateByHashOnGet bool
|
ValidateByHashOnGet bool
|
||||||
ValidateAge bool
|
ValidateAge bool
|
||||||
MaxAllowedBuiltAge time.Duration
|
MaxAllowedBuiltAge time.Duration
|
||||||
|
RequireUpdateCheck bool
|
||||||
ListingFileTimeout time.Duration
|
ListingFileTimeout time.Duration
|
||||||
UpdateTimeout time.Duration
|
UpdateTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
@ -52,6 +53,7 @@ type Curator struct {
|
||||||
validateByHashOnGet bool
|
validateByHashOnGet bool
|
||||||
validateAge bool
|
validateAge bool
|
||||||
maxAllowedBuiltAge time.Duration
|
maxAllowedBuiltAge time.Duration
|
||||||
|
requireUpdateCheck bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCurator(cfg Config) (Curator, error) {
|
func NewCurator(cfg Config) (Curator, error) {
|
||||||
|
@ -81,6 +83,7 @@ func NewCurator(cfg Config) (Curator, error) {
|
||||||
validateByHashOnGet: cfg.ValidateByHashOnGet,
|
validateByHashOnGet: cfg.ValidateByHashOnGet,
|
||||||
validateAge: cfg.ValidateAge,
|
validateAge: cfg.ValidateAge,
|
||||||
maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
|
maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
|
||||||
|
requireUpdateCheck: cfg.RequireUpdateCheck,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +153,9 @@ func (c *Curator) Update() (bool, error) {
|
||||||
|
|
||||||
updateAvailable, metadata, updateEntry, err := c.IsUpdateAvailable()
|
updateAvailable, metadata, updateEntry, err := c.IsUpdateAvailable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// we want to continue if possible even if we can't check for an update
|
if c.requireUpdateCheck {
|
||||||
|
return false, fmt.Errorf("check for vulnerability database update failed: %+v", err)
|
||||||
|
}
|
||||||
log.Warnf("unable to check for vulnerability database update")
|
log.Warnf("unable to check for vulnerability database update")
|
||||||
log.Debugf("check for vulnerability update failed: %+v", err)
|
log.Debugf("check for vulnerability update failed: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -374,6 +380,171 @@ func TestCurator_validateStaleness(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_requireUpdateCheck(t *testing.T) {
|
||||||
|
toJson := func(listing any) []byte {
|
||||||
|
listingContents := bytes.Buffer{}
|
||||||
|
enc := json.NewEncoder(&listingContents)
|
||||||
|
_ = enc.Encode(listing)
|
||||||
|
return listingContents.Bytes()
|
||||||
|
}
|
||||||
|
checksum := func(b []byte) string {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(b)
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
makeTarGz := func(mod time.Time, contents []byte) []byte {
|
||||||
|
metadata := toJson(MetadataJSON{
|
||||||
|
Built: mod.Format(time.RFC3339),
|
||||||
|
Version: 5,
|
||||||
|
Checksum: "sha256:" + checksum(contents),
|
||||||
|
})
|
||||||
|
tgz := bytes.Buffer{}
|
||||||
|
gz := gzip.NewWriter(&tgz)
|
||||||
|
w := tar.NewWriter(gz)
|
||||||
|
_ = w.WriteHeader(&tar.Header{
|
||||||
|
Name: "metadata.json",
|
||||||
|
Size: int64(len(metadata)),
|
||||||
|
Mode: 0600,
|
||||||
|
})
|
||||||
|
_, _ = w.Write(metadata)
|
||||||
|
_ = w.WriteHeader(&tar.Header{
|
||||||
|
Name: "vulnerability.db",
|
||||||
|
Size: int64(len(contents)),
|
||||||
|
Mode: 0600,
|
||||||
|
})
|
||||||
|
_, _ = w.Write(contents)
|
||||||
|
_ = w.Close()
|
||||||
|
_ = gz.Close()
|
||||||
|
return tgz.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
newTime := time.Date(2024, 06, 13, 17, 13, 13, 0, time.UTC)
|
||||||
|
midTime := time.Date(2022, 06, 13, 17, 13, 13, 0, time.UTC)
|
||||||
|
oldTime := time.Date(2020, 06, 13, 17, 13, 13, 0, time.UTC)
|
||||||
|
|
||||||
|
newDB := makeTarGz(newTime, []byte("some-good-contents"))
|
||||||
|
|
||||||
|
midMetadata := toJson(MetadataJSON{
|
||||||
|
Built: midTime.Format(time.RFC3339),
|
||||||
|
Version: 5,
|
||||||
|
Checksum: "sha256:deadbeefcafe",
|
||||||
|
})
|
||||||
|
|
||||||
|
var handlerFunc http.HandlerFunc
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
handlerFunc(w, r)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
newDbURI := "/db.tar.gz"
|
||||||
|
|
||||||
|
newListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{
|
||||||
|
Built: newTime,
|
||||||
|
URL: mustUrl(url.Parse(srv.URL + newDbURI)),
|
||||||
|
Checksum: "sha256:" + checksum(newDB),
|
||||||
|
}}}})
|
||||||
|
|
||||||
|
oldListing := toJson(Listing{Available: map[int][]ListingEntry{5: {ListingEntry{
|
||||||
|
Built: oldTime,
|
||||||
|
URL: mustUrl(url.Parse(srv.URL + newDbURI)),
|
||||||
|
Checksum: "sha256:" + checksum(newDB),
|
||||||
|
}}}})
|
||||||
|
|
||||||
|
newListingURI := "/listing.json"
|
||||||
|
oldListingURI := "/oldlisting.json"
|
||||||
|
badListingURI := "/badlisting.json"
|
||||||
|
|
||||||
|
handlerFunc = func(response http.ResponseWriter, request *http.Request) {
|
||||||
|
switch request.RequestURI {
|
||||||
|
case newListingURI:
|
||||||
|
response.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = response.Write(newListing)
|
||||||
|
case oldListingURI:
|
||||||
|
response.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = response.Write(oldListing)
|
||||||
|
case newDbURI:
|
||||||
|
response.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = response.Write(newDB)
|
||||||
|
default:
|
||||||
|
http.Error(response, "not found", http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
dbDir map[string][]byte
|
||||||
|
wantResult bool
|
||||||
|
wantErr require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "listing with update",
|
||||||
|
config: Config{
|
||||||
|
ListingURL: srv.URL + newListingURI,
|
||||||
|
RequireUpdateCheck: true,
|
||||||
|
},
|
||||||
|
dbDir: map[string][]byte{
|
||||||
|
"5/metadata.json": midMetadata,
|
||||||
|
},
|
||||||
|
wantResult: true,
|
||||||
|
wantErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no update",
|
||||||
|
config: Config{
|
||||||
|
ListingURL: srv.URL + oldListingURI,
|
||||||
|
RequireUpdateCheck: false,
|
||||||
|
},
|
||||||
|
dbDir: map[string][]byte{
|
||||||
|
"5/metadata.json": midMetadata,
|
||||||
|
},
|
||||||
|
wantResult: false,
|
||||||
|
wantErr: require.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update error fail",
|
||||||
|
config: Config{
|
||||||
|
ListingURL: srv.URL + badListingURI,
|
||||||
|
RequireUpdateCheck: true,
|
||||||
|
},
|
||||||
|
wantResult: false,
|
||||||
|
wantErr: require.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update error continue",
|
||||||
|
config: Config{
|
||||||
|
ListingURL: srv.URL + badListingURI,
|
||||||
|
RequireUpdateCheck: false,
|
||||||
|
},
|
||||||
|
wantResult: false,
|
||||||
|
wantErr: require.NoError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dbTmpDir := t.TempDir()
|
||||||
|
tt.config.DBRootDir = dbTmpDir
|
||||||
|
tt.config.ListingFileTimeout = 1 * time.Minute
|
||||||
|
tt.config.UpdateTimeout = 1 * time.Minute
|
||||||
|
for filePath, contents := range tt.dbDir {
|
||||||
|
fullPath := filepath.Join(dbTmpDir, filepath.FromSlash(filePath))
|
||||||
|
err := os.MkdirAll(filepath.Dir(fullPath), 0700|os.ModeDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = os.WriteFile(fullPath, contents, 0700)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
c, err := NewCurator(tt.config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result, err := c.Update()
|
||||||
|
require.Equal(t, tt.wantResult, result)
|
||||||
|
tt.wantErr(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCuratorTimeoutBehavior(t *testing.T) {
|
func TestCuratorTimeoutBehavior(t *testing.T) {
|
||||||
failAfter := 10 * time.Second
|
failAfter := 10 * time.Second
|
||||||
success := make(chan struct{})
|
success := make(chan struct{})
|
||||||
|
|
Loading…
Reference in a new issue