feat(sbom): wire archive orchestrator into CreateSBOM pipeline

Integrate the archive extraction orchestrator into the SBOM creation
pipeline, enabling recursive archive cataloging when configured:

- Add Archive field to CreateSBOMConfig with WithArchiveConfig builder
- Add Exclusions field to CreateSBOMConfig with WithExclusions builder
  so user --exclude patterns also filter content inside extracted
  archives, not only the source filesystem
- Add setupArchiveOrchestration() that creates the orchestrator when
  MaxDepth > 0, runs the iterative extract loop, and returns the
  composite resolver with archive contents visible to all catalogers
- Include archive config in configuration audit trail
- Add CLI archive config section (max-depth, size/file limits,
  exclude-extensions) wired through Catalog.ToArchiveConfig()
- CLI Catalog.ToSBOMConfig threads cfg.Exclusions through
  WithExclusions so child resolvers respect them

When MaxDepth is 0 (default), behavior is unchanged. When > 0, archives
discovered during scanning are extracted and their contents made visible
to all catalogers via the CompositeResolver.

Signed-off-by: Thomas Bechtold <thomasbechtold@jpberlin.de>
This commit is contained in:
Thomas Bechtold
2026-04-27 08:49:00 +02:00
parent 5d6ad0b9e8
commit 3fc5194f77
5 changed files with 143 additions and 1 deletions
+36
View File
@@ -0,0 +1,36 @@
package options
import (
"github.com/anchore/clio"
"github.com/anchore/syft/syft/cataloging"
)
type archiveConfig struct {
MaxDepth int `yaml:"max-depth" json:"max-depth" mapstructure:"max-depth"`
MaxExtractionSizeBytes int64 `yaml:"max-extraction-size-bytes" json:"max-extraction-size-bytes" mapstructure:"max-extraction-size-bytes"`
MaxFileCount int `yaml:"max-file-count" json:"max-file-count" mapstructure:"max-file-count"`
MaxTotalExtractionBytes int64 `yaml:"max-total-extraction-bytes" json:"max-total-extraction-bytes" mapstructure:"max-total-extraction-bytes"`
ExcludeExtensions []string `yaml:"exclude-extensions" json:"exclude-extensions" mapstructure:"exclude-extensions"`
}
var _ interface {
clio.FieldDescriber
} = (*archiveConfig)(nil)
func defaultArchiveConfig() archiveConfig {
return archiveConfig{
MaxDepth: cataloging.DefaultArchiveMaxDepth,
MaxExtractionSizeBytes: cataloging.DefaultArchiveMaxExtractionSizeBytes,
MaxFileCount: cataloging.DefaultArchiveMaxFileCount,
MaxTotalExtractionBytes: cataloging.DefaultArchiveMaxTotalExtractionBytes,
}
}
func (o *archiveConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.MaxDepth, `maximum depth of recursive archive extraction (0 = disabled, no recursive extraction)
enabling this allows syft to discover packages inside nested archives (e.g., a tar.gz containing Python packages)`)
descriptions.Add(&o.MaxExtractionSizeBytes, `maximum total bytes to extract from a single archive before stopping`)
descriptions.Add(&o.MaxFileCount, `maximum number of files to extract from a single archive before stopping`)
descriptions.Add(&o.MaxTotalExtractionBytes, `maximum total bytes to extract across all archives before stopping`)
descriptions.Add(&o.ExcludeExtensions, `archive file extensions to skip during recursive extraction (e.g., [".rpm", ".deb"])`)
}
+16
View File
@@ -37,6 +37,7 @@ type Catalog struct {
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
License licenseConfig `yaml:"license" json:"license" mapstructure:"license"`
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
Archive archiveConfig `yaml:"archive" json:"archive" mapstructure:"archive"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
@@ -76,6 +77,7 @@ func DefaultCatalog() Catalog {
Scope: source.SquashedScope.String(),
Package: defaultPackageConfig(),
License: defaultLicenseConfig(),
Archive: defaultArchiveConfig(),
LinuxKernel: defaultLinuxKernelConfig(),
JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(),
@@ -102,6 +104,8 @@ func (cfg Catalog) ToSBOMConfig(id clio.Identification) *syft.CreateSBOMConfig {
WithPackagesConfig(cfg.ToPackagesConfig()).
WithLicenseConfig(cfg.ToLicenseConfig()).
WithFilesConfig(cfg.ToFilesConfig()).
WithArchiveConfig(cfg.ToArchiveConfig()).
WithExclusions(cfg.Exclusions).
WithCatalogerSelection(
cataloging.NewSelectionRequest().
WithDefaults(cfg.DefaultCatalogers...).
@@ -109,6 +113,18 @@ func (cfg Catalog) ToSBOMConfig(id clio.Identification) *syft.CreateSBOMConfig {
)
}
func (cfg Catalog) ToArchiveConfig() cataloging.ArchiveSearchConfig {
return cataloging.ArchiveSearchConfig{
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
MaxDepth: cfg.Archive.MaxDepth,
MaxExtractionSizeBytes: cfg.Archive.MaxExtractionSizeBytes,
MaxFileCount: cfg.Archive.MaxFileCount,
MaxTotalExtractionBytes: cfg.Archive.MaxTotalExtractionBytes,
ExcludeExtensions: cfg.Archive.ExcludeExtensions,
}
}
func (cfg Catalog) ToSearchConfig() cataloging.SearchConfig {
return cataloging.SearchConfig{
Scope: source.ParseScope(cfg.Scope),