diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 290a1d28e70..99359abec51 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -64,6 +64,7 @@ func DefaultCatalog() Catalog { Package: defaultPackageConfig(), LinuxKernel: defaultLinuxKernelConfig(), Golang: defaultGolangConfig(), + Java: defaultJavaConfig(), File: defaultFileConfig(), Relationships: defaultRelationshipsConfig(), Source: defaultSourceConfig(), @@ -150,6 +151,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements, }, JavaArchive: java.DefaultArchiveCatalogerConfig(). + WithUseMavenLocalRepository(cfg.Java.UseMavenLocalRepository). + WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir). WithUseNetwork(cfg.Java.UseNetwork). WithMavenBaseURL(cfg.Java.MavenURL). WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth), diff --git a/cmd/syft/internal/options/java.go b/cmd/syft/internal/options/java.go index 79b0fd12771..8894c760b75 100644 --- a/cmd/syft/internal/options/java.go +++ b/cmd/syft/internal/options/java.go @@ -1,24 +1,46 @@ package options -import "github.com/anchore/clio" +import ( + "github.com/anchore/clio" + "github.com/anchore/syft/syft/pkg/cataloger/java" +) type javaConfig struct { UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"` + UseMavenLocalRepository bool `yaml:"use-maven-local-repository" json:"use-maven-local-repository" mapstructure:"use-maven-local-repository"` + MavenLocalRepositoryDir string `yaml:"maven-local-repository-dir" json:"maven-local-repository-dir" mapstructure:"maven-local-repository-dir"` MavenURL string `yaml:"maven-url" json:"maven-url" mapstructure:"maven-url"` MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"` } +func defaultJavaConfig() javaConfig { + def := java.DefaultArchiveCatalogerConfig() + + return javaConfig{ + UseNetwork: def.UseNetwork, + MaxParentRecursiveDepth: def.MaxParentRecursiveDepth, + UseMavenLocalRepository: def.UseMavenLocalRepository, + MavenLocalRepositoryDir: def.MavenLocalRepositoryDir, + MavenURL: def.MavenBaseURL, + } +} + var _ interface { clio.FieldDescriber } = (*javaConfig)(nil) func (o *javaConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { - descriptions.Add(&o.UseNetwork, `enables Syft to use the network to fill in more detailed information about artifacts -currently this enables searching maven-url for license data -when running across pom.xml files that could have more information, syft will -explicitly search maven for license information by querying the online pom when this is true -this option is helpful for when the parent pom has more data, -that is not accessible from within the final built artifact`) + descriptions.Add(&o.UseNetwork, `enables Syft to use the network to fetch version and license information for packages when +a parent or imported pom file is not found in the local maven repository. +the pom files are downloaded from the remote Maven repository at 'maven-url'`) descriptions.Add(&o.MavenURL, `maven repository to use, defaults to Maven central`) - descriptions.Add(&o.MaxParentRecursiveDepth, `depth to recursively resolve parent POMs`) + descriptions.Add(&o.MaxParentRecursiveDepth, `depth to recursively resolve parent POMs, no limit if <= 0`) + descriptions.Add(&o.UseMavenLocalRepository, `use the local Maven repository to retrieve pom files. When Maven is installed and was previously used +for building the software that is being scanned, then most pom files will be available in this +repository on the local file system. this greatly speeds up scans. when all pom files are available +in the local repository, then 'use-network' is not needed. +TIP: If you want to download all required pom files to the local repository without running a full +build, run 'mvn help:effective-pom' before performing the scan with syft.`) + descriptions.Add(&o.MavenLocalRepositoryDir, `override the default location of the local Maven repository. +the default is the subdirectory '.m2/repository' in your home directory`) } diff --git a/syft/file/license.go b/syft/file/license.go index 12d487d540c..08d77e052e8 100644 --- a/syft/file/license.go +++ b/syft/file/license.go @@ -21,7 +21,7 @@ type LicenseEvidence struct { func NewLicense(value string) License { spdxExpression, err := license.ParseExpression(value) if err != nil { - log.Trace("unable to parse license expression: %s, %w", value, err) + log.WithFields("error", err, "value", value).Trace("unable to parse license expression") } return License{ diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go index a3cf11dece8..2dcbb7f8bec 100644 --- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go +++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go @@ -310,6 +310,11 @@ func TestFileParser(t *testing.T, fixturePath string, parser generic.Parser, exp NewCatalogTester().FromFile(t, fixturePath).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser) } +func TestCataloger(t *testing.T, fixtureDir string, cataloger pkg.Cataloger, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) { + t.Helper() + NewCatalogTester().FromDirectory(t, fixtureDir).Expects(expectedPkgs, expectedRelationships).TestCataloger(t, cataloger) +} + func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) { t.Helper() diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 789b3b9d3f1..663563c613d 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -9,8 +9,10 @@ import ( "slices" "strings" + "github.com/vifraa/gopom" "golang.org/x/exp/maps" + "github.com/anchore/syft/internal" intFile "github.com/anchore/syft/internal/file" "github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/log" @@ -52,6 +54,7 @@ type archiveParser struct { fileInfo archiveFilename detectNested bool cfg ArchiveCatalogerConfig + maven *mavenResolver } type genericArchiveParserAdapter struct { @@ -106,6 +109,7 @@ func newJavaArchiveParser(reader file.LocationReadCloser, detectNested bool, cfg fileInfo: newJavaArchiveFilename(currentFilepath), detectNested: detectNested, cfg: cfg, + maven: newMavenResolver(nil, cfg), }, cleanupFn, nil } @@ -197,7 +201,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, return nil, err } - licenses, name, version, err := j.parseLicenses(ctx, manifest) + name, version, licenses, err := j.discoverNameVersionLicense(ctx, manifest) if err != nil { return nil, err } @@ -220,7 +224,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, }, nil } -func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaManifest) ([]pkg.License, string, string, error) { +func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, error) { // we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest // TODO: when we support locations of paths within archives we should start passing the specific manifest location object instead of the top jar licenses := pkg.NewLicensesFromLocation(j.location, selectLicenses(manifest)...) @@ -231,24 +235,18 @@ func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaMan 3. manifest 4. filename */ - name, version, pomLicenses := j.guessMainPackageNameAndVersionFromPomInfo(ctx) - if name == "" { - name = selectName(manifest, j.fileInfo) + groupID, artifactID, version, parsedPom := j.discoverMainPackageFromPomInfo(ctx) + if artifactID == "" { + artifactID = selectName(manifest, j.fileInfo) } if version == "" { version = selectVersion(manifest, j.fileInfo) } - if len(licenses) == 0 { - // Today we don't have a way to distinguish between licenses from the manifest and licenses from the pom.xml - // until the file.Location object can support sub-paths (i.e. paths within archives, recursively; issue https://github.com/anchore/syft/issues/2211). - // Until then it's less confusing to use the licenses from the pom.xml only if the manifest did not list any. - licenses = append(licenses, pomLicenses...) - } if len(licenses) == 0 { fileLicenses, err := j.getLicenseFromFileInArchive() if err != nil { - return nil, "", "", err + return "", "", nil, err } if fileLicenses != nil { licenses = append(licenses, fileLicenses...) @@ -256,50 +254,73 @@ func (j *archiveParser) parseLicenses(ctx context.Context, manifest *pkg.JavaMan } // If we didn't find any licenses in the archive so far, we'll try again in Maven Central using groupIDFromJavaMetadata - if len(licenses) == 0 && j.cfg.UseNetwork { - licenses = findLicenseFromJavaMetadata(ctx, name, manifest, version, j, licenses) + if len(licenses) == 0 { + // Today we don't have a way to distinguish between licenses from the manifest and licenses from the pom.xml + // until the file.Location object can support sub-paths (i.e. paths within archives, recursively; issue https://github.com/anchore/syft/issues/2211). + // Until then it's less confusing to use the licenses from the pom.xml only if the manifest did not list any. + licenses = j.findLicenseFromJavaMetadata(ctx, groupID, artifactID, version, parsedPom, manifest) } - return licenses, name, version, nil + return artifactID, version, licenses, nil } -func findLicenseFromJavaMetadata(ctx context.Context, name string, manifest *pkg.JavaManifest, version string, j *archiveParser, licenses []pkg.License) []pkg.License { - var groupID = name - if gID := groupIDFromJavaMetadata(name, pkg.JavaArchive{Manifest: manifest}); gID != "" { - groupID = gID +// findLicenseFromJavaMetadata attempts to find license information from all available maven metadata properties and pom info +func (j *archiveParser) findLicenseFromJavaMetadata(ctx context.Context, groupID, artifactID, version string, parsedPom *parsedPomProject, manifest *pkg.JavaManifest) []pkg.License { + if groupID == "" { + if gID := groupIDFromJavaMetadata(artifactID, pkg.JavaArchive{Manifest: manifest}); gID != "" { + groupID = gID + } + } + + var err error + var pomLicenses []gopom.License + if parsedPom != nil { + pomLicenses, err = j.maven.resolveLicenses(ctx, parsedPom.project) + if err != nil { + log.WithFields("error", err, "mavenID", j.maven.resolveMavenID(ctx, parsedPom.project)).Debug("error attempting to resolve pom licenses") + } + } + + if err == nil && len(pomLicenses) == 0 { + pomLicenses, err = j.maven.findLicenses(ctx, groupID, artifactID, version) + if err != nil { + log.WithFields("error", err, "mavenID", mavenID{groupID, artifactID, version}).Debug("error attempting to find licenses") + } } - pomLicenses := recursivelyFindLicensesFromParentPom(ctx, groupID, name, version, j.cfg) if len(pomLicenses) == 0 { // Try removing the last part of the groupId, as sometimes it duplicates the artifactId packages := strings.Split(groupID, ".") groupID = strings.Join(packages[:len(packages)-1], ".") - pomLicenses = recursivelyFindLicensesFromParentPom(ctx, groupID, name, version, j.cfg) + pomLicenses, err = j.maven.findLicenses(ctx, groupID, artifactID, version) + if err != nil { + log.WithFields("error", err, "mavenID", mavenID{groupID, artifactID, version}).Debug("error attempting to find sub-group licenses") + } } - if len(pomLicenses) > 0 { - pkgLicenses := pkg.NewLicensesFromLocation(j.location, pomLicenses...) - if pkgLicenses != nil { - licenses = append(licenses, pkgLicenses...) - } + return toPkgLicenses(&j.location, pomLicenses) +} + +func toPkgLicenses(location *file.Location, licenses []gopom.License) []pkg.License { + var out []pkg.License + for _, license := range licenses { + out = append(out, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), location)) } - return licenses + return out } type parsedPomProject struct { - *pkg.JavaPomProject - Licenses []pkg.License + path string + project *gopom.Project } -func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo(ctx context.Context) (name, version string, licenses []pkg.License) { - pomPropertyMatches := j.fileManifest.GlobMatch(false, pomPropertiesGlob) - pomMatches := j.fileManifest.GlobMatch(false, pomXMLGlob) - var pomPropertiesObject pkg.JavaPomProperties - var pomProjectObject *parsedPomProject +// discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information +func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (group, name, version string, parsedPom *parsedPomProject) { + var pomProperties pkg.JavaPomProperties // Find the pom.properties/pom.xml if the names seem like a plausible match - properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, pomPropertyMatches) - projects, _ := pomProjectByParentPath(j.archivePath, j.location, pomMatches) + properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomPropertiesGlob)) + projects, _ := pomProjectByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomXMLGlob)) // map of all the artifacts in the pom properties, in order to chek exact match with the filename artifactsMap := make(map[string]bool) @@ -312,41 +333,32 @@ func (j *archiveParser) guessMainPackageNameAndVersionFromPomInfo(ctx context.Co for _, parentPath := range parentPaths { propertiesObj := properties[parentPath] if artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) { - pomPropertiesObject = propertiesObj + pomProperties = propertiesObj if proj, exists := projects[parentPath]; exists { - pomProjectObject = proj + parsedPom = proj break } } } - name = pomPropertiesObject.ArtifactID - if name == "" && pomProjectObject != nil { - name = pomProjectObject.ArtifactID - } - version = pomPropertiesObject.Version - if version == "" && pomProjectObject != nil { - version = pomProjectObject.Version - } - if j.cfg.UseNetwork { - if pomProjectObject == nil { - // If we have no pom.xml, check maven central using pom.properties - parentLicenses := recursivelyFindLicensesFromParentPom(ctx, pomPropertiesObject.GroupID, pomPropertiesObject.ArtifactID, pomPropertiesObject.Version, j.cfg) - if len(parentLicenses) > 0 { - for _, licenseName := range parentLicenses { - licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil)) - } - } - } else { - findPomLicenses(ctx, pomProjectObject, j.cfg) - } - } + group = pomProperties.GroupID + name = pomProperties.ArtifactID + version = pomProperties.Version - if pomProjectObject != nil { - licenses = pomProjectObject.Licenses + if parsedPom != nil && parsedPom.project != nil { + id := j.maven.resolveMavenID(ctx, parsedPom.project) + if group == "" { + group = id.GroupID + } + if name == "" { + name = id.ArtifactID + } + if version == "" { + version = id.Version + } } - return name, version, licenses + return group, name, version, parsedPom } func artifactIDMatchesFilename(artifactID, fileName string, artifactsMap map[string]bool) bool { @@ -361,24 +373,6 @@ func artifactIDMatchesFilename(artifactID, fileName string, artifactsMap map[str return strings.HasPrefix(artifactID, fileName) || strings.HasSuffix(fileName, artifactID) } -func findPomLicenses(ctx context.Context, pomProjectObject *parsedPomProject, cfg ArchiveCatalogerConfig) { - // If we don't have any licenses until now, and if we have a parent Pom, then we'll check the parent pom in maven central for licenses. - if pomProjectObject != nil && pomProjectObject.Parent != nil && len(pomProjectObject.Licenses) == 0 { - parentLicenses := recursivelyFindLicensesFromParentPom( - ctx, - pomProjectObject.Parent.GroupID, - pomProjectObject.Parent.ArtifactID, - pomProjectObject.Parent.Version, - cfg) - - if len(parentLicenses) > 0 { - for _, licenseName := range parentLicenses { - pomProjectObject.Licenses = append(pomProjectObject.Licenses, pkg.NewLicenseFromFields(licenseName, "", nil)) - } - } - } -} - // discoverPkgsFromAllMavenFiles parses Maven POM properties/xml for a given // parent package, returning all listed Java packages found for each pom // properties discovered and potentially updating the given parentPkg with new @@ -403,12 +397,12 @@ func (j *archiveParser) discoverPkgsFromAllMavenFiles(ctx context.Context, paren } for parentPath, propertiesObj := range properties { - var pomProject *parsedPomProject + var parsedPom *parsedPomProject if proj, exists := projects[parentPath]; exists { - pomProject = proj + parsedPom = proj } - pkgFromPom := newPackageFromMavenData(ctx, propertiesObj, pomProject, parentPkg, j.location, j.cfg) + pkgFromPom := newPackageFromMavenData(ctx, j.maven, propertiesObj, parsedPom, parentPkg, j.location) if pkgFromPom != nil { pkgs = append(pkgs, *pkgFromPom) } @@ -422,7 +416,7 @@ func getDigestsFromArchive(archivePath string) ([]file.Digest, error) { if err != nil { return nil, fmt.Errorf("unable to open archive path (%s): %w", archivePath, err) } - defer archiveCloser.Close() + defer internal.CloseAndLogError(archiveCloser, archivePath) // grab and assign digest for the entire archive digests, err := intFile.NewDigestsFromFile(archiveCloser, javaArchiveHashes) @@ -576,30 +570,26 @@ func pomProjectByParentPath(archivePath string, location file.Location, extractP projectByParentPath := make(map[string]*parsedPomProject) for filePath, fileContents := range contentsOfMavenProjectFiles { // TODO: when we support locations of paths within archives we should start passing the specific pom.xml location object instead of the top jar - pomProject, err := parsePomXMLProject(filePath, strings.NewReader(fileContents), location) + pom, err := decodePomXML(strings.NewReader(fileContents)) if err != nil { log.WithFields("contents-path", filePath, "location", location.Path()).Warnf("failed to parse pom.xml: %+v", err) continue } - - if pomProject == nil { + if pom == nil { continue } - // If we don't have a version, then maybe the parent pom has it... - if (pomProject.Parent == nil && pomProject.Version == "") || pomProject.ArtifactID == "" { - // TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package - continue + projectByParentPath[path.Dir(filePath)] = &parsedPomProject{ + path: filePath, + project: pom, } - - projectByParentPath[path.Dir(filePath)] = pomProject } return projectByParentPath, nil } // newPackageFromMavenData processes a single Maven POM properties for a given parent package, returning all listed Java packages found and // associating each discovered package to the given parent package. Note the pom.xml is optional, the pom.properties is not. -func newPackageFromMavenData(ctx context.Context, pomProperties pkg.JavaPomProperties, parsedPomProject *parsedPomProject, parentPkg *pkg.Package, location file.Location, cfg ArchiveCatalogerConfig) *pkg.Package { +func newPackageFromMavenData(ctx context.Context, r *mavenResolver, pomProperties pkg.JavaPomProperties, parsedPom *parsedPomProject, parentPkg *pkg.Package, location file.Location) *pkg.Package { // keep the artifact name within the virtual path if this package does not match the parent package vPathSuffix := "" groupID := "" @@ -622,25 +612,24 @@ func newPackageFromMavenData(ctx context.Context, pomProperties pkg.JavaPomPrope virtualPath := location.Path() + vPathSuffix var pkgPomProject *pkg.JavaPomProject - licenses := make([]pkg.License, 0) - if cfg.UseNetwork { - if parsedPomProject == nil { - // If we have no pom.xml, check maven central using pom.properties - parentLicenses := recursivelyFindLicensesFromParentPom(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version, cfg) - if len(parentLicenses) > 0 { - for _, licenseName := range parentLicenses { - licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil)) - } - } - } else { - findPomLicenses(ctx, parsedPomProject, cfg) - } + var err error + var pomLicenses []gopom.License + if parsedPom == nil { + // If we have no pom.xml, check maven central using pom.properties + pomLicenses, err = r.findLicenses(ctx, pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version) + } else { + pkgPomProject = newPomProject(ctx, r, parsedPom.path, parsedPom.project) + pomLicenses, err = r.resolveLicenses(ctx, parsedPom.project) + } + + if err != nil { + log.WithFields("error", err, "mavenID", mavenID{pomProperties.GroupID, pomProperties.ArtifactID, pomProperties.Version}).Debug("error attempting to resolve licenses") } - if parsedPomProject != nil { - pkgPomProject = parsedPomProject.JavaPomProject - licenses = append(licenses, parsedPomProject.Licenses...) + licenses := make([]pkg.License, 0) + for _, license := range pomLicenses { + licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), &location)) } p := pkg.Package{ diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index 74a2195e392..ccddc8c7528 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -5,8 +5,6 @@ import ( "context" "fmt" "io" - "net/http" - "net/http/httptest" "os" "os/exec" "path/filepath" @@ -20,6 +18,7 @@ import ( "github.com/scylladb/go-set/strset" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/vifraa/gopom" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -28,61 +27,14 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) -func generateJavaBuildFixture(t *testing.T, fixturePath string) { - if _, err := os.Stat(fixturePath); !os.IsNotExist(err) { - // fixture already exists... - return - } - - makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/") - t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask)) - - cwd, err := os.Getwd() - if err != nil { - t.Errorf("unable to get cwd: %+v", err) - } - - cmd := exec.Command("make", makeTask) - cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/") - - run(t, cmd) -} - -func generateMockMavenHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - // Set the Content-Type header to indicate that the response is XML - w.Header().Set("Content-Type", "application/xml") - // Copy the file's content to the response writer - file, err := os.Open(responseFixture) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer file.Close() - _, err = io.Copy(w, file) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - } -} - -type handlerPath struct { - path string - handler func(w http.ResponseWriter, r *http.Request) -} - func TestSearchMavenForLicenses(t *testing.T) { - mux, url, teardown := setup() - defer teardown() + url := mockMavenRepo(t) + tests := []struct { name string fixture string detectNested bool config ArchiveCatalogerConfig - requestPath string - requestHandlers []handlerPath expectedLicenses []pkg.License }{ { @@ -91,23 +43,16 @@ func TestSearchMavenForLicenses(t *testing.T) { detectNested: false, config: ArchiveCatalogerConfig{ UseNetwork: true, + UseMavenLocalRepository: false, MavenBaseURL: url, - MaxParentRecursiveDepth: 2, - }, - requestHandlers: []handlerPath{ - { - path: "/org/opensaml/opensaml-parent/3.4.6/opensaml-parent-3.4.6.pom", - handler: generateMockMavenHandler("test-fixtures/maven-xml-responses/opensaml-parent-3.4.6.pom"), - }, - { - path: "/net/shibboleth/parent/7.11.2/parent-7.11.2.pom", - handler: generateMockMavenHandler("test-fixtures/maven-xml-responses/parent-7.11.2.pom"), - }, }, expectedLicenses: []pkg.License{ { - Type: license.Declared, - Value: `The Apache Software License, Version 2.0`, + Type: license.Declared, + Value: `The Apache Software License, Version 2.0`, + URLs: []string{ + "http://www.apache.org/licenses/LICENSE-2.0.txt", + }, SPDXExpression: ``, }, }, @@ -116,11 +61,6 @@ func TestSearchMavenForLicenses(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // configure maven central requests - for _, hdlr := range tc.requestHandlers { - mux.HandleFunc(hdlr.path, hdlr.handler) - } - // setup metadata fixture; note: // this fixture has a pomProjectObject and has a parent object // it has no licenses on either which is the condition for testing @@ -138,34 +78,9 @@ func TestSearchMavenForLicenses(t *testing.T) { defer cleanupFn() // assert licenses are discovered from upstream - _, _, licenses := ap.guessMainPackageNameAndVersionFromPomInfo(context.Background()) - assert.Equal(t, tc.expectedLicenses, licenses) - }) - } -} - -func TestFormatMavenURL(t *testing.T) { - tests := []struct { - name string - groupID string - artifactID string - version string - expected string - }{ - { - name: "formatMavenURL correctly assembles the pom URL", - groupID: "org.springframework.boot", - artifactID: "spring-boot-starter-test", - version: "3.1.5", - expected: "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-test/3.1.5/spring-boot-starter-test-3.1.5.pom", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - requestURL, err := formatMavenPomURL(tc.groupID, tc.artifactID, tc.version, mavenBaseURL) - assert.NoError(t, err, "expected no err; got %w", err) - assert.Equal(t, tc.expected, requestURL) + _, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background()) + licenses, _ := ap.maven.resolveLicenses(context.Background(), parsedPom.project) + assert.Equal(t, tc.expectedLicenses, toPkgLicenses(nil, licenses)) }) } } @@ -424,10 +339,14 @@ func TestParseJar(t *testing.T) { test.expected[k] = p } + cfg := ArchiveCatalogerConfig{ + UseNetwork: false, + UseMavenLocalRepository: false, + } parser, cleanupFn, err := newJavaArchiveParser(file.LocationReadCloser{ Location: file.NewLocation(fixture.Name()), ReadCloser: fixture, - }, false, ArchiveCatalogerConfig{UseNetwork: false}) + }, false, cfg) defer cleanupFn() require.NoError(t, err) @@ -843,26 +762,23 @@ func Test_newPackageFromMavenData(t *testing.T) { Version: "1.0", }, project: &parsedPomProject{ - JavaPomProject: &pkg.JavaPomProject{ - Parent: &pkg.JavaPomParent{ - GroupID: "some-parent-group-id", - ArtifactID: "some-parent-artifact-id", - Version: "1.0-parent", + project: &gopom.Project{ + Parent: &gopom.Parent{ + GroupID: ptr("some-parent-group-id"), + ArtifactID: ptr("some-parent-artifact-id"), + Version: ptr("1.0-parent"), }, - Name: "some-name", - GroupID: "some-group-id", - ArtifactID: "some-artifact-id", - Version: "1.0", - Description: "desc", - URL: "aweso.me", - }, - Licenses: []pkg.License{ - { - Value: "MIT", - SPDXExpression: "MIT", - Type: license.Declared, - URLs: []string{"https://opensource.org/licenses/MIT"}, - Locations: file.NewLocationSet(file.NewLocation("some-license-path")), + Name: ptr("some-name"), + GroupID: ptr("some-group-id"), + ArtifactID: ptr("some-artifact-id"), + Version: ptr("1.0"), + Description: ptr("desc"), + URL: ptr("aweso.me"), + Licenses: &[]gopom.License{ + { + Name: ptr("MIT"), + URL: ptr("https://opensource.org/licenses/MIT"), + }, }, }, }, @@ -898,7 +814,7 @@ func Test_newPackageFromMavenData(t *testing.T) { SPDXExpression: "MIT", Type: license.Declared, URLs: []string{"https://opensource.org/licenses/MIT"}, - Locations: file.NewLocationSet(file.NewLocation("some-license-path")), + Locations: file.NewLocationSet(file.NewLocation("given/virtual/path")), }, ), Metadata: pkg.JavaArchive{ @@ -1122,7 +1038,8 @@ func Test_newPackageFromMavenData(t *testing.T) { } test.expectedParent.Locations = locations - actualPackage := newPackageFromMavenData(context.Background(), test.props, test.project, test.parent, file.NewLocation(virtualPath), DefaultArchiveCatalogerConfig()) + r := newMavenResolver(nil, DefaultArchiveCatalogerConfig()) + actualPackage := newPackageFromMavenData(context.Background(), r, test.props, test.project, test.parent, file.NewLocation(virtualPath)) if test.expectedPackage == nil { require.Nil(t, actualPackage) } else { @@ -1337,6 +1254,8 @@ func Test_parseJavaArchive_regressions(t *testing.T) { PomProject: &pkg.JavaPomProject{ Path: "META-INF/maven/org.apache.directory.api/api-asn1-api/pom.xml", ArtifactID: "api-asn1-api", + GroupID: "org.apache.directory.api", + Version: "2.0.0", Name: "Apache Directory API ASN.1 API", Description: "ASN.1 API", Parent: &pkg.JavaPomParent{ @@ -1388,14 +1307,12 @@ func Test_parseJavaArchive_regressions(t *testing.T) { func Test_deterministicMatchingPomProperties(t *testing.T) { tests := []struct { - fixture string - expectedName string - expectedVersion string + fixture string + expected mavenID }{ { - fixture: "multiple-matching-2.11.5", - expectedName: "multiple-matching-1", - expectedVersion: "2.11.5", + fixture: "multiple-matching-2.11.5", + expected: mavenID{"org.multiple", "multiple-matching-1", "2.11.5"}, }, } @@ -1415,9 +1332,8 @@ func Test_deterministicMatchingPomProperties(t *testing.T) { defer cleanupFn() require.NoError(t, err) - name, version, _ := parser.guessMainPackageNameAndVersionFromPomInfo(context.TODO()) - require.Equal(t, test.expectedName, name) - require.Equal(t, test.expectedVersion, version) + groupID, artifactID, version, _ := parser.discoverMainPackageFromPomInfo(context.TODO()) + require.Equal(t, test.expected, mavenID{groupID, artifactID, version}) }() } }) @@ -1436,6 +1352,26 @@ func assignParent(parent *pkg.Package, childPackages ...pkg.Package) { } } +func generateJavaBuildFixture(t *testing.T, fixturePath string) { + if _, err := os.Stat(fixturePath); !os.IsNotExist(err) { + // fixture already exists... + return + } + + makeTask := strings.TrimPrefix(fixturePath, "test-fixtures/java-builds/") + t.Logf(color.Bold.Sprintf("Generating Fixture from 'make %s'", makeTask)) + + cwd, err := os.Getwd() + if err != nil { + t.Errorf("unable to get cwd: %+v", err) + } + + cmd := exec.Command("make", makeTask) + cmd.Dir = filepath.Join(cwd, "test-fixtures/java-builds/") + + run(t, cmd) +} + func generateJavaMetadataJarFixture(t *testing.T, fixtureName string) string { fixturePath := filepath.Join("test-fixtures/jar-metadata/cache/", fixtureName+".jar") if _, err := os.Stat(fixturePath); !os.IsNotExist(err) { @@ -1504,20 +1440,7 @@ func run(t testing.TB, cmd *exec.Cmd) { } } -// setup sets up a test HTTP server for mocking requests to maven central. -// The returned url is injected into the Config so the client uses the test server. -// Tests should register handlers on mux to simulate the expected request/response structure -func setup() (mux *http.ServeMux, serverURL string, teardown func()) { - // mux is the HTTP request multiplexer used with the test server. - mux = http.NewServeMux() - - // We want to ensure that tests catch mistakes where the endpoint URL is - // specified as absolute rather than relative. It only makes a difference - // when there's a non-empty base URL path. So, use that. See issue #752. - apiHandler := http.NewServeMux() - apiHandler.Handle("/", mux) - // server is a test HTTP server used to provide mock API responses. - server := httptest.NewServer(apiHandler) - - return mux, server.URL, server.Close +// ptr returns a pointer to the given value +func ptr[T any](value T) *T { + return &value } diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index 9552b142ddf..11e48b7f5ad 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -32,10 +32,9 @@ func NewArchiveCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger { // NewPomCataloger returns a cataloger capable of parsing dependencies from a pom.xml file. // Pom files list dependencies that maybe not be locally installed yet. func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger { - gap := newGenericArchiveParserAdapter(cfg) - - return generic.NewCataloger("java-pom-cataloger"). - WithParserByGlobs(gap.parserPomXML, "**/pom.xml") + return pomXMLCataloger{ + cfg: cfg, + } } // NewGradleLockfileCataloger returns a cataloger capable of parsing dependencies from a gradle.lockfile file. diff --git a/syft/pkg/cataloger/java/config.go b/syft/pkg/cataloger/java/config.go index 14c31d33426..29096d59bff 100644 --- a/syft/pkg/cataloger/java/config.go +++ b/syft/pkg/cataloger/java/config.go @@ -7,6 +7,8 @@ const mavenBaseURL = "https://repo1.maven.org/maven2" type ArchiveCatalogerConfig struct { cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"` UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"` + UseMavenLocalRepository bool `yaml:"use-maven-localrepository" json:"use-maven-localrepository" mapstructure:"use-maven-localrepository"` + MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"` MavenBaseURL string `yaml:"maven-base-url" json:"maven-base-url" mapstructure:"maven-base-url"` MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"` } @@ -15,8 +17,10 @@ func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig { return ArchiveCatalogerConfig{ ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(), UseNetwork: false, + UseMavenLocalRepository: false, + MavenLocalRepositoryDir: defaultMavenLocalRepoDir(), MavenBaseURL: mavenBaseURL, - MaxParentRecursiveDepth: 5, + MaxParentRecursiveDepth: 0, // unlimited } } @@ -25,6 +29,16 @@ func (j ArchiveCatalogerConfig) WithUseNetwork(input bool) ArchiveCatalogerConfi return j } +func (j ArchiveCatalogerConfig) WithUseMavenLocalRepository(input bool) ArchiveCatalogerConfig { + j.UseMavenLocalRepository = input + return j +} + +func (j ArchiveCatalogerConfig) WithMavenLocalRepositoryDir(input string) ArchiveCatalogerConfig { + j.MavenLocalRepositoryDir = input + return j +} + func (j ArchiveCatalogerConfig) WithMavenBaseURL(input string) ArchiveCatalogerConfig { if input != "" { j.MavenBaseURL = input @@ -33,9 +47,7 @@ func (j ArchiveCatalogerConfig) WithMavenBaseURL(input string) ArchiveCatalogerC } func (j ArchiveCatalogerConfig) WithArchiveTraversal(search cataloging.ArchiveSearchConfig, maxDepth int) ArchiveCatalogerConfig { - if maxDepth > 0 { - j.MaxParentRecursiveDepth = maxDepth - } + j.MaxParentRecursiveDepth = maxDepth j.ArchiveSearchConfig = search return j } diff --git a/syft/pkg/cataloger/java/maven_repo_utils.go b/syft/pkg/cataloger/java/maven_repo_utils.go deleted file mode 100644 index e84db154a4f..00000000000 --- a/syft/pkg/cataloger/java/maven_repo_utils.go +++ /dev/null @@ -1,136 +0,0 @@ -package java - -import ( - "context" - "fmt" - "io" - "net/http" - "net/url" - "strings" - "time" - - "github.com/vifraa/gopom" - - "github.com/anchore/syft/internal/log" -) - -func formatMavenPomURL(groupID, artifactID, version, mavenBaseURL string) (requestURL string, err error) { - // groupID needs to go from maven.org -> maven/org - urlPath := strings.Split(groupID, ".") - artifactPom := fmt.Sprintf("%s-%s.pom", artifactID, version) - urlPath = append(urlPath, artifactID, version, artifactPom) - - // ex:"https://repo1.maven.org/maven2/groupID/artifactID/artifactPom - requestURL, err = url.JoinPath(mavenBaseURL, urlPath...) - if err != nil { - return requestURL, fmt.Errorf("could not construct maven url: %w", err) - } - return requestURL, err -} - -// An artifact can have its version defined in a parent's DependencyManagement section -func recursivelyFindVersionFromParentPom(ctx context.Context, groupID, artifactID, parentGroupID, parentArtifactID, parentVersion string, cfg ArchiveCatalogerConfig) string { - // As there can be nested parent poms, we'll recursively check for the version until we reach the max depth - for i := 0; i < cfg.MaxParentRecursiveDepth; i++ { - parentPom, err := getPomFromMavenRepo(ctx, parentGroupID, parentArtifactID, parentVersion, cfg.MavenBaseURL) - if err != nil { - // We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error - log.Tracef("unable to get parent pom from Maven central: %v", err) - break - } - if parentPom != nil && parentPom.DependencyManagement != nil { - for _, dependency := range *parentPom.DependencyManagement.Dependencies { - if groupID == *dependency.GroupID && artifactID == *dependency.ArtifactID && dependency.Version != nil { - return *dependency.Version - } - } - } - if parentPom == nil || parentPom.Parent == nil { - break - } - parentGroupID = *parentPom.Parent.GroupID - parentArtifactID = *parentPom.Parent.ArtifactID - parentVersion = *parentPom.Parent.Version - } - return "" -} - -func recursivelyFindLicensesFromParentPom(ctx context.Context, groupID, artifactID, version string, cfg ArchiveCatalogerConfig) []string { - var licenses []string - // As there can be nested parent poms, we'll recursively check for licenses until we reach the max depth - for i := 0; i < cfg.MaxParentRecursiveDepth; i++ { - parentPom, err := getPomFromMavenRepo(ctx, groupID, artifactID, version, cfg.MavenBaseURL) - if err != nil { - // We don't want to abort here as the parent pom might not exist in Maven Central, we'll just log the error - log.Tracef("unable to get parent pom from Maven central: %v", err) - return []string{} - } - parentLicenses := parseLicensesFromPom(parentPom) - if len(parentLicenses) > 0 || parentPom == nil || parentPom.Parent == nil { - licenses = parentLicenses - break - } - - groupID = *parentPom.Parent.GroupID - artifactID = *parentPom.Parent.ArtifactID - version = *parentPom.Parent.Version - } - - return licenses -} - -func getPomFromMavenRepo(ctx context.Context, groupID, artifactID, version, mavenBaseURL string) (*gopom.Project, error) { - requestURL, err := formatMavenPomURL(groupID, artifactID, version, mavenBaseURL) - if err != nil { - return nil, err - } - log.Tracef("trying to fetch parent pom from Maven central %s", requestURL) - - mavenRequest, err := http.NewRequest(http.MethodGet, requestURL, nil) - if err != nil { - return nil, fmt.Errorf("unable to format request for Maven central: %w", err) - } - - httpClient := &http.Client{ - Timeout: time.Second * 10, - } - - mavenRequest = mavenRequest.WithContext(ctx) - - resp, err := httpClient.Do(mavenRequest) - if err != nil { - return nil, fmt.Errorf("unable to get pom from Maven central: %w", err) - } - defer func() { - if err := resp.Body.Close(); err != nil { - log.Errorf("unable to close body: %+v", err) - } - }() - - bytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err) - } - - pom, err := decodePomXML(strings.NewReader(string(bytes))) - if err != nil { - return nil, fmt.Errorf("unable to parse pom from Maven central: %w", err) - } - - return &pom, nil -} - -func parseLicensesFromPom(pom *gopom.Project) []string { - var licenses []string - if pom != nil && pom.Licenses != nil { - for _, license := range *pom.Licenses { - if license.Name != nil { - licenses = append(licenses, *license.Name) - } else if license.URL != nil { - licenses = append(licenses, *license.URL) - } - } - } - - return licenses -} diff --git a/syft/pkg/cataloger/java/maven_resolver.go b/syft/pkg/cataloger/java/maven_resolver.go new file mode 100644 index 00000000000..5fddccc3191 --- /dev/null +++ b/syft/pkg/cataloger/java/maven_resolver.go @@ -0,0 +1,624 @@ +package java + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + "path/filepath" + "reflect" + "regexp" + "slices" + "strings" + "time" + + "github.com/vifraa/gopom" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/cache" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/file" +) + +// mavenID is the unique identifier for a package in Maven +type mavenID struct { + GroupID string + ArtifactID string + Version string +} + +func (m mavenID) String() string { + return fmt.Sprintf("(groupId: %s artifactId: %s version: %s)", m.GroupID, m.ArtifactID, m.Version) +} + +var expressionMatcher = regexp.MustCompile("[$][{][^}]+[}]") + +// mavenResolver is a short-lived utility to resolve maven poms from multiple sources, including: +// the scanned filesystem, local maven cache directories, remote maven repositories, and the syft cache +type mavenResolver struct { + cfg ArchiveCatalogerConfig + cache cache.Cache + resolved map[mavenID]*gopom.Project + remoteRequestTimeout time.Duration + checkedLocalRepo bool + // fileResolver and pomLocations are used to resolve parent poms by relativePath + fileResolver file.Resolver + pomLocations map[*gopom.Project]file.Location +} + +// newMavenResolver constructs a new mavenResolver with the given configuration. +// NOTE: the fileResolver is optional and if provided will be used to resolve parent poms by relative path +func newMavenResolver(fileResolver file.Resolver, cfg ArchiveCatalogerConfig) *mavenResolver { + return &mavenResolver{ + cfg: cfg, + cache: cache.GetManager().GetCache("java/maven/repo", "v1"), + resolved: map[mavenID]*gopom.Project{}, + remoteRequestTimeout: time.Second * 10, + fileResolver: fileResolver, + pomLocations: map[*gopom.Project]file.Location{}, + } +} + +// getPropertyValue gets property values by emulating maven property resolution logic, looking in the project's variables +// as well as supporting the project expressions like ${project.parent.groupId}. +// Properties which are not resolved result in empty string "" +func (r *mavenResolver) getPropertyValue(ctx context.Context, propertyValue *string, resolutionContext ...*gopom.Project) string { + if propertyValue == nil { + return "" + } + resolved, err := r.resolveExpression(ctx, resolutionContext, *propertyValue, nil) + if err != nil { + log.WithFields("error", err, "propertyValue", *propertyValue).Debug("error resolving maven property") + return "" + } + return resolved +} + +// resolveExpression resolves an expression, which may be a plain string or a string with ${ property.references } +func (r *mavenResolver) resolveExpression(ctx context.Context, resolutionContext []*gopom.Project, expression string, resolving []string) (string, error) { + var err error + return expressionMatcher.ReplaceAllStringFunc(expression, func(match string) string { + propertyExpression := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing } + resolved, e := r.resolveProperty(ctx, resolutionContext, propertyExpression, resolving) + if e != nil { + err = errors.Join(err, e) + return "" + } + return resolved + }), err +} + +// resolveProperty resolves properties recursively from the root project +func (r *mavenResolver) resolveProperty(ctx context.Context, resolutionContext []*gopom.Project, propertyExpression string, resolving []string) (string, error) { + // prevent cycles + if slices.Contains(resolving, propertyExpression) { + return "", fmt.Errorf("cycle detected resolving: %s", propertyExpression) + } + if len(resolutionContext) == 0 { + return "", fmt.Errorf("no project variable resolution context provided for expression: '%s'", propertyExpression) + } + resolving = append(resolving, propertyExpression) + + // only resolve project. properties in the context of the current project pom + value, err := r.resolveProjectProperty(ctx, resolutionContext, resolutionContext[len(resolutionContext)-1], propertyExpression, resolving) + if err != nil { + return value, err + } + if value != "" { + return value, nil + } + + for _, pom := range resolutionContext { + current := pom + for parentDepth := 0; current != nil; parentDepth++ { + if r.cfg.MaxParentRecursiveDepth > 0 && parentDepth > r.cfg.MaxParentRecursiveDepth { + return "", fmt.Errorf("maximum parent recursive depth (%v) reached resolving property: %v", r.cfg.MaxParentRecursiveDepth, propertyExpression) + } + if current.Properties != nil && current.Properties.Entries != nil { + if value, ok := current.Properties.Entries[propertyExpression]; ok { + return r.resolveExpression(ctx, resolutionContext, value, resolving) // property values can contain expressions + } + } + current, err = r.resolveParent(ctx, current) + if err != nil { + return "", err + } + } + } + + return "", fmt.Errorf("unable to resolve property: %s", propertyExpression) +} + +// resolveProjectProperty resolves properties on the project +// +//nolint:gocognit +func (r *mavenResolver) resolveProjectProperty(ctx context.Context, resolutionContext []*gopom.Project, pom *gopom.Project, propertyExpression string, resolving []string) (string, error) { + // see if we have a project.x expression and process this based + // on the xml tags in gopom + parts := strings.Split(propertyExpression, ".") + numParts := len(parts) + if numParts > 1 && strings.TrimSpace(parts[0]) == "project" { + pomValue := reflect.ValueOf(pom).Elem() + pomValueType := pomValue.Type() + for partNum := 1; partNum < numParts; partNum++ { + if pomValueType.Kind() != reflect.Struct { + break + } + + part := parts[partNum] + // these two fields are directly inherited from the pom parent values + if partNum == 1 && pom.Parent != nil { + switch part { + case "version": + if pom.Version == nil && pom.Parent.Version != nil { + return r.resolveExpression(ctx, resolutionContext, *pom.Parent.Version, resolving) + } + case "groupID": + if pom.GroupID == nil && pom.Parent.GroupID != nil { + return r.resolveExpression(ctx, resolutionContext, *pom.Parent.GroupID, resolving) + } + } + } + for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ { + f := pomValueType.Field(fieldNum) + tag := f.Tag.Get("xml") + tag = strings.Split(tag, ",")[0] + // a segment of the property name matches the xml tag for the field, + // so we need to recurse down the nested structs or return a match + // if we're done. + if part != tag { + continue + } + + pomValue = pomValue.Field(fieldNum) + pomValueType = pomValue.Type() + if pomValueType.Kind() == reflect.Ptr { + // we were recursing down the nested structs, but one of the steps + // we need to take is a nil pointer, so give up + if pomValue.IsNil() { + return "", fmt.Errorf("property undefined: %s", propertyExpression) + } + pomValue = pomValue.Elem() + if !pomValue.IsZero() { + // we found a non-zero value whose tag matches this part of the property name + pomValueType = pomValue.Type() + } + } + // If this was the last part of the property name, return the value + if partNum == numParts-1 { + value := fmt.Sprintf("%v", pomValue.Interface()) + return r.resolveExpression(ctx, resolutionContext, value, resolving) + } + break + } + } + } + return "", nil +} + +// resolveMavenID creates a new mavenID from a pom, resolving parent information as necessary +func (r *mavenResolver) resolveMavenID(ctx context.Context, pom *gopom.Project) mavenID { + if pom == nil { + return mavenID{} + } + groupID := r.getPropertyValue(ctx, pom.GroupID, pom) + artifactID := r.getPropertyValue(ctx, pom.ArtifactID, pom) + version := r.getPropertyValue(ctx, pom.Version, pom) + if pom.Parent != nil { + if groupID == "" { + groupID = r.getPropertyValue(ctx, pom.Parent.GroupID, pom) + } + if artifactID == "" { + artifactID = r.getPropertyValue(ctx, pom.Parent.ArtifactID, pom) + } + if version == "" { + version = r.getPropertyValue(ctx, pom.Parent.Version, pom) + } + } + return mavenID{groupID, artifactID, version} +} + +// resolveDependencyID creates a new mavenID from a dependency element in a pom, resolving information as necessary +func (r *mavenResolver) resolveDependencyID(ctx context.Context, pom *gopom.Project, dep gopom.Dependency) mavenID { + if pom == nil { + return mavenID{} + } + + groupID := r.getPropertyValue(ctx, dep.GroupID, pom) + artifactID := r.getPropertyValue(ctx, dep.ArtifactID, pom) + version := r.getPropertyValue(ctx, dep.Version, pom) + + var err error + if version == "" { + version, err = r.findInheritedVersion(ctx, pom, groupID, artifactID) + } + + depID := mavenID{groupID, artifactID, version} + + if err != nil { + log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "dependencyID", depID) + } + + return depID +} + +// findPom gets a pom from cache, local repository, or from a remote Maven repository depending on configuration +func (r *mavenResolver) findPom(ctx context.Context, groupID, artifactID, version string) (*gopom.Project, error) { + if groupID == "" || artifactID == "" || version == "" { + return nil, fmt.Errorf("invalid maven pom specification, require non-empty values for groupID: '%s', artifactID: '%s', version: '%s'", groupID, artifactID, version) + } + + id := mavenID{groupID, artifactID, version} + pom := r.resolved[id] + + if pom != nil { + return pom, nil + } + + var errs error + + // try to resolve first from local maven repo + if r.cfg.UseMavenLocalRepository { + pom, err := r.findPomInLocalRepository(groupID, artifactID, version) + if pom != nil { + r.resolved[id] = pom + return pom, nil + } + errs = errors.Join(errs, err) + } + + // resolve via network maven repository + if pom == nil && r.cfg.UseNetwork { + pom, err := r.findPomInRemoteRepository(ctx, groupID, artifactID, version) + if pom != nil { + r.resolved[id] = pom + return pom, nil + } + errs = errors.Join(errs, err) + } + + return nil, fmt.Errorf("unable to resolve pom %s %s %s: %w", groupID, artifactID, version, errs) +} + +// findPomInLocalRepository attempts to get the POM from the users local maven repository +func (r *mavenResolver) findPomInLocalRepository(groupID, artifactID, version string) (*gopom.Project, error) { + groupPath := filepath.Join(strings.Split(groupID, ".")...) + pomFilePath := filepath.Join(r.cfg.MavenLocalRepositoryDir, groupPath, artifactID, version, artifactID+"-"+version+".pom") + pomFile, err := os.Open(pomFilePath) + if err != nil { + if !r.checkedLocalRepo && errors.Is(err, os.ErrNotExist) { + r.checkedLocalRepo = true + // check if the directory exists at all, and if not just stop trying to resolve local maven files + fi, err := os.Stat(r.cfg.MavenLocalRepositoryDir) + if errors.Is(err, os.ErrNotExist) || !fi.IsDir() { + log.WithFields("error", err, "repositoryDir", r.cfg.MavenLocalRepositoryDir). + Info("local maven repository is not a readable directory, stopping local resolution") + r.cfg.UseMavenLocalRepository = false + } + } + return nil, err + } + defer internal.CloseAndLogError(pomFile, pomFilePath) + + return decodePomXML(pomFile) +} + +// findPomInRemoteRepository download the pom file from a (remote) Maven repository over HTTP +func (r *mavenResolver) findPomInRemoteRepository(ctx context.Context, groupID, artifactID, version string) (*gopom.Project, error) { + if groupID == "" || artifactID == "" || version == "" { + return nil, fmt.Errorf("missing/incomplete maven artifact coordinates -- groupId: '%s' artifactId: '%s', version: '%s'", groupID, artifactID, version) + } + + requestURL, err := remotePomURL(r.cfg.MavenBaseURL, groupID, artifactID, version) + if err != nil { + return nil, fmt.Errorf("unable to find pom in remote due to: %w", err) + } + + // Downloading snapshots requires additional steps to determine the latest snapshot version. + // See: https://maven.apache.org/ref/3-LATEST/maven-repository-metadata/ + if strings.HasSuffix(version, "-SNAPSHOT") { + return nil, fmt.Errorf("downloading snapshot artifacts is not supported, got: %s", requestURL) + } + + cacheKey := strings.TrimPrefix(strings.TrimPrefix(requestURL, "http://"), "https://") + reader, err := r.cacheResolveReader(cacheKey, func() (io.ReadCloser, error) { + if err != nil { + return nil, err + } + log.WithFields("url", requestURL).Info("fetching parent pom from remote maven repository") + + req, err := http.NewRequest(http.MethodGet, requestURL, nil) + if err != nil { + return nil, fmt.Errorf("unable to create request for Maven central: %w", err) + } + + req = req.WithContext(ctx) + + client := http.Client{ + Timeout: r.remoteRequestTimeout, + } + + resp, err := client.Do(req) //nolint:bodyclose + if err != nil { + return nil, fmt.Errorf("unable to get pom from Maven repository %v: %w", requestURL, err) + } + if resp.StatusCode == http.StatusNotFound { + return nil, fmt.Errorf("pom not found in Maven repository at: %v", requestURL) + } + return resp.Body, err + }) + if err != nil { + return nil, err + } + if reader, ok := reader.(io.Closer); ok { + defer internal.CloseAndLogError(reader, requestURL) + } + pom, err := decodePomXML(reader) + if err != nil { + return nil, fmt.Errorf("unable to parse pom from Maven repository url %v: %w", requestURL, err) + } + return pom, nil +} + +// cacheResolveReader attempts to get a reader from cache, otherwise caches the contents of the resolve() function. +// this function is guaranteed to return an unread reader for the correct contents. +// NOTE: this could be promoted to the internal cache package as a specialized version of the cache.Resolver +// if there are more users of this functionality +func (r *mavenResolver) cacheResolveReader(key string, resolve func() (io.ReadCloser, error)) (io.Reader, error) { + reader, err := r.cache.Read(key) + if err == nil && reader != nil { + return reader, err + } + + contentReader, err := resolve() + if err != nil { + return nil, err + } + defer internal.CloseAndLogError(contentReader, key) + + // store the contents to return a new reader with the same content + contents, err := io.ReadAll(contentReader) + if err != nil { + return nil, err + } + err = r.cache.Write(key, bytes.NewBuffer(contents)) + return bytes.NewBuffer(contents), err +} + +// resolveParent attempts to resolve the parent for the given pom +func (r *mavenResolver) resolveParent(ctx context.Context, pom *gopom.Project) (*gopom.Project, error) { + if pom == nil || pom.Parent == nil { + return nil, nil + } + parent := pom.Parent + pomWithoutParent := *pom + pomWithoutParent.Parent = nil + groupID := r.getPropertyValue(ctx, parent.GroupID, &pomWithoutParent) + artifactID := r.getPropertyValue(ctx, parent.ArtifactID, &pomWithoutParent) + version := r.getPropertyValue(ctx, parent.Version, &pomWithoutParent) + + // check cache before resolving + parentID := mavenID{groupID, artifactID, version} + if resolvedParent, ok := r.resolved[parentID]; ok { + return resolvedParent, nil + } + + // check if the pom exists in the fileResolver + parentPom := r.findParentPomByRelativePath(ctx, pom, parentID) + if parentPom != nil { + return parentPom, nil + } + + // find POM normally + return r.findPom(ctx, groupID, artifactID, version) +} + +// findInheritedVersion attempts to find the version of a dependency (groupID, artifactID) by searching all parent poms and imported managed dependencies +// +//nolint:gocognit,funlen +func (r *mavenResolver) findInheritedVersion(ctx context.Context, pom *gopom.Project, groupID, artifactID string, resolutionContext ...*gopom.Project) (string, error) { + if pom == nil { + return "", fmt.Errorf("nil pom provided to findInheritedVersion") + } + if r.cfg.MaxParentRecursiveDepth > 0 && len(resolutionContext) > r.cfg.MaxParentRecursiveDepth { + return "", fmt.Errorf("maximum depth reached attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.resolveMavenID(ctx, pom)) + } + if slices.Contains(resolutionContext, pom) { + return "", fmt.Errorf("cycle detected attempting to resolve version for: %s:%s at: %v", groupID, artifactID, r.resolveMavenID(ctx, pom)) + } + resolutionContext = append(resolutionContext, pom) + + var err error + var version string + + // check for entries in dependencyManagement first + for _, dep := range pomManagedDependencies(pom) { + depGroupID := r.getPropertyValue(ctx, dep.GroupID, resolutionContext...) + depArtifactID := r.getPropertyValue(ctx, dep.ArtifactID, resolutionContext...) + if depGroupID == groupID && depArtifactID == artifactID { + version = r.getPropertyValue(ctx, dep.Version, resolutionContext...) + if version != "" { + return version, nil + } + } + + // imported pom files should be treated just like parent poms, they are used to define versions of dependencies + if deref(dep.Type) == "pom" && deref(dep.Scope) == "import" { + depVersion := r.getPropertyValue(ctx, dep.Version, resolutionContext...) + + depPom, err := r.findPom(ctx, depGroupID, depArtifactID, depVersion) + if err != nil || depPom == nil { + log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "dependencyID", mavenID{depGroupID, depArtifactID, depVersion}). + Debug("unable to find imported pom looking for managed dependencies") + continue + } + version, err = r.findInheritedVersion(ctx, depPom, groupID, artifactID, resolutionContext...) + if err != nil { + log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "dependencyID", mavenID{depGroupID, depArtifactID, depVersion}). + Debug("error during findInheritedVersion") + } + if version != "" { + return version, nil + } + } + } + + // recursively check parents + parent, err := r.resolveParent(ctx, pom) + if err != nil { + return "", err + } + if parent != nil { + version, err = r.findInheritedVersion(ctx, parent, groupID, artifactID, resolutionContext...) + if err != nil { + return "", err + } + if version != "" { + return version, nil + } + } + + // check for inherited dependencies + for _, dep := range pomDependencies(pom) { + depGroupID := r.getPropertyValue(ctx, dep.GroupID, resolutionContext...) + depArtifactID := r.getPropertyValue(ctx, dep.ArtifactID, resolutionContext...) + if depGroupID == groupID && depArtifactID == artifactID { + version = r.getPropertyValue(ctx, dep.Version, resolutionContext...) + if version != "" { + return version, nil + } + } + } + + return "", nil +} + +// findLicenses search pom for license, traversing parent poms if needed +func (r *mavenResolver) findLicenses(ctx context.Context, groupID, artifactID, version string) ([]gopom.License, error) { + pom, err := r.findPom(ctx, groupID, artifactID, version) + if pom == nil || err != nil { + return nil, err + } + return r.resolveLicenses(ctx, pom) +} + +// resolveLicenses searches the pom for license, traversing parent poms if needed +func (r *mavenResolver) resolveLicenses(ctx context.Context, pom *gopom.Project, processing ...mavenID) ([]gopom.License, error) { + id := r.resolveMavenID(ctx, pom) + if slices.Contains(processing, id) { + return nil, fmt.Errorf("cycle detected resolving licenses for: %v", id) + } + if r.cfg.MaxParentRecursiveDepth > 0 && len(processing) > r.cfg.MaxParentRecursiveDepth { + return nil, fmt.Errorf("maximum parent recursive depth (%v) reached: %v", r.cfg.MaxParentRecursiveDepth, processing) + } + + directLicenses := r.pomLicenses(ctx, pom) + if len(directLicenses) > 0 { + return directLicenses, nil + } + + parent, err := r.resolveParent(ctx, pom) + if err != nil { + return nil, err + } + if parent == nil { + return nil, nil + } + return r.resolveLicenses(ctx, parent, append(processing, id)...) +} + +// pomLicenses appends the directly specified licenses with non-empty name or url +func (r *mavenResolver) pomLicenses(ctx context.Context, pom *gopom.Project) []gopom.License { + var out []gopom.License + for _, license := range deref(pom.Licenses) { + // if we find non-empty licenses, return them + name := r.getPropertyValue(ctx, license.Name, pom) + url := r.getPropertyValue(ctx, license.URL, pom) + if name != "" || url != "" { + out = append(out, license) + } + } + return out +} + +func (r *mavenResolver) findParentPomByRelativePath(ctx context.Context, pom *gopom.Project, parentID mavenID) *gopom.Project { + // don't resolve if no resolver + if r.fileResolver == nil { + return nil + } + + pomLocation, hasPomLocation := r.pomLocations[pom] + if !hasPomLocation || pom == nil || pom.Parent == nil { + return nil + } + relativePath := r.getPropertyValue(ctx, pom.Parent.RelativePath, pom) + if relativePath == "" { + return nil + } + p := pomLocation.Path() + p = path.Dir(p) + p = path.Join(p, relativePath) + p = path.Clean(p) + parentLocations, err := r.fileResolver.FilesByPath(p) + if err != nil || len(parentLocations) == 0 { + log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "relativePath", relativePath). + Trace("parent pom not found by relative path") + return nil + } + parentLocation := parentLocations[0] + + parentContents, err := r.fileResolver.FileContentsByLocation(parentLocation) + if err != nil || parentContents == nil { + log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "parentLocation", parentLocation). + Debug("unable to get contents of parent pom by relative path") + return nil + } + defer internal.CloseAndLogError(parentContents, parentLocation.RealPath) + parentPom, err := decodePomXML(parentContents) + if err != nil || parentPom == nil { + log.WithFields("error", err, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "parentLocation", parentLocation). + Debug("unable to parse parent pom") + return nil + } + // ensure parent matches + newParentID := r.resolveMavenID(ctx, parentPom) + if newParentID.ArtifactID != parentID.ArtifactID { + log.WithFields("newParentID", newParentID, "mavenID", r.resolveMavenID(ctx, pom), "parentID", parentID, "parentLocation", parentLocation). + Debug("parent IDs do not match resolving parent by relative path") + return nil + } + + r.resolved[parentID] = parentPom + r.pomLocations[parentPom] = parentLocation // for any future parent relativepath lookups + + return parentPom +} + +// pomDependencies returns all dependencies directly defined in a project, including all defined in profiles. +// does not resolve parent dependencies +func pomDependencies(pom *gopom.Project) []gopom.Dependency { + dependencies := deref(pom.Dependencies) + for _, profile := range deref(pom.Profiles) { + dependencies = append(dependencies, deref(profile.Dependencies)...) + } + return dependencies +} + +// pomManagedDependencies returns all directly defined managed dependencies in a project pom, including all defined in profiles. +// does not resolve parent managed dependencies +func pomManagedDependencies(pom *gopom.Project) []gopom.Dependency { + var dependencies []gopom.Dependency + if pom.DependencyManagement != nil { + dependencies = append(dependencies, deref(pom.DependencyManagement.Dependencies)...) + } + for _, profile := range deref(pom.Profiles) { + if profile.DependencyManagement != nil { + dependencies = append(dependencies, deref(profile.DependencyManagement.Dependencies)...) + } + } + return dependencies +} diff --git a/syft/pkg/cataloger/java/maven_resolver_test.go b/syft/pkg/cataloger/java/maven_resolver_test.go new file mode 100644 index 00000000000..d778efb6991 --- /dev/null +++ b/syft/pkg/cataloger/java/maven_resolver_test.go @@ -0,0 +1,359 @@ +package java + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/bmatcuk/doublestar/v4" + "github.com/stretchr/testify/require" + "github.com/vifraa/gopom" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/internal/fileresolver" +) + +func Test_resolveProperty(t *testing.T) { + tests := []struct { + name string + property string + pom gopom.Project + expected string + }{ + { + name: "property", + property: "${version.number}", + pom: gopom.Project{ + Properties: &gopom.Properties{ + Entries: map[string]string{ + "version.number": "12.5.0", + }, + }, + }, + expected: "12.5.0", + }, + { + name: "groupId", + property: "${project.groupId}", + pom: gopom.Project{ + GroupID: ptr("org.some.group"), + }, + expected: "org.some.group", + }, + { + name: "parent groupId", + property: "${project.parent.groupId}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + GroupID: ptr("org.some.parent"), + }, + }, + expected: "org.some.parent", + }, + { + name: "nil pointer halts search", + property: "${project.parent.groupId}", + pom: gopom.Project{ + Parent: nil, + }, + expected: "", + }, + { + name: "nil string pointer halts search", + property: "${project.parent.groupId}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + GroupID: nil, + }, + }, + expected: "", + }, + { + name: "double dereference", + property: "${springboot.version}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + Version: ptr("1.2.3"), + }, + Properties: &gopom.Properties{ + Entries: map[string]string{ + "springboot.version": "${project.parent.version}", + }, + }, + }, + expected: "1.2.3", + }, + { + name: "map missing stops double dereference", + property: "${springboot.version}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + Version: ptr("1.2.3"), + }, + }, + expected: "", + }, + { + name: "resolution halts even if it resolves to a variable", + property: "${springboot.version}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + Version: ptr("${undefined.version}"), + }, + Properties: &gopom.Properties{ + Entries: map[string]string{ + "springboot.version": "${project.parent.version}", + }, + }, + }, + expected: "", + }, + { + name: "resolution halts even if cyclic", + property: "${springboot.version}", + pom: gopom.Project{ + Properties: &gopom.Properties{ + Entries: map[string]string{ + "springboot.version": "${springboot.version}", + }, + }, + }, + expected: "", + }, + { + name: "resolution halts even if cyclic more steps", + property: "${cyclic.version}", + pom: gopom.Project{ + Properties: &gopom.Properties{ + Entries: map[string]string{ + "other.version": "${cyclic.version}", + "springboot.version": "${other.version}", + "cyclic.version": "${springboot.version}", + }, + }, + }, + expected: "", + }, + { + name: "resolution halts even if cyclic involving parent", + property: "${cyclic.version}", + pom: gopom.Project{ + Parent: &gopom.Parent{ + Version: ptr("${cyclic.version}"), + }, + Properties: &gopom.Properties{ + Entries: map[string]string{ + "other.version": "${parent.version}", + "springboot.version": "${other.version}", + "cyclic.version": "${springboot.version}", + }, + }, + }, + expected: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + r := newMavenResolver(nil, DefaultArchiveCatalogerConfig()) + resolved := r.getPropertyValue(context.Background(), ptr(test.property), &test.pom) + require.Equal(t, test.expected, resolved) + }) + } +} + +func Test_mavenResolverLocal(t *testing.T) { + dir, err := filepath.Abs("test-fixtures/pom/maven-repo") + require.NoError(t, err) + + tests := []struct { + name string + groupID string + artifactID string + version string + maxDepth int + expression string + expected string + wantErr require.ErrorAssertionFunc + }{ + { + name: "artifact id with variable from 2nd parent", + groupID: "my.org", + artifactID: "child-one", + version: "1.3.6", + expression: "${project.one}", + expected: "1", + }, + { + name: "depth limited large enough", + groupID: "my.org", + artifactID: "child-one", + version: "1.3.6", + expression: "${project.one}", + expected: "1", + maxDepth: 2, + }, + { + name: "depth limited should not resolve", + groupID: "my.org", + artifactID: "child-one", + version: "1.3.6", + expression: "${project.one}", + expected: "", + maxDepth: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := context.Background() + r := newMavenResolver(nil, ArchiveCatalogerConfig{ + UseNetwork: false, + UseMavenLocalRepository: true, + MavenLocalRepositoryDir: dir, + MaxParentRecursiveDepth: test.maxDepth, + }) + pom, err := r.findPom(ctx, test.groupID, test.artifactID, test.version) + if test.wantErr != nil { + test.wantErr(t, err) + } else { + require.NoError(t, err) + } + got := r.getPropertyValue(context.Background(), &test.expression, pom) + require.Equal(t, test.expected, got) + }) + } +} + +func Test_mavenResolverRemote(t *testing.T) { + url := mockMavenRepo(t) + + tests := []struct { + groupID string + artifactID string + version string + expression string + expected string + wantErr require.ErrorAssertionFunc + }{ + { + groupID: "my.org", + artifactID: "child-one", + version: "1.3.6", + expression: "${project.one}", + expected: "1", + }, + } + + for _, test := range tests { + t.Run(test.artifactID, func(t *testing.T) { + ctx := context.Background() + r := newMavenResolver(nil, ArchiveCatalogerConfig{ + UseNetwork: true, + UseMavenLocalRepository: false, + MavenBaseURL: url, + }) + pom, err := r.findPom(ctx, test.groupID, test.artifactID, test.version) + if test.wantErr != nil { + test.wantErr(t, err) + } else { + require.NoError(t, err) + } + got := r.getPropertyValue(context.Background(), &test.expression, pom) + require.Equal(t, test.expected, got) + }) + } +} + +func Test_relativePathParent(t *testing.T) { + resolver, err := fileresolver.NewFromDirectory("test-fixtures/pom/local", "") + require.NoError(t, err) + + r := newMavenResolver(resolver, DefaultArchiveCatalogerConfig()) + locs, err := resolver.FilesByPath("child-1/pom.xml") + require.NoError(t, err) + require.Len(t, locs, 1) + + loc := locs[0] + contents, err := resolver.FileContentsByLocation(loc) + require.NoError(t, err) + defer internal.CloseAndLogError(contents, loc.RealPath) + + pom, err := decodePomXML(contents) + require.NoError(t, err) + + r.pomLocations[pom] = loc + + ctx := context.Background() + parent, err := r.resolveParent(ctx, pom) + require.NoError(t, err) + require.Contains(t, r.pomLocations, parent) + + parent, err = r.resolveParent(ctx, parent) + require.NoError(t, err) + require.Contains(t, r.pomLocations, parent) + + got := r.getPropertyValue(ctx, ptr("${commons-exec_subversion}"), pom) + require.Equal(t, "3", got) +} + +// mockMavenRepo starts a remote maven repo serving all the pom files found in test-fixtures/pom/maven-repo +func mockMavenRepo(t *testing.T) (url string) { + t.Helper() + + return mockMavenRepoAt(t, "test-fixtures/pom/maven-repo") +} + +// mockMavenRepoAt starts a remote maven repo serving all the pom files found in the given directory +func mockMavenRepoAt(t *testing.T, dir string) (url string) { + t.Helper() + + // mux is the HTTP request multiplexer used with the test server. + mux := http.NewServeMux() + + // We want to ensure that tests catch mistakes where the endpoint URL is + // specified as absolute rather than relative. It only makes a difference + // when there's a non-empty base URL path. So, use that. See issue #752. + apiHandler := http.NewServeMux() + apiHandler.Handle("/", mux) + // server is a test HTTP server used to provide mock API responses. + server := httptest.NewServer(apiHandler) + + t.Cleanup(server.Close) + + matches, err := doublestar.Glob(os.DirFS(dir), filepath.Join("**", "*.pom")) + require.NoError(t, err) + + for _, match := range matches { + fullPath, err := filepath.Abs(filepath.Join(dir, match)) + require.NoError(t, err) + match = "/" + filepath.ToSlash(match) + mux.HandleFunc(match, mockMavenHandler(fullPath)) + } + + return server.URL +} + +func mockMavenHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + // Set the Content-Type header to indicate that the response is XML + w.Header().Set("Content-Type", "application/xml") + // Copy the file's content to the response writer + f, err := os.Open(responseFixture) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer internal.CloseAndLogError(f, responseFixture) + _, err = io.Copy(w, f) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} diff --git a/syft/pkg/cataloger/java/maven_utils.go b/syft/pkg/cataloger/java/maven_utils.go new file mode 100644 index 00000000000..9d365e151d6 --- /dev/null +++ b/syft/pkg/cataloger/java/maven_utils.go @@ -0,0 +1,74 @@ +package java + +import ( + "encoding/xml" + "fmt" + "io" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/mitchellh/go-homedir" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" +) + +// defaultMavenLocalRepoDir gets default location of the Maven local repository, generally at /.m2/repository +func defaultMavenLocalRepoDir() string { + homeDir, err := homedir.Dir() + if err != nil { + return "" + } + + mavenHome := filepath.Join(homeDir, ".m2") + + settingsXML := filepath.Join(mavenHome, "settings.xml") + settings, err := os.Open(settingsXML) + if err == nil && settings != nil { + defer internal.CloseAndLogError(settings, settingsXML) + localRepository := getSettingsXMLLocalRepository(settings) + if localRepository != "" { + return localRepository + } + } + return filepath.Join(mavenHome, "repository") +} + +// getSettingsXMLLocalRepository reads the provided settings.xml and parses the localRepository, if present +func getSettingsXMLLocalRepository(settingsXML io.Reader) string { + type settings struct { + LocalRepository string `xml:"localRepository"` + } + s := settings{} + err := xml.NewDecoder(settingsXML).Decode(&s) + if err != nil { + log.WithFields("error", err).Debug("unable to read maven settings.xml") + } + return s.LocalRepository +} + +// deref dereferences ptr if not nil, or returns the type default value if ptr is nil +func deref[T any](ptr *T) T { + if ptr == nil { + var t T + return t + } + return *ptr +} + +// remotePomURL returns a URL to download a POM from a remote repository +func remotePomURL(repoURL, groupID, artifactID, version string) (requestURL string, err error) { + // groupID needs to go from maven.org -> maven/org + urlPath := strings.Split(groupID, ".") + artifactPom := fmt.Sprintf("%s-%s.pom", artifactID, version) + urlPath = append(urlPath, artifactID, version, artifactPom) + + // ex: https://repo1.maven.org/maven2/groupID/artifactID/artifactPom + requestURL, err = url.JoinPath(repoURL, urlPath...) + if err != nil { + return requestURL, fmt.Errorf("could not construct maven url: %w", err) + } + return requestURL, err +} diff --git a/syft/pkg/cataloger/java/maven_utils_test.go b/syft/pkg/cataloger/java/maven_utils_test.go new file mode 100644 index 00000000000..8d599b501af --- /dev/null +++ b/syft/pkg/cataloger/java/maven_utils_test.go @@ -0,0 +1,103 @@ +package java + +import ( + "os" + "path/filepath" + "testing" + + "github.com/mitchellh/go-homedir" + "github.com/stretchr/testify/require" +) + +func Test_defaultMavenLocalRepoDir(t *testing.T) { + home, err := homedir.Dir() + require.NoError(t, err) + + fixtures, err := filepath.Abs("test-fixtures") + require.NoError(t, err) + + tests := []struct { + name string + home string + expected string + }{ + { + name: "default", + expected: filepath.Join(home, ".m2", "repository"), + home: "", + }, + { + name: "alternate dir", + expected: "/some/other/repo", + home: "test-fixtures/local-repository-settings", + }, + { + name: "explicit home", + expected: filepath.Join(fixtures, ".m2", "repository"), + home: "test-fixtures", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + homedir.Reset() + defer homedir.Reset() + if test.home != "" { + home, err := filepath.Abs(test.home) + require.NoError(t, err) + t.Setenv("HOME", home) + } + got := defaultMavenLocalRepoDir() + require.Equal(t, test.expected, got) + }) + } +} + +func Test_getSettingsXmlLocalRepository(t *testing.T) { + tests := []struct { + file string + expected string + }{ + { + expected: "/some/other/repo", + file: "test-fixtures/local-repository-settings/.m2/settings.xml", + }, + { + expected: "", + file: "invalid", + }, + } + for _, test := range tests { + t.Run(test.expected, func(t *testing.T) { + f, _ := os.Open(test.file) + defer f.Close() + got := getSettingsXMLLocalRepository(f) + require.Equal(t, test.expected, got) + }) + } +} + +func Test_remotePomURL(t *testing.T) { + tests := []struct { + name string + groupID string + artifactID string + version string + expected string + }{ + { + name: "formatMavenURL correctly assembles the pom URL", + groupID: "org.springframework.boot", + artifactID: "spring-boot-starter-test", + version: "3.1.5", + expected: "https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-starter-test/3.1.5/spring-boot-starter-test-3.1.5.pom", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + requestURL, err := remotePomURL(mavenBaseURL, tc.groupID, tc.artifactID, tc.version) + require.NoError(t, err, "expected no err; got %w", err) + require.Equal(t, tc.expected, requestURL) + }) + } +} diff --git a/syft/pkg/cataloger/java/parse_pom_xml.go b/syft/pkg/cataloger/java/parse_pom_xml.go index 5fe1f34d191..d9fe4b2c25a 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml.go +++ b/syft/pkg/cataloger/java/parse_pom_xml.go @@ -4,164 +4,175 @@ import ( "bytes" "context" "encoding/xml" + "errors" "fmt" "io" - "reflect" - "regexp" "strings" "github.com/saintfish/chardet" "github.com/vifraa/gopom" "golang.org/x/net/html/charset" + "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/pkg/cataloger/generic" ) -const pomXMLGlob = "*pom.xml" +const ( + pomXMLGlob = "*pom.xml" + pomCatalogerName = "java-pom-cataloger" +) + +type pomXMLCataloger struct { + cfg ArchiveCatalogerConfig +} -var propertyMatcher = regexp.MustCompile("[$][{][^}]+[}]") +func (p pomXMLCataloger) Name() string { + return pomCatalogerName +} -func (gap genericArchiveParserAdapter) parserPomXML(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { - pom, err := decodePomXML(reader) +func (p pomXMLCataloger) Catalog(ctx context.Context, fileResolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { + locations, err := fileResolver.FilesByGlob("**/pom.xml") if err != nil { return nil, nil, err } - var pkgs []pkg.Package - if pom.Dependencies != nil { - for _, dep := range *pom.Dependencies { - p := newPackageFromPom( - ctx, - pom, - dep, - gap.cfg, - reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), - ) - if p.Name == "" { - continue - } + r := newMavenResolver(fileResolver, p.cfg) - pkgs = append(pkgs, p) + var poms []*gopom.Project + for _, pomLocation := range locations { + pom, err := readPomFromLocation(fileResolver, pomLocation) + if err != nil || pom == nil { + log.WithFields("error", err, "pomLocation", pomLocation).Debug("error while reading pom") + continue } + + poms = append(poms, pom) + + // store information about this pom for future lookups + r.pomLocations[pom] = pomLocation + r.resolved[r.resolveMavenID(ctx, pom)] = pom } + var pkgs []pkg.Package + for _, pom := range poms { + pkgs = append(pkgs, processPomXML(ctx, r, pom, r.pomLocations[pom])...) + } return pkgs, nil, nil } -func parsePomXMLProject(path string, reader io.Reader, location file.Location) (*parsedPomProject, error) { - project, err := decodePomXML(reader) +func readPomFromLocation(fileResolver file.Resolver, pomLocation file.Location) (*gopom.Project, error) { + contents, err := fileResolver.FileContentsByLocation(pomLocation) if err != nil { return nil, err } - return newPomProject(path, project, location), nil + defer internal.CloseAndLogError(contents, pomLocation.RealPath) + return decodePomXML(contents) } -func newPomProject(path string, p gopom.Project, location file.Location) *parsedPomProject { - artifactID := safeString(p.ArtifactID) - name := safeString(p.Name) - projectURL := safeString(p.URL) - - var licenses []pkg.License - if p.Licenses != nil { - for _, license := range *p.Licenses { - var licenseName, licenseURL string - if license.Name != nil { - licenseName = *license.Name - } - if license.URL != nil { - licenseURL = *license.URL - } - - if licenseName == "" && licenseURL == "" { - continue - } +func processPomXML(ctx context.Context, r *mavenResolver, pom *gopom.Project, loc file.Location) []pkg.Package { + var pkgs []pkg.Package - licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, licenseURL, &location)) + pomID := r.resolveMavenID(ctx, pom) + for _, dep := range pomDependencies(pom) { + depID := r.resolveDependencyID(ctx, pom, dep) + log.WithFields("pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Trace("adding maven pom dependency") + + p, err := newPackageFromDependency( + ctx, + r, + pom, + dep, + loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + if err != nil { + log.WithFields("error", err, "pomLocation", loc, "mavenID", pomID, "dependencyID", depID).Debugf("error adding dependency") + } + if p == nil { + continue } + pkgs = append(pkgs, *p) } - log.WithFields("path", path, "artifactID", artifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml") - return &parsedPomProject{ - JavaPomProject: &pkg.JavaPomProject{ - Path: path, - Parent: pomParent(p, p.Parent), - GroupID: resolveProperty(p, p.GroupID, "groupId"), - ArtifactID: artifactID, - Version: resolveProperty(p, p.Version, "version"), - Name: name, - Description: cleanDescription(p.Description), - URL: projectURL, - }, - Licenses: licenses, + return pkgs +} + +func newPomProject(ctx context.Context, r *mavenResolver, path string, pom *gopom.Project) *pkg.JavaPomProject { + id := r.resolveMavenID(ctx, pom) + name := r.getPropertyValue(ctx, pom.Name, pom) + projectURL := r.getPropertyValue(ctx, pom.URL, pom) + + log.WithFields("path", path, "artifactID", id.ArtifactID, "name", name, "projectURL", projectURL).Trace("parsing pom.xml") + return &pkg.JavaPomProject{ + Path: path, + Parent: pomParent(ctx, r, pom), + GroupID: id.GroupID, + ArtifactID: id.ArtifactID, + Version: id.Version, + Name: name, + Description: cleanDescription(r.getPropertyValue(ctx, pom.Description, pom)), + URL: projectURL, } } -func newPackageFromPom(ctx context.Context, pom gopom.Project, dep gopom.Dependency, cfg ArchiveCatalogerConfig, locations ...file.Location) pkg.Package { +func newPackageFromDependency(ctx context.Context, r *mavenResolver, pom *gopom.Project, dep gopom.Dependency, locations ...file.Location) (*pkg.Package, error) { + id := r.resolveDependencyID(ctx, pom, dep) + m := pkg.JavaArchive{ PomProperties: &pkg.JavaPomProperties{ - GroupID: resolveProperty(pom, dep.GroupID, "groupId"), - ArtifactID: resolveProperty(pom, dep.ArtifactID, "artifactId"), - Scope: resolveProperty(pom, dep.Scope, "scope"), + GroupID: id.GroupID, + ArtifactID: id.ArtifactID, + Scope: r.getPropertyValue(ctx, dep.Scope, pom), }, } - name := safeString(dep.ArtifactID) - version := resolveProperty(pom, dep.Version, "version") + var err error + var licenses []pkg.License + dependencyPom, depErr := r.findPom(ctx, id.GroupID, id.ArtifactID, id.Version) + if depErr != nil { + err = errors.Join(err, depErr) + } - licenses := make([]pkg.License, 0) - if cfg.UseNetwork { - if version == "" { - // If we have no version then let's try to get it from a parent pom DependencyManagement section - version = recursivelyFindVersionFromParentPom(ctx, *dep.GroupID, *dep.ArtifactID, *pom.Parent.GroupID, *pom.Parent.ArtifactID, *pom.Parent.Version, cfg) - } - if version != "" { - parentLicenses := recursivelyFindLicensesFromParentPom( - ctx, - m.PomProperties.GroupID, - m.PomProperties.ArtifactID, - version, - cfg) - - if len(parentLicenses) > 0 { - for _, licenseName := range parentLicenses { - licenses = append(licenses, pkg.NewLicenseFromFields(licenseName, "", nil)) - } - } + if dependencyPom != nil { + depLicenses, _ := r.resolveLicenses(ctx, dependencyPom) + for _, license := range depLicenses { + licenses = append(licenses, pkg.NewLicenseFromFields(deref(license.Name), deref(license.URL), nil)) } } - p := pkg.Package{ - Name: name, - Version: version, + p := &pkg.Package{ + Name: id.ArtifactID, + Version: id.Version, Locations: file.NewLocationSet(locations...), Licenses: pkg.NewLicenseSet(licenses...), - PURL: packageURL(name, version, m), + PURL: packageURL(id.ArtifactID, id.Version, m), Language: pkg.Java, Type: pkg.JavaPkg, // TODO: should we differentiate between packages from jar/war/zip versus packages from a pom.xml that were not installed yet? + FoundBy: pomCatalogerName, Metadata: m, } p.SetID() - return p + return p, err } -func decodePomXML(content io.Reader) (project gopom.Project, err error) { +// decodePomXML decodes a pom XML file, detecting and converting non-UTF-8 charsets. this DOES NOT perform any logic to resolve properties such as groupID, artifactID, and version +func decodePomXML(content io.Reader) (project *gopom.Project, err error) { inputReader, err := getUtf8Reader(content) if err != nil { - return project, fmt.Errorf("unable to read pom.xml: %w", err) + return nil, fmt.Errorf("unable to read pom.xml: %w", err) } decoder := xml.NewDecoder(inputReader) // when an xml file has a character set declaration (e.g. '') read that and use the correct decoder decoder.CharsetReader = charset.NewReaderLabel - if err := decoder.Decode(&project); err != nil { - return project, fmt.Errorf("unable to unmarshal pom.xml: %w", err) + project = &gopom.Project{} + if err := decoder.Decode(project); err != nil { + return nil, fmt.Errorf("unable to unmarshal pom.xml: %w", err) } return project, nil @@ -194,29 +205,28 @@ func getUtf8Reader(content io.Reader) (io.Reader, error) { return inputReader, nil } -func pomParent(pom gopom.Project, parent *gopom.Parent) (result *pkg.JavaPomParent) { - if parent == nil { +func pomParent(ctx context.Context, r *mavenResolver, pom *gopom.Project) *pkg.JavaPomParent { + if pom == nil || pom.Parent == nil { return nil } - artifactID := safeString(parent.ArtifactID) - result = &pkg.JavaPomParent{ - GroupID: resolveProperty(pom, parent.GroupID, "groupId"), - ArtifactID: artifactID, - Version: resolveProperty(pom, parent.Version, "version"), - } + groupID := r.getPropertyValue(ctx, pom.Parent.GroupID, pom) + artifactID := r.getPropertyValue(ctx, pom.Parent.ArtifactID, pom) + version := r.getPropertyValue(ctx, pom.Parent.Version, pom) - if result.GroupID == "" && result.ArtifactID == "" && result.Version == "" { + if groupID == "" && artifactID == "" && version == "" { return nil } - return result -} -func cleanDescription(original *string) (cleaned string) { - if original == nil { - return "" + return &pkg.JavaPomParent{ + GroupID: groupID, + ArtifactID: artifactID, + Version: version, } - descriptionLines := strings.Split(*original, "\n") +} + +func cleanDescription(original string) (cleaned string) { + descriptionLines := strings.Split(original, "\n") for _, line := range descriptionLines { line = strings.TrimSpace(line) if len(line) == 0 { @@ -226,94 +236,3 @@ func cleanDescription(original *string) (cleaned string) { } return strings.TrimSpace(cleaned) } - -// resolveProperty emulates some maven property resolution logic by looking in the project's variables -// as well as supporting the project expressions like ${project.parent.groupId}. -// If no match is found, the entire expression including ${} is returned -func resolveProperty(pom gopom.Project, property *string, propertyName string) string { - propertyCase := safeString(property) - log.WithFields("existingPropertyValue", propertyCase, "propertyName", propertyName).Trace("resolving property") - seenBeforePropertyNames := map[string]struct{}{ - propertyName: {}, - } - result := recursiveResolveProperty(pom, propertyCase, seenBeforePropertyNames) - if propertyMatcher.MatchString(result) { - return "" // dereferencing variable failed; fall back to empty string - } - return result -} - -//nolint:gocognit -func recursiveResolveProperty(pom gopom.Project, propertyCase string, seenPropertyNames map[string]struct{}) string { - return propertyMatcher.ReplaceAllStringFunc(propertyCase, func(match string) string { - propertyName := strings.TrimSpace(match[2 : len(match)-1]) // remove leading ${ and trailing } - if _, seen := seenPropertyNames[propertyName]; seen { - return propertyCase - } - entries := pomProperties(pom) - if value, ok := entries[propertyName]; ok { - seenPropertyNames[propertyName] = struct{}{} - return recursiveResolveProperty(pom, value, seenPropertyNames) // recursively resolve in case a variable points to a variable. - } - - // if we don't find anything directly in the pom properties, - // see if we have a project.x expression and process this based - // on the xml tags in gopom - parts := strings.Split(propertyName, ".") - numParts := len(parts) - if numParts > 1 && strings.TrimSpace(parts[0]) == "project" { - pomValue := reflect.ValueOf(pom) - pomValueType := pomValue.Type() - for partNum := 1; partNum < numParts; partNum++ { - if pomValueType.Kind() != reflect.Struct { - break - } - part := parts[partNum] - for fieldNum := 0; fieldNum < pomValueType.NumField(); fieldNum++ { - f := pomValueType.Field(fieldNum) - tag := f.Tag.Get("xml") - tag = strings.Split(tag, ",")[0] - // a segment of the property name matches the xml tag for the field, - // so we need to recurse down the nested structs or return a match - // if we're done. - if part == tag { - pomValue = pomValue.Field(fieldNum) - pomValueType = pomValue.Type() - if pomValueType.Kind() == reflect.Ptr { - // we were recursing down the nested structs, but one of the steps - // we need to take is a nil pointer, so give up and return the original match - if pomValue.IsNil() { - return match - } - pomValue = pomValue.Elem() - if !pomValue.IsZero() { - // we found a non-zero value whose tag matches this part of the property name - pomValueType = pomValue.Type() - } - } - // If this was the last part of the property name, return the value - if partNum == numParts-1 { - return fmt.Sprintf("%v", pomValue.Interface()) - } - break - } - } - } - } - return match - }) -} - -func pomProperties(p gopom.Project) map[string]string { - if p.Properties != nil { - return p.Properties.Entries - } - return map[string]string{} -} - -func safeString(s *string) string { - if s == nil { - return "" - } - return *s -} diff --git a/syft/pkg/cataloger/java/parse_pom_xml_test.go b/syft/pkg/cataloger/java/parse_pom_xml_test.go index 66ebb521609..45650049072 100644 --- a/syft/pkg/cataloger/java/parse_pom_xml_test.go +++ b/syft/pkg/cataloger/java/parse_pom_xml_test.go @@ -1,6 +1,7 @@ package java import ( + "context" "encoding/base64" "io" "os" @@ -16,15 +17,17 @@ import ( "github.com/anchore/syft/syft/license" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" + "github.com/anchore/syft/syft/source" + "github.com/anchore/syft/syft/source/directorysource" ) -func Test_parserPomXML(t *testing.T) { +func Test_parsePomXML(t *testing.T) { tests := []struct { - input string + dir string expected []pkg.Package }{ { - input: "test-fixtures/pom/pom.xml", + dir: "test-fixtures/pom/local/example-java-app-maven", expected: []pkg.Package{ { Name: "joda-time", @@ -32,6 +35,7 @@ func Test_parserPomXML(t *testing.T) { PURL: "pkg:maven/com.joda/joda-time@2.9.2", Language: pkg.Java, Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, Metadata: pkg.JavaArchive{ PomProperties: &pkg.JavaPomProperties{ GroupID: "com.joda", @@ -45,6 +49,7 @@ func Test_parserPomXML(t *testing.T) { PURL: "pkg:maven/junit/junit@4.12", Language: pkg.Java, Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, Metadata: pkg.JavaArchive{ PomProperties: &pkg.JavaPomProperties{ GroupID: "junit", @@ -58,19 +63,19 @@ func Test_parserPomXML(t *testing.T) { } for _, test := range tests { - t.Run(test.input, func(t *testing.T) { + t.Run(test.dir, func(t *testing.T) { for i := range test.expected { - test.expected[i].Locations.Add(file.NewLocation(test.input)) + test.expected[i].Locations.Add(file.NewLocation("pom.xml")) } - gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{ + cat := NewPomCataloger(ArchiveCatalogerConfig{ ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ IncludeIndexedArchives: true, IncludeUnindexedArchives: true, }, }) - pkgtest.TestFileParser(t, test.input, gap.parserPomXML, test.expected, nil) + pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil) }) } } @@ -131,168 +136,129 @@ func Test_decodePomXML_surviveNonUtf8Encoding(t *testing.T) { func Test_parseCommonsTextPomXMLProject(t *testing.T) { tests := []struct { - input string + dir string expected []pkg.Package }{ { - input: "test-fixtures/pom/commons-text.pom.xml", - expected: []pkg.Package{ - { - Name: "commons-lang3", - Version: "3.12.0", - PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.apache.commons", - ArtifactID: "commons-lang3", - }, - }, - }, - { - Name: "junit-jupiter", - Version: "", - PURL: "pkg:maven/org.junit.jupiter/junit-jupiter", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.junit.jupiter", - ArtifactID: "junit-jupiter", - Scope: "test", - }, - }, - }, - { - Name: "assertj-core", - Version: "3.23.1", - PURL: "pkg:maven/org.assertj/assertj-core@3.23.1", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.assertj", - ArtifactID: "assertj-core", - Scope: "test", - }, - }, - }, - { - Name: "commons-io", - Version: "2.11.0", - PURL: "pkg:maven/commons-io/commons-io@2.11.0", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "commons-io", - ArtifactID: "commons-io", - Scope: "test", - }, - }, - }, - { - Name: "mockito-inline", - Version: "4.8.0", - PURL: "pkg:maven/org.mockito/mockito-inline@4.8.0", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.mockito", - ArtifactID: "mockito-inline", - Scope: "test", - }, - }, - }, - { - Name: "js", - Version: "22.0.0.2", - PURL: "pkg:maven/org.graalvm.js/js@22.0.0.2", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.graalvm.js", - ArtifactID: "js", - Scope: "test", - }, - }, - }, - { - Name: "js-scriptengine", - Version: "22.0.0.2", - PURL: "pkg:maven/org.graalvm.js/js-scriptengine@22.0.0.2", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.graalvm.js", - ArtifactID: "js-scriptengine", - Scope: "test", - }, - }, + dir: "test-fixtures/pom/local/commons-text-1.10.0", + + expected: getCommonsTextExpectedPackages(), + }, + } + + for _, test := range tests { + t.Run(test.dir, func(t *testing.T) { + for i := range test.expected { + test.expected[i].Locations.Add(file.NewLocation("pom.xml")) + } + + cat := NewPomCataloger(ArchiveCatalogerConfig{ + ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ + IncludeIndexedArchives: true, + IncludeUnindexedArchives: true, }, - { - Name: "commons-rng-simple", - Version: "1.4", - PURL: "pkg:maven/org.apache.commons/commons-rng-simple@1.4", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.apache.commons", - ArtifactID: "commons-rng-simple", - Scope: "test", - }, - }, + UseMavenLocalRepository: false, + }) + pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil) + }) + } +} + +func Test_parseCommonsTextPomXMLProjectWithLocalRepository(t *testing.T) { + // Using the local repository, the version of junit-jupiter will be resolved + expectedPackages := getCommonsTextExpectedPackages() + + for i := 0; i < len(expectedPackages); i++ { + if expectedPackages[i].Name == "junit-jupiter" { + expPkg := &expectedPackages[i] + expPkg.Version = "5.9.1" + expPkg.PURL = "pkg:maven/org.junit.jupiter/junit-jupiter@5.9.1" + expPkg.Metadata = pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.junit.jupiter", + ArtifactID: "junit-jupiter", + Scope: "test", }, - { - Name: "jmh-core", - Version: "1.35", - PURL: "pkg:maven/org.openjdk.jmh/jmh-core@1.35", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.openjdk.jmh", - ArtifactID: "jmh-core", - Scope: "test", - }, - }, + } + } + } + + tests := []struct { + dir string + expected []pkg.Package + }{ + { + dir: "test-fixtures/pom/local/commons-text-1.10.0", + expected: expectedPackages, + }, + } + + for _, test := range tests { + t.Run(test.dir, func(t *testing.T) { + for i := range test.expected { + test.expected[i].Locations.Add(file.NewLocation("pom.xml")) + } + + cat := NewPomCataloger(ArchiveCatalogerConfig{ + ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ + IncludeIndexedArchives: true, + IncludeUnindexedArchives: true, }, - { - Name: "jmh-generator-annprocess", - Version: "1.35", - PURL: "pkg:maven/org.openjdk.jmh/jmh-generator-annprocess@1.35", - Language: pkg.Java, - Type: pkg.JavaPkg, - Metadata: pkg.JavaArchive{ - PomProperties: &pkg.JavaPomProperties{ - GroupID: "org.openjdk.jmh", - ArtifactID: "jmh-generator-annprocess", - Scope: "test", - }, - }, + UseMavenLocalRepository: true, + MavenLocalRepositoryDir: "test-fixtures/pom/maven-repo", + }) + pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil) + }) + } +} + +func Test_parseCommonsTextPomXMLProjectWithNetwork(t *testing.T) { + url := mockMavenRepo(t) + + // Using the local repository, the version of junit-jupiter will be resolved + expectedPackages := getCommonsTextExpectedPackages() + + for i := 0; i < len(expectedPackages); i++ { + if expectedPackages[i].Name == "junit-jupiter" { + expPkg := &expectedPackages[i] + expPkg.Version = "5.9.1" + expPkg.PURL = "pkg:maven/org.junit.jupiter/junit-jupiter@5.9.1" + expPkg.Metadata = pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.junit.jupiter", + ArtifactID: "junit-jupiter", + Scope: "test", }, - }, + } + } + } + + tests := []struct { + dir string + expected []pkg.Package + }{ + { + dir: "test-fixtures/pom/local/commons-text-1.10.0", + expected: expectedPackages, }, } for _, test := range tests { - t.Run(test.input, func(t *testing.T) { + t.Run(test.dir, func(t *testing.T) { for i := range test.expected { - test.expected[i].Locations.Add(file.NewLocation(test.input)) + test.expected[i].Locations.Add(file.NewLocation("pom.xml")) } - gap := newGenericArchiveParserAdapter(ArchiveCatalogerConfig{ + cat := NewPomCataloger(ArchiveCatalogerConfig{ ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ IncludeIndexedArchives: true, IncludeUnindexedArchives: true, }, + UseNetwork: true, + MavenBaseURL: url, + UseMavenLocalRepository: false, }) - pkgtest.TestFileParser(t, test.input, gap.parserPomXML, test.expected, nil) + pkgtest.TestCataloger(t, test.dir, cat, test.expected, nil) }) } } @@ -302,63 +268,60 @@ func Test_parsePomXMLProject(t *testing.T) { jarLocation := file.NewLocation("path/to/archive.jar") tests := []struct { name string - expected parsedPomProject + project *pkg.JavaPomProject + licenses []pkg.License }{ { name: "go case", - expected: parsedPomProject{ - JavaPomProject: &pkg.JavaPomProject{ - Path: "test-fixtures/pom/commons-codec.pom.xml", - Parent: &pkg.JavaPomParent{ - GroupID: "org.apache.commons", - ArtifactID: "commons-parent", - Version: "42", - }, - GroupID: "commons-codec", - ArtifactID: "commons-codec", - Version: "1.11", - Name: "Apache Commons Codec", - Description: "The Apache Commons Codec package contains simple encoder and decoders for various formats such as Base64 and Hexadecimal. In addition to these widely used encoders and decoders, the codec package also maintains a collection of phonetic encoding utilities.", - URL: "http://commons.apache.org/proper/commons-codec/", + project: &pkg.JavaPomProject{ + Path: "test-fixtures/pom/commons-codec.pom.xml", + Parent: &pkg.JavaPomParent{ + GroupID: "org.apache.commons", + ArtifactID: "commons-parent", + Version: "42", }, + GroupID: "commons-codec", + ArtifactID: "commons-codec", + Version: "1.11", + Name: "Apache Commons Codec", + Description: "The Apache Commons Codec package contains simple encoder and decoders for various formats such as Base64 and Hexadecimal. In addition to these widely used encoders and decoders, the codec package also maintains a collection of phonetic encoding utilities.", + URL: "http://commons.apache.org/proper/commons-codec/", }, }, { name: "with license data", - expected: parsedPomProject{ - JavaPomProject: &pkg.JavaPomProject{ - Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml", - Parent: &pkg.JavaPomParent{ - GroupID: "org.sonatype.oss", - ArtifactID: "oss-parent", - Version: "7", - }, - GroupID: "org.neo4j.build.plugins", - ArtifactID: "license-maven-plugin", - Version: "4-SNAPSHOT", - Name: "${project.artifactId}", // TODO: this is not an ideal answer - Description: "Maven 2 plugin to check and update license headers in source files", - URL: "http://components.neo4j.org/${project.artifactId}/${project.version}", // TODO: this is not an ideal answer + project: &pkg.JavaPomProject{ + Path: "test-fixtures/pom/neo4j-license-maven-plugin.pom.xml", + Parent: &pkg.JavaPomParent{ + GroupID: "org.sonatype.oss", + ArtifactID: "oss-parent", + Version: "7", }, - Licenses: []pkg.License{ - { - Value: "The Apache Software License, Version 2.0", - SPDXExpression: "", // TODO: ideally we would parse this title to get Apache-2.0 (created issue #2210 https://github.com/anchore/syft/issues/2210) - Type: license.Declared, - URLs: []string{"http://www.apache.org/licenses/LICENSE-2.0.txt"}, - Locations: file.NewLocationSet(jarLocation), - }, - { - Value: "MIT", - SPDXExpression: "MIT", - Type: license.Declared, - Locations: file.NewLocationSet(jarLocation), - }, - { - Type: license.Declared, - URLs: []string{"https://opensource.org/license/unlicense/"}, - Locations: file.NewLocationSet(jarLocation), - }, + GroupID: "org.neo4j.build.plugins", + ArtifactID: "license-maven-plugin", + Version: "4-SNAPSHOT", + Name: "license-maven-plugin", + Description: "Maven 2 plugin to check and update license headers in source files", + URL: "http://components.neo4j.org/license-maven-plugin/4-SNAPSHOT", + }, + licenses: []pkg.License{ + { + Value: "The Apache Software License, Version 2.0", + SPDXExpression: "", // TODO: ideally we would parse this title to get Apache-2.0 (created issue #2210 https://github.com/anchore/syft/issues/2210) + Type: license.Declared, + URLs: []string{"http://www.apache.org/licenses/LICENSE-2.0.txt"}, + Locations: file.NewLocationSet(jarLocation), + }, + { + Value: "MIT", + SPDXExpression: "MIT", + Type: license.Declared, + Locations: file.NewLocationSet(jarLocation), + }, + { + Type: license.Declared, + URLs: []string{"https://opensource.org/license/unlicense/"}, + Locations: file.NewLocationSet(jarLocation), }, }, }, @@ -366,13 +329,20 @@ func Test_parsePomXMLProject(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - fixture, err := os.Open(test.expected.Path) + fixture, err := os.Open(test.project.Path) assert.NoError(t, err) + r := newMavenResolver(nil, ArchiveCatalogerConfig{}) + + pom, err := gopom.ParseFromReader(fixture) + require.NoError(t, err) - actual, err := parsePomXMLProject(fixture.Name(), fixture, jarLocation) + actual := newPomProject(context.Background(), r, fixture.Name(), pom) assert.NoError(t, err) + assert.Equal(t, test.project, actual) - assert.Equal(t, &test.expected, actual) + licenses := r.pomLicenses(context.Background(), pom) + assert.NoError(t, err) + assert.Equal(t, test.licenses, toPkgLicenses(&jarLocation, licenses)) }) } } @@ -386,7 +356,7 @@ func Test_pomParent(t *testing.T) { { name: "only group ID", input: &gopom.Parent{ - GroupID: stringPointer("org.something"), + GroupID: ptr("org.something"), }, expected: &pkg.JavaPomParent{ GroupID: "org.something", @@ -395,7 +365,7 @@ func Test_pomParent(t *testing.T) { { name: "only artifact ID", input: &gopom.Parent{ - ArtifactID: stringPointer("something"), + ArtifactID: ptr("something"), }, expected: &pkg.JavaPomParent{ ArtifactID: "something", @@ -404,7 +374,7 @@ func Test_pomParent(t *testing.T) { { name: "only Version", input: &gopom.Parent{ - Version: stringPointer("something"), + Version: ptr("something"), }, expected: &pkg.JavaPomParent{ Version: "something", @@ -423,7 +393,7 @@ func Test_pomParent(t *testing.T) { { name: "unused field", input: &gopom.Parent{ - RelativePath: stringPointer("something"), + RelativePath: ptr("something"), }, expected: nil, }, @@ -431,7 +401,8 @@ func Test_pomParent(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, pomParent(gopom.Project{}, test.input)) + r := newMavenResolver(nil, DefaultArchiveCatalogerConfig()) + assert.Equal(t, test.expected, pomParent(context.Background(), r, &gopom.Project{Parent: test.input})) }) } } @@ -454,184 +425,283 @@ func Test_cleanDescription(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.expected, cleanDescription(stringPointer(test.input))) + assert.Equal(t, test.expected, cleanDescription(test.input)) }) } } -func Test_resolveProperty(t *testing.T) { +func Test_resolveLicenses(t *testing.T) { + mavenURL := mockMavenRepo(t) + localM2 := "test-fixtures/pom/maven-repo" + localDir := "test-fixtures/pom/local" + containingDir := "test-fixtures/pom/local/contains-child-1" + + expectedLicenses := []pkg.License{ + { + Value: "Eclipse Public License v2.0", + SPDXExpression: "", + Type: license.Declared, + URLs: []string{"https://www.eclipse.org/legal/epl-v20.html"}, + }, + } + tests := []struct { name string - property string - pom gopom.Project - expected string + scanDir string + cfg ArchiveCatalogerConfig + expected []pkg.License }{ { - name: "property", - property: "${version.number}", - pom: gopom.Project{ - Properties: &gopom.Properties{ - Entries: map[string]string{ - "version.number": "12.5.0", - }, - }, + name: "local no resolution", + scanDir: containingDir, + cfg: ArchiveCatalogerConfig{ + UseMavenLocalRepository: false, + UseNetwork: false, + MavenLocalRepositoryDir: "", + MavenBaseURL: "", }, - expected: "12.5.0", + expected: nil, }, { - name: "groupId", - property: "${project.groupId}", - pom: gopom.Project{ - GroupID: stringPointer("org.some.group"), + name: "local all poms", + scanDir: localDir, + cfg: ArchiveCatalogerConfig{ + UseMavenLocalRepository: false, + UseNetwork: false, }, - expected: "org.some.group", + expected: expectedLicenses, }, { - name: "parent groupId", - property: "${project.parent.groupId}", - pom: gopom.Project{ - Parent: &gopom.Parent{ - GroupID: stringPointer("org.some.parent"), - }, + name: "local m2 cache", + scanDir: containingDir, + cfg: ArchiveCatalogerConfig{ + UseMavenLocalRepository: true, + MavenLocalRepositoryDir: localM2, + UseNetwork: false, + MavenBaseURL: "", }, - expected: "org.some.parent", + expected: expectedLicenses, }, { - name: "nil pointer halts search", - property: "${project.parent.groupId}", - pom: gopom.Project{ - Parent: nil, + name: "local with network", + scanDir: containingDir, + cfg: ArchiveCatalogerConfig{ + UseMavenLocalRepository: false, + UseNetwork: true, + MavenBaseURL: mavenURL, }, - expected: "", + expected: expectedLicenses, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cat := NewPomCataloger(test.cfg) + + ds, err := directorysource.NewFromPath(test.scanDir) + require.NoError(t, err) + + fr, err := ds.FileResolver(source.AllLayersScope) + require.NoError(t, err) + + ctx := context.TODO() + pkgs, _, err := cat.Catalog(ctx, fr) + require.NoError(t, err) + + var child1 pkg.Package + for _, p := range pkgs { + if p.Name == "child-one" { + child1 = p + break + } + } + require.Equal(t, "child-one", child1.Name) + + got := child1.Licenses.ToSlice() + for i := 0; i < len(got); i++ { + // ignore locations, just check license text + (&got[i]).Locations = file.LocationSet{} + } + require.ElementsMatch(t, test.expected, got) + }) + } +} + +func 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), "�") + }) + } +} + +func getCommonsTextExpectedPackages() []pkg.Package { + return []pkg.Package{ { - name: "nil string pointer halts search", - property: "${project.parent.groupId}", - pom: gopom.Project{ - Parent: &gopom.Parent{ - GroupID: nil, + Name: "commons-lang3", + Version: "3.12.0", + PURL: "pkg:maven/org.apache.commons/commons-lang3@3.12.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.apache.commons", + ArtifactID: "commons-lang3", }, }, - expected: "", }, { - name: "double dereference", - property: "${springboot.version}", - pom: gopom.Project{ - Parent: &gopom.Parent{ - Version: stringPointer("1.2.3"), - }, - Properties: &gopom.Properties{ - Entries: map[string]string{ - "springboot.version": "${project.parent.version}", - }, + Name: "junit-jupiter", + Version: "", + PURL: "pkg:maven/org.junit.jupiter/junit-jupiter", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.junit.jupiter", + ArtifactID: "junit-jupiter", + Scope: "test", }, }, - expected: "1.2.3", }, { - name: "map missing stops double dereference", - property: "${springboot.version}", - pom: gopom.Project{ - Parent: &gopom.Parent{ - Version: stringPointer("1.2.3"), + Name: "assertj-core", + Version: "3.23.1", + PURL: "pkg:maven/org.assertj/assertj-core@3.23.1", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.assertj", + ArtifactID: "assertj-core", + Scope: "test", }, }, - expected: "", }, { - name: "resolution halts even if it resolves to a variable", - property: "${springboot.version}", - pom: gopom.Project{ - Parent: &gopom.Parent{ - Version: stringPointer("${undefined.version}"), + Name: "commons-io", + Version: "2.11.0", + PURL: "pkg:maven/commons-io/commons-io@2.11.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "commons-io", + ArtifactID: "commons-io", + Scope: "test", }, - Properties: &gopom.Properties{ - Entries: map[string]string{ - "springboot.version": "${project.parent.version}", - }, + }, + }, + { + Name: "mockito-inline", + Version: "4.8.0", + PURL: "pkg:maven/org.mockito/mockito-inline@4.8.0", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.mockito", + ArtifactID: "mockito-inline", + Scope: "test", }, }, - expected: "", }, { - name: "resolution halts even if cyclic", - property: "${springboot.version}", - pom: gopom.Project{ - Properties: &gopom.Properties{ - Entries: map[string]string{ - "springboot.version": "${springboot.version}", - }, + Name: "js", + Version: "22.0.0.2", + PURL: "pkg:maven/org.graalvm.js/js@22.0.0.2", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.graalvm.js", + ArtifactID: "js", + Scope: "test", }, }, - expected: "", }, { - name: "resolution halts even if cyclic more steps", - property: "${cyclic.version}", - pom: gopom.Project{ - Properties: &gopom.Properties{ - Entries: map[string]string{ - "other.version": "${cyclic.version}", - "springboot.version": "${other.version}", - "cyclic.version": "${springboot.version}", - }, + Name: "js-scriptengine", + Version: "22.0.0.2", + PURL: "pkg:maven/org.graalvm.js/js-scriptengine@22.0.0.2", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.graalvm.js", + ArtifactID: "js-scriptengine", + Scope: "test", }, }, - expected: "", }, { - name: "resolution halts even if cyclic involving parent", - property: "${cyclic.version}", - pom: gopom.Project{ - Parent: &gopom.Parent{ - Version: stringPointer("${cyclic.version}"), + Name: "commons-rng-simple", + Version: "1.4", + PURL: "pkg:maven/org.apache.commons/commons-rng-simple@1.4", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.apache.commons", + ArtifactID: "commons-rng-simple", + Scope: "test", }, - Properties: &gopom.Properties{ - Entries: map[string]string{ - "other.version": "${parent.version}", - "springboot.version": "${other.version}", - "cyclic.version": "${springboot.version}", - }, + }, + }, + { + Name: "jmh-core", + Version: "1.35", + PURL: "pkg:maven/org.openjdk.jmh/jmh-core@1.35", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.openjdk.jmh", + ArtifactID: "jmh-core", + Scope: "test", }, }, - expected: "", }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - resolved := resolveProperty(test.pom, stringPointer(test.property), test.name) - assert.Equal(t, test.expected, resolved) - }) - } -} - -func stringPointer(s string) *string { - return &s -} - -func Test_getUtf8Reader(t *testing.T) { - tests := []struct { - name string - contents string - }{ { - name: "unknown encoding", - // random binary contents - contents: "BkiJz02JyEWE0nXR6TH///9NicpJweEETIucJIgAAABJicxPjQwhTY1JCE05WQh0BU2J0eunTYshTIusJIAAAAAPHwBNOeV1BUUx2+tWTIlUJDhMiUwkSEyJRCQgSIl8JFBMiQ==", + Name: "jmh-generator-annprocess", + Version: "1.35", + PURL: "pkg:maven/org.openjdk.jmh/jmh-generator-annprocess@1.35", + Language: pkg.Java, + Type: pkg.JavaPkg, + FoundBy: pomCatalogerName, + Metadata: pkg.JavaArchive{ + PomProperties: &pkg.JavaPomProperties{ + GroupID: "org.openjdk.jmh", + ArtifactID: "jmh-generator-annprocess", + Scope: "test", + }, + }, }, } - 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), "�") - }) - } } diff --git a/syft/pkg/cataloger/java/test-fixtures/local-repository-settings/.m2/settings.xml b/syft/pkg/cataloger/java/test-fixtures/local-repository-settings/.m2/settings.xml new file mode 100644 index 00000000000..f8b9552a4b7 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/local-repository-settings/.m2/settings.xml @@ -0,0 +1,4 @@ + + /some/other/repo + \ No newline at end of file diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/commons-text.pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/commons-text.pom.xml deleted file mode 100644 index 6f54a6ed6b1..00000000000 --- a/syft/pkg/cataloger/java/test-fixtures/pom/commons-text.pom.xml +++ /dev/null @@ -1,575 +0,0 @@ - - - - 4.0.0 - - org.apache.commons - commons-parent - 54 - - commons-text - 1.10.0 - Apache Commons Text - Apache Commons Text is a library focused on algorithms working on strings. - https://commons.apache.org/proper/commons-text - - - ISO-8859-1 - UTF-8 - 1.8 - 1.8 - - text - org.apache.commons.text - - 1.10.0 - (Java 8+) - - TEXT - 12318221 - - text - https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text - site-content - - 5.9.1 - 3.2.0 - 9.3 - - 4.7.2.0 - 4.7.2 - 3.19.0 - 6.49.0 - - 4.8.0 - 0.8.8 - - - 3.10.0 - 3.4.1 - - - 22.0.0.2 - 1.4 - - 0.16.0 - false - - 1.35 - 3.1.2 - - - 1.9 - RC1 - true - scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid} - Gary Gregory - 86fdc7e2a11262cb - - - - - org.apache.commons - commons-lang3 - 3.12.0 - - - - org.junit.jupiter - junit-jupiter - test - - - org.assertj - assertj-core - 3.23.1 - test - - - commons-io - commons-io - 2.11.0 - test - - - org.mockito - - mockito-inline - ${commons.mockito.version} - test - - - org.graalvm.js - js - ${graalvm.version} - test - - - org.graalvm.js - js-scriptengine - ${graalvm.version} - test - - - org.apache.commons - commons-rng-simple - ${commons.rng.version} - test - - - org.openjdk.jmh - jmh-core - ${jmh.version} - test - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh.version} - test - - - - - clean verify apache-rat:check japicmp:cmp checkstyle:check spotbugs:check javadoc:javadoc - - - - org.apache.rat - apache-rat-plugin - - - site-content/** - src/site/resources/download_lang.cgi - src/test/resources/org/apache/commons/text/stringEscapeUtilsTestData.txt - src/test/resources/org/apache/commons/text/lcs-perf-analysis-inputs.csv - src/site/resources/release-notes/RELEASE-NOTES-*.txt - - - - - maven-pmd-plugin - ${commons.pmd.version} - - ${maven.compiler.target} - - - - net.sourceforge.pmd - pmd-core - ${commons.pmd-impl.version} - - - net.sourceforge.pmd - pmd-java - ${commons.pmd-impl.version} - - - net.sourceforge.pmd - pmd-javascript - ${commons.pmd-impl.version} - - - net.sourceforge.pmd - pmd-jsp - ${commons.pmd-impl.version} - - - - - - - - maven-checkstyle-plugin - ${checkstyle.plugin.version} - - false - src/conf/checkstyle.xml - src/conf/checkstyle-header.txt - src/conf/checkstyle-suppressions.xml - src/conf/checkstyle-suppressions.xml - true - **/generated/**.java,**/jmh_generated/**.java - - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - - - - com.github.spotbugs - spotbugs-maven-plugin - ${commons.spotbugs.plugin.version} - - - com.github.spotbugs - spotbugs - ${commons.spotbugs.impl.version} - - - - src/conf/spotbugs-exclude-filter.xml - - - - maven-assembly-plugin - - - src/assembly/bin.xml - src/assembly/src.xml - - gnu - - - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - - - - ${commons.module.name} - - - - - - org.apache.maven.plugins - maven-scm-publish-plugin - - - javadocs - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - ${maven.compiler.source} - - - - - - - - - maven-checkstyle-plugin - ${checkstyle.plugin.version} - - false - src/conf/checkstyle.xml - src/conf/checkstyle-header.txt - src/conf/checkstyle-suppressions.xml - src/conf/checkstyle-suppressions.xml - true - **/generated/**.java,**/jmh_generated/**.java - - - - - checkstyle - - - - - - - com.github.spotbugs - spotbugs-maven-plugin - ${commons.spotbugs.plugin.version} - - src/conf/spotbugs-exclude-filter.xml - - - - com.github.siom79.japicmp - japicmp-maven-plugin - - - maven-pmd-plugin - 3.19.0 - - ${maven.compiler.target} - - - - - pmd - cpd - - - - - - org.codehaus.mojo - taglist-maven-plugin - 3.0.0 - - - - - Needs Work - - - TODO - exact - - - FIXME - exact - - - XXX - exact - - - - - Noteable Markers - - - NOTE - exact - - - NOPMD - exact - - - NOSONAR - exact - - - - - - - - - - - 2014 - - - - kinow - Bruno P. Kinoshita - kinow@apache.org - - - britter - Benedikt Ritter - britter@apache.org - - - chtompki - Rob Tompkins - chtompki@apache.org - - - ggregory - Gary Gregory - ggregory at apache.org - https://www.garygregory.com - The Apache Software Foundation - https://www.apache.org/ - - PMC Member - - America/New_York - - https://people.apache.org/~ggregory/img/garydgregory80.png - - - - djones - Duncan Jones - djones@apache.org - - - - - - Don Jeba - donjeba@yahoo.com - - - Sampanna Kahu - - - Jarek Strzelecki - - - Lee Adcock - - - Amey Jadiye - ameyjadiye@gmail.com - - - Arun Vinud S S - - - Ioannis Sermetziadis - - - Jostein Tveit - - - Luciano Medallia - - - Jan Martin Keil - - - Nandor Kollar - - - Nick Wong - - - Ali Ghanbari - https://ali-ghanbari.github.io/ - - - - - scm:git:https://gitbox.apache.org/repos/asf/commons-text - scm:git:https://gitbox.apache.org/repos/asf/commons-text - https://gitbox.apache.org/repos/asf?p=commons-text.git - - - - jira - https://issues.apache.org/jira/browse/TEXT - - - - - apache.website - Apache Commons Site - scm:svn:https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text/ - - - - - - setup-checkout - - - site-content - - - - - - org.apache.maven.plugins - maven-antrun-plugin - - - prepare-checkout - - run - - pre-site - - - - - - - - - - - - - - - - - - - - - - - - java9+ - - [9,) - - - - true - - - - benchmark - - true - org.apache - - - - - org.codehaus.mojo - exec-maven-plugin - 3.1.0 - - - benchmark - test - - exec - - - test - java - - -classpath - - org.openjdk.jmh.Main - -rf - json - -rff - target/jmh-result.${benchmark}.json - ${benchmark} - - - - - - - - - - \ No newline at end of file diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/local/child-1/pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/local/child-1/pom.xml new file mode 100644 index 00000000000..63ed7d474dd --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/local/child-1/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + + + + my.org + parent-one + 3.11.0 + ../parent-1/pom.xml + + + child-one + + ${project.one}.3.6 + jar + + + 3.12.0 + 4.2 + 4.12 + + + + + org.apache.commons + commons-lang3 + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/local/commons-text-1.10.0/pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/local/commons-text-1.10.0/pom.xml new file mode 100644 index 00000000000..e4ad83f1596 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/local/commons-text-1.10.0/pom.xml @@ -0,0 +1,263 @@ + + + + 4.0.0 + + org.apache.commons + commons-parent + 54 + + commons-text + 1.10.0 + Apache Commons Text + Apache Commons Text is a library focused on algorithms working on strings. + https://commons.apache.org/proper/commons-text + + + ISO-8859-1 + UTF-8 + 1.8 + 1.8 + + text + org.apache.commons.text + + 1.10.0 + (Java 8+) + + TEXT + 12318221 + + text + https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text + site-content + + 5.9.1 + 3.2.0 + 9.3 + + 4.7.2.0 + 4.7.2 + 3.19.0 + 6.49.0 + + 4.8.0 + 0.8.8 + + + 3.10.0 + 3.4.1 + + + 22.0.0.2 + 1.4 + + 0.16.0 + false + + 1.35 + 3.1.2 + + + 1.9 + RC1 + true + scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid} + Gary Gregory + 86fdc7e2a11262cb + + + + + org.apache.commons + commons-lang3 + 3.12.0 + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + 3.23.1 + test + + + commons-io + commons-io + 2.11.0 + test + + + org.mockito + + mockito-inline + ${commons.mockito.version} + test + + + org.graalvm.js + js + ${graalvm.version} + test + + + org.graalvm.js + js-scriptengine + ${graalvm.version} + test + + + org.apache.commons + commons-rng-simple + ${commons.rng.version} + test + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + + + + 2014 + + + scm:git:https://gitbox.apache.org/repos/asf/commons-text + scm:git:https://gitbox.apache.org/repos/asf/commons-text + https://gitbox.apache.org/repos/asf?p=commons-text.git + + + + jira + https://issues.apache.org/jira/browse/TEXT + + + + + apache.website + Apache Commons Site + scm:svn:https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-text/ + + + + + + setup-checkout + + + site-content + + + + + + org.apache.maven.plugins + maven-antrun-plugin + + + prepare-checkout + + run + + pre-site + + + + + + + + + + + + + + + + + + + + + + + + java9+ + + [9,) + + + + true + + + + benchmark + + true + org.apache + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + benchmark + test + + exec + + + test + java + + -classpath + + org.openjdk.jmh.Main + -rf + json + -rff + target/jmh-result.${benchmark}.json + ${benchmark} + + + + + + + + + + \ No newline at end of file diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/local/contains-child-1/pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/local/contains-child-1/pom.xml new file mode 100644 index 00000000000..18dcd8c4bbf --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/local/contains-child-1/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + contains-child-one + 5 + jar + + + + + my.org + child-one + 1.3.6 + + + + + + + my.org + child-one + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/local/example-java-app-maven/pom.xml similarity index 100% rename from syft/pkg/cataloger/java/test-fixtures/pom/pom.xml rename to syft/pkg/cataloger/java/test-fixtures/pom/local/example-java-app-maven/pom.xml diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/local/parent-1/pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/local/parent-1/pom.xml new file mode 100644 index 00000000000..4a6d1f323c2 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/local/parent-1/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + my.org + parent-two + 13.7.8 + ../parent-2/pom.xml + + + parent-one + 3.11.0 + pom + + + + 3.1${project.parent.version}.0 + 4.3 + + + + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + + + + + org.apache.commons + commons-text + ${commons.text.version} + + + org.apache.commons + commons-collections4 + ${commons.collections4.version} + + + junit + junit + ${commons.junit.version} + test + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/local/parent-2/pom.xml b/syft/pkg/cataloger/java/test-fixtures/pom/local/parent-2/pom.xml new file mode 100644 index 00000000000..5ca8cc4a202 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/local/parent-2/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + my.org + parent-two + 13.7.8 + pom + + + 3.14.0 + 4.4 + 1.12.0 + 4.13.2 + 3 + 1 + + + + + Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-v20.html + + + + + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + org.apache.commons + commons-text + ${commons.text.version} + + + junit + junit + ${commons.junit.version} + test + + + + + + + org.apache.commons + commons-text + ${commons.text.version} + + + org.apache.commons + commons-collections4 + ${commons.collections4.version} + + + junit + junit + ${commons.junit.version} + test + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/child-one/1.3.6/child-one-1.3.6.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/child-one/1.3.6/child-one-1.3.6.pom new file mode 100644 index 00000000000..6a72f2fd56d --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/child-one/1.3.6/child-one-1.3.6.pom @@ -0,0 +1,41 @@ + + + 4.0.0 + + + + + my.org + parent-one + 3.11.0 + + + child-one + + ${project.one}.3.6 + jar + + + 3.12.0 + 4.2 + 4.12 + + + + + org.apache.commons + commons-lang3 + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/child-two/2.1.90/child-two-2.1.90.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/child-two/2.1.90/child-two-2.1.90.pom new file mode 100644 index 00000000000..62a3592d181 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/child-two/2.1.90/child-two-2.1.90.pom @@ -0,0 +1,53 @@ + + + 4.0.0 + + + + + my.org + parent-one + 3.11.0 + + + ${project.parent.groupId} + child-two + 2.1.90 + jar + + + 4.2 + 4.12 + my.other.org + + + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-math${project.parent.version} + 3.5 + + + org.apache.commons + commons-exec + 1.${commons-exec_subversion} + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/parent-one/3.11.0/parent-one-3.11.0.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/parent-one/3.11.0/parent-one-3.11.0.pom new file mode 100644 index 00000000000..4dd7d533f73 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/parent-one/3.11.0/parent-one-3.11.0.pom @@ -0,0 +1,51 @@ + + + 4.0.0 + + my.org + parent-two + 13.7.8 + + + my.org + parent-one + 3.11.0 + pom + + + + 3.1${project.parent.version}.0 + 4.3 + + + + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + + + + + org.apache.commons + commons-text + ${commons.text.version} + + + org.apache.commons + commons-collections4 + ${commons.collections4.version} + + + junit + junit + ${commons.junit.version} + test + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/parent-two/13.7.8/parent-two-13.7.8.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/parent-two/13.7.8/parent-two-13.7.8.pom new file mode 100644 index 00000000000..3341e805ce9 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/my/org/parent-two/13.7.8/parent-two-13.7.8.pom @@ -0,0 +1,67 @@ + + + 4.0.0 + + my.org + parent-two + 13.7.8 + pom + + + + Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-v20.html + + + + + 3.14.0 + 4.4 + 1.12.0 + 4.13.2 + 3 + 1 + + + + + + org.apache.commons + commons-lang3 + ${commons.lang3.version} + + + org.apache.commons + commons-text + ${commons.text.version} + + + junit + junit + ${commons.junit.version} + test + + + + + + + org.apache.commons + commons-text + ${commons.text.version} + + + org.apache.commons + commons-collections4 + ${commons.collections4.version} + + + junit + junit + ${commons.junit.version} + test + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/maven-xml-responses/parent-7.11.2.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/net/shibboleth/parent/7.11.2/parent-7.11.2.pom similarity index 100% rename from syft/pkg/cataloger/java/test-fixtures/maven-xml-responses/parent-7.11.2.pom rename to syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/net/shibboleth/parent/7.11.2/parent-7.11.2.pom diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/apache/commons/commons-parent/54/commons-parent-54.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/apache/commons/commons-parent/54/commons-parent-54.pom new file mode 100644 index 00000000000..3fd66f094f4 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/apache/commons/commons-parent/54/commons-parent-54.pom @@ -0,0 +1,132 @@ + + + + 4.0.0 + + org.apache + apache + 27 + + org.apache.commons + commons-parent + 54 + pom + Apache Commons Parent + The Apache Commons Parent POM provides common settings for all Apache Commons components. + 2006 + https://commons.apache.org/commons-parent-pom.html + + + 3.3.9 + ${project.version} + true + Gary Gregory + 86fdc7e2a11262cb + + 1.3 + 1.3 + false + + + + + 1.22 + 1.0 + 3.4.2 + 3.3.0 + 1.12 + 2.12.1 + 3.2.0 + 9.3 + 2.7 + 3.10.1 + 4.3.0 + EpochMillis + 2.7.1 + 0.5.5 + 5.9.0 + + 3.12.1 + 3.2.1 + 4.7.2.0 + 3.5.2 + + + ${project.artifactId}-${commons.release.version} + + -bin + ${project.artifactId}-${commons.release.2.version} + + -bin + ${project.artifactId}-${commons.release.3.version} + + -bin + + -bin + + + 1.00 + 0.90 + 0.95 + 0.85 + 0.85 + 0.90 + false + + ${project.artifactId} + + ${project.artifactId} + + + org.apache.commons.${commons.packageId} + org.apache.commons.*;version=${project.version};-noimport:=true + * + + + true + + + ${project.build.directory}/osgi/MANIFEST.MF + + scp + + iso-8859-1 + ${commons.encoding} + ${commons.encoding} + ${commons.encoding} + yyyy-MM-dd HH:mm:ssZ + ${scmBranch}@r${buildNumber}; ${maven.build.timestamp} + + + + + + org.junit + junit-bom + ${commons.junit.version} + pom + import + + + + + + + site-basic + + true + true + true + true + true + true + true + true + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/junit/junit-bom/5.9.0/junit-bom-5.9.0.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/junit/junit-bom/5.9.0/junit-bom-5.9.0.pom new file mode 100644 index 00000000000..eb1b959bc86 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/junit/junit-bom/5.9.0/junit-bom-5.9.0.pom @@ -0,0 +1,151 @@ + + + 4.0.0 + org.junit + junit-bom + 5.9.0 + pom + + + Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-v20.html + + + + + bechte + Stefan Bechtold + stefan.bechtold@me.com + + + jlink + Johannes Link + business@johanneslink.net + + + marcphilipp + Marc Philipp + mail@marcphilipp.de + + + mmerdes + Matthias Merdes + matthias.merdes@heidelpay.com + + + sbrannen + Sam Brannen + sam@sambrannen.com + + + sormuras + Christian Stein + sormuras@gmail.com + + + juliette-derancourt + Juliette de Rancourt + derancourt.juliette@gmail.com + + + + scm:git:git://github.com/junit-team/junit5.git + scm:git:git://github.com/junit-team/junit5.git + https://github.com/junit-team/junit5 + + + + + org.junit.jupiter + junit-jupiter + 5.9.0 + + + org.junit.jupiter + junit-jupiter-api + 5.9.0 + + + org.junit.jupiter + junit-jupiter-engine + 5.9.0 + + + org.junit.jupiter + junit-jupiter-migrationsupport + 5.9.0 + + + org.junit.jupiter + junit-jupiter-params + 5.9.0 + + + org.junit.platform + junit-platform-commons + 1.9.0 + + + org.junit.platform + junit-platform-console + 1.9.0 + + + org.junit.platform + junit-platform-engine + 1.9.0 + + + org.junit.platform + junit-platform-jfr + 1.9.0 + + + org.junit.platform + junit-platform-launcher + 1.9.0 + + + org.junit.platform + junit-platform-reporting + 1.9.0 + + + org.junit.platform + junit-platform-runner + 1.9.0 + + + org.junit.platform + junit-platform-suite + 1.9.0 + + + org.junit.platform + junit-platform-suite-api + 1.9.0 + + + org.junit.platform + junit-platform-suite-commons + 1.9.0 + + + org.junit.platform + junit-platform-suite-engine + 1.9.0 + + + org.junit.platform + junit-platform-testkit + 1.9.0 + + + org.junit.vintage + junit-vintage-engine + 5.9.0 + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/junit/junit-bom/5.9.1/junit-bom-5.9.1.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/junit/junit-bom/5.9.1/junit-bom-5.9.1.pom new file mode 100644 index 00000000000..c21518a394c --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/junit/junit-bom/5.9.1/junit-bom-5.9.1.pom @@ -0,0 +1,152 @@ + + + 4.0.0 + org.junit + junit-bom + 5.9.1 + pom + JUnit 5 (Bill of Materials) + + + Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-v20.html + + + + + bechte + Stefan Bechtold + stefan.bechtold@me.com + + + jlink + Johannes Link + business@johanneslink.net + + + marcphilipp + Marc Philipp + mail@marcphilipp.de + + + mmerdes + Matthias Merdes + matthias.merdes@heidelpay.com + + + sbrannen + Sam Brannen + sam@sambrannen.com + + + sormuras + Christian Stein + sormuras@gmail.com + + + juliette-derancourt + Juliette de Rancourt + derancourt.juliette@gmail.com + + + + scm:git:git://github.com/junit-team/junit5.git + scm:git:git://github.com/junit-team/junit5.git + https://github.com/junit-team/junit5 + + + + + org.junit.jupiter + junit-jupiter + 5.9.1 + + + org.junit.jupiter + junit-jupiter-api + 5.9.1 + + + org.junit.jupiter + junit-jupiter-engine + 5.9.1 + + + org.junit.jupiter + junit-jupiter-migrationsupport + 5.9.1 + + + org.junit.jupiter + junit-jupiter-params + 5.9.1 + + + org.junit.platform + junit-platform-commons + 1.9.1 + + + org.junit.platform + junit-platform-console + 1.9.1 + + + org.junit.platform + junit-platform-engine + 1.9.1 + + + org.junit.platform + junit-platform-jfr + 1.9.1 + + + org.junit.platform + junit-platform-launcher + 1.9.1 + + + org.junit.platform + junit-platform-reporting + 1.9.1 + + + org.junit.platform + junit-platform-runner + 1.9.1 + + + org.junit.platform + junit-platform-suite + 1.9.1 + + + org.junit.platform + junit-platform-suite-api + 1.9.1 + + + org.junit.platform + junit-platform-suite-commons + 1.9.1 + + + org.junit.platform + junit-platform-suite-engine + 1.9.1 + + + org.junit.platform + junit-platform-testkit + 1.9.1 + + + org.junit.vintage + junit-vintage-engine + 5.9.1 + + + + diff --git a/syft/pkg/cataloger/java/test-fixtures/maven-xml-responses/opensaml-parent-3.4.6.pom b/syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/opensaml/opensaml-parent/3.4.6/opensaml-parent-3.4.6.pom similarity index 100% rename from syft/pkg/cataloger/java/test-fixtures/maven-xml-responses/opensaml-parent-3.4.6.pom rename to syft/pkg/cataloger/java/test-fixtures/pom/maven-repo/org/opensaml/opensaml-parent/3.4.6/opensaml-parent-3.4.6.pom diff --git a/syft/pkg/cataloger/swipl/package.go b/syft/pkg/cataloger/swipl/package.go index 252dda56b7f..6421ede70b6 100644 --- a/syft/pkg/cataloger/swipl/package.go +++ b/syft/pkg/cataloger/swipl/package.go @@ -1,8 +1,6 @@ package swipl import ( - // "strings" - "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" diff --git a/syft/pkg/license.go b/syft/pkg/license.go index fb79028cee5..ae311a13c35 100644 --- a/syft/pkg/license.go +++ b/syft/pkg/license.go @@ -70,7 +70,7 @@ func NewLicenseFromType(value string, t license.Type) License { var err error spdxExpression, err = license.ParseExpression(value) if err != nil { - log.Trace("unable to parse license expression: %w", err) + log.WithFields("error", err, "expression", value).Trace("unable to parse license expression") } }