pom

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 13 Imported by: 0

README

pom

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.

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.

Install

go get github.com/git-pkgs/pom

Stdlib only, no transitive dependencies.

Usage

import "github.com/git-pkgs/pom"

fetcher := pom.NewCachingFetcher(pom.NewHTTPFetcher("")) // "" = Maven Central
r := pom.NewResolver(fetcher)

ep, err := r.Resolve(ctx, pom.GAV{
    GroupID:    "com.fasterxml.jackson.core",
    ArtifactID: "jackson-databind",
    Version:    "2.17.2",
}, pom.Options{})

for _, d := range ep.Dependencies {
    fmt.Printf("%s:%s:%s (%s) [%s]\n",
        d.GroupID, d.ArtifactID, d.Version, d.Scope, d.Resolution)
}

If you already have a pom.xml in hand (from a source checkout, say) use ResolvePOM:

p, _ := pom.ParsePOM(bytes)
ep, _ := r.ResolvePOM(ctx, p, pom.Options{})

CLI

pom is a small binary that wraps the resolver and prints JSON. It exists so non-Go callers can replace mvn help:effective-pom without a JVM.

go install github.com/git-pkgs/pom/cmd/pom@latest

Resolve by coordinate (fetches the root and its parent chain from the repository):

pom com.fasterxml.jackson.core:jackson-databind:2.17.2

Or feed POM bytes you already have on stdin, which is what you want when wrapping an existing HTTP fetch:

curl -fsSL https://repo1.maven.org/maven2/.../foo-1.0.pom | pom -f -

Output is one JSON object with gav, packaging, name, description, url, licenses, scm, relocation, parents, dependencies (each tagged with resolution), and warnings. Pass -relocate to follow <distributionManagement><relocation> and resolve the target instead, -repo URL for a non-Central repository, and -profiles pessimistic or -profiles id1,id2 to control profile activation.

On an M1 the compiled binary resolves jackson-databind (four parents plus a BOM import, 12 dependencies) in ~380 ms wall time of which essentially all is network round-trips to Central; CPU time is under a millisecond. The same artifact through mvn help:effective-pom is ~1.7 s and ~150 MB resident.

Fetchers

Resolver takes anything that satisfies one method:

type Fetcher interface {
    Fetch(ctx context.Context, gav GAV) (*POM, error)
}

Three implementations ship in the box. HTTPFetcher reads from a Maven repository layout. DirFetcher reads from a flat directory of groupId_artifactId_version.pom files and is what the offline tests use. CachingFetcher wraps another fetcher and memoises by GAV, which you almost always want since released coordinates are immutable and parent POMs are heavily shared across a corpus.

If you have your own storage (the proxy cache, an S3 bucket, whatever) implement Fetch and pass it in.

Resolution tags

