From 55c585d7a059492330b93bfc170c0c8f539551a9 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 15 Aug 2026 11:54:00 +0100 Subject: [PATCH 1/2] Parse Maven build coordinates --- parse.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++ pom.go | 34 +++++++++++++++++-- pom_test.go | 80 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 206 insertions(+), 2 deletions(-) diff --git a/parse.go b/parse.go index e01e4ec..6578ebb 100644 --- a/parse.go +++ b/parse.go @@ -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: @@ -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" { @@ -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() } diff --git a/pom.go b/pom.go index 7c36789..81eea14 100644 --- a/pom.go +++ b/pom.go @@ -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"` @@ -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"` } @@ -169,6 +171,33 @@ type Dep struct { Exclusions []Exclusion `xml:"exclusions>exclusion"` } +// Build is the coordinate-bearing subset of a project's block. +type Build struct { + Plugins []Plugin `xml:"plugins>plugin"` + PluginManagement PluginManagement `xml:"pluginManagement"` + Extensions []Extension `xml:"extensions>extension"` +} + +// PluginManagement wraps the plugins declared under . +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 entry under a dependency. type Exclusion struct { GroupID string `xml:"groupId"` @@ -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 relevant to static evaluation. diff --git a/pom_test.go b/pom_test.go index 117e40e..19c489e 100644 --- a/pom_test.go +++ b/pom_test.go @@ -89,6 +89,86 @@ func TestParsePOM(t *testing.T) { } } +func TestParsePOMBuildCoordinates(t *testing.T) { + src := []byte(` + + + + maven-compiler-plugin + 4.0.0 + + + org.example + plugin-runtime + 1.0.0 + + + + + + + + org.example + managed-plugin + 2.0.0 + + + + + + org.example + extension + 3.0.0 + + + + + + release + + + + org.example + profile-plugin + 4.0.0 + + + + + org.example + profile-extension + 5.0.0 + + + + + +`) + + 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 From 495ecf9732e158e0a963a8ffa995c9c907500298 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sat, 15 Aug 2026 12:04:42 +0100 Subject: [PATCH 2/2] Document parsed build coordinates --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d20726c..f2620c9 100644 --- a/README.md +++ b/README.md @@ -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 `` and ``, expand `import` 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 `` and ``, expand `import` 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 `${jackson.version}` is useless until something resolves the property. See [scrutineer#46](https://github.com/alpha-omega-security/scrutineer/issues/46). @@ -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, `` 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 `` 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