feat: improved java maven property resolution (#2769)

Signed-off-by: Gijs Calis <51088038+GijsCalis@users.noreply.github.com>
Signed-off-by: Keith Zantow <kzantow@gmail.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Gijs Calis 2024-08-05 17:30:47 +02:00 committed by GitHub
parent cc15edca62
commit 9d40d1152e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 3013 additions and 1523 deletions

View File

@ -64,6 +64,7 @@ func DefaultCatalog() Catalog {
Package: defaultPackageConfig(),
LinuxKernel: defaultLinuxKernelConfig(),
Golang: defaultGolangConfig(),
Java: defaultJavaConfig(),
File: defaultFileConfig(),
Relationships: defaultRelationshipsConfig(),
Source: defaultSourceConfig(),
@ -150,6 +151,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
},
JavaArchive: java.DefaultArchiveCatalogerConfig().
WithUseMavenLocalRepository(cfg.Java.UseMavenLocalRepository).
WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir).
WithUseNetwork(cfg.Java.UseNetwork).
WithMavenBaseURL(cfg.Java.MavenURL).
WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth),

View File

@ -1,24 +1,46 @@
package options
import "github.com/anchore/clio"
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/java"
)
type javaConfig struct {
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
UseMavenLocalRepository bool `yaml:"use-maven-local-repository" json:"use-maven-local-repository" mapstructure:"use-maven-local-repository"`
MavenLocalRepositoryDir string `yaml:"maven-local-repository-dir" json:"maven-local-repository-dir" mapstructure:"maven-local-repository-dir"`
MavenURL string `yaml:"maven-url" json:"maven-url" mapstructure:"maven-url"`
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
}
func defaultJavaConfig() javaConfig {
def := java.DefaultArchiveCatalogerConfig()
return javaConfig{
UseNetwork: def.UseNetwork,
MaxParentRecursiveDepth: def.MaxParentRecursiveDepth,
UseMavenLocalRepository: def.UseMavenLocalRepository,
MavenLocalRepositoryDir: def.MavenLocalRepositoryDir,
MavenURL: def.MavenBaseURL,
}
}
var _ interface {
clio.FieldDescriber
} = (*javaConfig)(nil)
func (o *javaConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.UseNetwork, `enables Syft to use the network to fill in more detailed information about artifacts
currently this enables searching maven-url for license data
when running across pom.xml files that could have more information, syft will
explicitly search maven for license information by querying the online pom when this is true
this option is helpful for when the parent pom has more data,
that is not accessible from within the final built artifact`)
descriptions.Add(&o.UseNetwork, `enables Syft to use the network to fetch version and license information for packages when
a parent or imported pom file is not found in the local maven repository.
the pom files are downloaded from the remote Maven repository at 'maven-url'`)
descriptions.Add(&o.MavenURL, `maven repository to use, defaults to Maven central`)
descriptions.Add(&o.MaxParentRecursiveDepth, `depth to recursively resolve parent POMs`)
descriptions.Add(&o.MaxParentRecursiveDepth, `depth to recursively resolve parent POMs, no limit if <= 0`)
descriptions.Add(&o.UseMavenLocalRepository, `use the local Maven repository to retrieve pom files. When Maven is installed and was previously used
for building the software that is being scanned, then most pom files will be available in this
repository on the local file system. this greatly speeds up scans. when all pom files are available
in the local repository, then 'use-network' is not needed.
TIP: If you want to download all required pom files to the local repository without running a full
build, run 'mvn help:effective-pom' before performing the scan with syft.`)
descriptions.Add(&o.MavenLocalRepositoryDir, `override the default location of the local Maven repository.
the default is the subdirectory '.m2/repository' in your home directory`)
}

View File

