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

View File

@ -1,24 +1,46 @@
package options package options
import "github.com/anchore/clio" import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/pkg/cataloger/java"
)
type javaConfig struct { type javaConfig struct {
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"` 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"` 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"` 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 { var _ interface {
clio.FieldDescriber clio.FieldDescriber
} = (*javaConfig)(nil) } = (*javaConfig)(nil)
func (o *javaConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { 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 descriptions.Add(&o.UseNetwork, `enables Syft to use the network to fetch version and license information for packages when
currently this enables searching maven-url for license data a parent or imported pom file is not found in the local maven repository.
when running across pom.xml files that could have more information, syft will the pom files are downloaded from the remote Maven repository at 'maven-url'`)
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.MavenURL, `maven repository to use, defaults to Maven central`) 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 { func NewLicense(value string) License {
spdxExpression, err := license.ParseExpression(value) spdxExpression, err := license.ParseExpression(value)
if err != nil { 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{ 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) 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) { func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
t.Helper() t.Helper()

View File

@ -9,8 +9,10 @@ import (
"slices" "slices"
"strings" "strings"
"github.com/vifraa/gopom"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"github.com/anchore/syft/internal"
intFile "github.com/anchore/syft/internal/file" intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
@ -52,6 +54,7 @@ type archiveParser struct {
fileInfo archiveFilename fileInfo archiveFilename
detectNested bool detectNested bool
cfg ArchiveCatalogerConfig cfg ArchiveCatalogerConfig
maven *mavenResolver
} }
type genericArchiveParserAdapter struct { type genericArchiveParserAdapter struct {
@ -106,6 +109,7 @@ func newJavaArchiveParser(reader file.LocationReadCloser, detectNested bool, cfg
fileInfo: newJavaArchiveFilename(currentFilepath), fileInfo: newJavaArchiveFilename(currentFilepath),
detectNested: detectNested, detectNested: detectNested,
cfg: cfg, cfg: cfg,
maven: newMavenResolver(nil, cfg),
}, cleanupFn, nil }, cleanupFn, nil
} }
@ -197,7 +201,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
return nil, err return nil, err
} }
licenses, name, version, err := j.parseLicenses(ctx, manifest) name, version, licenses, err := j.discoverNameVersionLicense(ctx, manifest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -220,7 +224,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package,
}, nil }, 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 // 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 // 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)...) licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...)
@ -231,24 +235,18 @@ func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaMan
3. manifest 3. manifest
4. filename 4. filename
*/ */
name, version, pomLicenses := j.guessMainPackageNameAndVersionFromPomInfo(ctx) groupID, artifactID, version, parsedPom := j.discoverMainPackageFromPomInfo(ctx)
if name == "" { if artifactID == "" {
name = selectName(manifest, j.fileInfo) artifactID = selectName(manifest, j.fileInfo)
} }
if version == "" { if version == "" {
version = selectVersion(manifest, j.fileInfo) 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 { if len(licenses) == 0 {
fileLicenses, err := j.getLicenseFromFileInArchive() fileLicenses, err := j.getLicenseFromFileInArchive()
if err != nil { if err != nil {
return nil, "", "", err return "", "", nil, err
} }
if fileLicenses != nil { if fileLicenses != nil {
licenses = append(licenses, fileLicenses...) 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 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 { if len(licenses) == 0 {
licenses = findLicenseFromJavaMetadata(ctx, name, manifest, version, j, licenses) // 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 { // findLicenseFromJavaMetadata attempts to find license information from all available maven metadata properties and pom info
var groupID = name func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID, artifactID, version string, parsedPom *parsedPomProject, manifest *pkg.JavaManifest) []pkg.License {
if gID := groupIDFromJavaMetadata(name, pkg.JavaArchive{Manifest: manifest}); gID != "" { if groupID == "" {
groupID = gID 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 { if len(pomLicenses) == 0 {
// Try removing the last part of the groupId, as sometimes it duplicates the artifactId // Try removing the last part of the groupId, as sometimes it duplicates the artifactId
packages := strings.Split(groupID, ".") packages := strings.Split(groupID, ".")
groupID = strings.Join(packages[:len(packages)-1], ".") groupID = strings.Join(packages[:len(packages)-1], ".")
pomLicenses = recursivelyFindLicensesFromParentPom(ctx, groupID, name, version, j.cfg) 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")
if len(pomLicenses) > 0 {
pkgLicenses := pkg.NewLicensesFromLocation(j.location, pomLicenses...)
if pkgLicenses != nil {
licenses = append(licenses, pkgLicenses...)
} }
} }
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 { type parsedPomProject struct {
*pkg.JavaPomProject path string
Licenses []pkg.License project *gopom.Project
} }
func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo(ctx context.Context) (name, version string, licenses []pkg.License) { // discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information
pomPropertyMatches := j.fileManifest.GlobMatch(false, pomPropertiesGlob) func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (group, name, version string, parsedPom *parsedPomProject) {
pomMatches := j.fileManifest.GlobMatch(false, pomXMLGlob) var pomProperties pkg.JavaPomProperties
var pomPropertiesObject pkg.JavaPomProperties
var pomProjectObject *parsedPomProject
// Find the pom.properties/pom.xml if the names seem like a plausible match // Find the pom.properties/pom.xml if the names seem like a plausible match
properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, pomPropertyMatches) properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomPropertiesGlob))
projects, _ := pomProjectByParentPath(j.archivePath, j.location, pomMatches) 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 // map of all the artifacts in the pom properties, in order to chek exact match with the filename
artifactsMap := make(map[string]bool) artifactsMap := make(map[string]bool)
@ -312,41 +333,32 @@ func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo(ctx context.Co
for _, parentPath := range parentPaths { for _, parentPath := range parentPaths {
propertiesObj := properties[parentPath] propertiesObj := properties[parentPath]
if artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) { if artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) {
pomPropertiesObject = propertiesObj pomProperties = propertiesObj
if proj, exists := projects[parentPath]; exists { if proj, exists := projects[parentPath]; exists {
pomProjectObject = proj parsedPom = proj
break break
} }
} }
} }
name = pomPropertiesObject.ArtifactID group = pomProperties.GroupID
if name == "" && pomProjectObject != nil { name = pomProperties.ArtifactID
name = pomProjectObject.ArtifactID version = pomProperties.Version
}
version = pomPropertiesObject.Version if parsedPom != nil && parsedPom.project != nil {
if version == "" && pomProjectObject != nil { id := j.maven.resolveMavenID(ctx, parsedPom.project)
version = pomProjectObject.Version if group == "" {
} group = id.GroupID
if j.cfg.UseNetwork { }
if pomProjectObject == nil { if name == "" {
// If we have no pom.xml, check maven central using pom.properties name = id.ArtifactID
parentLicenses := recursivelyFindLicensesFromParentPom(ctx, pomPropertiesObject.GroupID, pomPropertiesObject.ArtifactID, pomPropertiesObject.Version, j.cfg) }
if len(parentLicenses) > 0 { if version == "" {
for _, licenseName := range parentLicenses { version = id.Version
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil))
}
}
} else {
findPomLicenses(ctx, pomProjectObject, j.cfg)
} }
} }
if pomProjectObject != nil { return group, name, version, parsedPom
licenses = pomProjectObject.Licenses
}
return name, version, licenses
} }
func artifactIDMatchesFilename(artifactID, fileName string, artifactsMap map[string]bool) bool { 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) 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 // discoverPkgsFromAllMavenFiles parses Maven POM properties/xml for a given
// parent package, returning all listed Java packages found for each pom // parent package, returning all listed Java packages found for each pom
// properties discovered and potentially updating the given parentPkg with new // 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 { for parentPath, propertiesObj := range properties {
var pomProject *parsedPomProject var parsedPom *parsedPomProject
if proj, exists := projects[parentPath]; exists { 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 { if pkgFromPom != nil {
pkgs = append(pkgs, *pkgFromPom) pkgs = append(pkgs, *pkgFromPom)
} }
@ -422,7 +416,7 @@ func getDigestsFromArchive(archivePath string) ([]file.Digest, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to open archive path (%s): %w", archivePath, err) 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 // grab and assign digest for the entire archive
digests, err := intFile.NewDigestsFromFile(archiveCloser, javaArchiveHashes) digests, err := intFile.NewDigestsFromFile(archiveCloser, javaArchiveHashes)
@ -576,30 +570,26 @@ func pomProjectByParentPath(archivePath string, location file.Location, extractP
projectByParentPath := make(map[string]*parsedPomProject) projectByParentPath := make(map[string]*parsedPomProject)
for filePath, fileContents := range contentsOfMavenProjectFiles { 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 // 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 { if err != nil {
log.WithFields("contents-path", filePath, "location", location.Path()).Warnf("failed to parse pom.xml: %+v", err) log.WithFields("contents-path", filePath, "location", location.Path()).Warnf("failed to parse pom.xml: %+v", err)
continue continue
} }
if pom == nil {
if pomProject == nil {
continue continue
} }
// If we don't have a version, then maybe the parent pom has it... projectByParentPath[path.Dir(filePath)] = &parsedPomProject{
if (pomProject.Parent == nil && pomProject.Version == "") || pomProject.ArtifactID == "" { path: filePath,
// 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 project: pom,
continue
} }
projectByParentPath[path.Dir(filePath)] = pomProject
} }
return projectByParentPath, nil return projectByParentPath, nil
} }
// newPackageFromMavenData processes a single Maven POM properties for a given parent package, returning all listed Java packages found and // 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. // 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 // keep the artifact name within the virtual path if this package does not match the parent package
vPathSuffix := "" vPathSuffix := ""
groupID := "" groupID := ""
@ -622,25 +612,24 @@ func newPackageFromMavenData(ctx context.Context, pomProperties pkg.JavaPomPrope
virtualPath := location.Path() + vPathSuffix virtualPath := location.Path() + vPathSuffix
var pkgPomProject *pkg.JavaPomProject var pkgPomProject *pkg.JavaPomProject
licenses := make([]pkg.License, 0)
if cfg.UseNetwork { var err error
if parsedPomProject == nil { var pomLicenses []gopom.License
// If we have no pom.xml, check maven central using pom.properties if parsedPom == nil {
parentLicenses := recursivelyFindLicensesFromParentPom(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version, cfg) // If we have no pom.xml, check maven central using pom.properties
if len(parentLicenses) > 0 { pomLicenses, err = r.findLicenses(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version)
for _, licenseName := range parentLicenses { } else {
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil)) pkgPomProject = newPomProject(ctx, r, parsedPom.path, parsedPom.project)
} pomLicenses, err = r.resolveLicenses(ctx, parsedPom.project)
}
} else {
findPomLicenses(ctx, parsedPomProject, cfg)
}
} }
if parsedPomProject != nil { if err != nil {
pkgPomProject = parsedPomProject.JavaPomProject log.WithFields("error", err, "mavenID", mavenID{pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version}).Debug("error attempting to resolve licenses")
licenses = append(licenses, parsedPomProject.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{ p := pkg.Package{

View File

@ -5,8 +5,6 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"net/http"
"net/http/httptest"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -20,6 +18,7 @@ import (
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/vifraa/gopom"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
@ -28,61 +27,14 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "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) { func TestSearchMavenForLicenses(t *testing.T) {
mux, url, teardown := setup() url := mockMavenRepo(t)
defer teardown()
tests := []struct { tests := []struct {
name string name string
fixture string fixture string
detectNested bool detectNested bool
config ArchiveCatalogerConfig config ArchiveCatalogerConfig
requestPath string
requestHandlers []handlerPath
expectedLicenses []pkg.License expectedLicenses []pkg.License
}{ }{
{ {
@ -91,23 +43,16 @@ func TestSearchMavenForLicenses(t *testing.T) {
detectNested: false, detectNested: false,
config: ArchiveCatalogerConfig{ config: ArchiveCatalogerConfig{
UseNetwork: true, UseNetwork: true,
UseMavenLocalRepository: false,
MavenBaseURL: url, 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{ expectedLicenses: []pkg.License{
{ {
Type: license.Declared, Type: license.Declared,
Value: `The Apache Software License, Version 2.0`, Value: `The Apache Software License, Version 2.0`,
URLs: []string{
"http://www.apache.org/licenses/LICENSE-2.0.txt",
},
SPDXExpression: ``, SPDXExpression: ``,
}, },
}, },
@ -116,11 +61,6 @@ func TestSearchMavenForLicenses(t *testing.T) {
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { 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: // setup metadata fixture; note:
// this fixture has a pomProjectObject and has a parent object // this fixture has a pomProjectObject and has a parent object
// it has no licenses on either which is the condition for testing // it has no licenses on either which is the condition for testing
@ -138,34 +78,9 @@ func TestSearchMavenForLicenses(t *testing.T) {
defer cleanupFn() defer cleanupFn()
// assert licenses are discovered from upstream // assert licenses are discovered from upstream
_, _, licenses := ap.guessMainPackageNameAndVersionFromPomInfo(context.Background()) _, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
assert.Equal(t, tc.expectedLicenses, licenses) licenses, _ := ap.maven.resolveLicenses(context.Background(), parsedPom.project)
}) assert.Equal(t, tc.expectedLicenses, toPkgLicenses(nil, 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)
}) })
} }
} }
@ -424,10 +339,14 @@ func TestParseJar(t *testing.T) {
test.expected[k] = p test.expected[k] = p
} }
cfg := ArchiveCatalogerConfig{
UseNetwork: false,
UseMavenLocalRepository: false,
}
parser, cleanupFn, err := newJavaArchiveParser(file.LocationReadCloser{ parser, cleanupFn, err := newJavaArchiveParser(file.LocationReadCloser{
Location: file.NewLocation(fixture.Name()), Location: file.NewLocation(fixture.Name()),
ReadCloser: fixture, ReadCloser: fixture,
}, false, ArchiveCatalogerConfig{UseNetwork: false}) }, false, cfg)
defer cleanupFn() defer cleanupFn()
require.NoError(t, err) require.NoError(t, err)
@ -843,26 +762,23 @@ func Test_newPackageFromMavenData(t *testing.T) {
Version: "1.0", Version: "1.0",
}, },
project: &parsedPomProject{ project: &parsedPomProject{
JavaPomProject: &pkg.JavaPomProject{ project: &gopom.Project{
Parent: &pkg.JavaPomParent{ Parent: &gopom.Parent{
GroupID: "some-parent-group-id", GroupID: ptr("some-parent-group-id"),
ArtifactID: "some-parent-artifact-id", ArtifactID: ptr("some-parent-artifact-id"),
Version: "1.0-parent", Version: ptr("1.0-parent"),
}, },
Name: "some-name", Name: ptr("some-name"),
GroupID: "some-group-id", GroupID: ptr("some-group-id"),
ArtifactID: "some-artifact-id", ArtifactID: ptr("some-artifact-id"),
Version: "1.0", Version: ptr("1.0"),
Description: "desc", Description: ptr("desc"),
URL: "aweso.me", URL: ptr("aweso.me"),
}, Licenses: &[]gopom.License{
Licenses: []pkg.License{ {
{ Name: ptr("MIT"),
Value: "MIT", URL: ptr("https://opensource.org/licenses/MIT"),
SPDXExpression: "MIT", },
Type: license.Declared,
URLs: []string{"https://opensource.org/licenses/MIT"},
Locations: file.NewLocationSet(file.NewLocation("some-license-path")),
}, },
}, },
}, },
@ -898,7 +814,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
SPDXExpression: "MIT", SPDXExpression: "MIT",
Type: license.Declared, Type: license.Declared,
URLs: []string{"https://opensource.org/licenses/MIT"}, 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{ Metadata: pkg.JavaArchive{
@ -1122,7 +1038,8 @@ func Test_newPackageFromMavenData(t *testing.T) {
} }
test.expectedParent.Locations = locations 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 { if test.expectedPackage == nil {
require.Nil(t, actualPackage) require.Nil(t, actualPackage)
} else { } else {
@ -1337,6 +1254,8 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
PomProject: &pkg.JavaPomProject{ PomProject: &pkg.JavaPomProject{
Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml", Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml",
ArtifactID: "api-asn1-api", ArtifactID: "api-asn1-api",
GroupID: "org.apache.directory.api",
Version: "2.0.0",
Name: "Apache Directory API ASN.1 API", Name: "Apache Directory API ASN.1 API",
Description: "ASN.1 API", Description: "ASN.1 API",
Parent: &pkg.JavaPomParent{ Parent: &pkg.JavaPomParent{
@ -1388,14 +1307,12 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
func Test_deterministicMatchingPomProperties(t *testing.T) { func Test_deterministicMatchingPomProperties(t *testing.T) {
tests := []struct { tests := []struct {
fixture string fixture string
expectedName string expected mavenID
expectedVersion string
}{ }{
{ {
fixture: "multiple-matching-2.11.5", fixture: "multiple-matching-2.11.5",
expectedName: "multiple-matching-1", expected: mavenID{"org.multiple", "multiple-matching-1", "2.11.5"},
expectedVersion: "2.11.5",
}, },
} }
@ -1415,9 +1332,8 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
defer cleanupFn() defer cleanupFn()
require.NoError(t, err) require.NoError(t, err)
name, version, _ := parser.guessMainPackageNameAndVersionFromPomInfo(context.TODO()) groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO())
require.Equal(t, test.expectedName, name) require.Equal(t, test.expected, mavenID{groupID, artifactID, version})
require.Equal(t, test.expectedVersion, 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 { func generateJavaMetadataJarFixture(t *testing.T, fixtureName string) string {
fixturePath := filepath.Join("test-fixtures/jar-metadata/cache/", fixtureName+".jar") fixturePath := filepath.Join("test-fixtures/jar-metadata/cache/", fixtureName+".jar")
if _, err := os.Stat(fixturePath); !os.IsNotExist(err) { 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. // ptr returns a pointer to the given value
// The returned url is injected into the Config so the client uses the test server. func ptr[T any](value T) *T {
// Tests should register handlers on mux to simulate the expected request/response structure return &value
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
} }

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. // NewPomCataloger returns a cataloger capable of parsing dependencies from a pom.xml file.
// Pom files list dependencies that maybe not be locally installed yet. // Pom files list dependencies that maybe not be locally installed yet.
func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger { func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger {
gap := newGenericArchiveParserAdapter(cfg) return pomXMLCataloger{
cfg: cfg,
return generic.NewCataloger("java-pom-cataloger"). }
WithParserByGlobs(gap.parserPomXML, "**/pom.xml")
} }
// NewGradleLockfileCataloger returns a cataloger capable of parsing dependencies from a gradle.lockfile file. // 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 { type ArchiveCatalogerConfig struct {
cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"` cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"`
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"` 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"` 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"` 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{ return ArchiveCatalogerConfig{
ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(), ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(),
UseNetwork: false, UseNetwork: false,
UseMavenLocalRepository: false,
MavenLocalRepositoryDir: defaultMavenLocalRepoDir(),
MavenBaseURL: mavenBaseURL, MavenBaseURL: mavenBaseURL,
MaxParentRecursiveDepth: 5, MaxParentRecursiveDepth: 0, // unlimited
} }
} }
@ -25,6 +29,16 @@ func (j ArchiveCatalogerConfig) WithUseNetwork(input bool) ArchiveCatalogerConfi
return j 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 { func (j ArchiveCatalogerConfig) WithMavenBaseURL(input string) ArchiveCatalogerConfig {
if input != "" { if input != "" {
j.MavenBaseURL = 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 { func (j ArchiveCatalogerConfig) WithArchiveTraversal(search cataloging.ArchiveSearchConfig, maxDepth int) ArchiveCatalogerConfig {
if maxDepth > 0 { j.MaxParentRecursiveDepth = maxDepth
j.MaxParentRecursiveDepth = maxDepth
}
j.ArchiveSearchConfig = search j.ArchiveSearchConfig = search
return j 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" "bytes"
"context" "context"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"io" "io"
"reflect"
"regexp"
"strings" "strings"
"github.com/saintfish/chardet" "github.com/saintfish/chardet"
"github.com/vifraa/gopom" "github.com/vifraa/gopom"
"golang.org/x/net/html/charset" "golang.org/x/net/html/charset"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "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) { func (p pomXMLCataloger) Name() string {
pom, err := decodePomXML(reader) 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 { if err != nil {
return nil, nil, err return nil, nil, err
} }
var pkgs []pkg.Package r := newMavenResolver(fileResolver, p.cfg)
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
}
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 return pkgs, nil, nil
} }
func parsePomXMLProject(path string, reader io.Reader, location file.Location) (*parsedPomProject, error) { func readPomFromLocation(fileResolver file.Resolver, pomLocation file.Location) (*gopom.Project, error) {
project, err := decodePomXML(reader) contents, err := fileResolver.FileContentsByLocation(pomLocation)
if err != nil { if err != nil {
return nil, err 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 { func processPomXML(ctx context.Context, r *mavenResolver, pom *gopom.Project, loc file.Location) []pkg.Package {
artifactID := safeString(p.ArtifactID) var pkgs []pkg.Package
name := safeString(p.Name)
projectURL := safeString(p.URL)
var licenses []pkg.License pomID := r.resolveMavenID(ctx, pom)
if p.Licenses != nil { for _, dep := range pomDependencies(pom) {
for _, license := range *p.Licenses { depID := r.resolveDependencyID(ctx, pom, dep)
var licenseName, licenseURL string log.WithFields("pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Trace("adding maven pom dependency")
if license.Name != nil {
licenseName = *license.Name
}
if license.URL != nil {
licenseURL = *license.URL
}
if licenseName == "" && licenseURL == "" { p, err := newPackageFromDependency(
continue ctx,
} r,
pom,
licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, licenseURL, &location)) 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 pkgs
return &parsedPomProject{ }
JavaPomProject: &pkg.JavaPomProject{
Path: path, func newPomProject(ctx context.Context, r *mavenResolver, path string, pom *gopom.Project) *pkg.JavaPomProject {
Parent: pomParent(p, p.Parent), id := r.resolveMavenID(ctx, pom)
GroupID: resolveProperty(p, p.GroupID, "groupId"), name := r.getPropertyValue(ctx, pom.Name, pom)
ArtifactID: artifactID, projectURL := r.getPropertyValue(ctx, pom.URL, pom)
Version: resolveProperty(p, p.Version, "version"),
Name: name, log.WithFields("path", path, "artifactID", id.ArtifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml")
Description: cleanDescription(p.Description), return &pkg.JavaPomProject{
URL: projectURL, Path: path,
}, Parent: pomParent(ctx, r, pom),
Licenses: licenses, 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{ m := pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{ PomProperties: &pkg.JavaPomProperties{
GroupID: resolveProperty(pom, dep.GroupID, "groupId"), GroupID: id.GroupID,
ArtifactID: resolveProperty(pom, dep.ArtifactID, "artifactId"), ArtifactID: id.ArtifactID,
Scope: resolveProperty(pom, dep.Scope, "scope"), Scope: r.getPropertyValue(ctx, dep.Scope, pom),
}, },
} }
name := safeString(dep.ArtifactID) var err error
version := resolveProperty(pom, dep.Version, "version") 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 dependencyPom != nil {
if cfg.UseNetwork { depLicenses, _ := r.resolveLicenses(ctx, dependencyPom)
if version == "" { for _, license := range depLicenses {
// If we have no version then let's try to get it from a parent pom DependencyManagement section licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), nil))
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))
}
}
} }
} }
p := pkg.Package{ p := &pkg.Package{
Name: name, Name: id.ArtifactID,
Version: version, Version: id.Version,
Locations: file.NewLocationSet(locations...), Locations: file.NewLocationSet(locations...),
Licenses: pkg.NewLicenseSet(licenses...), Licenses: pkg.NewLicenseSet(licenses...),
PURL: packageURL(name, version, m), PURL: packageURL(id.ArtifactID, id.Version, m),
Language: pkg.Java, 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? 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, Metadata: m,
} }
p.SetID() 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) inputReader, err := getUtf8Reader(content)
if err != nil { 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) 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 // 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 decoder.CharsetReader = charset.NewReaderLabel
if err := decoder.Decode(&project); err != nil { project = &gopom.Project{}
return project, fmt.Errorf("unable to unmarshal pom.xml: %w", err) if err := decoder.Decode(project); err != nil {
return nil, fmt.Errorf("unable to unmarshal pom.xml: %w", err)
} }
return project, nil return project, nil
@ -194,29 +205,28 @@ func getUtf8Reader(content io.Reader) (io.Reader, error) {
return inputReader, nil return inputReader, nil
} }
func pomParent(pom gopom.Project, parent *gopom.Parent) (result *pkg.JavaPomParent) { func pomParent(ctx context.Context, r *mavenResolver, pom *gopom.Project) *pkg.JavaPomParent {
if parent == nil { if pom == nil || pom.Parent == nil {
return nil return nil
} }
artifactID := safeString(parent.ArtifactID) groupID := r.getPropertyValue(ctx, pom.Parent.GroupID, pom)
result = &pkg.JavaPomParent{ artifactID := r.getPropertyValue(ctx, pom.Parent.ArtifactID, pom)
GroupID: resolveProperty(pom, parent.GroupID, "groupId"), version := r.getPropertyValue(ctx, pom.Parent.Version, pom)
if groupID == "" && artifactID == "" && version == "" {
return nil
}
return &pkg.JavaPomParent{
GroupID: groupID,
ArtifactID: artifactID, 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) { func cleanDescription(original string) (cleaned string) {
if original == nil { descriptionLines := strings.Split(original, "\n")
return ""
}
descriptionLines := strings.Split(*original, "\n")
for _, line := range descriptionLines { for _, line := range descriptionLines {
line = strings.TrimSpace(line) line = strings.TrimSpace(line)
if len(line) == 0 { if len(line) == 0 {
@ -226,94 +236,3 @@ func cleanDescription(original *string) (cleaned string) {
} }
return strings.TrimSpace(cleaned) 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 package java
import ( import (
"context"
"encoding/base64" "encoding/base64"
"io" "io"
"os" "os"
@ -16,15 +17,17 @@ import (
"github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/license"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "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 { tests := []struct {
input string dir string
expected []pkg.Package expected []pkg.Package
}{ }{
{ {
input: "test-fixtures/pom/pom.xml", dir: "test-fixtures/pom/local/example-java-app-maven",
expected: []pkg.Package{ expected: []pkg.Package{
{ {
Name: "joda-time", Name: "joda-time",
@ -32,6 +35,7 @@ func Test_parserPomXML(t *testing.T) {
PURL: "pkg:maven/com.joda/joda-time@2.9.2", PURL: "pkg:maven/com.joda/joda-time@2.9.2",
Language: pkg.Java, Language: pkg.Java,
Type: pkg.JavaPkg, Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{ Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{ PomProperties: &pkg.JavaPomProperties{
GroupID: "com.joda", GroupID: "com.joda",
@ -45,6 +49,7 @@ func Test_parserPomXML(t *testing.T) {
PURL: "pkg:maven/junit/junit@4.12", PURL: "pkg:maven/junit/junit@4.12",
Language: pkg.Java, Language: pkg.Java,
Type: pkg.JavaPkg, Type: pkg.JavaPkg,
FoundBy: pomCatalogerName,
Metadata: pkg.JavaArchive{ Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{ PomProperties: &pkg.JavaPomProperties{
GroupID: "junit", GroupID: "junit",
@ -58,19 +63,19 @@ func Test_parserPomXML(t *testing.T) {
} }
for _, test := range tests { 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 { 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{ ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: true, IncludeIndexedArchives: true,
IncludeUnindexedArchives: 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) { func Test_parseCommonsTextPomXMLProject(t *testing.T) {
tests := []struct { tests := []struct {
input string dir string
expected []pkg.Package expected []pkg.Package
}{ }{
{ {
input: "test-fixtures/pom/commons-text.pom.xml", dir: "test-fixtures/pom/local/commons-text-1.10.0",
expected: []pkg.Package{
{ expected: getCommonsTextExpectedPackages(),
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",
},
},
},
},
}, },
} }
for _, test := range tests { 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 { 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{ ArchiveSearchConfig: cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: true, IncludeIndexedArchives: true,
IncludeUnindexedArchives: 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") jarLocation := file.NewLocation("path/to/archive.jar")
tests := []struct { tests := []struct {
name string name string
expected parsedPomProject project *pkg.JavaPomProject
licenses []pkg.License
}{ }{
{ {
name: "go case", name: "go case",
expected: parsedPomProject{ project: &pkg.JavaPomProject{
JavaPomProject: &pkg.JavaPomProject{ Path: "test-fixtures/pom/commons-codec.pom.xml",
Path: "test-fixtures/pom/commons-codec.pom.xml", Parent: &pkg.JavaPomParent{
Parent: &pkg.JavaPomParent{ GroupID: "org.apache.commons",
GroupID: "org.apache.commons", ArtifactID: "commons-parent",
ArtifactID: "commons-parent", Version: "42",
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/",
}, },
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", name: "with license data",
expected: parsedPomProject{ project: &pkg.JavaPomProject{
JavaPomProject: &pkg.JavaPomProject{ Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml",
Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml", Parent: &pkg.JavaPomParent{
Parent: &pkg.JavaPomParent{ GroupID: "org.sonatype.oss",
GroupID: "org.sonatype.oss", ArtifactID: "oss-parent",
ArtifactID: "oss-parent", Version: "7",
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
}, },
Licenses: []pkg.License{ GroupID: "org.neo4j.build.plugins",
{ ArtifactID: "license-maven-plugin",
Value: "The Apache Software License, Version 2.0", Version: "4-SNAPSHOT",
SPDXExpression: "", // TODO: ideally we would parse this title to get Apache-2.0 (created issue #2210 https://github.com/anchore/syft/issues/2210) Name: "license-maven-plugin",
Type: license.Declared, Description: "Maven 2 plugin to check and update license headers in source files",
URLs: []string{"http://www.apache.org/licenses/LICENSE-2.0.txt"}, URL: "http://components.neo4j.org/license-maven-plugin/4-SNAPSHOT",
Locations: file.NewLocationSet(jarLocation), },
}, licenses: []pkg.License{
{ {
Value: "MIT", Value: "The Apache Software License, Version 2.0",
SPDXExpression: "MIT", 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, Type: license.Declared,
Locations: file.NewLocationSet(jarLocation), URLs: []string{"http://www.apache.org/licenses/LICENSE-2.0.txt"},
}, Locations: file.NewLocationSet(jarLocation),
{ },
Type: license.Declared, {
URLs: []string{"https://opensource.org/license/unlicense/"}, Value: "MIT",
Locations: file.NewLocationSet(jarLocation), 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { 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) 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.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", name: "only group ID",
input: &gopom.Parent{ input: &gopom.Parent{
GroupID: stringPointer("org.something"), GroupID: ptr("org.something"),
}, },
expected: &pkg.JavaPomParent{ expected: &pkg.JavaPomParent{
GroupID: "org.something", GroupID: "org.something",
@ -395,7 +365,7 @@ func Test_pomParent(t *testing.T) {
{ {
name: "only artifact ID", name: "only artifact ID",
input: &gopom.Parent{ input: &gopom.Parent{
ArtifactID: stringPointer("something"), ArtifactID: ptr("something"),
}, },
expected: &pkg.JavaPomParent{ expected: &pkg.JavaPomParent{
ArtifactID: "something", ArtifactID: "something",
@ -404,7 +374,7 @@ func Test_pomParent(t *testing.T) {
{ {
name: "only Version", name: "only Version",
input: &gopom.Parent{ input: &gopom.Parent{
Version: stringPointer("something"), Version: ptr("something"),
}, },
expected: &pkg.JavaPomParent{ expected: &pkg.JavaPomParent{
Version: "something", Version: "something",
@ -423,7 +393,7 @@ func Test_pomParent(t *testing.T) {
{ {
name: "unused field", name: "unused field",
input: &gopom.Parent{ input: &gopom.Parent{
RelativePath: stringPointer("something"), RelativePath: ptr("something"),
}, },
expected: nil, expected: nil,
}, },
@ -431,7 +401,8 @@ func Test_pomParent(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { 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 { tests := []struct {
name string name string
property string scanDir string
pom gopom.Project cfg ArchiveCatalogerConfig
expected string expected []pkg.License
}{ }{
{ {
name: "property", name: "local no resolution",
property: "${version.number}", scanDir: containingDir,
pom: gopom.Project{ cfg: ArchiveCatalogerConfig{
Properties: &gopom.Properties{ UseMavenLocalRepository: false,
Entries: map[string]string{ UseNetwork: false,
"version.number": "12.5.0", MavenLocalRepositoryDir: "",
}, MavenBaseURL: "",
},
}, },
expected: "12.5.0", expected: nil,
}, },
{ {
name: "groupId", name: "local all poms",
property: "${project.groupId}", scanDir: localDir,
pom: gopom.Project{ cfg: ArchiveCatalogerConfig{
GroupID: stringPointer("org.some.group"), UseMavenLocalRepository: false,
UseNetwork: false,
}, },
expected: "org.some.group", expected: expectedLicenses,
}, },
{ {
name: "parent groupId", name: "local m2 cache",
property: "${project.parent.groupId}", scanDir: containingDir,
pom: gopom.Project{ cfg: ArchiveCatalogerConfig{
Parent: &gopom.Parent{ UseMavenLocalRepository: true,
GroupID: stringPointer("org.some.parent"), MavenLocalRepositoryDir: localM2,
}, UseNetwork: false,
MavenBaseURL: "",
}, },
expected: "org.some.parent", expected: expectedLicenses,
}, },
{ {
name: "nil pointer halts search", name: "local with network",
property: "${project.parent.groupId}", scanDir: containingDir,
pom: gopom.Project{ cfg: ArchiveCatalogerConfig{
Parent: nil, UseMavenLocalRepository: false,
UseNetwork: true,
MavenBaseURL: mavenURL,
}, },
expected: "", expected: expectedLicenses,
},
{
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: "",
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
resolved := resolveProperty(test.pom, stringPointer(test.property), test.name) cat := NewPomCataloger(test.cfg)
assert.Equal(t, test.expected, resolved)
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) { func Test_getUtf8Reader(t *testing.T) {
tests := []struct { tests := []struct {
name string 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 package swipl
import ( import (
// "strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"

View File

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