add gcc and clang toolchains

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-12-09 17:43:38 -05:00
parent 9bf4c5bdf9
commit 32946ec41f
22 changed files with 806 additions and 256 deletions

View File

@ -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 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"` 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 configures Go-specific symbol capturing settings.
Go GoSymbolConfig `json:"go" yaml:"go" mapstructure:"go"` Go GoSymbolConfig `json:"go" yaml:"go" mapstructure:"go"`
} }
// GoSymbolConfig holds settings specific to capturing symbols from binaries built with the golang toolchain. // GoSymbolConfig holds settings specific to capturing symbols from binaries built with the golang toolchain.
type GoSymbolConfig struct { 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 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"` StandardLibrary bool `json:"standard-library" yaml:"standard-library" mapstructure:"standard-library"`
@ -104,8 +104,8 @@ func DefaultConfig() Config {
CaptureScope: []SymbolCaptureScope{ CaptureScope: []SymbolCaptureScope{
SymbolScopeGolang, SymbolScopeGolang,
}, },
Go: GoSymbolConfig{
Types: []string{"T", "t"}, Types: []string{"T", "t"},
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,

View File

@ -1,9 +1,7 @@
package executable package executable
import ( import (
"debug/buildinfo"
"debug/elf" "debug/elf"
"io"
"regexp" "regexp"
"strings" "strings"
@ -47,28 +45,10 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader, cfg
func elfToolchains(reader unionreader.UnionReader, f *elf.File) []file.Toolchain { func elfToolchains(reader unionreader.UnionReader, f *elf.File) []file.Toolchain {
return includeNoneNil( return includeNoneNil(
golangToolchainEvidence(reader), 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 { func includeNoneNil(evidence ...*file.Toolchain) []file.Toolchain {
var toolchains []file.Toolchain var toolchains []file.Toolchain
for _, e := range evidence { for _, e := range evidence {
@ -84,10 +64,20 @@ func elfNMSymbols(f *elf.File, cfg SymbolConfig, toolchains []file.Toolchain) []
return captureElfGoSymbols(f, cfg) return captureElfGoSymbols(f, cfg)
} }
// TODO: capture other symbol types (non-go) based on the scope selection (lib, app, etc) // include all symbols
syms, err := f.Symbols()
if err != nil {
log.WithFields("error", err).Trace("unable to read symbols from elf file")
return nil return nil
} }
var symbols []string
for _, sym := range syms {
symbols = append(symbols, sym.Name)
}
return symbols
}
func captureElfGoSymbols(f *elf.File, cfg SymbolConfig) []string { func captureElfGoSymbols(f *elf.File, cfg SymbolConfig) []string {
syms, err := f.Symbols() syms, err := f.Symbols()
if err != nil { if err != nil {
@ -96,7 +86,7 @@ func captureElfGoSymbols(f *elf.File, cfg SymbolConfig) []string {
} }
var symbols []string var symbols []string
filter := createGoSymbolFilter(cfg.Go) filter := createGoSymbolFilter(cfg)
for _, sym := range syms { for _, sym := range syms {
name, include := filter(sym.Name, elfSymbolType(sym, f.Sections)) name, include := filter(sym.Name, elfSymbolType(sym, f.Sections))
if include { if include {

View File

@ -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) { func Test_elfGoToolchainDetection(t *testing.T) {
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
t.Helper() t.Helper()
@ -296,20 +276,22 @@ func Test_elfGoSymbolCapture(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fixture string fixture string
cfg GoSymbolConfig cfg SymbolConfig
wantSymbols []string // exact symbol names that must be present wantSymbols []string // exact symbol names that must be present
wantMinSymbolCount int wantMinSymbolCount int
}{ }{
{ {
name: "capture all symbol types", name: "capture all symbol types",
fixture: "bin/hello_linux", fixture: "bin/hello_linux",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
// stdlib - fmt package (used via fmt.Println) // stdlib - fmt package (used via fmt.Println)
"fmt.(*fmt).fmtInteger", "fmt.(*fmt).fmtInteger",
@ -331,11 +313,13 @@ func Test_elfGoSymbolCapture(t *testing.T) {
{ {
name: "capture only third-party symbols", name: "capture only third-party symbols",
fixture: "bin/hello_linux", fixture: "bin/hello_linux",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ThirdPartyModules: true, ThirdPartyModules: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
"github.com/davecgh/go-spew/spew.(*dumpState).dump", "github.com/davecgh/go-spew/spew.(*dumpState).dump",
"github.com/davecgh/go-spew/spew.(*formatState).Format", "github.com/davecgh/go-spew/spew.(*formatState).Format",
@ -345,11 +329,13 @@ func Test_elfGoSymbolCapture(t *testing.T) {
{ {
name: "capture only extended stdlib symbols", name: "capture only extended stdlib symbols",
fixture: "bin/hello_linux", fixture: "bin/hello_linux",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
"golang.org/x/text/internal/language.Tag.String", "golang.org/x/text/internal/language.Tag.String",
"golang.org/x/text/internal/language.Parse", "golang.org/x/text/internal/language.Parse",
@ -358,14 +344,16 @@ func Test_elfGoSymbolCapture(t *testing.T) {
{ {
name: "capture with text section types only", name: "capture with text section types only",
fixture: "bin/hello_linux", fixture: "bin/hello_linux",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Types: []string{"T", "t"}, // text section (code) symbols Types: []string{"T", "t"}, // text section (code) symbols
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
"encoding/json.Marshal", "encoding/json.Marshal",
"strings.ToUpper", "strings.ToUpper",
@ -379,7 +367,7 @@ func Test_elfGoSymbolCapture(t *testing.T) {
f, err := elf.NewFile(reader) f, err := elf.NewFile(reader)
require.NoError(t, err) require.NoError(t, err)
symbols := captureElfGoSymbols(f, SymbolConfig{Go: tt.cfg}) symbols := captureElfGoSymbols(f, tt.cfg)
symbolSet := make(map[string]struct{}, len(symbols)) symbolSet := make(map[string]struct{}, len(symbols))
for _, sym := range symbols { for _, sym := range symbols {
symbolSet[sym] = struct{}{} symbolSet[sym] = struct{}{}
@ -415,8 +403,8 @@ func Test_elfNMSymbols_goReturnsSymbols(t *testing.T) {
{Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler}, {Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler},
} }
cfg := SymbolConfig{ cfg := SymbolConfig{
Go: GoSymbolConfig{
Types: []string{"T", "t"}, Types: []string{"T", "t"},
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,

View File

@ -31,7 +31,7 @@ const (
// createGoSymbolFilter creates a filter function for Go symbols based on the provided configuration. This filter function // 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 // 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. // 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) validNmTypes := buildNmTypes(cfg.Types)
return func(symName, symType string) (string, bool) { 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 // filter based on exported/unexported symbol configuration
exported := isExportedSymbol(symName) exported := isExportedSymbol(symName)
if !shouldIncludeByExportStatus(exported, cfg.ExportedSymbols, cfg.UnexportedSymbols) { if !shouldIncludeByExportStatus(exported, cfg.Go.ExportedSymbols, cfg.Go.UnexportedSymbols) {
return "", false return "", false
} }
// handle type equality functions (e.g., type:.eq.myStruct) // handle type equality functions (e.g., type:.eq.myStruct)
if isTypeEqualityFunction(symName) { if isTypeEqualityFunction(symName) {
if !cfg.TypeEqualityFunctions { if !cfg.Go.TypeEqualityFunctions {
return "", false return "", false
} }
return symName, true 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()) // handle GC shape stencil functions (e.g., go.shape.func())
if isGCShapeStencil(symName) { if isGCShapeStencil(symName) {
if !cfg.GCShapeStencils { if !cfg.Go.GCShapeStencils {
return "", false return "", false
} }
return symName, true return symName, true
} }
// normalize vendored module paths if configured // normalize vendored module paths if configured
symName = normalizeVendoredPath(symName, cfg.NormalizeVendoredModules) symName = normalizeVendoredPath(symName, cfg.Go.NormalizeVendoredModules)
// determine the package path for classification // determine the package path for classification
pkgPath := extractPackagePath(symName) pkgPath := extractPackagePath(symName)
// handle extended stdlib (golang.org/x/*) // handle extended stdlib (golang.org/x/*)
if isExtendedStdlib(pkgPath) { if isExtendedStdlib(pkgPath) {
if !cfg.ExtendedStandardLibrary { if !cfg.Go.ExtendedStandardLibrary {
return "", false return "", false
} }
return symName, true return symName, true
@ -83,14 +83,14 @@ func createGoSymbolFilter(cfg GoSymbolConfig) func(string, string) (string, bool
// handle stdlib packages // handle stdlib packages
if isStdlibPackage(pkgPath) { if isStdlibPackage(pkgPath) {
if !cfg.StandardLibrary { if !cfg.Go.StandardLibrary {
return "", false return "", false
} }
return symName, true return symName, true
} }
// this is a third-party package // this is a third-party package
if !cfg.ThirdPartyModules { if !cfg.Go.ThirdPartyModules {
return "", false return "", false
} }
return symName, true 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. // normalizeVendoredPath removes the "vendor/" prefix from vendored module paths if normalization is enabled.
func normalizeVendoredPath(symName string, normalize bool) string { func normalizeVendoredPath(symName string, normalize bool) string {
if normalize && strings.HasPrefix(symName, vendorPrefix) { if normalize && isVendoredPath(symName) {
return strings.TrimPrefix(symName, vendorPrefix) return strings.TrimPrefix(symName, vendorPrefix)
} }
return symName return symName

View File

@ -607,7 +607,7 @@ func Test_isStdlibPackage(t *testing.T) {
func Test_createGoSymbolFilter(t *testing.T) { func Test_createGoSymbolFilter(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
cfg GoSymbolConfig cfg SymbolConfig
symName string symName string
symType string symType string
wantName string wantName string
@ -616,11 +616,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// NM type filtering // NM type filtering
{ {
name: "valid NM type with defaults", name: "valid NM type with defaults",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "T", symType: "T",
wantName: "fmt.Println", wantName: "fmt.Println",
@ -628,11 +630,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "invalid NM type with defaults", name: "invalid NM type with defaults",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "X", // important! symType: "X", // important!
wantName: "", wantName: "",
@ -640,12 +644,14 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "custom NM types - included", name: "custom NM types - included",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Types: []string{"T"}, Types: []string{"T"},
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "T", symType: "T",
wantName: "fmt.Println", wantName: "fmt.Println",
@ -653,12 +659,14 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "custom NM types - excluded", name: "custom NM types - excluded",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Types: []string{"T"}, Types: []string{"T"},
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "t", symType: "t",
wantName: "", wantName: "",
@ -668,11 +676,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// floating point literal filtering // floating point literal filtering
{ {
name: "floating point literal filtered", name: "floating point literal filtered",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "$f64.3fceb851eb851eb8", symName: "$f64.3fceb851eb851eb8",
symType: "R", symType: "R",
wantName: "", wantName: "",
@ -682,11 +692,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// export status filtering // export status filtering
{ {
name: "exported symbol with only exported enabled", name: "exported symbol with only exported enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: false, UnexportedSymbols: false,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "T", symType: "T",
wantName: "fmt.Println", wantName: "fmt.Println",
@ -694,11 +706,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "unexported symbol with only exported enabled", name: "unexported symbol with only exported enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: false, UnexportedSymbols: false,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.println", symName: "fmt.println",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -708,11 +722,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// type equality functions // type equality functions
{ {
name: "type equality function - enabled", name: "type equality function - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
TypeEqualityFunctions: true, TypeEqualityFunctions: true,
}, },
},
symName: "type:.eq.myStruct", symName: "type:.eq.myStruct",
symType: "T", symType: "T",
wantName: "type:.eq.myStruct", wantName: "type:.eq.myStruct",
@ -720,11 +736,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "type equality function - disabled", name: "type equality function - disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
TypeEqualityFunctions: false, TypeEqualityFunctions: false,
}, },
},
symName: "type:.eq.myStruct", symName: "type:.eq.myStruct",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -734,11 +752,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// GC shape stencils // GC shape stencils
{ {
name: "gc shape stencil - enabled", name: "gc shape stencil - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
GCShapeStencils: true, GCShapeStencils: true,
}, },
},
symName: "go.shape.func()", symName: "go.shape.func()",
symType: "T", symType: "T",
wantName: "go.shape.func()", wantName: "go.shape.func()",
@ -746,11 +766,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "gc shape stencil - disabled", name: "gc shape stencil - disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
GCShapeStencils: false, GCShapeStencils: false,
}, },
},
symName: "go.shape.func()", symName: "go.shape.func()",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -758,11 +780,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "gc shape stencil embedded in generic - enabled", name: "gc shape stencil embedded in generic - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
GCShapeStencils: true, GCShapeStencils: true,
}, },
},
symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]", symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
symType: "T", symType: "T",
wantName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]", wantName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
@ -770,11 +794,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "gc shape stencil embedded in generic - disabled", name: "gc shape stencil embedded in generic - disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
GCShapeStencils: false, GCShapeStencils: false,
}, },
},
symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]", symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -784,12 +810,14 @@ func Test_createGoSymbolFilter(t *testing.T) {
// vendored module normalization // vendored module normalization
{ {
name: "vendored path - normalization enabled", name: "vendored path - normalization enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
ThirdPartyModules: true, ThirdPartyModules: true,
NormalizeVendoredModules: true, NormalizeVendoredModules: true,
}, },
},
symName: "vendor/github.com/foo/bar.Baz", symName: "vendor/github.com/foo/bar.Baz",
symType: "T", symType: "T",
wantName: "github.com/foo/bar.Baz", wantName: "github.com/foo/bar.Baz",
@ -797,12 +825,14 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "vendored path - normalization disabled", name: "vendored path - normalization disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
ThirdPartyModules: true, ThirdPartyModules: true,
NormalizeVendoredModules: false, NormalizeVendoredModules: false,
}, },
},
symName: "vendor/github.com/foo/bar.Baz", symName: "vendor/github.com/foo/bar.Baz",
symType: "T", symType: "T",
wantName: "vendor/github.com/foo/bar.Baz", wantName: "vendor/github.com/foo/bar.Baz",
@ -812,11 +842,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// extended stdlib // extended stdlib
{ {
name: "extended stdlib - enabled", name: "extended stdlib - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
}, },
},
symName: "golang.org/x/net/html.Parse", symName: "golang.org/x/net/html.Parse",
symType: "T", symType: "T",
wantName: "golang.org/x/net/html.Parse", wantName: "golang.org/x/net/html.Parse",
@ -824,11 +856,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "extended stdlib - disabled", name: "extended stdlib - disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
ExtendedStandardLibrary: false, ExtendedStandardLibrary: false,
}, },
},
symName: "golang.org/x/net/html.Parse", symName: "golang.org/x/net/html.Parse",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -838,11 +872,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// stdlib // stdlib
{ {
name: "stdlib - enabled", name: "stdlib - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "T", symType: "T",
wantName: "fmt.Println", wantName: "fmt.Println",
@ -850,11 +886,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "stdlib - disabled", name: "stdlib - disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: false, StandardLibrary: false,
}, },
},
symName: "fmt.Println", symName: "fmt.Println",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -862,11 +900,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "nested stdlib - enabled", name: "nested stdlib - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "net/http.ListenAndServe", symName: "net/http.ListenAndServe",
symType: "T", symType: "T",
wantName: "net/http.ListenAndServe", wantName: "net/http.ListenAndServe",
@ -876,11 +916,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// third party // third party
{ {
name: "third party - enabled", name: "third party - enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
ThirdPartyModules: true, ThirdPartyModules: true,
}, },
},
symName: "github.com/spf13/cobra.Command", symName: "github.com/spf13/cobra.Command",
symType: "T", symType: "T",
wantName: "github.com/spf13/cobra.Command", wantName: "github.com/spf13/cobra.Command",
@ -888,11 +930,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "third party - disabled", name: "third party - disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
ThirdPartyModules: false, ThirdPartyModules: false,
}, },
},
symName: "github.com/spf13/cobra.Command", symName: "github.com/spf13/cobra.Command",
symType: "T", symType: "T",
wantName: "", wantName: "",
@ -902,11 +946,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
// main package (treated as stdlib) // main package (treated as stdlib)
{ {
name: "main package - stdlib enabled", name: "main package - stdlib enabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: true, StandardLibrary: true,
}, },
},
symName: "main.main", symName: "main.main",
symType: "T", symType: "T",
wantName: "main.main", wantName: "main.main",
@ -914,11 +960,13 @@ func Test_createGoSymbolFilter(t *testing.T) {
}, },
{ {
name: "main package - stdlib disabled", name: "main package - stdlib disabled",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
StandardLibrary: false, StandardLibrary: false,
}, },
},
symName: "main.main", symName: "main.main",
symType: "T", symType: "T",
wantName: "", wantName: "",

View File

@ -59,7 +59,7 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader, cf
data.HasExports = machoHasExports(f) data.HasExports = machoHasExports(f)
} }
data.Toolchains = machoToolchains(reader, f) data.Toolchains = machoToolchains(reader)
if shouldCaptureSymbols(data, cfg) { if shouldCaptureSymbols(data, cfg) {
symbols = machoNMSymbols(f, cfg, data.Toolchains) symbols = machoNMSymbols(f, cfg, data.Toolchains)
} }
@ -72,7 +72,7 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader, cf
return nil return nil
} }
func machoToolchains(reader unionreader.UnionReader, f *macho.File) []file.Toolchain { func machoToolchains(reader unionreader.UnionReader) []file.Toolchain {
return includeNoneNil( return includeNoneNil(
golangToolchainEvidence(reader), golangToolchainEvidence(reader),
) )
@ -83,13 +83,17 @@ func machoNMSymbols(f *macho.File, cfg SymbolConfig, toolchains []file.Toolchain
return captureMachoGoSymbols(f, cfg) return captureMachoGoSymbols(f, cfg)
} }
// TODO: capture other symbol types (non-go) based on the scope selection (lib, app, etc) // include all symbols
return nil var symbols []string
for _, sym := range f.Symtab.Syms {
symbols = append(symbols, sym.Name)
}
return symbols
} }
func captureMachoGoSymbols(f *macho.File, cfg SymbolConfig) []string { func captureMachoGoSymbols(f *macho.File, cfg SymbolConfig) []string {
var symbols []string var symbols []string
filter := createGoSymbolFilter(cfg.Go) filter := createGoSymbolFilter(cfg)
for _, sym := range f.Symtab.Syms { for _, sym := range f.Symtab.Syms {
name, include := filter(sym.Name, machoSymbolType(sym, f.Sections)) name, include := filter(sym.Name, machoSymbolType(sym, f.Sections))
if include { if include {
@ -99,15 +103,6 @@ func captureMachoGoSymbols(f *macho.File, cfg SymbolConfig) []string {
return symbols 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 { func machoSymbolType(s macho.Symbol, sections []*macho.Section) string {
// stab (debugging) symbols get '-' // stab (debugging) symbols get '-'
if s.Type&machoNStab != 0 { 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 // lowercase for local symbols, uppercase for external
if !isExternal && typeChar != '-' && typeChar != '?' { if !isExternal && typeChar != '-' && typeChar != '?' {
typeChar = typeChar + 32 // convert to lowercase typeChar += 32 // convert to lowercase
} }
return string(typeChar) return string(typeChar)

View File

@ -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) { func Test_machoGoToolchainDetection(t *testing.T) {
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
t.Helper() t.Helper()
@ -163,10 +143,8 @@ func Test_machoGoToolchainDetection(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
reader := readerForFixture(t, tt.fixture) 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)) assert.Equal(t, tt.wantPresent, isGoToolchainPresent(toolchains))
if tt.wantPresent { if tt.wantPresent {
@ -190,20 +168,22 @@ func Test_machoGoSymbolCapture(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fixture string fixture string
cfg GoSymbolConfig cfg SymbolConfig
wantSymbols []string // exact symbol names that must be present wantSymbols []string // exact symbol names that must be present
wantMinSymbolCount int wantMinSymbolCount int
}{ }{
{ {
name: "capture all symbol types", name: "capture all symbol types",
fixture: "bin/hello_mac", fixture: "bin/hello_mac",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
// stdlib - fmt package (used via fmt.Println) // stdlib - fmt package (used via fmt.Println)
"fmt.(*fmt).fmtInteger", "fmt.(*fmt).fmtInteger",
@ -225,11 +205,13 @@ func Test_machoGoSymbolCapture(t *testing.T) {
{ {
name: "capture only third-party symbols", name: "capture only third-party symbols",
fixture: "bin/hello_mac", fixture: "bin/hello_mac",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ThirdPartyModules: true, ThirdPartyModules: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
"github.com/davecgh/go-spew/spew.(*dumpState).dump", "github.com/davecgh/go-spew/spew.(*dumpState).dump",
"github.com/davecgh/go-spew/spew.(*formatState).Format", "github.com/davecgh/go-spew/spew.(*formatState).Format",
@ -239,11 +221,13 @@ func Test_machoGoSymbolCapture(t *testing.T) {
{ {
name: "capture only extended stdlib symbols", name: "capture only extended stdlib symbols",
fixture: "bin/hello_mac", fixture: "bin/hello_mac",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Go: GoSymbolConfig{
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
"golang.org/x/text/internal/language.Tag.String", "golang.org/x/text/internal/language.Tag.String",
"golang.org/x/text/internal/language.Parse", "golang.org/x/text/internal/language.Parse",
@ -252,14 +236,16 @@ func Test_machoGoSymbolCapture(t *testing.T) {
{ {
name: "capture with text section types only", name: "capture with text section types only",
fixture: "bin/hello_mac", fixture: "bin/hello_mac",
cfg: GoSymbolConfig{ cfg: SymbolConfig{
Types: []string{"T", "t"}, // text section (code) symbols Types: []string{"T", "t"}, // text section (code) symbols
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,
ExportedSymbols: true, ExportedSymbols: true,
UnexportedSymbols: true, UnexportedSymbols: true,
}, },
},
wantSymbols: []string{ wantSymbols: []string{
"encoding/json.Marshal", "encoding/json.Marshal",
"strings.ToUpper", "strings.ToUpper",
@ -273,7 +259,7 @@ func Test_machoGoSymbolCapture(t *testing.T) {
f, err := macho.NewFile(reader) f, err := macho.NewFile(reader)
require.NoError(t, err) require.NoError(t, err)
symbols := captureMachoGoSymbols(f, SymbolConfig{Go: tt.cfg}) symbols := captureMachoGoSymbols(f, tt.cfg)
symbolSet := make(map[string]struct{}, len(symbols)) symbolSet := make(map[string]struct{}, len(symbols))
for _, sym := range symbols { for _, sym := range symbols {
symbolSet[sym] = struct{}{} symbolSet[sym] = struct{}{}
@ -309,8 +295,8 @@ func Test_machoNMSymbols_goReturnsSymbols(t *testing.T) {
{Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler}, {Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler},
} }
cfg := SymbolConfig{ cfg := SymbolConfig{
Go: GoSymbolConfig{
Types: []string{"T", "t"}, Types: []string{"T", "t"},
Go: GoSymbolConfig{
StandardLibrary: true, StandardLibrary: true,
ExtendedStandardLibrary: true, ExtendedStandardLibrary: true,
ThirdPartyModules: true, ThirdPartyModules: true,

View File

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

View File

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

View File

@ -0,0 +1,3 @@
bin/
Dockerfile.sha256
*.fingerprint

View File

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

View File

@ -0,0 +1 @@
FROM silkeh/clang:18.1.8

View File

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

View File

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

View File

@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}

View File

@ -0,0 +1 @@
FROM gcc:13.4.0

View File

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

View File

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

View File

@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}

View File

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

View File

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