From 32946ec41fa4b414a030b0917366f77f7179cb74 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 9 Dec 2025 17:43:38 -0500 Subject: [PATCH] add gcc and clang toolchains Signed-off-by: Alex Goodman --- syft/file/cataloger/executable/cataloger.go | 16 +- syft/file/cataloger/executable/elf.go | 38 +-- syft/file/cataloger/executable/elf_test.go | 76 +++--- syft/file/cataloger/executable/go_symbols.go | 18 +- .../cataloger/executable/go_symbols_test.go | 250 +++++++++++------- syft/file/cataloger/executable/macho.go | 25 +- syft/file/cataloger/executable/macho_test.go | 80 +++--- syft/file/cataloger/executable/symbols.go | 47 ++++ .../file/cataloger/executable/symbols_test.go | 226 ++++++++++++++++ .../test-fixtures/toolchains/.gitignore | 3 + .../test-fixtures/toolchains/Makefile | 15 ++ .../test-fixtures/toolchains/clang/Dockerfile | 1 + .../test-fixtures/toolchains/clang/Makefile | 39 +++ .../toolchains/clang/project/Makefile | 9 + .../toolchains/clang/project/hello.c | 6 + .../test-fixtures/toolchains/gcc/Dockerfile | 1 + .../test-fixtures/toolchains/gcc/Makefile | 39 +++ .../toolchains/gcc/project/Makefile | 9 + .../toolchains/gcc/project/hello.c | 6 + syft/file/cataloger/executable/toolchains.go | 81 ++++++ .../cataloger/executable/toolchains_test.go | 61 +++++ syft/file/executable.go | 16 +- 22 files changed, 806 insertions(+), 256 deletions(-) create mode 100644 syft/file/cataloger/executable/symbols.go create mode 100644 syft/file/cataloger/executable/symbols_test.go create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/.gitignore create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/clang/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/hello.c create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Dockerfile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/Makefile create mode 100644 syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/hello.c create mode 100644 syft/file/cataloger/executable/toolchains.go create mode 100644 syft/file/cataloger/executable/toolchains_test.go diff --git a/syft/file/cataloger/executable/cataloger.go b/syft/file/cataloger/executable/cataloger.go index eb76e1b36..c41359c94 100644 --- a/syft/file/cataloger/executable/cataloger.go +++ b/syft/file/cataloger/executable/cataloger.go @@ -26,7 +26,7 @@ import ( type SymbolCaptureScope string -//type SymbolTypes string +// type SymbolTypes string const ( SymbolScopeAll SymbolCaptureScope = "all" // any and all binaries @@ -35,8 +35,8 @@ const ( SymbolScopeGolang SymbolCaptureScope = "golang" // only binaries built with the golang toolchain SymbolScopeNone SymbolCaptureScope = "none" // do not capture any symbols - //SymbolTypeCode SymbolTypes = "code" - //SymbolTypeData SymbolTypes = "data" + // SymbolTypeCode SymbolTypes = "code" + // SymbolTypeData SymbolTypes = "data" ) type Config struct { @@ -55,16 +55,16 @@ type SymbolConfig struct { // CaptureScope defines the scope of symbols to capture from executables (all binaries, libraries only, applications only, golang binaries only, or none). CaptureScope []SymbolCaptureScope `json:"capture" yaml:"capture" mapstructure:"capture"` + // Types are the types of Go symbols to capture, relative to `go tool nm` output (e.g. T, t, R, r, D, d, B, b, C, U, etc). + // If empty, all symbol types are captured. + Types []string + // Go configures Go-specific symbol capturing settings. Go GoSymbolConfig `json:"go" yaml:"go" mapstructure:"go"` } // GoSymbolConfig holds settings specific to capturing symbols from binaries built with the golang toolchain. type GoSymbolConfig struct { - // Types are the types of Go symbols to capture, relative to `go tool nm` output (e.g. T, t, R, r, D, d, B, b, C, U, etc). - // If empty, all symbol types are captured. - Types []string - // StandardLibrary indicates whether to capture Go standard library symbols (e.g. "fmt", "net/http", etc). StandardLibrary bool `json:"standard-library" yaml:"standard-library" mapstructure:"standard-library"` @@ -104,8 +104,8 @@ func DefaultConfig() Config { CaptureScope: []SymbolCaptureScope{ SymbolScopeGolang, }, + Types: []string{"T", "t"}, Go: GoSymbolConfig{ - Types: []string{"T", "t"}, StandardLibrary: true, ExtendedStandardLibrary: true, ThirdPartyModules: true, diff --git a/syft/file/cataloger/executable/elf.go b/syft/file/cataloger/executable/elf.go index 4aebd67fc..d9706242d 100644 --- a/syft/file/cataloger/executable/elf.go +++ b/syft/file/cataloger/executable/elf.go @@ -1,9 +1,7 @@ package executable import ( - "debug/buildinfo" "debug/elf" - "io" "regexp" "strings" @@ -47,28 +45,10 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader, cfg func elfToolchains(reader unionreader.UnionReader, f *elf.File) []file.Toolchain { return includeNoneNil( golangToolchainEvidence(reader), + cToolchainEvidence(f), ) } -func shouldCaptureSymbols(data *file.Executable, cfg SymbolConfig) bool { - // TODO: IMPLEMENT ME! - return true -} - -// elfGolangToolchainEvidence attempts to extract Go toolchain information from the ELF file. -func golangToolchainEvidence(reader io.ReaderAt) *file.Toolchain { - bi, err := buildinfo.Read(reader) - if err != nil || bi == nil { - // not a golang binary - return nil - } - return &file.Toolchain{ - Name: "go", - Version: bi.GoVersion, - Kind: file.ToolchainKindCompiler, - } -} - func includeNoneNil(evidence ...*file.Toolchain) []file.Toolchain { var toolchains []file.Toolchain for _, e := range evidence { @@ -84,8 +64,18 @@ func elfNMSymbols(f *elf.File, cfg SymbolConfig, toolchains []file.Toolchain) [] return captureElfGoSymbols(f, cfg) } - // TODO: capture other symbol types (non-go) based on the scope selection (lib, app, etc) - return nil + // include all symbols + syms, err := f.Symbols() + if err != nil { + log.WithFields("error", err).Trace("unable to read symbols from elf file") + return nil + } + + var symbols []string + for _, sym := range syms { + symbols = append(symbols, sym.Name) + } + return symbols } func captureElfGoSymbols(f *elf.File, cfg SymbolConfig) []string { @@ -96,7 +86,7 @@ func captureElfGoSymbols(f *elf.File, cfg SymbolConfig) []string { } var symbols []string - filter := createGoSymbolFilter(cfg.Go) + filter := createGoSymbolFilter(cfg) for _, sym := range syms { name, include := filter(sym.Name, elfSymbolType(sym, f.Sections)) if include { diff --git a/syft/file/cataloger/executable/elf_test.go b/syft/file/cataloger/executable/elf_test.go index c846a5da4..728526f88 100644 --- a/syft/file/cataloger/executable/elf_test.go +++ b/syft/file/cataloger/executable/elf_test.go @@ -227,26 +227,6 @@ func Test_elfHasExports(t *testing.T) { } } -func Test_elfNMSymbols_nonGoReturnsNil(t *testing.T) { - // for non-Go binaries, elfNMSymbols should return nil since we only support Go for now - readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { - t.Helper() - f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture)) - require.NoError(t, err) - return f - } - - f, err := elf.NewFile(readerForFixture(t, "bin/hello_linux")) - require.NoError(t, err) - - // no Go toolchain present - toolchains := []file.Toolchain{} - cfg := SymbolConfig{} - - symbols := elfNMSymbols(f, cfg, toolchains) - assert.Nil(t, symbols, "expected nil symbols for non-Go binary") -} - func Test_elfGoToolchainDetection(t *testing.T) { readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { t.Helper() @@ -296,19 +276,21 @@ func Test_elfGoSymbolCapture(t *testing.T) { tests := []struct { name string fixture string - cfg GoSymbolConfig + cfg SymbolConfig wantSymbols []string // exact symbol names that must be present wantMinSymbolCount int }{ { name: "capture all symbol types", fixture: "bin/hello_linux", - cfg: GoSymbolConfig{ - StandardLibrary: true, - ExtendedStandardLibrary: true, - ThirdPartyModules: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + StandardLibrary: true, + ExtendedStandardLibrary: true, + ThirdPartyModules: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ // stdlib - fmt package (used via fmt.Println) @@ -331,10 +313,12 @@ func Test_elfGoSymbolCapture(t *testing.T) { { name: "capture only third-party symbols", fixture: "bin/hello_linux", - cfg: GoSymbolConfig{ - ThirdPartyModules: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ThirdPartyModules: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ "github.com/davecgh/go-spew/spew.(*dumpState).dump", @@ -345,10 +329,12 @@ func Test_elfGoSymbolCapture(t *testing.T) { { name: "capture only extended stdlib symbols", fixture: "bin/hello_linux", - cfg: GoSymbolConfig{ - ExtendedStandardLibrary: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExtendedStandardLibrary: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ "golang.org/x/text/internal/language.Tag.String", @@ -358,13 +344,15 @@ func Test_elfGoSymbolCapture(t *testing.T) { { name: "capture with text section types only", fixture: "bin/hello_linux", - cfg: GoSymbolConfig{ - Types: []string{"T", "t"}, // text section (code) symbols - StandardLibrary: true, - ExtendedStandardLibrary: true, - ThirdPartyModules: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Types: []string{"T", "t"}, // text section (code) symbols + Go: GoSymbolConfig{ + StandardLibrary: true, + ExtendedStandardLibrary: true, + ThirdPartyModules: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ "encoding/json.Marshal", @@ -379,7 +367,7 @@ func Test_elfGoSymbolCapture(t *testing.T) { f, err := elf.NewFile(reader) require.NoError(t, err) - symbols := captureElfGoSymbols(f, SymbolConfig{Go: tt.cfg}) + symbols := captureElfGoSymbols(f, tt.cfg) symbolSet := make(map[string]struct{}, len(symbols)) for _, sym := range symbols { symbolSet[sym] = struct{}{} @@ -415,8 +403,8 @@ func Test_elfNMSymbols_goReturnsSymbols(t *testing.T) { {Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler}, } cfg := SymbolConfig{ + Types: []string{"T", "t"}, Go: GoSymbolConfig{ - Types: []string{"T", "t"}, StandardLibrary: true, ExtendedStandardLibrary: true, ThirdPartyModules: true, diff --git a/syft/file/cataloger/executable/go_symbols.go b/syft/file/cataloger/executable/go_symbols.go index dfd973d6d..e0ef7db99 100644 --- a/syft/file/cataloger/executable/go_symbols.go +++ b/syft/file/cataloger/executable/go_symbols.go @@ -31,7 +31,7 @@ const ( // createGoSymbolFilter creates a filter function for Go symbols based on the provided configuration. This filter function // returns true if a symbol should be included based on its name and type. This also allows for modification of the symbol name // if necessary (e.g., normalization of vendored module paths). The returned name is only valid if the boolean is true. -func createGoSymbolFilter(cfg GoSymbolConfig) func(string, string) (string, bool) { +func createGoSymbolFilter(cfg SymbolConfig) func(string, string) (string, bool) { validNmTypes := buildNmTypes(cfg.Types) return func(symName, symType string) (string, bool) { @@ -47,13 +47,13 @@ func createGoSymbolFilter(cfg GoSymbolConfig) func(string, string) (string, bool // filter based on exported/unexported symbol configuration exported := isExportedSymbol(symName) - if !shouldIncludeByExportStatus(exported, cfg.ExportedSymbols, cfg.UnexportedSymbols) { + if !shouldIncludeByExportStatus(exported, cfg.Go.ExportedSymbols, cfg.Go.UnexportedSymbols) { return "", false } // handle type equality functions (e.g., type:.eq.myStruct) if isTypeEqualityFunction(symName) { - if !cfg.TypeEqualityFunctions { + if !cfg.Go.TypeEqualityFunctions { return "", false } return symName, true @@ -61,21 +61,21 @@ func createGoSymbolFilter(cfg GoSymbolConfig) func(string, string) (string, bool // handle GC shape stencil functions (e.g., go.shape.func()) if isGCShapeStencil(symName) { - if !cfg.GCShapeStencils { + if !cfg.Go.GCShapeStencils { return "", false } return symName, true } // normalize vendored module paths if configured - symName = normalizeVendoredPath(symName, cfg.NormalizeVendoredModules) + symName = normalizeVendoredPath(symName, cfg.Go.NormalizeVendoredModules) // determine the package path for classification pkgPath := extractPackagePath(symName) // handle extended stdlib (golang.org/x/*) if isExtendedStdlib(pkgPath) { - if !cfg.ExtendedStandardLibrary { + if !cfg.Go.ExtendedStandardLibrary { return "", false } return symName, true @@ -83,14 +83,14 @@ func createGoSymbolFilter(cfg GoSymbolConfig) func(string, string) (string, bool // handle stdlib packages if isStdlibPackage(pkgPath) { - if !cfg.StandardLibrary { + if !cfg.Go.StandardLibrary { return "", false } return symName, true } // this is a third-party package - if !cfg.ThirdPartyModules { + if !cfg.Go.ThirdPartyModules { return "", false } return symName, true @@ -151,7 +151,7 @@ func isGCShapeStencil(symName string) bool { // normalizeVendoredPath removes the "vendor/" prefix from vendored module paths if normalization is enabled. func normalizeVendoredPath(symName string, normalize bool) string { - if normalize && strings.HasPrefix(symName, vendorPrefix) { + if normalize && isVendoredPath(symName) { return strings.TrimPrefix(symName, vendorPrefix) } return symName diff --git a/syft/file/cataloger/executable/go_symbols_test.go b/syft/file/cataloger/executable/go_symbols_test.go index c7a1a6e63..7c7005357 100644 --- a/syft/file/cataloger/executable/go_symbols_test.go +++ b/syft/file/cataloger/executable/go_symbols_test.go @@ -607,7 +607,7 @@ func Test_isStdlibPackage(t *testing.T) { func Test_createGoSymbolFilter(t *testing.T) { tests := []struct { name string - cfg GoSymbolConfig + cfg SymbolConfig symName string symType string wantName string @@ -616,10 +616,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // NM type filtering { name: "valid NM type with defaults", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "fmt.Println", symType: "T", @@ -628,10 +630,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "invalid NM type with defaults", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "fmt.Println", symType: "X", // important! @@ -640,11 +644,13 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "custom NM types - included", - cfg: GoSymbolConfig{ - Types: []string{"T"}, - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Types: []string{"T"}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "fmt.Println", symType: "T", @@ -653,11 +659,13 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "custom NM types - excluded", - cfg: GoSymbolConfig{ - Types: []string{"T"}, - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Types: []string{"T"}, + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "fmt.Println", symType: "t", @@ -668,10 +676,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // floating point literal filtering { name: "floating point literal filtered", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "$f64.3fceb851eb851eb8", symType: "R", @@ -682,10 +692,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // export status filtering { name: "exported symbol with only exported enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: false, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: false, + StandardLibrary: true, + }, }, symName: "fmt.Println", symType: "T", @@ -694,10 +706,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "unexported symbol with only exported enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: false, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: false, + StandardLibrary: true, + }, }, symName: "fmt.println", symType: "T", @@ -708,10 +722,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // type equality functions { name: "type equality function - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - TypeEqualityFunctions: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + TypeEqualityFunctions: true, + }, }, symName: "type:.eq.myStruct", symType: "T", @@ -720,10 +736,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "type equality function - disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - TypeEqualityFunctions: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + TypeEqualityFunctions: false, + }, }, symName: "type:.eq.myStruct", symType: "T", @@ -734,10 +752,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // GC shape stencils { name: "gc shape stencil - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - GCShapeStencils: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + GCShapeStencils: true, + }, }, symName: "go.shape.func()", symType: "T", @@ -746,10 +766,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "gc shape stencil - disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - GCShapeStencils: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + GCShapeStencils: false, + }, }, symName: "go.shape.func()", symType: "T", @@ -758,10 +780,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "gc shape stencil embedded in generic - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - GCShapeStencils: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + GCShapeStencils: true, + }, }, symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]", symType: "T", @@ -770,10 +794,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "gc shape stencil embedded in generic - disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - GCShapeStencils: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + GCShapeStencils: false, + }, }, symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]", symType: "T", @@ -784,11 +810,13 @@ func Test_createGoSymbolFilter(t *testing.T) { // vendored module normalization { name: "vendored path - normalization enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - ThirdPartyModules: true, - NormalizeVendoredModules: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + ThirdPartyModules: true, + NormalizeVendoredModules: true, + }, }, symName: "vendor/github.com/foo/bar.Baz", symType: "T", @@ -797,11 +825,13 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "vendored path - normalization disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - ThirdPartyModules: true, - NormalizeVendoredModules: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + ThirdPartyModules: true, + NormalizeVendoredModules: false, + }, }, symName: "vendor/github.com/foo/bar.Baz", symType: "T", @@ -812,10 +842,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // extended stdlib { name: "extended stdlib - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - ExtendedStandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + ExtendedStandardLibrary: true, + }, }, symName: "golang.org/x/net/html.Parse", symType: "T", @@ -824,10 +856,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "extended stdlib - disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - ExtendedStandardLibrary: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + ExtendedStandardLibrary: false, + }, }, symName: "golang.org/x/net/html.Parse", symType: "T", @@ -838,10 +872,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // stdlib { name: "stdlib - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "fmt.Println", symType: "T", @@ -850,10 +886,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "stdlib - disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: false, + }, }, symName: "fmt.Println", symType: "T", @@ -862,10 +900,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "nested stdlib - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "net/http.ListenAndServe", symType: "T", @@ -876,10 +916,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // third party { name: "third party - enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - ThirdPartyModules: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + ThirdPartyModules: true, + }, }, symName: "github.com/spf13/cobra.Command", symType: "T", @@ -888,10 +930,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "third party - disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - ThirdPartyModules: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + ThirdPartyModules: false, + }, }, symName: "github.com/spf13/cobra.Command", symType: "T", @@ -902,10 +946,12 @@ func Test_createGoSymbolFilter(t *testing.T) { // main package (treated as stdlib) { name: "main package - stdlib enabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: true, + }, }, symName: "main.main", symType: "T", @@ -914,10 +960,12 @@ func Test_createGoSymbolFilter(t *testing.T) { }, { name: "main package - stdlib disabled", - cfg: GoSymbolConfig{ - ExportedSymbols: true, - UnexportedSymbols: true, - StandardLibrary: false, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExportedSymbols: true, + UnexportedSymbols: true, + StandardLibrary: false, + }, }, symName: "main.main", symType: "T", diff --git a/syft/file/cataloger/executable/macho.go b/syft/file/cataloger/executable/macho.go index 179a9ed0d..4e4985229 100644 --- a/syft/file/cataloger/executable/macho.go +++ b/syft/file/cataloger/executable/macho.go @@ -59,7 +59,7 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader, cf data.HasExports = machoHasExports(f) } - data.Toolchains = machoToolchains(reader, f) + data.Toolchains = machoToolchains(reader) if shouldCaptureSymbols(data, cfg) { symbols = machoNMSymbols(f, cfg, data.Toolchains) } @@ -72,7 +72,7 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader, cf return nil } -func machoToolchains(reader unionreader.UnionReader, f *macho.File) []file.Toolchain { +func machoToolchains(reader unionreader.UnionReader) []file.Toolchain { return includeNoneNil( golangToolchainEvidence(reader), ) @@ -83,13 +83,17 @@ func machoNMSymbols(f *macho.File, cfg SymbolConfig, toolchains []file.Toolchain return captureMachoGoSymbols(f, cfg) } - // TODO: capture other symbol types (non-go) based on the scope selection (lib, app, etc) - return nil + // include all symbols + var symbols []string + for _, sym := range f.Symtab.Syms { + symbols = append(symbols, sym.Name) + } + return symbols } func captureMachoGoSymbols(f *macho.File, cfg SymbolConfig) []string { var symbols []string - filter := createGoSymbolFilter(cfg.Go) + filter := createGoSymbolFilter(cfg) for _, sym := range f.Symtab.Syms { name, include := filter(sym.Name, machoSymbolType(sym, f.Sections)) if include { @@ -99,15 +103,6 @@ func captureMachoGoSymbols(f *macho.File, cfg SymbolConfig) []string { return symbols } -func isGoToolchainPresent(toolchains []file.Toolchain) bool { - for _, tc := range toolchains { - if tc.Name == "go" { - return true - } - } - return false -} - func machoSymbolType(s macho.Symbol, sections []*macho.Section) string { // stab (debugging) symbols get '-' if s.Type&machoNStab != 0 { @@ -133,7 +128,7 @@ func machoSymbolType(s macho.Symbol, sections []*macho.Section) string { // lowercase for local symbols, uppercase for external if !isExternal && typeChar != '-' && typeChar != '?' { - typeChar = typeChar + 32 // convert to lowercase + typeChar += 32 // convert to lowercase } return string(typeChar) diff --git a/syft/file/cataloger/executable/macho_test.go b/syft/file/cataloger/executable/macho_test.go index 568cd12b4..98bef0f9d 100644 --- a/syft/file/cataloger/executable/macho_test.go +++ b/syft/file/cataloger/executable/macho_test.go @@ -121,26 +121,6 @@ func Test_machoUniversal(t *testing.T) { } } -func Test_machoNMSymbols_nonGoReturnsNil(t *testing.T) { - // for non-Go binaries, machoNMSymbols should return nil since we only support Go for now - readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { - t.Helper() - f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture)) - require.NoError(t, err) - return f - } - - f, err := macho.NewFile(readerForFixture(t, "bin/hello_mac")) - require.NoError(t, err) - - // no Go toolchain present - toolchains := []file.Toolchain{} - cfg := SymbolConfig{} - - symbols := machoNMSymbols(f, cfg, toolchains) - assert.Nil(t, symbols, "expected nil symbols for non-Go binary") -} - func Test_machoGoToolchainDetection(t *testing.T) { readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { t.Helper() @@ -163,10 +143,8 @@ func Test_machoGoToolchainDetection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { reader := readerForFixture(t, tt.fixture) - f, err := macho.NewFile(reader) - require.NoError(t, err) - toolchains := machoToolchains(reader, f) + toolchains := machoToolchains(reader) assert.Equal(t, tt.wantPresent, isGoToolchainPresent(toolchains)) if tt.wantPresent { @@ -190,19 +168,21 @@ func Test_machoGoSymbolCapture(t *testing.T) { tests := []struct { name string fixture string - cfg GoSymbolConfig + cfg SymbolConfig wantSymbols []string // exact symbol names that must be present wantMinSymbolCount int }{ { name: "capture all symbol types", fixture: "bin/hello_mac", - cfg: GoSymbolConfig{ - StandardLibrary: true, - ExtendedStandardLibrary: true, - ThirdPartyModules: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + StandardLibrary: true, + ExtendedStandardLibrary: true, + ThirdPartyModules: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ // stdlib - fmt package (used via fmt.Println) @@ -225,10 +205,12 @@ func Test_machoGoSymbolCapture(t *testing.T) { { name: "capture only third-party symbols", fixture: "bin/hello_mac", - cfg: GoSymbolConfig{ - ThirdPartyModules: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ThirdPartyModules: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ "github.com/davecgh/go-spew/spew.(*dumpState).dump", @@ -239,10 +221,12 @@ func Test_machoGoSymbolCapture(t *testing.T) { { name: "capture only extended stdlib symbols", fixture: "bin/hello_mac", - cfg: GoSymbolConfig{ - ExtendedStandardLibrary: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Go: GoSymbolConfig{ + ExtendedStandardLibrary: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ "golang.org/x/text/internal/language.Tag.String", @@ -252,13 +236,15 @@ func Test_machoGoSymbolCapture(t *testing.T) { { name: "capture with text section types only", fixture: "bin/hello_mac", - cfg: GoSymbolConfig{ - Types: []string{"T", "t"}, // text section (code) symbols - StandardLibrary: true, - ExtendedStandardLibrary: true, - ThirdPartyModules: true, - ExportedSymbols: true, - UnexportedSymbols: true, + cfg: SymbolConfig{ + Types: []string{"T", "t"}, // text section (code) symbols + Go: GoSymbolConfig{ + StandardLibrary: true, + ExtendedStandardLibrary: true, + ThirdPartyModules: true, + ExportedSymbols: true, + UnexportedSymbols: true, + }, }, wantSymbols: []string{ "encoding/json.Marshal", @@ -273,7 +259,7 @@ func Test_machoGoSymbolCapture(t *testing.T) { f, err := macho.NewFile(reader) require.NoError(t, err) - symbols := captureMachoGoSymbols(f, SymbolConfig{Go: tt.cfg}) + symbols := captureMachoGoSymbols(f, tt.cfg) symbolSet := make(map[string]struct{}, len(symbols)) for _, sym := range symbols { symbolSet[sym] = struct{}{} @@ -309,8 +295,8 @@ func Test_machoNMSymbols_goReturnsSymbols(t *testing.T) { {Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler}, } cfg := SymbolConfig{ + Types: []string{"T", "t"}, Go: GoSymbolConfig{ - Types: []string{"T", "t"}, StandardLibrary: true, ExtendedStandardLibrary: true, ThirdPartyModules: true, diff --git a/syft/file/cataloger/executable/symbols.go b/syft/file/cataloger/executable/symbols.go new file mode 100644 index 000000000..34eac1aa6 --- /dev/null +++ b/syft/file/cataloger/executable/symbols.go @@ -0,0 +1,47 @@ +package executable + +import "github.com/anchore/syft/syft/file" + +// shouldCaptureSymbols determines whether symbols should be captured for the given executable +// based on the configured capture scopes. If any configured scope matches the executable's +// characteristics, symbols will be captured. +func shouldCaptureSymbols(data *file.Executable, cfg SymbolConfig) bool { + if data == nil { + return false + } + + 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 + } + case SymbolScopeGolang: + if hasGolangToolchain(data) { + return true + } + } + } + + // if no scopes matched, do not capture symbols (empty scope means none) + return false +} + +// hasGolangToolchain checks if the executable was built with the Go toolchain. +func hasGolangToolchain(data *file.Executable) bool { + for _, tc := range data.Toolchains { + if tc.Name == "go" { + return true + } + } + return false +} diff --git a/syft/file/cataloger/executable/symbols_test.go b/syft/file/cataloger/executable/symbols_test.go new file mode 100644 index 000000000..b3d99c185 --- /dev/null +++ b/syft/file/cataloger/executable/symbols_test.go @@ -0,0 +1,226 @@ +package executable + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" +) + +func TestShouldCaptureSymbols(t *testing.T) { + tests := []struct { + name string + data *file.Executable + cfg SymbolConfig + want bool + }{ + { + name: "nil data returns false", + data: nil, + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeAll}, + }, + want: false, + }, + { + name: "empty capture scope returns false", + data: &file.Executable{}, + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{}, + }, + 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{ + Toolchains: []file.Toolchain{ + {Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler}, + }, + }, + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + }, + want: true, + }, + { + name: "scope golang without go toolchain returns false", + data: &file.Executable{ + Toolchains: []file.Toolchain{ + {Name: "gcc", Version: "12.0.0", Kind: file.ToolchainKindCompiler}, + }, + }, + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + }, + want: false, + }, + { + name: "scope golang with empty toolchains returns false", + data: &file.Executable{}, + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + }, + 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{ + Toolchains: []file.Toolchain{ + {Name: "gcc", Version: "12.0.0", Kind: file.ToolchainKindCompiler}, + {Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler}, + }, + }, + cfg: SymbolConfig{ + CaptureScope: []SymbolCaptureScope{SymbolScopeGolang}, + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shouldCaptureSymbols(tt.data, tt.cfg) + require.Equal(t, tt.want, got) + }) + } +} + +func TestHasGolangToolchain(t *testing.T) { + tests := []struct { + name string + data *file.Executable + want bool + }{ + { + name: "empty toolchains", + data: &file.Executable{}, + want: false, + }, + { + name: "no go toolchain", + data: &file.Executable{ + Toolchains: []file.Toolchain{ + {Name: "gcc", Version: "12.0.0", Kind: file.ToolchainKindCompiler}, + {Name: "clang", Version: "15.0.0", Kind: file.ToolchainKindCompiler}, + }, + }, + want: false, + }, + { + name: "has go toolchain", + data: &file.Executable{ + Toolchains: []file.Toolchain{ + {Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler}, + }, + }, + want: true, + }, + { + name: "go toolchain among others", + data: &file.Executable{ + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := hasGolangToolchain(tt.data) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/.gitignore b/syft/file/cataloger/executable/test-fixtures/toolchains/.gitignore new file mode 100644 index 000000000..f63d726bc --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/.gitignore @@ -0,0 +1,3 @@ +bin/ +Dockerfile.sha256 +*.fingerprint diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/Makefile b/syft/file/cataloger/executable/test-fixtures/toolchains/Makefile new file mode 100644 index 000000000..dd352612e --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/Makefile @@ -0,0 +1,15 @@ +# invoke all make files in subdirectories +.PHONY: all gcc clang + +all: gcc clang + +gcc: + $(MAKE) -C gcc + +clang: + $(MAKE) -C clang + +%: + @for dir in gcc clang; do \ + $(MAKE) -C $$dir $@; \ + done diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile new file mode 100644 index 000000000..8b91ab65f --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Dockerfile @@ -0,0 +1 @@ +FROM silkeh/clang:18.1.8 diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Makefile b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Makefile new file mode 100644 index 000000000..e727f5951 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/Makefile @@ -0,0 +1,39 @@ +BIN=./bin +TOOL_IMAGE=localhost/syft-toolchain-clang-build-tools:latest +FINGERPRINT_FILE=$(BIN).fingerprint + +ifndef BIN + $(error BIN is not set) +endif + +.DEFAULT_GOAL := fixtures + +# requirement 1: 'fixtures' goal to generate any and all test fixtures +fixtures: build + +# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted +fingerprint: $(FINGERPRINT_FILE) + +tools-check: + @sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1) + +tools: + @(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) + +build: tools + @mkdir -p $(BIN) + docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make + +debug: + docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash + +# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint +.PHONY: $(FINGERPRINT_FILE) +$(FINGERPRINT_FILE): + @find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE) + +# requirement 4: 'clean' goal to remove all generated test fixtures +clean: + rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE) + +.PHONY: tools tools-check build debug clean diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/Makefile b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/Makefile new file mode 100644 index 000000000..3c2392076 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/Makefile @@ -0,0 +1,9 @@ +BIN=../bin + +all: $(BIN)/hello_clang + +$(BIN)/hello_clang: hello.c + clang hello.c -o $(BIN)/hello_clang + +clean: + rm -f $(BIN)/hello_clang diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/hello.c b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/hello.c new file mode 100644 index 000000000..f26b97c98 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/clang/project/hello.c @@ -0,0 +1,6 @@ +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Dockerfile b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Dockerfile new file mode 100644 index 000000000..6c46d0125 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Dockerfile @@ -0,0 +1 @@ +FROM gcc:13.4.0 diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Makefile b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Makefile new file mode 100644 index 000000000..944b78bd5 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/Makefile @@ -0,0 +1,39 @@ +BIN=./bin +TOOL_IMAGE=localhost/syft-toolchain-gcc-build-tools:latest +FINGERPRINT_FILE=$(BIN).fingerprint + +ifndef BIN + $(error BIN is not set) +endif + +.DEFAULT_GOAL := fixtures + +# requirement 1: 'fixtures' goal to generate any and all test fixtures +fixtures: build + +# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted +fingerprint: $(FINGERPRINT_FILE) + +tools-check: + @sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1) + +tools: + @(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256) + +build: tools + @mkdir -p $(BIN) + docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make + +debug: + docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash + +# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint +.PHONY: $(FINGERPRINT_FILE) +$(FINGERPRINT_FILE): + @find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE) + +# requirement 4: 'clean' goal to remove all generated test fixtures +clean: + rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE) + +.PHONY: tools tools-check build debug clean diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/Makefile b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/Makefile new file mode 100644 index 000000000..124b9d9ac --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/Makefile @@ -0,0 +1,9 @@ +BIN=../bin + +all: $(BIN)/hello_gcc + +$(BIN)/hello_gcc: hello.c + gcc hello.c -o $(BIN)/hello_gcc + +clean: + rm -f $(BIN)/hello_gcc diff --git a/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/hello.c b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/hello.c new file mode 100644 index 000000000..f26b97c98 --- /dev/null +++ b/syft/file/cataloger/executable/test-fixtures/toolchains/gcc/project/hello.c @@ -0,0 +1,6 @@ +#include + +int main() { + printf("Hello, World!\n"); + return 0; +} diff --git a/syft/file/cataloger/executable/toolchains.go b/syft/file/cataloger/executable/toolchains.go new file mode 100644 index 000000000..97b0983a4 --- /dev/null +++ b/syft/file/cataloger/executable/toolchains.go @@ -0,0 +1,81 @@ +package executable + +import ( + "debug/buildinfo" + "debug/elf" + "io" + "regexp" + "strings" + + "github.com/anchore/syft/syft/file" +) + +var ( + clangVersionPattern = regexp.MustCompile(`clang version (\d+\.\d+\.\d+)`) + gccVersionPattern = regexp.MustCompile(`GCC: \([^)]+\) (\d+\.\d+\.\d+)`) +) + +// elfGolangToolchainEvidence attempts to extract Go toolchain information from the ELF file. +func golangToolchainEvidence(reader io.ReaderAt) *file.Toolchain { + bi, err := buildinfo.Read(reader) + if err != nil || bi == nil { + // not a golang binary + return nil + } + return &file.Toolchain{ + Name: "go", + Version: bi.GoVersion, + Kind: file.ToolchainKindCompiler, + } +} + +// cToolchainEvidence attempts to extract C/C++ compiler information from the ELF .comment section. +// This detects GCC and Clang compilers based on their version strings. +func cToolchainEvidence(f *elf.File) *file.Toolchain { + commentSection := f.Section(".comment") + if commentSection == nil { + return nil + } + + data, err := commentSection.Data() + if err != nil { + return nil + } + + // the .comment section contains null-terminated strings + comments := strings.Split(string(data), "\x00") + + // check for clang first since clang binaries often have both GCC and clang entries + // (clang includes GCC compatibility info) + for _, comment := range comments { + if match := clangVersionPattern.FindStringSubmatch(comment); match != nil { + return &file.Toolchain{ + Name: "clang", + Version: match[1], + Kind: file.ToolchainKindCompiler, + } + } + } + + // if not clang, check for GCC + for _, comment := range comments { + if match := gccVersionPattern.FindStringSubmatch(comment); match != nil { + return &file.Toolchain{ + Name: "gcc", + Version: match[1], + Kind: file.ToolchainKindCompiler, + } + } + } + + return nil +} + +func isGoToolchainPresent(toolchains []file.Toolchain) bool { + for _, tc := range toolchains { + if tc.Name == "go" { + return true + } + } + return false +} diff --git a/syft/file/cataloger/executable/toolchains_test.go b/syft/file/cataloger/executable/toolchains_test.go new file mode 100644 index 000000000..bc98a11c5 --- /dev/null +++ b/syft/file/cataloger/executable/toolchains_test.go @@ -0,0 +1,61 @@ +package executable + +import ( + "debug/elf" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/internal/unionreader" +) + +func Test_cToolchainDetection(t *testing.T) { + readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { + t.Helper() + f, err := os.Open(filepath.Join("test-fixtures/toolchains", fixture)) + require.NoError(t, err) + return f + } + + tests := []struct { + name string + fixture string + want *file.Toolchain + }{ + { + name: "gcc binary", + fixture: "gcc/bin/hello_gcc", + want: &file.Toolchain{ + Name: "gcc", + Version: "13.4.0", + Kind: file.ToolchainKindCompiler, + }, + }, + { + name: "clang binary", + fixture: "clang/bin/hello_clang", + want: &file.Toolchain{ + Name: "clang", + Version: "18.1.8", + Kind: file.ToolchainKindCompiler, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reader := readerForFixture(t, tt.fixture) + f, err := elf.NewFile(reader) + require.NoError(t, err) + + got := cToolchainEvidence(f) + + if d := cmp.Diff(tt.want, got); d != "" { + t.Errorf("cToolchainEvidence() mismatch (-want +got):\n%s", d) + } + }) + } +} diff --git a/syft/file/executable.go b/syft/file/executable.go index 73de333e5..d99448d5f 100644 --- a/syft/file/executable.go +++ b/syft/file/executable.go @@ -7,7 +7,7 @@ type ( // RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary. RelocationReadOnly string - //SymbolType string + // SymbolType string ToolchainKind string ) @@ -26,11 +26,11 @@ const ( RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection //// from https://pkg.go.dev/cmd/nm - //SymbolTypeText SymbolType = "T" // text (code) segment symbol - //SymbolTypeTextStatic SymbolType = "t" // static text segment symbol - //SymbolTypeReadOnly SymbolType = "R" // read-only data segment symbol - //SymbolTypeReadOnlyStatic SymbolType = "r" // static read-only data segment symbol - //SymbolTypeData SymbolType = "D" // data segment symbol + // SymbolTypeText SymbolType = "T" // text (code) segment symbol + // SymbolTypeTextStatic SymbolType = "t" // static text segment symbol + // SymbolTypeReadOnly SymbolType = "R" // read-only data segment symbol + // SymbolTypeReadOnlyStatic SymbolType = "r" // static read-only data segment symbol + // SymbolTypeData SymbolType = "D" // data segment symbol //SymbolTypeDataStatic SymbolType = "d" // static data segment symbol //SymbolTypeBSS SymbolType = "B" // bss segment symbol //SymbolTypeBSSStatic SymbolType = "b" // static bss segment symbol @@ -56,7 +56,7 @@ 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"` + // Symbols []Symbol `json:"symbols,omitempty" yaml:"symbols" mapstructure:"symbols"` SymbolNames []string `json:"symbolNames,omitempty" yaml:"symbolNames" mapstructure:"symbolNames"` // Toolchains captures information about the the compiler, linker, runtime, or other toolchains used to build (or otherwise exist within) the executable. @@ -71,7 +71,7 @@ type Toolchain struct { // TODO: should we allow for aux information here? free form? } -//type Symbol struct { +// type Symbol struct { // //Type SymbolType `json:"type" yaml:"type" mapstructure:"type"` // Type string `json:"type" yaml:"type" mapstructure:"type"` // Name string `json:"name" yaml:"name" mapstructure:"name"`