@ -21,7 +21,7 @@ type LicenseEvidence struct {
func NewLicense(value string) License {
spdxExpression, err := license.ParseExpression(value)
if err != nil {
log.Trace("unable to parse license expression: %s, %w", value, err)
log.WithFields("error", err, "value", value).Trace("unable to parse license expression")
}
return License{

View File

@ -310,6 +310,11 @@ func TestFileParser(t *testing.T, fixturePath string, parser generic.Parser, exp
NewCatalogTester().FromFile(t, fixturePath).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser)
}
func TestCataloger(t *testing.T, fixtureDir string, cataloger pkg.Cataloger, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
t.Helper()
NewCatalogTester().FromDirectory(t, fixtureDir).Expects(expectedPkgs, expectedRelationships).TestCataloger(t, cataloger)
}
func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
t.Helper()

View File

@ -9,8 +9,10 @@ import (
"slices"
"strings"
"github.com/vifraa/gopom"
"golang.org/x/exp/maps"
"github.com/anchore/syft/internal"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log"
@ -52,6 +54,7 @@ type archiveParser struct {
fileInfo archiveFilename
detectNested bool
cfg ArchiveCatalogerConfig
maven *mavenResolver
}
type genericArchiveParserAdapter struct {
@ -106,6 +109,7 @@ func newJavaArchiveParser(reader file.LocationReadCloser, detectNested bool, cfg
fileInfo: newJavaArchiveFilename(currentFilepath),
detectNested: detectNested,
cfg: cfg,
maven: newMavenResolver(nil, cfg),
}, cleanupFn, nil
}
@ -197,7 +201,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
return nil, err
}
licenses, name, version, err := j.parseLicenses(ctx, manifest)
name, version, licenses, err := j.discoverNameVersionLicense(ctx, manifest)
if err != nil {
return nil, err
}
@ -220,7 +224,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
}, nil
}
func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaManifest) ([]pkg.License, string, string, error) {
func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, error) {
// we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest
// TODO: when we support locations of paths within archives we should start passing the specific manifest location object instead of the top jar
licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...)
@ -231,24 +235,18 @@ func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaMan
3. manifest
4. filename
*/
name, version, pomLicenses := j.guessMainPackageNameAndVersionFromPomInfo(ctx)
if name == "" {
name = selectName(manifest, j.fileInfo)
groupID, artifactID, version, parsedPom := j.discoverMainPackageFromPomInfo(ctx)
if artifactID == "" {
artifactID = selectName(manifest, j.fileInfo)
}
if version == "" {
version = selectVersion(manifest, j.fileInfo)
}
if len(licenses) == 0 {
// Today we don't have a way to distinguish between licenses from the manifest and licenses from the pom.xml
// until the file.Location object can support sub-paths (i.e. paths within archives, recursively; issue https://github.com/anchore/syft/issues/2211).
// Until then it's less confusing to use the licenses from the pom.xml only if the manifest did not list any.
licenses = append(licenses, pomLicenses...)
}
if len(licenses) == 0 {
fileLicenses, err := j.getLicenseFromFileInArchive()
if err != nil {
return nil, "", "", err
return "", "", nil, err
}
if fileLicenses != nil {
licenses = append(licenses, fileLicenses...)
@ -256,50 +254,73 @@ func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaMan
}
// If we didn't find any licenses in the archive so far, we'll try again in Maven Central using groupIDFromJavaMetadata
if len(licenses) == 0 && j.cfg.UseNetwork {
licenses = findLicenseFromJavaMetadata(ctx, name, manifest, version, j, licenses)
if len(licenses) == 0 {
// Today we don't have a way to distinguish between licenses from the manifest and licenses from the pom.xml
// until the file.Location object can support sub-paths (i.e. paths within archives, recursively; issue https://github.com/anchore/syft/issues/2211).
// Until then it's less confusing to use the licenses from the pom.xml only if the manifest did not list any.
licenses = j.findLicenseFromJavaMetadata(ctx, groupID, artifactID, version, parsedPom, manifest)
}
return licenses, name, version, nil
return artifactID, version, licenses, nil
}
func findLicenseFromJavaMetadata(ctx context.Context, name string, manifest *pkg.JavaManifest, version string, j *archiveParser, licenses []pkg.License) []pkg.License {
var groupID = name
if gID := groupIDFromJavaMetadata(name, pkg.JavaArchive{Manifest: manifest}); gID != "" {
groupID = gID
// findLicenseFromJavaMetadata attempts to find license information from all available maven metadata properties and pom info
func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID, artifactID, version string, parsedPom *parsedPomProject, manifest *pkg.JavaManifest) []pkg.License {
if groupID == "" {
if gID := groupIDFromJavaMetadata(artifactID, pkg.JavaArchive{Manifest: manifest}); gID != "" {
groupID = gID
}
}
var err error
var pomLicenses []gopom.License
if parsedPom != nil {
pomLicenses, err = j.maven.resolveLicenses(ctx, parsedPom.project)
if err != nil {
log.WithFields("error", err, "mavenID", j.maven.resolveMavenID(ctx, parsedPom.project)).Debug("error attempting to resolve pom licenses")
}
}
if err == nil && len(pomLicenses) == 0 {
pomLicenses, err = j.maven.findLicenses(ctx, groupID, artifactID, version)
if err != nil {
log.WithFields("error", err, "mavenID", mavenID{groupID, artifactID, version}).Debug("error attempting to find licenses")
}
}
pomLicenses := recursivelyFindLicensesFromParentPom(ctx, groupID, name, version, j.cfg)
if len(pomLicenses) == 0 {
// Try removing the last part of the groupId, as sometimes it duplicates the artifactId
packages := strings.Split(groupID, ".")
groupID = strings.Join(packages[:len(packages)-1], ".")
pomLicenses = recursivelyFindLicensesFromParentPom(ctx, groupID, name, version, j.cfg)
}
if len(pomLicenses) > 0 {
pkgLicenses := pkg.NewLicensesFromLocation(j.location, pomLicenses...)
if pkgLicenses != nil {
licenses = append(licenses, pkgLicenses...)
pomLicenses, err = j.maven.findLicenses(ctx, groupID, artifactID, version)
if err != nil {
log.WithFields("error", err, "mavenID", mavenID{groupID, artifactID, version}).Debug("error attempting to find sub-group licenses")
}
}
return licenses
return toPkgLicenses(&j.location, pomLicenses)
}
func toPkgLicenses(location *file.Location, licenses []gopom.License) []pkg.License {
var out []pkg.License
for _, license := range licenses {
out = append(out, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), location))
}
return out
}
type parsedPomProject struct {
*pkg.JavaPomProject
Licenses []pkg.License
path string
project *gopom.Project
}
func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo(ctx context.Context) (name, version string, licenses []pkg.License) {
pomPropertyMatches := j.fileManifest.GlobMatch(false, pomPropertiesGlob)
pomMatches := j.fileManifest.GlobMatch(false, pomXMLGlob)
var pomPropertiesObject pkg.JavaPomProperties
var pomProjectObject *parsedPomProject
// discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information
func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (group, name, version string, parsedPom *parsedPomProject) {
var pomProperties pkg.JavaPomProperties
// Find the pom.properties/pom.xml if the names seem like a plausible match
properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, pomPropertyMatches)
projects, _ := pomProjectByParentPath(j.archivePath, j.location, pomMatches)
properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomPropertiesGlob))
projects, _ := pomProjectByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomXMLGlob))
// map of all the artifacts in the pom properties, in order to chek exact match with the filename
artifactsMap := make(map[string]bool)
@ -312,41 +333,32 @@ func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo(ctx context.Co
for _, parentPath := range parentPaths {
propertiesObj := properties[parentPath]
if artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) {
pomPropertiesObject = propertiesObj
pomProperties = propertiesObj
if proj, exists := projects[parentPath]; exists {
pomProjectObject = proj
parsedPom = proj
break
}
}
}
name = pomPropertiesObject.ArtifactID
if name == "" && pomProjectObject != nil {
name = pomProjectObject.ArtifactID
}
version = pomPropertiesObject.Version
if version == "" && pomProjectObject != nil {
version = pomProjectObject.Version
}
if j.cfg.UseNetwork {
if pomProjectObject == nil {
// If we have no pom.xml, check maven central using pom.properties
parentLicenses := recursivelyFindLicensesFromParentPom(ctx, pomPropertiesObject.GroupID, pomPropertiesObject.ArtifactID, pomPropertiesObject.Version, j.cfg)
if len(parentLicenses) > 0 {
for _, licenseName := range parentLicenses {
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
}
}
} else {
findPomLicenses(ctx, pomProjectObject, j.cfg)
group = pomProperties.GroupID
name = pomProperties.ArtifactID
version = pomProperties.Version
if parsedPom != nil && parsedPom.project != nil {
id := j.maven.resolveMavenID(ctx, parsedPom.project)
if group == "" {
group = id.GroupID
}
if name == "" {
name = id.ArtifactID
}
if version == "" {
version = id.Version
}
}
if pomProjectObject != nil {
licenses = pomProjectObject.Licenses
}
return name, version, licenses
return group, name, version, parsedPom
}
func artifactIDMatchesFilename(artifactID, fileName string, artifactsMap map[string]bool) bool {
@ -361,24 +373,6 @@ func artifactIDMatchesFilename(artifactID, fileName string, artifactsMap map[str
return strings.HasPrefix(artifactID, fileName) || strings.HasSuffix(fileName, artifactID)
}
func findPomLicenses(ctx context.Context, pomProjectObject *parsedPomProject, cfg ArchiveCatalogerConfig) {
// If we don't have any licenses until now, and if we have a parent Pom, then we'll check the parent pom in maven central for licenses.
if pomProjectObject != nil && pomProjectObject.Parent != nil && len(pomProjectObject.Licenses) == 0 {
parentLicenses := recursivelyFindLicensesFromParentPom(
ctx,
pomProjectObject.Parent.GroupID,
pomProjectObject.Parent.ArtifactID,
pomProjectObject.Parent.Version,
cfg)
if len(parentLicenses) > 0 {
for _, licenseName := range parentLicenses {
pomProjectObject.Licenses = append(pomProjectObject.Licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
}
}
}
}
// discoverPkgsFromAllMavenFiles parses Maven POM properties/xml for a given
// parent package, returning all listed Java packages found for each pom
// properties discovered and potentially updating the given parentPkg with new
@ -403,12 +397,12 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(ctx context.Context, paren
}
for parentPath, propertiesObj := range properties {
var pomProject *parsedPomProject
var parsedPom *parsedPomProject
if proj, exists := projects[parentPath]; exists {
pomProject = proj
parsedPom = proj
}
pkgFromPom := newPackageFromMavenData(ctx, propertiesObj, pomProject, parentPkg, j.location, j.cfg)
pkgFromPom := newPackageFromMavenData(ctx, j.maven, propertiesObj, parsedPom, parentPkg, j.location)
if pkgFromPom != nil {
pkgs = append(pkgs, *pkgFromPom)
}
@ -422,7 +416,7 @@ func getDigestsFromArchive(archivePath string) ([]file.Digest, error) {
if err != nil {
return nil, fmt.Errorf("unable to open archive path (%s): %w", archivePath, err)
}
defer archiveCloser.Close()
defer internal.CloseAndLogError(archiveCloser, archivePath)
// grab and assign digest for the entire archive
digests, err := intFile.NewDigestsFromFile(archiveCloser, javaArchiveHashes)
@ -576,30 +570,26 @@ func pomProjectByParentPath(archivePath string, location file.Location, extractP
projectByParentPath := make(map[string]*parsedPomProject)
for filePath, fileContents := range contentsOfMavenProjectFiles {
// TODO: when we support locations of paths within archives we should start passing the specific pom.xml location object instead of the top jar
pomProject, err := parsePomXMLProject(filePath, strings.NewReader(fileContents), location)
pom, err := decodePomXML(strings.NewReader(fileContents))
if err != nil {
log.WithFields("contents-path", filePath, "location", location.Path()).Warnf("failed to parse pom.xml: %+v", err)
continue
}
if pomProject == nil {
if pom == nil {
continue
}
// If we don't have a version, then maybe the parent pom has it...
if (pomProject.Parent == nil && pomProject.Version == "") || pomProject.ArtifactID == "" {
// TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package
continue
projectByParentPath[path.Dir(filePath)] = &parsedPomProject{
path: filePath,
project: pom,
}
projectByParentPath[path.Dir(filePath)] = pomProject
}
return projectByParentPath, nil
}
// newPackageFromMavenData processes a single Maven POM properties for a given parent package, returning all listed Java packages found and
// associating each discovered package to the given parent package. Note the pom.xml is optional, the pom.properties is not.
func newPackageFromMavenData(ctx context.Context, pomProperties pkg.JavaPomProperties, parsedPomProject *parsedPomProject, parentPkg *pkg.Package, location file.Location, cfg ArchiveCatalogerConfig) *pkg.Package {
func newPackageFromMavenData(ctx context.Context, r *mavenResolver, pomProperties pkg.JavaPomProperties, parsedPom *parsedPomProject, parentPkg *pkg.Package, location file.Location) *pkg.Package {
// keep the artifact name within the virtual path if this package does not match the parent package
vPathSuffix := ""
groupID := ""
@ -622,25 +612,24 @@ func newPackageFromMavenData(ctx context.Context, pomProperties pkg.JavaPomPrope
virtualPath := location.Path() + vPathSuffix
var pkgPomProject *pkg.JavaPomProject
licenses := make([]pkg.License, 0)
if cfg.UseNetwork {
if parsedPomProject == nil {
// If we have no pom.xml, check maven central using pom.properties
parentLicenses := recursivelyFindLicensesFromParentPom(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version, cfg)
if len(parentLicenses) > 0 {
for _, licenseName := range parentLicenses {
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
}
}
} else {
findPomLicenses(ctx, parsedPomProject, cfg)
}
var err error
var pomLicenses []gopom.License
if parsedPom == nil {
// If we have no pom.xml, check maven central using pom.properties
pomLicenses, err = r.findLicenses(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version)
} else {
pkgPomProject = newPomProject(ctx, r, parsedPom.path, parsedPom.project)
pomLicenses, err = r.resolveLicenses(ctx, parsedPom.project)
}
if parsedPomProject != nil {
pkgPomProject = parsedPomProject.JavaPomProject
licenses = append(licenses, parsedPomProject.Licenses...)
if err != nil {
log.WithFields("error", err, "mavenID", mavenID{pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version}).Debug("error attempting to resolve licenses")
}
licenses := make([]pkg.License, 0)
for _, license := range pomLicenses {
licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), &location))
}
p := pkg.Package{

View File

@ -5,8 +5,6 @@ import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
@ -20,6 +18,7 @@ import (
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vifraa/gopom"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@ -28,61 +27,14 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func generateJavaBuildFixture(t *testing.T, fixturePath string) {
if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
// fixture already exists...
return
}
makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/")
t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("make", makeTask)
cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/")
run(t, cmd)
}
func generateMockMavenHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
// Set the Content-Type header to indicate that the response is XML
w.Header().Set("Content-Type", "application/xml")
// Copy the file's content to the response writer
file, err := os.Open(responseFixture)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer file.Close()
_, err = io.Copy(w, file)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}
type handlerPath struct {
path string
handler func(w http.ResponseWriter, r *http.Request)
}
func TestSearchMavenForLicenses(t *testing.T) {
mux, url, teardown := setup()
defer teardown()
url := mockMavenRepo(t)
tests := []struct {
name string
fixture string
detectNested bool
config ArchiveCatalogerConfig
requestPath string
requestHandlers []handlerPath
expectedLicenses []pkg.License
}{
{
@ -91,23 +43,16 @@ func TestSearchMavenForLicenses(t *testing.T) {
detectNested: false,
config: ArchiveCatalogerConfig{
UseNetwork: true,
UseMavenLocalRepository: false,
MavenBaseURL: url,
MaxParentRecursiveDepth: 2,
},
requestHandlers: []handlerPath{
{
path: "/org/opensaml/opensaml-parent/3.4.6/opensaml-parent-3.4.6.pom",
handler: generateMockMavenHandler("test-fixtures/maven-xml-responses/opensaml-parent-3.4.6.pom"),
},
{
path: "/net/shibboleth/parent/7.11.2/parent-7.11.2.pom",
handler: generateMockMavenHandler("test-fixtures/maven-xml-responses/parent-7.11.2.pom"),
},
},
expectedLicenses: []pkg.License{
{
Type: license.Declared,
Value: `The Apache Software License, Version 2.0`,
Type: license.Declared,
Value: `The Apache Software License, Version 2.0`,
URLs: []string{
"http://www.apache.org/licenses/LICENSE-2.0.txt",
},
SPDXExpression: ``,
},
},
@ -116,11 +61,6 @@ func TestSearchMavenForLicenses(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// configure maven central requests
for _, hdlr := range tc.requestHandlers {
mux.HandleFunc(hdlr.path, hdlr.handler)
}
// setup metadata fixture; note:
// this fixture has a pomProjectObject and has a parent object
// it has no licenses on either which is the condition for testing
@ -138,34 +78,9 @@ func TestSearchMavenForLicenses(t *testing.T) {
defer cleanupFn()
// assert licenses are discovered from upstream
_, _, licenses := ap.guessMainPackageNameAndVersionFromPomInfo(context.Background())
assert.Equal(t, tc.expectedLicenses, licenses)
})
}
}
func TestFormatMavenURL(t *testing.T) {
tests := []struct {
name string
groupID string
artifactID string
version string
expected string
}{
{
name: "formatMavenURL correctly assembles the pom URL",
groupID: "org.springframework.boot",
artifactID: "spring-boot-starter-test",
version: "3.1.5",
expected: "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-test/3.1.5/spring-boot-starter-test-3.1.5.pom",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
requestURL, err := formatMavenPomURL(tc.groupID, tc.artifactID, tc.version, mavenBaseURL)
assert.NoError(t, err, "expected no err; got %w", err)
assert.Equal(t, tc.expected, requestURL)
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
licenses, _ := ap.maven.resolveLicenses(context.Background(), parsedPom.project)
assert.Equal(t, tc.expectedLicenses, toPkgLicenses(nil, licenses))
})
}
}
@ -424,10 +339,14 @@ func TestParseJar(t *testing.T) {
test.expected[k] = p
}
cfg := ArchiveCatalogerConfig{
UseNetwork: false,
UseMavenLocalRepository: false,
}
parser, cleanupFn, err := newJavaArchiveParser(file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture,
}, false, ArchiveCatalogerConfig{UseNetwork: false})
}, false, cfg)
defer cleanupFn()
require.NoError(t, err)
@ -843,26 +762,23 @@ func Test_newPackageFromMavenData(t *testing.T) {
Version: "1.0",
},
project: &parsedPomProject{
JavaPomProject: &pkg.JavaPomProject{
Parent: &pkg.JavaPomParent{
GroupID: "some-parent-group-id",
ArtifactID: "some-parent-artifact-id",
Version: "1.0-parent",
project: &gopom.Project{
Parent: &gopom.Parent{
GroupID: ptr("some-parent-group-id"),
ArtifactID: ptr("some-parent-artifact-id"),
Version: ptr("1.0-parent"),
},
Name: "some-name",
GroupID: "some-group-id",
ArtifactID: "some-artifact-id",
Version: "1.0",
Description: "desc",
URL: "aweso.me",
},
Licenses: []pkg.License{
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
URLs: []string{"https://opensource.org/licenses/MIT"},
Locations: file.NewLocationSet(file.NewLocation("some-license-path")),
Name: ptr("some-name"),
GroupID: ptr("some-group-id"),
ArtifactID: ptr("some-artifact-id"),
Version: ptr("1.0"),
Description: ptr("desc"),
URL: ptr("aweso.me"),
Licenses: &[]gopom.License{
{
Name: ptr("MIT"),
URL: ptr("https://opensource.org/licenses/MIT"),
},
},
},
},
@ -898,7 +814,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
SPDXExpression: "MIT",
Type: license.Declared,
URLs: []string{"https://opensource.org/licenses/MIT"},
Locations: file.NewLocationSet(file.NewLocation("some-license-path")),
Locations: file.NewLocationSet(file.NewLocation("given/virtual/path")),
},
),
Metadata: pkg.JavaArchive{
@ -1122,7 +1038,8 @@ func Test_newPackageFromMavenData(t *testing.T) {
}
test.expectedParent.Locations = locations
actualPackage := newPackageFromMavenData(context.Background(), test.props, test.project, test.parent, file.NewLocation(virtualPath), DefaultArchiveCatalogerConfig())
r := newMavenResolver(nil, DefaultArchiveCatalogerConfig())
actualPackage := newPackageFromMavenData(context.Background(), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
if test.expectedPackage == nil {
require.Nil(t, actualPackage)
} else {
@ -1337,6 +1254,8 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
PomProject: &pkg.JavaPomProject{
Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml",
ArtifactID: "api-asn1-api",
GroupID: "org.apache.directory.api",
Version: "2.0.0",
Name: "Apache Directory API ASN.1 API",
Description: "ASN.1 API",
Parent: &pkg.JavaPomParent{
@ -1388,14 +1307,12 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
func Test_deterministicMatchingPomProperties(t *testing.T) {
tests := []struct {
fixture string
expectedName string
expectedVersion string
fixture string
expected mavenID
}{
{
fixture: "multiple-matching-2.11.5",
expectedName: "multiple-matching-1",
expectedVersion: "2.11.5",
fixture: "multiple-matching-2.11.5",
expected: mavenID{"org.multiple", "multiple-matching-1", "2.11.5"},
},
}
@ -1415,9 +1332,8 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
defer cleanupFn()
require.NoError(t, err)
name, version, _ := parser.guessMainPackageNameAndVersionFromPomInfo(context.TODO())
require.Equal(t, test.expectedName, name)
require.Equal(t, test.expectedVersion, version)
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO())
require.Equal(t, test.expected, mavenID{groupID, artifactID, version})
}()
}
})
@ -1436,6 +1352,26 @@ func assignParent(parent *pkg.Package, childPackages ...pkg.Package) {
}
}
func generateJavaBuildFixture(t *testing.T, fixturePath string) {
if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
// fixture already exists...
return
}
makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/")
t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask))
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("make", makeTask)
cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/")
run(t, cmd)
}
func generateJavaMetadataJarFixture(t *testing.T, fixtureName string) string {
fixturePath := filepath.Join("test-fixtures/jar-metadata/cache/", fixtureName+".jar")
if _, err := os.Stat(fixturePath); !os.IsNotExist(err) {
@ -1504,20 +1440,7 @@ func run(t testing.TB, cmd *exec.Cmd) {
}
}
// setup sets up a test HTTP server for mocking requests to maven central.
// The returned url is injected into the Config so the client uses the test server.
// Tests should register handlers on mux to simulate the expected request/response structure
func setup() (mux *http.ServeMux, serverURL string, teardown func()) {
// mux is the HTTP request multiplexer used with the test server.
mux = http.NewServeMux()
// We want to ensure that tests catch mistakes where the endpoint URL is
// specified as absolute rather than relative. It only makes a difference
// when there's a non-empty base URL path. So, use that. See issue #752.
apiHandler := http.NewServeMux()
apiHandler.Handle("/", mux)
// server is a test HTTP server used to provide mock API responses.
server := httptest.NewServer(apiHandler)
return mux, server.URL, server.Close
// ptr returns a pointer to the given value
func ptr[T any](value T) *T {
return &value
}

View File

@ -32,10 +32,9 @@ func NewArchiveCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
// NewPomCataloger returns a cataloger capable of parsing dependencies from a pom.xml file.
// Pom files list dependencies that maybe not be locally installed yet.
func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
gap := newGenericArchiveParserAdapter(cfg)
return generic.NewCataloger("java-pom-cataloger").
WithParserByGlobs(gap.parserPomXML, "**/pom.xml")
return pomXMLCataloger{
cfg: cfg,
}
}
// NewGradleLockfileCataloger returns a cataloger capable of parsing dependencies from a gradle.lockfile file.

View File

@ -7,6 +7,8 @@ const mavenBaseURL = "https://repo1.maven.org/maven2"
type ArchiveCatalogerConfig struct {
cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"`
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
UseMavenLocalRepository bool `yaml:"use-maven-localrepository" json:"use-maven-localrepository" mapstructure:"use-maven-localrepository"`
MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"`
MavenBaseURL string `yaml:"maven-base-url" json:"maven-base-url" mapstructure:"maven-base-url"`
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
}
@ -15,8 +17,10 @@ func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig {
return ArchiveCatalogerConfig{
ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(),
UseNetwork: false,
UseMavenLocalRepository: false,
MavenLocalRepositoryDir: defaultMavenLocalRepoDir(),
MavenBaseURL: mavenBaseURL,
MaxParentRecursiveDepth: 5,
MaxParentRecursiveDepth: 0, // unlimited
}
}
@ -25,6 +29,16 @@ func (j ArchiveCatalogerConfig) WithUseNetwork(input bool) ArchiveCatalogerConfi
return j
}
func (j ArchiveCatalogerConfig) WithUseMavenLocalRepository(input bool) ArchiveCatalogerConfig {
j.UseMavenLocalRepository = input
return j
}
func (j ArchiveCatalogerConfig) WithMavenLocalRepositoryDir(input string) ArchiveCatalogerConfig {
j.MavenLocalRepositoryDir = input
return j
}
func (j ArchiveCatalogerConfig) WithMavenBaseURL(input string) ArchiveCatalogerConfig {
if input != "" {
j.MavenBaseURL = input
@ -33,9 +47,7 @@ func (j ArchiveCatalogerConfig) WithMavenBaseURL(input string) ArchiveCatalogerC
}
func (j ArchiveCatalogerConfig) WithArchiveTraversal(search cataloging.ArchiveSearchConfig, maxDepth int) ArchiveCatalogerConfig {
if maxDepth > 0 {
j.MaxParentRecursiveDepth = maxDepth
}
j.MaxParentRecursiveDepth = maxDepth
j.ArchiveSearchConfig = search
return j
}

View File

@ -1,136 +0,0 @@
package java
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/vifraa/gopom"
"github.com/anchore/syft/internal/log"
)
func formatMavenPomURL(groupID, artifactID, version, mavenBaseURL string) (requestURL string, err error) {
// groupID needs to go from maven.org -> maven/org
urlPath := strings.Split(groupID, ".")
artifactPom := fmt.Sprintf("%s-%s.pom", artifactID, version)
urlPath = append(urlPath, artifactID, version, artifactPom)
// ex:"https://repo1.maven.org/maven2/groupID/artifactID/artifactPom
requestURL, err = url.JoinPath(mavenBaseURL, urlPath...)
if err != nil {
return requestURL, fmt.Errorf("could not construct maven url: %w", err)
}
return requestURL, err
}
// An artifact can have its version defined in a parent's DependencyManagement section
func recursivelyFindVersionFromParentPom(ctx context.Context, groupID, artifactID, parentGroupID, parentArtifactID, parentVersion string, cfg ArchiveCatalogerConfig) string {
// As there can be nested parent poms, we'll recursively check for the version until we reach the max depth
for i := 0; i < cfg.MaxParentRecursiveDepth; i++ {
parentPom, err := getPomFromMavenRepo(ctx, parentGroupID, parentArtifactID, parentVersion, cfg.MavenBaseURL)
if err != nil {
// We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error
log.Tracef("unable to get parent pom from Maven central: %v", err)
break
}
if parentPom != nil && parentPom.DependencyManagement != nil {
for _, dependency := range *parentPom.DependencyManagement.Dependencies {
if groupID == *dependency.GroupID && artifactID == *dependency.ArtifactID && dependency.Version != nil {
return *dependency.Version
}
}
}
if parentPom == nil || parentPom.Parent == nil {
break
}
parentGroupID = *parentPom.Parent.GroupID
parentArtifactID = *parentPom.Parent.ArtifactID
parentVersion = *parentPom.Parent.Version
}
return ""
}
func recursivelyFindLicensesFromParentPom(ctx context.Context, groupID, artifactID, version string, cfg ArchiveCatalogerConfig) []string {
var licenses []string
// As there can be nested parent poms, we'll recursively check for licenses until we reach the max depth
for i := 0; i < cfg.MaxParentRecursiveDepth; i++ {
parentPom, err := getPomFromMavenRepo(ctx, groupID, artifactID, version, cfg.MavenBaseURL)
if err != nil {
// We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error
log.Tracef("unable to get parent pom from Maven central: %v", err)
return []string{}
}
parentLicenses := parseLicensesFromPom(parentPom)
if len(parentLicenses) > 0 || parentPom == nil || parentPom.Parent == nil {
licenses = parentLicenses
break
}
groupID = *parentPom.Parent.GroupID
artifactID = *parentPom.Parent.ArtifactID
version = *parentPom.Parent.Version
}
return licenses
}
func getPomFromMavenRepo(ctx context.Context, groupID, artifactID, version, mavenBaseURL string) (*gopom.Project, error) {
requestURL, err := formatMavenPomURL(groupID, artifactID, version, mavenBaseURL)
if err != nil {
return nil, err
}
log.Tracef("trying to fetch parent pom from Maven central %s", requestURL)
mavenRequest, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to format request for Maven central: %w", err)
}
httpClient := &http.Client{
Timeout: time.Second * 10,
}
mavenRequest = mavenRequest.WithContext(ctx)
resp, err := httpClient.Do(mavenRequest)
if err != nil {
return nil, fmt.Errorf("unable to get pom from Maven central: %w", err)
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Errorf("unable to close body: %+v", err)
}
}()
bytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err)
}
pom, err := decodePomXML(strings.NewReader(string(bytes)))
if err != nil {
return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err)
}
return &pom, nil
}
func parseLicensesFromPom(pom *gopom.Project) []string {
var licenses []string
if pom != nil && pom.Licenses != nil {
for _, license := range *pom.Licenses {
if license.Name != nil {
licenses = append(licenses, *license.Name)
} else if license.URL != nil {
licenses = append(licenses, *license.URL)
}
}
}
return licenses
}

