mirror of
https://github.com/anchore/grype
synced 2024-11-10 06:34:13 +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 {
|
||||
dbDefaults := options.DefaultDatabase(id)
|
||||
// by default, require update check success for db operations which check for updates
|
||||
dbDefaults.RequireUpdateCheck = true
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
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,
|
||||
// After this period (5 days) the db data is considered stale
|
||||
MaxAllowedBuiltAge: defaultMaxDBAge,
|
||||
RequireUpdateCheck: false,
|
||||
UpdateAvailableTimeout: defaultUpdateAvailableTimeout,
|
||||
UpdateDownloadTimeout: defaultUpdateDownloadTimeout,
|
||||
}
|
||||
|
@ -54,6 +56,7 @@ func (cfg Database) ToCuratorConfig() db.Config {
|
|||
ValidateByHashOnGet: cfg.ValidateByHashOnStart,
|
||||
ValidateAge: cfg.ValidateAge,
|
||||
MaxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
|
||||
RequireUpdateCheck: cfg.RequireUpdateCheck,
|
||||
ListingFileTimeout: cfg.UpdateAvailableTimeout,
|
||||
UpdateTimeout: cfg.UpdateDownloadTimeout,
|
||||
}
|
||||
|
@ -69,6 +72,7 @@ func (cfg *Database) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
|||
descriptions.Add(&cfg.MaxAllowedBuiltAge, `Max allowed age for vulnerability database,
|
||||
age being the time since it was built
|
||||
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
|
||||
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
|
||||
|
|
|
@ -37,6 +37,7 @@ type Config struct {
|
|||
ValidateByHashOnGet bool
|
||||
ValidateAge bool
|
||||
MaxAllowedBuiltAge time.Duration
|
||||
RequireUpdateCheck bool
|
||||
ListingFileTimeout time.Duration
|
||||
UpdateTimeout time.Duration
|
||||
}
|
||||
|
@ -52,6 +53,7 @@ type Curator struct {
|
|||
validateByHashOnGet bool
|
||||
validateAge bool
|
||||
maxAllowedBuiltAge time.Duration
|
||||
requireUpdateCheck bool
|
||||
}
|
||||
|
||||
func NewCurator(cfg Config) (Curator, error) {
|
||||
|
@ -81,6 +83,7 @@ func NewCurator(cfg Config) (Curator, error) {
|
|||
validateByHashOnGet: cfg.ValidateByHashOnGet,
|
||||
validateAge: cfg.ValidateAge,
|
||||
maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
|
||||
requireUpdateCheck: cfg.RequireUpdateCheck,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -150,7 +153,9 @@ func (c *Curator) Update() (bool, error) {
|
|||
|
||||
updateAvailable, metadata, updateEntry, err := c.IsUpdateAvailable()
|
||||
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.Debugf("check for vulnerability update failed: %+v", err)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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) {
|
||||
failAfter := 10 * time.Second
|
||||
success := make(chan struct{})
|
||||
|
|
Loading…
Reference in a new issue