mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 10:36:45 +01:00
feat: Java dependency graph information (#3363)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
parent
b505317e10
commit
a00533c836
@ -179,7 +179,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
|||||||
WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir).
|
WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir).
|
||||||
WithUseNetwork(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Java, task.Maven), cfg.Java.UseNetwork)).
|
WithUseNetwork(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Java, task.Maven), cfg.Java.UseNetwork)).
|
||||||
WithMavenBaseURL(cfg.Java.MavenURL).
|
WithMavenBaseURL(cfg.Java.MavenURL).
|
||||||
WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth),
|
WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth).
|
||||||
|
WithResolveTransitiveDependencies(cfg.Java.ResolveTransitiveDependencies),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,22 +6,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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"`
|
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"`
|
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"`
|
||||||
|
ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultJavaConfig() javaConfig {
|
func defaultJavaConfig() javaConfig {
|
||||||
def := java.DefaultArchiveCatalogerConfig()
|
def := java.DefaultArchiveCatalogerConfig()
|
||||||
|
|
||||||
return javaConfig{
|
return javaConfig{
|
||||||
UseNetwork: nil, // this defaults to false, which is the API default
|
UseNetwork: nil, // this defaults to false, which is the API default
|
||||||
MaxParentRecursiveDepth: def.MaxParentRecursiveDepth,
|
MaxParentRecursiveDepth: def.MaxParentRecursiveDepth,
|
||||||
UseMavenLocalRepository: nil, // this defaults to false, which is the API default
|
UseMavenLocalRepository: nil, // this defaults to false, which is the API default
|
||||||
MavenLocalRepositoryDir: def.MavenLocalRepositoryDir,
|
MavenLocalRepositoryDir: def.MavenLocalRepositoryDir,
|
||||||
MavenURL: def.MavenBaseURL,
|
MavenURL: def.MavenBaseURL,
|
||||||
|
ResolveTransitiveDependencies: def.ResolveTransitiveDependencies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,4 +45,5 @@ TIP: If you want to download all required pom files to the local repository with
|
|||||||
build, run 'mvn help:effective-pom' before performing the scan with syft.`)
|
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.
|
descriptions.Add(&o.MavenLocalRepositoryDir, `override the default location of the local Maven repository.
|
||||||
the default is the subdirectory '.m2/repository' in your home directory`)
|
the default is the subdirectory '.m2/repository' in your home directory`)
|
||||||
|
descriptions.Add(&o.ResolveTransitiveDependencies, `resolve transient dependencies such as those defined in a dependency's POM on Maven central`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -265,7 +265,7 @@ var dirOnlyTestCases = []testCase{
|
|||||||
name: "find java packages including pom.xml", // directory scans can include packages that have yet to be installed
|
name: "find java packages including pom.xml", // directory scans can include packages that have yet to be installed
|
||||||
pkgType: pkg.JavaPkg,
|
pkgType: pkg.JavaPkg,
|
||||||
pkgLanguage: pkg.Java,
|
pkgLanguage: pkg.Java,
|
||||||
duplicates: 1, // joda-time is included in both pom.xml AND the .jar collection
|
duplicates: 2, // joda-time and example-java-app-maven are included in both pom.xml AND the .jar collection
|
||||||
pkgInfo: map[string]string{
|
pkgInfo: map[string]string{
|
||||||
"example-java-app-maven": "0.1.0",
|
"example-java-app-maven": "0.1.0",
|
||||||
"joda-time": "2.9.2",
|
"joda-time": "2.9.2",
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/vifraa/gopom"
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
@ -22,6 +21,7 @@ import (
|
|||||||
"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"
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/java/internal/maven"
|
||||||
)
|
)
|
||||||
|
|
||||||
var archiveFormatGlobs = []string{
|
var archiveFormatGlobs = []string{
|
||||||
@ -57,7 +57,7 @@ type archiveParser struct {
|
|||||||
fileInfo archiveFilename
|
fileInfo archiveFilename
|
||||||
detectNested bool
|
detectNested bool
|
||||||
cfg ArchiveCatalogerConfig
|
cfg ArchiveCatalogerConfig
|
||||||
maven *mavenResolver
|
maven *maven.Resolver
|
||||||
licenseScanner licenses.Scanner
|
licenseScanner licenses.Scanner
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,15 +69,20 @@ func newGenericArchiveParserAdapter(cfg ArchiveCatalogerConfig) genericArchivePa
|
|||||||
return genericArchiveParserAdapter{cfg: cfg}
|
return genericArchiveParserAdapter{cfg: cfg}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives.
|
// parseJavaArchive is a parser function for java archive contents, returning all Java libraries and nested archives
|
||||||
func (gap genericArchiveParserAdapter) parseJavaArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
func (gap genericArchiveParserAdapter) parseJavaArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
|
return gap.processJavaArchive(ctx, reader, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processJavaArchive processes an archive for java contents, returning all Java libraries and nested archives
|
||||||
|
func (gap genericArchiveParserAdapter) processJavaArchive(ctx context.Context, reader file.LocationReadCloser, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
parser, cleanupFn, err := newJavaArchiveParser(ctx, reader, true, gap.cfg)
|
parser, cleanupFn, err := newJavaArchiveParser(ctx, reader, true, gap.cfg)
|
||||||
// note: even on error, we should always run cleanup functions
|
// note: even on error, we should always run cleanup functions
|
||||||
defer cleanupFn()
|
defer cleanupFn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return parser.parse(ctx)
|
return parser.parse(ctx, parentPkg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// uniquePkgKey creates a unique string to identify the given package.
|
// uniquePkgKey creates a unique string to identify the given package.
|
||||||
@ -115,34 +120,62 @@ func newJavaArchiveParser(ctx context.Context, reader file.LocationReadCloser, d
|
|||||||
fileInfo: newJavaArchiveFilename(currentFilepath),
|
fileInfo: newJavaArchiveFilename(currentFilepath),
|
||||||
detectNested: detectNested,
|
detectNested: detectNested,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
maven: newMavenResolver(nil, cfg),
|
maven: maven.NewResolver(nil, cfg.mavenConfig()),
|
||||||
licenseScanner: licenseScanner,
|
licenseScanner: licenseScanner,
|
||||||
}, cleanupFn, nil
|
}, cleanupFn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the loaded archive and return all packages found.
|
// parse the loaded archive and return all packages found.
|
||||||
func (j *archiveParser) parse(ctx context.Context) ([]pkg.Package, []artifact.Relationship, error) {
|
func (j *archiveParser) parse(ctx context.Context, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
var pkgs []pkg.Package
|
var pkgs []pkg.Package
|
||||||
var relationships []artifact.Relationship
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
// find the parent package from the java manifest
|
// find the parent package from the java manifest
|
||||||
parentPkg, err := j.discoverMainPackage(ctx)
|
mainPkg, err := j.discoverMainPackage(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("could not generate package from %s: %w", j.location, err)
|
return nil, nil, fmt.Errorf("could not generate package from %s: %w", j.location, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// find aux packages from pom.properties/pom.xml and potentially modify the existing parentPkg
|
// find aux packages from pom.properties/pom.xml and potentially modify the existing parentPkg
|
||||||
// NOTE: we cannot generate sha1 digests from packages discovered via pom.properties/pom.xml
|
// NOTE: we cannot generate sha1 digests from packages discovered via pom.properties/pom.xml
|
||||||
auxPkgs, err := j.discoverPkgsFromAllMavenFiles(ctx, parentPkg)
|
// IMPORTANT!: discoverPkgsFromAllMavenFiles may change mainPkg information, so needs to be called before SetID and before copying for relationships, etc.
|
||||||
|
auxPkgs, err := j.discoverPkgsFromAllMavenFiles(ctx, mainPkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
pkgs = append(pkgs, auxPkgs...)
|
|
||||||
|
if mainPkg != nil {
|
||||||
|
finalizePackage(mainPkg)
|
||||||
|
pkgs = append(pkgs, *mainPkg)
|
||||||
|
|
||||||
|
if parentPkg != nil {
|
||||||
|
relationships = append(relationships, artifact.Relationship{
|
||||||
|
From: *mainPkg,
|
||||||
|
To: *parentPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range auxPkgs {
|
||||||
|
auxPkg := &auxPkgs[i]
|
||||||
|
|
||||||
|
finalizePackage(auxPkg)
|
||||||
|
pkgs = append(pkgs, *auxPkg)
|
||||||
|
|
||||||
|
if mainPkg != nil {
|
||||||
|
relationships = append(relationships, artifact.Relationship{
|
||||||
|
From: *auxPkg,
|
||||||
|
To: *mainPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var errs error
|
var errs error
|
||||||
if j.detectNested {
|
if j.detectNested {
|
||||||
// find nested java archive packages
|
// find nested java archive packages
|
||||||
nestedPkgs, nestedRelationships, err := j.discoverPkgsFromNestedArchives(ctx, parentPkg)
|
nestedPkgs, nestedRelationships, err := j.discoverPkgsFromNestedArchives(ctx, mainPkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = unknown.Append(errs, j.location, err)
|
errs = unknown.Append(errs, j.location, err)
|
||||||
}
|
}
|
||||||
@ -157,29 +190,6 @@ func (j *archiveParser) parse(ctx context.Context) ([]pkg.Package, []artifact.Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lastly, add the parent package to the list (assuming the parent exists)
|
|
||||||
if parentPkg != nil {
|
|
||||||
pkgs = append([]pkg.Package{*parentPkg}, pkgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add pURLs to all packages found
|
|
||||||
// note: since package information may change after initial creation when parsing multiple locations within the
|
|
||||||
// jar, we wait until the conclusion of the parsing process before synthesizing pURLs.
|
|
||||||
for i := range pkgs {
|
|
||||||
p := &pkgs[i]
|
|
||||||
if m, ok := p.Metadata.(pkg.JavaArchive); ok {
|
|
||||||
p.PURL = packageURL(p.Name, p.Version, m)
|
|
||||||
|
|
||||||
if strings.Contains(p.PURL, "io.jenkins.plugins") || strings.Contains(p.PURL, "org.jenkins-ci.plugins") {
|
|
||||||
p.Type = pkg.JenkinsPluginPkg
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithFields("package", p.String()).Warn("unable to extract java metadata to generate purl")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.SetID()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pkgs) == 0 {
|
if len(pkgs) == 0 {
|
||||||
errs = unknown.Appendf(errs, j.location, "no package identified in archive")
|
errs = unknown.Appendf(errs, j.location, "no package identified in archive")
|
||||||
}
|
}
|
||||||
@ -187,6 +197,22 @@ func (j *archiveParser) parse(ctx context.Context) ([]pkg.Package, []artifact.Re
|
|||||||
return pkgs, relationships, errs
|
return pkgs, relationships, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// finalizePackage potentially updates some package information such as classifying the package as a Jenkins plugin,
|
||||||
|
// sets the PURL, and calls p.SetID()
|
||||||
|
func finalizePackage(p *pkg.Package) {
|
||||||
|
if m, ok := p.Metadata.(pkg.JavaArchive); ok {
|
||||||
|
p.PURL = packageURL(p.Name, p.Version, m)
|
||||||
|
|
||||||
|
if strings.Contains(p.PURL, "io.jenkins.plugins") || strings.Contains(p.PURL, "org.jenkins-ci.plugins") {
|
||||||
|
p.Type = pkg.JenkinsPluginPkg
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.WithFields("package", p.String()).Warn("unable to extract java metadata to generate purl")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.SetID()
|
||||||
|
}
|
||||||
|
|
||||||
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
|
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
|
||||||
func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, error) {
|
func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, error) {
|
||||||
// search and parse java manifest files
|
// search and parse java manifest files
|
||||||
@ -297,18 +323,18 @@ func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var pomLicenses []gopom.License
|
var pomLicenses []maven.License
|
||||||
if parsedPom != nil {
|
if parsedPom != nil {
|
||||||
pomLicenses, err = j.maven.resolveLicenses(ctx, parsedPom.project)
|
pomLicenses, err = j.maven.ResolveLicenses(ctx, parsedPom.project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("error", err, "mavenID", j.maven.getMavenID(ctx, parsedPom.project)).Trace("error attempting to resolve pom licenses")
|
log.WithFields("error", err, "mavenID", j.maven.ResolveID(ctx, parsedPom.project)).Trace("error attempting to resolve pom licenses")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && len(pomLicenses) == 0 {
|
if err == nil && len(pomLicenses) == 0 {
|
||||||
pomLicenses, err = j.maven.findLicenses(ctx, groupID, artifactID, version)
|
pomLicenses, err = j.maven.FindLicenses(ctx, groupID, artifactID, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("error", err, "mavenID", mavenID{groupID, artifactID, version}).Trace("error attempting to find licenses")
|
log.WithFields("error", err, "mavenID", maven.NewID(groupID, artifactID, version)).Trace("error attempting to find licenses")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,26 +342,37 @@ func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID
|
|||||||
// 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, err = j.maven.findLicenses(ctx, groupID, artifactID, version)
|
pomLicenses, err = j.maven.FindLicenses(ctx, groupID, artifactID, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("error", err, "mavenID", mavenID{groupID, artifactID, version}).Trace("error attempting to find sub-group licenses")
|
log.WithFields("error", err, "mavenID", maven.NewID(groupID, artifactID, version)).Trace("error attempting to find sub-group licenses")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return toPkgLicenses(&j.location, pomLicenses)
|
return toPkgLicenses(&j.location, pomLicenses)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPkgLicenses(location *file.Location, licenses []gopom.License) []pkg.License {
|
func toPkgLicenses(location *file.Location, licenses []maven.License) []pkg.License {
|
||||||
var out []pkg.License
|
var out []pkg.License
|
||||||
for _, license := range licenses {
|
for _, license := range licenses {
|
||||||
out = append(out, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), location))
|
name := ""
|
||||||
|
if license.Name != nil {
|
||||||
|
name = *license.Name
|
||||||
|
}
|
||||||
|
url := ""
|
||||||
|
if license.URL != nil {
|
||||||
|
url = *license.URL
|
||||||
|
}
|
||||||
|
if name == "" && url == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, pkg.NewLicenseFromFields(name, url, location))
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
type parsedPomProject struct {
|
type parsedPomProject struct {
|
||||||
path string
|
path string
|
||||||
project *gopom.Project
|
project *maven.Project
|
||||||
}
|
}
|
||||||
|
|
||||||
// discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information
|
// discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information
|
||||||
@ -370,7 +407,7 @@ func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (gro
|
|||||||
version = pomProperties.Version
|
version = pomProperties.Version
|
||||||
|
|
||||||
if parsedPom != nil && parsedPom.project != nil {
|
if parsedPom != nil && parsedPom.project != nil {
|
||||||
id := j.maven.getMavenID(ctx, parsedPom.project)
|
id := j.maven.ResolveID(ctx, parsedPom.project)
|
||||||
if group == "" {
|
if group == "" {
|
||||||
group = id.GroupID
|
group = id.GroupID
|
||||||
}
|
}
|
||||||
@ -507,7 +544,7 @@ func discoverPkgsFromOpeners(ctx context.Context, location file.Location, opener
|
|||||||
var relationships []artifact.Relationship
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
for pathWithinArchive, archiveOpener := range openers {
|
for pathWithinArchive, archiveOpener := range openers {
|
||||||
nestedPkgs, nestedRelationships, err := discoverPkgsFromOpener(ctx, location, pathWithinArchive, archiveOpener, cfg)
|
nestedPkgs, nestedRelationships, err := discoverPkgsFromOpener(ctx, location, pathWithinArchive, archiveOpener, cfg, parentPkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("location", location.Path()).Warnf("unable to discover java packages from opener: %+v", err)
|
log.WithFields("location", location.Path()).Warnf("unable to discover java packages from opener: %+v", err)
|
||||||
continue
|
continue
|
||||||
@ -531,7 +568,7 @@ func discoverPkgsFromOpeners(ctx context.Context, location file.Location, opener
|
|||||||
}
|
}
|
||||||
|
|
||||||
// discoverPkgsFromOpener finds Java archives within the given file.
|
// discoverPkgsFromOpener finds Java archives within the given file.
|
||||||
func discoverPkgsFromOpener(ctx context.Context, location file.Location, pathWithinArchive string, archiveOpener intFile.Opener, cfg ArchiveCatalogerConfig) ([]pkg.Package, []artifact.Relationship, error) {
|
func discoverPkgsFromOpener(ctx context.Context, location file.Location, pathWithinArchive string, archiveOpener intFile.Opener, cfg ArchiveCatalogerConfig, parentPkg *pkg.Package) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
archiveReadCloser, err := archiveOpener.Open()
|
archiveReadCloser, err := archiveOpener.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
|
return nil, nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
|
||||||
@ -546,10 +583,10 @@ func discoverPkgsFromOpener(ctx context.Context, location file.Location, pathWit
|
|||||||
nestedLocation := file.NewLocationFromCoordinates(location.Coordinates)
|
nestedLocation := file.NewLocationFromCoordinates(location.Coordinates)
|
||||||
nestedLocation.AccessPath = nestedPath
|
nestedLocation.AccessPath = nestedPath
|
||||||
gap := newGenericArchiveParserAdapter(cfg)
|
gap := newGenericArchiveParserAdapter(cfg)
|
||||||
nestedPkgs, nestedRelationships, err := gap.parseJavaArchive(ctx, nil, nil, file.LocationReadCloser{
|
nestedPkgs, nestedRelationships, err := gap.processJavaArchive(ctx, file.LocationReadCloser{
|
||||||
Location: nestedLocation,
|
Location: nestedLocation,
|
||||||
ReadCloser: archiveReadCloser,
|
ReadCloser: archiveReadCloser,
|
||||||
})
|
}, parentPkg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("unable to process nested java archive (%s): %w", pathWithinArchive, err)
|
return nil, nil, fmt.Errorf("unable to process nested java archive (%s): %w", pathWithinArchive, err)
|
||||||
}
|
}
|
||||||
@ -595,7 +632,7 @@ 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
|
||||||
pom, err := decodePomXML(strings.NewReader(fileContents))
|
pom, err := maven.ParsePomXML(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
|
||||||
@ -614,7 +651,7 @@ func pomProjectByParentPath(archivePath string, location file.Location, extractP
|
|||||||
|
|
||||||
// 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, r *mavenResolver, pomProperties pkg.JavaPomProperties, parsedPom *parsedPomProject, parentPkg *pkg.Package, location file.Location) *pkg.Package {
|
func newPackageFromMavenData(ctx context.Context, r *maven.Resolver, 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 := ""
|
||||||
@ -639,23 +676,20 @@ func newPackageFromMavenData(ctx context.Context, r *mavenResolver, pomPropertie
|
|||||||
var pkgPomProject *pkg.JavaPomProject
|
var pkgPomProject *pkg.JavaPomProject
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var pomLicenses []gopom.License
|
var pomLicenses []maven.License
|
||||||
if parsedPom == nil {
|
if parsedPom == nil {
|
||||||
// If we have no pom.xml, check maven central using pom.properties
|
// If we have no pom.xml, check maven central using pom.properties
|
||||||
pomLicenses, err = r.findLicenses(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version)
|
pomLicenses, err = r.FindLicenses(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version)
|
||||||
} else {
|
} else {
|
||||||
pkgPomProject = newPomProject(ctx, r, parsedPom.path, parsedPom.project)
|
pkgPomProject = newPomProject(ctx, r, parsedPom.path, parsedPom.project)
|
||||||
pomLicenses, err = r.resolveLicenses(ctx, parsedPom.project)
|
pomLicenses, err = r.ResolveLicenses(ctx, parsedPom.project)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("error", err, "mavenID", mavenID{pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version}).Trace("error attempting to resolve licenses")
|
log.WithFields("error", err, "mavenID", maven.NewID(pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version)).Trace("error attempting to resolve licenses")
|
||||||
}
|
}
|
||||||
|
|
||||||
licenses := make([]pkg.License, 0)
|
licenseSet := pkg.NewLicenseSet(toPkgLicenses(&location, pomLicenses)...)
|
||||||
for _, license := range pomLicenses {
|
|
||||||
licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), &location))
|
|
||||||
}
|
|
||||||
|
|
||||||
p := pkg.Package{
|
p := pkg.Package{
|
||||||
Name: pomProperties.ArtifactID,
|
Name: pomProperties.ArtifactID,
|
||||||
@ -663,7 +697,7 @@ func newPackageFromMavenData(ctx context.Context, r *mavenResolver, pomPropertie
|
|||||||
Locations: file.NewLocationSet(
|
Locations: file.NewLocationSet(
|
||||||
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||||
),
|
),
|
||||||
Licenses: pkg.NewLicenseSet(licenses...),
|
Licenses: licenseSet,
|
||||||
Language: pkg.Java,
|
Language: pkg.Java,
|
||||||
Type: pomProperties.PkgTypeIndicated(),
|
Type: pomProperties.PkgTypeIndicated(),
|
||||||
Metadata: pkg.JavaArchive{
|
Metadata: pkg.JavaArchive{
|
||||||
|
|||||||
@ -18,7 +18,6 @@ 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/internal/licenses"
|
"github.com/anchore/syft/internal/licenses"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
@ -26,10 +25,12 @@ 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/pkg/cataloger/java/internal/maven"
|
||||||
|
maventest "github.com/anchore/syft/syft/pkg/cataloger/java/internal/maven/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSearchMavenForLicenses(t *testing.T) {
|
func TestSearchMavenForLicenses(t *testing.T) {
|
||||||
url := mockMavenRepo(t)
|
url := maventest.MockRepo(t, "internal/maven/test-fixtures/maven-repo")
|
||||||
|
|
||||||
ctx := licenses.SetContextLicenseScanner(context.Background(), licenses.TestingOnlyScanner())
|
ctx := licenses.SetContextLicenseScanner(context.Background(), licenses.TestingOnlyScanner())
|
||||||
|
|
||||||
@ -83,8 +84,8 @@ func TestSearchMavenForLicenses(t *testing.T) {
|
|||||||
|
|
||||||
// assert licenses are discovered from upstream
|
// assert licenses are discovered from upstream
|
||||||
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
|
_, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background())
|
||||||
licenses, _ := ap.maven.resolveLicenses(context.Background(), parsedPom.project)
|
resolvedLicenses, _ := ap.maven.ResolveLicenses(context.Background(), parsedPom.project)
|
||||||
assert.Equal(t, tc.expectedLicenses, toPkgLicenses(nil, licenses))
|
assert.Equal(t, tc.expectedLicenses, toPkgLicenses(nil, resolvedLicenses))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -362,9 +363,11 @@ func TestParseJar(t *testing.T) {
|
|||||||
defer cleanupFn()
|
defer cleanupFn()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
actual, _, err := parser.parse(context.Background())
|
actual, _, err := parser.parse(context.Background(), nil)
|
||||||
if test.wantErr != nil {
|
if test.wantErr != nil {
|
||||||
test.wantErr(t, err)
|
test.wantErr(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(actual) != len(test.expected) {
|
if len(actual) != len(test.expected) {
|
||||||
@ -635,10 +638,10 @@ func TestParseNestedJar(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
|
gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{})
|
||||||
|
|
||||||
actual, _, err := gap.parseJavaArchive(context.Background(), nil, nil, file.LocationReadCloser{
|
actual, _, err := gap.processJavaArchive(context.Background(), file.LocationReadCloser{
|
||||||
Location: file.NewLocation(fixture.Name()),
|
Location: file.NewLocation(fixture.Name()),
|
||||||
ReadCloser: fixture,
|
ReadCloser: fixture,
|
||||||
})
|
}, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
expectedNameVersionPairSet := strset.New()
|
expectedNameVersionPairSet := strset.New()
|
||||||
@ -776,8 +779,8 @@ func Test_newPackageFromMavenData(t *testing.T) {
|
|||||||
Version: "1.0",
|
Version: "1.0",
|
||||||
},
|
},
|
||||||
project: &parsedPomProject{
|
project: &parsedPomProject{
|
||||||
project: &gopom.Project{
|
project: &maven.Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &maven.Parent{
|
||||||
GroupID: ptr("some-parent-group-id"),
|
GroupID: ptr("some-parent-group-id"),
|
||||||
ArtifactID: ptr("some-parent-artifact-id"),
|
ArtifactID: ptr("some-parent-artifact-id"),
|
||||||
Version: ptr("1.0-parent"),
|
Version: ptr("1.0-parent"),
|
||||||
@ -788,7 +791,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
|
|||||||
Version: ptr("1.0"),
|
Version: ptr("1.0"),
|
||||||
Description: ptr("desc"),
|
Description: ptr("desc"),
|
||||||
URL: ptr("aweso.me"),
|
URL: ptr("aweso.me"),
|
||||||
Licenses: &[]gopom.License{
|
Licenses: &[]maven.License{
|
||||||
{
|
{
|
||||||
Name: ptr("MIT"),
|
Name: ptr("MIT"),
|
||||||
URL: ptr("https://opensource.org/licenses/MIT"),
|
URL: ptr("https://opensource.org/licenses/MIT"),
|
||||||
@ -1052,7 +1055,7 @@ func Test_newPackageFromMavenData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
test.expectedParent.Locations = locations
|
test.expectedParent.Locations = locations
|
||||||
|
|
||||||
r := newMavenResolver(nil, DefaultArchiveCatalogerConfig())
|
r := maven.NewResolver(nil, maven.DefaultConfig())
|
||||||
actualPackage := newPackageFromMavenData(context.Background(), r, test.props, test.project, test.parent, file.NewLocation(virtualPath))
|
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)
|
||||||
@ -1093,6 +1096,76 @@ func Test_artifactIDMatchesFilename(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Test_parseJavaArchive_regressions(t *testing.T) {
|
func Test_parseJavaArchive_regressions(t *testing.T) {
|
||||||
|
apiAll := pkg.Package{
|
||||||
|
Name: "api-all",
|
||||||
|
Version: "2.0.0",
|
||||||
|
Type: pkg.JavaPkg,
|
||||||
|
Language: pkg.Java,
|
||||||
|
PURL: "pkg:maven/org.apache.directory.api/api-all@2.0.0",
|
||||||
|
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar")),
|
||||||
|
Metadata: pkg.JavaArchive{
|
||||||
|
VirtualPath: "test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar",
|
||||||
|
Manifest: &pkg.JavaManifest{
|
||||||
|
Main: []pkg.KeyValue{
|
||||||
|
{
|
||||||
|
Key: "Manifest-Version",
|
||||||
|
Value: "1.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "Built-By",
|
||||||
|
Value: "elecharny",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "Created-By",
|
||||||
|
Value: "Apache Maven 3.6.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "Build-Jdk",
|
||||||
|
Value: "1.8.0_191",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PomProperties: &pkg.JavaPomProperties{
|
||||||
|
Path: "META-INF/maven/org.apache.directory.api/api-all/pom.properties",
|
||||||
|
GroupID: "org.apache.directory.api",
|
||||||
|
ArtifactID: "api-all",
|
||||||
|
Version: "2.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiAsn1Api := pkg.Package{
|
||||||
|
Name: "api-asn1-api",
|
||||||
|
Version: "2.0.0",
|
||||||
|
PURL: "pkg:maven/org.apache.directory.api/api-asn1-api@2.0.0",
|
||||||
|
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar")),
|
||||||
|
Type: pkg.JavaPkg,
|
||||||
|
Language: pkg.Java,
|
||||||
|
Metadata: pkg.JavaArchive{
|
||||||
|
VirtualPath: "test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar:org.apache.directory.api:api-asn1-api",
|
||||||
|
PomProperties: &pkg.JavaPomProperties{
|
||||||
|
Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.properties",
|
||||||
|
GroupID: "org.apache.directory.api",
|
||||||
|
ArtifactID: "api-asn1-api",
|
||||||
|
Version: "2.0.0",
|
||||||
|
},
|
||||||
|
PomProject: &pkg.JavaPomProject{
|
||||||
|
Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml",
|
||||||
|
ArtifactID: "api-asn1-api",
|
||||||
|
GroupID: "org.apache.directory.api",
|
||||||
|
Version: "2.0.0",
|
||||||
|
Name: "Apache Directory API ASN.1 API",
|
||||||
|
Description: "ASN.1 API",
|
||||||
|
Parent: &pkg.JavaPomParent{
|
||||||
|
GroupID: "org.apache.directory.api",
|
||||||
|
ArtifactID: "api-asn1-parent",
|
||||||
|
Version: "2.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Parent: &apiAll,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
fixtureName string
|
fixtureName string
|
||||||
@ -1214,73 +1287,14 @@ func Test_parseJavaArchive_regressions(t *testing.T) {
|
|||||||
fixtureName: "api-all-2.0.0-sources",
|
fixtureName: "api-all-2.0.0-sources",
|
||||||
assignParent: true,
|
assignParent: true,
|
||||||
expectedPkgs: []pkg.Package{
|
expectedPkgs: []pkg.Package{
|
||||||
|
apiAll,
|
||||||
|
apiAsn1Api,
|
||||||
|
},
|
||||||
|
expectedRelationships: []artifact.Relationship{
|
||||||
{
|
{
|
||||||
Name: "api-all",
|
From: apiAsn1Api,
|
||||||
Version: "2.0.0",
|
To: apiAll,
|
||||||
Type: pkg.JavaPkg,
|
Type: artifact.DependencyOfRelationship,
|
||||||
Language: pkg.Java,
|
|
||||||
PURL: "pkg:maven/org.apache.directory.api/api-all@2.0.0",
|
|
||||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar")),
|
|
||||||
Metadata: pkg.JavaArchive{
|
|
||||||
VirtualPath: "test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar",
|
|
||||||
Manifest: &pkg.JavaManifest{
|
|
||||||
Main: []pkg.KeyValue{
|
|
||||||
{
|
|
||||||
Key: "Manifest-Version",
|
|
||||||
Value: "1.0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "Built-By",
|
|
||||||
Value: "elecharny",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "Created-By",
|
|
||||||
Value: "Apache Maven 3.6.0",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Key: "Build-Jdk",
|
|
||||||
Value: "1.8.0_191",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PomProperties: &pkg.JavaPomProperties{
|
|
||||||
Path: "META-INF/maven/org.apache.directory.api/api-all/pom.properties",
|
|
||||||
GroupID: "org.apache.directory.api",
|
|
||||||
ArtifactID: "api-all",
|
|
||||||
Version: "2.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "api-asn1-api",
|
|
||||||
Version: "2.0.0",
|
|
||||||
PURL: "pkg:maven/org.apache.directory.api/api-asn1-api@2.0.0",
|
|
||||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar")),
|
|
||||||
Type: pkg.JavaPkg,
|
|
||||||
Language: pkg.Java,
|
|
||||||
Metadata: pkg.JavaArchive{
|
|
||||||
VirtualPath: "test-fixtures/jar-metadata/cache/api-all-2.0.0-sources.jar:org.apache.directory.api:api-asn1-api",
|
|
||||||
PomProperties: &pkg.JavaPomProperties{
|
|
||||||
Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.properties",
|
|
||||||
GroupID: "org.apache.directory.api",
|
|
||||||
ArtifactID: "api-asn1-api",
|
|
||||||
Version: "2.0.0",
|
|
||||||
},
|
|
||||||
PomProject: &pkg.JavaPomProject{
|
|
||||||
Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml",
|
|
||||||
ArtifactID: "api-asn1-api",
|
|
||||||
GroupID: "org.apache.directory.api",
|
|
||||||
Version: "2.0.0",
|
|
||||||
Name: "Apache Directory API ASN.1 API",
|
|
||||||
Description: "ASN.1 API",
|
|
||||||
Parent: &pkg.JavaPomParent{
|
|
||||||
GroupID: "org.apache.directory.api",
|
|
||||||
ArtifactID: "api-asn1-parent",
|
|
||||||
Version: "2.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Parent: nil,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -1364,11 +1378,11 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
fixture string
|
fixture string
|
||||||
expected mavenID
|
expected maven.ID
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
fixture: "multiple-matching-2.11.5",
|
fixture: "multiple-matching-2.11.5",
|
||||||
expected: mavenID{"org.multiple", "multiple-matching-1", "2.11.5"},
|
expected: maven.NewID("org.multiple", "multiple-matching-1", "2.11.5"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1391,7 +1405,7 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO())
|
groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO())
|
||||||
require.Equal(t, test.expected, mavenID{groupID, artifactID, version})
|
require.Equal(t, test.expected, maven.NewID(groupID, artifactID, version))
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -1401,10 +1415,7 @@ func Test_deterministicMatchingPomProperties(t *testing.T) {
|
|||||||
func assignParent(parent *pkg.Package, childPackages ...pkg.Package) {
|
func assignParent(parent *pkg.Package, childPackages ...pkg.Package) {
|
||||||
for i, jp := range childPackages {
|
for i, jp := range childPackages {
|
||||||
if v, ok := jp.Metadata.(pkg.JavaArchive); ok {
|
if v, ok := jp.Metadata.(pkg.JavaArchive); ok {
|
||||||
parent := *parent
|
v.Parent = parent
|
||||||
// PURL are not calculated after the fact for parent
|
|
||||||
parent.PURL = ""
|
|
||||||
v.Parent = &parent
|
|
||||||
childPackages[i].Metadata = v
|
childPackages[i].Metadata = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
package java
|
package java
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/cataloging"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
const mavenBaseURL = "https://repo1.maven.org/maven2"
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/java/internal/maven"
|
||||||
|
)
|
||||||
|
|
||||||
type ArchiveCatalogerConfig struct {
|
type ArchiveCatalogerConfig struct {
|
||||||
cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"`
|
cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"`
|
||||||
@ -11,16 +14,19 @@ type ArchiveCatalogerConfig struct {
|
|||||||
MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"`
|
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"`
|
||||||
|
ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig {
|
func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig {
|
||||||
|
mavenCfg := maven.DefaultConfig()
|
||||||
return ArchiveCatalogerConfig{
|
return ArchiveCatalogerConfig{
|
||||||
ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(),
|
ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(),
|
||||||
UseNetwork: false,
|
UseNetwork: mavenCfg.UseNetwork,
|
||||||
UseMavenLocalRepository: false,
|
UseMavenLocalRepository: mavenCfg.UseLocalRepository,
|
||||||
MavenLocalRepositoryDir: defaultMavenLocalRepoDir(),
|
MavenLocalRepositoryDir: mavenCfg.LocalRepositoryDir,
|
||||||
MavenBaseURL: mavenBaseURL,
|
MavenBaseURL: strings.Join(mavenCfg.Repositories, ","),
|
||||||
MaxParentRecursiveDepth: 0, // unlimited
|
MaxParentRecursiveDepth: mavenCfg.MaxParentRecursiveDepth,
|
||||||
|
ResolveTransitiveDependencies: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +52,23 @@ func (j ArchiveCatalogerConfig) WithMavenBaseURL(input string) ArchiveCatalogerC
|
|||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j ArchiveCatalogerConfig) WithResolveTransitiveDependencies(resolveTransitiveDependencies bool) ArchiveCatalogerConfig {
|
||||||
|
j.ResolveTransitiveDependencies = resolveTransitiveDependencies
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
|
||||||
func (j ArchiveCatalogerConfig) WithArchiveTraversal(search cataloging.ArchiveSearchConfig, maxDepth int) ArchiveCatalogerConfig {
|
func (j ArchiveCatalogerConfig) WithArchiveTraversal(search cataloging.ArchiveSearchConfig, maxDepth int) ArchiveCatalogerConfig {
|
||||||
j.MaxParentRecursiveDepth = maxDepth
|
j.MaxParentRecursiveDepth = maxDepth
|
||||||
j.ArchiveSearchConfig = search
|
j.ArchiveSearchConfig = search
|
||||||
return j
|
return j
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j ArchiveCatalogerConfig) mavenConfig() maven.Config {
|
||||||
|
return maven.Config{
|
||||||
|
UseNetwork: j.UseNetwork,
|
||||||
|
UseLocalRepository: j.UseMavenLocalRepository,
|
||||||
|
LocalRepositoryDir: j.MavenLocalRepositoryDir,
|
||||||
|
Repositories: strings.Split(j.MavenBaseURL, ","),
|
||||||
|
MaxParentRecursiveDepth: j.MaxParentRecursiveDepth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package java
|
package maven
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@ -15,6 +15,35 @@ import (
|
|||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const mavenBaseURL = "https://repo1.maven.org/maven2"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// UseNetwork instructs the maven resolver to use network operations to resolve maven artifacts
|
||||||
|
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
|
||||||
|
|
||||||
|
// Repositories are the set of remote repositories the network resolution should use
|
||||||
|
Repositories []string `yaml:"maven-repositories" json:"maven-repositories" mapstructure:"maven-repositories"`
|
||||||
|
|
||||||
|
// UseLocalRepository instructs the maven resolver to look in the host maven cache, usually ~/.m2/repository
|
||||||
|
UseLocalRepository bool `yaml:"use-maven-local-repository" json:"use-maven-local-repository" mapstructure:"use-maven-local-repository"`
|
||||||
|
|
||||||
|
// LocalRepositoryDir is an alternate directory to use to look up the local repository
|
||||||
|
LocalRepositoryDir string `yaml:"maven-local-repository-dir" json:"maven-local-repository-dir" mapstructure:"maven-local-repository-dir"`
|
||||||
|
|
||||||
|
// MaxParentRecursiveDepth allows for a maximum depth to use when recursively resolving parent poms and other information, 0 disables any maximum
|
||||||
|
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
UseNetwork: false,
|
||||||
|
Repositories: []string{mavenBaseURL},
|
||||||
|
UseLocalRepository: false,
|
||||||
|
LocalRepositoryDir: defaultMavenLocalRepoDir(),
|
||||||
|
MaxParentRecursiveDepth: 0, // unlimited
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// defaultMavenLocalRepoDir gets default location of the Maven local repository, generally at <USER HOME DIR>/.m2/repository
|
// defaultMavenLocalRepoDir gets default location of the Maven local repository, generally at <USER HOME DIR>/.m2/repository
|
||||||
func defaultMavenLocalRepoDir() string {
|
func defaultMavenLocalRepoDir() string {
|
||||||
homeDir, err := homedir.Dir()
|
homeDir, err := homedir.Dir()
|
||||||
@ -49,15 +78,6 @@ func getSettingsXMLLocalRepository(settingsXML io.Reader) string {
|
|||||||
return s.LocalRepository
|
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
|
// remotePomURL returns a URL to download a POM from a remote repository
|
||||||
func remotePomURL(repoURL, groupID, artifactID, version string) (requestURL string, err error) {
|
func remotePomURL(repoURL, groupID, artifactID, version string) (requestURL string, err error) {
|
||||||
// groupID needs to go from maven.org -> maven/org
|
// groupID needs to go from maven.org -> maven/org
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package java
|
package maven
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_defaultMavenLocalRepoDir(t *testing.T) {
|
func Test_defaultMavenLocalRepoDir(t *testing.T) {
|
||||||
@ -69,7 +71,7 @@ func Test_getSettingsXmlLocalRepository(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.expected, func(t *testing.T) {
|
t.Run(test.expected, func(t *testing.T) {
|
||||||
f, _ := os.Open(test.file)
|
f, _ := os.Open(test.file)
|
||||||
defer f.Close()
|
defer internal.CloseAndLogError(f, test.file)
|
||||||
got := getSettingsXMLLocalRepository(f)
|
got := getSettingsXMLLocalRepository(f)
|
||||||
require.Equal(t, test.expected, got)
|
require.Equal(t, test.expected, got)
|
||||||
})
|
})
|
||||||
@ -85,7 +87,7 @@ func Test_remotePomURL(t *testing.T) {
|
|||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "formatMavenURL correctly assembles the pom URL",
|
name: "remotePomURL correctly assembles the pom URL",
|
||||||
groupID: "org.springframework.boot",
|
groupID: "org.springframework.boot",
|
||||||
artifactID: "spring-boot-starter-test",
|
artifactID: "spring-boot-starter-test",
|
||||||
version: "3.1.5",
|
version: "3.1.5",
|
||||||
67
syft/pkg/cataloger/java/internal/maven/pom_parser.go
Normal file
67
syft/pkg/cataloger/java/internal/maven/pom_parser.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package maven
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/saintfish/chardet"
|
||||||
|
"github.com/vifraa/gopom"
|
||||||
|
"golang.org/x/net/html/charset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Project = gopom.Project
|
||||||
|
Properties = gopom.Properties
|
||||||
|
Parent = gopom.Parent
|
||||||
|
Dependency = gopom.Dependency
|
||||||
|
License = gopom.License
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParsePomXML 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 ParsePomXML(content io.Reader) (project *Project, err error) {
|
||||||
|
inputReader, err := getUtf8Reader(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to read pom.xml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder := xml.NewDecoder(inputReader)
|
||||||
|
// when an xml file has a character set declaration (e.g. '<?xml version="1.0" encoding="ISO-8859-1"?>') read that and use the correct decoder
|
||||||
|
decoder.CharsetReader = charset.NewReaderLabel
|
||||||
|
|
||||||
|
project = &Project{}
|
||||||
|
if err := decoder.Decode(project); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to unmarshal pom.xml: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUtf8Reader(content io.Reader) (io.Reader, error) {
|
||||||
|
pomContents, err := io.ReadAll(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
detector := chardet.NewTextDetector()
|
||||||
|
detection, err := detector.DetectBest(pomContents)
|
||||||
|
|
||||||
|
var inputReader io.Reader
|
||||||
|
if err == nil && detection != nil {
|
||||||
|
if detection.Charset == "UTF-8" {
|
||||||
|
inputReader = bytes.NewReader(pomContents)
|
||||||
|
} else {
|
||||||
|
inputReader, err = charset.NewReaderLabel(detection.Charset, bytes.NewReader(pomContents))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to get encoding: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we could not detect the encoding, but we want a valid file to read. Replace unreadable
|
||||||
|
// characters with the UTF-8 replacement character.
|
||||||
|
inputReader = strings.NewReader(strings.ToValidUTF8(string(pomContents), "<22>"))
|
||||||
|
}
|
||||||
|
return inputReader, nil
|
||||||
|
}
|
||||||
93
syft/pkg/cataloger/java/internal/maven/pom_parser_test.go
Normal file
93
syft/pkg/cataloger/java/internal/maven/pom_parser_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package maven
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getUtf8Reader(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
contents string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "unknown encoding",
|
||||||
|
// random binary contents
|
||||||
|
contents: "BkiJz02JyEWE0nXR6TH///9NicpJweEETIucJIgAAABJicxPjQwhTY1JCE05WQh0BU2J0eunTYshTIusJIAAAAAPHwBNOeV1BUUx2+tWTIlUJDhMiUwkSEyJRCQgSIl8JFBMiQ==",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(tt.contents))
|
||||||
|
|
||||||
|
got, err := getUtf8Reader(decoder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
gotBytes, err := io.ReadAll(got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
// if we couldn't decode the section as UTF-8, we should get a replacement character
|
||||||
|
assert.Contains(t, string(gotBytes), "<22>")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_decodePomXML_surviveNonUtf8Encoding(t *testing.T) {
|
||||||
|
// regression for https://github.com/anchore/syft/issues/2044
|
||||||
|
|
||||||
|
// we are storing the base64 contents of the pom.xml file. We are doing this to prevent accidental changes to the
|
||||||
|
// file, which is extremely important for this test.
|
||||||
|
|
||||||
|
// for instance, even changing a single character in the file and saving in an IntelliJ IDE will automatically
|
||||||
|
// convert the file to UTF-8, which will break this test:
|
||||||
|
|
||||||
|
// xxd with the original pom.xml
|
||||||
|
// 00000780: 6964 3e0d 0a20 2020 2020 2020 2020 2020 id>..
|
||||||
|
// 00000790: 203c 6e61 6d65 3e4a e972 f46d 6520 4d69 <name>J.r.me Mi
|
||||||
|
// 000007a0: 7263 3c2f 6e61 6d65 3e0d 0a20 2020 2020 rc</name>..
|
||||||
|
|
||||||
|
// xxd with the pom.xml converted to UTF-8 (from a simple change with IntelliJ)
|
||||||
|
// 00000780: 6964 3e0d 0a20 2020 2020 2020 2020 2020 id>..
|
||||||
|
// 00000790: 203c 6e61 6d65 3e4a efbf bd72 efbf bd6d <name>J...r...m
|
||||||
|
// 000007a0: 6520 4d69 7263 3c2f 6e61 6d65 3e0d 0a20 e Mirc</name>..
|
||||||
|
|
||||||
|
// Note that the name "Jérôme Mirc" was originally interpreted as "J.r.me Mi" and after the save
|
||||||
|
// is now encoded as "J...r...m" which is not what we want (note the extra bytes for each non UTF-8 character.
|
||||||
|
// The original 0xe9 byte (é) was converted to 0xefbfbd (<28>) which is the UTF-8 replacement character.
|
||||||
|
// This is quite silly on the part of IntelliJ, but it is what it is.
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
fixture string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "undeclared encoding",
|
||||||
|
fixture: "test-fixtures/undeclared-iso-8859-encoded-pom.xml.base64",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "declared encoding",
|
||||||
|
fixture: "test-fixtures/declared-iso-8859-encoded-pom.xml.base64",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
fh, err := os.Open(c.fixture)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer internal.CloseAndLogError(fh, c.fixture)
|
||||||
|
|
||||||
|
decoder := base64.NewDecoder(base64.StdEncoding, fh)
|
||||||
|
|
||||||
|
proj, err := ParsePomXML(decoder)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, proj.Developers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package java
|
package maven
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -24,56 +24,69 @@ import (
|
|||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mavenID is the unique identifier for a package in Maven
|
// ID is the unique identifier for a package in Maven
|
||||||
type mavenID struct {
|
type ID struct {
|
||||||
GroupID string
|
GroupID string
|
||||||
ArtifactID string
|
ArtifactID string
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m mavenID) String() string {
|
func NewID(groupID, artifactID, version string) ID {
|
||||||
|
return ID{
|
||||||
|
GroupID: groupID,
|
||||||
|
ArtifactID: artifactID,
|
||||||
|
Version: version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m ID) String() string {
|
||||||
return fmt.Sprintf("(groupId: %s artifactId: %s version: %s)", m.GroupID, m.ArtifactID, m.Version)
|
return fmt.Sprintf("(groupId: %s artifactId: %s version: %s)", m.GroupID, m.ArtifactID, m.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valid indicates that the given maven ID has values for groupId, artifactId, and version
|
||||||
|
func (m ID) Valid() bool {
|
||||||
|
return m.GroupID != "" && m.ArtifactID != "" && m.Version != ""
|
||||||
|
}
|
||||||
|
|
||||||
var expressionMatcher = regexp.MustCompile("[$][{][^}]+[}]")
|
var expressionMatcher = regexp.MustCompile("[$][{][^}]+[}]")
|
||||||
|
|
||||||
// mavenResolver is a short-lived utility to resolve maven poms from multiple sources, including:
|
// Resolver 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
|
// the scanned filesystem, local maven cache directories, remote maven repositories, and the syft cache
|
||||||
type mavenResolver struct {
|
type Resolver struct {
|
||||||
cfg ArchiveCatalogerConfig
|
cfg Config
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
resolved map[mavenID]*gopom.Project
|
resolved map[ID]*Project
|
||||||
remoteRequestTimeout time.Duration
|
remoteRequestTimeout time.Duration
|
||||||
checkedLocalRepo bool
|
checkedLocalRepo bool
|
||||||
// fileResolver and pomLocations are used to resolve parent poms by relativePath
|
// fileResolver and pomLocations are used to resolve parent poms by relativePath
|
||||||
fileResolver file.Resolver
|
fileResolver file.Resolver
|
||||||
pomLocations map[*gopom.Project]file.Location
|
pomLocations map[*Project]file.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMavenResolver constructs a new mavenResolver with the given configuration.
|
// NewResolver constructs a new Resolver with the given configuration.
|
||||||
// NOTE: the fileResolver is optional and if provided will be used to resolve parent poms by relative path
|
// 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 {
|
func NewResolver(fileResolver file.Resolver, cfg Config) *Resolver {
|
||||||
return &mavenResolver{
|
return &Resolver{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
cache: cache.GetManager().GetCache("java/maven/repo", "v1"),
|
cache: cache.GetManager().GetCache("java/maven/repo", "v1"),
|
||||||
resolved: map[mavenID]*gopom.Project{},
|
resolved: map[ID]*Project{},
|
||||||
remoteRequestTimeout: time.Second * 10,
|
remoteRequestTimeout: time.Second * 10,
|
||||||
fileResolver: fileResolver,
|
fileResolver: fileResolver,
|
||||||
pomLocations: map[*gopom.Project]file.Location{},
|
pomLocations: map[*Project]file.Location{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPropertyValue gets property values by emulating maven property resolution logic, looking in the project's variables
|
// ResolveProperty 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}.
|
// as well as supporting the project expressions like ${project.parent.groupId}.
|
||||||
// Properties which are not resolved result in empty string ""
|
// Properties which are not resolved result in empty string ""
|
||||||
func (r *mavenResolver) getPropertyValue(ctx context.Context, propertyValue *string, resolutionContext ...*gopom.Project) string {
|
func (r *Resolver) ResolveProperty(ctx context.Context, pom *Project, propertyValue *string) string {
|
||||||
return r.resolvePropertyValue(ctx, propertyValue, nil, resolutionContext...)
|
return r.resolvePropertyValue(ctx, propertyValue, nil, pom)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvePropertyValue resolves property values by emulating maven property resolution logic, looking in the project's variables
|
// resolvePropertyValue resolves 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}.
|
// as well as supporting the project expressions like ${project.parent.groupId}.
|
||||||
// Properties which are not resolved result in empty string ""
|
// Properties which are not resolved result in empty string ""
|
||||||
func (r *mavenResolver) resolvePropertyValue(ctx context.Context, propertyValue *string, resolvingProperties []string, resolutionContext ...*gopom.Project) string {
|
func (r *Resolver) resolvePropertyValue(ctx context.Context, propertyValue *string, resolvingProperties []string, resolutionContext ...*Project) string {
|
||||||
if propertyValue == nil {
|
if propertyValue == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@ -86,7 +99,7 @@ func (r *mavenResolver) resolvePropertyValue(ctx context.Context, propertyValue
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveExpression resolves an expression, which may be a plain string or a string with ${ property.references }
|
// 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, resolvingProperties []string) (string, error) {
|
func (r *Resolver) resolveExpression(ctx context.Context, resolutionContext []*Project, expression string, resolvingProperties []string) (string, error) {
|
||||||
log.Tracef("resolving expression: '%v' in context: %v", expression, resolutionContext)
|
log.Tracef("resolving expression: '%v' in context: %v", expression, resolutionContext)
|
||||||
|
|
||||||
var errs error
|
var errs error
|
||||||
@ -103,7 +116,7 @@ func (r *mavenResolver) resolveExpression(ctx context.Context, resolutionContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveProperty resolves properties recursively from the root project
|
// resolveProperty resolves properties recursively from the root project
|
||||||
func (r *mavenResolver) resolveProperty(ctx context.Context, resolutionContext []*gopom.Project, propertyExpression string, resolvingProperties []string) (string, error) {
|
func (r *Resolver) resolveProperty(ctx context.Context, resolutionContext []*Project, propertyExpression string, resolvingProperties []string) (string, error) {
|
||||||
// prevent cycles
|
// prevent cycles
|
||||||
if slices.Contains(resolvingProperties, propertyExpression) {
|
if slices.Contains(resolvingProperties, propertyExpression) {
|
||||||
return "", fmt.Errorf("cycle detected resolving: %s", propertyExpression)
|
return "", fmt.Errorf("cycle detected resolving: %s", propertyExpression)
|
||||||
@ -146,7 +159,7 @@ func (r *mavenResolver) resolveProperty(ctx context.Context, resolutionContext [
|
|||||||
// resolveProjectProperty resolves properties on the project
|
// resolveProjectProperty resolves properties on the project
|
||||||
//
|
//
|
||||||
//nolint:gocognit
|
//nolint:gocognit
|
||||||
func (r *mavenResolver) resolveProjectProperty(ctx context.Context, resolutionContext []*gopom.Project, pom *gopom.Project, propertyExpression string, resolving []string) (string, error) {
|
func (r *Resolver) resolveProjectProperty(ctx context.Context, resolutionContext []*Project, pom *Project, propertyExpression string, resolving []string) (string, error) {
|
||||||
// see if we have a project.x expression and process this based
|
// see if we have a project.x expression and process this based
|
||||||
// on the xml tags in gopom
|
// on the xml tags in gopom
|
||||||
parts := strings.Split(propertyExpression, ".")
|
parts := strings.Split(propertyExpression, ".")
|
||||||
@ -210,19 +223,48 @@ func (r *mavenResolver) resolveProjectProperty(ctx context.Context, resolutionCo
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMavenID creates a new mavenID from a pom, resolving parent information as necessary
|
// ResolveParent resolves the parent definition, and returns a POM for the parent, which is possibly incomplete, or nil
|
||||||
func (r *mavenResolver) getMavenID(ctx context.Context, resolutionContext ...*gopom.Project) mavenID {
|
func (r *Resolver) ResolveParent(ctx context.Context, pom *Project) (*Project, error) {
|
||||||
return r.resolveMavenID(ctx, nil, resolutionContext...)
|
if pom == nil || pom.Parent == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent, err := r.resolveParent(ctx, pom)
|
||||||
|
if parent != nil {
|
||||||
|
return parent, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groupID := r.ResolveProperty(ctx, pom, pom.Parent.GroupID)
|
||||||
|
if groupID == "" {
|
||||||
|
groupID = r.ResolveProperty(ctx, pom, pom.GroupID)
|
||||||
|
}
|
||||||
|
artifactID := r.ResolveProperty(ctx, pom, pom.Parent.ArtifactID)
|
||||||
|
version := r.ResolveProperty(ctx, pom, pom.Parent.Version)
|
||||||
|
|
||||||
|
if artifactID != "" && version != "" {
|
||||||
|
return &Project{
|
||||||
|
GroupID: &groupID,
|
||||||
|
ArtifactID: &artifactID,
|
||||||
|
Version: &version,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("unsufficient information to create a parent pom project, id: %s", NewID(groupID, artifactID, version))
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveMavenID creates a new mavenID from a pom, resolving parent information as necessary
|
// ResolveID creates an ID from a pom, resolving parent information as necessary
|
||||||
func (r *mavenResolver) resolveMavenID(ctx context.Context, resolvingProperties []string, resolutionContext ...*gopom.Project) mavenID {
|
func (r *Resolver) ResolveID(ctx context.Context, pom *Project) ID {
|
||||||
|
return r.resolveID(ctx, nil, pom)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveID creates a new ID from a pom, resolving parent information as necessary
|
||||||
|
func (r *Resolver) resolveID(ctx context.Context, resolvingProperties []string, resolutionContext ...*Project) ID {
|
||||||
if len(resolutionContext) == 0 || resolutionContext[0] == nil {
|
if len(resolutionContext) == 0 || resolutionContext[0] == nil {
|
||||||
return mavenID{}
|
return ID{}
|
||||||
}
|
}
|
||||||
pom := resolutionContext[len(resolutionContext)-1] // get topmost pom
|
pom := resolutionContext[len(resolutionContext)-1] // get topmost pom
|
||||||
if pom == nil {
|
if pom == nil {
|
||||||
return mavenID{}
|
return ID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupID := r.resolvePropertyValue(ctx, pom.GroupID, resolvingProperties, resolutionContext...)
|
groupID := r.resolvePropertyValue(ctx, pom.GroupID, resolvingProperties, resolutionContext...)
|
||||||
@ -239,50 +281,50 @@ func (r *mavenResolver) resolveMavenID(ctx context.Context, resolvingProperties
|
|||||||
version = r.resolvePropertyValue(ctx, pom.Parent.Version, resolvingProperties, resolutionContext...)
|
version = r.resolvePropertyValue(ctx, pom.Parent.Version, resolvingProperties, resolutionContext...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return mavenID{groupID, artifactID, version}
|
return ID{groupID, artifactID, version}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveDependencyID creates a new mavenID from a dependency element in a pom, resolving information as necessary
|
// ResolveDependencyID creates an ID 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 {
|
func (r *Resolver) ResolveDependencyID(ctx context.Context, pom *Project, dep Dependency) ID {
|
||||||
if pom == nil {
|
if pom == nil {
|
||||||
return mavenID{}
|
return ID{}
|
||||||
}
|
}
|
||||||
|
|
||||||
groupID := r.getPropertyValue(ctx, dep.GroupID, pom)
|
groupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, pom)
|
||||||
artifactID := r.getPropertyValue(ctx, dep.ArtifactID, pom)
|
artifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, pom)
|
||||||
version := r.getPropertyValue(ctx, dep.Version, pom)
|
version := r.resolvePropertyValue(ctx, dep.Version, nil, pom)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if version == "" {
|
if version == "" {
|
||||||
version, err = r.findInheritedVersion(ctx, pom, groupID, artifactID)
|
version, err = r.resolveInheritedVersion(ctx, pom, groupID, artifactID)
|
||||||
}
|
}
|
||||||
|
|
||||||
depID := mavenID{groupID, artifactID, version}
|
depID := ID{groupID, artifactID, version}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("error", err, "mavenID", r.getMavenID(ctx, pom), "dependencyID", depID)
|
log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", depID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return depID
|
return depID
|
||||||
}
|
}
|
||||||
|
|
||||||
// findPom gets a pom from cache, local repository, or from a remote Maven repository depending on configuration
|
// 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) {
|
func (r *Resolver) FindPom(ctx context.Context, groupID, artifactID, version string) (*Project, error) {
|
||||||
if groupID == "" || artifactID == "" || version == "" {
|
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)
|
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}
|
id := ID{groupID, artifactID, version}
|
||||||
pom := r.resolved[id]
|
existingPom := r.resolved[id]
|
||||||
|
|
||||||
if pom != nil {
|
if existingPom != nil {
|
||||||
return pom, nil
|
return existingPom, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs error
|
var errs error
|
||||||
|
|
||||||
// try to resolve first from local maven repo
|
// try to resolve first from local maven repo
|
||||||
if r.cfg.UseMavenLocalRepository {
|
if r.cfg.UseLocalRepository {
|
||||||
pom, err := r.findPomInLocalRepository(groupID, artifactID, version)
|
pom, err := r.findPomInLocalRepository(groupID, artifactID, version)
|
||||||
if pom != nil {
|
if pom != nil {
|
||||||
r.resolved[id] = pom
|
r.resolved[id] = pom
|
||||||
@ -292,8 +334,8 @@ func (r *mavenResolver) findPom(ctx context.Context, groupID, artifactID, versio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolve via network maven repository
|
// resolve via network maven repository
|
||||||
if pom == nil && r.cfg.UseNetwork {
|
if r.cfg.UseNetwork {
|
||||||
pom, err := r.findPomInRemoteRepository(ctx, groupID, artifactID, version)
|
pom, err := r.findPomInRemotes(ctx, groupID, artifactID, version)
|
||||||
if pom != nil {
|
if pom != nil {
|
||||||
r.resolved[id] = pom
|
r.resolved[id] = pom
|
||||||
return pom, nil
|
return pom, nil
|
||||||
@ -305,35 +347,50 @@ func (r *mavenResolver) findPom(ctx context.Context, groupID, artifactID, versio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findPomInLocalRepository attempts to get the POM from the users local maven repository
|
// findPomInLocalRepository attempts to get the POM from the users local maven repository
|
||||||
func (r *mavenResolver) findPomInLocalRepository(groupID, artifactID, version string) (*gopom.Project, error) {
|
func (r *Resolver) findPomInLocalRepository(groupID, artifactID, version string) (*Project, error) {
|
||||||
groupPath := filepath.Join(strings.Split(groupID, ".")...)
|
groupPath := filepath.Join(strings.Split(groupID, ".")...)
|
||||||
pomFilePath := filepath.Join(r.cfg.MavenLocalRepositoryDir, groupPath, artifactID, version, artifactID+"-"+version+".pom")
|
pomFilePath := filepath.Join(r.cfg.LocalRepositoryDir, groupPath, artifactID, version, artifactID+"-"+version+".pom")
|
||||||
pomFile, err := os.Open(pomFilePath)
|
pomFile, err := os.Open(pomFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !r.checkedLocalRepo && errors.Is(err, os.ErrNotExist) {
|
if !r.checkedLocalRepo && errors.Is(err, os.ErrNotExist) {
|
||||||
r.checkedLocalRepo = true
|
r.checkedLocalRepo = true
|
||||||
// check if the directory exists at all, and if not just stop trying to resolve local maven files
|
// 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)
|
fi, err := os.Stat(r.cfg.LocalRepositoryDir)
|
||||||
if errors.Is(err, os.ErrNotExist) || !fi.IsDir() {
|
if errors.Is(err, os.ErrNotExist) || !fi.IsDir() {
|
||||||
log.WithFields("error", err, "repositoryDir", r.cfg.MavenLocalRepositoryDir).
|
log.WithFields("error", err, "repositoryDir", r.cfg.LocalRepositoryDir).
|
||||||
Info("local maven repository is not a readable directory, stopping local resolution")
|
Info("local maven repository is not a readable directory, stopping local resolution")
|
||||||
r.cfg.UseMavenLocalRepository = false
|
r.cfg.UseLocalRepository = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer internal.CloseAndLogError(pomFile, pomFilePath)
|
defer internal.CloseAndLogError(pomFile, pomFilePath)
|
||||||
|
|
||||||
return decodePomXML(pomFile)
|
return ParsePomXML(pomFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findPomInRemotes download the pom file from all configured Maven repositories over HTTP
|
||||||
|
func (r *Resolver) findPomInRemotes(ctx context.Context, groupID, artifactID, version string) (*Project, error) {
|
||||||
|
var errs error
|
||||||
|
for _, repo := range r.cfg.Repositories {
|
||||||
|
pom, err := r.findPomInRemoteRepository(ctx, repo, groupID, artifactID, version)
|
||||||
|
if err != nil {
|
||||||
|
errs = errors.Join(errs, err)
|
||||||
|
}
|
||||||
|
if pom != nil {
|
||||||
|
return pom, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("pom for %v not found in any remote repository: %w", ID{groupID, artifactID, version}, errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findPomInRemoteRepository download the pom file from a (remote) Maven repository over HTTP
|
// 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) {
|
func (r *Resolver) findPomInRemoteRepository(ctx context.Context, repo string, groupID, artifactID, version string) (*Project, error) {
|
||||||
if groupID == "" || artifactID == "" || version == "" {
|
if groupID == "" || artifactID == "" || version == "" {
|
||||||
return nil, fmt.Errorf("missing/incomplete maven artifact coordinates -- groupId: '%s' artifactId: '%s', version: '%s'", 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)
|
requestURL, err := remotePomURL(repo, groupID, artifactID, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to find pom in remote due to: %w", err)
|
return nil, fmt.Errorf("unable to find pom in remote due to: %w", err)
|
||||||
}
|
}
|
||||||
@ -377,7 +434,7 @@ func (r *mavenResolver) findPomInRemoteRepository(ctx context.Context, groupID,
|
|||||||
if reader, ok := reader.(io.Closer); ok {
|
if reader, ok := reader.(io.Closer); ok {
|
||||||
defer internal.CloseAndLogError(reader, requestURL)
|
defer internal.CloseAndLogError(reader, requestURL)
|
||||||
}
|
}
|
||||||
pom, err := decodePomXML(reader)
|
pom, err := ParsePomXML(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to parse pom from Maven repository url %v: %w", requestURL, err)
|
return nil, fmt.Errorf("unable to parse pom from Maven repository url %v: %w", requestURL, err)
|
||||||
}
|
}
|
||||||
@ -388,7 +445,7 @@ func (r *mavenResolver) findPomInRemoteRepository(ctx context.Context, groupID,
|
|||||||
// this function is guaranteed to return an unread reader for the correct contents.
|
// 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
|
// 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
|
// if there are more users of this functionality
|
||||||
func (r *mavenResolver) cacheResolveReader(key string, resolve func() (io.ReadCloser, error)) (io.Reader, error) {
|
func (r *Resolver) cacheResolveReader(key string, resolve func() (io.ReadCloser, error)) (io.Reader, error) {
|
||||||
reader, err := r.cache.Read(key)
|
reader, err := r.cache.Read(key)
|
||||||
if err == nil && reader != nil {
|
if err == nil && reader != nil {
|
||||||
return reader, err
|
return reader, err
|
||||||
@ -410,7 +467,7 @@ func (r *mavenResolver) cacheResolveReader(key string, resolve func() (io.ReadCl
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveParent attempts to resolve the parent for the given pom
|
// resolveParent attempts to resolve the parent for the given pom
|
||||||
func (r *mavenResolver) resolveParent(ctx context.Context, pom *gopom.Project, resolvingProperties ...string) (*gopom.Project, error) {
|
func (r *Resolver) resolveParent(ctx context.Context, pom *Project, resolvingProperties ...string) (*Project, error) {
|
||||||
if pom == nil || pom.Parent == nil {
|
if pom == nil || pom.Parent == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@ -422,7 +479,7 @@ func (r *mavenResolver) resolveParent(ctx context.Context, pom *gopom.Project, r
|
|||||||
version := r.resolvePropertyValue(ctx, parent.Version, resolvingProperties, &pomWithoutParent)
|
version := r.resolvePropertyValue(ctx, parent.Version, resolvingProperties, &pomWithoutParent)
|
||||||
|
|
||||||
// check cache before resolving
|
// check cache before resolving
|
||||||
parentID := mavenID{groupID, artifactID, version}
|
parentID := ID{groupID, artifactID, version}
|
||||||
if resolvedParent, ok := r.resolved[parentID]; ok {
|
if resolvedParent, ok := r.resolved[parentID]; ok {
|
||||||
return resolvedParent, nil
|
return resolvedParent, nil
|
||||||
}
|
}
|
||||||
@ -434,21 +491,21 @@ func (r *mavenResolver) resolveParent(ctx context.Context, pom *gopom.Project, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
// find POM normally
|
// find POM normally
|
||||||
return r.findPom(ctx, groupID, artifactID, version)
|
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
|
// resolveInheritedVersion attempts to find the version of a dependency (groupID, artifactID) by searching all parent poms and imported managed dependencies
|
||||||
//
|
//
|
||||||
//nolint:gocognit,funlen
|
//nolint:gocognit,funlen
|
||||||
func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Project, groupID, artifactID string, resolutionContext ...*gopom.Project) (string, error) {
|
func (r *Resolver) resolveInheritedVersion(ctx context.Context, pom *Project, groupID, artifactID string, resolutionContext ...*Project) (string, error) {
|
||||||
if pom == nil {
|
if pom == nil {
|
||||||
return "", fmt.Errorf("nil pom provided to findInheritedVersion")
|
return "", fmt.Errorf("nil pom provided to findInheritedVersion")
|
||||||
}
|
}
|
||||||
if r.cfg.MaxParentRecursiveDepth > 0 && len(resolutionContext) > r.cfg.MaxParentRecursiveDepth {
|
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.getMavenID(ctx, pom))
|
return "", fmt.Errorf("maximum depth reached attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.ResolveID(ctx, pom))
|
||||||
}
|
}
|
||||||
if slices.Contains(resolutionContext, pom) {
|
if slices.Contains(resolutionContext, pom) {
|
||||||
return "", fmt.Errorf("cycle detected attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.getMavenID(ctx, pom))
|
return "", fmt.Errorf("cycle detected attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.ResolveID(ctx, pom))
|
||||||
}
|
}
|
||||||
resolutionContext = append(resolutionContext, pom)
|
resolutionContext = append(resolutionContext, pom)
|
||||||
|
|
||||||
@ -457,10 +514,10 @@ func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Pro
|
|||||||
|
|
||||||
// check for entries in dependencyManagement first
|
// check for entries in dependencyManagement first
|
||||||
for _, dep := range pomManagedDependencies(pom) {
|
for _, dep := range pomManagedDependencies(pom) {
|
||||||
depGroupID := r.getPropertyValue(ctx, dep.GroupID, resolutionContext...)
|
depGroupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, resolutionContext...)
|
||||||
depArtifactID := r.getPropertyValue(ctx, dep.ArtifactID, resolutionContext...)
|
depArtifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, resolutionContext...)
|
||||||
if depGroupID == groupID && depArtifactID == artifactID {
|
if depGroupID == groupID && depArtifactID == artifactID {
|
||||||
version = r.getPropertyValue(ctx, dep.Version, resolutionContext...)
|
version = r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...)
|
||||||
if version != "" {
|
if version != "" {
|
||||||
return version, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
@ -468,17 +525,17 @@ func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Pro
|
|||||||
|
|
||||||
// imported pom files should be treated just like parent poms, they are used to define versions of dependencies
|
// 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" {
|
if deref(dep.Type) == "pom" && deref(dep.Scope) == "import" {
|
||||||
depVersion := r.getPropertyValue(ctx, dep.Version, resolutionContext...)
|
depVersion := r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...)
|
||||||
|
|
||||||
depPom, err := r.findPom(ctx, depGroupID, depArtifactID, depVersion)
|
depPom, err := r.FindPom(ctx, depGroupID, depArtifactID, depVersion)
|
||||||
if err != nil || depPom == nil {
|
if err != nil || depPom == nil {
|
||||||
log.WithFields("error", err, "mavenID", r.getMavenID(ctx, pom), "dependencyID", mavenID{depGroupID, depArtifactID, depVersion}).
|
log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", ID{depGroupID, depArtifactID, depVersion}).
|
||||||
Debug("unable to find imported pom looking for managed dependencies")
|
Debug("unable to find imported pom looking for managed dependencies")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
version, err = r.findInheritedVersion(ctx, depPom, groupID, artifactID, resolutionContext...)
|
version, err = r.resolveInheritedVersion(ctx, depPom, groupID, artifactID, resolutionContext...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields("error", err, "mavenID", r.getMavenID(ctx, pom), "dependencyID", mavenID{depGroupID, depArtifactID, depVersion}).
|
log.WithFields("error", err, "ID", r.ResolveID(ctx, pom), "dependencyID", ID{depGroupID, depArtifactID, depVersion}).
|
||||||
Debug("error during findInheritedVersion")
|
Debug("error during findInheritedVersion")
|
||||||
}
|
}
|
||||||
if version != "" {
|
if version != "" {
|
||||||
@ -493,7 +550,7 @@ func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Pro
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if parent != nil {
|
if parent != nil {
|
||||||
version, err = r.findInheritedVersion(ctx, parent, groupID, artifactID, resolutionContext...)
|
version, err = r.resolveInheritedVersion(ctx, parent, groupID, artifactID, resolutionContext...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -503,11 +560,11 @@ func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for inherited dependencies
|
// check for inherited dependencies
|
||||||
for _, dep := range pomDependencies(pom) {
|
for _, dep := range DirectPomDependencies(pom) {
|
||||||
depGroupID := r.getPropertyValue(ctx, dep.GroupID, resolutionContext...)
|
depGroupID := r.resolvePropertyValue(ctx, dep.GroupID, nil, resolutionContext...)
|
||||||
depArtifactID := r.getPropertyValue(ctx, dep.ArtifactID, resolutionContext...)
|
depArtifactID := r.resolvePropertyValue(ctx, dep.ArtifactID, nil, resolutionContext...)
|
||||||
if depGroupID == groupID && depArtifactID == artifactID {
|
if depGroupID == groupID && depArtifactID == artifactID {
|
||||||
version = r.getPropertyValue(ctx, dep.Version, resolutionContext...)
|
version = r.resolvePropertyValue(ctx, dep.Version, nil, resolutionContext...)
|
||||||
if version != "" {
|
if version != "" {
|
||||||
return version, nil
|
return version, nil
|
||||||
}
|
}
|
||||||
@ -517,18 +574,24 @@ func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Pro
|
|||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// findLicenses search pom for license, traversing parent poms if needed
|
// FindLicenses attempts to find a pom, and once found attempts to resolve licenses traversing
|
||||||
func (r *mavenResolver) findLicenses(ctx context.Context, groupID, artifactID, version string) ([]gopom.License, error) {
|
// parent poms as necessary
|
||||||
pom, err := r.findPom(ctx, groupID, artifactID, version)
|
func (r *Resolver) FindLicenses(ctx context.Context, groupID, artifactID, version string) ([]gopom.License, error) {
|
||||||
|
pom, err := r.FindPom(ctx, groupID, artifactID, version)
|
||||||
if pom == nil || err != nil {
|
if pom == nil || err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return r.resolveLicenses(ctx, pom)
|
return r.resolveLicenses(ctx, pom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveLicenses searches the pom for license, resolving and traversing parent poms if needed
|
||||||
|
func (r *Resolver) ResolveLicenses(ctx context.Context, pom *Project) ([]License, error) {
|
||||||
|
return r.resolveLicenses(ctx, pom)
|
||||||
|
}
|
||||||
|
|
||||||
// resolveLicenses searches the pom for license, traversing parent poms if needed
|
// 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) {
|
func (r *Resolver) resolveLicenses(ctx context.Context, pom *Project, processing ...ID) ([]License, error) {
|
||||||
id := r.getMavenID(ctx, pom)
|
id := r.ResolveID(ctx, pom)
|
||||||
if slices.Contains(processing, id) {
|
if slices.Contains(processing, id) {
|
||||||
return nil, fmt.Errorf("cycle detected resolving licenses for: %v", id)
|
return nil, fmt.Errorf("cycle detected resolving licenses for: %v", id)
|
||||||
}
|
}
|
||||||
@ -552,12 +615,12 @@ func (r *mavenResolver) resolveLicenses(ctx context.Context, pom *gopom.Project,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pomLicenses appends the directly specified licenses with non-empty name or url
|
// pomLicenses appends the directly specified licenses with non-empty name or url
|
||||||
func (r *mavenResolver) pomLicenses(ctx context.Context, pom *gopom.Project) []gopom.License {
|
func (r *Resolver) pomLicenses(ctx context.Context, pom *Project) []License {
|
||||||
var out []gopom.License
|
var out []License
|
||||||
for _, license := range deref(pom.Licenses) {
|
for _, license := range deref(pom.Licenses) {
|
||||||
// if we find non-empty licenses, return them
|
// if we find non-empty licenses, return them
|
||||||
name := r.getPropertyValue(ctx, license.Name, pom)
|
name := r.resolvePropertyValue(ctx, license.Name, nil, pom)
|
||||||
url := r.getPropertyValue(ctx, license.URL, pom)
|
url := r.resolvePropertyValue(ctx, license.URL, nil, pom)
|
||||||
if name != "" || url != "" {
|
if name != "" || url != "" {
|
||||||
out = append(out, license)
|
out = append(out, license)
|
||||||
}
|
}
|
||||||
@ -565,8 +628,8 @@ func (r *mavenResolver) pomLicenses(ctx context.Context, pom *gopom.Project) []g
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mavenResolver) findParentPomByRelativePath(ctx context.Context, pom *gopom.Project, parentID mavenID, resolvingProperties []string) *gopom.Project {
|
func (r *Resolver) findParentPomByRelativePath(ctx context.Context, pom *Project, parentID ID, resolvingProperties []string) *Project {
|
||||||
// don't resolve if no resolver
|
// can't resolve without a file resolver
|
||||||
if r.fileResolver == nil {
|
if r.fileResolver == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -588,7 +651,7 @@ func (r *mavenResolver) findParentPomByRelativePath(ctx context.Context, pom *go
|
|||||||
}
|
}
|
||||||
parentLocations, err := r.fileResolver.FilesByPath(p)
|
parentLocations, err := r.fileResolver.FilesByPath(p)
|
||||||
if err != nil || len(parentLocations) == 0 {
|
if err != nil || len(parentLocations) == 0 {
|
||||||
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, resolvingProperties, pom), "parentID", parentID, "relativePath", relativePath).
|
log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "relativePath", relativePath).
|
||||||
Trace("parent pom not found by relative path")
|
Trace("parent pom not found by relative path")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -596,34 +659,49 @@ func (r *mavenResolver) findParentPomByRelativePath(ctx context.Context, pom *go
|
|||||||
|
|
||||||
parentContents, err := r.fileResolver.FileContentsByLocation(parentLocation)
|
parentContents, err := r.fileResolver.FileContentsByLocation(parentLocation)
|
||||||
if err != nil || parentContents == nil {
|
if err != nil || parentContents == nil {
|
||||||
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
|
log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
|
||||||
Debug("unable to get contents of parent pom by relative path")
|
Debug("unable to get contents of parent pom by relative path")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer internal.CloseAndLogError(parentContents, parentLocation.RealPath)
|
defer internal.CloseAndLogError(parentContents, parentLocation.RealPath)
|
||||||
parentPom, err := decodePomXML(parentContents)
|
parentPom, err := ParsePomXML(parentContents)
|
||||||
if err != nil || parentPom == nil {
|
if err != nil || parentPom == nil {
|
||||||
log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
|
log.WithFields("error", err, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
|
||||||
Debug("unable to parse parent pom")
|
Debug("unable to parse parent pom")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// ensure parent matches
|
// ensure parent matches
|
||||||
newParentID := r.resolveMavenID(ctx, resolvingProperties, parentPom)
|
newParentID := r.resolveID(ctx, resolvingProperties, parentPom)
|
||||||
if newParentID.ArtifactID != parentID.ArtifactID {
|
if newParentID.ArtifactID != parentID.ArtifactID {
|
||||||
log.WithFields("newParentID", newParentID, "mavenID", r.resolveMavenID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
|
log.WithFields("newParentID", newParentID, "mavenID", r.resolveID(ctx, resolvingProperties, pom), "parentID", parentID, "parentLocation", parentLocation).
|
||||||
Debug("parent IDs do not match resolving parent by relative path")
|
Debug("parent IDs do not match resolving parent by relative path")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r.resolved[parentID] = parentPom
|
r.resolved[parentID] = parentPom
|
||||||
r.pomLocations[parentPom] = parentLocation // for any future parent relativepath lookups
|
r.pomLocations[parentPom] = parentLocation // for any future parent relativePath lookups
|
||||||
|
|
||||||
return parentPom
|
return parentPom
|
||||||
}
|
}
|
||||||
|
|
||||||
// pomDependencies returns all dependencies directly defined in a project, including all defined in profiles.
|
// AddPom allows for adding known pom files with locations within the file resolver, these locations may be used
|
||||||
// does not resolve parent dependencies
|
// while resolving parent poms by relative path
|
||||||
func pomDependencies(pom *gopom.Project) []gopom.Dependency {
|
func (r *Resolver) AddPom(ctx context.Context, pom *Project, location file.Location) {
|
||||||
|
r.pomLocations[pom] = location
|
||||||
|
// by calling resolve ID here, this will lookup necessary parent poms by relative path, and
|
||||||
|
// track any poms we found with complete version information if enough is available to resolve
|
||||||
|
id := r.ResolveID(ctx, pom)
|
||||||
|
if id.Valid() {
|
||||||
|
_, existing := r.resolved[id]
|
||||||
|
if !existing {
|
||||||
|
r.resolved[id] = pom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectPomDependencies returns all dependencies directly defined in a project, including all defined in profiles.
|
||||||
|
// This does not resolve any parent or transitive dependencies
|
||||||
|
func DirectPomDependencies(pom *Project) []Dependency {
|
||||||
dependencies := deref(pom.Dependencies)
|
dependencies := deref(pom.Dependencies)
|
||||||
for _, profile := range deref(pom.Profiles) {
|
for _, profile := range deref(pom.Profiles) {
|
||||||
dependencies = append(dependencies, deref(profile.Dependencies)...)
|
dependencies = append(dependencies, deref(profile.Dependencies)...)
|
||||||
@ -633,8 +711,8 @@ func pomDependencies(pom *gopom.Project) []gopom.Dependency {
|
|||||||
|
|
||||||
// pomManagedDependencies returns all directly defined managed dependencies in a project pom, including all defined in profiles.
|
// pomManagedDependencies returns all directly defined managed dependencies in a project pom, including all defined in profiles.
|
||||||
// does not resolve parent managed dependencies
|
// does not resolve parent managed dependencies
|
||||||
func pomManagedDependencies(pom *gopom.Project) []gopom.Dependency {
|
func pomManagedDependencies(pom *Project) []Dependency {
|
||||||
var dependencies []gopom.Dependency
|
var dependencies []Dependency
|
||||||
if pom.DependencyManagement != nil {
|
if pom.DependencyManagement != nil {
|
||||||
dependencies = append(dependencies, deref(pom.DependencyManagement.Dependencies)...)
|
dependencies = append(dependencies, deref(pom.DependencyManagement.Dependencies)...)
|
||||||
}
|
}
|
||||||
@ -645,3 +723,12 @@ func pomManagedDependencies(pom *gopom.Project) []gopom.Dependency {
|
|||||||
}
|
}
|
||||||
return dependencies
|
return dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@ -1,34 +1,30 @@
|
|||||||
package java
|
package maven
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/vifraa/gopom"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||||
|
maventest "github.com/anchore/syft/syft/pkg/cataloger/java/internal/maven/test"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_resolveProperty(t *testing.T) {
|
func Test_resolveProperty(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
property string
|
property string
|
||||||
pom gopom.Project
|
pom Project
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "property",
|
name: "property",
|
||||||
property: "${version.number}",
|
property: "${version.number}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Properties: &gopom.Properties{
|
Properties: &Properties{
|
||||||
Entries: map[string]string{
|
Entries: map[string]string{
|
||||||
"version.number": "12.5.0",
|
"version.number": "12.5.0",
|
||||||
},
|
},
|
||||||
@ -39,7 +35,7 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "groupId",
|
name: "groupId",
|
||||||
property: "${project.groupId}",
|
property: "${project.groupId}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
GroupID: ptr("org.some.group"),
|
GroupID: ptr("org.some.group"),
|
||||||
},
|
},
|
||||||
expected: "org.some.group",
|
expected: "org.some.group",
|
||||||
@ -47,8 +43,8 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "parent groupId",
|
name: "parent groupId",
|
||||||
property: "${project.parent.groupId}",
|
property: "${project.parent.groupId}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &Parent{
|
||||||
GroupID: ptr("org.some.parent"),
|
GroupID: ptr("org.some.parent"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -57,7 +53,7 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "nil pointer halts search",
|
name: "nil pointer halts search",
|
||||||
property: "${project.parent.groupId}",
|
property: "${project.parent.groupId}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: nil,
|
Parent: nil,
|
||||||
},
|
},
|
||||||
expected: "",
|
expected: "",
|
||||||
@ -65,8 +61,8 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "nil string pointer halts search",
|
name: "nil string pointer halts search",
|
||||||
property: "${project.parent.groupId}",
|
property: "${project.parent.groupId}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &Parent{
|
||||||
GroupID: nil,
|
GroupID: nil,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -75,11 +71,11 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "double dereference",
|
name: "double dereference",
|
||||||
property: "${springboot.version}",
|
property: "${springboot.version}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &Parent{
|
||||||
Version: ptr("1.2.3"),
|
Version: ptr("1.2.3"),
|
||||||
},
|
},
|
||||||
Properties: &gopom.Properties{
|
Properties: &Properties{
|
||||||
Entries: map[string]string{
|
Entries: map[string]string{
|
||||||
"springboot.version": "${project.parent.version}",
|
"springboot.version": "${project.parent.version}",
|
||||||
},
|
},
|
||||||
@ -90,8 +86,8 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "map missing stops double dereference",
|
name: "map missing stops double dereference",
|
||||||
property: "${springboot.version}",
|
property: "${springboot.version}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &Parent{
|
||||||
Version: ptr("1.2.3"),
|
Version: ptr("1.2.3"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -100,11 +96,11 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "resolution halts even if it resolves to a variable",
|
name: "resolution halts even if it resolves to a variable",
|
||||||
property: "${springboot.version}",
|
property: "${springboot.version}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &Parent{
|
||||||
Version: ptr("${undefined.version}"),
|
Version: ptr("${undefined.version}"),
|
||||||
},
|
},
|
||||||
Properties: &gopom.Properties{
|
Properties: &Properties{
|
||||||
Entries: map[string]string{
|
Entries: map[string]string{
|
||||||
"springboot.version": "${project.parent.version}",
|
"springboot.version": "${project.parent.version}",
|
||||||
},
|
},
|
||||||
@ -115,8 +111,8 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "resolution halts even if cyclic",
|
name: "resolution halts even if cyclic",
|
||||||
property: "${springboot.version}",
|
property: "${springboot.version}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Properties: &gopom.Properties{
|
Properties: &Properties{
|
||||||
Entries: map[string]string{
|
Entries: map[string]string{
|
||||||
"springboot.version": "${springboot.version}",
|
"springboot.version": "${springboot.version}",
|
||||||
},
|
},
|
||||||
@ -127,8 +123,8 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "resolution halts even if cyclic more steps",
|
name: "resolution halts even if cyclic more steps",
|
||||||
property: "${cyclic.version}",
|
property: "${cyclic.version}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Properties: &gopom.Properties{
|
Properties: &Properties{
|
||||||
Entries: map[string]string{
|
Entries: map[string]string{
|
||||||
"other.version": "${cyclic.version}",
|
"other.version": "${cyclic.version}",
|
||||||
"springboot.version": "${other.version}",
|
"springboot.version": "${other.version}",
|
||||||
@ -141,11 +137,11 @@ func Test_resolveProperty(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "resolution halts even if cyclic involving parent",
|
name: "resolution halts even if cyclic involving parent",
|
||||||
property: "${cyclic.version}",
|
property: "${cyclic.version}",
|
||||||
pom: gopom.Project{
|
pom: Project{
|
||||||
Parent: &gopom.Parent{
|
Parent: &Parent{
|
||||||
Version: ptr("${cyclic.version}"),
|
Version: ptr("${cyclic.version}"),
|
||||||
},
|
},
|
||||||
Properties: &gopom.Properties{
|
Properties: &Properties{
|
||||||
Entries: map[string]string{
|
Entries: map[string]string{
|
||||||
"other.version": "${parent.version}",
|
"other.version": "${parent.version}",
|
||||||
"springboot.version": "${other.version}",
|
"springboot.version": "${other.version}",
|
||||||
@ -159,15 +155,15 @@ func Test_resolveProperty(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) {
|
||||||
r := newMavenResolver(nil, DefaultArchiveCatalogerConfig())
|
r := NewResolver(nil, DefaultConfig())
|
||||||
resolved := r.getPropertyValue(context.Background(), ptr(test.property), &test.pom)
|
resolved := r.ResolveProperty(context.Background(), &test.pom, ptr(test.property))
|
||||||
require.Equal(t, test.expected, resolved)
|
require.Equal(t, test.expected, resolved)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_mavenResolverLocal(t *testing.T) {
|
func Test_mavenResolverLocal(t *testing.T) {
|
||||||
dir, err := filepath.Abs("test-fixtures/pom/maven-repo")
|
dir, err := filepath.Abs("test-fixtures/maven-repo")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@ -211,26 +207,26 @@ func Test_mavenResolverLocal(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) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
r := newMavenResolver(nil, ArchiveCatalogerConfig{
|
r := NewResolver(nil, Config{
|
||||||
UseNetwork: false,
|
UseNetwork: false,
|
||||||
UseMavenLocalRepository: true,
|
UseLocalRepository: true,
|
||||||
MavenLocalRepositoryDir: dir,
|
LocalRepositoryDir: dir,
|
||||||
MaxParentRecursiveDepth: test.maxDepth,
|
MaxParentRecursiveDepth: test.maxDepth,
|
||||||
})
|
})
|
||||||
pom, err := r.findPom(ctx, test.groupID, test.artifactID, test.version)
|
pom, err := r.FindPom(ctx, test.groupID, test.artifactID, test.version)
|
||||||
if test.wantErr != nil {
|
if test.wantErr != nil {
|
||||||
test.wantErr(t, err)
|
test.wantErr(t, err)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
got := r.getPropertyValue(context.Background(), &test.expression, pom)
|
got := r.ResolveProperty(context.Background(), pom, &test.expression)
|
||||||
require.Equal(t, test.expected, got)
|
require.Equal(t, test.expected, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_mavenResolverRemote(t *testing.T) {
|
func Test_mavenResolverRemote(t *testing.T) {
|
||||||
url := mockMavenRepo(t)
|
url := maventest.MockRepo(t, "test-fixtures/maven-repo")
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
groupID string
|
groupID string
|
||||||
@ -252,25 +248,25 @@ func Test_mavenResolverRemote(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.artifactID, func(t *testing.T) {
|
t.Run(test.artifactID, func(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
r := newMavenResolver(nil, ArchiveCatalogerConfig{
|
r := NewResolver(nil, Config{
|
||||||
UseNetwork: true,
|
UseNetwork: true,
|
||||||
UseMavenLocalRepository: false,
|
UseLocalRepository: false,
|
||||||
MavenBaseURL: url,
|
Repositories: strings.Split(url, ","),
|
||||||
})
|
})
|
||||||
pom, err := r.findPom(ctx, test.groupID, test.artifactID, test.version)
|
pom, err := r.FindPom(ctx, test.groupID, test.artifactID, test.version)
|
||||||
if test.wantErr != nil {
|
if test.wantErr != nil {
|
||||||
test.wantErr(t, err)
|
test.wantErr(t, err)
|
||||||
} else {
|
} else {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
got := r.getPropertyValue(context.Background(), &test.expression, pom)
|
got := r.ResolveProperty(context.Background(), pom, &test.expression)
|
||||||
require.Equal(t, test.expected, got)
|
require.Equal(t, test.expected, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_relativePathParent(t *testing.T) {
|
func Test_relativePathParent(t *testing.T) {
|
||||||
resolver, err := fileresolver.NewFromDirectory("test-fixtures/pom/local", "")
|
resolver, err := fileresolver.NewFromDirectory("test-fixtures/local", "")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@ -278,12 +274,12 @@ func Test_relativePathParent(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
pom string
|
pom string
|
||||||
validate func(t *testing.T, r *mavenResolver, pom *gopom.Project)
|
validate func(t *testing.T, r *Resolver, pom *Project)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "basic",
|
name: "basic",
|
||||||
pom: "child-1/pom.xml",
|
pom: "child-1/pom.xml",
|
||||||
validate: func(t *testing.T, r *mavenResolver, pom *gopom.Project) {
|
validate: func(t *testing.T, r *Resolver, pom *Project) {
|
||||||
parent, err := r.resolveParent(ctx, pom)
|
parent, err := r.resolveParent(ctx, pom)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, r.pomLocations, parent)
|
require.Contains(t, r.pomLocations, parent)
|
||||||
@ -292,16 +288,15 @@ func Test_relativePathParent(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Contains(t, r.pomLocations, parent)
|
require.Contains(t, r.pomLocations, parent)
|
||||||
|
|
||||||
got := r.getPropertyValue(ctx, ptr("${commons-exec_subversion}"), pom)
|
got := r.ResolveProperty(ctx, pom, ptr("${commons-exec_subversion}"))
|
||||||
require.Equal(t, "3", got)
|
require.Equal(t, "3", got)
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parent property",
|
name: "parent property",
|
||||||
pom: "child-2/pom.xml",
|
pom: "child-2/pom.xml",
|
||||||
validate: func(t *testing.T, r *mavenResolver, pom *gopom.Project) {
|
validate: func(t *testing.T, r *Resolver, pom *Project) {
|
||||||
id := r.getMavenID(ctx, pom)
|
id := r.ResolveID(ctx, pom)
|
||||||
// child.parent.version = ${revision}
|
// child.parent.version = ${revision}
|
||||||
// parent.revision = 3.3.3
|
// parent.revision = 3.3.3
|
||||||
require.Equal(t, id.Version, "3.3.3")
|
require.Equal(t, id.Version, "3.3.3")
|
||||||
@ -310,9 +305,9 @@ func Test_relativePathParent(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "invalid parent",
|
name: "invalid parent",
|
||||||
pom: "child-3/pom.xml",
|
pom: "child-3/pom.xml",
|
||||||
validate: func(t *testing.T, r *mavenResolver, pom *gopom.Project) {
|
validate: func(t *testing.T, r *Resolver, pom *Project) {
|
||||||
require.NotNil(t, pom)
|
require.NotNil(t, pom)
|
||||||
id := r.getMavenID(ctx, pom)
|
id := r.ResolveID(ctx, pom)
|
||||||
// version should not be resolved to anything
|
// version should not be resolved to anything
|
||||||
require.Equal(t, "", id.Version)
|
require.Equal(t, "", id.Version)
|
||||||
},
|
},
|
||||||
@ -321,7 +316,7 @@ func Test_relativePathParent(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) {
|
||||||
r := newMavenResolver(resolver, DefaultArchiveCatalogerConfig())
|
r := NewResolver(resolver, DefaultConfig())
|
||||||
locs, err := resolver.FilesByPath(test.pom)
|
locs, err := resolver.FilesByPath(test.pom)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, locs, 1)
|
require.Len(t, locs, 1)
|
||||||
@ -331,7 +326,7 @@ func Test_relativePathParent(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer internal.CloseAndLogError(contents, loc.RealPath)
|
defer internal.CloseAndLogError(contents, loc.RealPath)
|
||||||
|
|
||||||
pom, err := decodePomXML(contents)
|
pom, err := ParsePomXML(contents)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
r.pomLocations[pom] = loc
|
r.pomLocations[pom] = loc
|
||||||
@ -341,59 +336,7 @@ func Test_relativePathParent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockMavenRepo starts a remote maven repo serving all the pom files found in test-fixtures/pom/maven-repo
|
// ptr returns a pointer to the given value
|
||||||
func mockMavenRepo(t *testing.T) (url string) {
|
func ptr[T any](value T) *T {
|
||||||
t.Helper()
|
return &value
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
65
syft/pkg/cataloger/java/internal/maven/test/mock_repo.go
Normal file
65
syft/pkg/cataloger/java/internal/maven/test/mock_repo.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package maventest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bmatcuk/doublestar/v4"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockRepo starts a remote maven repo serving all the pom files found in a maven-structured directory
|
||||||
|
func MockRepo(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) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, _ *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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,17 @@
|
|||||||
package java
|
package java
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/saintfish/chardet"
|
|
||||||
"github.com/vifraa/gopom"
|
|
||||||
"golang.org/x/net/html/charset"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/unknown"
|
"github.com/anchore/syft/internal/unknown"
|
||||||
"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/java/internal/maven"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -40,10 +33,11 @@ func (p pomXMLCataloger) Catalog(ctx context.Context, fileResolver file.Resolver
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
r := newMavenResolver(fileResolver, p.cfg)
|
r := maven.NewResolver(fileResolver, p.cfg.mavenConfig())
|
||||||
|
|
||||||
var errs error
|
var errs error
|
||||||
var poms []*gopom.Project
|
var poms []*maven.Project
|
||||||
|
pomLocations := map[*maven.Project]file.Location{}
|
||||||
for _, pomLocation := range locations {
|
for _, pomLocation := range locations {
|
||||||
pom, err := readPomFromLocation(fileResolver, pomLocation)
|
pom, err := readPomFromLocation(fileResolver, pomLocation)
|
||||||
if err != nil || pom == nil {
|
if err != nil || pom == nil {
|
||||||
@ -53,59 +47,172 @@ func (p pomXMLCataloger) Catalog(ctx context.Context, fileResolver file.Resolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
poms = append(poms, pom)
|
poms = append(poms, pom)
|
||||||
|
pomLocations[pom] = pomLocation
|
||||||
// store information about this pom for future lookups
|
r.AddPom(ctx, pom, pomLocation)
|
||||||
r.pomLocations[pom] = pomLocation
|
|
||||||
r.resolved[r.getMavenID(ctx, pom)] = pom
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var pkgs []pkg.Package
|
var pkgs []pkg.Package
|
||||||
|
var relationships []artifact.Relationship
|
||||||
|
resolved := map[maven.ID]*pkg.Package{}
|
||||||
|
|
||||||
|
// catalog all the main packages first so these can be referenced later when building the dependency graph
|
||||||
for _, pom := range poms {
|
for _, pom := range poms {
|
||||||
pkgs = append(pkgs, processPomXML(ctx, r, pom, r.pomLocations[pom])...)
|
location := pomLocations[pom] // should always exist
|
||||||
|
|
||||||
|
id := r.ResolveID(ctx, pom)
|
||||||
|
mainPkg := newPackageFromMavenPom(ctx, r, pom, location)
|
||||||
|
if mainPkg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resolved[id] = mainPkg
|
||||||
|
pkgs = append(pkgs, *mainPkg)
|
||||||
}
|
}
|
||||||
return pkgs, nil, errs
|
|
||||||
|
// catalog all dependencies
|
||||||
|
for _, pom := range poms {
|
||||||
|
location := pomLocations[pom] // should always exist
|
||||||
|
|
||||||
|
id := r.ResolveID(ctx, pom)
|
||||||
|
mainPkg := resolved[id]
|
||||||
|
|
||||||
|
newPkgs, newRelationships, newErrs := collectDependencies(ctx, r, resolved, mainPkg, pom, location, p.cfg.ResolveTransitiveDependencies)
|
||||||
|
pkgs = append(pkgs, newPkgs...)
|
||||||
|
relationships = append(relationships, newRelationships...)
|
||||||
|
errs = unknown.Join(errs, newErrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkgs, relationships, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPomFromLocation(fileResolver file.Resolver, pomLocation file.Location) (*gopom.Project, error) {
|
func readPomFromLocation(fileResolver file.Resolver, pomLocation file.Location) (*maven.Project, error) {
|
||||||
contents, err := fileResolver.FileContentsByLocation(pomLocation)
|
contents, err := fileResolver.FileContentsByLocation(pomLocation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer internal.CloseAndLogError(contents, pomLocation.RealPath)
|
defer internal.CloseAndLogError(contents, pomLocation.RealPath)
|
||||||
return decodePomXML(contents)
|
return maven.ParsePomXML(contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
func processPomXML(ctx context.Context, r *mavenResolver, pom *gopom.Project, loc file.Location) []pkg.Package {
|
// newPackageFromMavenPom processes a single Maven POM for a given parent package, returning only the main package from the pom
|
||||||
var pkgs []pkg.Package
|
func newPackageFromMavenPom(ctx context.Context, r *maven.Resolver, pom *maven.Project, location file.Location) *pkg.Package {
|
||||||
|
id := r.ResolveID(ctx, pom)
|
||||||
pomID := r.getMavenID(ctx, pom)
|
parent, err := r.ResolveParent(ctx, pom)
|
||||||
for _, dep := range pomDependencies(pom) {
|
if err != nil {
|
||||||
depID := r.resolveDependencyID(ctx, pom, dep)
|
// this is expected in many cases, there will be no network access and the maven resolver is unable to
|
||||||
log.WithFields("pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Trace("adding maven pom dependency")
|
// look up information, so we can continue with what little information we have
|
||||||
|
log.Trace("unable to resolve parent due to: %v", err)
|
||||||
p, err := newPackageFromDependency(
|
|
||||||
ctx,
|
|
||||||
r,
|
|
||||||
pom,
|
|
||||||
dep,
|
|
||||||
loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields("error", err, "pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Debugf("error adding dependency")
|
|
||||||
}
|
|
||||||
if p == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pkgs = append(pkgs, *p)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkgs
|
var javaPomParent *pkg.JavaPomParent
|
||||||
|
if parent != nil { // parent is returned in both cases: when it is resolved or synthesized from the pom.parent info
|
||||||
|
parentID := r.ResolveID(ctx, parent)
|
||||||
|
javaPomParent = &pkg.JavaPomParent{
|
||||||
|
GroupID: parentID.GroupID,
|
||||||
|
ArtifactID: parentID.ArtifactID,
|
||||||
|
Version: parentID.Version,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pomLicenses, err := r.ResolveLicenses(ctx, pom)
|
||||||
|
if err != nil {
|
||||||
|
log.Tracef("error resolving licenses: %v", err)
|
||||||
|
}
|
||||||
|
licenses := toPkgLicenses(&location, pomLicenses)
|
||||||
|
|
||||||
|
m := pkg.JavaArchive{
|
||||||
|
PomProject: &pkg.JavaPomProject{
|
||||||
|
Parent: javaPomParent,
|
||||||
|
GroupID: id.GroupID,
|
||||||
|
ArtifactID: id.ArtifactID,
|
||||||
|
Version: id.Version,
|
||||||
|
Name: r.ResolveProperty(ctx, pom, pom.Name),
|
||||||
|
Description: r.ResolveProperty(ctx, pom, pom.Description),
|
||||||
|
URL: r.ResolveProperty(ctx, pom, pom.URL),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &pkg.Package{
|
||||||
|
Name: id.ArtifactID,
|
||||||
|
Version: id.Version,
|
||||||
|
Locations: file.NewLocationSet(
|
||||||
|
location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||||
|
),
|
||||||
|
Licenses: pkg.NewLicenseSet(licenses...),
|
||||||
|
Language: pkg.Java,
|
||||||
|
Type: pkg.JavaPkg,
|
||||||
|
FoundBy: pomCatalogerName,
|
||||||
|
PURL: packageURL(id.ArtifactID, id.Version, m),
|
||||||
|
Metadata: m,
|
||||||
|
}
|
||||||
|
|
||||||
|
finalizePackage(p)
|
||||||
|
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPomProject(ctx context.Context, r *mavenResolver, path string, pom *gopom.Project) *pkg.JavaPomProject {
|
func collectDependencies(ctx context.Context, r *maven.Resolver, resolved map[maven.ID]*pkg.Package, parentPkg *pkg.Package, pom *maven.Project, loc file.Location, includeTransitiveDependencies bool) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
id := r.getMavenID(ctx, pom)
|
var errs error
|
||||||
name := r.getPropertyValue(ctx, pom.Name, pom)
|
var pkgs []pkg.Package
|
||||||
projectURL := r.getPropertyValue(ctx, pom.URL, pom)
|
var relationships []artifact.Relationship
|
||||||
|
|
||||||
|
pomID := r.ResolveID(ctx, pom)
|
||||||
|
for _, dep := range maven.DirectPomDependencies(pom) {
|
||||||
|
depID := r.ResolveDependencyID(ctx, pom, dep)
|
||||||
|
log.WithFields("pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Trace("adding maven pom dependency")
|
||||||
|
|
||||||
|
// we may have a reference to a package pointing to an existing pom on the filesystem, but we don't want to duplicate these entries
|
||||||
|
depPkg := resolved[depID]
|
||||||
|
if depPkg == nil {
|
||||||
|
p, err := newPackageFromDependency(
|
||||||
|
ctx,
|
||||||
|
r,
|
||||||
|
pom,
|
||||||
|
dep,
|
||||||
|
loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err, "pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Debugf("error adding dependency")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p == nil {
|
||||||
|
// we don't have a valid package, just continue to the next dependency
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
depPkg = p
|
||||||
|
resolved[depID] = depPkg
|
||||||
|
|
||||||
|
// only resolve transitive dependencies if we're not already looking these up for the specific package
|
||||||
|
if includeTransitiveDependencies && depID.Valid() {
|
||||||
|
depPom, err := r.FindPom(ctx, depID.GroupID, depID.ArtifactID, depID.Version)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("mavenID", depID, "error", err).Debug("error finding pom")
|
||||||
|
}
|
||||||
|
if depPom != nil {
|
||||||
|
transitivePkgs, transitiveRelationships, transitiveErrs := collectDependencies(ctx, r, resolved, depPkg, depPom, loc, includeTransitiveDependencies)
|
||||||
|
pkgs = append(pkgs, transitivePkgs...)
|
||||||
|
relationships = append(relationships, transitiveRelationships...)
|
||||||
|
errs = unknown.Join(errs, transitiveErrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgs = append(pkgs, *depPkg)
|
||||||
|
if parentPkg != nil {
|
||||||
|
relationships = append(relationships, artifact.Relationship{
|
||||||
|
From: *depPkg,
|
||||||
|
To: *parentPkg,
|
||||||
|
Type: artifact.DependencyOfRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkgs, relationships, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPomProject(ctx context.Context, r *maven.Resolver, path string, pom *maven.Project) *pkg.JavaPomProject {
|
||||||
|
id := r.ResolveID(ctx, pom)
|
||||||
|
name := r.ResolveProperty(ctx, pom, pom.Name)
|
||||||
|
projectURL := r.ResolveProperty(ctx, pom, pom.URL)
|
||||||
|
|
||||||
log.WithFields("path", path, "artifactID", id.ArtifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml")
|
log.WithFields("path", path, "artifactID", id.ArtifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml")
|
||||||
return &pkg.JavaPomProject{
|
return &pkg.JavaPomProject{
|
||||||
@ -115,34 +222,43 @@ func newPomProject(ctx context.Context, r *mavenResolver, path string, pom *gopo
|
|||||||
ArtifactID: id.ArtifactID,
|
ArtifactID: id.ArtifactID,
|
||||||
Version: id.Version,
|
Version: id.Version,
|
||||||
Name: name,
|
Name: name,
|
||||||
Description: cleanDescription(r.getPropertyValue(ctx, pom.Description, pom)),
|
Description: cleanDescription(r.ResolveProperty(ctx, pom, pom.Description)),
|
||||||
URL: projectURL,
|
URL: projectURL,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPackageFromDependency(ctx context.Context, r *mavenResolver, pom *gopom.Project, dep gopom.Dependency, locations ...file.Location) (*pkg.Package, error) {
|
func newPackageFromDependency(ctx context.Context, r *maven.Resolver, pom *maven.Project, dep maven.Dependency, locations ...file.Location) (*pkg.Package, error) {
|
||||||
id := r.resolveDependencyID(ctx, pom, dep)
|
id := r.ResolveDependencyID(ctx, pom, dep)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var licenses []pkg.License
|
||||||
|
dependencyPom, depErr := r.FindPom(ctx, id.GroupID, id.ArtifactID, id.Version)
|
||||||
|
if depErr != nil {
|
||||||
|
err = errors.Join(err, depErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pomProject *pkg.JavaPomProject
|
||||||
|
if dependencyPom != nil {
|
||||||
|
depLicenses, _ := r.ResolveLicenses(ctx, dependencyPom)
|
||||||
|
licenses = append(licenses, toPkgLicenses(nil, depLicenses)...)
|
||||||
|
pomProject = &pkg.JavaPomProject{
|
||||||
|
Parent: pomParent(ctx, r, dependencyPom),
|
||||||
|
GroupID: id.GroupID,
|
||||||
|
ArtifactID: id.ArtifactID,
|
||||||
|
Version: id.Version,
|
||||||
|
Name: r.ResolveProperty(ctx, pom, pom.Name),
|
||||||
|
Description: r.ResolveProperty(ctx, pom, pom.Description),
|
||||||
|
URL: r.ResolveProperty(ctx, pom, pom.URL),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m := pkg.JavaArchive{
|
m := pkg.JavaArchive{
|
||||||
PomProperties: &pkg.JavaPomProperties{
|
PomProperties: &pkg.JavaPomProperties{
|
||||||
GroupID: id.GroupID,
|
GroupID: id.GroupID,
|
||||||
ArtifactID: id.ArtifactID,
|
ArtifactID: id.ArtifactID,
|
||||||
Scope: r.getPropertyValue(ctx, dep.Scope, pom),
|
Scope: r.ResolveProperty(ctx, pom, dep.Scope),
|
||||||
},
|
},
|
||||||
}
|
PomProject: pomProject,
|
||||||
|
|
||||||
var err error
|
|
||||||
var licenses []pkg.License
|
|
||||||
dependencyPom, depErr := r.findPom(ctx, id.GroupID, id.ArtifactID, id.Version)
|
|
||||||
if depErr != nil {
|
|
||||||
err = errors.Join(err, depErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dependencyPom != nil {
|
|
||||||
depLicenses, _ := r.resolveLicenses(ctx, dependencyPom)
|
|
||||||
for _, license := range depLicenses {
|
|
||||||
licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), nil))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p := &pkg.Package{
|
p := &pkg.Package{
|
||||||
@ -157,65 +273,19 @@ func newPackageFromDependency(ctx context.Context, r *mavenResolver, pom *gopom.
|
|||||||
Metadata: m,
|
Metadata: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.SetID()
|
finalizePackage(p)
|
||||||
|
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 pomParent(ctx context.Context, r *maven.Resolver, pom *maven.Project) *pkg.JavaPomParent {
|
||||||
func decodePomXML(content io.Reader) (project *gopom.Project, err error) {
|
|
||||||
inputReader, err := getUtf8Reader(content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to read pom.xml: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder := xml.NewDecoder(inputReader)
|
|
||||||
// when an xml file has a character set declaration (e.g. '<?xml version="1.0" encoding="ISO-8859-1"?>') read that and use the correct decoder
|
|
||||||
decoder.CharsetReader = charset.NewReaderLabel
|
|
||||||
|
|
||||||
project = &gopom.Project{}
|
|
||||||
if err := decoder.Decode(project); err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to unmarshal pom.xml: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUtf8Reader(content io.Reader) (io.Reader, error) {
|
|
||||||
pomContents, err := io.ReadAll(content)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
detector := chardet.NewTextDetector()
|
|
||||||
detection, err := detector.DetectBest(pomContents)
|
|
||||||
|
|
||||||
var inputReader io.Reader
|
|
||||||
if err == nil && detection != nil {
|
|
||||||
if detection.Charset == "UTF-8" {
|
|
||||||
inputReader = bytes.NewReader(pomContents)
|
|
||||||
} else {
|
|
||||||
inputReader, err = charset.NewReaderLabel(detection.Charset, bytes.NewReader(pomContents))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to get encoding: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// we could not detect the encoding, but we want a valid file to read. Replace unreadable
|
|
||||||
// characters with the UTF-8 replacement character.
|
|
||||||
inputReader = strings.NewReader(strings.ToValidUTF8(string(pomContents), "<22>"))
|
|
||||||
}
|
|
||||||
return inputReader, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pomParent(ctx context.Context, r *mavenResolver, pom *gopom.Project) *pkg.JavaPomParent {
|
|
||||||
if pom == nil || pom.Parent == nil {
|
if pom == nil || pom.Parent == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
groupID := r.getPropertyValue(ctx, pom.Parent.GroupID, pom)
|
groupID := r.ResolveProperty(ctx, pom, pom.Parent.GroupID)
|
||||||
artifactID := r.getPropertyValue(ctx, pom.Parent.ArtifactID, pom)
|
artifactID := r.ResolveProperty(ctx, pom, pom.Parent.ArtifactID)
|
||||||
version := r.getPropertyValue(ctx, pom.Parent.Version, pom)
|
version := r.ResolveProperty(ctx, pom, pom.Parent.Version)
|
||||||
|
|
||||||
if groupID == "" && artifactID == "" && version == "" {
|
if groupID == "" && artifactID == "" && version == "" {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,34 @@
|
|||||||
|
<?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.other.group</groupId>
|
||||||
|
<artifactId>transitive-top-level</artifactId>
|
||||||
|
<version>99</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>
|
||||||
|
<dependency>
|
||||||
|
<groupId>my.org</groupId>
|
||||||
|
<artifactId>child-two</artifactId>
|
||||||
|
<version>2.1.90</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
Loading…
x
Reference in New Issue
Block a user