View File

@ -0,0 +1,624 @@
package java
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"reflect"
"regexp"
"slices"
"strings"
"time"
"github.com/vifraa/gopom"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/cache"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
)
// mavenID is the unique identifier for a package in Maven
type mavenID struct {
GroupID string
ArtifactID string
Version string
}
func (m mavenID) String() string {
return fmt.Sprintf("(groupId: %s artifactId: %s version: %s)", m.GroupID, m.ArtifactID, m.Version)
}
var expressionMatcher = regexp.MustCompile("[$][{][^}]+[}]")
// mavenResolver is a short-lived utility to resolve maven poms from multiple sources, including:
// the scanned filesystem, local maven cache directories, remote maven repositories, and the syft cache
type mavenResolver struct {
cfg ArchiveCatalogerConfig
cache cache.Cache
resolved map[mavenID]*gopom.Project
remoteRequestTimeout time.Duration
checkedLocalRepo bool
// fileResolver and pomLocations are used to resolve parent poms by relativePath
fileResolver file.Resolver
pomLocations map[*gopom.Project]file.Location
}
// newMavenResolver constructs a new mavenResolver with the given configuration.
// NOTE: the fileResolver is optional and if provided will be used to resolve parent poms by relative path
func newMavenResolver(fileResolver file.Resolver, cfg ArchiveCatalogerConfig) *mavenResolver {
return &mavenResolver{
cfg: cfg,
cache: cache.GetManager().GetCache("java/maven/repo", "v1"),
resolved: map[mavenID]*gopom.Project{},
remoteRequestTimeout: time.Second * 10,
fileResolver: fileResolver,
pomLocations: map[*gopom.Project]file.Location{},
}
}
// getPropertyValue gets property values by emulating maven property resolution logic, looking in the project's variables
// as well as supporting the project expressions like ${project.parent.groupId}.
// Properties which are not resolved result in empty string ""
func (r *mavenResolver) getPropertyValue(ctx context.Context, propertyValue *string, resolutionContext ...*gopom.Project) string {
if propertyValue == nil {
return ""
}
resolved, err := r.resolveExpression(ctx, resolutionContext, *propertyValue, nil)
if err != nil {
log.WithFields("error", err, "propertyValue", *propertyValue).Debug("error resolving maven property")
return ""
}
return resolved
}
// resolveExpression resolves an expression, which may be a plain string or a string with ${ property.references }
func (r *mavenResolver) resolveExpression(ctx context.Context, resolutionContext []*gopom.Project, expression string, resolving []string) (string, error) {
var err error
return expressionMatcher.ReplaceAllStringFunc(expression, func(match string) string {
propertyExpression := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing }
resolved, e := r.resolveProperty(ctx, resolutionContext, propertyExpression, resolving)
if e != nil {
err = errors.Join(err, e)
return ""
}
return resolved
}), err
}
// resolveProperty resolves properties recursively from the root project
func (r *mavenResolver) resolveProperty(ctx context.Context, resolutionContext []*gopom.Project, propertyExpression string, resolving []string) (string, error) {
// prevent cycles
if slices.Contains(resolving, propertyExpression) {
return "", fmt.Errorf("cycle detected resolving: %s", propertyExpression)
}
if len(resolutionContext) == 0 {
return "", fmt.Errorf("no project variable resolution context provided for expression: '%s'", propertyExpression)
}
resolving = append(resolving, propertyExpression)
// only resolve project. properties in the context of the current project pom
value, err := r.resolveProjectProperty(ctx, resolutionContext, resolutionContext[len(resolutionContext)-1], propertyExpression, resolving)
if err != nil {
return value, err
}
if value != "" {
return value, nil
}
for _, pom := range resolutionContext {
current := pom
for parentDepth := 0; current != nil; parentDepth++ {
if r.cfg.MaxParentRecursiveDepth > 0 && parentDepth > r.cfg.MaxParentRecursiveDepth {
return "", fmt.Errorf("maximum parent recursive depth (%v) reached resolving property: %v", r.cfg.MaxParentRecursiveDepth, propertyExpression)
}
if current.Properties != nil && current.Properties.Entries != nil {
if value, ok := current.Properties.Entries[propertyExpression]; ok {
return r.resolveExpression(ctx, resolutionContext, value, resolving) // property values can contain expressions
}
}
current, err = r.resolveParent(ctx, current)
if err != nil {
return "", err
}
}
}
return "", fmt.Errorf("unable to resolve property: %s", propertyExpression)
}
// resolveProjectProperty resolves properties on the project
//
//nolint:gocognit
func (r *mavenResolver) resolveProjectProperty(ctx context.Context, resolutionContext []*gopom.Project, pom *gopom.Project, propertyExpression string, resolving []string) (string, error) {
// see if we have a project.x expression and process this based
// on the xml tags in gopom
parts := strings.Split(propertyExpression, ".")
numParts := len(parts)
if numParts > 1 && strings.TrimSpace(parts[0]) == "project" {
pomValue := reflect.ValueOf(pom).Elem()
pomValueType := pomValue.Type()
for partNum := 1; partNum < numParts; partNum++ {
if pomValueType.Kind() != reflect.Struct {
break
}
part := parts[partNum]
// these two fields are directly inherited from the pom parent values
if partNum == 1 && pom.Parent != nil {
switch part {
case "version":
if pom.Version == nil && pom.Parent.Version != nil {
return r.resolveExpression(ctx, resolutionContext, *pom.Parent.Version, resolving)
}
case "groupID":
if pom.GroupID == nil && pom.Parent.GroupID != nil {
return r.resolveExpression(ctx, resolutionContext, *pom.Parent.GroupID, resolving)
}
}
}
for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ {
f := pomValueType.Field(fieldNum)
tag := f.Tag.Get("xml")
tag = strings.Split(tag, ",")[0]
// a segment of the property name matches the xml tag for the field,
// so we need to recurse down the nested structs or return a match
// if we're done.
if part != tag {
continue
}
pomValue = pomValue.Field(fieldNum)
pomValueType = pomValue.Type()
if pomValueType.Kind() == reflect.Ptr {
// we were recursing down the nested structs, but one of the steps
// we need to take is a nil pointer, so give up
if pomValue.IsNil() {
return "", fmt.Errorf("property undefined: %s", propertyExpression)
}
pomValue = pomValue.Elem()
if !pomValue.IsZero() {
// we found a non-zero value whose tag matches this part of the property name
pomValueType = pomValue.Type()
}
}
// If this was the last part of the property name, return the value
if partNum == numParts-1 {
value := fmt.Sprintf("%v", pomValue.Interface())
return r.resolveExpression(ctx, resolutionContext, value, resolving)
}
break
}
}
}
return "", nil
}
// resolveMavenID creates a new mavenID from a pom, resolving parent information as necessary
func (r *mavenResolver) resolveMavenID(ctx context.Context, pom *gopom.Project) mavenID {
if pom == nil {
return mavenID{}
}
groupID := r.getPropertyValue(ctx, pom.GroupID, pom)
artifactID := r.getPropertyValue(ctx, pom.ArtifactID, pom)
version := r.getPropertyValue(ctx, pom.Version, pom)
if pom.Parent != nil {
if groupID == "" {
groupID = r.getPropertyValue(ctx, pom.Parent.GroupID, pom)
}
if artifactID == "" {
artifactID = r.getPropertyValue(ctx, pom.Parent.ArtifactID, pom)
}
if version == "" {
version = r.getPropertyValue(ctx, pom.Parent.Version, pom)
}
}
return mavenID{groupID, artifactID, version}
}
// resolveDependencyID creates a new mavenID from a dependency element in a pom, resolving information as necessary
func (r *mavenResolver) resolveDependencyID(ctx context.Context, pom *gopom.Project, dep gopom.Dependency) mavenID {
if pom == nil {
return mavenID{}
}
groupID := r.getPropertyValue(ctx, dep.GroupID, pom)
artifactID := r.getPropertyValue(ctx, dep.ArtifactID, pom)
version := r.getPropertyValue(ctx, dep.Version, pom)
var err error
if version == "" {
version, err = r.findInheritedVersion(ctx, pom, groupID, artifactID)
}
depID := mavenID{groupID, artifactID, version}
if err != nil {
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "dependencyID", depID)
}
return depID
}
// findPom gets a pom from cache, local repository, or from a remote Maven repository depending on configuration
func (r *mavenResolver) findPom(ctx context.Context, groupID, artifactID, version string) (*gopom.Project, error) {
if groupID == "" || artifactID == "" || version == "" {
return nil, fmt.Errorf("invalid maven pom specification, require non-empty values for groupID: '%s', artifactID: '%s', version: '%s'", groupID, artifactID, version)
}
id := mavenID{groupID, artifactID, version}
pom := r.resolved[id]
if pom != nil {
return pom, nil
}
var errs error
// try to resolve first from local maven repo
if r.cfg.UseMavenLocalRepository {
pom, err := r.findPomInLocalRepository(groupID, artifactID, version)
if pom != nil {
r.resolved[id] = pom
return pom, nil
}
errs = errors.Join(errs, err)
}
// resolve via network maven repository
if pom == nil && r.cfg.UseNetwork {
pom, err := r.findPomInRemoteRepository(ctx, groupID, artifactID, version)
if pom != nil {
r.resolved[id] = pom
return pom, nil
}
errs = errors.Join(errs, err)
}
return nil, fmt.Errorf("unable to resolve pom %s %s %s: %w", groupID, artifactID, version, errs)
}
// findPomInLocalRepository attempts to get the POM from the users local maven repository
func (r *mavenResolver) findPomInLocalRepository(groupID, artifactID, version string) (*gopom.Project, error) {
groupPath := filepath.Join(strings.Split(groupID, ".")...)
pomFilePath := filepath.Join(r.cfg.MavenLocalRepositoryDir, groupPath, artifactID, version, artifactID+"-"+version+".pom")
pomFile, err := os.Open(pomFilePath)
if err != nil {
if !r.checkedLocalRepo && errors.Is(err, os.ErrNotExist) {
r.checkedLocalRepo = true
// check if the directory exists at all, and if not just stop trying to resolve local maven files
fi, err := os.Stat(r.cfg.MavenLocalRepositoryDir)
if errors.Is(err, os.ErrNotExist) || !fi.IsDir() {
log.WithFields("error", err, "repositoryDir", r.cfg.MavenLocalRepositoryDir).
Info("local maven repository is not a readable directory, stopping local resolution")
r.cfg.UseMavenLocalRepository = false
}
}
return nil, err
}
defer internal.CloseAndLogError(pomFile, pomFilePath)
return decodePomXML(pomFile)
}
// findPomInRemoteRepository download the pom file from a (remote) Maven repository over HTTP
func (r *mavenResolver) findPomInRemoteRepository(ctx context.Context, groupID, artifactID, version string) (*gopom.Project, error) {
if groupID == "" || artifactID == "" || version == "" {
return nil, fmt.Errorf("missing/incomplete maven artifact coordinates -- groupId: '%s' artifactId: '%s', version: '%s'", groupID, artifactID, version)
}
requestURL, err := remotePomURL(r.cfg.MavenBaseURL, groupID, artifactID, version)
if err != nil {
return nil, fmt.Errorf("unable to find pom in remote due to: %w", err)
}
// Downloading snapshots requires additional steps to determine the latest snapshot version.
// See: https://maven.apache.org/ref/3-LATEST/maven-repository-metadata/
if strings.HasSuffix(version, "-SNAPSHOT") {
return nil, fmt.Errorf("downloading snapshot artifacts is not supported, got: %s", requestURL)
}
cacheKey := strings.TrimPrefix(strings.TrimPrefix(requestURL, "http://"), "https://")
reader, err := r.cacheResolveReader(cacheKey, func() (io.ReadCloser, error) {
if err != nil {
return nil, err
}
log.WithFields("url", requestURL).Info("fetching parent pom from remote maven repository")
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return nil, fmt.Errorf("unable to create request for Maven central: %w", err)
}
req = req.WithContext(ctx)
client := http.Client{
Timeout: r.remoteRequestTimeout,
}
resp, err := client.Do(req) //nolint:bodyclose
if err != nil {
return nil, fmt.Errorf("unable to get pom from Maven repository %v: %w", requestURL, err)
}
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("pom not found in Maven repository at: %v", requestURL)
}
return resp.Body, err
})
if err != nil {
return nil, err
}
if reader, ok := reader.(io.Closer); ok {
defer internal.CloseAndLogError(reader, requestURL)
}
pom, err := decodePomXML(reader)
if err != nil {
return nil, fmt.Errorf("unable to parse pom from Maven repository url %v: %w", requestURL, err)
}
return pom, nil
}
// cacheResolveReader attempts to get a reader from cache, otherwise caches the contents of the resolve() function.
// this function is guaranteed to return an unread reader for the correct contents.
// NOTE: this could be promoted to the internal cache package as a specialized version of the cache.Resolver
// if there are more users of this functionality
func (r *mavenResolver) cacheResolveReader(key string, resolve func() (io.ReadCloser, error)) (io.Reader, error) {
reader, err := r.cache.Read(key)
if err == nil && reader != nil {
return reader, err
}
contentReader, err := resolve()
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(contentReader, key)
// store the contents to return a new reader with the same content
contents, err := io.ReadAll(contentReader)
if err != nil {
return nil, err
}
err = r.cache.Write(key, bytes.NewBuffer(contents))
return bytes.NewBuffer(contents), err
}
// resolveParent attempts to resolve the parent for the given pom
func (r *mavenResolver) resolveParent(ctx context.Context, pom *gopom.Project) (*gopom.Project, error) {
if pom == nil || pom.Parent == nil {
return nil, nil
}
parent := pom.Parent
pomWithoutParent := *pom
pomWithoutParent.Parent = nil
groupID := r.getPropertyValue(ctx, parent.GroupID, &pomWithoutParent)
artifactID := r.getPropertyValue(ctx, parent.ArtifactID, &pomWithoutParent)
version := r.getPropertyValue(ctx, parent.Version, &pomWithoutParent)
// check cache before resolving
parentID := mavenID{groupID, artifactID, version}
if resolvedParent, ok := r.resolved[parentID]; ok {
return resolvedParent, nil
}
// check if the pom exists in the fileResolver
parentPom := r.findParentPomByRelativePath(ctx, pom, parentID)
if parentPom != nil {
return parentPom, nil
}
// find POM normally
return r.findPom(ctx, groupID, artifactID, version)
}
// findInheritedVersion attempts to find the version of a dependency (groupID, artifactID) by searching all parent poms and imported managed dependencies
//
//nolint:gocognit,funlen
func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Project, groupID, artifactID string, resolutionContext ...*gopom.Project) (string, error) {
if pom == nil {
return "", fmt.Errorf("nil pom provided to findInheritedVersion")
}
if r.cfg.MaxParentRecursiveDepth > 0 && len(resolutionContext) > r.cfg.MaxParentRecursiveDepth {
return "", fmt.Errorf("maximum depth reached attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.resolveMavenID(ctx, pom))
}
if slices.Contains(resolutionContext, pom) {
return "", fmt.Errorf("cycle detected attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.resolveMavenID(ctx, pom))
}
resolutionContext = append(resolutionContext, pom)
var err error
var version string
// check for entries in dependencyManagement first
for _, dep := range pomManagedDependencies(pom) {
depGroupID := r.getPropertyValue(ctx, dep.GroupID, resolutionContext...)
depArtifactID := r.getPropertyValue(ctx, dep.ArtifactID, resolutionContext...)
if depGroupID == groupID && depArtifactID == artifactID {
version = r.getPropertyValue(ctx, dep.Version, resolutionContext...)
if version != "" {
return version, nil
}
}
// imported pom files should be treated just like parent poms, they are used to define versions of dependencies
if deref(dep.Type) == "pom" && deref(dep.Scope) == "import" {
depVersion := r.getPropertyValue(ctx, dep.Version, resolutionContext...)
depPom, err := r.findPom(ctx, depGroupID, depArtifactID, depVersion)
if err != nil || depPom == nil {
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "dependencyID", mavenID{depGroupID, depArtifactID, depVersion}).
Debug("unable to find imported pom looking for managed dependencies")
continue
}
version, err = r.findInheritedVersion(ctx, depPom, groupID, artifactID, resolutionContext...)
if err != nil {
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "dependencyID", mavenID{depGroupID, depArtifactID, depVersion}).
Debug("error during findInheritedVersion")
}
if version != "" {
return version, nil
}
}
}
// recursively check parents
parent, err := r.resolveParent(ctx, pom)
if err != nil {
return "", err
}
if parent != nil {
version, err = r.findInheritedVersion(ctx, parent, groupID, artifactID, resolutionContext...)
if err != nil {
return "", err
}
if version != "" {
return version, nil
}
}
// check for inherited dependencies
for _, dep := range pomDependencies(pom) {
depGroupID := r.getPropertyValue(ctx, dep.GroupID, resolutionContext...)
depArtifactID := r.getPropertyValue(ctx, dep.ArtifactID, resolutionContext...)
if depGroupID == groupID && depArtifactID == artifactID {
version = r.getPropertyValue(ctx, dep.Version, resolutionContext...)
if version != "" {
return version, nil
}
}
}
return "", nil
}
// findLicenses search pom for license, traversing parent poms if needed
func (r *mavenResolver) findLicenses(ctx context.Context, groupID, artifactID, version string) ([]gopom.License, error) {
pom, err := r.findPom(ctx, groupID, artifactID, version)
if pom == nil || err != nil {
return nil, err
}
return r.resolveLicenses(ctx, pom)
}
// resolveLicenses searches the pom for license, traversing parent poms if needed
func (r *mavenResolver) resolveLicenses(ctx context.Context, pom *gopom.Project, processing ...mavenID) ([]gopom.License, error) {
id := r.resolveMavenID(ctx, pom)
if slices.Contains(processing, id) {
return nil, fmt.Errorf("cycle detected resolving licenses for: %v", id)
}
if r.cfg.MaxParentRecursiveDepth > 0 && len(processing) > r.cfg.MaxParentRecursiveDepth {
return nil, fmt.Errorf("maximum parent recursive depth (%v) reached: %v", r.cfg.MaxParentRecursiveDepth, processing)
}
directLicenses := r.pomLicenses(ctx, pom)
if len(directLicenses) > 0 {
return directLicenses, nil
}
parent, err := r.resolveParent(ctx, pom)
if err != nil {
return nil, err
}
if parent == nil {
return nil, nil
}
return r.resolveLicenses(ctx, parent, append(processing, id)...)
}
// pomLicenses appends the directly specified licenses with non-empty name or url
func (r *mavenResolver) pomLicenses(ctx context.Context, pom *gopom.Project) []gopom.License {
var out []gopom.License
for _, license := range deref(pom.Licenses) {
// if we find non-empty licenses, return them
name := r.getPropertyValue(ctx, license.Name, pom)
url := r.getPropertyValue(ctx, license.URL, pom)
if name != "" || url != "" {
out = append(out, license)
}
}
return out
}
func (r *mavenResolver) findParentPomByRelativePath(ctx context.Context, pom *gopom.Project, parentID mavenID) *gopom.Project {
// don't resolve if no resolver
if r.fileResolver == nil {
return nil
}
pomLocation, hasPomLocation := r.pomLocations[pom]
if !hasPomLocation || pom == nil || pom.Parent == nil {
return nil
}
relativePath := r.getPropertyValue(ctx, pom.Parent.RelativePath, pom)
if relativePath == "" {
return nil
}
p := pomLocation.Path()
p = path.Dir(p)
p = path.Join(p, relativePath)
p = path.Clean(p)
parentLocations, err := r.fileResolver.FilesByPath(p)
if err != nil || len(parentLocations) == 0 {
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "relativePath", relativePath).
Trace("parent pom not found by relative path")
return nil
}
parentLocation := parentLocations[0]
parentContents, err := r.fileResolver.FileContentsByLocation(parentLocation)
if err != nil || parentContents == nil {
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "parentLocation", parentLocation).
Debug("unable to get contents of parent pom by relative path")
return nil
}
defer internal.CloseAndLogError(parentContents, parentLocation.RealPath)
parentPom, err := decodePomXML(parentContents)
if err != nil || parentPom == nil {
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "parentLocation", parentLocation).
Debug("unable to parse parent pom")
return nil
}
// ensure parent matches
newParentID := r.resolveMavenID(ctx, parentPom)
if newParentID.ArtifactID != parentID.ArtifactID {
log.WithFields("newParentID", newParentID, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "parentLocation", parentLocation).
Debug("parent IDs do not match resolving parent by relative path")
return nil
}
r.resolved[parentID] = parentPom
r.pomLocations[parentPom] = parentLocation // for any future parent relativepath lookups
return parentPom
}
// pomDependencies returns all dependencies directly defined in a project, including all defined in profiles.
// does not resolve parent dependencies
func pomDependencies(pom *gopom.Project) []gopom.Dependency {
dependencies := deref(pom.Dependencies)
for _, profile := range deref(pom.Profiles) {
dependencies = append(dependencies, deref(profile.Dependencies)...)
}
return dependencies
}
// pomManagedDependencies returns all directly defined managed dependencies in a project pom, including all defined in profiles.
// does not resolve parent managed dependencies
func pomManagedDependencies(pom *gopom.Project) []gopom.Dependency {
var dependencies []gopom.Dependency
if pom.DependencyManagement != nil {
dependencies = append(dependencies, deref(pom.DependencyManagement.Dependencies)...)
}
for _, profile := range deref(pom.Profiles) {
if profile.DependencyManagement != nil {
dependencies = append(dependencies, deref(profile.DependencyManagement.Dependencies)...)
}
}
return dependencies
}

