diff --git a/internal/capabilities/generate/discover_cataloger_configs.go b/internal/capabilities/generate/discover_cataloger_configs.go index a6d1b7e34..f7570f6b3 100644 --- a/internal/capabilities/generate/discover_cataloger_configs.go +++ b/internal/capabilities/generate/discover_cataloger_configs.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strings" "github.com/anchore/syft/internal/capabilities/internal" @@ -31,6 +32,10 @@ type ConfigInfo struct { var appConfigAnnotationPattern = regexp.MustCompile(`^//\s*app-config:\s*(.+)$`) +// embeddedFieldsAnnotationPattern matches "// capabilities-fields: A, B" on an embedded struct, which limits +// the embedded fields listed to the ones named (for a shared type the cataloger reads only part of). +var embeddedFieldsAnnotationPattern = regexp.MustCompile(`^//\s*capabilities-fields:\s*(.+)$`) + // DiscoverConfigs walks the cataloger directory and discovers all configuration structs // Returns map where key is "packageName.StructName" (e.g., "golang.CatalogerConfig") func DiscoverConfigs(repoRoot string) (map[string]ConfigInfo, error) { @@ -160,6 +165,9 @@ func extractCatalogerConfigFieldsRecursive(structType *ast.StructType, visitedTy if len(field.Names) == 0 { // this is an embedded field - resolve and extract its fields embeddedFields := resolveEmbeddedStructFields(field.Type, visitedTypes) + if only := embeddedFieldAllowlist(field.Doc); only != nil { + embeddedFields = slices.DeleteFunc(embeddedFields, func(f ConfigField) bool { return !only[f.Name] }) + } fields = append(fields, embeddedFields...) continue } @@ -195,6 +203,26 @@ func extractCatalogerConfigFieldsRecursive(structType *ast.StructType, visitedTy return fields } +// embeddedFieldAllowlist returns the field names from a "capabilities-fields:" annotation, or nil when there +// is none (every exported embedded field is listed). +func embeddedFieldAllowlist(commentGroup *ast.CommentGroup) map[string]bool { + if commentGroup == nil { + return nil + } + for _, comment := range commentGroup.List { + matches := embeddedFieldsAnnotationPattern.FindStringSubmatch(comment.Text) + if len(matches) < 2 { + continue + } + only := make(map[string]bool) + for _, name := range strings.Split(matches[1], ",") { + only[strings.TrimSpace(name)] = true + } + return only + } + return nil +} + // resolveEmbeddedStructFields resolves an embedded struct type and extracts its fields recursively func resolveEmbeddedStructFields(fieldType ast.Expr, visitedTypes map[string]bool) []ConfigField { // extract the type name from the expression diff --git a/internal/capabilities/generate/discover_cataloger_configs_test.go b/internal/capabilities/generate/discover_cataloger_configs_test.go index f22bf7b9f..9add0a439 100644 --- a/internal/capabilities/generate/discover_cataloger_configs_test.go +++ b/internal/capabilities/generate/discover_cataloger_configs_test.go @@ -457,3 +457,32 @@ func TestDiscoverAllowedConfigStructs(t *testing.T) { t.Logf(" - %s", configType) } } + +func TestEmbeddedFieldAllowlist(t *testing.T) { + comments := func(lines ...string) *ast.CommentGroup { + g := &ast.CommentGroup{} + for _, l := range lines { + g.List = append(g.List, &ast.Comment{Text: l}) + } + return g + } + + tests := []struct { + name string + doc *ast.CommentGroup + want map[string]bool + }{ + {name: "no doc lists every field", doc: nil, want: nil}, + {name: "no annotation lists every field", doc: comments("// Deprecated: use something else."), want: nil}, + { + name: "annotation limits to the named fields", + doc: comments("// Deprecated: use something else.", "// capabilities-fields: IncludeIndexedArchives, IncludeUnindexedArchives"), + want: map[string]bool{"IncludeIndexedArchives": true, "IncludeUnindexedArchives": true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, embeddedFieldAllowlist(tt.doc)) + }) + } +} diff --git a/syft/pkg/cataloger/java/capabilities.yaml b/syft/pkg/cataloger/java/capabilities.yaml index e651f4a75..84469f497 100644 --- a/syft/pkg/cataloger/java/capabilities.yaml +++ b/syft/pkg/cataloger/java/capabilities.yaml @@ -7,12 +7,6 @@ configs: # AUTO-GENERATED - config structs and their fields description: IncludeIndexedArchives indicates whether to search within indexed archive files (e.g., .zip). - key: IncludeUnindexedArchives description: IncludeUnindexedArchives indicates whether to search within unindexed archive files (e.g., .tar*). - - key: MaxDepth - description: 'MaxDepth is how many levels of nested archives to catalog: 0 disables it, negative goes as deep as 16 levels.' - - key: MaxMemoryBytes - description: MaxMemoryBytes bounds the archive content held in memory at once; content that does not fit is written to disk. Zero holds nothing in memory, negative is unbounded. - - key: MaxDiskBytes - description: MaxDiskBytes bounds the archive content written to disk at once; an archive whose content does not fit is skipped or truncated. Zero writes nothing to disk, negative is unbounded. MaxMemoryBytes and MaxDiskBytes cannot both be zero while MaxDepth is not. - key: UseNetwork description: UseNetwork enables network operations for java package metadata enrichment, such as fetching parent POMs and license information. app_key: java.use-network diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index d2b437e0e..3568e3526 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -20,7 +20,7 @@ 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") + log.Warn("java archive cataloger ignores nested archive depth and limits, set them with syft.CreateSBOMConfig.Archive") } gap := newGenericArchiveParserAdapter(cfg) diff --git a/syft/pkg/cataloger/java/config.go b/syft/pkg/cataloger/java/config.go index a388e9e09..a67033640 100644 --- a/syft/pkg/cataloger/java/config.go +++ b/syft/pkg/cataloger/java/config.go @@ -11,6 +11,7 @@ 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. + // capabilities-fields: IncludeIndexedArchives, IncludeUnindexedArchives cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"` // UseNetwork enables network operations for java package metadata enrichment, such as fetching parent POMs and license information.