diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 0414e921f..c2b749758 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -210,7 +210,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithUseNetwork(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Java, task.Maven), cfg.Java.UseNetwork)). WithMavenBaseURL(cfg.Java.MavenURL). WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth). - WithResolveTransitiveDependencies(cfg.Java.ResolveTransitiveDependencies), + WithResolveTransitiveDependencies(cfg.Java.ResolveTransitiveDependencies). + WithDetectContainedPackages(cfg.Java.DetectContainedPackages), } } diff --git a/cmd/syft/internal/options/java.go b/cmd/syft/internal/options/java.go index 80849f242..5dbe16a0a 100644 --- a/cmd/syft/internal/options/java.go +++ b/cmd/syft/internal/options/java.go @@ -12,6 +12,7 @@ type javaConfig struct { 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"` ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"` + DetectContainedPackages bool `yaml:"detect-contained-packages" json:"detect-contained-packages" mapstructure:"detect-contained-packages"` } func defaultJavaConfig() javaConfig { @@ -24,6 +25,7 @@ func defaultJavaConfig() javaConfig { MavenLocalRepositoryDir: def.MavenLocalRepositoryDir, MavenURL: def.MavenBaseURL, ResolveTransitiveDependencies: def.ResolveTransitiveDependencies, + DetectContainedPackages: def.DetectContainedPackages, } } @@ -46,4 +48,6 @@ 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`) descriptions.Add(&o.ResolveTransitiveDependencies, `resolve transient dependencies such as those defined in a dependency's POM on Maven central`) + descriptions.Add(&o.DetectContainedPackages, `collect all Java package names contained within JAR files. This extracts package information +from class file paths within the archive`) } diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index ca5426ae6..271626323 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -890,19 +890,25 @@ func isValidMultiReleaseVersion(s string) bool { return false } - if s == "9" { - return true - } - - // 0 is not allowed + // Must start with 1-9 (format: {1-9}{0-9}*) if s[0] < '1' || s[0] > '9' { return false } - // Ony digits are allowed - return strings.IndexFunc(s, func(r rune) bool { + // Only digits are allowed + if strings.IndexFunc(s, func(r rune) bool { return r < '0' || r > '9' - }) != -1 + }) != -1 { + return false + } + + // Per spec: "Any versioned directory with N < 9 is ignored" + // Single digit must be 9; multi-digit (10+) is always >= 9 + if len(s) == 1 && s[0] < '9' { + return false + } + + return true } func (j *archiveParser) discoverContainedPackages() []string { diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index 9cb541e91..a0a3b5e8e 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -1817,3 +1817,44 @@ func Test_jarPomPropertyResolutionDoesNotPanic(t *testing.T) { _, _, err = ap.parse(ctx, nil) require.NoError(t, err) } + +func Test_isValidMultiReleaseVersion(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + // Valid versions (multi-release JARs require version >= 9) + {"9", true}, + {"10", true}, + {"11", true}, + {"17", true}, + {"21", true}, + {"100", true}, + + // Invalid versions - less than 9 (per spec: "Any versioned directory with N < 9 is ignored") + {"1", false}, + {"2", false}, + {"8", false}, + + // Invalid versions - format errors + {"", false}, // empty string + {"0", false}, // zero not allowed (first digit must be 1-9) + {"01", false}, // leading zero + {"9a", false}, // contains non-digit + {"a9", false}, // starts with non-digit + {"abc", false}, // all non-digits + {"-1", false}, // negative (starts with non-digit) + {"9.0", false}, // contains non-digit (period) + {"11-ea", false}, // contains non-digit (dash and letters) + {" 9", false}, // leading space + {"9 ", false}, // trailing space + {"1.8", false}, // old-style version format + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("input=%q", tt.input), func(t *testing.T) { + result := isValidMultiReleaseVersion(tt.input) + assert.Equal(t, tt.expected, result, "isValidMultiReleaseVersion(%q)", tt.input) + }) + } +} diff --git a/syft/pkg/cataloger/java/config.go b/syft/pkg/cataloger/java/config.go index 005bd871a..b5c090b23 100644 --- a/syft/pkg/cataloger/java/config.go +++ b/syft/pkg/cataloger/java/config.go @@ -36,7 +36,7 @@ type ArchiveCatalogerConfig struct { // DetectContainedPackages enables collecting all package names contained in a jar. // app-config: java.detect-contained-packages - DetectContainedPackages bool `yaml:"detect-contained-packages" json:"detect-contained-packages" mapstructure:"detect-contained-packages"` + DetectContainedPackages bool `yaml:"detect-contained-packages" json:"detect-contained-packages" mapstructure:"detect-contained-packages"` } func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig { diff --git a/syft/pkg/java.go b/syft/pkg/java.go index 713919827..64a54d314 100644 --- a/syft/pkg/java.go +++ b/syft/pkg/java.go @@ -115,7 +115,7 @@ type JavaArchive struct { ArchiveDigests []file.Digest `hash:"ignore" json:"digest,omitempty"` // ContainedPackages is a list of all package names contained in the jar - ContainedPackages []string `mapstructure:"ContainedPackages" json:"containedPackages"` + ContainedPackages []string `mapstructure:"ContainedPackages" json:"containedPackages,omitempty"` // Parent is reference to parent package (for nested archives) Parent *Package `hash:"ignore" json:"-"`