View File

@ -0,0 +1,359 @@
package java
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/bmatcuk/doublestar/v4"
"github.com/stretchr/testify/require"
"github.com/vifraa/gopom"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/internal/fileresolver"
)
func Test_resolveProperty(t *testing.T) {
tests := []struct {
name string
property string
pom gopom.Project
expected string
}{
{
name: "property",
property: "${version.number}",
pom: gopom.Project{
Properties: &gopom.Properties{
Entries: map[string]string{
"version.number": "12.5.0",
},
},
},
expected: "12.5.0",
},
{
name: "groupId",
property: "${project.groupId}",
pom: gopom.Project{
GroupID: ptr("org.some.group"),
},
expected: "org.some.group",
},
{
name: "parent groupId",
property: "${project.parent.groupId}",
pom: gopom.Project{
Parent: &gopom.Parent{
GroupID: ptr("org.some.parent"),
},
},
expected: "org.some.parent",
},
{
name: "nil pointer halts search",
property: "${project.parent.groupId}",
pom: gopom.Project{
Parent: nil,
},
expected: "",
},
{
name: "nil string pointer halts search",
property: "${project.parent.groupId}",
pom: gopom.Project{
Parent: &gopom.Parent{
GroupID: nil,
},
},
expected: "",
},
{
name: "double dereference",
property: "${springboot.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: ptr("1.2.3"),
},
Properties: &gopom.Properties{
Entries: map[string]string{
"springboot.version": "${project.parent.version}",
},
},
},
expected: "1.2.3",
},
{
name: "map missing stops double dereference",
property: "${springboot.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: ptr("1.2.3"),
},
},
expected: "",
},
{
name: "resolution halts even if it resolves to a variable",
property: "${springboot.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: ptr("${undefined.version}"),
},
Properties: &gopom.Properties{
Entries: map[string]string{
"springboot.version": "${project.parent.version}",
},
},
},
expected: "",
},
{
name: "resolution halts even if cyclic",
property: "${springboot.version}",
pom: gopom.Project{
Properties: &gopom.Properties{
Entries: map[string]string{
"springboot.version": "${springboot.version}",
},
},
},
expected: "",
},
{
name: "resolution halts even if cyclic more steps",
property: "${cyclic.version}",
pom: gopom.Project{
Properties: &gopom.Properties{
Entries: map[string]string{
"other.version": "${cyclic.version}",
"springboot.version": "${other.version}",
"cyclic.version": "${springboot.version}",
},
},
},
expected: "",
},
{
name: "resolution halts even if cyclic involving parent",
property: "${cyclic.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: ptr("${cyclic.version}"),
},
Properties: &gopom.Properties{
Entries: map[string]string{
"other.version": "${parent.version}",
"springboot.version": "${other.version}",
"cyclic.version": "${springboot.version}",
},
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r := newMavenResolver(nil, DefaultArchiveCatalogerConfig())
resolved := r.getPropertyValue(context.Background(), ptr(test.property), &test.pom)
require.Equal(t, test.expected, resolved)
})
}
}
func Test_mavenResolverLocal(t *testing.T) {
dir, err := filepath.Abs("test-fixtures/pom/maven-repo")
require.NoError(t, err)
tests := []struct {
name string
groupID string
artifactID string
version string
maxDepth int
expression string
expected string
wantErr require.ErrorAssertionFunc
}{
{
name: "artifact id with variable from 2nd parent",
groupID: "my.org",
artifactID: "child-one",
version: "1.3.6",
expression: "${project.one}",
expected: "1",
},
{
name: "depth limited large enough",
groupID: "my.org",
artifactID: "child-one",
version: "1.3.6",
expression: "${project.one}",
expected: "1",
maxDepth: 2,
},
{
name: "depth limited should not resolve",
groupID: "my.org",
artifactID: "child-one",
version: "1.3.6",
expression: "${project.one}",
expected: "",
maxDepth: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()
r := newMavenResolver(nil, ArchiveCatalogerConfig{
UseNetwork: false,
UseMavenLocalRepository: true,
MavenLocalRepositoryDir: dir,
MaxParentRecursiveDepth: test.maxDepth,
})
pom, err := r.findPom(ctx, test.groupID, test.artifactID, test.version)
if test.wantErr != nil {
test.wantErr(t, err)
} else {
require.NoError(t, err)
}
got := r.getPropertyValue(context.Background(), &test.expression, pom)
require.Equal(t, test.expected, got)
})
}
}
func Test_mavenResolverRemote(t *testing.T) {
url := mockMavenRepo(t)
tests := []struct {
groupID string
artifactID string
version string
expression string
expected string
wantErr require.ErrorAssertionFunc
}{
{
groupID: "my.org",
artifactID: "child-one",
version: "1.3.6",
expression: "${project.one}",
expected: "1",
},
}
for _, test := range tests {
t.Run(test.artifactID, func(t *testing.T) {
ctx := context.Background()
r := newMavenResolver(nil, ArchiveCatalogerConfig{
UseNetwork: true,
UseMavenLocalRepository: false,
MavenBaseURL: url,
})
pom, err := r.findPom(ctx, test.groupID, test.artifactID, test.version)
if test.wantErr != nil {
test.wantErr(t, err)
} else {
require.NoError(t, err)
}
got := r.getPropertyValue(context.Background(), &test.expression, pom)
require.Equal(t, test.expected, got)
})
}
}
func Test_relativePathParent(t *testing.T) {
resolver, err := fileresolver.NewFromDirectory("test-fixtures/pom/local", "")
require.NoError(t, err)
r := newMavenResolver(resolver, DefaultArchiveCatalogerConfig())
locs, err := resolver.FilesByPath("child-1/pom.xml")
require.NoError(t, err)
require.Len(t, locs, 1)
loc := locs[0]
contents, err := resolver.FileContentsByLocation(loc)
require.NoError(t, err)
defer internal.CloseAndLogError(contents, loc.RealPath)
pom, err := decodePomXML(contents)
require.NoError(t, err)
r.pomLocations[pom] = loc
ctx := context.Background()
parent, err := r.resolveParent(ctx, pom)
require.NoError(t, err)
require.Contains(t, r.pomLocations, parent)
parent, err = r.resolveParent(ctx, parent)
require.NoError(t, err)
require.Contains(t, r.pomLocations, parent)
got := r.getPropertyValue(ctx, ptr("${commons-exec_subversion}"), pom)
require.Equal(t, "3", got)
}
// mockMavenRepo starts a remote maven repo serving all the pom files found in test-fixtures/pom/maven-repo
func mockMavenRepo(t *testing.T) (url string) {
t.Helper()
return mockMavenRepoAt(t, "test-fixtures/pom/maven-repo")
}
// mockMavenRepoAt starts a remote maven repo serving all the pom files found in the given directory
func mockMavenRepoAt(t *testing.T, dir string) (url string) {
t.Helper()
// mux is the HTTP request multiplexer used with the test server.
mux := http.NewServeMux()
// We want to ensure that tests catch mistakes where the endpoint URL is
// specified as absolute rather than relative. It only makes a difference
// when there's a non-empty base URL path. So, use that. See issue #752.
apiHandler := http.NewServeMux()
apiHandler.Handle("/", mux)
// server is a test HTTP server used to provide mock API responses.
server := httptest.NewServer(apiHandler)
t.Cleanup(server.Close)
matches, err := doublestar.Glob(os.DirFS(dir), filepath.Join("**", "*.pom"))
require.NoError(t, err)
for _, match := range matches {
fullPath, err := filepath.Abs(filepath.Join(dir, match))
require.NoError(t, err)
match = "/" + filepath.ToSlash(match)
mux.HandleFunc(match, mockMavenHandler(fullPath))
}
return server.URL
}
func mockMavenHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
// Set the Content-Type header to indicate that the response is XML
w.Header().Set("Content-Type", "application/xml")
// Copy the file's content to the response writer
f, err := os.Open(responseFixture)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer internal.CloseAndLogError(f, responseFixture)
_, err = io.Copy(w, f)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

View File

@ -0,0 +1,74 @@
package java
import (
"encoding/xml"
"fmt"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/mitchellh/go-homedir"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
)
// defaultMavenLocalRepoDir gets default location of the Maven local repository, generally at <USER HOME DIR>/.m2/repository
func defaultMavenLocalRepoDir() string {
homeDir, err := homedir.Dir()
if err != nil {
return ""
}
mavenHome := filepath.Join(homeDir, ".m2")
settingsXML := filepath.Join(mavenHome, "settings.xml")
settings, err := os.Open(settingsXML)
if err == nil && settings != nil {
defer internal.CloseAndLogError(settings, settingsXML)
localRepository := getSettingsXMLLocalRepository(settings)
if localRepository != "" {
return localRepository
}
}
return filepath.Join(mavenHome, "repository")
}
// getSettingsXMLLocalRepository reads the provided settings.xml and parses the localRepository, if present
func getSettingsXMLLocalRepository(settingsXML io.Reader) string {
type settings struct {
LocalRepository string `xml:"localRepository"`
}
s := settings{}
err := xml.NewDecoder(settingsXML).Decode(&s)
if err != nil {
log.WithFields("error", err).Debug("unable to read maven settings.xml")
}
return s.LocalRepository
}
// deref dereferences ptr if not nil, or returns the type default value if ptr is nil
func deref[T any](ptr *T) T {
if ptr == nil {
var t T
return t
}
return *ptr
}
// remotePomURL returns a URL to download a POM from a remote repository
func remotePomURL(repoURL, groupID, artifactID, version string) (requestURL string, err error) {
// groupID needs to go from maven.org -> maven/org
urlPath := strings.Split(groupID, ".")
artifactPom := fmt.Sprintf("%s-%s.pom", artifactID, version)
urlPath = append(urlPath, artifactID, version, artifactPom)
// ex: https://repo1.maven.org/maven2/groupID/artifactID/artifactPom
requestURL, err = url.JoinPath(repoURL, urlPath...)
if err != nil {
return requestURL, fmt.Errorf("could not construct maven url: %w", err)
}
return requestURL, err
}

View File

@ -0,0 +1,103 @@
package java
import (
"os"
"path/filepath"
"testing"
"github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/require"
)
func Test_defaultMavenLocalRepoDir(t *testing.T) {
home, err := homedir.Dir()
require.NoError(t, err)
fixtures, err := filepath.Abs("test-fixtures")
require.NoError(t, err)
tests := []struct {
name string
home string
expected string
}{
{
name: "default",
expected: filepath.Join(home, ".m2", "repository"),
home: "",
},
{
name: "alternate dir",
expected: "/some/other/repo",
home: "test-fixtures/local-repository-settings",
},
{
name: "explicit home",
expected: filepath.Join(fixtures, ".m2", "repository"),
home: "test-fixtures",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
homedir.Reset()
defer homedir.Reset()
if test.home != "" {
home, err := filepath.Abs(test.home)
require.NoError(t, err)
t.Setenv("HOME", home)
}
got := defaultMavenLocalRepoDir()
require.Equal(t, test.expected, got)
})
}
}
func Test_getSettingsXmlLocalRepository(t *testing.T) {
tests := []struct {
file string
expected string
}{
{
expected: "/some/other/repo",
file: "test-fixtures/local-repository-settings/.m2/settings.xml",
},
{
expected: "",
file: "invalid",
},
}
for _, test := range tests {
t.Run(test.expected, func(t *testing.T) {
f, _ := os.Open(test.file)
defer f.Close()
got := getSettingsXMLLocalRepository(f)
require.Equal(t, test.expected, got)
})
}
}
func Test_remotePomURL(t *testing.T) {
tests := []struct {
name string
groupID string
artifactID string
version string
expected string
}{
{
name: "formatMavenURL correctly assembles the pom URL",
groupID: "org.springframework.boot",
artifactID: "spring-boot-starter-test",
version: "3.1.5",
expected: "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-test/3.1.5/spring-boot-starter-test-3.1.5.pom",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
requestURL, err := remotePomURL(mavenBaseURL, tc.groupID, tc.artifactID, tc.version)
require.NoError(t, err, "expected no err; got %w", err)
require.Equal(t, tc.expected, requestURL)
})
}
}

