Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Pure-Go effective-POM resolution for Maven artifacts. No JVM, no shelling out to `mvn`.

This computes the subset of `mvn help:effective-pom` that matters for dependency analysis: walk the parent chain, merge `<properties>` and `<dependencyManagement>`, expand `<scope>import</scope>` BOMs, apply profiles, interpolate `${...}`, and fill in missing versions. It does not touch plugins, lifecycle, or build configuration.
This computes the subset of `mvn help:effective-pom` that matters for dependency analysis: walk the parent chain, merge `<properties>` and `<dependencyManagement>`, expand `<scope>import</scope>` BOMs, apply profiles, interpolate `${...}`, and fill in missing versions. `ParsePOM` also retains raw coordinates from build plugins, plugin management, plugin dependencies, and build extensions for callers inspecting source declarations. The effective-POM resolver does not merge plugins or interpret lifecycle and build configuration.

The motivating use case is vulnerability matching, where a dependency declared as `<version>${jackson.version}</version>` is useless until something resolves the property. See [scrutineer#46](https://github.com/alpha-omega-security/scrutineer/issues/46).

Expand Down Expand Up @@ -120,7 +120,7 @@ One known divergence: dependencies whose identity is OS-gated (netty's `${os.det

## What this doesn't do

Plugin merging, lifecycle binding, `<build>` configuration, repository declarations, `settings.xml`, mirror selection, version-range mediation, transitive resolution. This is a model builder, not a dependency resolver. If you need a full tree, feed the output of this into something that walks transitive edges.
Plugin merging, lifecycle binding, interpretation of arbitrary `<build>` configuration, repository declarations, `settings.xml`, mirror selection, version-range mediation, transitive resolution. This is a model builder, not a dependency resolver. If you need a full tree, feed the output of this into something that walks transitive edges.

## License

Expand Down
94 changes: 94 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ func decodeProject(dec *xml.Decoder, start xml.StartElement, p *POM) error {
err = decodeDependencies(dec, tok, &p.Dependencies)
case "dependencyManagement":
err = decodeDepMgmt(dec, tok, &p.DependencyManagement)
case "build":
err = decodeBuild(dec, tok, &p.Build)
case "profiles":
err = decodeProfiles(dec, tok, &p.Profiles)
default:
Expand Down Expand Up @@ -251,6 +253,96 @@ func decodeDep(dec *xml.Decoder, start xml.StartElement) (Dep, error) {
return dep, err
}

func decodeBuild(dec *xml.Decoder, start xml.StartElement, build *Build) error {
return decodeFields(dec, start, func(child xml.StartElement) error {
switch child.Name.Local {
case "plugins":
return decodePlugins(dec, child, &build.Plugins)
case "pluginManagement":
return decodePluginManagement(dec, child, &build.PluginManagement)
case "extensions":
return decodeExtensions(dec, child, &build.Extensions)
default:
return dec.Skip()
}
})
}

func decodePlugins(dec *xml.Decoder, start xml.StartElement, plugins *[]Plugin) error {
return decodeFields(dec, start, func(child xml.StartElement) error {
if child.Name.Local != "plugin" {
return dec.Skip()
}
plugin, err := decodePlugin(dec, child)
if err == nil {
*plugins = append(*plugins, plugin)
}
return err
})
}

func decodePluginManagement(dec *xml.Decoder, start xml.StartElement, management *PluginManagement) error {
return decodeFields(dec, start, func(child xml.StartElement) error {
if child.Name.Local != "plugins" {
return dec.Skip()
}
return decodePlugins(dec, child, &management.Plugins)
})
}

func decodePlugin(dec *xml.Decoder, start xml.StartElement) (Plugin, error) {
var plugin Plugin
err := decodeFields(dec, start, func(child xml.StartElement) error {
var err error
switch child.Name.Local {
case elementGroupID:
plugin.GroupID, err = decodeText(dec, child)
case elementArtifactID:
plugin.ArtifactID, err = decodeText(dec, child)
case elementVersion:
plugin.Version, err = decodeText(dec, child)
case elementDependencies:
err = decodeDependencies(dec, child, &plugin.Dependencies)
default:
err = dec.Skip()
}
return err
})
return plugin, err
}

func decodeExtensions(dec *xml.Decoder, start xml.StartElement, extensions *[]Extension) error {
return decodeFields(dec, start, func(child xml.StartElement) error {
if child.Name.Local != "extension" {
return dec.Skip()
}
extension, err := decodeExtension(dec, child)
if err == nil {
*extensions = append(*extensions, extension)
}
return err
})
}

func decodeExtension(dec *xml.Decoder, start xml.StartElement) (Extension, error) {
var extension Extension
err := decodeFields(dec, start, func(child xml.StartElement) error {
var err error
switch child.Name.Local {
case elementGroupID:
extension.GroupID, err = decodeText(dec, child)
case elementArtifactID:
extension.ArtifactID, err = decodeText(dec, child)
case elementVersion:
extension.Version, err = decodeText(dec, child)
default:
err = dec.Skip()
}
return err
})
return extension, err
}

func decodeExclusions(dec *xml.Decoder, start xml.StartElement, exclusions *[]Exclusion) error {
return decodeFields(dec, start, func(child xml.StartElement) error {
if child.Name.Local != "exclusion" {
Expand Down Expand Up @@ -304,6 +396,8 @@ func decodeProfile(dec *xml.Decoder, start xml.StartElement) (Profile, error) {
err = decodeDependencies(dec, child, &profile.Dependencies)
case "dependencyManagement":
err = decodeDepMgmt(dec, child, &profile.DependencyManagement)
case "build":
err = decodeBuild(dec, child, &profile.Build)
default:
err = dec.Skip()
}
Expand Down
34 changes: 32 additions & 2 deletions pom.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ func ParseGAV(s string) (GAV, error) {
return g, nil
}

// POM is the parsed subset of a project object model that the resolver
// cares about. Fields are raw (uninterpolated) as read from XML.
// POM is the parsed subset of a project object model used for dependency
// resolution and source-level coordinate inspection. Fields are raw
// (uninterpolated) as read from XML.
type POM struct {
XMLName xml.Name `xml:"project"`

Expand All @@ -76,6 +77,7 @@ type POM struct {
Properties Properties `xml:"properties"`
Dependencies []Dep `xml:"dependencies>dependency"`
DependencyManagement DepMgmt `xml:"dependencyManagement"`
Build Build `xml:"build"`

Profiles []Profile `xml:"profiles>profile"`
}
Expand Down Expand Up @@ -169,6 +171,33 @@ type Dep struct {
Exclusions []Exclusion `xml:"exclusions>exclusion"`
}

// Build is the coordinate-bearing subset of a project's <build> block.
type Build struct {
Plugins []Plugin `xml:"plugins>plugin"`
PluginManagement PluginManagement `xml:"pluginManagement"`
Extensions []Extension `xml:"extensions>extension"`
}

// PluginManagement wraps the plugins declared under <pluginManagement>.
type PluginManagement struct {
Plugins []Plugin `xml:"plugins>plugin"`
}

// Plugin is a build plugin coordinate and its directly declared dependencies.
type Plugin struct {
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
Dependencies []Dep `xml:"dependencies>dependency"`
}

// Extension is a build extension coordinate.
type Extension struct {
GroupID string `xml:"groupId"`
ArtifactID string `xml:"artifactId"`
Version string `xml:"version"`
}

// Exclusion is a <exclusion> entry under a dependency.
type Exclusion struct {
GroupID string `xml:"groupId"`
Expand Down Expand Up @@ -196,6 +225,7 @@ type Profile struct {
Properties Properties `xml:"properties"`
Dependencies []Dep `xml:"dependencies>dependency"`
DependencyManagement DepMgmt `xml:"dependencyManagement"`
Build Build `xml:"build"`
}

// Activation holds the parts of <activation> relevant to static evaluation.
Expand Down
80 changes: 80 additions & 0 deletions pom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,86 @@ func TestParsePOM(t *testing.T) {
}
}

func TestParsePOMBuildCoordinates(t *testing.T) {
src := []byte(`<project>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>4.0.0</version>
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>plugin-runtime</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.example</groupId>
<artifactId>managed-plugin</artifactId>
<version>2.0.0</version>
</plugin>
</plugins>
</pluginManagement>
<extensions>
<extension>
<groupId>org.example</groupId>
<artifactId>extension</artifactId>
<version>3.0.0</version>
</extension>
</extensions>
</build>
<profiles>
<profile>
<id>release</id>
<build>
<plugins>
<plugin>
<groupId>org.example</groupId>
<artifactId>profile-plugin</artifactId>
<version>4.0.0</version>
</plugin>
</plugins>
<extensions>
<extension>
<groupId>org.example</groupId>
<artifactId>profile-extension</artifactId>
<version>5.0.0</version>
</extension>
</extensions>
</build>
</profile>
</profiles>
</project>`)

p, err := ParsePOM(src)
if err != nil {
t.Fatalf("ParsePOM: %v", err)
}
if len(p.Build.Plugins) != 1 || p.Build.Plugins[0].ArtifactID != "maven-compiler-plugin" {
t.Errorf("plugins not parsed: %+v", p.Build.Plugins)
}
if len(p.Build.Plugins[0].Dependencies) != 1 || p.Build.Plugins[0].Dependencies[0].ArtifactID != "plugin-runtime" {
t.Errorf("plugin dependencies not parsed: %+v", p.Build.Plugins[0].Dependencies)
}
if len(p.Build.PluginManagement.Plugins) != 1 || p.Build.PluginManagement.Plugins[0].ArtifactID != "managed-plugin" {
t.Errorf("pluginManagement not parsed: %+v", p.Build.PluginManagement)
}
if len(p.Build.Extensions) != 1 || p.Build.Extensions[0].ArtifactID != "extension" {
t.Errorf("extensions not parsed: %+v", p.Build.Extensions)
}
if len(p.Profiles) != 1 || len(p.Profiles[0].Build.Plugins) != 1 || p.Profiles[0].Build.Plugins[0].ArtifactID != "profile-plugin" {
t.Errorf("profile plugins not parsed: %+v", p.Profiles)
}
if len(p.Profiles[0].Build.Extensions) != 1 || p.Profiles[0].Build.Extensions[0].ArtifactID != "profile-extension" {
t.Errorf("profile extensions not parsed: %+v", p.Profiles[0].Build.Extensions)
}
}

func TestParseGAV(t *testing.T) {
tests := []struct {
in string
Expand Down