Skip to content

Commit

Permalink
Transitive dependency support for Maven pom.xml (#1002)
Browse files Browse the repository at this point in the history
Issue #35

In this PR, the new Maven extractor invokes Maven resolver to compute
the transitive dependencies of a Maven pom.xml.

Since managed dependencies are not actually being depended on, they are
not in the resolved dependency graph, and thus they are not included in
the scan results.
  • Loading branch information
cuixq authored May 31, 2024
1 parent b60b594 commit f2a30a8
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 168 deletions.
15 changes: 6 additions & 9 deletions internal/manifest/fixtures/maven/interpolation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,12 @@
<artifactId>my.package</artifactId>
<version>${my.package.version}</version>
</dependency>

<dependency>
<groupId>org.mine</groupId>
<artifactId>ranged-package</artifactId>
<version>${version-range}</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mine</groupId>
<artifactId>ranged-package</artifactId>
<version>${version-range}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
4 changes: 4 additions & 0 deletions internal/manifest/fixtures/maven/one-package.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>

<properties>
<mavenVersion>3.0</mavenVersion>
</properties>
Expand Down
18 changes: 18 additions & 0 deletions internal/manifest/fixtures/maven/transitive.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>

<dependencies>
<dependency>
<groupId>org.direct</groupId>
<artifactId>alice</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.direct</groupId>
<artifactId>bob</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</project>
4 changes: 4 additions & 0 deletions internal/manifest/fixtures/maven/two-packages.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>

<properties>
<mavenVersion>3.0</mavenVersion>
</properties>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>

<properties>
<mavenVersion>3.0</mavenVersion>
</properties>
Expand Down
6 changes: 5 additions & 1 deletion internal/manifest/fixtures/maven/with-scope.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<project>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0</version>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
42 changes: 42 additions & 0 deletions internal/manifest/fixtures/universe/basic-universe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.apache.maven:maven-artifact
1.0.0
org.direct:alice
1.0.0
org.transitive:[email protected]
org.transitive:[email protected]
org.direct:bob
2.0.0
org.transitive:[email protected]
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:[email protected]
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
128 changes: 56 additions & 72 deletions internal/manifest/maven.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import (
"fmt"
"path/filepath"

depsdevpb "deps.dev/api/v3"
"deps.dev/util/maven"
"deps.dev/util/semver"
"deps.dev/util/resolve"
"deps.dev/util/resolve/dep"
mavenresolve "deps.dev/util/resolve/maven"
"github.com/google/osv-scanner/internal/resolution/client"
"github.com/google/osv-scanner/internal/resolution/util"
"github.com/google/osv-scanner/pkg/lockfile"
"golang.org/x/exp/maps"
)

type MavenResolverExtractor struct {
Client depsdevpb.InsightsClient
client.DependencyClient
}

func (e MavenResolverExtractor) ShouldExtract(path string) bool {
Expand All @@ -32,91 +35,72 @@ func (e MavenResolverExtractor) Extract(f lockfile.DepFile) ([]lockfile.PackageD
return []lockfile.PackageDetails{}, fmt.Errorf("could not interpolate Maven project %s: %w", project.ProjectKey.Name(), err)
}

details := map[string]lockfile.PackageDetails{}

for _, dep := range project.Dependencies {
name := dep.Name()
v, err := e.resolveVersion(ctx, dep)
if err != nil {
return []lockfile.PackageDetails{}, err
}
pkgDetails := lockfile.PackageDetails{
Name: name,
Version: v,
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
}
if dep.Scope != "" {
pkgDetails.DepGroups = append(pkgDetails.DepGroups, string(dep.Scope))
}
// A dependency may be declared more than one times, we keep the details
// from the last declared one as what `mvn` does.
details[name] = pkgDetails
}

// managed dependencies take precedent over standard dependencies
for _, dep := range project.DependencyManagement.Dependencies {
name := dep.Name()
v, err := e.resolveVersion(ctx, dep)
if err != nil {
return []lockfile.PackageDetails{}, err
}
pkgDetails := lockfile.PackageDetails{
Name: name,
Version: v,
Ecosystem: lockfile.MavenEcosystem,
CompareAs: lockfile.MavenEcosystem,
}
if dep.Scope != "" {
pkgDetails.DepGroups = append(pkgDetails.DepGroups, string(dep.Scope))
overrideClient := client.NewOverrideClient(e.DependencyClient)
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))
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, ""),
}
// A dependency may be declared more than one times, we keep the details
// from the last declared one as what `mvn` does.
details[name] = pkgDetails
}
overrideClient.AddVersion(root, reqs)

return maps.Values(details), nil
}

func (e MavenResolverExtractor) resolveVersion(ctx context.Context, dep maven.Dependency) (string, error) {
constraint, err := semver.Maven.ParseConstraint(string(dep.Version))
g, err := resolver.Resolve(ctx, root.VersionKey)
if err != nil {
return "", fmt.Errorf("failed parsing Maven constraint %s: %w", dep.Version, err)
}
if constraint.IsSimple() {
// Return the constraint if it is a simple version string.
return constraint.String(), nil
return []lockfile.PackageDetails{}, fmt.Errorf("failed resolving %v: %w", root, err)
}

// Otherwise return the greatest version matching the constraint.
// TODO: invoke Maven resolver to decide the exact version.
resp, err := e.Client.GetPackage(ctx, &depsdevpb.GetPackageRequest{
PackageKey: &depsdevpb.PackageKey{
System: depsdevpb.System_MAVEN,
Name: dep.Name(),
},
})
if err != nil {
return "", fmt.Errorf("requesting versions of Maven package %s: %w", dep.Name(), err)
for i, e := range g.Edges {
e.Type = dep.Type{}
g.Edges[i] = e
}

var result *semver.Version
for _, ver := range resp.GetVersions() {
v, _ := semver.Maven.Parse(ver.GetVersionKey().GetVersion())
if constraint.MatchVersion(v) && result.Compare(v) < 0 {
result = v
details := map[string]lockfile.PackageDetails{}
for i := 1; i < len(g.Nodes); i++ {
// Ignore the first node which is the root.
node := g.Nodes[i]
pkgDetails := util.VKToPackageDetails(node.Version)
// 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.
for _, dep := range project.Dependencies {
if dep.Name() != pkgDetails.Name {
continue
}
if dep.Scope != "" && dep.Scope != "compile" {
pkgDetails.DepGroups = append(pkgDetails.DepGroups, string(dep.Scope))
}
}
details[pkgDetails.Name] = pkgDetails
}

return result.String(), nil
return maps.Values(details), nil
}

func ParseMavenWithResolver(depsdev depsdevpb.InsightsClient, pathToLockfile string) ([]lockfile.PackageDetails, error) {
func ParseMavenWithResolver(depClient client.DependencyClient, pathToLockfile string) ([]lockfile.PackageDetails, error) {
f, err := lockfile.OpenLocalDepFile(pathToLockfile)
if err != nil {
return []lockfile.PackageDetails{}, err
}
defer f.Close()

return MavenResolverExtractor{Client: depsdev}.Extract(f)
return MavenResolverExtractor{DependencyClient: depClient}.Extract(f)
}
Loading

0 comments on commit f2a30a8

Please sign in to comment.