diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index 95f2fe1ce..49ffd7c89 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -179,10 +179,6 @@ func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig { } func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { - archiveSearch := cataloging.ArchiveSearchConfig{ - IncludeIndexedArchives: cfg.Package.SearchIndexedArchives, - IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives, - } return pkgcataloging.Config{ Binary: binary.DefaultClassifierCatalogerConfig(), Cpp: cpp.DefaultCatalogerConfig(). @@ -229,7 +225,7 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config { WithMavenLocalRepositoryDir(cfg.Java.MavenLocalRepositoryDir). WithUseNetwork(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Java, task.Maven), cfg.Java.UseNetwork)). WithMavenBaseURL(cfg.Java.MavenURL). - WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth). + WithMaxParentRecursiveDepth(cfg.Java.MaxParentRecursiveDepth). WithResolveTransitiveDependencies(cfg.Java.ResolveTransitiveDependencies), } } diff --git a/cmd/syft/internal/options/pkg.go b/cmd/syft/internal/options/pkg.go index c79027a2a..b72def965 100644 --- a/cmd/syft/internal/options/pkg.go +++ b/cmd/syft/internal/options/pkg.go @@ -20,10 +20,10 @@ var _ interface { func (o *packageConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { descriptions.Add(&o.SearchIndexedArchives, `search within archives that do contain a file index to search against (zip) -note: for now this only applies to the java package cataloger`) +note: this only applies to the java package cataloger, and is ignored when nested-archive-max-depth is not 0 (every archive is searched)`) descriptions.Add(&o.SearchUnindexedArchives, `search within archives that do not contain a file index to search against (tar, tar.gz, tar.bz2, etc) note: enabling this may result in a performance impact since all discovered compressed tars will be decompressed -note: for now this only applies to the java package cataloger`) +note: this only applies to the java package cataloger, and is ignored when nested-archive-max-depth is not 0 (every archive is searched)`) descriptions.Add(&o.ExcludeBinaryOverlapByOwnership, `allows users to exclude synthetic binary packages from the sbom these packages are removed if an overlap with a non-synthetic package is found`) descriptions.Add(&o.NestedArchiveMaxDepth, `maximum depth to recursively catalog nested archives (0 = disabled, -1 = unlimited)`) diff --git a/syft/archive_cataloger_test.go b/syft/archive_cataloger_test.go index 15584f695..59f746453 100644 --- a/syft/archive_cataloger_test.go +++ b/syft/archive_cataloger_test.go @@ -305,13 +305,8 @@ func TestArchiveCataloger(t *testing.T) { func TestArchiveCataloger_featureOff(t *testing.T) { scanDir := writeFixture(t) - // java's squash-inlined MaxDepth is a config field a consumer can set: it must not enable the - // feature and must not be overwritten - supplied := java.DefaultArchiveCatalogerConfig() - supplied.ArchiveSearchConfig = supplied.ArchiveSearchConfig.WithMaxDepth(3) cfg := DefaultCreateSBOMConfig(). - WithCatalogerSelection(cataloging.NewSelectionRequest().WithDefaults("java")). - WithPackagesConfig(pkgcataloging.Config{JavaArchive: supplied}) + WithCatalogerSelection(cataloging.NewSelectionRequest().WithDefaults("java")) s := scanDirWithExclusions(t, scanDir, cfg, fixtureExclusions...) @@ -321,7 +316,41 @@ func TestArchiveCataloger_featureOff(t *testing.T) { "war-lib": {"app.war:WEB-INF/lib/war-lib-2.0.jar"}, "zip-lib": {"bundle.zip:lib/zip-lib-1.2.jar"}, }, javaVirtualPaths(s), "java's own recursion finds each once, under the virtual path the task also reports") - assert.Equal(t, 3, cfg.Packages.JavaArchive.MaxDepth, "the consumer's value must survive") + + t.Run("the top-level archive search booleans reach java", func(t *testing.T) { + cfg := DefaultCreateSBOMConfig(). + WithCatalogerSelection(cataloging.NewSelectionRequest().WithDefaults("java")). + WithArchiveConfig(cataloging.DefaultArchiveSearchConfig().WithIncludeUnindexedArchives(true)) + + s := scanDirWithExclusions(t, scanDir, cfg, fixtureExclusions...) + + assert.Contains(t, javaVirtualPaths(s), "tgz-lib") + assert.False(t, cfg.Packages.JavaArchive.IncludeUnindexedArchives, "the caller's config is not written to") + }) +} + +func TestArchiveCataloger_javaArchiveDepthIsAConfigError(t *testing.T) { + // depth and limits are not java settings; setting them on java's deprecated copy must fail fast + // rather than be silently ignored + for name, set := range map[string]func(*cataloging.ArchiveSearchConfig){ + "max depth": func(c *cataloging.ArchiveSearchConfig) { c.MaxDepth = 3 }, + "max memory bytes": func(c *cataloging.ArchiveSearchConfig) { c.MaxMemoryBytes = 1 }, + "max disk bytes": func(c *cataloging.ArchiveSearchConfig) { c.MaxDiskBytes = 1 }, + } { + t.Run(name, func(t *testing.T) { + supplied := java.DefaultArchiveCatalogerConfig() + supplied.ArchiveSearchConfig = cataloging.ArchiveSearchConfig{} + set(&supplied.ArchiveSearchConfig) + cfg := DefaultCreateSBOMConfig().WithPackagesConfig(pkgcataloging.Config{JavaArchive: supplied}) + + src, err := directorysource.New(directorysource.Config{Path: t.TempDir()}) + require.NoError(t, err) + t.Cleanup(func() { _ = src.Close() }) + + _, err = cfg.Create(context.Background(), src) + require.ErrorContains(t, err, "set them with CreateSBOMConfig.Archive") + }) + } } // packageLocations lists "archivePath|realPath" for every location of every package with the name. diff --git a/syft/create_sbom_config.go b/syft/create_sbom_config.go index 3b373faaf..bb64e438d 100644 --- a/syft/create_sbom_config.go +++ b/syft/create_sbom_config.go @@ -279,7 +279,7 @@ func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []t SearchConfig: c.Search, RelationshipsConfig: c.Relationships, DataGenerationConfig: c.DataGeneration, - PackagesConfig: c.Packages, + PackagesConfig: c.packagesWithArchiveSearch(), LicenseConfig: c.Licenses, ComplianceConfig: c.Compliance, FilesConfig: c.Files, @@ -480,12 +480,33 @@ func (c *CreateSBOMConfig) osFeatureDetectionTasks() []task.Task { return tasks } +// packagesWithArchiveSearch returns a copy of the packages config with the top-level archive search +// booleans applied to java, which is still the only cataloger that reads them. +// +// ponytail: a top-level boolean wins only when it differs from the default, so callers who set just +// java's deprecated copy see no change. Delete this along with that field. +func (c *CreateSBOMConfig) packagesWithArchiveSearch() pkgcataloging.Config { + pkgs := c.Packages + def := cataloging.DefaultArchiveSearchConfig() + java := &pkgs.JavaArchive.ArchiveSearchConfig + if c.Archive.IncludeIndexedArchives != def.IncludeIndexedArchives { + java.IncludeIndexedArchives = c.Archive.IncludeIndexedArchives + } + if c.Archive.IncludeUnindexedArchives != def.IncludeUnindexedArchives { + java.IncludeUnindexedArchives = c.Archive.IncludeUnindexedArchives + } + return pkgs +} + func (c *CreateSBOMConfig) validate() error { if c.Relationships.ExcludeBinaryPackagesWithFileOwnershipOverlap { if !c.Relationships.PackageFileOwnershipOverlap { return fmt.Errorf("invalid configuration: to exclude binary packages based on file ownership overlap relationships, cataloging file ownership overlap relationships must be enabled") } } + if j := c.Packages.JavaArchive.ArchiveSearchConfig; j.MaxDepth != 0 || j.MaxMemoryBytes != 0 || j.MaxDiskBytes != 0 { + return fmt.Errorf("invalid configuration: nested archive depth and limits are not java cataloger settings, set them with CreateSBOMConfig.Archive") + } return nil } diff --git a/syft/pkg/cataloger/java/archive_parser_traversal_test.go b/syft/pkg/cataloger/java/archive_parser_traversal_test.go index 8b3695198..ba8c0305c 100644 --- a/syft/pkg/cataloger/java/archive_parser_traversal_test.go +++ b/syft/pkg/cataloger/java/archive_parser_traversal_test.go @@ -114,17 +114,6 @@ func Test_nestedArchiveOwnershipSwitch(t *testing.T) { require.Len(t, pkgs, 1) assert.Equal(t, "example-app", pkgs[0].Name) }) - - t.Run("java's own archive-search depth does not hand off recursion", func(t *testing.T) { - // a library consumer setting java's squash-inlined MaxDepth must neither enable nor disable - // nested archive cataloging: that knob is Archive.MaxDepth on CreateSBOMConfig. Getting it - // wrong silently drops nested jars for every consumer that is not the CLI. - cfg := DefaultArchiveCatalogerConfig() - cfg.ArchiveSearchConfig = cfg.ArchiveSearchConfig.WithMaxDepth(2) - - pkgs := parse(t, pkgtest.Context(t), cfg) - require.Len(t, pkgs, 2, "this cataloger must still recurse itself") - }) } func Test_NewArchiveCataloger_wrappedParserRegistration(t *testing.T) { diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index fedd88298..c64508ff4 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -9,6 +9,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/anchore/syft/internal/archive" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" @@ -18,6 +19,9 @@ import ( // NewArchiveCataloger returns a new Java archive cataloger object for detecting packages with archives (jar, war, ear, par, sar, jpi, hpi, and native-image formats) func NewArchiveCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger { + if a := cfg.ArchiveSearchConfig; a.MaxDepth != 0 || a.MaxMemoryBytes != 0 || a.MaxDiskBytes != 0 { + log.Debug("java archive cataloger ignores nested archive depth and limits, set them with syft.CreateSBOMConfig.Archive") + } gap := newGenericArchiveParserAdapter(cfg) c := generic.NewCataloger("java-archive-cataloger"). diff --git a/syft/pkg/cataloger/java/config.go b/syft/pkg/cataloger/java/config.go index 652ca827b..a388e9e09 100644 --- a/syft/pkg/cataloger/java/config.go +++ b/syft/pkg/cataloger/java/config.go @@ -8,6 +8,9 @@ import ( ) type ArchiveCatalogerConfig struct { + // Deprecated: use syft.CreateSBOMConfig.Archive. Only the two Include* booleans are read here, and + // setting MaxDepth, MaxMemoryBytes or MaxDiskBytes is a configuration error. This will be removed in a + // future release. cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"` // UseNetwork enables network operations for java package metadata enrichment, such as fetching parent POMs and license information. @@ -37,8 +40,13 @@ type ArchiveCatalogerConfig struct { func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig { mavenCfg := maven.DefaultConfig() + search := cataloging.DefaultArchiveSearchConfig() return ArchiveCatalogerConfig{ - ArchiveSearchConfig: cataloging.DefaultArchiveSearchConfig(), + // only the booleans: depth and limits are not java settings + ArchiveSearchConfig: cataloging.ArchiveSearchConfig{ + IncludeIndexedArchives: search.IncludeIndexedArchives, + IncludeUnindexedArchives: search.IncludeUnindexedArchives, + }, UseNetwork: mavenCfg.UseNetwork, UseMavenLocalRepository: mavenCfg.UseLocalRepository, MavenLocalRepositoryDir: mavenCfg.LocalRepositoryDir, @@ -75,12 +83,19 @@ func (j ArchiveCatalogerConfig) WithResolveTransitiveDependencies(resolveTransit return j } +// Deprecated: set archive search with syft.CreateSBOMConfig.Archive and the parent depth with +// WithMaxParentRecursiveDepth. This will be removed in a future release. func (j ArchiveCatalogerConfig) WithArchiveTraversal(search cataloging.ArchiveSearchConfig, maxDepth int) ArchiveCatalogerConfig { j.MaxParentRecursiveDepth = maxDepth j.ArchiveSearchConfig = search return j } +func (j ArchiveCatalogerConfig) WithMaxParentRecursiveDepth(depth int) ArchiveCatalogerConfig { + j.MaxParentRecursiveDepth = depth + return j +} + func (j ArchiveCatalogerConfig) mavenConfig() maven.Config { return maven.Config{ UseNetwork: j.UseNetwork,