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

Allow configuring timeout for external sources #1812

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4cbb4f0
adding abort-after property to external-sources
pouyan021 Apr 18, 2024
b8fe080
considering the timeout in java matcher
pouyan021 Apr 18, 2024
49ae4c9
make the maven search method respect the context time out
pouyan021 Apr 18, 2024
7043535
adding a number of additional tests for the external-sources time-out…
pouyan021 Apr 18, 2024
db426d2
facilitating java matcher mocks to imitate the external-sources abort…
pouyan021 Apr 18, 2024
1e947b6
updating readme to reflect the new changes in external-sources
pouyan021 Apr 18, 2024
0bf64dd
adjust multi level configuration + faster tests
wagoodman May 16, 2024
2c79d3a
Merge branch 'main' into feat/allow-configuring-timeout-for-external-…
pouyan021 Jun 12, 2024
3d85ae1
chore(deps): bump anchore/sbom-action from 0.15.11 to 0.16.0 (#1871)
dependabot[bot] May 20, 2024
af3ff15
chore(deps): bump github/codeql-action from 2.13.4 to 3.25.6 (#1870)
dependabot[bot] May 20, 2024
c50a72a
chore(deps): update tools to latest versions (#1864)
anchore-actions-token-generator[bot] May 20, 2024
09422ac
chore(deps): bump actions/checkout from 4.1.5 to 4.1.6 (#1868)
dependabot[bot] May 20, 2024
75353d7
chore(deps): bump github.com/docker/docker (#1867)
dependabot[bot] May 20, 2024
3cdc811
disable TUI for simpler commands (#1872)
wagoodman May 21, 2024
84d7d80
feat: add config command (#1876)
kzantow May 23, 2024
991513f
chore(deps): update tools to latest versions (#1883)
anchore-actions-token-generator[bot] May 24, 2024
20ac044
chore(deps): bump github.com/gabriel-vasile/mimetype from 1.4.3 to 1.…
dependabot[bot] May 24, 2024
5e77a81
chore(deps): bump github.com/charmbracelet/bubbletea (#1890)
dependabot[bot] May 28, 2024
48fc783
chore(deps): update tools to latest versions (#1891)
anchore-actions-token-generator[bot] May 28, 2024
ff84a81
chore(deps): bump github.com/hashicorp/go-version from 1.6.0 to 1.7.0…
dependabot[bot] May 28, 2024
6578ae8
chore(deps): bump github.com/charmbracelet/lipgloss (#1888)
dependabot[bot] May 28, 2024
ecee300
Update syft to 1.4.2-0.20240528141306-ac34808b9c55 (#1895)
wagoodman May 28, 2024
5f6ff53
chore(deps): bump docker/login-action from 3.1.0 to 3.2.0 (#1896)
dependabot[bot] May 28, 2024
b3d1bcf
update syft to v1.5.0 (#1897)
wagoodman May 28, 2024
20f16c4
chore(deps): update tools to latest versions (#1898)
anchore-actions-token-generator[bot] May 30, 2024
0a2fdb4
fix: main mod pseudo version default off (#1894)
luhring May 30, 2024
f043899
fix: uppercased package in json (#1900)
kzantow May 30, 2024
c9ec282
fix: add note about TMPDIR env var (#1880)
avtar May 31, 2024
e5002a8
chore(deps): bump github.com/charmbracelet/bubbletea (#1902)
dependabot[bot] May 31, 2024
2b08d92
chore(deps): bump github/codeql-action from 3.25.6 to 3.25.7 (#1901)
dependabot[bot] May 31, 2024
5d1324f
use dco tool during gh app outage (#1910)
wagoodman Jun 4, 2024
22ec108
remove dco workflow (#1914)
wagoodman Jun 6, 2024
25c0b28
chore(deps): bump github.com/docker/docker (#1916)
dependabot[bot] Jun 6, 2024
ea1324c
chore(deps): bump github/codeql-action from 3.25.7 to 3.25.8 (#1909)
dependabot[bot] Jun 6, 2024
92a889f
add skopeo to managed utilities (#1915)
wagoodman Jun 6, 2024
237d79f
feat(signature): Checksum signature verification (#1670)
hibare Jun 6, 2024
9decc9b
chore(deps): bump actions/checkout from 4.1.1 to 4.1.6 (#1920)
dependabot[bot] Jun 7, 2024
417c9ea
chore(deps): update tools to latest versions (#1919)
anchore-actions-token-generator[bot] Jun 7, 2024
e561e7e
chore(deps): update tools to latest versions (#1921)
anchore-actions-token-generator[bot] Jun 10, 2024
3433baf
chore(deps): update tools to latest versions (#1925)
anchore-actions-token-generator[bot] Jun 11, 2024
8d9e74e
sort order for matches should consider fix info (#1933)
wagoodman Jun 12, 2024
ccd8c3e
Updating maven URLs in README.md (#1934)
JoshuaCooper Jun 12, 2024
75a337c
Merge remote-tracking branch 'origin/feat/allow-configuring-timeout-f…
pouyan021 Sep 17, 2024
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,9 +277,12 @@ feature is currently disabled by default. To enable this feature add the followi
```yaml
external-sources:
enable: true
abort-after: 10m
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can clarify what this means by changing the name some. This could be interpreted as either:
a. aborting looking up from external sources in general after the duration elapses
b. aborting a single request to an external source after the duration elapses

From the functionality implemented b is implied.

Regarding naming and the above context, request-timeout feels like a more descriptive name.

maven:
search-upstream-by-sha1: true
wagoodman marked this conversation as resolved.
Show resolved Hide resolved
base-url: https://repo1.maven.org/maven2
abort-after: 5m #override the global config
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @wagoodman - I know he's pretty sensitive to duplicate fields that override each other so I'd like him to chime in on where he sees this going or what his preference would be


```

You can also configure the base-url if you're using another registry as your maven endpoint.
Expand Down
38 changes: 30 additions & 8 deletions cmd/grype/cli/options/datasources.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,31 @@
package options

import (
"github.com/anchore/clio"

Check failure on line 4 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Unit tests

other declaration of clio

Check failure on line 4 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Integration tests

other declaration of clio

Check failure on line 4 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Quality tests

other declaration of clio
"time"
"github.com/anchore/clio"

Check failure on line 6 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Unit tests

clio redeclared in this block

Check failure on line 6 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Unit tests

"github.com/anchore/clio" imported and not used

Check failure on line 6 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Integration tests

clio redeclared in this block

Check failure on line 6 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Integration tests

"github.com/anchore/clio" imported and not used

Check failure on line 6 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Quality tests

clio redeclared in this block

Check failure on line 6 in cmd/grype/cli/options/datasources.go

View workflow job for this annotation

GitHub Actions / Quality tests

"github.com/anchore/clio" imported and not used
"github.com/anchore/grype/grype/matcher/java"
)

const (
defaultMavenBaseURL = "https://search.maven.org/solrsearch/select"
defaultAbortAfter = 10 * time.Minute
)

type externalSources struct {
Enable bool `yaml:"enable" json:"enable" mapstructure:"enable"`
Maven maven `yaml:"maven" json:"maven" mapstructure:"maven"`
Enable bool `yaml:"enable" json:"enable" mapstructure:"enable"`
AbortAfter *time.Duration `yaml:"abort-after" json:"abortAfter" mapstructure:"abort-after"`
Maven maven `yaml:"maven" json:"maven" mapstructure:"maven"`
}

var _ interface {
clio.FieldDescriber
} = (*externalSources)(nil)

type maven struct {
SearchUpstreamBySha1 bool `yaml:"search-upstream" json:"searchUpstreamBySha1" mapstructure:"search-maven-upstream"`
BaseURL string `yaml:"base-url" json:"baseUrl" mapstructure:"base-url"`
SearchUpstreamBySha1 bool `yaml:"search-upstream" json:"searchUpstreamBySha1" mapstructure:"search-maven-upstream"`
BaseURL string `yaml:"base-url" json:"baseUrl" mapstructure:"base-url"`
AbortAfter *time.Duration `yaml:"abort-after" json:"abortAfter" mapstructure:"abort-after"`
}

func defaultExternalSources() externalSources {
Expand All @@ -32,16 +37,33 @@
}
}

func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {
func (cfg *externalSources) PostLoad() error {
// always respect if global config is disabled
smu := cfg.Maven.SearchUpstreamBySha1
if !cfg.Enable {
smu = cfg.Enable
cfg.Maven.SearchUpstreamBySha1 = false
}

cfg.Maven.AbortAfter = multiLevelOption[time.Duration](defaultAbortAfter, cfg.AbortAfter, cfg.Maven.AbortAfter)

return nil
}

func (cfg externalSources) ToJavaMatcherConfig() java.ExternalSearchConfig {
return java.ExternalSearchConfig{
SearchMavenUpstream: smu,
SearchMavenUpstream: cfg.Maven.SearchUpstreamBySha1,
MavenBaseURL: cfg.Maven.BaseURL,
AbortAfter: *cfg.Maven.AbortAfter,
}
}

func multiLevelOption[T any](defaultValue T, option ...*T) *T {
result := defaultValue
for _, opt := range option {
if opt != nil {
result = *opt
}
}
return &result
}

func (cfg *externalSources) DescribeFields(descriptions clio.FieldDescriptionSet) {
Expand Down
17 changes: 14 additions & 3 deletions grype/matcher/java/matcher.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package java

import (
"context"
"fmt"
"net/http"
"time"

"github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/match"
Expand All @@ -25,6 +27,7 @@ type Matcher struct {
type ExternalSearchConfig struct {
SearchMavenUpstream bool
MavenBaseURL string
AbortAfter time.Duration
}

type MatcherConfig struct {
Expand Down Expand Up @@ -53,7 +56,15 @@ func (m *Matcher) Type() match.MatcherType {
func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
var matches []match.Match
if m.cfg.SearchMavenUpstream {
upstreamMatches, err := m.matchUpstreamMavenPackages(store, d, p)
timeout := m.cfg.AbortAfter
ctx := context.Background()
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, m.cfg.AbortAfter)
defer cancel()
}

upstreamMatches, err := m.matchUpstreamMavenPackages(ctx, store, d, p)
if err != nil {
log.Debugf("failed to match against upstream data for %s: %v", p.Name, err)
} else {
Expand All @@ -73,13 +84,13 @@ func (m *Matcher) Match(store vulnerability.Provider, d *distro.Distro, p pkg.Pa
return matches, nil
}

func (m *Matcher) matchUpstreamMavenPackages(store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
func (m *Matcher) matchUpstreamMavenPackages(ctx context.Context, store vulnerability.Provider, d *distro.Distro, p pkg.Package) ([]match.Match, error) {
var matches []match.Match

if metadata, ok := p.Metadata.(pkg.JavaMetadata); ok {
for _, digest := range metadata.ArchiveDigests {
if digest.Algorithm == "sha1" {
indirectPackage, err := m.GetMavenPackageBySha(digest.Value)
indirectPackage, err := m.GetMavenPackageBySha(ctx, digest.Value)
if err != nil {
return nil, err
}
Expand Down
42 changes: 36 additions & 6 deletions grype/matcher/java/matcher_mocks_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package java

import (
"context"
"testing"
"time"

"github.com/anchore/grype/grype/distro"
"github.com/anchore/grype/grype/pkg"
"github.com/anchore/grype/grype/version"
Expand Down Expand Up @@ -49,16 +53,42 @@ func newMockProvider() *mockProvider {
}

type mockMavenSearcher struct {
pkg pkg.Package
tb testing.TB
pkg *pkg.Package
work *time.Duration
}

func (m mockMavenSearcher) GetMavenPackageBySha(string) (*pkg.Package, error) {
return &m.pkg, nil
func newMockSearcher(tb testing.TB) mockMavenSearcher {
return mockMavenSearcher{
tb: tb,
}
}

func newMockSearcher(pkg pkg.Package) MavenSearcher {
return mockMavenSearcher{
pkg,
func (m mockMavenSearcher) WithPackage(p pkg.Package) mockMavenSearcher {
m.pkg = &p
return m
}

func (m mockMavenSearcher) WithWorkDuration(duration time.Duration) mockMavenSearcher {
m.work = &duration
return m
}

func (m mockMavenSearcher) GetMavenPackageBySha(ctx context.Context, sha1 string) (*pkg.Package, error) {
deadline, ok := ctx.Deadline()

m.tb.Log("GetMavenPackageBySha called with deadline:", deadline, "deadline set:", ok)

if m.work != nil {
select {
case <-time.After(*m.work):
return m.pkg, nil
case <-ctx.Done():
// If the context is done before the sleep is over, return a context.DeadlineExceeded error
return m.pkg, ctx.Err()
}
} else {
return m.pkg, ctx.Err()
}
}

Expand Down
43 changes: 41 additions & 2 deletions grype/matcher/java/matcher_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package java

import (
"context"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -36,10 +38,12 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
},
UseCPEs: false,
},
MavenSearcher: newMockSearcher(p),
// no duration will return immediately with the mock data
MavenSearcher: newMockSearcher(t).WithPackage(p),
}
store := newMockProvider()
actual, _ := matcher.matchUpstreamMavenPackages(store, nil, p)
ctx := context.Background()
actual, _ := matcher.matchUpstreamMavenPackages(ctx, store, nil, p)

assert.Len(t, actual, 2, "unexpected matches count")

Expand All @@ -64,3 +68,38 @@ func TestMatcherJava_matchUpstreamMavenPackage(t *testing.T) {
t.Logf("discovered CVES: %+v", foundCVEs)
}
}
func TestMatcherJava_TestMatchUpstreamMavenPackagesTimeout(t *testing.T) {
p := pkg.Package{
ID: pkg.ID(uuid.NewString()),
Name: "org.springframework.spring-webmvc",
Version: "5.1.5.RELEASE",
Language: syftPkg.Java,
Type: syftPkg.JavaPkg,
Metadata: pkg.JavaMetadata{
ArchiveDigests: []pkg.Digest{
{
Algorithm: "sha1",
Value: "236e3bfdbdc6c86629237a74f0f11414adb4e211",
},
},
},
}
matcher := Matcher{
cfg: MatcherConfig{
ExternalSearchConfig: ExternalSearchConfig{
SearchMavenUpstream: true,
},
UseCPEs: false,
},
MavenSearcher: newMockSearcher(t).WithPackage(p).WithWorkDuration(10 * time.Second),
}
store := newMockProvider()

// Create a context with a very short timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()

_, err := matcher.matchUpstreamMavenPackages(ctx, store, nil, p)

require.ErrorIs(t, err, context.DeadlineExceeded)
}
106 changes: 64 additions & 42 deletions grype/matcher/java/maven_search.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package java

import (
"context"
"encoding/json"
"errors"
"fmt"
Expand All @@ -14,7 +15,7 @@ import (
// MavenSearcher is the interface that wraps the GetMavenPackageBySha method.
type MavenSearcher interface {
// GetMavenPackageBySha provides an interface for building a package from maven data based on a sha1 digest
GetMavenPackageBySha(string) (*pkg.Package, error)
GetMavenPackageBySha(context.Context, string) (*pkg.Package, error)
}

// mavenSearch implements the MavenSearcher interface
Expand All @@ -37,52 +38,73 @@ type mavenAPIResponse struct {
} `json:"response"`
}

func (ms *mavenSearch) GetMavenPackageBySha(sha1 string) (*pkg.Package, error) {
req, err := http.NewRequest(http.MethodGet, ms.baseURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to initialize HTTP client: %w", err)
}
func (ms *mavenSearch) GetMavenPackageBySha(context context.Context, sha1 string) (*pkg.Package, error) {
resultChan := make(chan *pkg.Package, 1)
errChan := make(chan error, 1)
go func() {
req, err := http.NewRequest(http.MethodGet, ms.baseURL, nil)
if err != nil {
errChan <- fmt.Errorf("unable to initialize HTTP client: %w", err)
return
}

q := req.URL.Query()
q.Set("q", fmt.Sprintf(sha1Query, sha1))
q.Set("rows", "1")
q.Set("wt", "json")
req.URL.RawQuery = q.Encode()
q := req.URL.Query()
q.Set("q", fmt.Sprintf(sha1Query, sha1))
q.Set("rows", "1")
q.Set("wt", "json")
req.URL.RawQuery = q.Encode()

resp, err := ms.client.Do(req)
if err != nil {
return nil, fmt.Errorf("sha1 search error: %w", err)
}
defer resp.Body.Close()
resp, err := ms.client.Do(req)
if err != nil {
errChan <- fmt.Errorf("sha1 search error: %w", err)
return
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %s from %s", resp.Status, req.URL.String())
}
if resp.StatusCode != http.StatusOK {
errChan <- fmt.Errorf("status %s from %s", resp.Status, req.URL.String())
return
}

var res mavenAPIResponse
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
return nil, fmt.Errorf("json decode error: %w", err)
}
var res mavenAPIResponse
if err = json.NewDecoder(resp.Body).Decode(&res); err != nil {
errChan <- fmt.Errorf("json decode error: %w", err)
return
}

if len(res.Response.Docs) == 0 {
return nil, fmt.Errorf("digest %s: %w", sha1, errors.New("no artifact found"))
}
if len(res.Response.Docs) == 0 {
errChan <- fmt.Errorf("digest %s: %w", sha1, errors.New("no artifact found"))
return
}

// artifacts might have the same SHA-1 digests.
// e.g. "javax.servlet:jstl" and "jstl:jstl"
docs := res.Response.Docs
sort.Slice(docs, func(i, j int) bool {
return docs[i].ID < docs[j].ID
})
d := docs[0]

// artifacts might have the same SHA-1 digests.
// e.g. "javax.servlet:jstl" and "jstl:jstl"
docs := res.Response.Docs
sort.Slice(docs, func(i, j int) bool {
return docs[i].ID < docs[j].ID
})
d := docs[0]
resultChan <- &pkg.Package{
Name: fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID),
Version: d.Version,
Language: syftPkg.Java,
Metadata: pkg.JavaMetadata{
PomArtifactID: d.ArtifactID,
PomGroupID: d.GroupID,
},
}
}()

return &pkg.Package{
Name: fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID),
Version: d.Version,
Language: syftPkg.Java,
Metadata: pkg.JavaMetadata{
PomArtifactID: d.ArtifactID,
PomGroupID: d.GroupID,
},
}, nil
select {
case <-context.Done():
// The context was canceled or its deadline was exceeded
return nil, context.Err()
case res := <-resultChan:
// The work finished before the context was done
return res, nil
case err := <-errChan:
// There was an error getting the package
return nil, err
}
}
Loading