diff --git a/cmd/grype/cli/commands/listing_test.go b/cmd/grype/cli/commands/listing_test.go new file mode 100644 index 00000000..fa879f3d --- /dev/null +++ b/cmd/grype/cli/commands/listing_test.go @@ -0,0 +1,41 @@ +package commands + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/anchore/clio" +) + +func Test_ListingUserAgent(t *testing.T) { + listingFile := "/listing.json" + + got := "" + + // setup mock + handler := http.NewServeMux() + handler.HandleFunc(listingFile, func(w http.ResponseWriter, r *http.Request) { + got = r.Header.Get("User-Agent") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("1.0.0")) + }) + mockSrv := httptest.NewServer(handler) + defer mockSrv.Close() + + dbOptions := *dbOptionsDefault(clio.Identification{ + Name: "the-app", + Version: "v3.2.1", + }) + dbOptions.DB.RequireUpdateCheck = true + dbOptions.DB.UpdateURL = mockSrv.URL + listingFile + + _ = runDBList(&dbListOptions{ + Output: "", + DBOptions: dbOptions, + }) + + if got != "the-app v3.2.1" { + t.Errorf("expected User-Agent header to match, got: %v", got) + } +} diff --git a/cmd/grype/cli/options/database.go b/cmd/grype/cli/options/database.go index 23e47997..986cfab2 100644 --- a/cmd/grype/cli/options/database.go +++ b/cmd/grype/cli/options/database.go @@ -12,16 +12,17 @@ import ( ) type Database struct { - Dir string `yaml:"cache-dir" json:"cache-dir" mapstructure:"cache-dir"` - UpdateURL string `yaml:"update-url" json:"update-url" mapstructure:"update-url"` - CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` - AutoUpdate bool `yaml:"auto-update" json:"auto-update" mapstructure:"auto-update"` - 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"` + ID clio.Identification `yaml:"-" json:"-" mapstructure:"-"` + Dir string `yaml:"cache-dir" json:"cache-dir" mapstructure:"cache-dir"` + UpdateURL string `yaml:"update-url" json:"update-url" mapstructure:"update-url"` + CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` + AutoUpdate bool `yaml:"auto-update" json:"auto-update" mapstructure:"auto-update"` + 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"` } var _ interface { @@ -36,6 +37,7 @@ const ( func DefaultDatabase(id clio.Identification) Database { return Database{ + ID: id, Dir: path.Join(xdg.CacheHome, id.Name, "db"), UpdateURL: internal.DBUpdateURL, AutoUpdate: true, @@ -50,6 +52,7 @@ func DefaultDatabase(id clio.Identification) Database { func (cfg Database) ToCuratorConfig() db.Config { return db.Config{ + ID: cfg.ID, DBRootDir: cfg.Dir, ListingURL: cfg.UpdateURL, CACert: cfg.CACert, diff --git a/grype/db/curator.go b/grype/db/curator.go index 0c5c1dd0..7af7d341 100644 --- a/grype/db/curator.go +++ b/grype/db/curator.go @@ -17,6 +17,7 @@ import ( partybus "github.com/wagoodman/go-partybus" progress "github.com/wagoodman/go-progress" + "github.com/anchore/clio" grypeDB "github.com/anchore/grype/grype/db/v5" "github.com/anchore/grype/grype/db/v5/store" "github.com/anchore/grype/grype/event" @@ -31,6 +32,7 @@ const ( ) type Config struct { + ID clio.Identification DBRootDir string ListingURL string CACert string @@ -75,8 +77,8 @@ func NewCurator(cfg Config) (Curator, error) { return Curator{ fs: fs, targetSchema: vulnerability.SchemaVersion, - listingDownloader: file.NewGetter(listingClient), - updateDownloader: file.NewGetter(dbClient), + listingDownloader: file.NewGetter(cfg.ID, listingClient), + updateDownloader: file.NewGetter(cfg.ID, dbClient), dbDir: dbDir, dbPath: path.Join(dbDir, FileName), listingURL: cfg.ListingURL, diff --git a/internal/file/getter.go b/internal/file/getter.go index 216a0965..12d765f5 100644 --- a/internal/file/getter.go +++ b/internal/file/getter.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-getter/helper/url" "github.com/wagoodman/go-progress" + "github.com/anchore/clio" "github.com/anchore/grype/internal/stringutil" ) @@ -32,10 +33,13 @@ type HashiGoGetter struct { // NewGetter creates and returns a new Getter. Providing an http.Client is optional. If one is provided, // it will be used for all HTTP(S) getting; otherwise, go-getter's default getters will be used. -func NewGetter(httpClient *http.Client) *HashiGoGetter { +func NewGetter(id clio.Identification, httpClient *http.Client) *HashiGoGetter { return &HashiGoGetter{ httpGetter: getter.HttpGetter{ Client: httpClient, + Header: http.Header{ + "User-Agent": []string{fmt.Sprintf("%v %v", id.Name, id.Version)}, + }, }, } } diff --git a/internal/file/getter_test.go b/internal/file/getter_test.go index f0bdaf90..a8f47ce8 100644 --- a/internal/file/getter_test.go +++ b/internal/file/getter_test.go @@ -14,6 +14,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + + "github.com/anchore/clio" ) func TestGetter_GetFile(t *testing.T) { @@ -45,7 +47,7 @@ func TestGetter_GetFile(t *testing.T) { tc.prepareClient(httpClient) } - getter := NewGetter(httpClient) + getter := NewGetter(testID, httpClient) requestURL := createRequestURL(t, server, requestPath) tempDir := t.TempDir() @@ -72,7 +74,7 @@ func TestGetter_GetToDir_FilterNonArchivesWired(t *testing.T) { for _, test := range testCases { t.Run(test.name, func(t *testing.T) { - test.assert(t, NewGetter(nil).GetToDir(t.TempDir(), test.source)) + test.assert(t, NewGetter(testID, nil).GetToDir(t.TempDir(), test.source)) }) } } @@ -138,7 +140,7 @@ func TestGetter_GetToDir_CertConcerns(t *testing.T) { tc.prepareClient(httpClient) } - getter := NewGetter(httpClient) + getter := NewGetter(testID, httpClient) requestURL := createRequestURL(t, server, requestPath) tempDir := t.TempDir() @@ -193,6 +195,11 @@ func withResponseForPath(t *testing.T, path string, response []byte) muxOption { } } +var testID = clio.Identification{ + Name: "test-app", + Version: "v0.5.3", +} + func newTestServer(t *testing.T, muxOptions ...muxOption) *httptest.Server { t.Helper()