fix: make CreateSBOMConfig.Archive the one place to configure archive search

`ArchiveSearchConfig` lived in two places and each half was dead in one of them: the top-level booleans were never read (only java's copy was), and java's depth and limits did nothing. A library caller setting `Packages.JavaArchive.MaxDepth = 3` got no nested cataloging, and no error.

- java's embedded `ArchiveSearchConfig` and `WithArchiveTraversal` are deprecated; `WithMaxParentRecursiveDepth` replaces the latter's other half
- `CreateSBOM` now rejects `MaxDepth`, `MaxMemoryBytes` or `MaxDiskBytes` set on java's copy with an `invalid configuration` error
- the top-level `IncludeIndexedArchives` / `IncludeUnindexedArchives` are overlaid onto a copy of java's config, and win when they differ from the default. Callers who only set java's copy see no change
- `java.DefaultArchiveCatalogerConfig()` now carries only the booleans, so its descriptor no longer reports limits that java never used
- the CLI fills only the top-level config, and the option descriptions now say the search booleans are ignored when nested cataloging is on

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-24 11:18:51 -04:00
parent a4a1d7afa7
commit 3f0a599d31
7 changed files with 81 additions and 27 deletions
+1 -5
View File
@@ -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),
}
}
+2 -2
View File
@@ -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)`)
+36 -7
View File
@@ -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.
+22 -1
View File
@@ -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
}
@@ -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) {
+4
View File
@@ -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").
+16 -1
View File
@@ -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,