fix: keep unread nested archive limits out of java capabilities

- java embeds the shared `cataloging.ArchiveSearchConfig` but only reads the two `Include*` booleans,
  and setting depth or byte limits there fails `CreateSBOM`
- a `capabilities-fields:` annotation on an embedded struct now limits which of its fields are
  listed, so the docs stop advertising settings that error
- building the java cataloger directly with those fields set now warns instead of debug logging

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-10-21 11:45:41 -04:00
parent 36d417d52a
commit 72835436c4
5 changed files with 59 additions and 7 deletions
@@ -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
@@ -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))
})
}
}
@@ -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
+1 -1
View File
@@ -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)
+1
View File
@@ -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.