Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/grype/cli/options/datasources.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type maven struct {
SearchUpstreamBySha1 bool `yaml:"search-upstream" json:"searchUpstreamBySha1" mapstructure:"search-maven-upstream"`
BaseURL string `yaml:"base-url" json:"baseUrl" mapstructure:"base-url"`
RateLimit time.Duration `yaml:"rate-limit" json:"rateLimit" mapstructure:"rate-limit"`
Timeout time.Duration `yaml:"timeout" json:"timeout" mapstructure:"timeout"`
}

func defaultExternalSources() externalSources {
Expand All @@ -32,6 +33,7 @@ func defaultExternalSources() externalSources {
SearchUpstreamBySha1: true,
BaseURL: defaultMavenBaseURL,
RateLimit: 300 * time.Millisecond,
Timeout: 10 * time.Second,
},
}
}
Expand All @@ -46,11 +48,13 @@ func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {
SearchMavenUpstream: smu,
MavenBaseURL: cfg.Maven.BaseURL,
MavenRateLimit: cfg.Maven.RateLimit,
MavenTimeout: cfg.Maven.Timeout,
}
}

func (cfg *externalSources) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&cfg.Enable, `enable Grype searching network source for additional information`)
descriptions.Add(&cfg.Maven.SearchUpstreamBySha1, `search for Maven artifacts by SHA1`)
descriptions.Add(&cfg.Maven.BaseURL, `base URL of the Maven repository to search`)
descriptions.Add(&cfg.Maven.Timeout, `per-request timeout for Maven SHA1 lookups; 0 disables the timeout`)
}
8 changes: 7 additions & 1 deletion grype/matcher/java/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type ExternalSearchConfig struct {
SearchMavenUpstream bool
MavenBaseURL string
MavenRateLimit time.Duration
MavenTimeout time.Duration
}

type MatcherConfig struct {
Expand All @@ -36,9 +37,14 @@ type MatcherConfig struct {
}

func NewJavaMatcher(cfg MatcherConfig) *Matcher {
client := http.DefaultClient
if cfg.MavenTimeout > 0 {
// dedicated client so a configured per-request timeout does not bleed onto unrelated callers
client = &http.Client{Timeout: cfg.MavenTimeout}
}
return &Matcher{
cfg: cfg,
MavenSearcher: newMavenSearch(http.DefaultClient, cfg.MavenBaseURL, cfg.MavenRateLimit),
MavenSearcher: newMavenSearch(client, cfg.MavenBaseURL, cfg.MavenRateLimit),
}
}

Expand Down
55 changes: 55 additions & 0 deletions grype/matcher/java/maven_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,58 @@ func withinDelta(got, want, delta time.Duration) bool {
}
return diff <= delta
}

func TestNewJavaMatcherTimeout(t *testing.T) {
// stand up a server that hangs forever so any successful response would only happen
// because the configured timeout failed to abort the request. The hang channel must be
// closed before ts.Close() so any in-flight handlers unblock cleanly during shutdown.
hang := make(chan struct{})
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
<-hang
}))
t.Cleanup(func() {
close(hang)
ts.Close()
})

t.Run("timeout aborts hanging upstream", func(t *testing.T) {
m := NewJavaMatcher(MatcherConfig{
ExternalSearchConfig: ExternalSearchConfig{
MavenBaseURL: ts.URL,
MavenRateLimit: time.Nanosecond,
MavenTimeout: 100 * time.Millisecond,
},
})

start := time.Now()
_, err := m.GetMavenPackageBySha(context.Background(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
elapsed := time.Since(start)

if err == nil {
t.Fatal("expected timeout error, got nil")
}
// give the runtime a generous ceiling so this test stays stable on slow CI
if elapsed > 2*time.Second {
t.Errorf("timeout did not fire promptly: elapsed = %v", elapsed)
}
})

t.Run("zero timeout disables the per-request limit", func(t *testing.T) {
m := NewJavaMatcher(MatcherConfig{
ExternalSearchConfig: ExternalSearchConfig{
MavenBaseURL: ts.URL,
// MavenTimeout intentionally unset (zero value)
},
})

// the underlying Matcher's MavenSearcher should be using http.DefaultClient,
// which has no Timeout set
ms, ok := m.MavenSearcher.(*mavenSearch)
if !ok {
t.Fatalf("MavenSearcher is not *mavenSearch, got %T", m.MavenSearcher)
}
if ms.client.Timeout != 0 {
t.Errorf("expected zero timeout on default client, got %v", ms.client.Timeout)
}
})
}