From 786e2fffd43f7b6f98bddb7e804abc98df98d86c Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Tue, 2 Dec 2025 11:04:38 +1300
Subject: [PATCH 01/10] test: add `transitive.xml` case to `pomlxml` extractor
---
.../language/java/pomxml/pomxml_test.go | 44 +++++++++++++++++++
.../java/pomxml/testdata/transitive.xml | 33 ++++++++++++++
2 files changed, 77 insertions(+)
create mode 100644 extractor/filesystem/language/java/pomxml/testdata/transitive.xml
diff --git a/extractor/filesystem/language/java/pomxml/pomxml_test.go b/extractor/filesystem/language/java/pomxml/pomxml_test.go
index e4710dc39..5d6d3ad26 100644
--- a/extractor/filesystem/language/java/pomxml/pomxml_test.go
+++ b/extractor/filesystem/language/java/pomxml/pomxml_test.go
@@ -336,6 +336,50 @@ func TestExtractor_Extract(t *testing.T) {
},
},
},
+ {
+ Name: "transitive dependencies",
+ InputConfig: extracttest.ScanInputMockConfig{
+ Path: "testdata/transitive.xml",
+ },
+ WantPackages: []*extractor.Package{
+ {
+ Name: "org.direct:alice",
+ Version: "1.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "alice",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.direct:bob",
+ Version: "2.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "bob",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.direct:chris",
+ Version: "3.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "chris",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ },
+ },
}
for _, tt := range tests {
diff --git a/extractor/filesystem/language/java/pomxml/testdata/transitive.xml b/extractor/filesystem/language/java/pomxml/testdata/transitive.xml
new file mode 100644
index 000000000..52e416a0b
--- /dev/null
+++ b/extractor/filesystem/language/java/pomxml/testdata/transitive.xml
@@ -0,0 +1,33 @@
+
+ com.mycompany.app
+ my-app
+ 1.0
+
+
+
+
+ org.transitive
+ frank
+ 4.4.4
+
+
+
+
+
+
+ org.direct
+ alice
+ 1.0.0
+
+
+ org.direct
+ bob
+ 2.0.0
+
+
+ org.direct
+ chris
+ 3.0.0
+
+
+
From 11726766775ea5db0fdd152d3cff5ca9a04e4be9 Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Tue, 2 Dec 2025 10:30:33 +1300
Subject: [PATCH 02/10] feat: lets just reimplement the extractor logic in the
enricher
---
.../transitivedependency/pomxml/pomxml.go | 329 ++++++++++++++++++
.../pomxml/pomxml_test.go | 268 ++++++++++++++
.../pomxml/testdata/transitive.xml | 33 ++
.../testdata/universe/basic-universe.yaml | 60 ++++
.../language/java/javalockfile/metadata.go | 3 +
.../language/java/pomxmlnet/pomxmlnet.go | 3 +
6 files changed, 696 insertions(+)
create mode 100644 enricher/transitivedependency/pomxml/pomxml.go
create mode 100644 enricher/transitivedependency/pomxml/pomxml_test.go
create mode 100644 enricher/transitivedependency/pomxml/testdata/transitive.xml
create mode 100644 enricher/transitivedependency/pomxml/testdata/universe/basic-universe.yaml
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
new file mode 100644
index 000000000..43f28e4f6
--- /dev/null
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -0,0 +1,329 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package pomxml implements an enricher to perform dependency resolution for Java pom.xml files.
+package pomxml
+
+import (
+ "context"
+ "fmt"
+ "maps"
+ "slices"
+ "strings"
+
+ "deps.dev/util/maven"
+ "deps.dev/util/resolve"
+ mavenresolve "deps.dev/util/resolve/maven"
+ "github.com/google/osv-scalibr/clients/datasource"
+ "github.com/google/osv-scalibr/clients/resolution"
+ "github.com/google/osv-scalibr/enricher"
+ "github.com/google/osv-scalibr/extractor"
+ "github.com/google/osv-scalibr/extractor/filesystem"
+ "github.com/google/osv-scalibr/extractor/filesystem/language/java/javalockfile"
+ "github.com/google/osv-scalibr/extractor/filesystem/language/java/pomxml"
+ "github.com/google/osv-scalibr/internal/mavenutil"
+ "github.com/google/osv-scalibr/inventory"
+ "github.com/google/osv-scalibr/log"
+ "github.com/google/osv-scalibr/plugin"
+ "github.com/google/osv-scalibr/purl"
+)
+
+const (
+ // Name is the unique name of this enricher.
+ Name = "transitivedependency/pomxml"
+)
+
+// Enricher performs dependency resolution for pom.xml.
+type Enricher struct {
+ depClient resolve.Client
+ MavenClient *datasource.MavenRegistryAPIClient
+}
+
+// Name returns the name of the enricher.
+func (Enricher) Name() string {
+ return Name
+}
+
+// Version returns the version of the enricher.
+func (Enricher) Version() int {
+ return 0
+}
+
+// Requirements returns the requirements of the enricher.
+func (Enricher) Requirements() *plugin.Capabilities {
+ return &plugin.Capabilities{
+ Network: plugin.NetworkOnline,
+ }
+}
+
+// RequiredPlugins returns the names of the plugins required by the enricher.
+func (Enricher) RequiredPlugins() []string {
+ return []string{pomxml.Name}
+}
+
+// Config is the configuration for the pomxmlnet Extractor.
+type Config struct {
+ *datasource.MavenRegistryAPIClient
+
+ DependencyClient resolve.Client
+}
+
+// NewConfig returns the configuration given the URL of the Maven registry to fetch metadata.
+func NewConfig(remote, local string, disableGoogleAuth bool) Config {
+ // No need to check errors since we are using the default Maven Central URL.
+ mavenClient, _ := datasource.NewMavenRegistryAPIClient(context.Background(), datasource.MavenRegistry{
+ URL: remote,
+ ReleasesEnabled: true,
+ }, local, disableGoogleAuth)
+ depClient := resolution.NewMavenRegistryClientWithAPI(mavenClient)
+ return Config{
+ DependencyClient: depClient,
+ MavenRegistryAPIClient: mavenClient,
+ }
+}
+
+// DefaultConfig returns the default configuration for the pomxmlnet extractor.
+func DefaultConfig() Config {
+ return NewConfig("", "", false)
+}
+
+// New makes a new pom.xml transitive extractor with the given config.
+func New(c Config) *Enricher {
+ return &Enricher{
+ depClient: c.DependencyClient,
+ MavenClient: c.MavenRegistryAPIClient,
+ }
+}
+
+// NewDefault returns an extractor with the default config settings.
+func NewDefault() *Enricher { return New(DefaultConfig()) }
+
+// packageWithIndex holds the package with its index in inv.Packages
+type packageWithIndex struct {
+ pkg *extractor.Package
+ index int
+}
+
+// groupPackages groups packages found in pom.xml files by the first location that they are found
+// and returns a map of location -> package name -> package with index.
+func groupPackages(pkgs []*extractor.Package) map[string]map[string]packageWithIndex {
+ result := make(map[string]map[string]packageWithIndex)
+ for i, pkg := range pkgs {
+ if !slices.Contains(pkg.Plugins, pomxml.Name) {
+ continue
+ }
+ if len(pkg.Locations) == 0 {
+ log.Warnf("package %s has no locations", pkg.Name)
+ continue
+ }
+ // Use the path where this package is first found.
+ path := pkg.Locations[0]
+ if _, ok := result[path]; !ok {
+ result[path] = make(map[string]packageWithIndex)
+ }
+ result[path][pkg.Name] = packageWithIndex{pkg, i}
+ }
+ return result
+}
+
+// Enrich enriches the inventory in pom.xml files with transitive dependencies.
+func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *inventory.Inventory) error {
+ pkgGroups := groupPackages(inv.Packages)
+
+ for path, pkgMap := range pkgGroups {
+ f, err := input.ScanRoot.FS.Open(path)
+
+ if err != nil {
+ return err
+ }
+
+ enrichedInv, err := e.extract(ctx, &filesystem.ScanInput{
+ Path: path,
+ Reader: f,
+ Info: nil,
+ FS: input.ScanRoot.FS,
+ Root: input.ScanRoot.Path,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ for _, pkg := range enrichedInv.Packages {
+ indexPkg, ok := pkgMap[pkg.Name]
+ if ok {
+ // This dependency is in manifest, update the version and plugins.
+ i := indexPkg.index
+ inv.Packages[i].Version = pkg.Version
+ inv.Packages[i].Plugins = append(inv.Packages[i].Plugins, Name)
+ } else {
+ // This dependency is not found in manifest, so it's a transitive dependency.
+ inv.Packages = append(inv.Packages, pkg)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (e Enricher) extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
+ var project maven.Project
+ if err := datasource.NewMavenDecoder(input.Reader).Decode(&project); err != nil {
+ return inventory.Inventory{}, fmt.Errorf("could not extract: %w", err)
+ }
+ // Empty JDK and ActivationOS indicates merging the default profiles.
+ if err := project.MergeProfiles("", maven.ActivationOS{}); err != nil {
+ return inventory.Inventory{}, fmt.Errorf("failed to merge profiles: %w", err)
+ }
+ // Interpolate the repositories so that properties are resolved.
+ if err := project.InterpolateRepositories(); err != nil {
+ return inventory.Inventory{}, fmt.Errorf("failed to interpolate project: %w", err)
+ }
+ // Clear the registries that may be from other extraction.
+ e.MavenClient = e.MavenClient.WithoutRegistries()
+ for _, repo := range project.Repositories {
+ if repo.URL.ContainsProperty() {
+ continue
+ }
+ if err := e.MavenClient.AddRegistry(ctx, datasource.MavenRegistry{
+ URL: string(repo.URL),
+ ID: string(repo.ID),
+ ReleasesEnabled: repo.Releases.Enabled.Boolean(),
+ SnapshotsEnabled: repo.Snapshots.Enabled.Boolean(),
+ }); err != nil {
+ return inventory.Inventory{}, fmt.Errorf("failed to add registry %s: %w", repo.URL, err)
+ }
+ }
+ // Merging parents data by parsing local parent pom.xml or fetching from upstream.
+ if err := mavenutil.MergeParents(ctx, project.Parent, &project, mavenutil.Options{
+ Input: input,
+ Client: e.MavenClient,
+ AddRegistry: true,
+ AllowLocal: true,
+ InitialParentIndex: 1,
+ }); err != nil {
+ return inventory.Inventory{}, fmt.Errorf("failed to merge parents: %w", err)
+ }
+ // Process the dependencies:
+ // - dedupe dependencies and dependency management
+ // - import dependency management
+ // - fill in missing dependency version requirement
+ project.ProcessDependencies(func(groupID, artifactID, version maven.String) (maven.DependencyManagement, error) {
+ return mavenutil.GetDependencyManagement(ctx, e.MavenClient, groupID, artifactID, version)
+ })
+
+ registries := e.MavenClient.GetRegistries()
+
+ if registries := e.MavenClient.GetRegistries(); len(registries) > 0 {
+ clientRegs := make([]resolution.Registry, len(registries))
+ for i, reg := range registries {
+ clientRegs[i] = reg
+ }
+ if cl, ok := e.depClient.(resolution.ClientWithRegistries); ok {
+ if err := cl.AddRegistries(ctx, clientRegs); err != nil {
+ return inventory.Inventory{}, err
+ }
+ }
+ }
+
+ overrideClient := resolution.NewOverrideClient(e.depClient)
+ resolver := mavenresolve.NewResolver(overrideClient)
+
+ // Resolve the dependencies.
+ root := resolve.Version{
+ VersionKey: resolve.VersionKey{
+ PackageKey: resolve.PackageKey{
+ System: resolve.Maven,
+ Name: project.ProjectKey.Name(),
+ },
+ VersionType: resolve.Concrete,
+ Version: string(project.Version),
+ }}
+ reqs := make([]resolve.RequirementVersion, len(project.Dependencies)+len(project.DependencyManagement.Dependencies))
+ for i, d := range project.Dependencies {
+ reqs[i] = resolve.RequirementVersion{
+ VersionKey: resolve.VersionKey{
+ PackageKey: resolve.PackageKey{
+ System: resolve.Maven,
+ Name: d.Name(),
+ },
+ VersionType: resolve.Requirement,
+ Version: string(d.Version),
+ },
+ Type: resolve.MavenDepType(d, ""),
+ }
+ }
+ for i, d := range project.DependencyManagement.Dependencies {
+ reqs[len(project.Dependencies)+i] = resolve.RequirementVersion{
+ VersionKey: resolve.VersionKey{
+ PackageKey: resolve.PackageKey{
+ System: resolve.Maven,
+ Name: d.Name(),
+ },
+ VersionType: resolve.Requirement,
+ Version: string(d.Version),
+ },
+ Type: resolve.MavenDepType(d, mavenutil.OriginManagement),
+ }
+ }
+ overrideClient.AddVersion(root, reqs)
+
+ g, err := resolver.Resolve(ctx, root.VersionKey)
+ if err != nil {
+ return inventory.Inventory{}, fmt.Errorf("failed resolving %v: %w", root, err)
+ }
+ if len(g.Nodes) <= 1 && g.Error != "" {
+ // Multi-registry error may be appended to the resolved graph so only return error when the graph is empty.
+ return inventory.Inventory{}, fmt.Errorf("failed resolving %v: %s", root, g.Error)
+ }
+
+ details := map[string]*extractor.Package{}
+ for i := 1; i < len(g.Nodes); i++ {
+ // Ignore the first node which is the root.
+ node := g.Nodes[i]
+ depGroups := []string{}
+ groupID, artifactID, _ := strings.Cut(node.Version.Name, ":")
+ // We are only able to know dependency groups of direct dependencies but
+ // not transitive dependencies because the nodes in the resolve graph does
+ // not have the scope information.
+ isDirect := false
+ for _, dep := range project.Dependencies {
+ if dep.Name() != node.Version.Name {
+ continue
+ }
+ isDirect = true
+ if dep.Scope != "" && dep.Scope != "compile" {
+ depGroups = append(depGroups, string(dep.Scope))
+ }
+ break
+ }
+ pkg := extractor.Package{
+ Name: node.Version.Name,
+ Version: node.Version.Version,
+ PURLType: purl.TypeMaven,
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: artifactID,
+ GroupID: groupID,
+ DepGroupVals: depGroups,
+ IsTransitive: !isDirect,
+ Registries: registries,
+ },
+ // TODO(#408): Add merged paths in here as well
+ Locations: []string{input.Path},
+ }
+ details[pkg.Name] = &pkg
+ }
+
+ return inventory.Inventory{Packages: slices.Collect(maps.Values(details))}, nil
+}
diff --git a/enricher/transitivedependency/pomxml/pomxml_test.go b/enricher/transitivedependency/pomxml/pomxml_test.go
new file mode 100644
index 000000000..72f6b9767
--- /dev/null
+++ b/enricher/transitivedependency/pomxml/pomxml_test.go
@@ -0,0 +1,268 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pomxml_test
+
+import (
+ "sort"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/osv-scalibr/clients/clienttest"
+ "github.com/google/osv-scalibr/clients/datasource"
+ "github.com/google/osv-scalibr/enricher"
+ "github.com/google/osv-scalibr/enricher/transitivedependency/pomxml"
+ "github.com/google/osv-scalibr/extractor"
+ "github.com/google/osv-scalibr/extractor/filesystem/language/java/javalockfile"
+ scalibrfs "github.com/google/osv-scalibr/fs"
+ "github.com/google/osv-scalibr/inventory"
+ "github.com/google/osv-scalibr/purl"
+)
+
+func TestEnricher_Enrich(t *testing.T) {
+ input := enricher.ScanInput{
+ ScanRoot: &scalibrfs.ScanRoot{
+ Path: "testdata",
+ FS: scalibrfs.DirFS("."),
+ },
+ }
+ inv := inventory.Inventory{
+ Packages: []*extractor.Package{
+ {
+ // Not a Java package.
+ Name: "abc",
+ Version: "1.0.0",
+ PURLType: purl.TypePyPi,
+ Locations: []string{"testdata/poetry/poetry.lock"},
+ Plugins: []string{"python/poetrylock"},
+ },
+ {
+ // Not extracted from a pom.xml
+ Name: "abc",
+ Version: "1.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/java/gradle.lockfile"},
+ Plugins: []string{"java/gradlelockfile"},
+ },
+ {
+ Name: "org.direct:alice",
+ Version: "1.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"java/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "alice",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.direct:bob",
+ Version: "2.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"java/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "bob",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.direct:chris",
+ Version: "3.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"java/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "chris",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ },
+ }
+
+ srv := clienttest.NewMockHTTPServer(t)
+ srv.SetResponse(t, "org/upstream/parent-pom/1.0/parent-pom-1.0.pom", []byte(`
+
+ org.upstream
+ parent-pom
+ 1.0
+ pom
+
+
+ org.eve
+ eve
+ 5.0.0
+
+
+
+ `))
+ srv.SetResponse(t, "org/import/import/1.2.3/import-1.2.3.pom", []byte(`
+
+ org.import
+ import
+ 1.2.3
+ pom
+
+
+
+ org.frank
+ frank
+ 6.0.0
+
+
+
+
+ `))
+
+ apiClient, err := datasource.NewDefaultMavenRegistryAPIClient(t.Context(), srv.URL)
+ if err != nil {
+ t.Fatalf("%v", err)
+ }
+
+ resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml")
+
+ enrichy := pomxml.New(pomxml.Config{
+ DependencyClient: resolutionClient,
+ MavenRegistryAPIClient: apiClient,
+ })
+
+ err = enrichy.Enrich(t.Context(), &input, &inv)
+ if err != nil {
+ t.Fatalf("failed to enrich: %v", err)
+ }
+
+ wantInventory := inventory.Inventory{
+ Packages: []*extractor.Package{
+ {
+ // Not a Java package.
+ Name: "abc",
+ Version: "1.0.0",
+ PURLType: purl.TypePyPi,
+ Locations: []string{"testdata/poetry/poetry.lock"},
+ Plugins: []string{"python/poetrylock"},
+ },
+ {
+ // Not extracted from a pom.xml
+ Name: "abc",
+ Version: "1.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/java/gradle.lockfile"},
+ Plugins: []string{"java/gradlelockfile"},
+ },
+ {
+ Name: "org.direct:alice",
+ Version: "1.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"java/pomxml", "transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "alice",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.direct:bob",
+ Version: "2.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"java/pomxml", "transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "bob",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.direct:chris",
+ Version: "3.0.0",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"java/pomxml", "transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "chris",
+ GroupID: "org.direct",
+ IsTransitive: false,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.transitive:chuck",
+ Version: "1.1.1",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "chuck",
+ GroupID: "org.transitive",
+ IsTransitive: true,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.transitive:dave",
+ Version: "2.2.2",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "dave",
+ GroupID: "org.transitive",
+ IsTransitive: true,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.transitive:eve",
+ Version: "3.3.3",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "eve",
+ GroupID: "org.transitive",
+ IsTransitive: true,
+ DepGroupVals: []string{},
+ },
+ },
+ {
+ Name: "org.transitive:frank",
+ Version: "4.4.4",
+ PURLType: purl.TypeMaven,
+ Locations: []string{"testdata/transitive.xml"},
+ Plugins: []string{"transitivedependency/pomxml"},
+ Metadata: &javalockfile.Metadata{
+ ArtifactID: "frank",
+ GroupID: "org.transitive",
+ IsTransitive: true,
+ DepGroupVals: []string{},
+ },
+ },
+ },
+ }
+ sort.Slice(inv.Packages, func(i, j int) bool {
+ return inv.Packages[i].Name < inv.Packages[j].Name
+ })
+ if diff := cmp.Diff(wantInventory, inv); diff != "" {
+ t.Errorf("%s.Enrich() diff (-want +got):\n%s", enrichy.Name(), diff)
+ }
+}
diff --git a/enricher/transitivedependency/pomxml/testdata/transitive.xml b/enricher/transitivedependency/pomxml/testdata/transitive.xml
new file mode 100644
index 000000000..52e416a0b
--- /dev/null
+++ b/enricher/transitivedependency/pomxml/testdata/transitive.xml
@@ -0,0 +1,33 @@
+
+ com.mycompany.app
+ my-app
+ 1.0
+
+
+
+
+ org.transitive
+ frank
+ 4.4.4
+
+
+
+
+
+
+ org.direct
+ alice
+ 1.0.0
+
+
+ org.direct
+ bob
+ 2.0.0
+
+
+ org.direct
+ chris
+ 3.0.0
+
+
+
diff --git a/enricher/transitivedependency/pomxml/testdata/universe/basic-universe.yaml b/enricher/transitivedependency/pomxml/testdata/universe/basic-universe.yaml
new file mode 100644
index 000000000..2bf2b3272
--- /dev/null
+++ b/enricher/transitivedependency/pomxml/testdata/universe/basic-universe.yaml
@@ -0,0 +1,60 @@
+system: maven
+schema: |
+ com.google.code.findbugs:jsr305
+ 3.0.2
+ io.netty:netty-all
+ 4.1.9
+ 4.1.42.Final
+ junit:junit
+ 4.12
+ org.alice:alice
+ 1.0.0
+ org.apache.maven:maven-artifact
+ 1.0.0
+ org.bob:bob
+ 2.0.0
+ org.chuck:chuck
+ 3.0.0
+ org.dave:dave
+ 4.0.0
+ org.direct:alice
+ 1.0.0
+ org.transitive:chuck@1.1.1
+ org.transitive:dave@2.2.2
+ org.direct:bob
+ 2.0.0
+ org.transitive:eve@3.3.3
+ org.direct:chris
+ 3.0.0
+ org.transitive:frank@3.3.3
+ org.eve:eve
+ 5.0.0
+ org.frank:frank
+ 6.0.0
+ org.mine:my.package
+ 2.3.4
+ org.mine:mypackage
+ 1.0.0
+ org.mine:ranged-package
+ 9.4.35
+ 9.4.36
+ 9.4.37
+ 9.5
+ org.slf4j:slf4j-log4j12
+ 1.7.25
+ org.transitive:chuck
+ 1.1.1
+ 2.2.2
+ org.transitive:eve@2.2.2
+ 3.3.3
+ org.transitive:dave
+ 1.1.1
+ 2.2.2
+ 3.3.3
+ org.transitive:eve
+ 1.1.1
+ 2.2.2
+ 3.3.3
+ org.transitive:frank
+ 3.3.3
+ 4.4.4
diff --git a/extractor/filesystem/language/java/javalockfile/metadata.go b/extractor/filesystem/language/java/javalockfile/metadata.go
index 962d5f2a2..357dd76b6 100644
--- a/extractor/filesystem/language/java/javalockfile/metadata.go
+++ b/extractor/filesystem/language/java/javalockfile/metadata.go
@@ -15,6 +15,8 @@
// Package javalockfile provides shared structures for Java extractors.
package javalockfile
+import "github.com/google/osv-scalibr/clients/datasource"
+
// Metadata holds parsing information for a Java package.
type Metadata struct {
ArtifactID string
@@ -23,6 +25,7 @@ type Metadata struct {
Classifier string
DepGroupVals []string
IsTransitive bool // Only set in pomxmlnet extractor
+ Registries []datasource.MavenRegistry
}
// DepGroups returns the dependency groups for the package.
diff --git a/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go b/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
index d43cca960..0e8b1004e 100644
--- a/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
+++ b/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
@@ -136,6 +136,8 @@ func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (in
return mavenutil.GetDependencyManagement(ctx, e.MavenClient, groupID, artifactID, version)
})
+ registries := e.MavenClient.GetRegistries()
+
if registries := e.MavenClient.GetRegistries(); len(registries) > 0 {
clientRegs := make([]resolution.Registry, len(registries))
for i, reg := range registries {
@@ -228,6 +230,7 @@ func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (in
GroupID: groupID,
DepGroupVals: depGroups,
IsTransitive: !isDirect,
+ Registries: registries,
},
// TODO(#408): Add merged paths in here as well
Locations: []string{input.Path},
From 0f072b98d2b1142a70d06fc6236dae4e013fff0d Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Tue, 2 Dec 2025 16:46:26 +1300
Subject: [PATCH 03/10] fix: include the plugin name
---
enricher/transitivedependency/pomxml/pomxml.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index 43f28e4f6..ca6352503 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -321,6 +321,7 @@ func (e Enricher) extract(ctx context.Context, input *filesystem.ScanInput) (inv
},
// TODO(#408): Add merged paths in here as well
Locations: []string{input.Path},
+ Plugins: []string{Name},
}
details[pkg.Name] = &pkg
}
From a4dbe45a8c38ec7c77deda0e99e02d46ec951eb3 Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Tue, 2 Dec 2025 16:48:18 +1300
Subject: [PATCH 04/10] fix: require direct fs
---
enricher/transitivedependency/pomxml/pomxml.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index ca6352503..d719eb8ad 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -63,7 +63,8 @@ func (Enricher) Version() int {
// Requirements returns the requirements of the enricher.
func (Enricher) Requirements() *plugin.Capabilities {
return &plugin.Capabilities{
- Network: plugin.NetworkOnline,
+ Network: plugin.NetworkOnline,
+ DirectFS: true,
}
}
From 6e8a1dee12eb8082add0435da113eb4560c13c50 Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Tue, 2 Dec 2025 16:51:40 +1300
Subject: [PATCH 05/10] fix: register enricher
---
enricher/enricherlist/list.go | 2 ++
enricher/transitivedependency/pomxml/pomxml.go | 4 +++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/enricher/enricherlist/list.go b/enricher/enricherlist/list.go
index 8c75f5e4c..91ebd4385 100644
--- a/enricher/enricherlist/list.go
+++ b/enricher/enricherlist/list.go
@@ -30,6 +30,7 @@ import (
"github.com/google/osv-scalibr/enricher/reachability/java"
"github.com/google/osv-scalibr/enricher/secrets/convert"
"github.com/google/osv-scalibr/enricher/secrets/hashicorp"
+ "github.com/google/osv-scalibr/enricher/transitivedependency/pomxml"
"github.com/google/osv-scalibr/enricher/transitivedependency/requirements"
"github.com/google/osv-scalibr/enricher/vex/filter"
"github.com/google/osv-scalibr/enricher/vulnmatch/osvdev"
@@ -138,6 +139,7 @@ var (
// TransitiveDependency enrichers.
TransitiveDependency = InitMap{
requirements.Name: {requirements.New},
+ pomxml.Name: {noCFG(pomxml.NewDefault)},
}
// PackageDeprecation enricher.
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index d719eb8ad..6c9642f62 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -108,7 +108,9 @@ func New(c Config) *Enricher {
}
// NewDefault returns an extractor with the default config settings.
-func NewDefault() *Enricher { return New(DefaultConfig()) }
+func NewDefault() enricher.Enricher {
+ return *New(DefaultConfig())
+}
// packageWithIndex holds the package with its index in inv.Packages
type packageWithIndex struct {
From a84898c213e393141205d46aff5149ecb812170d Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Mon, 8 Dec 2025 13:29:43 +1300
Subject: [PATCH 06/10] fix: we don't need to track Maven registries in the
metadata
---
enricher/transitivedependency/pomxml/pomxml.go | 3 ---
extractor/filesystem/language/java/javalockfile/metadata.go | 3 ---
extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go | 3 ---
3 files changed, 9 deletions(-)
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index 6c9642f62..ee7467456 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -226,8 +226,6 @@ func (e Enricher) extract(ctx context.Context, input *filesystem.ScanInput) (inv
return mavenutil.GetDependencyManagement(ctx, e.MavenClient, groupID, artifactID, version)
})
- registries := e.MavenClient.GetRegistries()
-
if registries := e.MavenClient.GetRegistries(); len(registries) > 0 {
clientRegs := make([]resolution.Registry, len(registries))
for i, reg := range registries {
@@ -320,7 +318,6 @@ func (e Enricher) extract(ctx context.Context, input *filesystem.ScanInput) (inv
GroupID: groupID,
DepGroupVals: depGroups,
IsTransitive: !isDirect,
- Registries: registries,
},
// TODO(#408): Add merged paths in here as well
Locations: []string{input.Path},
diff --git a/extractor/filesystem/language/java/javalockfile/metadata.go b/extractor/filesystem/language/java/javalockfile/metadata.go
index 357dd76b6..962d5f2a2 100644
--- a/extractor/filesystem/language/java/javalockfile/metadata.go
+++ b/extractor/filesystem/language/java/javalockfile/metadata.go
@@ -15,8 +15,6 @@
// Package javalockfile provides shared structures for Java extractors.
package javalockfile
-import "github.com/google/osv-scalibr/clients/datasource"
-
// Metadata holds parsing information for a Java package.
type Metadata struct {
ArtifactID string
@@ -25,7 +23,6 @@ type Metadata struct {
Classifier string
DepGroupVals []string
IsTransitive bool // Only set in pomxmlnet extractor
- Registries []datasource.MavenRegistry
}
// DepGroups returns the dependency groups for the package.
diff --git a/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go b/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
index 0e8b1004e..d43cca960 100644
--- a/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
+++ b/extractor/filesystem/language/java/pomxmlnet/pomxmlnet.go
@@ -136,8 +136,6 @@ func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (in
return mavenutil.GetDependencyManagement(ctx, e.MavenClient, groupID, artifactID, version)
})
- registries := e.MavenClient.GetRegistries()
-
if registries := e.MavenClient.GetRegistries(); len(registries) > 0 {
clientRegs := make([]resolution.Registry, len(registries))
for i, reg := range registries {
@@ -230,7 +228,6 @@ func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (in
GroupID: groupID,
DepGroupVals: depGroups,
IsTransitive: !isDirect,
- Registries: registries,
},
// TODO(#408): Add merged paths in here as well
Locations: []string{input.Path},
From 7dfdb298deefec984c1c9fa80e53a117cfbdd790 Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Mon, 8 Dec 2025 13:41:12 +1300
Subject: [PATCH 07/10] refactor: deduplicate utility functions
---
.../transitivedependency/internal/grouping.go | 36 ++++++++++++++++
.../transitivedependency/pomxml/pomxml.go | 34 ++-------------
.../requirements/requirements.go | 41 ++++---------------
3 files changed, 46 insertions(+), 65 deletions(-)
create mode 100644 enricher/transitivedependency/internal/grouping.go
diff --git a/enricher/transitivedependency/internal/grouping.go b/enricher/transitivedependency/internal/grouping.go
new file mode 100644
index 000000000..9c31dce0c
--- /dev/null
+++ b/enricher/transitivedependency/internal/grouping.go
@@ -0,0 +1,36 @@
+package internal
+
+import (
+ "slices"
+
+ "github.com/google/osv-scalibr/extractor"
+ "github.com/google/osv-scalibr/log"
+)
+
+// PackageWithIndex holds the package with its index in inv.Packages
+type PackageWithIndex struct {
+ Pkg *extractor.Package
+ Index int
+}
+
+// GroupPackagesFromPlugin groups packages that were added by a particular plugin by the first location
+// that they are found and returns a map of location -> package name -> package with index.
+func GroupPackagesFromPlugin(pkgs []*extractor.Package, pluginName string) map[string]map[string]PackageWithIndex {
+ result := make(map[string]map[string]PackageWithIndex)
+ for i, pkg := range pkgs {
+ if !slices.Contains(pkg.Plugins, pluginName) {
+ continue
+ }
+ if len(pkg.Locations) == 0 {
+ log.Warnf("package %s has no locations", pkg.Name)
+ continue
+ }
+ // Use the path where this package is first found.
+ path := pkg.Locations[0]
+ if _, ok := result[path]; !ok {
+ result[path] = make(map[string]PackageWithIndex)
+ }
+ result[path][pkg.Name] = PackageWithIndex{pkg, i}
+ }
+ return result
+}
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index ee7467456..7e5b946a1 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -28,13 +28,13 @@ import (
"github.com/google/osv-scalibr/clients/datasource"
"github.com/google/osv-scalibr/clients/resolution"
"github.com/google/osv-scalibr/enricher"
+ "github.com/google/osv-scalibr/enricher/transitivedependency/internal"
"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/java/javalockfile"
"github.com/google/osv-scalibr/extractor/filesystem/language/java/pomxml"
"github.com/google/osv-scalibr/internal/mavenutil"
"github.com/google/osv-scalibr/inventory"
- "github.com/google/osv-scalibr/log"
"github.com/google/osv-scalibr/plugin"
"github.com/google/osv-scalibr/purl"
)
@@ -112,37 +112,9 @@ func NewDefault() enricher.Enricher {
return *New(DefaultConfig())
}
-// packageWithIndex holds the package with its index in inv.Packages
-type packageWithIndex struct {
- pkg *extractor.Package
- index int
-}
-
-// groupPackages groups packages found in pom.xml files by the first location that they are found
-// and returns a map of location -> package name -> package with index.
-func groupPackages(pkgs []*extractor.Package) map[string]map[string]packageWithIndex {
- result := make(map[string]map[string]packageWithIndex)
- for i, pkg := range pkgs {
- if !slices.Contains(pkg.Plugins, pomxml.Name) {
- continue
- }
- if len(pkg.Locations) == 0 {
- log.Warnf("package %s has no locations", pkg.Name)
- continue
- }
- // Use the path where this package is first found.
- path := pkg.Locations[0]
- if _, ok := result[path]; !ok {
- result[path] = make(map[string]packageWithIndex)
- }
- result[path][pkg.Name] = packageWithIndex{pkg, i}
- }
- return result
-}
-
// Enrich enriches the inventory in pom.xml files with transitive dependencies.
func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *inventory.Inventory) error {
- pkgGroups := groupPackages(inv.Packages)
+ pkgGroups := internal.GroupPackagesFromPlugin(inv.Packages, pomxml.Name)
for path, pkgMap := range pkgGroups {
f, err := input.ScanRoot.FS.Open(path)
@@ -167,7 +139,7 @@ func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *in
indexPkg, ok := pkgMap[pkg.Name]
if ok {
// This dependency is in manifest, update the version and plugins.
- i := indexPkg.index
+ i := indexPkg.Index
inv.Packages[i].Version = pkg.Version
inv.Packages[i].Plugins = append(inv.Packages[i].Plugins, Name)
} else {
diff --git a/enricher/transitivedependency/requirements/requirements.go b/enricher/transitivedependency/requirements/requirements.go
index 814af8c4c..eeb461450 100644
--- a/enricher/transitivedependency/requirements/requirements.go
+++ b/enricher/transitivedependency/requirements/requirements.go
@@ -27,6 +27,7 @@ import (
cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
"github.com/google/osv-scalibr/clients/resolution"
"github.com/google/osv-scalibr/enricher"
+ "github.com/google/osv-scalibr/enricher/transitivedependency/internal"
"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/extractor/filesystem/language/python/requirements"
"github.com/google/osv-scalibr/inventory"
@@ -76,19 +77,19 @@ func New(cfg *cpb.PluginConfig) enricher.Enricher {
// Enrich enriches the inventory in requirements.txt with transitive dependencies.
func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *inventory.Inventory) error {
- pkgGroups := groupPackages(inv.Packages)
+ pkgGroups := internal.GroupPackagesFromPlugin(inv.Packages, requirements.Name)
for path, pkgMap := range pkgGroups {
- packages := make([]packageWithIndex, 0, len(pkgMap))
+ packages := make([]internal.PackageWithIndex, 0, len(pkgMap))
for _, indexPkg := range pkgMap {
packages = append(packages, indexPkg)
}
- slices.SortFunc(packages, func(a, b packageWithIndex) int {
- return a.index - b.index
+ slices.SortFunc(packages, func(a, b internal.PackageWithIndex) int {
+ return a.Index - b.Index
})
list := make([]*extractor.Package, 0, len(packages))
for _, indexPkg := range packages {
- list = append(list, indexPkg.pkg)
+ list = append(list, indexPkg.Pkg)
}
if len(list) == 0 || len(list[0].Metadata.(*requirements.Metadata).HashCheckingModeValues) > 0 {
// Do not perform transitive extraction with hash-checking mode.
@@ -109,7 +110,7 @@ func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *in
indexPkg, ok := pkgMap[pkg.Name]
if ok {
// This dependency is in manifest, update the version and plugins.
- i := indexPkg.index
+ i := indexPkg.Index
inv.Packages[i].Version = pkg.Version
inv.Packages[i].Plugins = append(inv.Packages[i].Plugins, Name)
} else {
@@ -121,34 +122,6 @@ func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *in
return nil
}
-// packageWithIndex holds the package with its index in inv.Packages
-type packageWithIndex struct {
- pkg *extractor.Package
- index int
-}
-
-// groupPackages groups packages found in requirements.txt by the first location that they are found
-// and returns a map of location -> package name -> package with index.
-func groupPackages(pkgs []*extractor.Package) map[string]map[string]packageWithIndex {
- result := make(map[string]map[string]packageWithIndex)
- for i, pkg := range pkgs {
- if !slices.Contains(pkg.Plugins, requirements.Name) {
- continue
- }
- if len(pkg.Locations) == 0 {
- log.Warnf("package %s has no locations", pkg.Name)
- continue
- }
- // Use the path where this package is first found.
- path := pkg.Locations[0]
- if _, ok := result[path]; !ok {
- result[path] = make(map[string]packageWithIndex)
- }
- result[path][pkg.Name] = packageWithIndex{pkg, i}
- }
- return result
-}
-
// resolve performs dependency resolution for packages found in a single requirements.txt.
func (e Enricher) resolve(ctx context.Context, path string, list []*extractor.Package) ([]*extractor.Package, error) {
overrideClient := resolution.NewOverrideClient(e.Client)
From dee8eb56526dfceb6a94a51618f2df363290badc Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Mon, 8 Dec 2025 13:57:24 +1300
Subject: [PATCH 08/10] refactor: deduplicate inv package adding logic
---
.../transitivedependency/internal/grouping.go | 17 +++++++++++++++++
enricher/transitivedependency/pomxml/pomxml.go | 13 +------------
.../requirements/requirements.go | 13 +------------
3 files changed, 19 insertions(+), 24 deletions(-)
diff --git a/enricher/transitivedependency/internal/grouping.go b/enricher/transitivedependency/internal/grouping.go
index 9c31dce0c..546b7c23b 100644
--- a/enricher/transitivedependency/internal/grouping.go
+++ b/enricher/transitivedependency/internal/grouping.go
@@ -4,6 +4,7 @@ import (
"slices"
"github.com/google/osv-scalibr/extractor"
+ "github.com/google/osv-scalibr/inventory"
"github.com/google/osv-scalibr/log"
)
@@ -34,3 +35,19 @@ func GroupPackagesFromPlugin(pkgs []*extractor.Package, pluginName string) map[s
}
return result
}
+
+// Add handles supplementing an inventory with enriched packages
+func Add(enrichedPkgs []*extractor.Package, inv *inventory.Inventory, pluginName string, existingPackages map[string]PackageWithIndex) {
+ for _, pkg := range enrichedPkgs {
+ indexPkg, ok := existingPackages[pkg.Name]
+ if ok {
+ // This dependency is in manifest, update the version and plugins.
+ i := indexPkg.Index
+ inv.Packages[i].Version = pkg.Version
+ inv.Packages[i].Plugins = append(inv.Packages[i].Plugins, pluginName)
+ } else {
+ // This dependency is not found in manifest, so it's a transitive dependency.
+ inv.Packages = append(inv.Packages, pkg)
+ }
+ }
+}
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index 7e5b946a1..99fe99576 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -135,18 +135,7 @@ func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *in
return err
}
- for _, pkg := range enrichedInv.Packages {
- indexPkg, ok := pkgMap[pkg.Name]
- if ok {
- // This dependency is in manifest, update the version and plugins.
- i := indexPkg.Index
- inv.Packages[i].Version = pkg.Version
- inv.Packages[i].Plugins = append(inv.Packages[i].Plugins, Name)
- } else {
- // This dependency is not found in manifest, so it's a transitive dependency.
- inv.Packages = append(inv.Packages, pkg)
- }
- }
+ internal.Add(enrichedInv.Packages, inv, Name, pkgMap)
}
return nil
diff --git a/enricher/transitivedependency/requirements/requirements.go b/enricher/transitivedependency/requirements/requirements.go
index eeb461450..ce8880197 100644
--- a/enricher/transitivedependency/requirements/requirements.go
+++ b/enricher/transitivedependency/requirements/requirements.go
@@ -106,18 +106,7 @@ func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *in
continue
}
- for _, pkg := range pkgs {
- indexPkg, ok := pkgMap[pkg.Name]
- if ok {
- // This dependency is in manifest, update the version and plugins.
- i := indexPkg.Index
- inv.Packages[i].Version = pkg.Version
- inv.Packages[i].Plugins = append(inv.Packages[i].Plugins, Name)
- } else {
- // This dependency is not found in manifest, so it's a transitive dependency.
- inv.Packages = append(inv.Packages, pkg)
- }
- }
+ internal.Add(pkgs, inv, Name, pkgMap)
}
return nil
}
From 6076636fd1afee35dfbc4d786b6ee04d462462d6 Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Mon, 8 Dec 2025 14:08:05 +1300
Subject: [PATCH 09/10] refactor: use different config setup
---
enricher/enricherlist/list.go | 2 +-
.../transitivedependency/pomxml/pomxml.go | 42 +++++++------------
.../pomxml/pomxml_test.go | 8 ++--
3 files changed, 21 insertions(+), 31 deletions(-)
diff --git a/enricher/enricherlist/list.go b/enricher/enricherlist/list.go
index 91ebd4385..a4fb57fbb 100644
--- a/enricher/enricherlist/list.go
+++ b/enricher/enricherlist/list.go
@@ -139,7 +139,7 @@ var (
// TransitiveDependency enrichers.
TransitiveDependency = InitMap{
requirements.Name: {requirements.New},
- pomxml.Name: {noCFG(pomxml.NewDefault)},
+ pomxml.Name: {pomxml.New},
}
// PackageDeprecation enricher.
diff --git a/enricher/transitivedependency/pomxml/pomxml.go b/enricher/transitivedependency/pomxml/pomxml.go
index 99fe99576..34de4b697 100644
--- a/enricher/transitivedependency/pomxml/pomxml.go
+++ b/enricher/transitivedependency/pomxml/pomxml.go
@@ -25,6 +25,7 @@ import (
"deps.dev/util/maven"
"deps.dev/util/resolve"
mavenresolve "deps.dev/util/resolve/maven"
+ cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
"github.com/google/osv-scalibr/clients/datasource"
"github.com/google/osv-scalibr/clients/resolution"
"github.com/google/osv-scalibr/enricher"
@@ -46,7 +47,7 @@ const (
// Enricher performs dependency resolution for pom.xml.
type Enricher struct {
- depClient resolve.Client
+ DepClient resolve.Client
MavenClient *datasource.MavenRegistryAPIClient
}
@@ -80,38 +81,27 @@ type Config struct {
DependencyClient resolve.Client
}
-// NewConfig returns the configuration given the URL of the Maven registry to fetch metadata.
-func NewConfig(remote, local string, disableGoogleAuth bool) Config {
+// New makes a new pom.xml transitive enricher with the given config.
+func New(cfg *cpb.PluginConfig) enricher.Enricher {
+ upstreamRegistry := ""
+ specific := plugin.FindConfig(cfg, func(c *cpb.PluginSpecificConfig) *cpb.POMXMLNetConfig { return c.GetPomXmlNet() })
+ if specific != nil {
+ upstreamRegistry = specific.UpstreamRegistry
+ }
+
// No need to check errors since we are using the default Maven Central URL.
mavenClient, _ := datasource.NewMavenRegistryAPIClient(context.Background(), datasource.MavenRegistry{
- URL: remote,
+ URL: upstreamRegistry,
ReleasesEnabled: true,
- }, local, disableGoogleAuth)
+ }, cfg.LocalRegistry, cfg.DisableGoogleAuth)
depClient := resolution.NewMavenRegistryClientWithAPI(mavenClient)
- return Config{
- DependencyClient: depClient,
- MavenRegistryAPIClient: mavenClient,
- }
-}
-// DefaultConfig returns the default configuration for the pomxmlnet extractor.
-func DefaultConfig() Config {
- return NewConfig("", "", false)
-}
-
-// New makes a new pom.xml transitive extractor with the given config.
-func New(c Config) *Enricher {
return &Enricher{
- depClient: c.DependencyClient,
- MavenClient: c.MavenRegistryAPIClient,
+ DepClient: depClient,
+ MavenClient: mavenClient,
}
}
-// NewDefault returns an extractor with the default config settings.
-func NewDefault() enricher.Enricher {
- return *New(DefaultConfig())
-}
-
// Enrich enriches the inventory in pom.xml files with transitive dependencies.
func (e Enricher) Enrich(ctx context.Context, input *enricher.ScanInput, inv *inventory.Inventory) error {
pkgGroups := internal.GroupPackagesFromPlugin(inv.Packages, pomxml.Name)
@@ -192,14 +182,14 @@ func (e Enricher) extract(ctx context.Context, input *filesystem.ScanInput) (inv
for i, reg := range registries {
clientRegs[i] = reg
}
- if cl, ok := e.depClient.(resolution.ClientWithRegistries); ok {
+ if cl, ok := e.DepClient.(resolution.ClientWithRegistries); ok {
if err := cl.AddRegistries(ctx, clientRegs); err != nil {
return inventory.Inventory{}, err
}
}
}
- overrideClient := resolution.NewOverrideClient(e.depClient)
+ overrideClient := resolution.NewOverrideClient(e.DepClient)
resolver := mavenresolve.NewResolver(overrideClient)
// Resolve the dependencies.
diff --git a/enricher/transitivedependency/pomxml/pomxml_test.go b/enricher/transitivedependency/pomxml/pomxml_test.go
index 72f6b9767..aa1ee7e12 100644
--- a/enricher/transitivedependency/pomxml/pomxml_test.go
+++ b/enricher/transitivedependency/pomxml/pomxml_test.go
@@ -19,6 +19,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
+ cpb "github.com/google/osv-scalibr/binary/proto/config_go_proto"
"github.com/google/osv-scalibr/clients/clienttest"
"github.com/google/osv-scalibr/clients/datasource"
"github.com/google/osv-scalibr/enricher"
@@ -138,10 +139,9 @@ func TestEnricher_Enrich(t *testing.T) {
resolutionClient := clienttest.NewMockResolutionClient(t, "testdata/universe/basic-universe.yaml")
- enrichy := pomxml.New(pomxml.Config{
- DependencyClient: resolutionClient,
- MavenRegistryAPIClient: apiClient,
- })
+ enrichy := pomxml.New(&cpb.PluginConfig{})
+ enrichy.(*pomxml.Enricher).DepClient = resolutionClient
+ enrichy.(*pomxml.Enricher).MavenClient = apiClient
err = enrichy.Enrich(t.Context(), &input, &inv)
if err != nil {
From ed77a8a3a698c5e10dc9ab0ba15f3bb9af33a7ac Mon Sep 17 00:00:00 2001
From: Gareth Jones <3151613+G-Rath@users.noreply.github.com>
Date: Mon, 8 Dec 2025 14:09:04 +1300
Subject: [PATCH 10/10] chore: add package copyright and comment
---
.../transitivedependency/internal/grouping.go | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/enricher/transitivedependency/internal/grouping.go b/enricher/transitivedependency/internal/grouping.go
index 546b7c23b..9d981c344 100644
--- a/enricher/transitivedependency/internal/grouping.go
+++ b/enricher/transitivedependency/internal/grouping.go
@@ -1,3 +1,18 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package internal contains miscellaneous functions and objects useful within transitive dependency enrichers
package internal
import (