View File

@ -4,164 +4,175 @@ import (
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"github.com/saintfish/chardet"
"github.com/vifraa/gopom"
"golang.org/x/net/html/charset"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
const pomXMLGlob = "*pom.xml"
const (
pomXMLGlob = "*pom.xml"
pomCatalogerName = "java-pom-cataloger"
)
var propertyMatcher = regexp.MustCompile("[$][{][^}]+[}]")
type pomXMLCataloger struct {
cfg ArchiveCatalogerConfig
}
func (gap genericArchiveParserAdapter) parserPomXML(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
pom, err := decodePomXML(reader)
func (p pomXMLCataloger) Name() string {
return pomCatalogerName
}
func (p pomXMLCataloger) Catalog(ctx context.Context, fileResolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
locations, err := fileResolver.FilesByGlob("**/pom.xml")
if err != nil {
return nil, nil, err
}
var pkgs []pkg.Package
if pom.Dependencies != nil {
for _, dep := range *pom.Dependencies {
p := newPackageFromPom(
ctx,
pom,
dep,
gap.cfg,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if p.Name == "" {
continue
}
r := newMavenResolver(fileResolver, p.cfg)
pkgs = append(pkgs, p)
var poms []*gopom.Project
for _, pomLocation := range locations {
pom, err := readPomFromLocation(fileResolver, pomLocation)
if err != nil || pom == nil {
log.WithFields("error", err, "pomLocation", pomLocation).Debug("error while reading pom")
continue
}
poms = append(poms, pom)
// store information about this pom for future lookups
r.pomLocations[pom] = pomLocation
r.resolved[r.resolveMavenID(ctx, pom)] = pom
}
var pkgs []pkg.Package
for _, pom := range poms {
pkgs = append(pkgs, processPomXML(ctx, r, pom, r.pomLocations[pom])...)
}
return pkgs, nil, nil
}
func parsePomXMLProject(path string, reader io.Reader, location file.Location) (*parsedPomProject, error) {
project, err := decodePomXML(reader)
func readPomFromLocation(fileResolver file.Resolver, pomLocation file.Location) (*gopom.Project, error) {
contents, err := fileResolver.FileContentsByLocation(pomLocation)
if err != nil {
return nil, err
}
return newPomProject(path, project, location), nil
defer internal.CloseAndLogError(contents, pomLocation.RealPath)
return decodePomXML(contents)
}
func newPomProject(path string, p gopom.Project, location file.Location) *parsedPomProject {
artifactID := safeString(p.ArtifactID)
name := safeString(p.Name)
projectURL := safeString(p.URL)
func processPomXML(ctx context.Context, r *mavenResolver, pom *gopom.Project, loc file.Location) []pkg.Package {
var pkgs []pkg.Package
var licenses []pkg.License
if p.Licenses != nil {
for _, license := range *p.Licenses {
var licenseName, licenseURL string
if license.Name != nil {
licenseName = *license.Name
}
if license.URL != nil {
licenseURL = *license.URL
}
pomID := r.resolveMavenID(ctx, pom)
for _, dep := range pomDependencies(pom) {
depID := r.resolveDependencyID(ctx, pom, dep)
log.WithFields("pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Trace("adding maven pom dependency")
if licenseName == "" && licenseURL == "" {
continue
}
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, licenseURL, &location))
p, err := newPackageFromDependency(
ctx,
r,
pom,
dep,
loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if err != nil {
log.WithFields("error", err, "pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Debugf("error adding dependency")
}
if p == nil {
continue
}
pkgs = append(pkgs, *p)
}
log.WithFields("path", path, "artifactID", artifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml")
return &parsedPomProject{
JavaPomProject: &pkg.JavaPomProject{
Path: path,
Parent: pomParent(p, p.Parent),
GroupID: resolveProperty(p, p.GroupID, "groupId"),
ArtifactID: artifactID,
Version: resolveProperty(p, p.Version, "version"),
Name: name,
Description: cleanDescription(p.Description),
URL: projectURL,
},
Licenses: licenses,
return pkgs
}
func newPomProject(ctx context.Context, r *mavenResolver, path string, pom *gopom.Project) *pkg.JavaPomProject {
id := r.resolveMavenID(ctx, pom)
name := r.getPropertyValue(ctx, pom.Name, pom)
projectURL := r.getPropertyValue(ctx, pom.URL, pom)
log.WithFields("path", path, "artifactID", id.ArtifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml")
return &pkg.JavaPomProject{
Path: path,
Parent: pomParent(ctx, r, pom),
GroupID: id.GroupID,
ArtifactID: id.ArtifactID,
Version: id.Version,
Name: name,
Description: cleanDescription(r.getPropertyValue(ctx, pom.Description, pom)),
URL: projectURL,
}
}
func newPackageFromPom(ctx context.Context, pom gopom.Project, dep gopom.Dependency, cfg ArchiveCatalogerConfig, locations ...file.Location) pkg.Package {
func newPackageFromDependency(ctx context.Context, r *mavenResolver, pom *gopom.Project, dep gopom.Dependency, locations ...file.Location) (*pkg.Package, error) {
id := r.resolveDependencyID(ctx, pom, dep)
m := pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: resolveProperty(pom, dep.GroupID, "groupId"),
ArtifactID: resolveProperty(pom, dep.ArtifactID, "artifactId"),
Scope: resolveProperty(pom, dep.Scope, "scope"),
GroupID: id.GroupID,
ArtifactID: id.ArtifactID,
Scope: r.getPropertyValue(ctx, dep.Scope, pom),
},
}
name := safeString(dep.ArtifactID)
version := resolveProperty(pom, dep.Version, "version")
var err error
var licenses []pkg.License
dependencyPom, depErr := r.findPom(ctx, id.GroupID, id.ArtifactID, id.Version)
if depErr != nil {
err = errors.Join(err, depErr)
}
licenses := make([]pkg.License, 0)
if cfg.UseNetwork {
if version == "" {
// If we have no version then let's try to get it from a parent pom DependencyManagement section
version = recursivelyFindVersionFromParentPom(ctx, *dep.GroupID, *dep.ArtifactID, *pom.Parent.GroupID, *pom.Parent.ArtifactID, *pom.Parent.Version, cfg)
}
if version != "" {
parentLicenses := recursivelyFindLicensesFromParentPom(
ctx,
m.PomProperties.GroupID,
m.PomProperties.ArtifactID,
version,
cfg)
if len(parentLicenses) > 0 {
for _, licenseName := range parentLicenses {
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
}
}
if dependencyPom != nil {
depLicenses, _ := r.resolveLicenses(ctx, dependencyPom)
for _, license := range depLicenses {
licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), nil))
}
}
p := pkg.Package{
Name: name,
Version: version,
p := &pkg.Package{
Name: id.ArtifactID,
Version: id.Version,
Locations: file.NewLocationSet(locations...),
Licenses: pkg.NewLicenseSet(licenses...),
PURL: packageURL(name, version, m),
PURL: packageURL(id.ArtifactID, id.Version, m),
Language: pkg.Java,
Type: pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a pom.xml that were not installed yet?
FoundBy: pomCatalogerName,
Metadata: m,
}
p.SetID()
return p
return p, err
}
func decodePomXML(content io.Reader) (project gopom.Project, err error) {
// decodePomXML decodes a pom XML file, detecting and converting non-UTF-8 charsets. this DOES NOT perform any logic to resolve properties such as groupID, artifactID, and version
func decodePomXML(content io.Reader) (project *gopom.Project, err error) {
inputReader, err := getUtf8Reader(content)
if err != nil {
return project, fmt.Errorf("unable to read pom.xml: %w", err)
return nil, fmt.Errorf("unable to read pom.xml: %w", err)
}
decoder := xml.NewDecoder(inputReader)
// when an xml file has a character set declaration (e.g. '<?xml version="1.0" encoding="ISO-8859-1"?>') read that and use the correct decoder
decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(&project); err != nil {
return project, fmt.Errorf("unable to unmarshal pom.xml: %w", err)
project = &gopom.Project{}
if err := decoder.Decode(project); err != nil {
return nil, fmt.Errorf("unable to unmarshal pom.xml: %w", err)
}
return project, nil
@ -194,29 +205,28 @@ func getUtf8Reader(content io.Reader) (io.Reader, error) {
return inputReader, nil
}
func pomParent(pom gopom.Project, parent *gopom.Parent) (result *pkg.JavaPomParent) {
if parent == nil {
func pomParent(ctx context.Context, r *mavenResolver, pom *gopom.Project) *pkg.JavaPomParent {
if pom == nil || pom.Parent == nil {
return nil
}
artifactID := safeString(parent.ArtifactID)
result = &pkg.JavaPomParent{
GroupID: resolveProperty(pom, parent.GroupID, "groupId"),
groupID := r.getPropertyValue(ctx, pom.Parent.GroupID, pom)
artifactID := r.getPropertyValue(ctx, pom.Parent.ArtifactID, pom)
version := r.getPropertyValue(ctx, pom.Parent.Version, pom)
if groupID == "" && artifactID == "" && version == "" {
return nil
}
return &pkg.JavaPomParent{
GroupID: groupID,
ArtifactID: artifactID,
Version: resolveProperty(pom, parent.Version, "version"),
Version: version,
}
if result.GroupID == "" && result.ArtifactID == "" && result.Version == "" {
return nil
}
return result
}
func cleanDescription(original *string) (cleaned string) {
if original == nil {
return ""
}
descriptionLines := strings.Split(*original, "\n")
func cleanDescription(original string) (cleaned string) {
descriptionLines := strings.Split(original, "\n")
for _, line := range descriptionLines {
line = strings.TrimSpace(line)
if len(line) == 0 {
@ -226,94 +236,3 @@ func cleanDescription(original *string) (cleaned string) {
}
return strings.TrimSpace(cleaned)
}
// resolveProperty emulates some maven property resolution logic by looking in the project's variables
// as well as supporting the project expressions like ${project.parent.groupId}.
// If no match is found, the entire expression including ${} is returned
func resolveProperty(pom gopom.Project, property *string, propertyName string) string {
propertyCase := safeString(property)
log.WithFields("existingPropertyValue", propertyCase, "propertyName", propertyName).Trace("resolving property")
seenBeforePropertyNames := map[string]struct{}{
propertyName: {},
}
result := recursiveResolveProperty(pom, propertyCase, seenBeforePropertyNames)
if propertyMatcher.MatchString(result) {
return "" // dereferencing variable failed; fall back to empty string
}
return result
}
//nolint:gocognit
func recursiveResolveProperty(pom gopom.Project, propertyCase string, seenPropertyNames map[string]struct{}) string {
return propertyMatcher.ReplaceAllStringFunc(propertyCase, func(match string) string {
propertyName := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing }
if _, seen := seenPropertyNames[propertyName]; seen {
return propertyCase
}
entries := pomProperties(pom)
if value, ok := entries[propertyName]; ok {
seenPropertyNames[propertyName] = struct{}{}
return recursiveResolveProperty(pom, value, seenPropertyNames) // recursively resolve in case a variable points to a variable.
}
// if we don't find anything directly in the pom properties,
// see if we have a project.x expression and process this based
// on the xml tags in gopom
parts := strings.Split(propertyName, ".")
numParts := len(parts)
if numParts > 1 && strings.TrimSpace(parts[0]) == "project" {
pomValue := reflect.ValueOf(pom)
pomValueType := pomValue.Type()
for partNum := 1; partNum < numParts; partNum++ {
if pomValueType.Kind() != reflect.Struct {
break
}
part := parts[partNum]
for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ {
f := pomValueType.Field(fieldNum)
tag := f.Tag.Get("xml")
tag = strings.Split(tag, ",")[0]
// a segment of the property name matches the xml tag for the field,
// so we need to recurse down the nested structs or return a match
// if we're done.
if part == tag {
pomValue = pomValue.Field(fieldNum)
pomValueType = pomValue.Type()
if pomValueType.Kind() == reflect.Ptr {
// we were recursing down the nested structs, but one of the steps
// we need to take is a nil pointer, so give up and return the original match
if pomValue.IsNil() {
return match
}
pomValue = pomValue.Elem()
if !pomValue.IsZero() {
// we found a non-zero value whose tag matches this part of the property name
pomValueType = pomValue.Type()
}
}
// If this was the last part of the property name, return the value
if partNum == numParts-1 {
return fmt.Sprintf("%v", pomValue.Interface())
}
break
}
}
}
}
return match
})
}
func pomProperties(p gopom.Project) map[string]string {
if p.Properties != nil {
return p.Properties.Entries
}
return map[string]string{}
}
func safeString(s *string) string {
if s == nil {
return ""
}
return *s
}

View File

@ -1,6 +1,7 @@
package java
import (
"context"
"encoding/base64"
"io"
"os"
@ -16,15 +17,17 @@ import (
"github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
)
func Test_parserPomXML(t *testing.T) {
func Test_parsePomXML(t *testing.T) {
tests := []struct {
input string
dir string
expected []pkg.Package
}{
{
input: "test-fixtures/pom/pom.xml",
dir: "test-fixtures/pom/local/example-java-app-maven",
expected: []pkg.Package{
{
Name: "joda-time",
@ -32,6 +35,7 @@ func Test_parserPomXML(t *testing.T) {
PURL: "pkg:maven/com.joda/joda-time@2.9.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "com.joda",
@ -45,6 +49,7 @@ func Test_parserPomXML(t *testing.T) {
PURL: "pkg:maven/junit/junit@4.12",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "junit",
@ -58,19 +63,19 @@ func Test_parserPomXML(t *testing.T) {
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
t.Run(test.dir, func(t *testing.T) {
for i := range test.expected {
test.expected[i].Locations.Add(file.NewLocation(test.input))
test.expected[i].Locations.Add(file.NewLocation("pom.xml"))
}
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{
cat := NewPomCataloger(ArchiveCatalogerConfig{
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: true,
IncludeUnindexedArchives: true,
},
})
pkgtest.TestFileParser(t, test.input, gap.parserPomXML, test.expected, nil)
pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil)
})
}
}
@ -131,168 +136,129 @@ func Test_decodePomXML_surviveNonUtf8Encoding(t *testing.T) {
func Test_parseCommonsTextPomXMLProject(t *testing.T) {
tests := []struct {
input string
dir string
expected []pkg.Package
}{
{
input: "test-fixtures/pom/commons-text.pom.xml",
expected: []pkg.Package{
{
Name: "commons-lang3",
Version: "3.12.0",
PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache.commons",
ArtifactID: "commons-lang3",
},
},
},
{
Name: "junit-jupiter",
Version: "",
PURL: "pkg:maven/org.junit.jupiter/junit-jupiter",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.junit.jupiter",
ArtifactID: "junit-jupiter",
Scope: "test",
},
},
},
{
Name: "assertj-core",
Version: "3.23.1",
PURL: "pkg:maven/org.assertj/assertj-core@3.23.1",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.assertj",
ArtifactID: "assertj-core",
Scope: "test",
},
},
},
{
Name: "commons-io",
Version: "2.11.0",
PURL: "pkg:maven/commons-io/commons-io@2.11.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "commons-io",
ArtifactID: "commons-io",
Scope: "test",
},
},
},
{
Name: "mockito-inline",
Version: "4.8.0",
PURL: "pkg:maven/org.mockito/mockito-inline@4.8.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.mockito",
ArtifactID: "mockito-inline",
Scope: "test",
},
},
},
{
Name: "js",
Version: "22.0.0.2",
PURL: "pkg:maven/org.graalvm.js/js@22.0.0.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.graalvm.js",
ArtifactID: "js",
Scope: "test",
},
},
},
{
Name: "js-scriptengine",
Version: "22.0.0.2",
PURL: "pkg:maven/org.graalvm.js/js-scriptengine@22.0.0.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.graalvm.js",
ArtifactID: "js-scriptengine",
Scope: "test",
},
},
},
{
Name: "commons-rng-simple",
Version: "1.4",
PURL: "pkg:maven/org.apache.commons/commons-rng-simple@1.4",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache.commons",
ArtifactID: "commons-rng-simple",
Scope: "test",
},
},
},
{
Name: "jmh-core",
Version: "1.35",
PURL: "pkg:maven/org.openjdk.jmh/jmh-core@1.35",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.openjdk.jmh",
ArtifactID: "jmh-core",
Scope: "test",
},
},
},
{
Name: "jmh-generator-annprocess",
Version: "1.35",
PURL: "pkg:maven/org.openjdk.jmh/jmh-generator-annprocess@1.35",
Language: pkg.Java,
Type: pkg.JavaPkg,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.openjdk.jmh",
ArtifactID: "jmh-generator-annprocess",
Scope: "test",
},
},
},
},
dir: "test-fixtures/pom/local/commons-text-1.10.0",
expected: getCommonsTextExpectedPackages(),
},
}
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
t.Run(test.dir, func(t *testing.T) {
for i := range test.expected {
test.expected[i].Locations.Add(file.NewLocation(test.input))
test.expected[i].Locations.Add(file.NewLocation("pom.xml"))
}
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{
cat := NewPomCataloger(ArchiveCatalogerConfig{
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: true,
IncludeUnindexedArchives: true,
},
UseMavenLocalRepository: false,
})
pkgtest.TestFileParser(t, test.input, gap.parserPomXML, test.expected, nil)
pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil)
})
}
}
func Test_parseCommonsTextPomXMLProjectWithLocalRepository(t *testing.T) {
// Using the local repository, the version of junit-jupiter will be resolved
expectedPackages := getCommonsTextExpectedPackages()
for i := 0; i < len(expectedPackages); i++ {
if expectedPackages[i].Name == "junit-jupiter" {
expPkg := &expectedPackages[i]
expPkg.Version = "5.9.1"
expPkg.PURL = "pkg:maven/org.junit.jupiter/junit-jupiter@5.9.1"
expPkg.Metadata = pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.junit.jupiter",
ArtifactID: "junit-jupiter",
Scope: "test",
},
}
}
}
tests := []struct {
dir string
expected []pkg.Package
}{
{
dir: "test-fixtures/pom/local/commons-text-1.10.0",
expected: expectedPackages,
},
}
for _, test := range tests {
t.Run(test.dir, func(t *testing.T) {
for i := range test.expected {
test.expected[i].Locations.Add(file.NewLocation("pom.xml"))
}
cat := NewPomCataloger(ArchiveCatalogerConfig{
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: true,
IncludeUnindexedArchives: true,
},
UseMavenLocalRepository: true,
MavenLocalRepositoryDir: "test-fixtures/pom/maven-repo",
})
pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil)
})
}
}
func Test_parseCommonsTextPomXMLProjectWithNetwork(t *testing.T) {
url := mockMavenRepo(t)
// Using the local repository, the version of junit-jupiter will be resolved
expectedPackages := getCommonsTextExpectedPackages()
for i := 0; i < len(expectedPackages); i++ {
if expectedPackages[i].Name == "junit-jupiter" {
expPkg := &expectedPackages[i]
expPkg.Version = "5.9.1"
expPkg.PURL = "pkg:maven/org.junit.jupiter/junit-jupiter@5.9.1"
expPkg.Metadata = pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.junit.jupiter",
ArtifactID: "junit-jupiter",
Scope: "test",
},
}
}
}
tests := []struct {
dir string
expected []pkg.Package
}{
{
dir: "test-fixtures/pom/local/commons-text-1.10.0",
expected: expectedPackages,
},
}
for _, test := range tests {
t.Run(test.dir, func(t *testing.T) {
for i := range test.expected {
test.expected[i].Locations.Add(file.NewLocation("pom.xml"))
}
cat := NewPomCataloger(ArchiveCatalogerConfig{
ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: true,
IncludeUnindexedArchives: true,
},
UseNetwork: true,
MavenBaseURL: url,
UseMavenLocalRepository: false,
})
pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil)
})
}
}
@ -302,63 +268,60 @@ func Test_parsePomXMLProject(t *testing.T) {
jarLocation := file.NewLocation("path/to/archive.jar")
tests := []struct {
name string
expected parsedPomProject
project *pkg.JavaPomProject
licenses []pkg.License
}{
{
name: "go case",
expected: parsedPomProject{
JavaPomProject: &pkg.JavaPomProject{
Path: "test-fixtures/pom/commons-codec.pom.xml",
Parent: &pkg.JavaPomParent{
GroupID: "org.apache.commons",
ArtifactID: "commons-parent",
Version: "42",
},
GroupID: "commons-codec",
ArtifactID: "commons-codec",
Version: "1.11",
Name: "Apache Commons Codec",
Description: "The Apache Commons Codec package contains simple encoder and decoders for various formats such as Base64 and Hexadecimal. In addition to these widely used encoders and decoders, the codec package also maintains a collection of phonetic encoding utilities.",
URL: "http://commons.apache.org/proper/commons-codec/",
project: &pkg.JavaPomProject{
Path: "test-fixtures/pom/commons-codec.pom.xml",
Parent: &pkg.JavaPomParent{
GroupID: "org.apache.commons",
ArtifactID: "commons-parent",
Version: "42",
},
GroupID: "commons-codec",
ArtifactID: "commons-codec",
Version: "1.11",
Name: "Apache Commons Codec",
Description: "The Apache Commons Codec package contains simple encoder and decoders for various formats such as Base64 and Hexadecimal. In addition to these widely used encoders and decoders, the codec package also maintains a collection of phonetic encoding utilities.",
URL: "http://commons.apache.org/proper/commons-codec/",
},
},
{
name: "with license data",
expected: parsedPomProject{
JavaPomProject: &pkg.JavaPomProject{
Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml",
Parent: &pkg.JavaPomParent{
GroupID: "org.sonatype.oss",
ArtifactID: "oss-parent",
Version: "7",
},
GroupID: "org.neo4j.build.plugins",
ArtifactID: "license-maven-plugin",
Version: "4-SNAPSHOT",
Name: "${project.artifactId}", // TODO: this is not an ideal answer
Description: "Maven 2 plugin to check and update license headers in source files",
URL: "http://components.neo4j.org/${project.artifactId}/${project.version}", // TODO: this is not an ideal answer
project: &pkg.JavaPomProject{
Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml",
Parent: &pkg.JavaPomParent{
GroupID: "org.sonatype.oss",
ArtifactID: "oss-parent",
Version: "7",
},
Licenses: []pkg.License{
{
Value: "The Apache Software License, Version 2.0",
SPDXExpression: "", // TODO: ideally we would parse this title to get Apache-2.0 (created issue #2210 https://github.com/anchore/syft/issues/2210)
Type: license.Declared,
URLs: []string{"http://www.apache.org/licenses/LICENSE-2.0.txt"},
Locations: file.NewLocationSet(jarLocation),
},
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
Locations: file.NewLocationSet(jarLocation),
},
{
Type: license.Declared,
URLs: []string{"https://opensource.org/license/unlicense/"},
Locations: file.NewLocationSet(jarLocation),
},
GroupID: "org.neo4j.build.plugins",
ArtifactID: "license-maven-plugin",
Version: "4-SNAPSHOT",
Name: "license-maven-plugin",
Description: "Maven 2 plugin to check and update license headers in source files",
URL: "http://components.neo4j.org/license-maven-plugin/4-SNAPSHOT",
},
licenses: []pkg.License{
{
Value: "The Apache Software License, Version 2.0",
SPDXExpression: "", // TODO: ideally we would parse this title to get Apache-2.0 (created issue #2210 https://github.com/anchore/syft/issues/2210)
Type: license.Declared,
URLs: []string{"http://www.apache.org/licenses/LICENSE-2.0.txt"},
Locations: file.NewLocationSet(jarLocation),
},
{
Value: "MIT",
SPDXExpression: "MIT",
Type: license.Declared,
Locations: file.NewLocationSet(jarLocation),
},
{
Type: license.Declared,
URLs: []string{"https://opensource.org/license/unlicense/"},
Locations: file.NewLocationSet(jarLocation),
},
},
},
@ -366,13 +329,20 @@ func Test_parsePomXMLProject(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
fixture, err := os.Open(test.expected.Path)
fixture, err := os.Open(test.project.Path)
assert.NoError(t, err)
r := newMavenResolver(nil, ArchiveCatalogerConfig{})
actual, err := parsePomXMLProject(fixture.Name(), fixture, jarLocation)
pom, err := gopom.ParseFromReader(fixture)
require.NoError(t, err)
actual := newPomProject(context.Background(), r, fixture.Name(), pom)
assert.NoError(t, err)
assert.Equal(t, test.project, actual)
assert.Equal(t, &test.expected, actual)
licenses := r.pomLicenses(context.Background(), pom)
assert.NoError(t, err)
assert.Equal(t, test.licenses, toPkgLicenses(&jarLocation, licenses))
})
}
}
@ -386,7 +356,7 @@ func Test_pomParent(t *testing.T) {
{
name: "only group ID",
input: &gopom.Parent{
GroupID: stringPointer("org.something"),
GroupID: ptr("org.something"),
},
expected: &pkg.JavaPomParent{
GroupID: "org.something",
@ -395,7 +365,7 @@ func Test_pomParent(t *testing.T) {
{
name: "only artifact ID",
input: &gopom.Parent{
ArtifactID: stringPointer("something"),
ArtifactID: ptr("something"),
},
expected: &pkg.JavaPomParent{
ArtifactID: "something",
@ -404,7 +374,7 @@ func Test_pomParent(t *testing.T) {
{
name: "only Version",
input: &gopom.Parent{
Version: stringPointer("something"),
Version: ptr("something"),
},
expected: &pkg.JavaPomParent{
Version: "something",
@ -423,7 +393,7 @@ func Test_pomParent(t *testing.T) {
{
name: "unused field",
input: &gopom.Parent{
RelativePath: stringPointer("something"),
RelativePath: ptr("something"),
},
expected: nil,
},
@ -431,7 +401,8 @@ func Test_pomParent(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, pomParent(gopom.Project{}, test.input))
r := newMavenResolver(nil, DefaultArchiveCatalogerConfig())
assert.Equal(t, test.expected, pomParent(context.Background(), r, &gopom.Project{Parent: test.input}))
})
}
}
@ -454,163 +425,108 @@ func Test_cleanDescription(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, cleanDescription(stringPointer(test.input)))
assert.Equal(t, test.expected, cleanDescription(test.input))
})
}
}
func Test_resolveProperty(t *testing.T) {
func Test_resolveLicenses(t *testing.T) {
mavenURL := mockMavenRepo(t)
localM2 := "test-fixtures/pom/maven-repo"
localDir := "test-fixtures/pom/local"
containingDir := "test-fixtures/pom/local/contains-child-1"
expectedLicenses := []pkg.License{
{
Value: "Eclipse Public License v2.0",
SPDXExpression: "",
Type: license.Declared,
URLs: []string{"https://www.eclipse.org/legal/epl-v20.html"},
},
}
tests := []struct {
name string
property string
pom gopom.Project
expected string
scanDir string
cfg ArchiveCatalogerConfig
expected []pkg.License
}{
{
name: "property",
property: "${version.number}",
pom: gopom.Project{
Properties: &gopom.Properties{
Entries: map[string]string{
"version.number": "12.5.0",
},
},
name: "local no resolution",
scanDir: containingDir,
cfg: ArchiveCatalogerConfig{
UseMavenLocalRepository: false,
UseNetwork: false,
MavenLocalRepositoryDir: "",
MavenBaseURL: "",
},
expected: "12.5.0",
expected: nil,
},
{
name: "groupId",
property: "${project.groupId}",
pom: gopom.Project{
GroupID: stringPointer("org.some.group"),
name: "local all poms",
scanDir: localDir,
cfg: ArchiveCatalogerConfig{
UseMavenLocalRepository: false,
UseNetwork: false,
},
expected: "org.some.group",
expected: expectedLicenses,
},
{
name: "parent groupId",
property: "${project.parent.groupId}",
pom: gopom.Project{
Parent: &gopom.Parent{
GroupID: stringPointer("org.some.parent"),
},
name: "local m2 cache",
scanDir: containingDir,
cfg: ArchiveCatalogerConfig{
UseMavenLocalRepository: true,
MavenLocalRepositoryDir: localM2,
UseNetwork: false,
MavenBaseURL: "",
},
expected: "org.some.parent",
expected: expectedLicenses,
},
{
name: "nil pointer halts search",
property: "${project.parent.groupId}",
pom: gopom.Project{
Parent: nil,
name: "local with network",
scanDir: containingDir,
cfg: ArchiveCatalogerConfig{
UseMavenLocalRepository: false,
UseNetwork: true,
MavenBaseURL: mavenURL,
},
expected: "",
},
{
name: "nil string pointer halts search",
property: "${project.parent.groupId}",
pom: gopom.Project{
Parent: &gopom.Parent{
GroupID: nil,
},
},
expected: "",
},
{
name: "double dereference",
property: "${springboot.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: stringPointer("1.2.3"),
},
Properties: &gopom.Properties{
Entries: map[string]string{
"springboot.version": "${project.parent.version}",
},
},
},
expected: "1.2.3",
},
{
name: "map missing stops double dereference",
property: "${springboot.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: stringPointer("1.2.3"),
},
},
expected: "",
},
{
name: "resolution halts even if it resolves to a variable",
property: "${springboot.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: stringPointer("${undefined.version}"),
},
Properties: &gopom.Properties{
Entries: map[string]string{
"springboot.version": "${project.parent.version}",
},
},
},
expected: "",
},
{
name: "resolution halts even if cyclic",
property: "${springboot.version}",
pom: gopom.Project{
Properties: &gopom.Properties{
Entries: map[string]string{
"springboot.version": "${springboot.version}",
},
},
},
expected: "",
},
{
name: "resolution halts even if cyclic more steps",
property: "${cyclic.version}",
pom: gopom.Project{
Properties: &gopom.Properties{
Entries: map[string]string{
"other.version": "${cyclic.version}",
"springboot.version": "${other.version}",
"cyclic.version": "${springboot.version}",
},
},
},
expected: "",
},
{
name: "resolution halts even if cyclic involving parent",
property: "${cyclic.version}",
pom: gopom.Project{
Parent: &gopom.Parent{
Version: stringPointer("${cyclic.version}"),
},
Properties: &gopom.Properties{
Entries: map[string]string{
"other.version": "${parent.version}",
"springboot.version": "${other.version}",
"cyclic.version": "${springboot.version}",
},
},
},
expected: "",
expected: expectedLicenses,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolved := resolveProperty(test.pom, stringPointer(test.property), test.name)
assert.Equal(t, test.expected, resolved)
cat := NewPomCataloger(test.cfg)
ds, err := directorysource.NewFromPath(test.scanDir)
require.NoError(t, err)
fr, err := ds.FileResolver(source.AllLayersScope)
require.NoError(t, err)
ctx := context.TODO()
pkgs, _, err := cat.Catalog(ctx, fr)
require.NoError(t, err)
var child1 pkg.Package
for _, p := range pkgs {
if p.Name == "child-one" {
child1 = p
break
}
}
require.Equal(t, "child-one", child1.Name)
got := child1.Licenses.ToSlice()
for i := 0; i < len(got); i++ {
// ignore locations, just check license text
(&got[i]).Locations = file.LocationSet{}
}
require.ElementsMatch(t, test.expected, got)
})
}
}
func stringPointer(s string) *string {
return &s
}
func Test_getUtf8Reader(t *testing.T) {
tests := []struct {
name string
@ -635,3 +551,157 @@ func Test_getUtf8Reader(t *testing.T) {
})
}
}
func getCommonsTextExpectedPackages() []pkg.Package {
return []pkg.Package{
{
Name: "commons-lang3",
Version: "3.12.0",
PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache.commons",
ArtifactID: "commons-lang3",
},
},
},
{
Name: "junit-jupiter",
Version: "",
PURL: "pkg:maven/org.junit.jupiter/junit-jupiter",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.junit.jupiter",
ArtifactID: "junit-jupiter",
Scope: "test",
},
},
},
{
Name: "assertj-core",
Version: "3.23.1",
PURL: "pkg:maven/org.assertj/assertj-core@3.23.1",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.assertj",
ArtifactID: "assertj-core",
Scope: "test",
},
},
},
{
Name: "commons-io",
Version: "2.11.0",
PURL: "pkg:maven/commons-io/commons-io@2.11.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "commons-io",
ArtifactID: "commons-io",
Scope: "test",
},
},
},
{
Name: "mockito-inline",
Version: "4.8.0",
PURL: "pkg:maven/org.mockito/mockito-inline@4.8.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.mockito",
ArtifactID: "mockito-inline",
Scope: "test",
},
},
},
{
Name: "js",
Version: "22.0.0.2",
PURL: "pkg:maven/org.graalvm.js/js@22.0.0.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.graalvm.js",
ArtifactID: "js",
Scope: "test",
},
},
},
{
Name: "js-scriptengine",
Version: "22.0.0.2",
PURL: "pkg:maven/org.graalvm.js/js-scriptengine@22.0.0.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.graalvm.js",
ArtifactID: "js-scriptengine",
Scope: "test",
},
},
},
{
Name: "commons-rng-simple",
Version: "1.4",
PURL: "pkg:maven/org.apache.commons/commons-rng-simple@1.4",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache.commons",
ArtifactID: "commons-rng-simple",
Scope: "test",
},
},
},
{
Name: "jmh-core",
Version: "1.35",
PURL: "pkg:maven/org.openjdk.jmh/jmh-core@1.35",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.openjdk.jmh",
ArtifactID: "jmh-core",
Scope: "test",
},
},
},
{
Name: "jmh-generator-annprocess",
Version: "1.35",
PURL: "pkg:maven/org.openjdk.jmh/jmh-generator-annprocess@1.35",
Language: pkg.Java,
Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.openjdk.jmh",
ArtifactID: "jmh-generator-annprocess",
Scope: "test",
},
},
},
}
}

