Skip to content

Commit

Permalink
fix: fail when grype cant check for db update (#1247)
Browse files Browse the repository at this point in the history
Signed-off-by: Shane Dell <[email protected]>
Signed-off-by: Keith Zantow <[email protected]>
Co-authored-by: Keith Zantow <[email protected]>
  • Loading branch information
shanedell and kzantow committed Aug 15, 2024
1 parent b26f3e2 commit d21c549
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 2 deletions.
5 changes: 4 additions & 1 deletion cmd/grype/cli/commands/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/grype/cli/options/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand All @@ -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,
}
Expand All @@ -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,
}
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion grype/db/curator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type Config struct {
ValidateByHashOnGet bool
ValidateAge bool
MaxAllowedBuiltAge time.Duration
RequireUpdateCheck bool
ListingFileTimeout time.Duration
UpdateTimeout time.Duration
}
Expand All @@ -52,6 +53,7 @@ type Curator struct {
validateByHashOnGet bool
validateAge bool
maxAllowedBuiltAge time.Duration
requireUpdateCheck bool
}

func NewCurator(cfg Config) (Curator, error) {
Expand Down Expand Up @@ -81,6 +83,7 @@ func NewCurator(cfg Config) (Curator, error) {
validateByHashOnGet: cfg.ValidateByHashOnGet,
validateAge: cfg.ValidateAge,
maxAllowedBuiltAge: cfg.MaxAllowedBuiltAge,
requireUpdateCheck: cfg.RequireUpdateCheck,
}, nil
}

Expand Down Expand Up @@ -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)
}
Expand Down
171 changes: 171 additions & 0 deletions grype/db/curator_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package db

import (
"archive/tar"
"bufio"
"bytes"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -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{})
Expand Down

0 comments on commit d21c549

Please sign in to comment.