Skip to content

Commit b497bdb

Browse files
feat: Implement ecosystem enumeration in Go (#5140)
Translated all the ecosystem queries for version enumeration into go - Bioconductor seems to be broken (the newer releases 500 on for the requests). We have 0 Bioconductor vulns, so I just disabled enumeration for them (but left the code in for reference) - Removed the Alpine enumeration, because querying the aports repo is quite heavy - opam is still Implemented and disabled, as it is in python - Most of the ecosystems are uninteresting - they're mostly just parsing versions out of JSON (though please look at them!). Debian does the most work in its thing - Added go-vcr so the tests don't depend on network requests. We'd want to have something regenerate these once in a while/test against live to make sure nothing starts breaking. --------- Co-authored-by: Rex P <106129829+another-rex@users.noreply.github.com>
1 parent 64186c4 commit b497bdb

58 files changed

Lines changed: 4142 additions & 109 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

go/go.mod

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,10 @@ require (
2828
google.golang.org/api v0.273.1
2929
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9
3030
google.golang.org/protobuf v1.36.11
31+
gopkg.in/dnaeon/go-vcr.v4 v4.0.6
3132
k8s.io/apimachinery v0.35.3
3233
)
3334

34-
require (
35-
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
36-
github.com/charmbracelet/x/termios v0.1.1 // indirect
37-
github.com/charmbracelet/x/windows v0.2.2 // indirect
38-
github.com/clipperhouse/displaywidth v0.11.0 // indirect
39-
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
40-
github.com/muesli/cancelreader v0.2.2 // indirect
41-
)
42-
4335
require (
4436
cel.dev/expr v0.25.1 // indirect
4537
cloud.google.com/go v0.123.0 // indirect
@@ -55,8 +47,13 @@ require (
5547
github.com/ProtonMail/go-crypto v1.4.1 // indirect
5648
github.com/cespare/xxhash/v2 v2.3.0 // indirect
5749
github.com/charmbracelet/colorprofile v0.4.2 // indirect
50+
github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect
5851
github.com/charmbracelet/x/ansi v0.11.6 // indirect
5952
github.com/charmbracelet/x/term v0.2.2 // indirect
53+
github.com/charmbracelet/x/termios v0.1.1 // indirect
54+
github.com/charmbracelet/x/windows v0.2.2 // indirect
55+
github.com/clipperhouse/displaywidth v0.11.0 // indirect
56+
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
6057
github.com/cloudflare/circl v1.6.3 // indirect
6158
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
6259
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
@@ -79,14 +76,15 @@ require (
7976
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
8077
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
8178
github.com/mattn/go-runewidth v0.0.19 // indirect
79+
github.com/muesli/cancelreader v0.2.2 // indirect
8280
github.com/pjbgf/sha1cd v0.5.0 // indirect
8381
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
8482
github.com/rivo/uniseg v0.4.7 // indirect
8583
github.com/sergi/go-diff v1.4.0 // indirect
86-
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect; indirecta
84+
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
8785
github.com/tidwall/match v1.1.1 // indirect
8886
github.com/tidwall/pretty v1.2.0 // indirect
89-
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
87+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
9088
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
9189
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
9290
go.opencensus.io v0.24.0 // indirect
@@ -95,8 +93,8 @@ require (
9593
go.opentelemetry.io/otel/metric v1.43.0 // indirect
9694
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
9795
go.yaml.in/yaml/v2 v2.4.3 // indirect
96+
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
9897
golang.org/x/crypto v0.49.0 // indirect
99-
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
10098
golang.org/x/net v0.52.0 // indirect
10199
golang.org/x/oauth2 v0.36.0 // indirect
102100
golang.org/x/sys v0.42.0 // indirect

go/go.sum

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
218218
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
219219
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
220220
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
221+
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
221222
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
222-
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
223-
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
224223
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
225224
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
226225
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
@@ -257,13 +256,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
257256
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
258257
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
259258
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
259+
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
260+
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
260261
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
261262
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
262263
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
263264
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
264265
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
265-
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
266-
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
266+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
267+
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
267268
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
268269
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
269270
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -340,6 +341,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
340341
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
341342
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
342343
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
344+
gopkg.in/dnaeon/go-vcr.v4 v4.0.6 h1:PiJkrakkmzc5s7EfBnZOnyiLwi7o7A9fwPzN0X2uwe0=
345+
gopkg.in/dnaeon/go-vcr.v4 v4.0.6/go.mod h1:sbq5oMEcM4PXngbcNbHhzfCP9OdZodLhrbRYoyg09HY=
343346
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
344347
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
345348
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

go/osv/ecosystem/bioconductor.go

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,72 @@
1414

1515
package ecosystem
1616

17+
import (
18+
"errors"
19+
"fmt"
20+
"net/url"
21+
)
22+
1723
type bioconductorEcosystem struct {
1824
semverLikeEcosystem
25+
26+
p *Provider
27+
}
28+
29+
var _ Ecosystem = bioconductorEcosystem{}
30+
31+
// FIXME(michaelkedar): Newer releases (3.22+) of bioconductor seem to be returning 500s for package queries.
32+
// 500 seems to be the response when the bioc_version is invalid (i.e. it's also 500 if bioc_version is e.g. 12.3).
33+
// I am guessing the API has changed or is broken for newer bioc versions.
34+
//
35+
// OSV.dev currently has zero Bioconductor packages, so I'm not going to spend time debugging this.
36+
// Removing the Enumerable interface for now (but keeping the code for reference).
37+
// var _ Enumerable = bioconductorEcosystem{}
38+
39+
func apiPackageURLPositBioconductor(pkg, biocVersion string) string {
40+
// Use the Posit Public Package Manager API to pull both the current and
41+
// older versions for a specific package since Bioconductor doesn't natively
42+
// support this functionality.
43+
return fmt.Sprintf("https://packagemanager.posit.co/__api__/repos/4/packages/%s?bioc_version=%s",
44+
url.PathEscape(pkg),
45+
url.QueryEscape(biocVersion),
46+
)
47+
}
48+
49+
const apiBiocVersionsURL = "https://packagemanager.posit.co/__api__/status"
50+
51+
func (e bioconductorEcosystem) getVersions(pkg string) ([]string, error) {
52+
biocVersions, err := e.getBiocVersions()
53+
if err != nil {
54+
return nil, err
55+
}
56+
var versions []string
57+
for _, biocVersion := range biocVersions {
58+
res, err := e.p.fetchJSONPaths(apiPackageURLPositBioconductor(pkg, biocVersion), "version")
59+
if err != nil {
60+
if errors.Is(err, ErrPackageNotFound) {
61+
continue
62+
}
63+
64+
return nil, fmt.Errorf("failed to get Bioconductor versions for %s: %w", pkg, err)
65+
}
66+
if len(res) > 0 && res[0] != "" {
67+
versions = append(versions, res[0])
68+
}
69+
}
70+
71+
if len(versions) == 0 {
72+
return nil, ErrPackageNotFound
73+
}
74+
75+
return sortVersions(e, versions)
1976
}
2077

21-
var _ Enumerable = bioconductorEcosystem{}
78+
func (e bioconductorEcosystem) getBiocVersions() ([]string, error) {
79+
versions, err := e.p.fetchJSONPaths(apiBiocVersionsURL, "bioc_versions.#.bioc_version")
80+
if err != nil {
81+
return nil, fmt.Errorf("failed to get Bioconductor versions: %w", err)
82+
}
2283

23-
func (e bioconductorEcosystem) GetVersions(_ string) ([]string, error) {
24-
panic("not yet implemented")
84+
return versions, nil
2585
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package ecosystem
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
)
9+
10+
// TODO(michaelkedar): See bioconductor.go for why these are skipped.
11+
12+
func TestBioconductor_GetBiocVersions(t *testing.T) {
13+
t.SkipNow()
14+
p := getTestProvider(t)
15+
versions, err := bioconductorEcosystem{p: p}.getBiocVersions()
16+
if err != nil {
17+
t.Errorf("getBiocVersions() error = %v", err)
18+
return
19+
}
20+
if len(versions) == 0 {
21+
t.Errorf("getBiocVersions() returned no versions")
22+
return
23+
}
24+
expectedVersions := []string{"3.23", "3.22", "3.21", "3.20", "3.19", "3.18", "3.17", "3.16", "3.15", "3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "3.8", "3.7", "3.6", "3.5", "3.4", "3.3", "3.2", "3.1"}
25+
if diff := cmp.Diff(expectedVersions, versions); diff != "" {
26+
t.Errorf("getBiocVersions() diff: %s", diff)
27+
}
28+
}
29+
30+
func TestBioconductor_GetVersions(t *testing.T) {
31+
t.SkipNow()
32+
p := getTestProvider(t)
33+
versions, err := bioconductorEcosystem{p: p}.getVersions("a4") // TODO(michaelkedar): getVersions -> GetVersions
34+
if err != nil {
35+
t.Errorf("GetVersions() error = %v", err)
36+
return
37+
}
38+
if len(versions) == 0 {
39+
t.Errorf("GetVersions() returned no versions")
40+
return
41+
}
42+
expectedVersions := []string{} // ???
43+
if diff := cmp.Diff(expectedVersions, versions); diff != "" {
44+
t.Errorf("GetVersions() diff: %s", diff)
45+
}
46+
}
47+
48+
func TestBioconductor_GetVersionsNotFound(t *testing.T) {
49+
t.SkipNow()
50+
p := getTestProvider(t)
51+
_, err := bioconductorEcosystem{p: p}.getVersions("doesnotexist123456") // TODO(michaelkedar): getVersions -> GetVersions
52+
if !errors.Is(err, ErrPackageNotFound) {
53+
t.Errorf("GetVersions() error = %v, want %v", err, ErrPackageNotFound)
54+
}
55+
}

go/osv/ecosystem/cran.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,27 @@
1414

1515
package ecosystem
1616

17-
import "github.com/google/osv-scalibr/semantic"
17+
import (
18+
"fmt"
19+
"net/url"
1820

19-
type cranEcosystem struct{}
21+
"github.com/google/osv-scalibr/semantic"
22+
)
23+
24+
type cranEcosystem struct {
25+
p *Provider
26+
}
2027

2128
var _ Enumerable = cranEcosystem{}
2229

30+
func cranAPIURL(pkg string) string {
31+
// Use the Posit Public Package Manager API to pull both the current
32+
// and archived versions for a specific package since CRAN doesn't
33+
// natively support this functionality.
34+
path, _ := url.JoinPath("https://packagemanager.posit.co/__api__/repos/2/packages/", url.PathEscape(pkg))
35+
return path
36+
}
37+
2338
func (e cranEcosystem) Parse(version string) (Version, error) {
2439
ver, err := semantic.ParseCRANVersion(version)
2540
if err != nil {
@@ -37,6 +52,11 @@ func (e cranEcosystem) IsSemver() bool {
3752
return false
3853
}
3954

40-
func (e cranEcosystem) GetVersions(_ string) ([]string, error) {
41-
panic("not yet implemented")
55+
func (e cranEcosystem) GetVersions(pkg string) ([]string, error) {
56+
versions, err := e.p.fetchJSONPaths(cranAPIURL(pkg), "version", "archived.#.version")
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to get CRAN versions for %s: %w", pkg, err)
59+
}
60+
61+
return sortVersions(e, versions)
4262
}

go/osv/ecosystem/cran_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package ecosystem
2+
3+
import (
4+
"errors"
5+
"testing"
6+
)
7+
8+
func TestCRAN_GetVersions(t *testing.T) {
9+
t.Parallel()
10+
p := getTestProvider(t)
11+
e, ok := p.Get("CRAN")
12+
if !ok {
13+
t.Fatalf("Failed to retrieve CRAN ecosystem")
14+
}
15+
ecosystem := e.(Enumerable)
16+
17+
t.Run("readxl", func(t *testing.T) {
18+
versions, err := ecosystem.GetVersions("readxl")
19+
if err != nil {
20+
t.Fatalf("failed to get CRAN versions for readxl: %v", err)
21+
}
22+
// Test typical semver X.Y.Z version
23+
checkNextVersion(t, versions, "0.1.0", "0.1.1")
24+
checkNextVersion(t, versions, "0.1.1", "1.0.0")
25+
})
26+
27+
t.Run("aqp", func(t *testing.T) {
28+
// Test atypical versioned package
29+
versions, err := ecosystem.GetVersions("aqp")
30+
if err != nil {
31+
t.Fatalf("failed to get CRAN versions for aqp: %v", err)
32+
}
33+
checkNextVersion(t, versions, "0.99-8.1", "0.99-8.47")
34+
})
35+
}
36+
37+
func TestCRAN_GetVersions_NotFound(t *testing.T) {
38+
t.Parallel()
39+
p := getTestProvider(t)
40+
e, ok := p.Get("CRAN")
41+
if !ok {
42+
t.Fatalf("Failed to retrieve CRAN ecosystem")
43+
}
44+
ecosystem := e.(Enumerable)
45+
_, err := ecosystem.GetVersions("doesnotexist123456")
46+
if !errors.Is(err, ErrPackageNotFound) {
47+
t.Errorf("expected ErrPackageNotFound, got %v", err)
48+
}
49+
}

0 commit comments

Comments
 (0)