wire up cli config

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-12-10 12:53:41 -05:00
parent 41aa6f6753
commit a05608a4c8
16 changed files with 463 additions and 175 deletions

View File

@ -147,6 +147,13 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
c.Content.SkipFilesAboveSize = cfg.File.Content.SkipFilesAboveSize c.Content.SkipFilesAboveSize = cfg.File.Content.SkipFilesAboveSize
c.Executable.Globs = cfg.File.Executable.Globs 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 return c
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/anchore/clio" "github.com/anchore/clio"
intFile "github.com/anchore/syft/internal/file" intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/file/cataloger/executable"
) )
type fileConfig struct { type fileConfig struct {
@ -28,11 +29,27 @@ type fileContent struct {
} }
type fileExecutable 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 { func defaultFileConfig() fileConfig {
return fileConfig{ api := executable.DefaultConfig()
// start with API defaults and override CLI-specific values
cfg := fileConfig{
Metadata: fileMetadata{ Metadata: fileMetadata{
Selection: file.FilesOwnedByPackageSelection, Selection: file.FilesOwnedByPackageSelection,
Digests: []string{"sha1", "sha256"}, Digests: []string{"sha1", "sha256"},
@ -41,9 +58,19 @@ func defaultFileConfig() fileConfig {
SkipFilesAboveSize: 250 * intFile.KB, SkipFilesAboveSize: 250 * intFile.KB,
}, },
Executable: fileExecutable{ 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 { var _ interface {
@ -64,7 +91,7 @@ func (c *fileConfig) PostLoad() error {
} }
func (c *fileConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { 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: Options include:
- "all": capture all files from the search space - "all": capture all files from the search space
- "owned-by-package": capture only files owned by packages - "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.Content.Globs, `file globs for the cataloger to match on`)
descriptions.Add(&c.Executable.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")`)
} }

View File

@ -31,7 +31,8 @@ func TestAllPackageCatalogersReachableInTasks(t *testing.T) {
taskFactories := task.DefaultPackageTaskFactories() taskFactories := task.DefaultPackageTaskFactories()
taskTagsByName := make(map[string][]string) taskTagsByName := make(map[string][]string)
for _, factory := range taskFactories { for _, factory := range taskFactories {
tsk := factory(task.DefaultCatalogingFactoryConfig()) tsk, err := factory(task.DefaultCatalogingFactoryConfig())
require.NoError(t, err)
if taskTagsByName[tsk.Name()] != nil { if taskTagsByName[tsk.Name()] != nil {
t.Fatalf("duplicate task name: %q", tsk.Name()) t.Fatalf("duplicate task name: %q", tsk.Name())
} }

View File

@ -1,6 +1,7 @@
package task package task
import ( import (
"errors"
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
@ -8,7 +9,7 @@ import (
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
) )
type factory func(cfg CatalogingFactoryConfig) Task type factory func(cfg CatalogingFactoryConfig) (Task, error)
type Factories []factory type Factories []factory
@ -16,9 +17,13 @@ func (f Factories) Tasks(cfg CatalogingFactoryConfig) ([]Task, error) {
var allTasks []Task var allTasks []Task
taskNames := strset.New() taskNames := strset.New()
duplicateTaskNames := strset.New() duplicateTaskNames := strset.New()
var err error var errs []error
for _, fact := range f { for _, fact := range f {
tsk := fact(cfg) tsk, err := fact(cfg)
if err != nil {
errs = append(errs, err)
continue
}
if tsk == nil { if tsk == nil {
continue continue
} }
@ -33,8 +38,8 @@ func (f Factories) Tasks(cfg CatalogingFactoryConfig) ([]Task, error) {
if duplicateTaskNames.Size() > 0 { if duplicateTaskNames.Size() > 0 {
names := duplicateTaskNames.List() names := duplicateTaskNames.List()
sort.Strings(names) 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...)
} }

View File

@ -26,8 +26,8 @@ func DefaultFileTaskFactories() Factories {
} }
func newFileDigestCatalogerTaskFactory(tags ...string) factory { func newFileDigestCatalogerTaskFactory(tags ...string) factory {
return func(cfg CatalogingFactoryConfig) Task { return func(cfg CatalogingFactoryConfig) (Task, error) {
return newFileDigestCatalogerTask(cfg.FilesConfig.Selection, cfg.FilesConfig.Hashers, tags...) 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 { func newFileMetadataCatalogerTaskFactory(tags ...string) factory {
return func(cfg CatalogingFactoryConfig) Task { return func(cfg CatalogingFactoryConfig) (Task, error) {
return newFileMetadataCatalogerTask(cfg.FilesConfig.Selection, tags...) return newFileMetadataCatalogerTask(cfg.FilesConfig.Selection, tags...), nil
} }
} }
@ -88,8 +88,8 @@ func newFileMetadataCatalogerTask(selection file.Selection, tags ...string) Task
} }
func newFileContentCatalogerTaskFactory(tags ...string) factory { func newFileContentCatalogerTaskFactory(tags ...string) factory {
return func(cfg CatalogingFactoryConfig) Task { return func(cfg CatalogingFactoryConfig) (Task, error) {
return newFileContentCatalogerTask(cfg.FilesConfig.Content, tags...) return newFileContentCatalogerTask(cfg.FilesConfig.Content, tags...), nil
} }
} }
@ -114,12 +114,16 @@ func newFileContentCatalogerTask(cfg filecontent.Config, tags ...string) Task {
} }
func newExecutableCatalogerTaskFactory(tags ...string) factory { 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...) 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 { fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
if selection == file.NoFilesSelection { if selection == file.NoFilesSelection {
return nil return nil
@ -136,7 +140,7 @@ func newExecutableCatalogerTask(selection file.Selection, cfg executable.Config,
return err 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 // TODO: this should be replaced with a fix that allows passing a coordinate or location iterator to the cataloger

View File

@ -21,14 +21,14 @@ import (
) )
func newPackageTaskFactory(catalogerFactory func(CatalogingFactoryConfig) pkg.Cataloger, tags ...string) factory { func newPackageTaskFactory(catalogerFactory func(CatalogingFactoryConfig) pkg.Cataloger, tags ...string) factory {
return func(cfg CatalogingFactoryConfig) Task { return func(cfg CatalogingFactoryConfig) (Task, error) {
return NewPackageTask(cfg, catalogerFactory(cfg), tags...) return NewPackageTask(cfg, catalogerFactory(cfg), tags...), nil
} }
} }
func newSimplePackageTaskFactory(catalogerFactory func() pkg.Cataloger, tags ...string) factory { func newSimplePackageTaskFactory(catalogerFactory func() pkg.Cataloger, tags ...string) factory {
return func(cfg CatalogingFactoryConfig) Task { return func(cfg CatalogingFactoryConfig) (Task, error) {
return NewPackageTask(cfg, catalogerFactory(), tags...) return NewPackageTask(cfg, catalogerFactory(), tags...), nil
} }
} }

View File

@ -1285,7 +1285,7 @@
"type": "string" "type": "string"
}, },
"type": "array", "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": { "toolchains": {
"items": { "items": {
@ -4238,13 +4238,16 @@
"Toolchain": { "Toolchain": {
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string",
"description": "Name is the name of the toolchain (e.g., \"gcc\", \"clang\", \"ld\", etc.)."
}, },
"version": { "version": {
"type": "string" "type": "string",
"description": "Version is the version of the toolchain."
}, },
"kind": { "kind": {
"type": "string" "type": "string",
"description": "Kind indicates the type of toolchain (e.g., compiler, linker, runtime)."
} }
}, },
"type": "object", "type": "object",

View File

@ -1285,7 +1285,7 @@
"type": "string" "type": "string"
}, },
"type": "array", "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": { "toolchains": {
"items": { "items": {
@ -4238,13 +4238,16 @@
"Toolchain": { "Toolchain": {
"properties": { "properties": {
"name": { "name": {
"type": "string" "type": "string",
"description": "Name is the name of the toolchain (e.g., \"gcc\", \"clang\", \"ld\", etc.)."
}, },
"version": { "version": {
"type": "string" "type": "string",
"description": "Version is the version of the toolchain."
}, },
"kind": { "kind": {
"type": "string" "type": "string",
"description": "Kind indicates the type of toolchain (e.g., compiler, linker, runtime)."
} }
}, },
"type": "object", "type": "object",

View File

@ -24,20 +24,11 @@ import (
"github.com/anchore/syft/syft/internal/unionreader" "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 SymbolCaptureScope string
// type SymbolTypes string const SymbolScopeGolang SymbolCaptureScope = "golang" // only binaries built with the golang toolchain
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"
)
type Config struct { type Config struct {
// MIMETypes are the MIME types that will be considered for executable cataloging. // 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"` 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 { type Cataloger struct {
config Config config Config
} }
@ -101,10 +150,8 @@ func DefaultConfig() Config {
MIMETypes: m, MIMETypes: m,
Globs: nil, Globs: nil,
Symbols: SymbolConfig{ Symbols: SymbolConfig{
CaptureScope: []SymbolCaptureScope{ CaptureScope: []SymbolCaptureScope{}, // important! by default we do not capture any symbols unless explicitly configured
SymbolScopeGolang, Types: []string{"T"}, // by default only capture "T" (text/code) symbols, since vulnerability data tracks accessible function symbols
},
Types: []string{"T", "t"},
Go: GoSymbolConfig{ Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,

View File

@ -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)
})
}
}

View File

@ -21,6 +21,21 @@ var goNMTypes = []string{
"U", // referenced but undefined symbol "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 ( const (
vendorPrefix = "vendor/" vendorPrefix = "vendor/"
extendedStdlibPrefix = "golang.org/x/" extendedStdlibPrefix = "golang.org/x/"

View File

@ -11,20 +11,7 @@ func shouldCaptureSymbols(data *file.Executable, cfg SymbolConfig) bool {
} }
for _, scope := range cfg.CaptureScope { for _, scope := range cfg.CaptureScope {
switch scope { switch scope { //nolint:gocritic // lets elect a pattern as if we'll have multiple options in the future...
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
}
case SymbolScopeGolang: case SymbolScopeGolang:
if hasGolangToolchain(data) { if hasGolangToolchain(data) {
return true return true

View File

@ -19,7 +19,7 @@ func TestShouldCaptureSymbols(t *testing.T) {
name: "nil data returns false", name: "nil data returns false",
data: nil, data: nil,
cfg: SymbolConfig{ cfg: SymbolConfig{
CaptureScope: []SymbolCaptureScope{SymbolScopeAll}, CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
}, },
want: false, want: false,
}, },
@ -31,62 +31,6 @@ func TestShouldCaptureSymbols(t *testing.T) {
}, },
want: false, 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", name: "scope golang with go toolchain returns true",
data: &file.Executable{ data: &file.Executable{
@ -119,38 +63,6 @@ func TestShouldCaptureSymbols(t *testing.T) {
}, },
want: false, 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", name: "go toolchain among multiple toolchains returns true",
data: &file.Executable{ data: &file.Executable{
@ -210,7 +122,6 @@ func TestHasGolangToolchain(t *testing.T) {
Toolchains: []file.Toolchain{ Toolchains: []file.Toolchain{
{Name: "gcc", Version: "12.0.0", Kind: file.ToolchainKindCompiler}, {Name: "gcc", Version: "12.0.0", Kind: file.ToolchainKindCompiler},
{Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler}, {Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler},
{Name: "ld", Version: "2.38", Kind: file.ToolchainKindLinker},
}, },
}, },
want: true, want: true,

View File

@ -1,5 +1,4 @@
# Stage 1: Build binaries for multiple platforms FROM golang:1.24
FROM golang:1.24 AS builder
WORKDIR /app WORKDIR /app
@ -8,18 +7,8 @@ RUN go mod download
COPY main.go ./ 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 .
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 /

View File

@ -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

View File

@ -7,15 +7,13 @@ type (
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary. // RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
RelocationReadOnly string 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 ToolchainKind string
) )
const ( const (
ToolchainKindCompiler ToolchainKind = "compiler" ToolchainKindCompiler ToolchainKind = "compiler"
ToolchainKindLinker ToolchainKind = "linker"
ToolchainKindRuntime ToolchainKind = "runtime"
ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems
MachO ExecutableFormat = "macho" // Mach object file format used on macOS and iOS 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"` ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
// Symbols captures the selection from the symbol table found in the binary. // 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"` 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. // 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 { type Toolchain struct {
Name string `json:"name" yaml:"name" mapstructure:"name"` // Name is the name of the toolchain (e.g., "gcc", "clang", "ld", etc.).
Version string `json:"version,omitempty" yaml:"version,omitempty" mapstructure:"version"` Name string `json:"name" yaml:"name" mapstructure:"name"`
Kind ToolchainKind `json:"kind" yaml:"kind" mapstructure:"kind"`
// 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. // ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.