mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3549b04e18 | ||
|
|
89824f0ae7 | ||
|
|
b361427043 | ||
|
|
281a9b87de | ||
|
|
bf1f0ceea3 | ||
|
|
703edff876 | ||
|
|
a05608a4c8 | ||
|
|
41aa6f6753 | ||
|
|
1a70ffe2fa | ||
|
|
33c5e40431 | ||
|
|
32946ec41f | ||
|
|
9bf4c5bdf9 |
@@ -16,8 +16,6 @@ import (
|
||||
"github.com/anchore/syft/syft/cataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||
"github.com/anchore/syft/syft/file/cataloger/executable"
|
||||
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||
@@ -142,18 +140,21 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
|
||||
log.WithFields("error", err).Warn("unable to configure file hashers")
|
||||
}
|
||||
|
||||
return filecataloging.Config{
|
||||
Selection: cfg.File.Metadata.Selection,
|
||||
Hashers: hashers,
|
||||
Content: filecontent.Config{
|
||||
Globs: cfg.File.Content.Globs,
|
||||
SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize,
|
||||
},
|
||||
Executable: executable.Config{
|
||||
MIMETypes: executable.DefaultConfig().MIMETypes,
|
||||
Globs: cfg.File.Executable.Globs,
|
||||
},
|
||||
}
|
||||
c := filecataloging.DefaultConfig()
|
||||
c.Selection = cfg.File.Metadata.Selection
|
||||
c.Hashers = hashers
|
||||
c.Content.Globs = cfg.File.Content.Globs
|
||||
c.Content.SkipFilesAboveSize = cfg.File.Content.SkipFilesAboveSize
|
||||
c.Executable.Globs = cfg.File.Executable.Globs
|
||||
|
||||
// symbol capture configuration
|
||||
c.Executable.Symbols.CaptureScope = cfg.File.Executable.Symbols.CaptureScope
|
||||
c.Executable.Symbols.Types = cfg.File.Executable.Symbols.Types
|
||||
c.Executable.Symbols.Go.StandardLibrary = cfg.File.Executable.Symbols.Go.StandardLibrary
|
||||
c.Executable.Symbols.Go.ExtendedStandardLibrary = cfg.File.Executable.Symbols.Go.ExtendedStandardLibrary
|
||||
c.Executable.Symbols.Go.ThirdPartyModules = cfg.File.Executable.Symbols.Go.ThirdPartyModules
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
|
||||
@@ -303,6 +304,11 @@ func (cfg *Catalog) PostLoad() error {
|
||||
return fmt.Errorf("cannot enable exclude-binary-overlap-by-ownership without enabling package-file-ownership-overlap")
|
||||
}
|
||||
|
||||
// validate file executable options
|
||||
if err := cfg.ToFilesConfig().Executable.Validate(); err != nil {
|
||||
return fmt.Errorf("invalid file executable configuration: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/anchore/clio"
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/file/cataloger/executable"
|
||||
)
|
||||
|
||||
type fileConfig struct {
|
||||
@@ -28,11 +29,27 @@ type fileContent struct {
|
||||
}
|
||||
|
||||
type fileExecutable struct {
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||
Symbols fileSymbolConfig `yaml:"symbols" json:"symbols" mapstructure:"symbols"`
|
||||
}
|
||||
|
||||
type fileSymbolConfig struct {
|
||||
CaptureScope []executable.SymbolCaptureScope `yaml:"capture" json:"capture" mapstructure:"capture"`
|
||||
Types []string `yaml:"types" json:"types" mapstructure:"types"`
|
||||
Go fileGoSymbolConfig `yaml:"go" json:"go" mapstructure:"go"`
|
||||
}
|
||||
|
||||
type fileGoSymbolConfig struct {
|
||||
StandardLibrary bool `yaml:"standard-library" json:"standard-library" mapstructure:"standard-library"`
|
||||
ExtendedStandardLibrary bool `yaml:"extended-standard-library" json:"extended-standard-library" mapstructure:"extended-standard-library"`
|
||||
ThirdPartyModules bool `yaml:"third-party-modules" json:"third-party-modules" mapstructure:"third-party-modules"`
|
||||
}
|
||||
|
||||
func defaultFileConfig() fileConfig {
|
||||
return fileConfig{
|
||||
api := executable.DefaultConfig()
|
||||
|
||||
// start with API defaults and override CLI-specific values
|
||||
cfg := fileConfig{
|
||||
Metadata: fileMetadata{
|
||||
Selection: file.FilesOwnedByPackageSelection,
|
||||
Digests: []string{"sha1", "sha256"},
|
||||
@@ -41,9 +58,19 @@ func defaultFileConfig() fileConfig {
|
||||
SkipFilesAboveSize: 250 * intFile.KB,
|
||||
},
|
||||
Executable: fileExecutable{
|
||||
Globs: nil,
|
||||
Globs: api.Globs,
|
||||
Symbols: fileSymbolConfig{
|
||||
CaptureScope: api.Symbols.CaptureScope,
|
||||
Types: api.Symbols.Types,
|
||||
Go: fileGoSymbolConfig{
|
||||
StandardLibrary: api.Symbols.Go.StandardLibrary,
|
||||
ExtendedStandardLibrary: api.Symbols.Go.ExtendedStandardLibrary,
|
||||
ThirdPartyModules: api.Symbols.Go.ThirdPartyModules,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
var _ interface {
|
||||
@@ -64,7 +91,7 @@ func (c *fileConfig) PostLoad() error {
|
||||
}
|
||||
|
||||
func (c *fileConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
|
||||
descriptions.Add(&c.Metadata.Selection, `select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
descriptions.Add(&c.Metadata.Selection, `select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||
Options include:
|
||||
- "all": capture all files from the search space
|
||||
- "owned-by-package": capture only files owned by packages
|
||||
@@ -75,4 +102,13 @@ Options include:
|
||||
descriptions.Add(&c.Content.Globs, `file globs for the cataloger to match on`)
|
||||
|
||||
descriptions.Add(&c.Executable.Globs, `file globs for the cataloger to match on`)
|
||||
|
||||
// symbol capture configuration
|
||||
descriptions.Add(&c.Executable.Symbols.CaptureScope, `the scope of symbols to capture from executables (options: "golang")`)
|
||||
descriptions.Add(&c.Executable.Symbols.Types, `the types of symbols to capture, relative to "go tool nm" output (options: "T", "t", "R", "r", "D", "d", "B", "b", "C", "U")`)
|
||||
|
||||
// go symbol configuration
|
||||
descriptions.Add(&c.Executable.Symbols.Go.StandardLibrary, `capture Go standard library symbols (e.g. "fmt", "net/http")`)
|
||||
descriptions.Add(&c.Executable.Symbols.Go.ExtendedStandardLibrary, `capture extended Go standard library symbols (e.g. "golang.org/x/net")`)
|
||||
descriptions.Add(&c.Executable.Symbols.Go.ThirdPartyModules, `capture third-party module symbols (e.g. "github.com/spf13/cobra")`)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package internal
|
||||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.1.2"
|
||||
JSONSchemaVersion = "16.1.3"
|
||||
|
||||
// Changelog
|
||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||
// 16.1.1 - correct elf package osCpe field according to the document of systemd (also add appCpe field)
|
||||
// 16.1.2 - add DotnetDepsEntry.type package metadata field
|
||||
// 16.1.3 - add file executable toolchain and symbol information
|
||||
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.2/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.3/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@@ -1287,6 +1287,20 @@
|
||||
"elfSecurityFeatures": {
|
||||
"$ref": "#/$defs/ELFSecurityFeatures",
|
||||
"description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF."
|
||||
},
|
||||
"symbolNames": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Symbols captures the selection from the symbol table found in the binary."
|
||||
},
|
||||
"toolchains": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/Toolchain"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Toolchains captures information about the compiler, linker, runtime, or other toolchains used to build (or otherwise exist within) the executable."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -4229,6 +4243,27 @@
|
||||
],
|
||||
"description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)."
|
||||
},
|
||||
"Toolchain": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Name is the name of the toolchain (e.g., \"gcc\", \"clang\", \"ld\", etc.)."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Version is the version of the toolchain."
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Kind indicates the type of toolchain (e.g., compiler, linker, runtime)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"kind"
|
||||
]
|
||||
},
|
||||
"WordpressPluginEntry": {
|
||||
"properties": {
|
||||
"pluginInstallDirectory": {
|
||||
|
||||
@@ -24,9 +24,119 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
// SymbolCaptureScope defines the scope of symbols to capture from executables. For the meantime only golang binaries are supported,
|
||||
// however, in the future this can be expanded to include rust audit binaries, libraries only, applications only, or all binaries.
|
||||
type SymbolCaptureScope string
|
||||
|
||||
const SymbolScopeGolang SymbolCaptureScope = "golang" // only binaries built with the golang toolchain
|
||||
|
||||
type Config struct {
|
||||
// MIMETypes are the MIME types that will be considered for executable cataloging.
|
||||
MIMETypes []string `json:"mime-types" yaml:"mime-types" mapstructure:"mime-types"`
|
||||
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
|
||||
|
||||
// Globs are the glob patterns that will be used to filter which files are cataloged.
|
||||
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
|
||||
|
||||
// Symbols configures symbol extraction settings.
|
||||
Symbols SymbolConfig `json:"symbols" yaml:"symbols" mapstructure:"symbols"`
|
||||
}
|
||||
|
||||
// SymbolConfig holds settings related to symbol capturing from executables.
|
||||
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 `json:"types" yaml:"types" mapstructure:"types"`
|
||||
|
||||
// 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 {
|
||||
// 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"`
|
||||
|
||||
// ExtendedStandardLibrary indicates whether to capture extended Go standard library symbols (e.g. "golang.org/x/net", etc).
|
||||
ExtendedStandardLibrary bool `json:"extended-standard-library" yaml:"extended-standard-library" mapstructure:"extended-standard-library"`
|
||||
|
||||
// ThirdPartyModules indicates whether to capture third-party module symbols (e.g. github.com/spf13/cobra, etc).
|
||||
ThirdPartyModules bool `json:"third-party-modules" yaml:"third-party-modules" mapstructure:"third-party-modules"`
|
||||
|
||||
// NormalizeVendoredModules indicates whether to normalize vendored module paths by removing the "vendor/" prefix when capturing third-party module symbols.
|
||||
NormalizeVendoredModules bool `json:"normalize-vendored-modules" yaml:"normalize-vendored-modules" mapstructure:"normalize-vendored-modules"`
|
||||
|
||||
// TypeEqualityFunctions indicates whether to capture type equality functions (e.g. "type..eq..T1..T2") when capturing Go symbols. These are automatically generated by the Go compiler for generic types.
|
||||
TypeEqualityFunctions bool `json:"type-equality-functions" yaml:"type-equality-functions" mapstructure:"type-equality-functions"`
|
||||
|
||||
// GCShapeStencils indicates whether to capture GC shape stencil functions (e.g. "go.shape.*") when capturing Go symbols. These are related to how generics are implemented and are not user defined or directly callable.
|
||||
GCShapeStencils bool `json:"gc-shape-stencils" yaml:"gc-shape-stencils" mapstructure:"gc-shape-stencils"`
|
||||
|
||||
// ExportedSymbols indicates whether to capture only exported (public/global) symbols from Go binaries.
|
||||
ExportedSymbols bool `json:"exported-symbols" yaml:"exported-symbols" mapstructure:"exported-symbols"`
|
||||
|
||||
// UnexportedSymbols indicates whether to capture unexported (private/local) symbols from Go binaries.
|
||||
UnexportedSymbols bool `json:"unexported-symbols" yaml:"unexported-symbols" mapstructure:"unexported-symbols"`
|
||||
}
|
||||
|
||||
// Validate checks for logical configuration inconsistencies and returns an error if any are found.
|
||||
func (c Config) Validate() error {
|
||||
return c.Symbols.Validate()
|
||||
}
|
||||
|
||||
// Validate checks for logical configuration inconsistencies in symbol capture settings.
|
||||
func (s SymbolConfig) Validate() error {
|
||||
// validate that all CaptureScope values are valid
|
||||
for _, scope := range s.CaptureScope {
|
||||
if !isValidCaptureScope(scope) {
|
||||
return fmt.Errorf("invalid symbol capture scope %q: valid values are %q", scope, SymbolScopeGolang)
|
||||
}
|
||||
}
|
||||
|
||||
// validate NM types if specified
|
||||
if len(s.Types) > 0 {
|
||||
for _, t := range s.Types {
|
||||
if !isValidNMType(t) {
|
||||
return fmt.Errorf("invalid NM type %q: valid values are %v", t, validNMTypes())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remaining validations only apply when Go symbol capture is enabled
|
||||
if !s.hasGolangScope() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// if Go symbol capture is enabled, at least one of exported/unexported must be true
|
||||
if !s.Go.ExportedSymbols && !s.Go.UnexportedSymbols {
|
||||
return fmt.Errorf("both exported-symbols and unexported-symbols are disabled; no Go symbols would be captured")
|
||||
}
|
||||
|
||||
// if Go symbol capture is enabled, at least one module source must be enabled
|
||||
if !s.Go.StandardLibrary && !s.Go.ExtendedStandardLibrary && !s.Go.ThirdPartyModules {
|
||||
return fmt.Errorf("all module sources (standard-library, extended-standard-library, third-party-modules) are disabled; no meaningful Go symbols would be captured")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SymbolConfig) hasGolangScope() bool {
|
||||
for _, scope := range s.CaptureScope {
|
||||
if scope == SymbolScopeGolang {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isValidCaptureScope(scope SymbolCaptureScope) bool {
|
||||
switch scope { //nolint:gocritic // lets elect a pattern as if we'll have multiple options in the future...
|
||||
case SymbolScopeGolang:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Cataloger struct {
|
||||
@@ -39,6 +149,20 @@ func DefaultConfig() Config {
|
||||
return Config{
|
||||
MIMETypes: m,
|
||||
Globs: nil,
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{}, // important! by default we do not capture any symbols unless explicitly configured
|
||||
Types: []string{"T", "t"}, // by default only capture "T" (text/code) symbols, since vulnerability data tracks accessible function symbols
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
NormalizeVendoredModules: true,
|
||||
ExportedSymbols: true,
|
||||
TypeEqualityFunctions: false, // capturing this adds a lot of noise and have arguably little value
|
||||
GCShapeStencils: false, // capturing this adds a lot of noise and have arguably little value
|
||||
UnexportedSymbols: false, // vulnerabilities tend to track only exported symbols
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +193,7 @@ func (i *Cataloger) CatalogCtx(ctx context.Context, resolver file.Resolver) (map
|
||||
errs := sync.Collect(&ctx, cataloging.ExecutorFile, sync.ToSeq(locs), func(loc file.Location) (*file.Executable, error) {
|
||||
prog.AtomicStage.Set(loc.Path())
|
||||
|
||||
exec, err := processExecutableLocation(loc, resolver)
|
||||
exec, err := i.processExecutableLocation(loc, resolver)
|
||||
if err != nil {
|
||||
err = unknown.New(loc, err)
|
||||
}
|
||||
@@ -89,7 +213,7 @@ func (i *Cataloger) CatalogCtx(ctx context.Context, resolver file.Resolver) (map
|
||||
return results, errs
|
||||
}
|
||||
|
||||
func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file.Executable, error) {
|
||||
func (i *Cataloger) processExecutableLocation(loc file.Location, resolver file.Resolver) (*file.Executable, error) {
|
||||
reader, err := resolver.FileContentsByLocation(loc)
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Debug("unable to get file contents")
|
||||
@@ -103,7 +227,52 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file
|
||||
return nil, fmt.Errorf("unable to get union reader: %w", err)
|
||||
}
|
||||
|
||||
return processExecutable(loc, uReader)
|
||||
return i.processExecutable(loc, uReader)
|
||||
}
|
||||
|
||||
func (i *Cataloger) processExecutable(loc file.Location, reader unionreader.UnionReader) (*file.Executable, error) {
|
||||
data := file.Executable{}
|
||||
|
||||
// determine the executable format
|
||||
|
||||
format, err := findExecutableFormat(reader)
|
||||
if err != nil {
|
||||
log.Debugf("unable to determine executable kind for %v: %v", loc.RealPath, err)
|
||||
return nil, fmt.Errorf("unable to determine executable kind: %w", err)
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
// this is not an "unknown", so just log -- this binary does not have parseable data in it
|
||||
log.Debugf("unable to determine executable format for %q", loc.RealPath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data.Format = format
|
||||
|
||||
switch format {
|
||||
case file.ELF:
|
||||
if err = findELFFeatures(&data, reader, i.config.Symbols); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine ELF features")
|
||||
err = fmt.Errorf("unable to determine ELF features: %w", err)
|
||||
}
|
||||
case file.PE:
|
||||
if err = findPEFeatures(&data, reader, i.config.Symbols); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine PE features")
|
||||
err = fmt.Errorf("unable to determine PE features: %w", err)
|
||||
}
|
||||
case file.MachO:
|
||||
if err = findMachoFeatures(&data, reader, i.config.Symbols); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine Macho features")
|
||||
err = fmt.Errorf("unable to determine Macho features: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// always allocate collections for presentation
|
||||
if data.ImportedLibraries == nil {
|
||||
data.ImportedLibraries = []string{}
|
||||
}
|
||||
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||
@@ -152,51 +321,6 @@ func locationMatchesGlob(loc file.Location, globs []string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func processExecutable(loc file.Location, reader unionreader.UnionReader) (*file.Executable, error) {
|
||||
data := file.Executable{}
|
||||
|
||||
// determine the executable format
|
||||
|
||||
format, err := findExecutableFormat(reader)
|
||||
if err != nil {
|
||||
log.Debugf("unable to determine executable kind for %v: %v", loc.RealPath, err)
|
||||
return nil, fmt.Errorf("unable to determine executable kind: %w", err)
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
// this is not an "unknown", so just log -- this binary does not have parseable data in it
|
||||
log.Debugf("unable to determine executable format for %q", loc.RealPath)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
data.Format = format
|
||||
|
||||
switch format {
|
||||
case file.ELF:
|
||||
if err = findELFFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine ELF features")
|
||||
err = fmt.Errorf("unable to determine ELF features: %w", err)
|
||||
}
|
||||
case file.PE:
|
||||
if err = findPEFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine PE features")
|
||||
err = fmt.Errorf("unable to determine PE features: %w", err)
|
||||
}
|
||||
case file.MachO:
|
||||
if err = findMachoFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err, "path", loc.RealPath).Trace("unable to determine Macho features")
|
||||
err = fmt.Errorf("unable to determine Macho features: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// always allocate collections for presentation
|
||||
if data.ImportedLibraries == nil {
|
||||
data.ImportedLibraries = []string{}
|
||||
}
|
||||
|
||||
return &data, err
|
||||
}
|
||||
|
||||
func findExecutableFormat(reader unionreader.UnionReader) (file.ExecutableFormat, error) {
|
||||
// read the first sector of the file
|
||||
buf := make([]byte, 512)
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
package executable
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
func TestDefaultConfig_SymbolCaptureIsDisabled(t *testing.T) {
|
||||
// symbol capture should be disabled by default -- this is an expensive operation space-wise in the SBOM
|
||||
// and should only be enabled when explicitly configured by the user.
|
||||
cfg := DefaultConfig()
|
||||
|
||||
require.Empty(t, cfg.Symbols.CaptureScope, "symbol capture should be disabled by default (empty capture scope)")
|
||||
|
||||
assert.Equal(t, cfg.Symbols.Types, []string{"T", "t"})
|
||||
|
||||
// verify that shouldCaptureSymbols returns false for any executable when using default config
|
||||
assert.False(t, shouldCaptureSymbols(nil, cfg.Symbols), "should not capture symbols for nil executable")
|
||||
assert.False(t, shouldCaptureSymbols(&file.Executable{}, cfg.Symbols), "should not capture symbols for empty executable")
|
||||
assert.False(t, shouldCaptureSymbols(&file.Executable{
|
||||
Toolchains: []file.Toolchain{
|
||||
{Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler},
|
||||
},
|
||||
}, cfg.Symbols), "should not capture symbols even for go binaries when using default config")
|
||||
}
|
||||
|
||||
func TestConfig_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg Config
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "default config is valid",
|
||||
cfg: DefaultConfig(),
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "valid config with golang scope enabled",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "empty capture scope with Go settings is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "invalid capture scope",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{"invalid-scope"},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "invalid NM type",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
Types: []string{"X", "Y"},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "valid NM types",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
Types: []string{"T", "t", "R"},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "both exported and unexported disabled with golang scope",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: false,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "both exported and unexported disabled without golang scope is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "all module sources disabled with golang scope",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.Error,
|
||||
},
|
||||
{
|
||||
name: "all module sources disabled without golang scope is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only standard library enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only extended stdlib enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only third party modules enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
{
|
||||
name: "only unexported symbols enabled is valid",
|
||||
cfg: Config{
|
||||
Symbols: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: require.NoError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cfg.Validate()
|
||||
tt.wantErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymbolConfig_Validate_ErrorMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg SymbolConfig
|
||||
wantErrContain string
|
||||
}{
|
||||
{
|
||||
name: "invalid capture scope error message",
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{"rust"},
|
||||
},
|
||||
wantErrContain: "invalid symbol capture scope",
|
||||
},
|
||||
{
|
||||
name: "invalid NM type error message",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"Z"},
|
||||
},
|
||||
wantErrContain: "invalid NM type",
|
||||
},
|
||||
{
|
||||
name: "both export options disabled error message",
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: false,
|
||||
UnexportedSymbols: false,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
wantErrContain: "both exported-symbols and unexported-symbols are disabled",
|
||||
},
|
||||
{
|
||||
name: "all module sources disabled error message",
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{SymbolScopeGolang},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
ExtendedStandardLibrary: false,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
wantErrContain: "all module sources",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.cfg.Validate()
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tt.wantErrContain)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader, cfg SymbolConfig) error {
|
||||
f, err := elf.NewFile(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -34,10 +34,168 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) erro
|
||||
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
||||
data.HasEntrypoint = elfHasEntrypoint(f)
|
||||
data.HasExports = elfHasExports(f)
|
||||
data.Toolchains = elfToolchains(reader, f)
|
||||
if shouldCaptureSymbols(data, cfg) {
|
||||
data.SymbolNames = elfNMSymbols(f, cfg, data.Toolchains)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func elfToolchains(reader unionreader.UnionReader, f *elf.File) []file.Toolchain {
|
||||
return includeNoneNil(
|
||||
golangToolchainEvidence(reader),
|
||||
cToolchainEvidence(f),
|
||||
)
|
||||
}
|
||||
|
||||
func includeNoneNil(evidence ...*file.Toolchain) []file.Toolchain {
|
||||
var toolchains []file.Toolchain
|
||||
for _, e := range evidence {
|
||||
if e != nil {
|
||||
toolchains = append(toolchains, *e)
|
||||
}
|
||||
}
|
||||
return toolchains
|
||||
}
|
||||
|
||||
func elfNMSymbols(f *elf.File, cfg SymbolConfig, toolchains []file.Toolchain) []string {
|
||||
if isGoToolchainPresent(toolchains) {
|
||||
return captureElfGoSymbols(f, cfg)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
syms, err := f.Symbols()
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Trace("unable to read symbols from elf file")
|
||||
return nil
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
filter := createGoSymbolFilter(cfg)
|
||||
for _, sym := range syms {
|
||||
name, include := filter(sym.Name, elfSymbolType(sym, f.Sections))
|
||||
if include {
|
||||
symbols = append(symbols, name)
|
||||
}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
// elfSymbolType returns the nm-style single character representing the symbol type.
|
||||
// This mimics the output of `nm` for ELF binaries.
|
||||
func elfSymbolType(sym elf.Symbol, sections []*elf.Section) string {
|
||||
binding := elf.ST_BIND(sym.Info)
|
||||
symType := elf.ST_TYPE(sym.Info)
|
||||
|
||||
// handle special section indices first
|
||||
switch sym.Section {
|
||||
case elf.SHN_UNDEF:
|
||||
// undefined symbols
|
||||
if binding == elf.STB_WEAK {
|
||||
if symType == elf.STT_OBJECT {
|
||||
return "v" // weak object
|
||||
}
|
||||
return "w" // weak symbol
|
||||
}
|
||||
return "U" // undefined (always uppercase)
|
||||
|
||||
case elf.SHN_ABS:
|
||||
// absolute symbols
|
||||
if binding == elf.STB_LOCAL {
|
||||
return "a"
|
||||
}
|
||||
return "A"
|
||||
|
||||
case elf.SHN_COMMON:
|
||||
// common symbols (uninitialized data)
|
||||
return "C" // always uppercase per nm convention
|
||||
}
|
||||
|
||||
// for defined symbols, determine type based on section characteristics
|
||||
typeChar := elfSectionTypeChar(sym.Section, sections)
|
||||
|
||||
// handle weak symbols
|
||||
if binding == elf.STB_WEAK {
|
||||
if typeChar == 'U' || typeChar == 'u' {
|
||||
if symType == elf.STT_OBJECT {
|
||||
return "v"
|
||||
}
|
||||
return "w"
|
||||
}
|
||||
// weak defined symbol
|
||||
if binding == elf.STB_LOCAL {
|
||||
return strings.ToLower(string(typeChar))
|
||||
}
|
||||
// use 'W' for weak defined, or 'V' for weak object
|
||||
if symType == elf.STT_OBJECT {
|
||||
return "V"
|
||||
}
|
||||
return "W"
|
||||
}
|
||||
|
||||
// local symbols are lowercase, global symbols are uppercase
|
||||
if binding == elf.STB_LOCAL {
|
||||
return strings.ToLower(string(typeChar))
|
||||
}
|
||||
return string(typeChar)
|
||||
}
|
||||
|
||||
// elfSectionTypeChar returns the nm-style character based on section flags and type.
|
||||
func elfSectionTypeChar(sectIdx elf.SectionIndex, sections []*elf.Section) byte {
|
||||
idx := int(sectIdx)
|
||||
// the sections slice from debug/elf includes the NULL section at index 0, so we use idx directly
|
||||
if idx < 0 || idx >= len(sections) {
|
||||
return '?'
|
||||
}
|
||||
|
||||
section := sections[idx]
|
||||
flags := section.Flags
|
||||
stype := section.Type
|
||||
|
||||
// check section characteristics to determine symbol type
|
||||
switch {
|
||||
case flags&elf.SHF_EXECINSTR != 0:
|
||||
// executable section -> text
|
||||
return 'T'
|
||||
|
||||
case stype == elf.SHT_NOBITS:
|
||||
// uninitialized data section -> BSS
|
||||
return 'B'
|
||||
|
||||
case flags&elf.SHF_WRITE == 0 && flags&elf.SHF_ALLOC != 0:
|
||||
// read-only allocated section -> rodata
|
||||
return 'R'
|
||||
|
||||
case flags&elf.SHF_WRITE != 0 && flags&elf.SHF_ALLOC != 0:
|
||||
// writable allocated section -> data
|
||||
return 'D'
|
||||
|
||||
case flags&elf.SHF_ALLOC != 0:
|
||||
// other allocated section
|
||||
return 'D'
|
||||
|
||||
default:
|
||||
// non-allocated sections (debug info, etc.)
|
||||
// nm typically shows 'n' for debug, but we'll use 'N' for consistency
|
||||
return 'N'
|
||||
}
|
||||
}
|
||||
|
||||
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
||||
return &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: isElfSymbolTableStripped(f),
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -226,3 +227,350 @@ func Test_elfHasExports(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfGoToolchainDetection(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
wantPresent bool
|
||||
}{
|
||||
{
|
||||
name: "go binary has toolchain",
|
||||
fixture: "bin/hello_linux",
|
||||
wantPresent: true,
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
toolchains := elfToolchains(reader, f)
|
||||
assert.Equal(t, tt.wantPresent, isGoToolchainPresent(toolchains))
|
||||
|
||||
if tt.wantPresent {
|
||||
require.NotEmpty(t, toolchains)
|
||||
assert.Equal(t, "go", toolchains[0].Name)
|
||||
assert.NotEmpty(t, toolchains[0].Version)
|
||||
assert.Equal(t, file.ToolchainKindCompiler, toolchains[0].Kind)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfCgoToolchainDetection(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
t.Run("cgo binary has both go and c toolchains", func(t *testing.T) {
|
||||
reader := readerForFixture(t, "bin/hello_linux_cgo")
|
||||
f, err := elf.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := elfToolchains(reader, f)
|
||||
|
||||
// versions are dynamic based on Docker image, so we ignore them in comparison
|
||||
want := []file.Toolchain{
|
||||
{Name: "go", Kind: file.ToolchainKindCompiler},
|
||||
{Name: "gcc", Kind: file.ToolchainKindCompiler},
|
||||
}
|
||||
|
||||
if d := cmp.Diff(want, toolchains, cmpopts.IgnoreFields(file.Toolchain{}, "Version")); d != "" {
|
||||
t.Errorf("elfToolchains() mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
|
||||
// verify versions are populated
|
||||
for _, tc := range toolchains {
|
||||
assert.NotEmpty(t, tc.Version, "expected version to be set for %s toolchain", tc.Name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func Test_elfGoSymbolCapture(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
cfg SymbolConfig
|
||||
wantSymbols []string // exact symbol names that must be present
|
||||
wantMinSymbolCount int
|
||||
}{
|
||||
{
|
||||
name: "capture all symbol types",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
// stdlib - fmt package (used via fmt.Println)
|
||||
"fmt.(*fmt).fmtInteger",
|
||||
"fmt.(*pp).doPrintf",
|
||||
// stdlib - strings package (used via strings.ToUpper)
|
||||
"strings.ToUpper",
|
||||
"strings.Map",
|
||||
// stdlib - encoding/json package (used via json.Marshal)
|
||||
"encoding/json.Marshal",
|
||||
// extended stdlib - golang.org/x/text (used via language.English)
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Language.String",
|
||||
// third-party - go-spew (used via spew.Dump)
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
wantMinSymbolCount: 50,
|
||||
},
|
||||
{
|
||||
name: "capture only third-party symbols",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.(*formatState).Format",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture only extended stdlib symbols",
|
||||
fixture: "bin/hello_linux",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExtendedStandardLibrary: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Parse",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture with text section types only",
|
||||
fixture: "bin/hello_linux",
|
||||
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",
|
||||
"strings.ToUpper",
|
||||
},
|
||||
wantMinSymbolCount: 10,
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
symbols := captureElfGoSymbols(f, tt.cfg)
|
||||
symbolSet := make(map[string]struct{}, len(symbols))
|
||||
for _, sym := range symbols {
|
||||
symbolSet[sym] = struct{}{}
|
||||
}
|
||||
|
||||
if tt.wantMinSymbolCount > 0 {
|
||||
assert.GreaterOrEqual(t, len(symbols), tt.wantMinSymbolCount,
|
||||
"expected at least %d symbols, got %d", tt.wantMinSymbolCount, len(symbols))
|
||||
}
|
||||
|
||||
for _, want := range tt.wantSymbols {
|
||||
_, found := symbolSet[want]
|
||||
assert.True(t, found, "expected symbol %q to be present", want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfNMSymbols_goReturnsSymbols(t *testing.T) {
|
||||
// for Go binaries, elfNMSymbols should return symbols when Go toolchain is present
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
reader := readerForFixture(t, "bin/hello_linux")
|
||||
f, err := elf.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := []file.Toolchain{
|
||||
{Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler},
|
||||
}
|
||||
cfg := SymbolConfig{
|
||||
Types: []string{"T", "t"},
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
},
|
||||
}
|
||||
|
||||
symbols := elfNMSymbols(f, cfg, toolchains)
|
||||
assert.NotNil(t, symbols, "expected symbols for Go binary")
|
||||
assert.NotEmpty(t, symbols, "expected non-empty symbols for Go binary")
|
||||
}
|
||||
|
||||
func Test_elfSymbolType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sym elf.Symbol
|
||||
sections []*elf.Section
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "undefined symbol",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_UNDEF,
|
||||
},
|
||||
want: "U",
|
||||
},
|
||||
{
|
||||
name: "absolute symbol global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_ABS,
|
||||
},
|
||||
want: "A",
|
||||
},
|
||||
{
|
||||
name: "absolute symbol local",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_LOCAL)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_ABS,
|
||||
},
|
||||
want: "a",
|
||||
},
|
||||
{
|
||||
name: "common symbol",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: elf.SHN_COMMON,
|
||||
},
|
||||
want: "C",
|
||||
},
|
||||
{
|
||||
name: "weak undefined symbol",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_WEAK)<<4 | byte(elf.STT_NOTYPE),
|
||||
Section: elf.SHN_UNDEF,
|
||||
},
|
||||
want: "w",
|
||||
},
|
||||
{
|
||||
name: "weak undefined object",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_WEAK)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: elf.SHN_UNDEF,
|
||||
},
|
||||
want: "v",
|
||||
},
|
||||
{
|
||||
name: "text section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_FUNC),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}}, // index 1: .text
|
||||
},
|
||||
want: "T",
|
||||
},
|
||||
{
|
||||
name: "text section local",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_LOCAL)<<4 | byte(elf.STT_FUNC),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC | elf.SHF_EXECINSTR}}, // index 1: .text
|
||||
},
|
||||
want: "t",
|
||||
},
|
||||
{
|
||||
name: "data section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC | elf.SHF_WRITE}}, // index 1: .data
|
||||
},
|
||||
want: "D",
|
||||
},
|
||||
{
|
||||
name: "bss section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NOBITS, Flags: elf.SHF_ALLOC | elf.SHF_WRITE}}, // index 1: .bss
|
||||
},
|
||||
want: "B",
|
||||
},
|
||||
{
|
||||
name: "rodata section global",
|
||||
sym: elf.Symbol{
|
||||
Info: byte(elf.STB_GLOBAL)<<4 | byte(elf.STT_OBJECT),
|
||||
Section: 1,
|
||||
},
|
||||
sections: []*elf.Section{
|
||||
{SectionHeader: elf.SectionHeader{Type: elf.SHT_NULL}}, // index 0: NULL section
|
||||
{SectionHeader: elf.SectionHeader{Flags: elf.SHF_ALLOC}}, // index 1: .rodata (no write flag = read-only)
|
||||
},
|
||||
want: "R",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := elfSymbolType(tt.sym, tt.sections)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package executable
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
)
|
||||
|
||||
var goNMTypes = []string{
|
||||
"T", // text (code) segment symbol
|
||||
"t", // static text segment symbol
|
||||
"R", // read-only data segment symbol
|
||||
"r", // static read-only data segment symbol
|
||||
"D", // data segment symbol
|
||||
"d", // static data segment symbol
|
||||
"B", // bss segment symbol
|
||||
"b", // static bss segment symbol
|
||||
"C", // constant address
|
||||
"U", // referenced but undefined symbol
|
||||
}
|
||||
|
||||
// validNMTypes returns the list of valid NM types for Go symbols.
|
||||
func validNMTypes() []string {
|
||||
return goNMTypes
|
||||
}
|
||||
|
||||
// isValidNMType checks if the given type is a valid NM type.
|
||||
func isValidNMType(t string) bool {
|
||||
for _, valid := range goNMTypes {
|
||||
if t == valid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
vendorPrefix = "vendor/"
|
||||
extendedStdlibPrefix = "golang.org/x/"
|
||||
typeEqualityPrefix = "type:.eq."
|
||||
gcShapeStencilPrefix = "go.shape."
|
||||
)
|
||||
|
||||
// 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 SymbolConfig) func(string, string) (string, bool) {
|
||||
validNmTypes := buildNmTypes(cfg.Types)
|
||||
|
||||
return func(symName, symType string) (string, bool) {
|
||||
// check if this is a valid type
|
||||
if !validNmTypes.Has(symType) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// filter out floating point literals and other compiler literals (e.g., $f64.3fceb851eb851eb8)
|
||||
if isCompilerLiteral(symName) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// filter based on exported/unexported symbol configuration
|
||||
exported := isExportedSymbol(symName)
|
||||
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.Go.TypeEqualityFunctions {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// handle GC shape stencil functions (e.g., go.shape.func())
|
||||
if isGCShapeStencil(symName) {
|
||||
if !cfg.Go.GCShapeStencils {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// normalize vendored module paths if configured
|
||||
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.Go.ExtendedStandardLibrary {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// handle stdlib packages
|
||||
if isStdlibPackage(pkgPath) {
|
||||
if !cfg.Go.StandardLibrary {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
|
||||
// this is a third-party package
|
||||
if !cfg.Go.ThirdPartyModules {
|
||||
return "", false
|
||||
}
|
||||
return symName, true
|
||||
}
|
||||
}
|
||||
|
||||
// buildNmTypes creates a set of valid NM types from the configuration.
|
||||
// If no types are specified, all default types are used.
|
||||
func buildNmTypes(types []string) *strset.Set {
|
||||
tys := strset.New(types...)
|
||||
if tys.Size() == 0 {
|
||||
return strset.New(goNMTypes...)
|
||||
}
|
||||
|
||||
// only allow valid nm types to continue...
|
||||
return strset.Intersection(strset.New(goNMTypes...), tys)
|
||||
}
|
||||
|
||||
// isCompilerLiteral checks if a symbol is literal symbol inserted by the compiler.
|
||||
// This includes floating point literals, int constants, and others.
|
||||
// These have the format: $f64.3fceb851eb851eb8 or $f32.3f800000
|
||||
// where the hex represents the IEEE 754 representation of the value.
|
||||
func isCompilerLiteral(symName string) bool {
|
||||
return strings.HasPrefix(symName, "$")
|
||||
}
|
||||
|
||||
// shouldIncludeByExportStatus determines if a symbol should be included based on its
|
||||
// export status and the configuration settings for exported/unexported symbols.
|
||||
func shouldIncludeByExportStatus(exported, includeExported, includeUnexported bool) bool {
|
||||
if exported && !includeExported {
|
||||
return false
|
||||
}
|
||||
if !exported && !includeUnexported {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isTypeEqualityFunction checks if a symbol is a compiler-generated type equality function.
|
||||
// These are automatically generated by the Go compiler for generic types
|
||||
// and have the format: type:.eq.TypeName
|
||||
func isTypeEqualityFunction(symName string) bool {
|
||||
return strings.HasPrefix(symName, typeEqualityPrefix)
|
||||
}
|
||||
|
||||
// isGCShapeStencil checks if a symbol is a GC shape stencil function.
|
||||
// These are related to how generics are implemented and are not user defined or directly callable.
|
||||
// They can appear as a prefix (e.g., "go.shape.func()") or within generic type parameters
|
||||
// (e.g., "slices.partitionCmpFunc[go.shape.struct { ... }]").
|
||||
func isGCShapeStencil(symName string) bool {
|
||||
// check for prefix: go.shape.*
|
||||
if strings.HasPrefix(symName, gcShapeStencilPrefix) {
|
||||
return true
|
||||
}
|
||||
// check for embedded in generic type parameter: [go.shape.*
|
||||
return strings.Contains(symName, "["+gcShapeStencilPrefix)
|
||||
}
|
||||
|
||||
// normalizeVendoredPath removes the "vendor/" prefix from vendored module paths if normalization is enabled.
|
||||
func normalizeVendoredPath(symName string, normalize bool) string {
|
||||
if normalize && isVendoredPath(symName) {
|
||||
return strings.TrimPrefix(symName, vendorPrefix)
|
||||
}
|
||||
return symName
|
||||
}
|
||||
|
||||
// isVendoredPath checks if a symbol name represents a vendored module path.
|
||||
func isVendoredPath(symName string) bool {
|
||||
return strings.HasPrefix(symName, vendorPrefix)
|
||||
}
|
||||
|
||||
// isExtendedStdlib checks if a package path is part of the Go extended standard library (golang.org/x/*).
|
||||
func isExtendedStdlib(pkgPath string) bool {
|
||||
return strings.HasPrefix(pkgPath, extendedStdlibPrefix)
|
||||
}
|
||||
|
||||
// extractPackagePath extracts the package import path from a symbol name.
|
||||
// For example, "github.com/foo/bar.Baz" returns "github.com/foo/bar".
|
||||
func extractPackagePath(symName string) string {
|
||||
lastDot := strings.LastIndex(symName, ".")
|
||||
if lastDot == -1 {
|
||||
return symName
|
||||
}
|
||||
return symName[:lastDot]
|
||||
}
|
||||
|
||||
// isExportedSymbol checks if a symbol is exported (public) by examining if the first
|
||||
// character of the symbol name (after the last '.') is uppercase.
|
||||
func isExportedSymbol(symName string) bool {
|
||||
lastDot := strings.LastIndex(symName, ".")
|
||||
if lastDot == -1 || lastDot >= len(symName)-1 {
|
||||
return false
|
||||
}
|
||||
firstRune, _ := utf8.DecodeRuneInString(symName[lastDot+1:])
|
||||
return unicode.IsUpper(firstRune)
|
||||
}
|
||||
|
||||
// isStdlibPackage determines if a package path represents a Go standard library package.
|
||||
// Stdlib packages don't contain a '.' in their path (they use simple names like "fmt", "net/http").
|
||||
// Third-party packages start with a domain containing a '.' (e.g., "github.com", "golang.org").
|
||||
func isStdlibPackage(pkgPath string) bool {
|
||||
// the "main" package is treated as stdlib for our purposes
|
||||
if pkgPath == "main" {
|
||||
return true
|
||||
}
|
||||
// stdlib packages don't contain dots in their import path
|
||||
return !strings.Contains(pkgPath, ".")
|
||||
}
|
||||
@@ -0,0 +1,989 @@
|
||||
package executable
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_buildNmTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
types []string
|
||||
wantSize int
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "empty types uses defaults",
|
||||
types: nil,
|
||||
wantSize: len(goNMTypes),
|
||||
contains: []string{"T", "t", "R", "r", "D", "d", "B", "b", "C", "U"},
|
||||
},
|
||||
{
|
||||
name: "custom types",
|
||||
types: []string{"T", "t"},
|
||||
wantSize: 2,
|
||||
contains: []string{"T", "t"},
|
||||
},
|
||||
{
|
||||
name: "invalid types",
|
||||
types: []string{"T", "t", "m", ",", "thing!"},
|
||||
wantSize: 2,
|
||||
contains: []string{"T", "t"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := buildNmTypes(tt.types)
|
||||
assert.Equal(t, tt.wantSize, got.Size())
|
||||
for _, c := range tt.contains {
|
||||
assert.True(t, got.Has(c), "expected set to contain %q", c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isCompilerLiteral(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "64-bit float literal",
|
||||
symName: "$f64.3fceb851eb851eb8",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "32-bit float literal",
|
||||
symName: "$f32.3f800000",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "other dollar prefix",
|
||||
symName: "$something",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular symbol",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isCompilerLiteral(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_shouldIncludeByExportStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
exported bool
|
||||
includeExported bool
|
||||
includeUnexported bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exported symbol with both enabled",
|
||||
exported: true,
|
||||
includeExported: true,
|
||||
includeUnexported: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with both enabled",
|
||||
exported: false,
|
||||
includeExported: true,
|
||||
includeUnexported: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exported symbol with only exported enabled",
|
||||
exported: true,
|
||||
includeExported: true,
|
||||
includeUnexported: false,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with only exported enabled",
|
||||
exported: false,
|
||||
includeExported: true,
|
||||
includeUnexported: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exported symbol with only unexported enabled",
|
||||
exported: true,
|
||||
includeExported: false,
|
||||
includeUnexported: true,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with only unexported enabled",
|
||||
exported: false,
|
||||
includeExported: false,
|
||||
includeUnexported: true,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "exported symbol with both disabled",
|
||||
exported: true,
|
||||
includeExported: false,
|
||||
includeUnexported: false,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with both disabled",
|
||||
exported: false,
|
||||
includeExported: false,
|
||||
includeUnexported: false,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := shouldIncludeByExportStatus(tt.exported, tt.includeExported, tt.includeUnexported)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isTypeEqualityFunction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "type equality function",
|
||||
symName: "type:.eq.myStruct",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "type equality with package",
|
||||
symName: "type:.eq.main.MyType",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular function",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "similar but not type equality",
|
||||
symName: "mytype:.eq.something",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isTypeEqualityFunction(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isGCShapeStencil(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "gc shape stencil function prefix",
|
||||
symName: "go.shape.func()",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape with type prefix",
|
||||
symName: "go.shape.int",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic type parameter - struct",
|
||||
symName: `slices.partitionCmpFunc[go.shape.struct { Key string "json:\"key,omitempty\""; Value go.opentelemetry.io/otel/trace/internal/telemetry.Value "json:\"value,omitempty\"" }]`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic type parameter - interface",
|
||||
symName: "slices.pdqsortCmpFunc[go.shape.interface { Info() (io/fs.FileInfo, error); IsDir() bool; Name() string; Type() io/fs.FileMode }]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic - syft location",
|
||||
symName: `slices.partitionCmpFunc[go.shape.struct { github.com/anchore/syft/syft/file.LocationData "cyclonedx:\"\""; github.com/anchore/syft/syft/file.LocationMetadata "cyclonedx:\"\"" }]`,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape in generic - rotate",
|
||||
symName: "slices.rotateCmpFunc[go.shape.struct { Key go.opentelemetry.io/otel/attribute.Key; Value go.opentelemetry.io/otel/attribute.Value }]",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular function",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "go package but not shape",
|
||||
symName: "go.string.something",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "generic without go.shape",
|
||||
symName: "slices.Sort[int]",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "go.shape in comment or string would not match",
|
||||
symName: "mypackage.FuncWithComment",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isGCShapeStencil(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_normalizeVendoredPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
normalize bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "vendored path with normalization enabled",
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
normalize: true,
|
||||
want: "github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "vendored path with normalization disabled",
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
normalize: false,
|
||||
want: "vendor/github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "non-vendored path with normalization enabled",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
normalize: true,
|
||||
want: "github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "non-vendored path with normalization disabled",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
normalize: false,
|
||||
want: "github.com/foo/bar.Baz",
|
||||
},
|
||||
{
|
||||
name: "stdlib path with normalization enabled",
|
||||
symName: "fmt.Println",
|
||||
normalize: true,
|
||||
want: "fmt.Println",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := normalizeVendoredPath(tt.symName, tt.normalize)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isVendoredPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "vendored third-party",
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "non-vendored third-party",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "stdlib",
|
||||
symName: "fmt.Println",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "main package",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isVendoredPath(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isExtendedStdlib(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgPath string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "golang.org/x/net",
|
||||
pkgPath: "golang.org/x/net",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "golang.org/x/text/encoding",
|
||||
pkgPath: "golang.org/x/text/encoding",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "golang.org/x/sys/unix",
|
||||
pkgPath: "golang.org/x/sys/unix",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "regular golang.org package",
|
||||
pkgPath: "golang.org/protobuf",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "github package",
|
||||
pkgPath: "github.com/foo/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "stdlib",
|
||||
pkgPath: "fmt",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
pkgPath: "",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isExtendedStdlib(tt.pkgPath)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_extractPackagePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple package",
|
||||
symName: "fmt.Println",
|
||||
want: "fmt",
|
||||
},
|
||||
{
|
||||
name: "nested stdlib package",
|
||||
symName: "net/http.ListenAndServe",
|
||||
want: "net/http",
|
||||
},
|
||||
{
|
||||
name: "third-party package",
|
||||
symName: "github.com/foo/bar.Baz",
|
||||
want: "github.com/foo/bar",
|
||||
},
|
||||
{
|
||||
name: "deep third-party package",
|
||||
symName: "github.com/foo/bar/pkg/util.Helper",
|
||||
want: "github.com/foo/bar/pkg/util",
|
||||
},
|
||||
{
|
||||
name: "main package",
|
||||
symName: "main.main",
|
||||
want: "main",
|
||||
},
|
||||
{
|
||||
name: "no dot (just package name)",
|
||||
symName: "fmt",
|
||||
want: "fmt",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "method with receiver",
|
||||
symName: "github.com/foo/bar.(*MyType).Method",
|
||||
want: "github.com/foo/bar.(*MyType)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractPackagePath(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isExportedSymbol(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
symName string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "exported function",
|
||||
symName: "fmt.Println",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported function",
|
||||
symName: "fmt.println",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exported in main",
|
||||
symName: "main.Main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported main",
|
||||
symName: "main.main",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exported third-party",
|
||||
symName: "github.com/foo/bar.Export",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unexported third-party",
|
||||
symName: "github.com/foo/bar.private",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "unicode uppercase",
|
||||
symName: "main.Über",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unicode lowercase",
|
||||
symName: "main.über",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no dot",
|
||||
symName: "nodot",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
symName: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "dot at end",
|
||||
symName: "main.",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "underscore start (unexported)",
|
||||
symName: "main._private",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isExportedSymbol(tt.symName)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isStdlibPackage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgPath string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "fmt",
|
||||
pkgPath: "fmt",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "net/http",
|
||||
pkgPath: "net/http",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "crypto/sha256",
|
||||
pkgPath: "crypto/sha256",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "main",
|
||||
pkgPath: "main",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "runtime",
|
||||
pkgPath: "runtime",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "github.com third-party",
|
||||
pkgPath: "github.com/foo/bar",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "golang.org/x extended stdlib",
|
||||
pkgPath: "golang.org/x/net",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "gopkg.in third-party",
|
||||
pkgPath: "gopkg.in/yaml.v3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
pkgPath: "",
|
||||
want: true, // no dots means stdlib by our heuristic
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isStdlibPackage(tt.pkgPath)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_createGoSymbolFilter(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg SymbolConfig
|
||||
symName string
|
||||
symType string
|
||||
wantName string
|
||||
keep bool
|
||||
}{
|
||||
// NM type filtering
|
||||
{
|
||||
name: "valid NM type with defaults",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "invalid NM type with defaults",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "X", // important!
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
{
|
||||
name: "custom NM types - included",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"T"},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "custom NM types - excluded",
|
||||
cfg: SymbolConfig{
|
||||
Types: []string{"T"},
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "t",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// floating point literal filtering
|
||||
{
|
||||
name: "floating point literal filtered",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "$f64.3fceb851eb851eb8",
|
||||
symType: "R",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// export status filtering
|
||||
{
|
||||
name: "exported symbol with only exported enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: false,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "unexported symbol with only exported enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: false,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.println",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// type equality functions
|
||||
{
|
||||
name: "type equality function - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
TypeEqualityFunctions: true,
|
||||
},
|
||||
},
|
||||
symName: "type:.eq.myStruct",
|
||||
symType: "T",
|
||||
wantName: "type:.eq.myStruct",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "type equality function - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
TypeEqualityFunctions: false,
|
||||
},
|
||||
},
|
||||
symName: "type:.eq.myStruct",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// GC shape stencils
|
||||
{
|
||||
name: "gc shape stencil - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: true,
|
||||
},
|
||||
},
|
||||
symName: "go.shape.func()",
|
||||
symType: "T",
|
||||
wantName: "go.shape.func()",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape stencil - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: false,
|
||||
},
|
||||
},
|
||||
symName: "go.shape.func()",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
{
|
||||
name: "gc shape stencil embedded in generic - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: true,
|
||||
},
|
||||
},
|
||||
symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
|
||||
symType: "T",
|
||||
wantName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "gc shape stencil embedded in generic - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
GCShapeStencils: false,
|
||||
},
|
||||
},
|
||||
symName: "slices.partitionCmpFunc[go.shape.struct { Key string; Value int }]",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// vendored module normalization
|
||||
{
|
||||
name: "vendored path - normalization enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
NormalizeVendoredModules: true,
|
||||
},
|
||||
},
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
symType: "T",
|
||||
wantName: "github.com/foo/bar.Baz",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "vendored path - normalization disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
NormalizeVendoredModules: false,
|
||||
},
|
||||
},
|
||||
symName: "vendor/github.com/foo/bar.Baz",
|
||||
symType: "T",
|
||||
wantName: "vendor/github.com/foo/bar.Baz",
|
||||
keep: true,
|
||||
},
|
||||
|
||||
// extended stdlib
|
||||
{
|
||||
name: "extended stdlib - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "golang.org/x/net/html.Parse",
|
||||
symType: "T",
|
||||
wantName: "golang.org/x/net/html.Parse",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "extended stdlib - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ExtendedStandardLibrary: false,
|
||||
},
|
||||
},
|
||||
symName: "golang.org/x/net/html.Parse",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// stdlib
|
||||
{
|
||||
name: "stdlib - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "fmt.Println",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "stdlib - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
},
|
||||
},
|
||||
symName: "fmt.Println",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
{
|
||||
name: "nested stdlib - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "net/http.ListenAndServe",
|
||||
symType: "T",
|
||||
wantName: "net/http.ListenAndServe",
|
||||
keep: true,
|
||||
},
|
||||
|
||||
// third party
|
||||
{
|
||||
name: "third party - enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: true,
|
||||
},
|
||||
},
|
||||
symName: "github.com/spf13/cobra.Command",
|
||||
symType: "T",
|
||||
wantName: "github.com/spf13/cobra.Command",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "third party - disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
ThirdPartyModules: false,
|
||||
},
|
||||
},
|
||||
symName: "github.com/spf13/cobra.Command",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
|
||||
// main package (treated as stdlib)
|
||||
{
|
||||
name: "main package - stdlib enabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: true,
|
||||
},
|
||||
},
|
||||
symName: "main.main",
|
||||
symType: "T",
|
||||
wantName: "main.main",
|
||||
keep: true,
|
||||
},
|
||||
{
|
||||
name: "main package - stdlib disabled",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
StandardLibrary: false,
|
||||
},
|
||||
},
|
||||
symName: "main.main",
|
||||
symType: "T",
|
||||
wantName: "",
|
||||
keep: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filter := createGoSymbolFilter(tt.cfg)
|
||||
require.NotNil(t, filter)
|
||||
|
||||
gotName, gotKeep := filter(tt.symName, tt.symType)
|
||||
assert.Equal(t, tt.keep, gotKeep)
|
||||
if gotKeep {
|
||||
assert.Equal(t, tt.wantName, gotName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,24 @@ import (
|
||||
|
||||
// source http://www.cilinder.be/docs/next/NeXTStep/3.3/nd/DevTools/14_MachO/MachO.htmld/index.html
|
||||
const (
|
||||
machoNPExt uint8 = 0x10 /* N_PEXT: private external symbol bit */
|
||||
machoNExt uint8 = 0x01 /* N_EXT: external symbol bit, set for external symbols */
|
||||
machoNStab uint8 = 0xe0 // N_STAB mask for debugging symbols
|
||||
machoNPExt uint8 = 0x10 // N_PEXT: private external symbol bit
|
||||
machoNType uint8 = 0x0e // N_TYPE mask for symbol type
|
||||
machoNExt uint8 = 0x01 // N_EXT: external symbol bit
|
||||
|
||||
// N_TYPE values (after masking with 0x0e)
|
||||
machoNUndf uint8 = 0x00 // undefined symbol
|
||||
machoNAbs uint8 = 0x02 // absolute symbol
|
||||
machoNSect uint8 = 0x0e // defined in section
|
||||
machoNPbud uint8 = 0x0c // prebound undefined
|
||||
machoNIndr uint8 = 0x0a // indirect symbol
|
||||
|
||||
// > #define LC_REQ_DYLD 0x80000000
|
||||
// > #define LC_MAIN (0x28|LC_REQ_DYLD) /* replacement for LC_UNIXTHREAD */
|
||||
lcMain = 0x28 | 0x80000000
|
||||
)
|
||||
|
||||
func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader, cfg SymbolConfig) error {
|
||||
// TODO: support security features
|
||||
|
||||
// a universal binary may have multiple architectures, so we need to check each one
|
||||
@@ -26,7 +36,7 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) er
|
||||
return err
|
||||
}
|
||||
|
||||
var libs []string
|
||||
var libs, symbols []string
|
||||
for _, r := range readers {
|
||||
f, err := macho.NewFile(r)
|
||||
if err != nil {
|
||||
@@ -48,14 +58,108 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) er
|
||||
if !data.HasExports {
|
||||
data.HasExports = machoHasExports(f)
|
||||
}
|
||||
|
||||
data.Toolchains = machoToolchains(reader)
|
||||
if shouldCaptureSymbols(data, cfg) {
|
||||
symbols = machoNMSymbols(f, cfg, data.Toolchains)
|
||||
}
|
||||
}
|
||||
|
||||
// de-duplicate libraries
|
||||
// de-duplicate libraries andn symbols
|
||||
data.ImportedLibraries = internal.NewSet(libs...).ToSlice()
|
||||
data.SymbolNames = internal.NewSet(symbols...).ToSlice()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func machoToolchains(reader unionreader.UnionReader) []file.Toolchain {
|
||||
return includeNoneNil(
|
||||
golangToolchainEvidence(reader),
|
||||
)
|
||||
}
|
||||
|
||||
func machoNMSymbols(f *macho.File, cfg SymbolConfig, toolchains []file.Toolchain) []string {
|
||||
if isGoToolchainPresent(toolchains) {
|
||||
return captureMachoGoSymbols(f, cfg)
|
||||
}
|
||||
|
||||
// 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)
|
||||
for _, sym := range f.Symtab.Syms {
|
||||
name, include := filter(sym.Name, machoSymbolType(sym, f.Sections))
|
||||
if include {
|
||||
symbols = append(symbols, name)
|
||||
}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
func machoSymbolType(s macho.Symbol, sections []*macho.Section) string {
|
||||
// stab (debugging) symbols get '-'
|
||||
if s.Type&machoNStab != 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
isExternal := s.Type&machoNExt != 0
|
||||
symType := s.Type & machoNType
|
||||
|
||||
var typeChar byte
|
||||
switch symType {
|
||||
case machoNUndf, machoNPbud:
|
||||
typeChar = 'U'
|
||||
case machoNAbs:
|
||||
typeChar = 'A'
|
||||
case machoNSect:
|
||||
typeChar = machoSectionTypeChar(s.Sect, sections)
|
||||
case machoNIndr:
|
||||
typeChar = 'I'
|
||||
default:
|
||||
typeChar = '?'
|
||||
}
|
||||
|
||||
// lowercase for local symbols, uppercase for external
|
||||
if !isExternal && typeChar != '-' && typeChar != '?' {
|
||||
typeChar += 32 // convert to lowercase
|
||||
}
|
||||
|
||||
return string(typeChar)
|
||||
}
|
||||
|
||||
// machoSectionTypeChar returns the nm-style character for a section-defined symbol.
|
||||
// Section numbers are 1-based; 0 means NO_SECT.
|
||||
func machoSectionTypeChar(sect uint8, sections []*macho.Section) byte {
|
||||
if sect == 0 || int(sect) > len(sections) {
|
||||
return 'S'
|
||||
}
|
||||
|
||||
section := sections[sect-1]
|
||||
seg := section.Seg
|
||||
|
||||
// match nm behavior based on segment and section names
|
||||
switch seg {
|
||||
case "__TEXT":
|
||||
return 'T'
|
||||
case "__DATA", "__DATA_CONST":
|
||||
switch section.Name {
|
||||
case "__bss", "__common":
|
||||
return 'B'
|
||||
default:
|
||||
return 'D'
|
||||
}
|
||||
default:
|
||||
return 'S'
|
||||
}
|
||||
}
|
||||
|
||||
func machoHasEntrypoint(f *macho.File) bool {
|
||||
// derived from struct entry_point_command found from which explicitly calls out LC_MAIN:
|
||||
// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
|
||||
|
||||
@@ -112,7 +112,7 @@ func Test_machoUniversal(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var data file.Executable
|
||||
err := findMachoFeatures(&data, readerForFixture(t, tt.fixture))
|
||||
err := findMachoFeatures(&data, readerForFixture(t, tt.fixture), SymbolConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.want.HasEntrypoint, data.HasEntrypoint)
|
||||
@@ -120,3 +120,327 @@ func Test_machoUniversal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_machoGoToolchainDetection(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
wantPresent bool
|
||||
}{
|
||||
{
|
||||
name: "go binary has toolchain",
|
||||
fixture: "bin/hello_mac",
|
||||
wantPresent: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := readerForFixture(t, tt.fixture)
|
||||
|
||||
toolchains := machoToolchains(reader)
|
||||
assert.Equal(t, tt.wantPresent, isGoToolchainPresent(toolchains))
|
||||
|
||||
if tt.wantPresent {
|
||||
require.NotEmpty(t, toolchains)
|
||||
assert.Equal(t, "go", toolchains[0].Name)
|
||||
assert.NotEmpty(t, toolchains[0].Version)
|
||||
assert.Equal(t, file.ToolchainKindCompiler, toolchains[0].Kind)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_machoGoSymbolCapture(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
cfg SymbolConfig
|
||||
wantSymbols []string // exact symbol names that must be present
|
||||
wantMinSymbolCount int
|
||||
}{
|
||||
{
|
||||
name: "capture all symbol types",
|
||||
fixture: "bin/hello_mac",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
// stdlib - fmt package (used via fmt.Println)
|
||||
"fmt.(*fmt).fmtInteger",
|
||||
"fmt.(*pp).doPrintf",
|
||||
// stdlib - strings package (used via strings.ToUpper)
|
||||
"strings.ToUpper",
|
||||
"strings.Map",
|
||||
// stdlib - encoding/json package (used via json.Marshal)
|
||||
"encoding/json.Marshal",
|
||||
// extended stdlib - golang.org/x/text (used via language.English)
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Language.String",
|
||||
// third-party - go-spew (used via spew.Dump)
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
wantMinSymbolCount: 50,
|
||||
},
|
||||
{
|
||||
name: "capture only third-party symbols",
|
||||
fixture: "bin/hello_mac",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.(*formatState).Format",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture only extended stdlib symbols",
|
||||
fixture: "bin/hello_mac",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExtendedStandardLibrary: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Parse",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture with text section types only",
|
||||
fixture: "bin/hello_mac",
|
||||
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",
|
||||
"strings.ToUpper",
|
||||
},
|
||||
wantMinSymbolCount: 10,
|
||||
},
|
||||
}
|
||||
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)
|
||||
|
||||
symbols := captureMachoGoSymbols(f, tt.cfg)
|
||||
symbolSet := make(map[string]struct{}, len(symbols))
|
||||
for _, sym := range symbols {
|
||||
symbolSet[sym] = struct{}{}
|
||||
}
|
||||
|
||||
if tt.wantMinSymbolCount > 0 {
|
||||
assert.GreaterOrEqual(t, len(symbols), tt.wantMinSymbolCount,
|
||||
"expected at least %d symbols, got %d", tt.wantMinSymbolCount, len(symbols))
|
||||
}
|
||||
|
||||
for _, want := range tt.wantSymbols {
|
||||
_, found := symbolSet[want]
|
||||
assert.True(t, found, "expected symbol %q to be present", want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_machoNMSymbols_goReturnsSymbols(t *testing.T) {
|
||||
// for Go binaries, machoNMSymbols should return symbols when Go toolchain is present
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
reader := readerForFixture(t, "bin/hello_mac")
|
||||
f, err := macho.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := []file.Toolchain{
|
||||
{Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler},
|
||||
}
|
||||
cfg := SymbolConfig{
|
||||
Types: []string{"T", "t"},
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
},
|
||||
}
|
||||
|
||||
symbols := machoNMSymbols(f, cfg, toolchains)
|
||||
assert.NotNil(t, symbols, "expected symbols for Go binary")
|
||||
assert.NotEmpty(t, symbols, "expected non-empty symbols for Go binary")
|
||||
}
|
||||
|
||||
func Test_machoSymbolType(t *testing.T) {
|
||||
// create minimal sections for testing
|
||||
textSection := &macho.Section{SectionHeader: macho.SectionHeader{Seg: "__TEXT"}}
|
||||
dataSection := &macho.Section{SectionHeader: macho.SectionHeader{Seg: "__DATA"}}
|
||||
bssSection := &macho.Section{SectionHeader: macho.SectionHeader{Seg: "__DATA", Name: "__bss"}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sym macho.Symbol
|
||||
sections []*macho.Section
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "undefined external symbol",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNExt, // external, undefined (N_TYPE = 0 = N_UNDF)
|
||||
},
|
||||
want: "U",
|
||||
},
|
||||
{
|
||||
name: "absolute external symbol",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNExt | machoNAbs, // external, absolute
|
||||
},
|
||||
want: "A",
|
||||
},
|
||||
{
|
||||
name: "absolute local symbol",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNAbs, // local, absolute
|
||||
},
|
||||
want: "a",
|
||||
},
|
||||
{
|
||||
name: "text section external",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNExt | machoNSect, // external, section-defined
|
||||
Sect: 1,
|
||||
},
|
||||
sections: []*macho.Section{textSection},
|
||||
want: "T",
|
||||
},
|
||||
{
|
||||
name: "text section local",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNSect, // local, section-defined
|
||||
Sect: 1,
|
||||
},
|
||||
sections: []*macho.Section{textSection},
|
||||
want: "t",
|
||||
},
|
||||
{
|
||||
name: "data section external",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNExt | machoNSect,
|
||||
Sect: 1,
|
||||
},
|
||||
sections: []*macho.Section{dataSection},
|
||||
want: "D",
|
||||
},
|
||||
{
|
||||
name: "bss section external",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNExt | machoNSect,
|
||||
Sect: 1,
|
||||
},
|
||||
sections: []*macho.Section{bssSection},
|
||||
want: "B",
|
||||
},
|
||||
{
|
||||
name: "stab debugging symbol",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNStab, // any stab symbol
|
||||
},
|
||||
want: "-",
|
||||
},
|
||||
{
|
||||
name: "indirect symbol",
|
||||
sym: macho.Symbol{
|
||||
Type: machoNExt | machoNIndr,
|
||||
},
|
||||
want: "I",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := machoSymbolType(tt.sym, tt.sections)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isGoToolchainPresent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toolchains []file.Toolchain
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty toolchains",
|
||||
toolchains: []file.Toolchain{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "go toolchain present",
|
||||
toolchains: []file.Toolchain{
|
||||
{Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "other toolchain only",
|
||||
toolchains: []file.Toolchain{
|
||||
{Name: "gcc", Version: "12.0", Kind: file.ToolchainKindCompiler},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "go among multiple toolchains",
|
||||
toolchains: []file.Toolchain{
|
||||
{Name: "gcc", Version: "12.0", Kind: file.ToolchainKindCompiler},
|
||||
{Name: "go", Version: "1.21.0", Kind: file.ToolchainKindCompiler},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := isGoToolchainPresent(tt.toolchains)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package executable
|
||||
|
||||
import (
|
||||
"debug/pe"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
@@ -9,7 +10,23 @@ import (
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func findPEFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
// PE symbol storage class constants
|
||||
const (
|
||||
peSymClassExternal = 2 // IMAGE_SYM_CLASS_EXTERNAL - external symbol
|
||||
peSymClassStatic = 3 // IMAGE_SYM_CLASS_STATIC - static symbol
|
||||
)
|
||||
|
||||
// PE section characteristic flags
|
||||
const (
|
||||
peSectionCntCode = 0x00000020 // IMAGE_SCN_CNT_CODE
|
||||
peSectionCntInitializedData = 0x00000040 // IMAGE_SCN_CNT_INITIALIZED_DATA
|
||||
peSectionCntUninitializedData = 0x00000080 // IMAGE_SCN_CNT_UNINITIALIZED_DATA
|
||||
peSectionMemExecute = 0x20000000 // IMAGE_SCN_MEM_EXECUTE
|
||||
peSectionMemRead = 0x40000000 // IMAGE_SCN_MEM_READ
|
||||
peSectionMemWrite = 0x80000000 // IMAGE_SCN_MEM_WRITE
|
||||
)
|
||||
|
||||
func findPEFeatures(data *file.Executable, reader unionreader.UnionReader, cfg SymbolConfig) error {
|
||||
// TODO: support security features
|
||||
|
||||
f, err := pe.NewFile(reader)
|
||||
@@ -25,6 +42,10 @@ func findPEFeatures(data *file.Executable, reader unionreader.UnionReader) error
|
||||
data.ImportedLibraries = libs
|
||||
data.HasEntrypoint = peHasEntrypoint(f)
|
||||
data.HasExports = peHasExports(f)
|
||||
data.Toolchains = peToolchains(reader)
|
||||
if shouldCaptureSymbols(data, cfg) {
|
||||
data.SymbolNames = peNMSymbols(f, cfg, data.Toolchains)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -82,3 +103,104 @@ func peHasExports(f *pe.File) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func peToolchains(reader unionreader.UnionReader) []file.Toolchain {
|
||||
return includeNoneNil(
|
||||
golangToolchainEvidence(reader),
|
||||
)
|
||||
}
|
||||
|
||||
func peNMSymbols(f *pe.File, cfg SymbolConfig, toolchains []file.Toolchain) []string {
|
||||
if isGoToolchainPresent(toolchains) {
|
||||
return capturePeGoSymbols(f, cfg)
|
||||
}
|
||||
|
||||
// include all symbols for non-Go binaries
|
||||
if f.Symbols == nil {
|
||||
return nil
|
||||
}
|
||||
var symbols []string
|
||||
for _, sym := range f.Symbols {
|
||||
symbols = append(symbols, sym.Name)
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
func capturePeGoSymbols(f *pe.File, cfg SymbolConfig) []string {
|
||||
if f.Symbols == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
filter := createGoSymbolFilter(cfg)
|
||||
for _, sym := range f.Symbols {
|
||||
name, include := filter(sym.Name, peSymbolType(sym, f.Sections))
|
||||
if include {
|
||||
symbols = append(symbols, name)
|
||||
}
|
||||
}
|
||||
return symbols
|
||||
}
|
||||
|
||||
// peSymbolType returns the nm-style single character representing the symbol type.
|
||||
// This mimics the output of `nm` for PE/COFF binaries.
|
||||
func peSymbolType(sym *pe.Symbol, sections []*pe.Section) string {
|
||||
// handle special section numbers first
|
||||
switch sym.SectionNumber {
|
||||
case 0:
|
||||
// IMAGE_SYM_UNDEFINED - undefined symbol
|
||||
return "U"
|
||||
case -1:
|
||||
// IMAGE_SYM_ABSOLUTE - absolute symbol
|
||||
if sym.StorageClass == peSymClassExternal {
|
||||
return "A"
|
||||
}
|
||||
return "a"
|
||||
case -2:
|
||||
// IMAGE_SYM_DEBUG - debugging symbol
|
||||
return "-"
|
||||
}
|
||||
|
||||
// for defined symbols, determine type based on section characteristics
|
||||
typeChar := peSectionTypeChar(sym.SectionNumber, sections)
|
||||
|
||||
// lowercase for static (local) symbols, uppercase for external (global)
|
||||
if sym.StorageClass != peSymClassExternal && typeChar != '-' && typeChar != '?' {
|
||||
return strings.ToLower(string(typeChar))
|
||||
}
|
||||
return string(typeChar)
|
||||
}
|
||||
|
||||
// peSectionTypeChar returns the nm-style character based on section characteristics.
|
||||
// Section numbers are 1-based.
|
||||
func peSectionTypeChar(sectNum int16, sections []*pe.Section) byte {
|
||||
idx := int(sectNum) - 1 // convert to 0-based index
|
||||
if idx < 0 || idx >= len(sections) {
|
||||
return '?'
|
||||
}
|
||||
|
||||
section := sections[idx]
|
||||
chars := section.Characteristics
|
||||
|
||||
// determine symbol type based on section characteristics
|
||||
switch {
|
||||
case chars&peSectionMemExecute != 0 || chars&peSectionCntCode != 0:
|
||||
// executable section -> text
|
||||
return 'T'
|
||||
|
||||
case chars&peSectionCntUninitializedData != 0:
|
||||
// uninitialized data section -> BSS
|
||||
return 'B'
|
||||
|
||||
case chars&peSectionMemWrite == 0 && chars&peSectionCntInitializedData != 0:
|
||||
// read-only initialized data -> rodata
|
||||
return 'R'
|
||||
|
||||
case chars&peSectionCntInitializedData != 0:
|
||||
// writable initialized data -> data
|
||||
return 'D'
|
||||
|
||||
default:
|
||||
return 'D'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
@@ -78,3 +79,300 @@ func Test_peHasExports(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_peGoToolchainDetection(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
wantPresent bool
|
||||
}{
|
||||
{
|
||||
name: "go binary has toolchain",
|
||||
fixture: "bin/hello.exe",
|
||||
wantPresent: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := readerForFixture(t, tt.fixture)
|
||||
|
||||
toolchains := peToolchains(reader)
|
||||
assert.Equal(t, tt.wantPresent, isGoToolchainPresent(toolchains))
|
||||
|
||||
if tt.wantPresent {
|
||||
require.NotEmpty(t, toolchains)
|
||||
assert.Equal(t, "go", toolchains[0].Name)
|
||||
assert.NotEmpty(t, toolchains[0].Version)
|
||||
assert.Equal(t, file.ToolchainKindCompiler, toolchains[0].Kind)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_peGoSymbolCapture(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
cfg SymbolConfig
|
||||
wantSymbols []string // exact symbol names that must be present
|
||||
wantMinSymbolCount int
|
||||
}{
|
||||
{
|
||||
name: "capture all symbol types",
|
||||
fixture: "bin/hello.exe",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
// stdlib - fmt package (used via fmt.Println)
|
||||
"fmt.(*fmt).fmtInteger",
|
||||
"fmt.(*pp).doPrintf",
|
||||
// stdlib - strings package (used via strings.ToUpper)
|
||||
"strings.ToUpper",
|
||||
"strings.Map",
|
||||
// stdlib - encoding/json package (used via json.Marshal)
|
||||
"encoding/json.Marshal",
|
||||
// extended stdlib - golang.org/x/text (used via language.English)
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Language.String",
|
||||
// third-party - go-spew (used via spew.Dump)
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
wantMinSymbolCount: 50,
|
||||
},
|
||||
{
|
||||
name: "capture only third-party symbols",
|
||||
fixture: "bin/hello.exe",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"github.com/davecgh/go-spew/spew.(*dumpState).dump",
|
||||
"github.com/davecgh/go-spew/spew.(*formatState).Format",
|
||||
"github.com/davecgh/go-spew/spew.fdump",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture only extended stdlib symbols",
|
||||
fixture: "bin/hello.exe",
|
||||
cfg: SymbolConfig{
|
||||
Go: GoSymbolConfig{
|
||||
ExtendedStandardLibrary: true,
|
||||
ExportedSymbols: true,
|
||||
UnexportedSymbols: true,
|
||||
},
|
||||
},
|
||||
wantSymbols: []string{
|
||||
"golang.org/x/text/internal/language.Tag.String",
|
||||
"golang.org/x/text/internal/language.Parse",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "capture with text section types only",
|
||||
fixture: "bin/hello.exe",
|
||||
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",
|
||||
"strings.ToUpper",
|
||||
},
|
||||
wantMinSymbolCount: 10,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reader := readerForFixture(t, tt.fixture)
|
||||
f, err := pe.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
symbols := capturePeGoSymbols(f, tt.cfg)
|
||||
symbolSet := make(map[string]struct{}, len(symbols))
|
||||
for _, sym := range symbols {
|
||||
symbolSet[sym] = struct{}{}
|
||||
}
|
||||
|
||||
if tt.wantMinSymbolCount > 0 {
|
||||
assert.GreaterOrEqual(t, len(symbols), tt.wantMinSymbolCount,
|
||||
"expected at least %d symbols, got %d", tt.wantMinSymbolCount, len(symbols))
|
||||
}
|
||||
|
||||
for _, want := range tt.wantSymbols {
|
||||
_, found := symbolSet[want]
|
||||
assert.True(t, found, "expected symbol %q to be present", want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_peNMSymbols_goReturnsSymbols(t *testing.T) {
|
||||
// for Go binaries, peNMSymbols should return symbols when Go toolchain is present
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/golang", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
reader := readerForFixture(t, "bin/hello.exe")
|
||||
f, err := pe.NewFile(reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
toolchains := []file.Toolchain{
|
||||
{Name: "go", Version: "1.24", Kind: file.ToolchainKindCompiler},
|
||||
}
|
||||
cfg := SymbolConfig{
|
||||
Types: []string{"T", "t"},
|
||||
Go: GoSymbolConfig{
|
||||
StandardLibrary: true,
|
||||
ExtendedStandardLibrary: true,
|
||||
ThirdPartyModules: true,
|
||||
ExportedSymbols: true,
|
||||
},
|
||||
}
|
||||
|
||||
symbols := peNMSymbols(f, cfg, toolchains)
|
||||
assert.NotNil(t, symbols, "expected symbols for Go binary")
|
||||
assert.NotEmpty(t, symbols, "expected non-empty symbols for Go binary")
|
||||
}
|
||||
|
||||
func Test_peSymbolType(t *testing.T) {
|
||||
// create minimal sections for testing
|
||||
textSection := &pe.Section{SectionHeader: pe.SectionHeader{Characteristics: peSectionCntCode | peSectionMemExecute | peSectionMemRead}}
|
||||
dataSection := &pe.Section{SectionHeader: pe.SectionHeader{Characteristics: peSectionCntInitializedData | peSectionMemRead | peSectionMemWrite}}
|
||||
rdataSection := &pe.Section{SectionHeader: pe.SectionHeader{Characteristics: peSectionCntInitializedData | peSectionMemRead}}
|
||||
bssSection := &pe.Section{SectionHeader: pe.SectionHeader{Characteristics: peSectionCntUninitializedData | peSectionMemRead | peSectionMemWrite}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sym *pe.Symbol
|
||||
sections []*pe.Section
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "undefined symbol",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 0,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
want: "U",
|
||||
},
|
||||
{
|
||||
name: "absolute symbol external",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: -1,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
want: "A",
|
||||
},
|
||||
{
|
||||
name: "absolute symbol static",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: -1,
|
||||
StorageClass: peSymClassStatic,
|
||||
},
|
||||
want: "a",
|
||||
},
|
||||
{
|
||||
name: "debug symbol",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: -2,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
want: "-",
|
||||
},
|
||||
{
|
||||
name: "text section external",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 1,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
sections: []*pe.Section{textSection},
|
||||
want: "T",
|
||||
},
|
||||
{
|
||||
name: "text section static",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 1,
|
||||
StorageClass: peSymClassStatic,
|
||||
},
|
||||
sections: []*pe.Section{textSection},
|
||||
want: "t",
|
||||
},
|
||||
{
|
||||
name: "data section external",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 1,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
sections: []*pe.Section{dataSection},
|
||||
want: "D",
|
||||
},
|
||||
{
|
||||
name: "data section static",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 1,
|
||||
StorageClass: peSymClassStatic,
|
||||
},
|
||||
sections: []*pe.Section{dataSection},
|
||||
want: "d",
|
||||
},
|
||||
{
|
||||
name: "rodata section external",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 1,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
sections: []*pe.Section{rdataSection},
|
||||
want: "R",
|
||||
},
|
||||
{
|
||||
name: "bss section external",
|
||||
sym: &pe.Symbol{
|
||||
SectionNumber: 1,
|
||||
StorageClass: peSymClassExternal,
|
||||
},
|
||||
sections: []*pe.Section{bssSection},
|
||||
want: "B",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peSymbolType(tt.sym, tt.sections)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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 { //nolint:gocritic // lets elect a pattern as if we'll have multiple options in the future...
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
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{SymbolScopeGolang},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty capture scope returns false",
|
||||
data: &file.Executable{},
|
||||
cfg: SymbolConfig{
|
||||
CaptureScope: []SymbolCaptureScope{},
|
||||
},
|
||||
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: "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},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := hasGolangToolchain(tt.data)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
FROM golang:1.24
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY main.go cgo_main.go ./
|
||||
|
||||
# pure Go builds (no CGO)
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /hello_linux .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o /hello_mac .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o /hello.exe .
|
||||
|
||||
# CGO-enabled build (Linux only, uses gcc)
|
||||
RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o /hello_linux_cgo ./cgo_main.go
|
||||
@@ -0,0 +1,42 @@
|
||||
BIN=./bin
|
||||
TOOL_IMAGE=localhost/syft-golang-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 -i -v $(shell pwd)/$(BIN):/out $(TOOL_IMAGE) sh -c \
|
||||
"cp /hello_linux /hello_mac /hello.exe /hello_linux_cgo /out/"
|
||||
|
||||
debug:
|
||||
docker run -it --rm -v $(shell pwd):/mount -w /mount $(TOOL_IMAGE) sh
|
||||
|
||||
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
|
||||
.PHONY: $(FINGERPRINT_FILE)
|
||||
$(FINGERPRINT_FILE):
|
||||
@find . -maxdepth 1 -type f \( -name "*.go" -o -name "go.*" -o -name "Dockerfile" -o -name "Makefile" \) \
|
||||
-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 fixtures fingerprint
|
||||
@@ -0,0 +1,18 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int get_length(const char* s) {
|
||||
return strlen(s);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
msg := C.CString("Hello from CGO!")
|
||||
length := C.get_length(msg)
|
||||
fmt.Printf("String length: %d\n", int(length))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module x
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
golang.org/x/text v0.21.0
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// use stdlib packages
|
||||
fmt.Println("Hello from Go!")
|
||||
fmt.Println(strings.ToUpper("test"))
|
||||
|
||||
// use golang.org/x package
|
||||
tag := language.English
|
||||
fmt.Println(tag.String())
|
||||
|
||||
// use third-party package
|
||||
spew.Dump(os.Args)
|
||||
|
||||
// use encoding/json
|
||||
data, _ := json.Marshal(map[string]string{"hello": "world"})
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
bin/
|
||||
Dockerfile.sha256
|
||||
*.fingerprint
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN apk add --no-cache clang18 musl-dev make
|
||||
|
||||
# create symlink so 'clang' command works (Alpine installs as clang-18)
|
||||
RUN ln -s /usr/bin/clang-18 /usr/bin/clang
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Hello, World!\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
FROM gcc:13.4.0
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Hello, World!\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,15 @@ type (
|
||||
|
||||
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
||||
RelocationReadOnly string
|
||||
|
||||
// ToolchainKind represents the type of toolchain used to build the executable. Today only "compiler" is supported,
|
||||
// however, this can be expanded in the future to include linkers, runtimes, etc.
|
||||
ToolchainKind string
|
||||
)
|
||||
|
||||
const (
|
||||
ToolchainKindCompiler ToolchainKind = "compiler"
|
||||
|
||||
ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems
|
||||
MachO ExecutableFormat = "macho" // Mach object file format used on macOS and iOS
|
||||
PE ExecutableFormat = "pe" // Portable Executable format used on Windows
|
||||
@@ -34,6 +40,23 @@ type Executable struct {
|
||||
|
||||
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
||||
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||
|
||||
// Symbols captures the selection from the symbol table found in the binary.
|
||||
SymbolNames []string `json:"symbolNames,omitempty" yaml:"symbolNames" mapstructure:"symbolNames"`
|
||||
|
||||
// Toolchains captures information about the compiler, linker, runtime, or other toolchains used to build (or otherwise exist within) the executable.
|
||||
Toolchains []Toolchain `json:"toolchains,omitempty" yaml:"toolchains" mapstructure:"toolchains"`
|
||||
}
|
||||
|
||||
type Toolchain struct {
|
||||
// Name is the name of the toolchain (e.g., "gcc", "clang", "ld", etc.).
|
||||
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||
|
||||
// Version is the version of the toolchain.
|
||||
Version string `json:"version,omitempty" yaml:"version,omitempty" mapstructure:"version"`
|
||||
|
||||
// Kind indicates the type of toolchain (e.g., compiler, linker, runtime).
|
||||
Kind ToolchainKind `json:"kind" yaml:"kind" mapstructure:"kind"`
|
||||
}
|
||||
|
||||
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
||||
|
||||
Reference in New Issue
Block a user