Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fail when grype cant check for db update #1247

Merged
merged 7 commits into from
Aug 15, 2024
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
2 changes: 1 addition & 1 deletion grype/match/matches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package match
import (
"testing"

"github.com/anchore/syft/syft/file"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/vulnerability"
"github.com/anchore/syft/syft/file"
syftPkg "github.com/anchore/syft/syft/pkg"
)

Expand Down
Loading