View File

@ -0,0 +1,4 @@
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>/some/other/repo</localRepository>
</settings>

View File

@ -1,575 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.commons</groupId>
<artifactId>commons-parent</artifactId>
<version>54</version>
</parent>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
<name>Apache Commons Text</name>
<description>Apache Commons Text is a library focused on algorithms working on strings.</description>
<url>https://commons.apache.org/proper/commons-text</url>
<properties>
<project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<commons.componentid>text</commons.componentid>
<commons.module.name>org.apache.commons.text</commons.module.name>
<commons.release.version>1.10.0</commons.release.version>
<commons.release.desc>(Java 8+)</commons.release.desc>
<commons.jira.id>TEXT</commons.jira.id>
<commons.jira.pid>12318221</commons.jira.pid>
<commons.site.path>text</commons.site.path>
<commons.scmPubUrl>https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text</commons.scmPubUrl>
<commons.scmPubCheckoutDirectory>site-content</commons.scmPubCheckoutDirectory>
<commons.junit.version>5.9.1</commons.junit.version>
<checkstyle.plugin.version>3.2.0</checkstyle.plugin.version>
<checkstyle.version>9.3</checkstyle.version>
<commons.spotbugs.plugin.version>4.7.2.0</commons.spotbugs.plugin.version>
<commons.spotbugs.impl.version>4.7.2</commons.spotbugs.impl.version>
<commons.pmd.version>3.19.0</commons.pmd.version>
<commons.pmd-impl.version>6.49.0</commons.pmd-impl.version>
<commons.mockito.version>4.8.0</commons.mockito.version>
<commons.jacoco.version>0.8.8</commons.jacoco.version>
<!-- apache-rat-plugin 0.13 and jdepend-maven-plugin 2.0 both fail with LinkageError when generating reports
with maven site plugin 3.11+. However, javadoc 3.4.0+ fails with site plugin versions lower than 3.11. So, we'll
use slightly older site and javadoc versions here in order to be able to generate all reports. -->
<commons.site-plugin.version>3.10.0</commons.site-plugin.version>
<commons.javadoc.version>3.4.1</commons.javadoc.version>
<!-- 22.1.0 requires Java 11 -->
<graalvm.version>22.0.0.2</graalvm.version>
<commons.rng.version>1.4</commons.rng.version>
<commons.japicmp.version>0.16.0</commons.japicmp.version>
<japicmp.skip>false</japicmp.skip>
<jmh.version>1.35</jmh.version>
<commons.project-info.version>3.1.2</commons.project-info.version>
<!-- Commons Release Plugin -->
<commons.bc.version>1.9</commons.bc.version>
<commons.rc.version>RC1</commons.rc.version>
<commons.release.isDistModule>true</commons.release.isDistModule>
<commons.distSvnStagingUrl>scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid}</commons.distSvnStagingUrl>
<commons.releaseManagerName>Gary Gregory</commons.releaseManagerName>
<commons.releaseManagerKey>86fdc7e2a11262cb</commons.releaseManagerKey>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<!-- Use mockito-inline instead of mockito-core to mock and spy on final classes. -->
<artifactId>mockito-inline</artifactId>
<version>${commons.mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${graalvm.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>${graalvm.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-rng-simple</artifactId>
<version>${commons.rng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<defaultGoal>clean verify apache-rat:check japicmp:cmp checkstyle:check spotbugs:check javadoc:javadoc</defaultGoal>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.rat</groupId>
<artifactId>apache-rat-plugin</artifactId>
<configuration>
<excludes>
<exclude>site-content/**</exclude>
<exclude>src/site/resources/download_lang.cgi</exclude>
<exclude>src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt</exclude>
<exclude>src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv</exclude>
<exclude>src/site/resources/release-notes/RELEASE-NOTES-*.txt</exclude>
</excludes>
</configuration>
</plugin><!-- override skip property of parent pom -->
<plugin>
<artifactId>maven-pmd-plugin</artifactId>
<version>${commons.pmd.version}</version>
<configuration>
<targetJdk>${maven.compiler.target}</targetJdk>
</configuration>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-core</artifactId>
<version>${commons.pmd-impl.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-java</artifactId>
<version>${commons.pmd-impl.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-javascript</artifactId>
<version>${commons.pmd-impl.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-jsp</artifactId>
<version>${commons.pmd-impl.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle.plugin.version}</version>
<configuration>
<enableRulesSummary>false</enableRulesSummary>
<configLocation>src/conf/checkstyle.xml</configLocation>
<headerLocation>src/conf/checkstyle-header.txt</headerLocation>
<suppressionsLocation>src/conf/checkstyle-suppressions.xml</suppressionsLocation>
<suppressionsFileExpression>src/conf/checkstyle-suppressions.xml</suppressionsFileExpression>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<excludes>**/generated/**.java,**/jmh_generated/**.java</excludes>
</configuration>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${commons.spotbugs.plugin.version}</version>
<dependencies>
<dependency>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs</artifactId>
<version>${commons.spotbugs.impl.version}</version>
</dependency>
</dependencies>
<configuration>
<excludeFilterFile>src/conf/spotbugs-exclude-filter.xml</excludeFilterFile>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>src/assembly/bin.xml</descriptor>
<descriptor>src/assembly/src.xml</descriptor>
</descriptors>
<tarLongFileMode>gnu</tarLongFileMode>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
<configuration>
<archive combine.children="append">
<manifestEntries>
<Automatic-Module-Name>${commons.module.name}</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-scm-publish-plugin</artifactId>
<configuration>
<ignorePathsToDelete>
<ignorePathToDelete>javadocs</ignorePathToDelete>
</ignorePathsToDelete>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
</configuration>
</plugin>
</plugins>
</build>
<reporting>
<plugins>
<plugin>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle.plugin.version}</version>
<configuration>
<enableRulesSummary>false</enableRulesSummary>
<configLocation>src/conf/checkstyle.xml</configLocation>
<headerLocation>src/conf/checkstyle-header.txt</headerLocation>
<suppressionsLocation>src/conf/checkstyle-suppressions.xml</suppressionsLocation>
<suppressionsFileExpression>src/conf/checkstyle-suppressions.xml</suppressionsFileExpression>
<includeTestSourceDirectory>true</includeTestSourceDirectory>
<excludes>**/generated/**.java,**/jmh_generated/**.java</excludes>
</configuration>
<reportSets>
<reportSet>
<reports>
<report>checkstyle</report>
</reports>
</reportSet>
</reportSets>
</plugin>
<!-- Requires setting 'export MAVEN_OPTS="-Xmx512m" ' -->
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${commons.spotbugs.plugin.version}</version>
<configuration>
<excludeFilterFile>src/conf/spotbugs-exclude-filter.xml</excludeFilterFile>
</configuration>
</plugin>
<plugin>
<groupId>com.github.siom79.japicmp</groupId>
<artifactId>japicmp-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-pmd-plugin</artifactId>
<version>3.19.0</version>
<configuration>
<targetJdk>${maven.compiler.target}</targetJdk>
</configuration>
<reportSets>
<reportSet>
<reports>
<report>pmd</report>
<report>cpd</report>
</reports>
</reportSet>
</reportSets>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>taglist-maven-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<tagListOptions>
<tagClasses>
<tagClass>
<displayName>Needs Work</displayName>
<tags>
<tag>
<matchString>TODO</matchString>
<matchType>exact</matchType>
</tag>
<tag>
<matchString>FIXME</matchString>
<matchType>exact</matchType>
</tag>
<tag>
<matchString>XXX</matchString>
<matchType>exact</matchType>
</tag>
</tags>
</tagClass>
<tagClass>
<displayName>Noteable Markers</displayName>
<tags>
<tag>
<matchString>NOTE</matchString>
<matchType>exact</matchType>
</tag>
<tag>
<matchString>NOPMD</matchString>
<matchType>exact</matchType>
</tag>
<tag>
<matchString>NOSONAR</matchString>
<matchType>exact</matchType>
</tag>
</tags>
</tagClass>
</tagClasses>
</tagListOptions>
</configuration>
</plugin>
</plugins>
</reporting>
<inceptionYear>2014</inceptionYear>
<developers>
<developer>
<id>kinow</id>
<name>Bruno P. Kinoshita</name>
<email>kinow@apache.org</email>
</developer>
<developer>
<id>britter</id>
<name>Benedikt Ritter</name>
<email>britter@apache.org</email>
</developer>
<developer>
<id>chtompki</id>
<name>Rob Tompkins</name>
<email>chtompki@apache.org</email>
</developer>
<developer>
<id>ggregory</id>
<name>Gary Gregory</name>
<email>ggregory at apache.org</email>
<url>https://www.garygregory.com</url>
<organization>The Apache Software Foundation</organization>
<organizationUrl>https://www.apache.org/</organizationUrl>
<roles>
<role>PMC Member</role>
</roles>
<timezone>America/New_York</timezone>
<properties>
<picUrl>https://people.apache.org/~ggregory/img/garydgregory80.png</picUrl>
</properties>
</developer>
<developer>
<id>djones</id>
<name>Duncan Jones</name>
<email>djones@apache.org</email>
</developer>
</developers>
<contributors>
<contributor>
<name>Don Jeba</name>
<email>donjeba@yahoo.com</email>
</contributor>
<contributor>
<name>Sampanna Kahu</name>
</contributor>
<contributor>
<name>Jarek Strzelecki</name>
</contributor>
<contributor>
<name>Lee Adcock</name>
</contributor>
<contributor>
<name>Amey Jadiye</name>
<email>ameyjadiye@gmail.com</email>
</contributor>
<contributor>
<name>Arun Vinud S S</name>
</contributor>
<contributor>
<name>Ioannis Sermetziadis</name>
</contributor>
<contributor>
<name>Jostein Tveit</name>
</contributor>
<contributor>
<name>Luciano Medallia</name>
</contributor>
<contributor>
<name>Jan Martin Keil</name>
</contributor>
<contributor>
<name>Nandor Kollar</name>
</contributor>
<contributor>
<name>Nick Wong</name>
</contributor>
<contributor>
<name>Ali Ghanbari</name>
<url>https://ali-ghanbari.github.io/</url>
</contributor>
</contributors>
<scm>
<connection>scm:git:https://gitbox.apache.org/repos/asf/commons-text</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/commons-text</developerConnection>
<url>https://gitbox.apache.org/repos/asf?p=commons-text.git</url>
</scm>
<issueManagement>
<system>jira</system>
<url>https://issues.apache.org/jira/browse/TEXT</url>
</issueManagement>
<distributionManagement>
<site>
<id>apache.website</id>
<name>Apache Commons Site</name>
<url>scm:svn:https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text/</url>
</site>
</distributionManagement>
<profiles>
<profile>
<id>setup-checkout</id>
<activation>
<file>
<missing>site-content</missing>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>prepare-checkout</id>
<goals>
<goal>run</goal>
</goals>
<phase>pre-site</phase>
<configuration>
<target>
<exec executable="svn">
<arg line="checkout --depth immediates ${commons.scmPubUrl} ${commons.scmPubCheckoutDirectory}"/>
</exec>
<exec executable="svn">
<arg line="update --set-depth exclude ${commons.scmPubCheckoutDirectory}/javadocs"/>
</exec>
<pathconvert pathsep=" " property="dirs">
<dirset dir="${commons.scmPubCheckoutDirectory}" includes="*"/>
</pathconvert>
<exec executable="svn">
<arg line="update --set-depth infinity ${dirs}"/>
</exec>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>java9+</id>
<activation>
<jdk>[9,)</jdk>
</activation>
<properties>
<!-- coverall version 4.3.0 does not work with java 9+, see https://github.com/trautonen/coveralls-maven-plugin/issues/112 -->
<coveralls.skip>true</coveralls.skip>
</properties>
</profile>
<profile>
<id>benchmark</id>
<properties>
<skipTests>true</skipTests>
<benchmark>org.apache</benchmark>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>benchmark</id>
<phase>test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<classpathScope>test</classpathScope>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>org.openjdk.jmh.Main</argument>
<argument>-rf</argument>
<argument>json</argument>
<argument>-rff</argument>
<argument>target/jmh-result.${benchmark}.json</argument>
<argument>${benchmark}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
$ docker run -it -\-rm -v "$HOME/.m2:/root/.m2" -v "$(pwd)":/wd -w /wd/child-1 maven:3.3-jdk-8 mvn dependency:tree
...
[INFO] my.org:child-one:jar:1.3.6
[INFO] +- org.apache.commons:commons-lang3:jar:3.12.0:compile
[INFO] +- org.apache.commons:commons-text:jar:1.12.0:compile
[INFO] +- org.apache.commons:commons-collections4:jar:4.2:compile
[INFO] \- junit:junit:jar:4.12:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.3:test
-->
<parent>
<groupId>my.org</groupId>
<artifactId>parent-one</artifactId>
<version>3.11.0</version>
<relativePath>../parent-1/pom.xml</relativePath>
</parent>
<artifactId>child-one</artifactId>
<!-- maven warns about this, but resolves the property -->
<version>${project.one}.3.6</version>
<packaging>jar</packaging>
<properties>
<commons.lang3.version>3.12.0</commons.lang3.version>
<commons.collections4.version>4.2</commons.collections4.version>
<commons.junit.version>4.12</commons.junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,263 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.commons</groupId>
<artifactId>commons-parent</artifactId>
<version>54</version>
</parent>
<artifactId>commons-text</artifactId>
<version>1.10.0</version>
<name>Apache Commons Text</name>
<description>Apache Commons Text is a library focused on algorithms working on strings.</description>
<url>https://commons.apache.org/proper/commons-text</url>
<properties>
<project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<commons.componentid>text</commons.componentid>
<commons.module.name>org.apache.commons.text</commons.module.name>
<commons.release.version>1.10.0</commons.release.version>
<commons.release.desc>(Java 8+)</commons.release.desc>
<commons.jira.id>TEXT</commons.jira.id>
<commons.jira.pid>12318221</commons.jira.pid>
<commons.site.path>text</commons.site.path>
<commons.scmPubUrl>https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text</commons.scmPubUrl>
<commons.scmPubCheckoutDirectory>site-content</commons.scmPubCheckoutDirectory>
<commons.junit.version>5.9.1</commons.junit.version>
<checkstyle.plugin.version>3.2.0</checkstyle.plugin.version>
<checkstyle.version>9.3</checkstyle.version>
<commons.spotbugs.plugin.version>4.7.2.0</commons.spotbugs.plugin.version>
<commons.spotbugs.impl.version>4.7.2</commons.spotbugs.impl.version>
<commons.pmd.version>3.19.0</commons.pmd.version>
<commons.pmd-impl.version>6.49.0</commons.pmd-impl.version>
<commons.mockito.version>4.8.0</commons.mockito.version>
<commons.jacoco.version>0.8.8</commons.jacoco.version>
<!-- apache-rat-plugin 0.13 and jdepend-maven-plugin 2.0 both fail with LinkageError when generating reports
with maven site plugin 3.11+. However, javadoc 3.4.0+ fails with site plugin versions lower than 3.11. So, we'll
use slightly older site and javadoc versions here in order to be able to generate all reports. -->
<commons.site-plugin.version>3.10.0</commons.site-plugin.version>
<commons.javadoc.version>3.4.1</commons.javadoc.version>
<!-- 22.1.0 requires Java 11 -->
<graalvm.version>22.0.0.2</graalvm.version>
<commons.rng.version>1.4</commons.rng.version>
<commons.japicmp.version>0.16.0</commons.japicmp.version>
<japicmp.skip>false</japicmp.skip>
<jmh.version>1.35</jmh.version>
<commons.project-info.version>3.1.2</commons.project-info.version>
<!-- Commons Release Plugin -->
<commons.bc.version>1.9</commons.bc.version>
<commons.rc.version>RC1</commons.rc.version>
<commons.release.isDistModule>true</commons.release.isDistModule>
<commons.distSvnStagingUrl>scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid}</commons.distSvnStagingUrl>
<commons.releaseManagerName>Gary Gregory</commons.releaseManagerName>
<commons.releaseManagerKey>86fdc7e2a11262cb</commons.releaseManagerKey>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<!-- Use mockito-inline instead of mockito-core to mock and spy on final classes. -->
<artifactId>mockito-inline</artifactId>
<version>${commons.mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${graalvm.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>${graalvm.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-rng-simple</artifactId>
<version>${commons.rng.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<inceptionYear>2014</inceptionYear>
<scm>
<connection>scm:git:https://gitbox.apache.org/repos/asf/commons-text</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/commons-text</developerConnection>
<url>https://gitbox.apache.org/repos/asf?p=commons-text.git</url>
</scm>
<issueManagement>
<system>jira</system>
<url>https://issues.apache.org/jira/browse/TEXT</url>
</issueManagement>
<distributionManagement>
<site>
<id>apache.website</id>
<name>Apache Commons Site</name>
<url>scm:svn:https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text/</url>
</site>
</distributionManagement>
<profiles>
<profile>
<id>setup-checkout</id>
<activation>
<file>
<missing>site-content</missing>
</file>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<id>prepare-checkout</id>
<goals>
<goal>run</goal>
</goals>
<phase>pre-site</phase>
<configuration>
<target>
<exec executable="svn">
<arg line="checkout --depth immediates ${commons.scmPubUrl} ${commons.scmPubCheckoutDirectory}"/>
</exec>
<exec executable="svn">
<arg line="update --set-depth exclude ${commons.scmPubCheckoutDirectory}/javadocs"/>
</exec>
<pathconvert pathsep=" " property="dirs">
<dirset dir="${commons.scmPubCheckoutDirectory}" includes="*"/>
</pathconvert>
<exec executable="svn">
<arg line="update --set-depth infinity ${dirs}"/>
</exec>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>java9+</id>
<activation>
<jdk>[9,)</jdk>
</activation>
<properties>
<!-- coverall version 4.3.0 does not work with java 9+, see https://github.com/trautonen/coveralls-maven-plugin/issues/112 -->
<coveralls.skip>true</coveralls.skip>
</properties>
</profile>
<profile>
<id>benchmark</id>
<properties>
<skipTests>true</skipTests>
<benchmark>org.apache</benchmark>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>benchmark</id>
<phase>test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<classpathScope>test</classpathScope>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>org.openjdk.jmh.Main</argument>
<argument>-rf</argument>
<argument>json</argument>
<argument>-rff</argument>
<argument>target/jmh-result.${benchmark}.json</argument>
<argument>${benchmark}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>contains-child-one</artifactId>
<version>5</version>
<packaging>jar</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>my.org</groupId>
<artifactId>child-one</artifactId>
<version>1.3.6</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>my.org</groupId>
<artifactId>child-one</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>my.org</groupId>
<artifactId>parent-two</artifactId>
<version>13.7.8</version>
<relativePath>../parent-2/pom.xml</relativePath>
</parent>
<artifactId>parent-one</artifactId>
<version>3.11.0</version>
<packaging>pom</packaging>
<properties>
<!-- maven resolves project.parent.version from the _child_ project root context -->
<commons.lang3.version>3.1${project.parent.version}.0</commons.lang3.version>
<commons.collections4.version>4.3</commons.collections4.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons.text.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons.collections4.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${commons.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>my.org</groupId>
<artifactId>parent-two</artifactId>
<version>13.7.8</version>
<packaging>pom</packaging>
<properties>
<commons.lang3.version>3.14.0</commons.lang3.version>
<commons.collections4.version>4.4</commons.collections4.version>
<commons.text.version>1.12.0</commons.text.version>
<commons.junit.version>4.13.2</commons.junit.version>
<commons-exec_subversion>3</commons-exec_subversion>
<project.one>1</project.one>
</properties>
<licenses>
<license>
<name>Eclipse Public License v2.0</name>
<url>https://www.eclipse.org/legal/epl-v20.html</url>
</license>
</licenses>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons.text.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${commons.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons.text.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons.collections4.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${commons.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
$ docker run -it -\-rm -v "$(pwd)":/wd -w /wd/child-1 maven:3.3-jdk-8 mvn dependency:tree
...
[INFO] my.org:child-1:jar:1.3.6
[INFO] +- org.apache.commons:commons-lang3:jar:3.12.0:compile
[INFO] +- org.apache.commons:commons-text:jar:1.12.0:compile
[INFO] +- org.apache.commons:commons-collections4:jar:4.2:compile
[INFO] \- junit:junit:jar:4.12:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.3:test
-->
<parent>
<groupId>my.org</groupId>
<artifactId>parent-one</artifactId>
<version>3.11.0</version>
</parent>
<artifactId>child-one</artifactId>
<!-- maven warns about this, but resolves the property -->
<version>${project.one}.3.6</version>
<packaging>jar</packaging>
<properties>
<commons.lang3.version>3.12.0</commons.lang3.version>
<commons.collections4.version>4.2</commons.collections4.version>
<commons.junit.version>4.12</commons.junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--
$ docker run -it -\-rm -v "$(pwd)":/wd -w /wd/child-2 maven:3.3-jdk-8 mvn dependency:tree
...
[INFO] my.org:child-2:jar:2.1.90
[INFO] +- org.apache.commons:commons-lang3:jar:3.13.0:compile
[INFO] +- org.apache.commons:commons-math3:jar:3.5:compile
[INFO] +- org.apache.commons:commons-exec:jar:1.3:compile
[INFO] +- org.apache.commons:commons-text:jar:1.12.0:compile
[INFO] +- org.apache.commons:commons-collections4:jar:4.2:compile
[INFO] \- junit:junit:jar:4.12:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.3:test
-->
<parent>
<groupId>my.org</groupId>
<artifactId>parent-one</artifactId>
<version>3.11.0</version>
</parent>
<groupId>${project.parent.groupId}</groupId>
<artifactId>child-two</artifactId>
<version>2.1.90</version>
<packaging>jar</packaging>
<properties>
<commons.collections4.version>4.2</commons.collections4.version>
<commons.junit.version>4.12</commons.junit.version>
<project.parent.groupId>my.other.org</project.parent.groupId>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math${project.parent.version}</artifactId>
<version>3.5</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.${commons-exec_subversion}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>my.org</groupId>
<artifactId>parent-two</artifactId>
<version>13.7.8</version>
</parent>
<groupId>my.org</groupId>
<artifactId>parent-one</artifactId>
<version>3.11.0</version>
<packaging>pom</packaging>
<properties>
<!-- maven resolves project.parent.version from the _child_ project root context -->
<commons.lang3.version>3.1${project.parent.version}.0</commons.lang3.version>
<commons.collections4.version>4.3</commons.collections4.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons.text.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons.collections4.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${commons.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>my.org</groupId>
<artifactId>parent-two</artifactId>
<version>13.7.8</version>
<packaging>pom</packaging>
<licenses>
<license>
<name>Eclipse Public License v2.0</name>
<url>https://www.eclipse.org/legal/epl-v20.html</url>
</license>
</licenses>
<properties>
<commons.lang3.version>3.14.0</commons.lang3.version>
<commons.collections4.version>4.4</commons.collections4.version>
<commons.text.version>1.12.0</commons.text.version>
<commons.junit.version>4.13.2</commons.junit.version>
<commons-exec_subversion>3</commons-exec_subversion>
<project.one>1</project.one>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons.lang3.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons.text.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${commons.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>${commons.text.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>${commons.collections4.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${commons.junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements... http://www.apache.org/licenses/LICENSE-2.0
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache</groupId>
<artifactId>apache</artifactId>
<version>27</version>
</parent>
<groupId>org.apache.commons</groupId>
<artifactId>commons-parent</artifactId>
<version>54</version>
<packaging>pom</packaging>
<name>Apache Commons Parent</name>
<description>The Apache Commons Parent POM provides common settings for all Apache Commons components.</description>
<inceptionYear>2006</inceptionYear>
<url>https://commons.apache.org/commons-parent-pom.html</url>
<properties>
<minimalMavenBuildVersion>3.3.9</minimalMavenBuildVersion>
<commons.release.version>${project.version}</commons.release.version>
<commons.release.isDistModule>true</commons.release.isDistModule>
<commons.releaseManagerName>Gary Gregory</commons.releaseManagerName>
<commons.releaseManagerKey>86fdc7e2a11262cb</commons.releaseManagerKey>
<maven.compiler.source>1.3</maven.compiler.source>
<maven.compiler.target>1.3</maven.compiler.target>
<commons.compiler.fork>false</commons.compiler.fork>
<commons.compiler.compilerVersion />
<commons.compiler.javac />
<commons.compiler.javadoc />
<commons.animal-sniffer.version>1.22</commons.animal-sniffer.version>
<commons.animal-sniffer.signature.version>1.0</commons.animal-sniffer.signature.version>
<commons.assembly-plugin.version>3.4.2</commons.assembly-plugin.version>
<commons.build-helper.version>3.3.0</commons.build-helper.version>
<commons.build-plugin.version>1.12</commons.build-plugin.version>
<commons.changes.version>2.12.1</commons.changes.version>
<commons.checkstyle-plugin.version>3.2.0</commons.checkstyle-plugin.version>
<commons.checkstyle.version>9.3</commons.checkstyle.version>
<commons.cobertura.version>2.7</commons.cobertura.version>
<commons.compiler.version>3.10.1</commons.compiler.version>
<commons.coveralls.version>4.3.0</commons.coveralls.version>
<commons.coveralls.timestampFormat>EpochMillis</commons.coveralls.timestampFormat>
<commons.cyclonedx.version>2.7.1</commons.cyclonedx.version>
<commons.spdx.version>0.5.5</commons.spdx.version>
<commons.junit.version>5.9.0</commons.junit.version>
<commons.site-plugin.version>3.12.1</commons.site-plugin.version>
<commons.source-plugin.version>3.2.1</commons.source-plugin.version>
<commons.spotbugs.plugin.version>4.7.2.0</commons.spotbugs.plugin.version>
<commons.wagon-ssh.version>3.5.2</commons.wagon-ssh.version>
<!-- Default values for the download-page generation by commons-build-plugin -->
<commons.release.name>${project.artifactId}-${commons.release.version}</commons.release.name>
<commons.release.desc />
<commons.binary.suffix>-bin</commons.binary.suffix>
<commons.release.2.name>${project.artifactId}-${commons.release.2.version}</commons.release.2.name>
<commons.release.2.desc />
<commons.release.2.binary.suffix>-bin</commons.release.2.binary.suffix>
<commons.release.3.name>${project.artifactId}-${commons.release.3.version}</commons.release.3.name>
<commons.release.3.desc />
<commons.release.3.binary.suffix>-bin</commons.release.3.binary.suffix>
<commons.release.4.desc />
<commons.release.4.binary.suffix>-bin</commons.release.4.binary.suffix>
<!-- Default values for the jacoco-maven-plugin reports -->
<commons.jacoco.classRatio>1.00</commons.jacoco.classRatio>
<commons.jacoco.instructionRatio>0.90</commons.jacoco.instructionRatio>
<commons.jacoco.methodRatio>0.95</commons.jacoco.methodRatio>
<commons.jacoco.branchRatio>0.85</commons.jacoco.branchRatio>
<commons.jacoco.complexityRatio>0.85</commons.jacoco.complexityRatio>
<commons.jacoco.lineRatio>0.90</commons.jacoco.lineRatio>
<commons.jacoco.haltOnFailure>false</commons.jacoco.haltOnFailure>
<commons.componentid>${project.artifactId}</commons.componentid>
<commons.packageId>${project.artifactId}</commons.packageId>
<!-- Configuration properties for the OSGi maven-bundle-plugin -->
<commons.osgi.symbolicName>org.apache.commons.${commons.packageId}</commons.osgi.symbolicName>
<commons.osgi.export>org.apache.commons.*;version=${project.version};-noimport:=true</commons.osgi.export>
<commons.osgi.import>*</commons.osgi.import>
<commons.osgi.dynamicImport />
<commons.osgi.private />
<commons.osgi.excludeDependencies>true</commons.osgi.excludeDependencies>
<!-- location of any manifest file used by maven-jar-plugin -->
<commons.manifestfile>${project.build.directory}/osgi/MANIFEST.MF</commons.manifestfile>
<commons.deployment.protocol>scp</commons.deployment.protocol>
<commons.encoding>iso-8859-1</commons.encoding>
<commons.docEncoding>${commons.encoding}</commons.docEncoding>
<project.build.sourceEncoding>${commons.encoding}</project.build.sourceEncoding>
<project.reporting.outputEncoding>${commons.encoding}</project.reporting.outputEncoding>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ssZ</maven.build.timestamp.format>
<implementation.build>${scmBranch}@r${buildNumber}; ${maven.build.timestamp}</implementation.build>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${commons.junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>site-basic</id>
<properties>
<skipTests>true</skipTests>
<maven.javadoc.skip>true</maven.javadoc.skip>
<cobertura.skip>true</cobertura.skip>
<spotbugs.skip>true</spotbugs.skip>
<checkstyle.skip>true</checkstyle.skip>
<rat.skip>true</rat.skip> <!-- from version 0.12 -->
<jacoco.skip>true</jacoco.skip>
<skipSurefireReport>true</skipSurefireReport>
</properties>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.9.0</version>
<packaging>pom</packaging>
<licenses>
<license>
<name>Eclipse Public License v2.0</name>
<url>https://www.eclipse.org/legal/epl-v20.html</url>
</license>
</licenses>
<developers>
<developer>
<id>bechte</id>
<name>Stefan Bechtold</name>
<email>stefan.bechtold@me.com</email>
</developer>
<developer>
<id>jlink</id>
<name>Johannes Link</name>
<email>business@johanneslink.net</email>
</developer>
<developer>
<id>marcphilipp</id>
<name>Marc Philipp</name>
<email>mail@marcphilipp.de</email>
</developer>
<developer>
<id>mmerdes</id>
<name>Matthias Merdes</name>
<email>matthias.merdes@heidelpay.com</email>
</developer>
<developer>
<id>sbrannen</id>
<name>Sam Brannen</name>
<email>sam@sambrannen.com</email>
</developer>
<developer>
<id>sormuras</id>
<name>Christian Stein</name>
<email>sormuras@gmail.com</email>
</developer>
<developer>
<id>juliette-derancourt</id>
<name>Juliette de Rancourt</name>
<email>derancourt.juliette@gmail.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/junit-team/junit5.git</connection>
<developerConnection>scm:git:git://github.com/junit-team/junit5.git</developerConnection>
<url>https://github.com/junit-team/junit5</url>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-migrationsupport</artifactId>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-console</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-engine</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-jfr</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-reporting</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-api</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-commons</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-engine</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-testkit</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.9.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.9.1</version>
<packaging>pom</packaging>
<name>JUnit 5 (Bill of Materials)</name>
<licenses>
<license>
<name>Eclipse Public License v2.0</name>
<url>https://www.eclipse.org/legal/epl-v20.html</url>
</license>
</licenses>
<developers>
<developer>
<id>bechte</id>
<name>Stefan Bechtold</name>
<email>stefan.bechtold@me.com</email>
</developer>
<developer>
<id>jlink</id>
<name>Johannes Link</name>
<email>business@johanneslink.net</email>
</developer>
<developer>
<id>marcphilipp</id>
<name>Marc Philipp</name>
<email>mail@marcphilipp.de</email>
</developer>
<developer>
<id>mmerdes</id>
<name>Matthias Merdes</name>
<email>matthias.merdes@heidelpay.com</email>
</developer>
<developer>
<id>sbrannen</id>
<name>Sam Brannen</name>
<email>sam@sambrannen.com</email>
</developer>
<developer>
<id>sormuras</id>
<name>Christian Stein</name>
<email>sormuras@gmail.com</email>
</developer>
<developer>
<id>juliette-derancourt</id>
<name>Juliette de Rancourt</name>
<email>derancourt.juliette@gmail.com</email>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/junit-team/junit5.git</connection>
<developerConnection>scm:git:git://github.com/junit-team/junit5.git</developerConnection>
<url>https://github.com/junit-team/junit5</url>
</scm>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-migrationsupport</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-console</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-engine</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-jfr</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-reporting</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-api</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-commons</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite-engine</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-testkit</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.9.1</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@ -1,8 +1,6 @@
package swipl
import (
// "strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"

View File

@ -70,7 +70,7 @@ func NewLicenseFromType(value string, t license.Type) License {
var err error
spdxExpression, err = license.ParseExpression(value)
if err != nil {
log.Trace("unable to parse license expression: %w", err)
log.WithFields("error", err, "expression", value).Trace("unable to parse license expression")
}
}