diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go index ff491b66b..bd606c09b 100644 --- a/cmd/syft/internal/options/catalog.go +++ b/cmd/syft/internal/options/catalog.go @@ -147,6 +147,13 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config { c.Content.SkipFilesAboveSize = cfg.File.Content.SkipFilesAboveSize c.Executable.Globs = cfg.File.Executable.Globs + // symbol capture configuration + c.Executable.Symbols.CaptureScope = cfg.File.Executable.Symbols.CaptureScope + c.Executable.Symbols.Types = cfg.File.Executable.Symbols.Types + c.Executable.Symbols.Go.StandardLibrary = cfg.File.Executable.Symbols.Go.StandardLibrary + c.Executable.Symbols.Go.ExtendedStandardLibrary = cfg.File.Executable.Symbols.Go.ExtendedStandardLibrary + c.Executable.Symbols.Go.ThirdPartyModules = cfg.File.Executable.Symbols.Go.ThirdPartyModules + return c } diff --git a/cmd/syft/internal/options/file.go b/cmd/syft/internal/options/file.go index 6ac9c8d2b..44d660c8b 100644 --- a/cmd/syft/internal/options/file.go +++ b/cmd/syft/internal/options/file.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/clio" intFile "github.com/anchore/syft/internal/file" "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/file/cataloger/executable" ) type fileConfig struct { @@ -28,11 +29,27 @@ type fileContent struct { } type fileExecutable struct { - Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"` + Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"` + Symbols fileSymbolConfig `yaml:"symbols" json:"symbols" mapstructure:"symbols"` +} + +type fileSymbolConfig struct { + CaptureScope []executable.SymbolCaptureScope `yaml:"capture" json:"capture" mapstructure:"capture"` + Types []string `yaml:"types" json:"types" mapstructure:"types"` + Go fileGoSymbolConfig `yaml:"go" json:"go" mapstructure:"go"` +} + +type fileGoSymbolConfig struct { + StandardLibrary bool `yaml:"standard-library" json:"standard-library" mapstructure:"standard-library"` + ExtendedStandardLibrary bool `yaml:"extended-standard-library" json:"extended-standard-library" mapstructure:"extended-standard-library"` + ThirdPartyModules bool `yaml:"third-party-modules" json:"third-party-modules" mapstructure:"third-party-modules"` } func defaultFileConfig() fileConfig { - return fileConfig{ + api := executable.DefaultConfig() + + // start with API defaults and override CLI-specific values + cfg := fileConfig{ Metadata: fileMetadata{ Selection: file.FilesOwnedByPackageSelection, Digests: []string{"sha1", "sha256"}, @@ -41,9 +58,19 @@ func defaultFileConfig() fileConfig { SkipFilesAboveSize: 250 * intFile.KB, }, Executable: fileExecutable{ - Globs: nil, + Globs: api.Globs, + Symbols: fileSymbolConfig{ + CaptureScope: api.Symbols.CaptureScope, + Types: api.Symbols.Types, + Go: fileGoSymbolConfig{ + StandardLibrary: api.Symbols.Go.StandardLibrary, + ExtendedStandardLibrary: api.Symbols.Go.ExtendedStandardLibrary, + ThirdPartyModules: api.Symbols.Go.ThirdPartyModules, + }, + }, }, } + return cfg } var _ interface { @@ -64,7 +91,7 @@ func (c *fileConfig) PostLoad() error { } func (c *fileConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { - descriptions.Add(&c.Metadata.Selection, `select which files should be captured by the file-metadata cataloger and included in the SBOM. + descriptions.Add(&c.Metadata.Selection, `select which files should be captured by the file-metadata cataloger and included in the SBOM. Options include: - "all": capture all files from the search space - "owned-by-package": capture only files owned by packages @@ -75,4 +102,13 @@ Options include: descriptions.Add(&c.Content.Globs, `file globs for the cataloger to match on`) descriptions.Add(&c.Executable.Globs, `file globs for the cataloger to match on`) + + // symbol capture configuration + descriptions.Add(&c.Executable.Symbols.CaptureScope, `the scope of symbols to capture from executables (options: "golang")`) + descriptions.Add(&c.Executable.Symbols.Types, `the types of symbols to capture, relative to "go tool nm" output (options: "T", "t", "R", "r", "D", "d", "B", "b", "C", "U")`) + + // go symbol configuration + descriptions.Add(&c.Executable.Symbols.Go.StandardLibrary, `capture Go standard library symbols (e.g. "fmt", "net/http")`) + descriptions.Add(&c.Executable.Symbols.Go.ExtendedStandardLibrary, `capture extended Go standard library symbols (e.g. "golang.org/x/net")`) + descriptions.Add(&c.Executable.Symbols.Go.ThirdPartyModules, `capture third-party module symbols (e.g. "github.com/spf13/cobra")`) } diff --git a/cmd/syft/internal/test/integration/package_catalogers_represented_test.go b/cmd/syft/internal/test/integration/package_catalogers_represented_test.go index ec7452eb8..329236644 100644 --- a/cmd/syft/internal/test/integration/package_catalogers_represented_test.go +++ b/cmd/syft/internal/test/integration/package_catalogers_represented_test.go @@ -31,7 +31,8 @@ func TestAllPackageCatalogersReachableInTasks(t *testing.T) { taskFactories := task.DefaultPackageTaskFactories() taskTagsByName := make(map[string][]string) for _, factory := range taskFactories { - tsk := factory(task.DefaultCatalogingFactoryConfig()) + tsk, err := factory(task.DefaultCatalogingFactoryConfig()) + require.NoError(t, err) if taskTagsByName[tsk.Name()] != nil { t.Fatalf("duplicate task name: %q", tsk.Name()) } diff --git a/internal/task/factory.go b/internal/task/factory.go index c51c1975b..ffae1605a 100644 --- a/internal/task/factory.go +++ b/internal/task/factory.go @@ -1,6 +1,7 @@ package task import ( + "errors" "fmt" "sort" "strings" @@ -8,7 +9,7 @@ import ( "github.com/scylladb/go-set/strset" ) -type factory func(cfg CatalogingFactoryConfig) Task +type factory func(cfg CatalogingFactoryConfig) (Task, error) type Factories []factory @@ -16,9 +17,13 @@ func (f Factories) Tasks(cfg CatalogingFactoryConfig) ([]Task, error) { var allTasks []Task taskNames := strset.New() duplicateTaskNames := strset.New() - var err error + var errs []error for _, fact := range f { - tsk := fact(cfg) + tsk, err := fact(cfg) + if err != nil { + errs = append(errs, err) + continue + } if tsk == nil { continue } @@ -33,8 +38,8 @@ func (f Factories) Tasks(cfg CatalogingFactoryConfig) ([]Task, error) { if duplicateTaskNames.Size() > 0 { names := duplicateTaskNames.List() sort.Strings(names) - err = fmt.Errorf("duplicate cataloger task names: %v", strings.Join(names, ", ")) + errs = append(errs, fmt.Errorf("duplicate cataloger task names: %v", strings.Join(names, ", "))) } - return allTasks, err + return allTasks, errors.Join(errs...) } diff --git a/internal/task/file_tasks.go b/internal/task/file_tasks.go index 7af11fcd1..c904e97cb 100644 --- a/internal/task/file_tasks.go +++ b/internal/task/file_tasks.go @@ -26,8 +26,8 @@ func DefaultFileTaskFactories() Factories { } func newFileDigestCatalogerTaskFactory(tags ...string) factory { - return func(cfg CatalogingFactoryConfig) Task { - return newFileDigestCatalogerTask(cfg.FilesConfig.Selection, cfg.FilesConfig.Hashers, tags...) + return func(cfg CatalogingFactoryConfig) (Task, error) { + return newFileDigestCatalogerTask(cfg.FilesConfig.Selection, cfg.FilesConfig.Hashers, tags...), nil } } @@ -57,8 +57,8 @@ func newFileDigestCatalogerTask(selection file.Selection, hashers []crypto.Hash, } func newFileMetadataCatalogerTaskFactory(tags ...string) factory { - return func(cfg CatalogingFactoryConfig) Task { - return newFileMetadataCatalogerTask(cfg.FilesConfig.Selection, tags...) + return func(cfg CatalogingFactoryConfig) (Task, error) { + return newFileMetadataCatalogerTask(cfg.FilesConfig.Selection, tags...), nil } } @@ -88,8 +88,8 @@ func newFileMetadataCatalogerTask(selection file.Selection, tags ...string) Task } func newFileContentCatalogerTaskFactory(tags ...string) factory { - return func(cfg CatalogingFactoryConfig) Task { - return newFileContentCatalogerTask(cfg.FilesConfig.Content, tags...) + return func(cfg CatalogingFactoryConfig) (Task, error) { + return newFileContentCatalogerTask(cfg.FilesConfig.Content, tags...), nil } } @@ -114,12 +114,16 @@ func newFileContentCatalogerTask(cfg filecontent.Config, tags ...string) Task { } func newExecutableCatalogerTaskFactory(tags ...string) factory { - return func(cfg CatalogingFactoryConfig) Task { + return func(cfg CatalogingFactoryConfig) (Task, error) { return newExecutableCatalogerTask(cfg.FilesConfig.Selection, cfg.FilesConfig.Executable, tags...) } } -func newExecutableCatalogerTask(selection file.Selection, cfg executable.Config, tags ...string) Task { +func newExecutableCatalogerTask(selection file.Selection, cfg executable.Config, tags ...string) (Task, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error { if selection == file.NoFilesSelection { return nil @@ -136,7 +140,7 @@ func newExecutableCatalogerTask(selection file.Selection, cfg executable.Config, return err } - return NewTask("file-executable-cataloger", fn, commonFileTags(tags)...) + return NewTask("file-executable-cataloger", fn, commonFileTags(tags)...), nil } // TODO: this should be replaced with a fix that allows passing a coordinate or location iterator to the cataloger diff --git a/internal/task/package_task_factory.go b/internal/task/package_task_factory.go index 0e83bf2ca..0f3f3218b 100644 --- a/internal/task/package_task_factory.go +++ b/internal/task/package_task_factory.go @@ -21,14 +21,14 @@ import ( ) func newPackageTaskFactory(catalogerFactory func(CatalogingFactoryConfig) pkg.Cataloger, tags ...string) factory { - return func(cfg CatalogingFactoryConfig) Task { - return NewPackageTask(cfg, catalogerFactory(cfg), tags...) + return func(cfg CatalogingFactoryConfig) (Task, error) { + return NewPackageTask(cfg, catalogerFactory(cfg), tags...), nil } } func newSimplePackageTaskFactory(catalogerFactory func() pkg.Cataloger, tags ...string) factory { - return func(cfg CatalogingFactoryConfig) Task { - return NewPackageTask(cfg, catalogerFactory(), tags...) + return func(cfg CatalogingFactoryConfig) (Task, error) { + return NewPackageTask(cfg, catalogerFactory(), tags...), nil } } diff --git a/schema/json/schema-16.1.1.json b/schema/json/schema-16.1.1.json index 606dd8abb..a4146954e 100644 --- a/schema/json/schema-16.1.1.json +++ b/schema/json/schema-16.1.1.json @@ -1285,7 +1285,7 @@ "type": "string" }, "type": "array", - "description": "Symbols captures the selection from the symbol table found in the binary.\nSymbols []Symbol `json:\"symbols,omitempty\" yaml:\"symbols\" mapstructure:\"symbols\"`" + "description": "Symbols captures the selection from the symbol table found in the binary." }, "toolchains": { "items": { @@ -4238,13 +4238,16 @@ "Toolchain": { "properties": { "name": { - "type": "string" + "type": "string", + "description": "Name is the name of the toolchain (e.g., \"gcc\", \"clang\", \"ld\", etc.)." }, "version": { - "type": "string" + "type": "string", + "description": "Version is the version of the toolchain." }, "kind": { - "type": "string" + "type": "string", + "description": "Kind indicates the type of toolchain (e.g., compiler, linker, runtime)." } }, "type": "object", diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index 606dd8abb..a4146954e 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1285,7 +1285,7 @@ "type": "string" }, "type": "array", - "description": "Symbols captures the selection from the symbol table found in the binary.\nSymbols []Symbol `json:\"symbols,omitempty\" yaml:\"symbols\" mapstructure:\"symbols\"`" + "description": "Symbols captures the selection from the symbol table found in the binary." }, "toolchains": { "items": { @@ -4238,13 +4238,16 @@ "Toolchain": { "properties": { "name": { - "type": "string" + "type": "string", + "description": "Name is the name of the toolchain (e.g., \"gcc\", \"clang\", \"ld\", etc.)." }, "version": { - "type": "string" + "type": "string", + "description": "Version is the version of the toolchain." }, "kind": { - "type": "string" + "type": "string", + "description": "Kind indicates the type of toolchain (e.g., compiler, linker, runtime)." } }, "type": "object", diff --git a/syft/file/cataloger/executable/cataloger.go b/syft/file/cataloger/executable/cataloger.go index c41359c94..8802eeb1b 100644 --- a/syft/file/cataloger/executable/cataloger.go +++ b/syft/file/cataloger/executable/cataloger.go @@ -24,20 +24,11 @@ import ( "github.com/anchore/syft/syft/internal/unionreader" ) +// SymbolCaptureScope defines the scope of symbols to capture from executables. For the meantime only golang binaries are supported, +// however, in the future this can be expanded to include rust audit binaries, libraries only, applications only, or all binaries. type SymbolCaptureScope string -// type SymbolTypes string - -const ( - SymbolScopeAll SymbolCaptureScope = "all" // any and all binaries - SymbolScopeLibraries SymbolCaptureScope = "libraries" // binaries with exported symbols - SymbolScopeApplications SymbolCaptureScope = "applications" // binaries with an entry point - SymbolScopeGolang SymbolCaptureScope = "golang" // only binaries built with the golang toolchain - SymbolScopeNone SymbolCaptureScope = "none" // do not capture any symbols - - // SymbolTypeCode SymbolTypes = "code" - // SymbolTypeData SymbolTypes = "data" -) +const SymbolScopeGolang SymbolCaptureScope = "golang" // only binaries built with the golang toolchain type Config struct { // MIMETypes are the MIME types that will be considered for executable cataloging. @@ -90,6 +81,64 @@ type GoSymbolConfig struct { UnexportedSymbols bool `json:"unexported-symbols" yaml:"unexported-symbols" mapstructure:"unexported-symbols"` } +// Validate checks for logical configuration inconsistencies and returns an error if any are found. +func (c Config) Validate() error { + return c.Symbols.Validate() +} + +// Validate checks for logical configuration inconsistencies in symbol capture settings. +func (s SymbolConfig) Validate() error { + // validate that all CaptureScope values are valid + for _, scope := range s.CaptureScope { + if !isValidCaptureScope(scope) { + return fmt.Errorf("invalid symbol capture scope %q: valid values are %q", scope, SymbolScopeGolang) + } + } + + // validate NM types if specified + if len(s.Types) > 0 { + for _, t := range s.Types { + if !isValidNMType(t) { + return fmt.Errorf("invalid NM type %q: valid values are %v", t, validNMTypes()) + } + } + } + + // remaining validations only apply when Go symbol capture is enabled + if !s.hasGolangScope() { + return nil + } + + // if Go symbol capture is enabled, at least one of exported/unexported must be true + if !s.Go.ExportedSymbols && !s.Go.UnexportedSymbols { + return fmt.Errorf("both exported-symbols and unexported-symbols are disabled; no Go symbols would be captured") + } + + // if Go symbol capture is enabled, at least one module source must be enabled + if !s.Go.StandardLibrary && !s.Go.ExtendedStandardLibrary && !s.Go.ThirdPartyModules { + return fmt.Errorf("all module sources (standard-library, extended-standard-library, third-party-modules) are disabled; no meaningful Go symbols would be captured") + } + + return nil +} + +func (s SymbolConfig) hasGolangScope() bool { + for _, scope := range s.CaptureScope { + if scope == SymbolScopeGolang { + return true + } + } + return false +} + +func isValidCaptureScope(scope SymbolCaptureScope) bool { + switch scope { //nolint:gocritic // lets elect a pattern as if we'll have multiple options in the future... + case SymbolScopeGolang: + return true + } + return false +} + type Cataloger struct { config Config } @@ -101,10 +150,8 @@ func DefaultConfig() Config { MIMETypes: m, Globs: nil, Symbols: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{ - SymbolScopeGolang, - }, - Types: []string{"T", "t"}, + CaptureScope: []SymbolCaptureScope{}, // important! by default we do not capture any symbols unless explicitly configured + Types: []string{"T"}, // by default only capture "T" (text/code) symbols, since vulnerability data tracks accessible function symbols Go: GoSymbolConfig{ StandardLibrary: true, ExtendedStandardLibrary: true, diff --git a/syft/file/cataloger/executable/config_test.go b/syft/file/cataloger/executable/config_test.go new file mode 100644 index 000000000..121c5b1f2 --- /dev/null +++ b/syft/file/cataloger/executable/config_test.go @@ -0,0 +1,273 @@ +package executable + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" +) + +func TestDefaultConfig_SymbolCaptureIsDisabled(t *testing.T) { + // symbol capture should be disabled by default -- this is an expensive operation space-wise in the SBOM + // and should only be enabled when explicitly configured by the user. + cfg := DefaultConfig() + + require.Empty(t, cfg.Symbols.CaptureScope, "symbol capture should be disabled by default (empty capture scope)") + + // verify that shouldCaptureSymbols returns false for any executable when using default config + assert.False(t, shouldCaptureSymbols(nil, cfg.Symbols), "should not capture symbols for nil executable") + assert.False(t, shouldCaptureSymbols(&file.Executable{}, cfg.Symbols), "should not capture symbols for empty executable") + assert.False(t, shouldCaptureSymbols(&file.Executable{ + Toolchains: []file.Toolchain{ + {Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler}, + }, + }, cfg.Symbols), "should not capture symbols even for go binaries when using default config") +} + +func TestConfig_Validate(t *testing.T) { + tests := []struct { + name string + cfg Config + wantErr require.ErrorAssertionFunc + }{ + { + name: "default config is valid", + cfg: DefaultConfig(), + wantErr: require.NoError, + }, + { + name: "valid config with golang scope enabled", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + ThirdPartyModules: true, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "empty capture scope with Go settings is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + ThirdPartyModules: true, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "invalid capture scope", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{"invalid-scope"}, + }, + }, + wantErr: require.Error, + }, + { + name: "invalid NM type", + cfg: Config{ + Symbols: SymbolConfig{ + Types: []string{"X", "Y"}, + }, + }, + wantErr: require.Error, + }, + { + name: "valid NM types", + cfg: Config{ + Symbols: SymbolConfig{ + Types: []string{"T", "t", "R"}, + }, + }, + wantErr: require.NoError, + }, + { + name: "both exported and unexported disabled with golang scope", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: false, + UnexportedSymbols: false, + ThirdPartyModules: true, + }, + }, + }, + wantErr: require.Error, + }, + { + name: "both exported and unexported disabled without golang scope is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{}, + Go: GoSymbolConfig{ + ExportedSymbols: false, + UnexportedSymbols: false, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "all module sources disabled with golang scope", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + StandardLibrary: false, + ExtendedStandardLibrary: false, + ThirdPartyModules: false, + }, + }, + }, + wantErr: require.Error, + }, + { + name: "all module sources disabled without golang scope is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + StandardLibrary: false, + ExtendedStandardLibrary: false, + ThirdPartyModules: false, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "only standard library enabled is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + StandardLibrary: true, + ExtendedStandardLibrary: false, + ThirdPartyModules: false, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "only extended stdlib enabled is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + StandardLibrary: false, + ExtendedStandardLibrary: true, + ThirdPartyModules: false, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "only third party modules enabled is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + StandardLibrary: false, + ExtendedStandardLibrary: false, + ThirdPartyModules: true, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "only unexported symbols enabled is valid", + cfg: Config{ + Symbols: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: false, + UnexportedSymbols: true, + ThirdPartyModules: true, + }, + }, + }, + wantErr: require.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + tt.wantErr(t, err) + }) + } +} + +func TestSymbolConfig_Validate_ErrorMessages(t *testing.T) { + tests := []struct { + name string + cfg SymbolConfig + wantErrContain string + }{ + { + name: "invalid capture scope error message", + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{"rust"}, + }, + wantErrContain: "invalid symbol capture scope", + }, + { + name: "invalid NM type error message", + cfg: SymbolConfig{ + Types: []string{"Z"}, + }, + wantErrContain: "invalid NM type", + }, + { + name: "both export options disabled error message", + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: false, + UnexportedSymbols: false, + ThirdPartyModules: true, + }, + }, + wantErrContain: "both exported-symbols and unexported-symbols are disabled", + }, + { + name: "all module sources disabled error message", + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + StandardLibrary: false, + ExtendedStandardLibrary: false, + ThirdPartyModules: false, + }, + }, + wantErrContain: "all module sources", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErrContain) + }) + } +} diff --git a/syft/file/cataloger/executable/go_symbols.go b/syft/file/cataloger/executable/go_symbols.go index e0ef7db99..84e35f18d 100644 --- a/syft/file/cataloger/executable/go_symbols.go +++ b/syft/file/cataloger/executable/go_symbols.go @@ -21,6 +21,21 @@ var goNMTypes = []string{ "U", // referenced but undefined symbol } +// validNMTypes returns the list of valid NM types for Go symbols. +func validNMTypes() []string { + return goNMTypes +} + +// isValidNMType checks if the given type is a valid NM type. +func isValidNMType(t string) bool { + for _, valid := range goNMTypes { + if t == valid { + return true + } + } + return false +} + const ( vendorPrefix = "vendor/" extendedStdlibPrefix = "golang.org/x/" diff --git a/syft/file/cataloger/executable/symbols.go b/syft/file/cataloger/executable/symbols.go index 34eac1aa6..69c0fa2d5 100644 --- a/syft/file/cataloger/executable/symbols.go +++ b/syft/file/cataloger/executable/symbols.go @@ -11,20 +11,7 @@ func shouldCaptureSymbols(data *file.Executable, cfg SymbolConfig) bool { } for _, scope := range cfg.CaptureScope { - switch scope { - case SymbolScopeNone: - // explicit "none" means don't capture (but continue checking other scopes) - continue - case SymbolScopeAll: - return true - case SymbolScopeLibraries: - if data.HasExports { - return true - } - case SymbolScopeApplications: - if data.HasEntrypoint { - return true - } + switch scope { //nolint:gocritic // lets elect a pattern as if we'll have multiple options in the future... case SymbolScopeGolang: if hasGolangToolchain(data) { return true diff --git a/syft/file/cataloger/executable/symbols_test.go b/syft/file/cataloger/executable/symbols_test.go index b3d99c185..f5b8e5a56 100644 --- a/syft/file/cataloger/executable/symbols_test.go +++ b/syft/file/cataloger/executable/symbols_test.go @@ -19,7 +19,7 @@ func TestShouldCaptureSymbols(t *testing.T) { name: "nil data returns false", data: nil, cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeAll}, + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, }, want: false, }, @@ -31,62 +31,6 @@ func TestShouldCaptureSymbols(t *testing.T) { }, want: false, }, - { - name: "scope none returns false", - data: &file.Executable{}, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeNone}, - }, - want: false, - }, - { - name: "scope all returns true", - data: &file.Executable{}, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeAll}, - }, - want: true, - }, - { - name: "scope libraries with exports returns true", - data: &file.Executable{ - HasExports: true, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeLibraries}, - }, - want: true, - }, - { - name: "scope libraries without exports returns false", - data: &file.Executable{ - HasExports: false, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeLibraries}, - }, - want: false, - }, - { - name: "scope applications with entrypoint returns true", - data: &file.Executable{ - HasEntrypoint: true, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeApplications}, - }, - want: true, - }, - { - name: "scope applications without entrypoint returns false", - data: &file.Executable{ - HasEntrypoint: false, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeApplications}, - }, - want: false, - }, { name: "scope golang with go toolchain returns true", data: &file.Executable{ @@ -119,38 +63,6 @@ func TestShouldCaptureSymbols(t *testing.T) { }, want: false, }, - { - name: "multiple scopes with one match returns true", - data: &file.Executable{ - HasExports: false, - HasEntrypoint: true, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeLibraries, SymbolScopeApplications}, - }, - want: true, - }, - { - name: "multiple scopes with no match returns false", - data: &file.Executable{ - HasExports: false, - HasEntrypoint: false, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeLibraries, SymbolScopeApplications}, - }, - want: false, - }, - { - name: "none scope followed by matching scope returns true", - data: &file.Executable{ - HasEntrypoint: true, - }, - cfg: SymbolConfig{ - CaptureScope: []SymbolCaptureScope{SymbolScopeNone, SymbolScopeApplications}, - }, - want: true, - }, { name: "go toolchain among multiple toolchains returns true", data: &file.Executable{ @@ -210,7 +122,6 @@ func TestHasGolangToolchain(t *testing.T) { Toolchains: []file.Toolchain{ {Name: "gcc", Version: "12.0.0", Kind: file.ToolchainKindCompiler}, {Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler}, - {Name: "ld", Version: "2.38", Kind: file.ToolchainKindLinker}, }, }, want: true, diff --git a/syft/file/cataloger/executable/test-fixtures/golang/Dockerfile b/syft/file/cataloger/executable/test-fixtures/golang/Dockerfile index 6ec16bcec..b827f2d93 100644 --- a/syft/file/cataloger/executable/test-fixtures/golang/Dockerfile +++ b/syft/file/cataloger/executable/test-fixtures/golang/Dockerfile @@ -1,5 +1,4 @@ -# Stage 1: Build binaries for multiple platforms -FROM golang:1.24 AS builder +FROM golang:1.24 WORKDIR /app @@ -8,18 +7,8 @@ RUN go mod download COPY main.go ./ -# build ELF (Linux) -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o hello_linux . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /hello_linux . -# build Mach-O (macOS) -RUN CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o hello_mac . +RUN CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o /hello_mac . -# build PE (Windows) -RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o hello.exe . - -# Stage 2: Minimal image with just the binaries -FROM scratch - -COPY --from=builder /app/hello_linux / -COPY --from=builder /app/hello_mac / -COPY --from=builder /app/hello.exe / +RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o /hello.exe . diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile index 67845790b..77d938395 100644 --- a/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile @@ -1 +1,6 @@ -FROM silkeh/clang:18@sha256:6984fdf656b270ff3129e339a25083e0f8355f681b72fdeb5407da21a9bbf74d +FROM alpine:3.21 + +RUN apk add --no-cache clang18 musl-dev make + +# create symlink so 'clang' command works (Alpine installs as clang-18) +RUN ln -s /usr/bin/clang-18 /usr/bin/clang diff --git a/syft/file/executable.go b/syft/file/executable.go index 31fd11c70..15705db18 100644 --- a/syft/file/executable.go +++ b/syft/file/executable.go @@ -7,15 +7,13 @@ type ( // RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary. RelocationReadOnly string - // SymbolType string - + // ToolchainKind represents the type of toolchain used to build the executable. Today only "compiler" is supported, + // however, this can be expanded in the future to include linkers, runtimes, etc. ToolchainKind string ) const ( ToolchainKindCompiler ToolchainKind = "compiler" - ToolchainKindLinker ToolchainKind = "linker" - ToolchainKindRuntime ToolchainKind = "runtime" ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems MachO ExecutableFormat = "macho" // Mach object file format used on macOS and iOS @@ -44,7 +42,6 @@ type Executable struct { ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"` // Symbols captures the selection from the symbol table found in the binary. - // Symbols []Symbol `json:"symbols,omitempty" yaml:"symbols" mapstructure:"symbols"` SymbolNames []string `json:"symbolNames,omitempty" yaml:"symbolNames" mapstructure:"symbolNames"` // Toolchains captures information about the compiler, linker, runtime, or other toolchains used to build (or otherwise exist within) the executable. @@ -52,9 +49,14 @@ type Executable struct { } type Toolchain struct { - Name string `json:"name" yaml:"name" mapstructure:"name"` - Version string `json:"version,omitempty" yaml:"version,omitempty" mapstructure:"version"` - Kind ToolchainKind `json:"kind" yaml:"kind" mapstructure:"kind"` + // Name is the name of the toolchain (e.g., "gcc", "clang", "ld", etc.). + Name string `json:"name" yaml:"name" mapstructure:"name"` + + // Version is the version of the toolchain. + Version string `json:"version,omitempty" yaml:"version,omitempty" mapstructure:"version"` + + // Kind indicates the type of toolchain (e.g., compiler, linker, runtime). + Kind ToolchainKind `json:"kind" yaml:"kind" mapstructure:"kind"` } // ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.