From 977c7a2474424590a1dca7fb11d3b10617b1281d Mon Sep 17 00:00:00 2001 From: Drew Bomhof Date: Wed, 7 Oct 2026 16:30:40 -0400 Subject: [PATCH] Add Red Hat Lightwell ecosystem support (version logic + PURL) Registers the Red Hat Lightwell ecosystem (Red Hat Lightwell:Maven / :PyPI). go/osv/ecosystem: redHatLightwellFactory delegates version semantics to the base ecosystem named by the :suffix (Maven/PyPI); an unsupported suffix yields a nil ecosystem (reported as not found). go/purl: generator delegates to the base ecosystem's registered PURL generator; no reverse parser is registered since pkg:maven/pkg:pypi types belong to the upstream ecosystems. Bumps the osv-schema bindings to the version defining EcosystemRedHatLightwell (ossf/osv-schema#571). Tests cover Maven/PyPI delegation and unsupported/missing suffixes. Companion to the source_test.yaml import (#6139) and tracking issue #6105. --- go/go.mod | 2 +- go/go.sum | 2 ++ go/osv/ecosystem/ecosystem.go | 1 + go/osv/ecosystem/red_hat_lightwell.go | 24 +++++++++++++ go/osv/ecosystem/red_hat_lightwell_test.go | 29 +++++++++++++++ go/purl/ecosystem_red_hat_lightwell.go | 39 +++++++++++++++++++++ go/purl/ecosystem_red_hat_lightwell_test.go | 34 ++++++++++++++++++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 go/osv/ecosystem/red_hat_lightwell.go create mode 100644 go/osv/ecosystem/red_hat_lightwell_test.go create mode 100644 go/purl/ecosystem_red_hat_lightwell.go create mode 100644 go/purl/ecosystem_red_hat_lightwell_test.go diff --git a/go/go.mod b/go/go.mod index 68638c04e30..65d59972f02 100644 --- a/go/go.mod +++ b/go/go.mod @@ -23,7 +23,7 @@ require ( github.com/klauspost/compress v1.20.0 github.com/microcosm-cc/bluemonday v1.0.27 github.com/nikolalohinski/gonja/v2 v2.9.0 - github.com/ossf/osv-schema/bindings/go v0.0.0-20260902031056-b388a18021a3 + github.com/ossf/osv-schema/bindings/go v0.0.0-20260924053548-8e3305dedc07 github.com/package-url/packageurl-go v0.1.7 github.com/pandatix/go-cvss v0.6.4 github.com/prometheus/client_golang v1.24.1 diff --git a/go/go.sum b/go/go.sum index 75b765b290f..3473da2ef0a 100644 --- a/go/go.sum +++ b/go/go.sum @@ -235,6 +235,8 @@ github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/ossf/osv-schema/bindings/go v0.0.0-20260902031056-b388a18021a3 h1:mNqW4tPTIPcx3KL5qZCByAHiTwGcwD/eCEfKOEvLA4g= github.com/ossf/osv-schema/bindings/go v0.0.0-20260902031056-b388a18021a3/go.mod h1:aW9XJyUJfg0r+HogMGMagTt4hF6ibBkH/FZenG/KiyE= +github.com/ossf/osv-schema/bindings/go v0.0.0-20260924053548-8e3305dedc07 h1:iSGN4UeIRS7B8jKaOsisArXvD7poOLUgEwcAIHuvTC4= +github.com/ossf/osv-schema/bindings/go v0.0.0-20260924053548-8e3305dedc07/go.mod h1:aW9XJyUJfg0r+HogMGMagTt4hF6ibBkH/FZenG/KiyE= github.com/package-url/packageurl-go v0.1.7 h1:iFWg6tzAjLA6F/qX3M5nZaiMHJgc+p2zxVyr/fY+sZY= github.com/package-url/packageurl-go v0.1.7/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= github.com/pandatix/go-cvss v0.6.4 h1:9w2RCO/Q4UTiJyEgpCHRiVc6CfrsFEnkoX+OtATqKio= diff --git a/go/osv/ecosystem/ecosystem.go b/go/osv/ecosystem/ecosystem.go index 9ae50baf1a5..18b6dfd2423 100644 --- a/go/osv/ecosystem/ecosystem.go +++ b/go/osv/ecosystem/ecosystem.go @@ -70,6 +70,7 @@ var ecosystems = map[osvconstants.Ecosystem]ecosystemFactory{ osvconstants.EcosystemPub: func(p *Provider, _ string) Ecosystem { return pubEcosystem{p: p} }, osvconstants.EcosystemPyPI: func(p *Provider, _ string) Ecosystem { return pypiEcosystem{p: p} }, osvconstants.EcosystemRedHat: statelessFactory[rpmEcosystem], + osvconstants.EcosystemRedHatLightwell: redHatLightwellFactory, osvconstants.EcosystemRockyLinux: statelessFactory[rpmEcosystem], osvconstants.EcosystemRoot: rootEcosystemFactory, osvconstants.EcosystemRubyGems: func(p *Provider, _ string) Ecosystem { return rubyGemsEcosystem{p: p} }, diff --git a/go/osv/ecosystem/red_hat_lightwell.go b/go/osv/ecosystem/red_hat_lightwell.go new file mode 100644 index 00000000000..c0abf874953 --- /dev/null +++ b/go/osv/ecosystem/red_hat_lightwell.go @@ -0,0 +1,24 @@ +package ecosystem + +import ( + "github.com/ossf/osv-schema/bindings/go/osvconstants" +) + +// redHatLightwellFactory builds the Ecosystem for a "Red Hat Lightwell:" +// OSV ecosystem, e.g. "Red Hat Lightwell:Maven" or "Red Hat Lightwell:PyPI". +// +// Red Hat Lightwell republishes upstream artifacts carrying a backport version +// marker (Maven ".rhlw-NNNNN", PyPI "+rhlw.N") that sorts within the base +// ecosystem's own version space, so version semantics delegate to the base +// ecosystem named by the ":" suffix. An unsupported suffix yields a nil +// Ecosystem, which Provider.Get reports as "not found". +func redHatLightwellFactory(p *Provider, suffix string) Ecosystem { + switch osvconstants.Ecosystem(suffix) { + case osvconstants.EcosystemMaven: + return mavenEcosystem{p: p} + case osvconstants.EcosystemPyPI: + return pypiEcosystem{p: p} + default: + return nil + } +} diff --git a/go/osv/ecosystem/red_hat_lightwell_test.go b/go/osv/ecosystem/red_hat_lightwell_test.go new file mode 100644 index 00000000000..1083d21e252 --- /dev/null +++ b/go/osv/ecosystem/red_hat_lightwell_test.go @@ -0,0 +1,29 @@ +package ecosystem + +import ( + "testing" +) + +func TestRedHatLightwell_Get(t *testing.T) { + p := NewProvider(nil) + + tests := []struct { + name string + eco string + found bool + }{ + {"maven suffix delegates", "Red Hat Lightwell:Maven", true}, + {"pypi suffix delegates", "Red Hat Lightwell:PyPI", true}, + {"unsupported suffix", "Red Hat Lightwell:npm", false}, + {"missing suffix", "Red Hat Lightwell", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, ok := p.Get(tt.eco) + if ok != tt.found { + t.Errorf("Get(%q) found = %v, want %v", tt.eco, ok, tt.found) + } + }) + } +} diff --git a/go/purl/ecosystem_red_hat_lightwell.go b/go/purl/ecosystem_red_hat_lightwell.go new file mode 100644 index 00000000000..b6b66bb9687 --- /dev/null +++ b/go/purl/ecosystem_red_hat_lightwell.go @@ -0,0 +1,39 @@ +package purl + +import ( + "fmt" + "strings" + + "github.com/ossf/osv-schema/bindings/go/osvconstants" + "github.com/package-url/packageurl-go" +) + +//nolint:gochecknoinits // init is used here to register the ecosystem with the global PURL registry. +func init() { + registerGenerator(osvconstants.EcosystemRedHatLightwell, generatorFunc(redHatLightwellGenerator)) + // No reverse parser is registered on purpose: Red Hat Lightwell records carry + // pkg:maven / pkg:pypi PURLs, whose types are already owned by the Maven/PyPI + // parsers (registerParser panics on collision). PURL -> ecosystem resolution + // for Lightwell is done via explicit ecosystem queries rather than the shared + // pkg:maven/pkg:pypi types, which belong to the upstream ecosystems. +} + +// redHatLightwellGenerator builds the PURL for a "Red Hat Lightwell:" +// package by delegating to the base ecosystem's registered generator, named by +// the ":" suffix (e.g. "Red Hat Lightwell:Maven" -> pkg:maven, ":PyPI" -> pkg:pypi). +func redHatLightwellGenerator(ecosystem, packageName string) (packageurl.PackageURL, error) { + _, suffix, _ := strings.Cut(ecosystem, ":") + base := osvconstants.Ecosystem(suffix) + + switch base { + case osvconstants.EcosystemMaven, osvconstants.EcosystemPyPI: + gen, ok := generators[base] + if !ok { + return packageurl.PackageURL{}, fmt.Errorf("no PURL generator registered for base ecosystem %q", base) + } + + return gen.generate(string(base), packageName) + default: + return packageurl.PackageURL{}, fmt.Errorf("unsupported Red Hat Lightwell ecosystem suffix %q", suffix) + } +} diff --git a/go/purl/ecosystem_red_hat_lightwell_test.go b/go/purl/ecosystem_red_hat_lightwell_test.go new file mode 100644 index 00000000000..129c9a8ef25 --- /dev/null +++ b/go/purl/ecosystem_red_hat_lightwell_test.go @@ -0,0 +1,34 @@ +package purl + +import ( + "testing" +) + +func TestRedHatLightwellGenerate(t *testing.T) { + tests := []struct { + ecosystem string + packageName string + want string + wantErr bool + }{ + {"Red Hat Lightwell:Maven", "org.apache.commons:commons-lang3", "pkg:maven/org.apache.commons/commons-lang3", false}, + {"Red Hat Lightwell:PyPI", "requests", "pkg:pypi/requests", false}, + // Unsupported / missing suffix. + {"Red Hat Lightwell:npm", "foo", "", true}, + {"Red Hat Lightwell", "foo", "", true}, + } + + for _, tt := range tests { + got, err := Generate(tt.ecosystem, tt.packageName) + if (err != nil) != tt.wantErr { + t.Errorf("Generate(%q, %q) error = %v, wantErr %v", tt.ecosystem, tt.packageName, err, tt.wantErr) + continue + } + if tt.wantErr { + continue + } + if got != tt.want { + t.Errorf("Generate(%q, %q) = %q, want %q", tt.ecosystem, tt.packageName, got, tt.want) + } + } +}