Every ResolvedDep carries a Resolution field explaining how its version was (or wasn't) determined:

value meaning
resolved concrete version produced
unresolved_property a ${name} survived interpolation and nothing defines it
unresolved_env references ${env.X}, never resolvable statically
unresolved_parent a parent POM in the chain couldn't be fetched, so the result is suspect
unresolved_profile_gated the property is only defined inside a profile that wasn't activated
unresolved_missing no version anywhere reachable: not on the dep, not in dependencyManagement, not in any BOM

When a tag is unresolved, Expression holds the original ${...} string so callers can report it.

Profiles

Options.Profiles controls which <profile> sections contribute. OnlyDefault (the default) activates only <activeByDefault>true</activeByDefault>. Pessimistic activates everything, on the basis that for vuln scanning a false positive beats a false negative. Explicit takes a list of IDs.

pom.Options{Profiles: pom.ProfileActivation{Mode: pom.Pessimistic}}

Dependencies contributed by a profile carry Profile set to the profile ID so callers can attribute findings.

Testing against real Maven

testdata/poms/ holds 72 real POMs fetched from Maven Central (roots, parents, and BOMs) covering 31 artifacts: jackson, spring, junit, guava, log4j, netty, okhttp, kotlin-stdlib, hibernate, kafka, grpc, protobuf, micrometer, reactor, testcontainers, postgresql, logback, plus the reflections and modelmapper artifacts that triggered scrutineer#46. testdata/expected/ holds the dependency lists that mvn help:effective-pom produced for each root. TestGoldenAgainstMaven resolves every root offline through DirFetcher and diffs 262 dependencies against the expected output.

To regenerate or extend the corpus, edit the corpus slice in tools/refresh/main.go and run:

go run ./tools/refresh

This needs network access and mvn on PATH.

One known divergence: dependencies whose identity is OS-gated (netty's ${os.detected.classifier}, set by the os-maven-plugin extension and overridden by per-OS profiles) can't be resolved statically. Maven's own output for those varies by host. The golden test logs and tolerates them rather than failing.

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.

License

MIT, see LICENSE.

Documentation

Overview

Package pom computes a useful subset of Maven's effective POM in pure Go.

It resolves parent chains, merges properties and dependencyManagement, imports BOMs, applies profiles, and interpolates ${...} expressions so that callers receive concrete dependency requirements suitable for vulnerability matching and supply-chain analysis. It does not attempt plugin merging, lifecycle binding, or any build-time semantics.

Index

Constants

View Source
const DefaultRepoURL = "https://repo1.maven.org/maven2"

DefaultRepoURL is the canonical Maven Central repository.

Variables

View Source
var DefaultUserAgent = "git-pkgs-pom/" + Version + " (+https://github.com/git-pkgs/pom)"

DefaultUserAgent identifies this library to upstream repositories.

View Source
var Version = "dev"

Version is set via -ldflags at release time.

Functions

func FixtureName

func FixtureName(gav GAV) string

FixtureName returns the on-disk filename DirFetcher expects for gav.

func POMURL

func POMURL(base string, gav GAV) string

POMURL builds the repository URL for gav's POM under base.

Types

type Activation

type Activation struct {
	ActiveByDefault string `xml:"activeByDefault"`
	JDK             string `xml:"jdk"`
	OS              struct {
		Name   string `xml:"name"`
		Family string `xml:"family"`
		Arch   string `xml:"arch"`
	} `xml:"os"`
	Property struct {
		Name  string `xml:"name"`
		Value string `xml:"value"`
	} `xml:"property"`
}

Activation holds the parts of <activation> relevant to static evaluation.

type CachingFetcher

type CachingFetcher struct {
	Inner Fetcher
	// contains filtered or unexported fields
}

CachingFetcher wraps another Fetcher and memoises results by GAV. Safe for concurrent use.

func NewCachingFetcher

func NewCachingFetcher(inner Fetcher) *CachingFetcher

NewCachingFetcher wraps inner with an in-memory cache.

func (*CachingFetcher) Fetch

func (f *CachingFetcher) Fetch(ctx context.Context, gav GAV) (*POM, error)

type Dep

type Dep struct {
	GroupID    string      `xml:"groupId"`
	ArtifactID string      `xml:"artifactId"`
	Version    string      `xml:"version"`
	Type       string      `xml:"type"`
	Classifier string      `xml:"classifier"`
	Scope      string      `xml:"scope"`
	Optional   string      `xml:"optional"`
	Exclusions []Exclusion `xml:"exclusions>exclusion"`
}

Dep is a <dependency> entry. Values are raw and may contain ${...} expressions until interpolation runs.

func (Dep) GAV

func (d Dep) GAV() GAV

type DepMgmt

type DepMgmt struct {
	Dependencies []Dep `xml:"dependencies>dependency"`
}

DepMgmt wraps the <dependencyManagement> section.

type DirFetcher

type DirFetcher struct {
	Dir string
}

DirFetcher reads POMs from a flat directory of files named "<groupId>_<artifactId>_<version>.pom". Used by tests so resolution runs entirely offline.

func (*DirFetcher) Fetch

func (f *DirFetcher) Fetch(_ context.Context, gav GAV) (*POM, error)

type DistMgmt

type DistMgmt struct {
	Relocation *Relocation `xml:"relocation"`
}

DistMgmt is the subset of <distributionManagement> we care about.

type EffectivePOM

type EffectivePOM struct {
	GAV GAV

	Packaging   string
	Name        string
	Description string
	URL         string
	Licenses    []License
	SCM         SCM

	// Relocation is set when the root POM declares a
	// <distributionManagement><relocation>. Callers may want to follow it
	// and re-resolve.
	Relocation *Relocation

	// Properties is the merged property map after parent inheritance,
	// profile contribution, project.* synthesis, and self-interpolation.
	Properties map[string]string

	// Dependencies are the project's direct dependencies with versions
	// filled from dependencyManagement and interpolated.
	Dependencies []ResolvedDep

	// DependencyManagement is the merged managed-dependency table, after
	// BOM expansion, keyed by management key.
	DependencyManagement map[string]Dep

	// Parents lists the parent chain from immediate parent to root.
	Parents []GAV

	// ActiveProfiles lists profile IDs that contributed to the merge.
	ActiveProfiles []string

	// Warnings collects non-fatal issues encountered during resolution
	// (parent fetch failures, BOM fetch failures, depth limits).
	Warnings []string
}

EffectivePOM is the merged, interpolated view of a coordinate.

type Exclusion

type Exclusion struct {
	GroupID    string `xml:"groupId"`
	ArtifactID string `xml:"artifactId"`
}

Exclusion is a <exclusion> entry under a dependency.

type Fetcher

type Fetcher interface {
	Fetch(ctx context.Context, gav GAV) (*POM, error)
}

Fetcher retrieves a parsed POM for a coordinate. Implementations are expected to be safe for concurrent use; the resolver itself is synchronous but callers may share a Fetcher across goroutines.

type GAV

type GAV struct {
	GroupID    string
	ArtifactID string
	Version    string
}

GAV identifies a Maven artifact by group, artifact and version.

func ParseGAV

func ParseGAV(s string) (GAV, error)

ParseGAV parses "g:a:v" or "g:a" into a GAV.

func (GAV) GA

func (g GAV) GA() string

GA returns the group:artifact key without version, used for dependencyManagement lookups.

func (GAV) String

func (g GAV) String() string

type HTTPFetcher

type HTTPFetcher struct {
	BaseURL   string
	UserAgent string
	Client    *http.Client
}

HTTPFetcher fetches POMs from a Maven repository layout over HTTP.

func NewHTTPFetcher

func NewHTTPFetcher(baseURL string) *HTTPFetcher

NewHTTPFetcher returns a fetcher for baseURL, defaulting to Maven Central.

func (*HTTPFetcher) Fetch

func (f *HTTPFetcher) Fetch(ctx context.Context, gav GAV) (*POM, error)

func (*HTTPFetcher) FetchBytes

func (f *HTTPFetcher) FetchBytes(ctx context.Context, gav GAV) ([]byte, error)

FetchBytes returns the raw POM bytes. Exposed so the refresh tool can persist fixtures without re-serialising.

type License

type License struct {
	Name string `xml:"name"`
	URL  string `xml:"url"`
}

License is a <license> entry.

type Options

type Options struct {
	Profiles ProfileActivation
}

Options tunes a single Resolve call.

type POM

type POM struct {
	XMLName xml.Name `xml:"project"`

	GroupID    string `xml:"groupId"`
	ArtifactID string `xml:"artifactId"`
	Version    string `xml:"version"`
	Packaging  string `xml:"packaging"`

	Parent *Parent `xml:"parent"`

	Name        string    `xml:"name"`
	Description string    `xml:"description"`
	URL         string    `xml:"url"`
	Licenses    []License `xml:"licenses>license"`
	SCM         SCM       `xml:"scm"`

	DistributionManagement DistMgmt `xml:"distributionManagement"`

	Properties           Properties `xml:"properties"`
	Dependencies         []Dep      `xml:"dependencies>dependency"`
	DependencyManagement DepMgmt    `xml:"dependencyManagement"`

	Profiles []Profile `xml:"profiles>profile"`
}

POM is the parsed subset of a project object model that the resolver cares about. Fields are raw (uninterpolated) as read from XML.

func ParsePOM

func ParsePOM(data []byte) (*POM, error)

ParsePOM decodes a POM from XML bytes. It is lenient about charset declarations and strict-mode failures that would otherwise reject real-world POMs published to Maven Central.

func (*POM) EffectiveGAV

func (p *POM) EffectiveGAV() GAV

EffectiveGAV returns the POM's own coordinates, falling back to the parent's groupId/version when the child omits them.

type Parent

type Parent struct {
	GroupID    string `xml:"groupId"`
	ArtifactID string `xml:"artifactId"`
	Version    string `xml:"version"`
}

Parent is the <parent> coordinate reference.

func (*Parent) GAV

func (p *Parent) GAV() GAV

type Profile

type Profile struct {
	ID                   string     `xml:"id"`
	Activation           Activation `xml:"activation"`
	Properties           Properties `xml:"properties"`
	Dependencies         []Dep      `xml:"dependencies>dependency"`
	DependencyManagement DepMgmt    `xml:"dependencyManagement"`
}

Profile is the subset of <profile> needed for dependency resolution.

type ProfileActivation

type ProfileActivation struct {
	Mode ProfileMode
	IDs  []string
}

ProfileActivation configures profile selection for a Resolve call.

type ProfileMode

type ProfileMode int

ProfileMode controls which <profile> sections contribute to the merge.

const (
	// OnlyDefault activates only profiles with <activeByDefault>true</activeByDefault>.
	OnlyDefault ProfileMode = iota
	// Pessimistic activates every profile, on the basis that for vuln
	// scanning a false positive is preferable to a false negative.
	Pessimistic
	// Explicit activates only the named profile IDs (plus activeByDefault).
	Explicit
)

type Properties

type Properties map[string]string

Properties holds <properties> children as a key/value map. The XML element names are arbitrary so a custom unmarshaler is required.

func (*Properties) UnmarshalXML

func (p *Properties) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error

type Relocation

type Relocation struct {
	GroupID    string `xml:"groupId"`
	ArtifactID string `xml:"artifactId"`
	Version    string `xml:"version"`
	Message    string `xml:"message"`
}

Relocation is a <relocation> redirect to another coordinate.

func (*Relocation) Target

func (r *Relocation) Target(from GAV) GAV

Target returns the coordinate this relocation points to, filling any omitted parts from the relocating POM's own coordinates.

type Resolution

type Resolution string

Resolution classifies how (or whether) a dependency's version was determined. Missing-version and unresolved-property are the same underlying problem so they share this taxonomy.

const (
	// Resolved means a concrete version string was produced.
	Resolved Resolution = "resolved"
	// UnresolvedProperty means a ${name} expression remained after
	// interpolation and no source defines it.
	UnresolvedProperty Resolution = "unresolved_property"
	// UnresolvedEnv means the version references ${env.X} which can never
	// be resolved statically.
	UnresolvedEnv Resolution = "unresolved_env"
	// UnresolvedParent means a parent POM in the chain could not be
	// fetched, so the whole resolution is suspect.
	UnresolvedParent Resolution = "unresolved_parent"
	// UnresolvedProfileGated means the version is only defined inside a
	// profile that was not activated under the current mode.
	UnresolvedProfileGated Resolution = "unresolved_profile_gated"
	// UnresolvedMissing means no version was declared anywhere reachable:
	// not on the dependency, not in dependencyManagement, not in any BOM.
	UnresolvedMissing Resolution = "unresolved_missing"
)

type ResolvedDep

type ResolvedDep struct {
	GroupID    string
	ArtifactID string
	Version    string
	Type       string
	Classifier string
	Scope      string
	Optional   bool
	Exclusions []Exclusion

	Resolution Resolution
	// Expression holds the original unresolved ${...} string when
	// Resolution is one of the unresolved_* values.
	Expression string
	// Profile is set when this dependency was contributed by a profile.
	Profile string
}

ResolvedDep is a dependency after merge and interpolation, tagged with how its version was (or wasn't) determined.

func (ResolvedDep) GAV

func (d ResolvedDep) GAV() GAV

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver computes effective POMs. It owns a Fetcher and uses it for the root, every parent in the chain, and every imported BOM, so all I/O goes through one place.

func NewResolver

func NewResolver(f Fetcher) *Resolver

NewResolver constructs a Resolver around f. Resolved POMs are memoised for the lifetime of the Resolver since released coordinates are immutable.

func (*Resolver) Resolve

func (r *Resolver) Resolve(ctx context.Context, gav GAV, opts Options) (*EffectivePOM, error)

Resolve fetches gav and computes its effective POM under opts.

func (*Resolver) ResolvePOM

func (r *Resolver) ResolvePOM(ctx context.Context, root *POM, opts Options) (*EffectivePOM, error)

ResolvePOM computes the effective POM for an already-parsed root POM. Useful when the caller holds a pom.xml from a source checkout that is not itself fetchable by coordinate.

type SCM

type SCM struct {
	URL                 string `xml:"url"`
	Connection          string `xml:"connection"`
	DeveloperConnection string `xml:"developerConnection"`
}

SCM is the <scm> block.

Directories

Path Synopsis
cmd
pom command
Command pom resolves a Maven artifact's effective POM in pure Go and prints the result as JSON.
Command pom resolves a Maven artifact's effective POM in pure Go and prints the result as JSON.
tools
refresh command
Command refresh downloads a corpus of real POMs (including their full parent and BOM closure) into testdata/poms, then runs the real `mvn help:effective-pom` against each root and writes the resulting dependency list to testdata/expected as JSON.
Command refresh downloads a corpus of real POMs (including their full parent and BOM closure) into testdata/poms, then runs the real `mvn help:effective-pom` against each root and writes the resulting dependency list to testdata/expected as JSON.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL