diff --git a/go.mod b/go.mod index 1782c1a4de5..3e21c186d56 100644 --- a/go.mod +++ b/go.mod @@ -67,6 +67,7 @@ require ( require ( github.com/invopop/jsonschema v0.13.0 + golang.org/x/time v0.8.0 golang.org/x/tools v0.29.0 ) @@ -278,7 +279,6 @@ require ( golang.org/x/sys v0.29.0 // indirect golang.org/x/term v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.8.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.215.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect diff --git a/grype/db/v5/matcher/java/matcher.go b/grype/db/v5/matcher/java/matcher.go index 3d8dbba7b2a..d84e3cbb820 100644 --- a/grype/db/v5/matcher/java/matcher.go +++ b/grype/db/v5/matcher/java/matcher.go @@ -57,7 +57,7 @@ func (m *Matcher) Match(store v5.VulnerabilityProvider, d *distro.Distro, p pkg. if strings.Contains(err.Error(), "no artifact found") { log.Debugf("no upstream maven artifact found for %s", p.Name) } - log.Errorf("failed to match against upstream data for %s: %v", p.Name, err) + log.WithFields("package", p.Name, "error", err).Warn("failed to resolve package details with maven") } else { matches = append(matches, upstreamMatches...) } diff --git a/grype/db/v5/matcher/java/matcher_integration_test.go b/grype/db/v5/matcher/java/matcher_integration_test.go index 15859d53b02..444faae16c3 100644 --- a/grype/db/v5/matcher/java/matcher_integration_test.go +++ b/grype/db/v5/matcher/java/matcher_integration_test.go @@ -1,5 +1,5 @@ -//go:build integration -// +build integration +//go:build api_limits +// +build api_limits package java diff --git a/grype/db/v5/matcher/java/maven_test.go b/grype/db/v5/matcher/java/maven_test.go new file mode 100644 index 00000000000..a57ec25651e --- /dev/null +++ b/grype/db/v5/matcher/java/maven_test.go @@ -0,0 +1,79 @@ +package java + +import ( + "context" + "golang.org/x/time/rate" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestNewMavenSearchRateLimiter(t *testing.T) { + // Create a test server + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // We don't need to respond with anything for this test + })) + defer ts.Close() + + t.Run("default initialization", func(t *testing.T) { + ms := newMavenSearch(http.DefaultClient, ts.URL) + + if ms.client == nil { + t.Error("HTTP client was not initialized") + } + + if ms.baseURL != ts.URL { + t.Errorf("unexpected base URL: got %q, want %q", ms.baseURL, ts.URL) + } + + if ms.rateLimiter == nil { + t.Error("rate limiter was not initialized") + } + }) + + t.Run("rate limiter configuration", func(t *testing.T) { + ms := newMavenSearch(http.DefaultClient, ts.URL) + + expectedRate := rate.Every(300 * time.Millisecond) + if ms.rateLimiter.Limit() != expectedRate { + t.Errorf("unexpected rate limit: got %v, want %v", ms.rateLimiter.Limit(), rate.Limit(expectedRate)) + } + + if ms.rateLimiter.Burst() != 1 { + t.Errorf("unexpected burst limit: got %d, want 1", ms.rateLimiter.Burst()) + } + }) + + t.Run("rate limiter behavior", func(t *testing.T) { + ms := newMavenSearch(http.DefaultClient, ts.URL) + ctx := context.Background() + + // First request should proceed immediately + start := time.Now() + err := ms.rateLimiter.Wait(ctx) + if err != nil { + t.Errorf("unexpected error on first wait: %v", err) + } + if elapsed := time.Since(start); elapsed > 50*time.Millisecond { + t.Errorf("first request took too long: %v", elapsed) + } + + // Second request should be delayed by ~300ms + start = time.Now() + err = ms.rateLimiter.Wait(ctx) + if err != nil { + t.Errorf("unexpected error on second wait: %v", err) + } + if elapsed := time.Since(start); elapsed < 250*time.Millisecond { + t.Errorf("rate limiting not enforced, second request took: %v", elapsed) + } + }) + + t.Run("nil client", func(t *testing.T) { + ms := newMavenSearch(nil, ts.URL) + if ms.rateLimiter == nil { + t.Error("rate limiter was not initialized with nil client") + } + }) +}