mirror of
https://github.com/anchore/syft.git
synced 2026-07-05 02:28:25 +02:00
add file-based toolchain detection
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
58e4dbbf01
commit
1c72d03da3
@ -17,8 +17,6 @@ import (
|
|||||||
"github.com/anchore/syft/syft/cataloging"
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||||
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
"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/binary"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
|
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||||
@ -144,18 +142,14 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
|
|||||||
log.WithFields("error", err).Warn("unable to configure file hashers")
|
log.WithFields("error", err).Warn("unable to configure file hashers")
|
||||||
}
|
}
|
||||||
|
|
||||||
return filecataloging.Config{
|
c := filecataloging.DefaultConfig()
|
||||||
Selection: cfg.File.Metadata.Selection,
|
c.Selection = cfg.File.Metadata.Selection
|
||||||
Hashers: hashers,
|
c.Hashers = hashers
|
||||||
Content: filecontent.Config{
|
c.Content.Globs = cfg.File.Content.Globs
|
||||||
Globs: cfg.File.Content.Globs,
|
c.Content.SkipFilesAboveSize = cfg.File.Content.SkipFilesAboveSize
|
||||||
SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize,
|
c.Executable.Globs = cfg.File.Executable.Globs
|
||||||
},
|
|
||||||
Executable: executable.Config{
|
return c
|
||||||
MIMETypes: executable.DefaultConfig().MIMETypes,
|
|
||||||
Globs: cfg.File.Executable.Globs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
|
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ package internal
|
|||||||
const (
|
const (
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
// 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.
|
// 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.4"
|
JSONSchemaVersion = "16.1.5"
|
||||||
|
|
||||||
// Changelog
|
// Changelog
|
||||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||||
@ -11,5 +11,6 @@ const (
|
|||||||
// 16.1.2 - placeholder for 16.1.2 changelog
|
// 16.1.2 - placeholder for 16.1.2 changelog
|
||||||
// 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata
|
// 16.1.3 - add GGUFFileParts to GGUFFileHeader metadata
|
||||||
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
|
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
|
||||||
|
// 16.1.5 - add file executable toolchain information
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|||||||
4334
schema/json/schema-16.1.5.json
Normal file
4334
schema/json/schema-16.1.5.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "anchore.io/schema/syft/json/16.1.4/document",
|
"$id": "anchore.io/schema/syft/json/16.1.5/document",
|
||||||
"$ref": "#/$defs/Document",
|
"$ref": "#/$defs/Document",
|
||||||
"$defs": {
|
"$defs": {
|
||||||
"AlpmDbEntry": {
|
"AlpmDbEntry": {
|
||||||
@ -1291,6 +1291,13 @@
|
|||||||
"elfSecurityFeatures": {
|
"elfSecurityFeatures": {
|
||||||
"$ref": "#/$defs/ELFSecurityFeatures",
|
"$ref": "#/$defs/ELFSecurityFeatures",
|
||||||
"description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF."
|
"description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF."
|
||||||
|
},
|
||||||
|
"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",
|
"type": "object",
|
||||||
@ -4269,6 +4276,27 @@
|
|||||||
],
|
],
|
||||||
"description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)."
|
"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\", \"rust\", \"lld\", \"GNU AS\", etc.)."
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Version is the version of the toolchain."
|
||||||
|
},
|
||||||
|
"component": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Component indicates which part of the toolchain this represents (e.g., compiler, linker, assembler)."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"component"
|
||||||
|
]
|
||||||
|
},
|
||||||
"WordpressPluginEntry": {
|
"WordpressPluginEntry": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"pluginInstallDirectory": {
|
"pluginInstallDirectory": {
|
||||||
|
|||||||
@ -25,7 +25,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
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"`
|
MIMETypes []string `json:"mime-types" yaml:"mime-types" mapstructure:"mime-types"`
|
||||||
|
|
||||||
|
// Globs are the glob patterns that will be used to filter which files are cataloged.
|
||||||
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
|
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +109,51 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file
|
|||||||
return processExecutable(loc, uReader)
|
return processExecutable(loc, uReader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 catalogingProgress(locations int64) *monitor.TaskProgress {
|
func catalogingProgress(locations int64) *monitor.TaskProgress {
|
||||||
info := monitor.GenericTask{
|
info := monitor.GenericTask{
|
||||||
Title: monitor.Title{
|
Title: monitor.Title{
|
||||||
@ -152,51 +200,6 @@ func locationMatchesGlob(loc file.Location, globs []string) (bool, error) {
|
|||||||
return false, nil
|
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) {
|
func findExecutableFormat(reader unionreader.UnionReader) (file.ExecutableFormat, error) {
|
||||||
// read the first sector of the file
|
// read the first sector of the file
|
||||||
buf := make([]byte, 512)
|
buf := make([]byte, 512)
|
||||||
|
|||||||
@ -34,10 +34,33 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) erro
|
|||||||
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
||||||
data.HasEntrypoint = elfHasEntrypoint(f)
|
data.HasEntrypoint = elfHasEntrypoint(f)
|
||||||
data.HasExports = elfHasExports(f)
|
data.HasExports = elfHasExports(f)
|
||||||
|
data.Toolchains = elfToolchains(reader, f)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func elfToolchains(reader unionreader.UnionReader, f *elf.File) []file.Toolchain {
|
||||||
|
// parse the .comment section and symbol tables once and share them across the detectors
|
||||||
|
comments := elfComments(f)
|
||||||
|
symbols := elfSymbolSet(f)
|
||||||
|
return includeNoneNil(
|
||||||
|
golangToolchainEvidence(reader),
|
||||||
|
cToolchainEvidence(comments, symbols),
|
||||||
|
rustToolchainEvidence(comments),
|
||||||
|
linkerToolchainEvidence(f, comments),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
||||||
return &file.ELFSecurityFeatures{
|
return &file.ELFSecurityFeatures{
|
||||||
SymbolTableStripped: isElfSymbolTableStripped(f),
|
SymbolTableStripped: isElfSymbolTableStripped(f),
|
||||||
@ -81,6 +104,28 @@ func boolRef(b bool) *bool {
|
|||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// elfSymbolSet collects the names from the static and dynamic symbol tables into a single set, so
|
||||||
|
// detectors can test for runtime markers without re-reading and re-iterating the symbol tables each
|
||||||
|
// time. It covers both tables since some evidence (e.g. the Fortran MAIN__ entry) lives only in the
|
||||||
|
// static symbol table.
|
||||||
|
func elfSymbolSet(f *elf.File) *strset.Set {
|
||||||
|
set := strset.New()
|
||||||
|
|
||||||
|
if syms, err := f.Symbols(); err == nil {
|
||||||
|
for _, sym := range syms {
|
||||||
|
set.Add(sym.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dynSyms, err := f.DynamicSymbols(); err == nil {
|
||||||
|
for _, sym := range dynSyms {
|
||||||
|
set.Add(sym.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
func checkElfNXProtection(file *elf.File) bool {
|
func checkElfNXProtection(file *elf.File) bool {
|
||||||
// find the program headers until you find the GNU_STACK segment
|
// find the program headers until you find the GNU_STACK segment
|
||||||
for _, prog := range file.Progs {
|
for _, prog := range file.Progs {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -226,3 +227,73 @@ 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("testdata/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, hasGoToolchain(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.ToolchainComponentCompiler, toolchains[0].Component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_elfCgoToolchainDetection(t *testing.T) {
|
||||||
|
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||||
|
t.Helper()
|
||||||
|
f, err := os.Open(filepath.Join("testdata/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", Component: file.ToolchainComponentCompiler},
|
||||||
|
{Name: "gcc", Component: file.ToolchainComponentCompiler},
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -48,6 +48,8 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) er
|
|||||||
if !data.HasExports {
|
if !data.HasExports {
|
||||||
data.HasExports = machoHasExports(f)
|
data.HasExports = machoHasExports(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.Toolchains = machoToolchains(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
// de-duplicate libraries
|
// de-duplicate libraries
|
||||||
@ -56,6 +58,12 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func machoToolchains(reader unionreader.UnionReader) []file.Toolchain {
|
||||||
|
return includeNoneNil(
|
||||||
|
golangToolchainEvidence(reader),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func machoHasEntrypoint(f *macho.File) bool {
|
func machoHasEntrypoint(f *macho.File) bool {
|
||||||
// derived from struct entry_point_command found from which explicitly calls out LC_MAIN:
|
// 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
|
// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
|
||||||
|
|||||||
@ -120,3 +120,39 @@ 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("testdata/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, hasGoToolchain(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.ToolchainComponentCompiler, toolchains[0].Component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ func findPEFeatures(data *file.Executable, reader unionreader.UnionReader) error
|
|||||||
data.ImportedLibraries = libs
|
data.ImportedLibraries = libs
|
||||||
data.HasEntrypoint = peHasEntrypoint(f)
|
data.HasEntrypoint = peHasEntrypoint(f)
|
||||||
data.HasExports = peHasExports(f)
|
data.HasExports = peHasExports(f)
|
||||||
|
data.Toolchains = peToolchains(reader)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -82,3 +83,9 @@ func peHasExports(f *pe.File) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func peToolchains(reader unionreader.UnionReader) []file.Toolchain {
|
||||||
|
return includeNoneNil(
|
||||||
|
golangToolchainEvidence(reader),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/internal/unionreader"
|
"github.com/anchore/syft/syft/internal/unionreader"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,3 +79,39 @@ 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("testdata/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, hasGoToolchain(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.ToolchainComponentCompiler, toolchains[0].Component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
bin
|
bin
|
||||||
actual_verify
|
actual_verify
|
||||||
Dockerfile.sha256
|
Dockerfile.sha256
|
||||||
|
*.fingerprint
|
||||||
18
syft/file/cataloger/executable/testdata/golang/Dockerfile
vendored
Normal file
18
syft/file/cataloger/executable/testdata/golang/Dockerfile
vendored
Normal file
@ -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
|
||||||
42
syft/file/cataloger/executable/testdata/golang/Makefile
vendored
Normal file
42
syft/file/cataloger/executable/testdata/golang/Makefile
vendored
Normal file
@ -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
|
||||||
18
syft/file/cataloger/executable/testdata/golang/cgo_main.go
vendored
Normal file
18
syft/file/cataloger/executable/testdata/golang/cgo_main.go
vendored
Normal file
@ -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))
|
||||||
|
}
|
||||||
8
syft/file/cataloger/executable/testdata/golang/go.mod
vendored
Normal file
8
syft/file/cataloger/executable/testdata/golang/go.mod
vendored
Normal file
@ -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
|
||||||
|
)
|
||||||
4
syft/file/cataloger/executable/testdata/golang/go.sum
vendored
Normal file
4
syft/file/cataloger/executable/testdata/golang/go.sum
vendored
Normal file
@ -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=
|
||||||
28
syft/file/cataloger/executable/testdata/golang/main.go
vendored
Normal file
28
syft/file/cataloger/executable/testdata/golang/main.go
vendored
Normal file
@ -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))
|
||||||
|
}
|
||||||
3
syft/file/cataloger/executable/testdata/toolchains/.gitignore
vendored
Normal file
3
syft/file/cataloger/executable/testdata/toolchains/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
bin/
|
||||||
|
Dockerfile.sha256
|
||||||
|
*.fingerprint
|
||||||
30
syft/file/cataloger/executable/testdata/toolchains/Makefile
vendored
Normal file
30
syft/file/cataloger/executable/testdata/toolchains/Makefile
vendored
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# invoke all make files in subdirectories
|
||||||
|
.PHONY: all gcc clang lld mold gold rust fortran
|
||||||
|
|
||||||
|
all: gcc clang lld mold gold rust fortran
|
||||||
|
|
||||||
|
gcc:
|
||||||
|
$(MAKE) -C gcc
|
||||||
|
|
||||||
|
clang:
|
||||||
|
$(MAKE) -C clang
|
||||||
|
|
||||||
|
lld:
|
||||||
|
$(MAKE) -C lld
|
||||||
|
|
||||||
|
mold:
|
||||||
|
$(MAKE) -C mold
|
||||||
|
|
||||||
|
gold:
|
||||||
|
$(MAKE) -C gold
|
||||||
|
|
||||||
|
rust:
|
||||||
|
$(MAKE) -C rust
|
||||||
|
|
||||||
|
fortran:
|
||||||
|
$(MAKE) -C fortran
|
||||||
|
|
||||||
|
%:
|
||||||
|
@for dir in gcc clang lld mold gold rust fortran; do \
|
||||||
|
$(MAKE) -C $$dir $@; \
|
||||||
|
done
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/clang/Dockerfile
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/clang/Dockerfile
vendored
Normal file
@ -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
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/clang/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/clang/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-clang-build-tools:latest
|
||||||
|
FINGERPRINT_FILE=$(BIN).fingerprint
|
||||||
|
|
||||||
|
ifndef BIN
|
||||||
|
$(error BIN is not set)
|
||||||
|
endif
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := fixtures
|
||||||
|
|
||||||
|
# requirement 1: 'fixtures' goal to generate any and all test fixtures
|
||||||
|
fixtures: build
|
||||||
|
|
||||||
|
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
|
||||||
|
fingerprint: $(FINGERPRINT_FILE)
|
||||||
|
|
||||||
|
tools-check:
|
||||||
|
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
|
||||||
|
|
||||||
|
tools:
|
||||||
|
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
|
||||||
|
|
||||||
|
build: tools
|
||||||
|
@mkdir -p $(BIN)
|
||||||
|
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
|
||||||
|
|
||||||
|
debug:
|
||||||
|
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
|
||||||
|
|
||||||
|
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
|
||||||
|
.PHONY: $(FINGERPRINT_FILE)
|
||||||
|
$(FINGERPRINT_FILE):
|
||||||
|
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
|
||||||
|
|
||||||
|
# requirement 4: 'clean' goal to remove all generated test fixtures
|
||||||
|
clean:
|
||||||
|
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
|
||||||
|
|
||||||
|
.PHONY: tools tools-check build debug clean
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/clang/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/clang/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_clang
|
||||||
|
|
||||||
|
$(BIN)/hello_clang: hello.c
|
||||||
|
clang hello.c -o $(BIN)/hello_clang
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_clang
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/clang/project/hello.c
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/clang/project/hello.c
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello, World!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1
syft/file/cataloger/executable/testdata/toolchains/fortran/Dockerfile
vendored
Normal file
1
syft/file/cataloger/executable/testdata/toolchains/fortran/Dockerfile
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
FROM gcc:13.4.0
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/fortran/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/fortran/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-fortran-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
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/fortran/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/fortran/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_fortran
|
||||||
|
|
||||||
|
$(BIN)/hello_fortran: hello.f90
|
||||||
|
gfortran hello.f90 -o $(BIN)/hello_fortran
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_fortran
|
||||||
3
syft/file/cataloger/executable/testdata/toolchains/fortran/project/hello.f90
vendored
Normal file
3
syft/file/cataloger/executable/testdata/toolchains/fortran/project/hello.f90
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
program hello
|
||||||
|
print *, "Hello, World!"
|
||||||
|
end program hello
|
||||||
1
syft/file/cataloger/executable/testdata/toolchains/gcc/Dockerfile
vendored
Normal file
1
syft/file/cataloger/executable/testdata/toolchains/gcc/Dockerfile
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
FROM gcc:13.4.0
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/gcc/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/gcc/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-gcc-build-tools:latest
|
||||||
|
FINGERPRINT_FILE=$(BIN).fingerprint
|
||||||
|
|
||||||
|
ifndef BIN
|
||||||
|
$(error BIN is not set)
|
||||||
|
endif
|
||||||
|
|
||||||
|
.DEFAULT_GOAL := fixtures
|
||||||
|
|
||||||
|
# requirement 1: 'fixtures' goal to generate any and all test fixtures
|
||||||
|
fixtures: build
|
||||||
|
|
||||||
|
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
|
||||||
|
fingerprint: $(FINGERPRINT_FILE)
|
||||||
|
|
||||||
|
tools-check:
|
||||||
|
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
|
||||||
|
|
||||||
|
tools:
|
||||||
|
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
|
||||||
|
|
||||||
|
build: tools
|
||||||
|
@mkdir -p $(BIN)
|
||||||
|
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
|
||||||
|
|
||||||
|
debug:
|
||||||
|
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
|
||||||
|
|
||||||
|
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
|
||||||
|
.PHONY: $(FINGERPRINT_FILE)
|
||||||
|
$(FINGERPRINT_FILE):
|
||||||
|
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
|
||||||
|
|
||||||
|
# requirement 4: 'clean' goal to remove all generated test fixtures
|
||||||
|
clean:
|
||||||
|
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
|
||||||
|
|
||||||
|
.PHONY: tools tools-check build debug clean
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/gcc/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/gcc/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_gcc
|
||||||
|
|
||||||
|
$(BIN)/hello_gcc: hello.c
|
||||||
|
gcc hello.c -o $(BIN)/hello_gcc
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_gcc
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/gcc/project/hello.c
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/gcc/project/hello.c
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello, World!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
3
syft/file/cataloger/executable/testdata/toolchains/gold/Dockerfile
vendored
Normal file
3
syft/file/cataloger/executable/testdata/toolchains/gold/Dockerfile
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache gcc binutils-gold musl-dev make
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/gold/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/gold/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-gold-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
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/gold/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/gold/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_gold
|
||||||
|
|
||||||
|
$(BIN)/hello_gold: hello.c
|
||||||
|
gcc hello.c -fuse-ld=gold -o $(BIN)/hello_gold
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_gold
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/gold/project/hello.c
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/gold/project/hello.c
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello, World!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/lld/Dockerfile
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/lld/Dockerfile
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache clang18 lld musl-dev make
|
||||||
|
|
||||||
|
# create symlink so 'clang' command works (Alpine installs as clang-18)
|
||||||
|
RUN ln -s /usr/bin/clang-18 /usr/bin/clang
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/lld/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/lld/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-lld-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
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/lld/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/lld/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_lld
|
||||||
|
|
||||||
|
$(BIN)/hello_lld: hello.c
|
||||||
|
clang hello.c -fuse-ld=lld -o $(BIN)/hello_lld
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_lld
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/lld/project/hello.c
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/lld/project/hello.c
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello, World!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
3
syft/file/cataloger/executable/testdata/toolchains/mold/Dockerfile
vendored
Normal file
3
syft/file/cataloger/executable/testdata/toolchains/mold/Dockerfile
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache gcc mold musl-dev make
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/mold/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/mold/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-mold-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
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/mold/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/mold/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_mold
|
||||||
|
|
||||||
|
$(BIN)/hello_mold: hello.c
|
||||||
|
gcc hello.c -fuse-ld=mold -o $(BIN)/hello_mold
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_mold
|
||||||
6
syft/file/cataloger/executable/testdata/toolchains/mold/project/hello.c
vendored
Normal file
6
syft/file/cataloger/executable/testdata/toolchains/mold/project/hello.c
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("Hello, World!\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1
syft/file/cataloger/executable/testdata/toolchains/rust/Dockerfile
vendored
Normal file
1
syft/file/cataloger/executable/testdata/toolchains/rust/Dockerfile
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
FROM rust:1.83.0
|
||||||
39
syft/file/cataloger/executable/testdata/toolchains/rust/Makefile
vendored
Normal file
39
syft/file/cataloger/executable/testdata/toolchains/rust/Makefile
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
BIN=./bin
|
||||||
|
TOOL_IMAGE=localhost/syft-toolchain-rust-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
|
||||||
9
syft/file/cataloger/executable/testdata/toolchains/rust/project/Makefile
vendored
Normal file
9
syft/file/cataloger/executable/testdata/toolchains/rust/project/Makefile
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
BIN=../bin
|
||||||
|
|
||||||
|
all: $(BIN)/hello_rust
|
||||||
|
|
||||||
|
$(BIN)/hello_rust: hello.rs
|
||||||
|
rustc hello.rs -o $(BIN)/hello_rust
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BIN)/hello_rust
|
||||||
3
syft/file/cataloger/executable/testdata/toolchains/rust/project/hello.rs
vendored
Normal file
3
syft/file/cataloger/executable/testdata/toolchains/rust/project/hello.rs
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, World!");
|
||||||
|
}
|
||||||
180
syft/file/cataloger/executable/toolchains.go
Normal file
180
syft/file/cataloger/executable/toolchains.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package executable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"debug/buildinfo"
|
||||||
|
"debug/elf"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: additional toolchain detectors we'd like to add. Each entry notes the signal location and the
|
||||||
|
// component it yields.
|
||||||
|
//
|
||||||
|
// ELF (.comment / notes / symbols — same mechanism as the detectors below):
|
||||||
|
// 1. rustc commit hash: scan rodata for "/rustc/<40-hex>/library/" panic paths; resolves rust provenance
|
||||||
|
// for stripped or pre-1.73 binaries (where the .comment producer string is absent). needs a
|
||||||
|
// commit->version lookup table to turn the hash into a semver. component: compiler.
|
||||||
|
// 2. gdc (GNU D): "GNU D" marker alongside the GCC version in .comment. component: compiler.
|
||||||
|
// 3. Swift (Linux): presence of "swift5_*" sections marks Swift; version only when a "swiftlang-"
|
||||||
|
// producer string is present (debug builds). component: compiler.
|
||||||
|
// 4. Haskell GHC: RTS symbols (hs_init, stg_*) for identity; derive an approximate version from the
|
||||||
|
// embedded "base_<x.y.z>" package symbol. component: compiler.
|
||||||
|
// 5. identity-only (no reliable version) via runtime symbol prefixes: OCaml (caml_*), Nim (NimMain),
|
||||||
|
// Crystal (__crystal_main), FreePascal (FPC_*), ldc/dmd D (_Dmain). component: compiler.
|
||||||
|
// 6. GNU assembler (GNU AS): only present in DWARF .debug_info producer strings, NOT .comment, so it
|
||||||
|
// requires DWARF parsing and is absent in stripped builds. component: assembler.
|
||||||
|
// 7. GNU ld (BFD): leaves no self-identifying marker; can only be inferred by elimination (ELF with a
|
||||||
|
// compiler .comment but no lld/mold/gold evidence) at low confidence and with no version.
|
||||||
|
//
|
||||||
|
// PE (Windows — needs debug/pe, not covered by this ELF-only file):
|
||||||
|
// 8. MSVC link.exe + cl.exe: parse the Rich Header (the "DanS".."Rich" block, XOR-decoded into
|
||||||
|
// ProdID/build pairs); map build numbers to Visual Studio/linker versions. components: linker + compiler.
|
||||||
|
// 9. .NET / CLR: COR20 header (optional-header DataDirectory[14]) marks a managed assembly; read the
|
||||||
|
// "TargetFrameworkAttribute" string (e.g. ".NETCoreApp,Version=v8.0") for the runtime version.
|
||||||
|
// component: runtime. (NativeAOT .NET is a native binary with runtime symbols only and no clean version.)
|
||||||
|
//
|
||||||
|
// Mach-O (macOS — needs debug/macho):
|
||||||
|
// 10. Swift: presence of "__swift5_*" sections; version via the "swiftlang-" producer when present.
|
||||||
|
// component: compiler.
|
||||||
|
|
||||||
|
var (
|
||||||
|
clangVersionPattern = regexp.MustCompile(`clang version (\d+\.\d+\.\d+)`)
|
||||||
|
gccVersionPattern = regexp.MustCompile(`GCC: \([^)]+\) (\d+\.\d+\.\d+)`)
|
||||||
|
// rustc embeds its own producer string in .comment for binaries built with rust >= 1.73 (e.g.
|
||||||
|
// "rustc version 1.83.0 (90b35a623 2024-11-26)"). The shipped libstd objects instead carry the
|
||||||
|
// "clang LLVM (rustc version ...)" form, so accept both.
|
||||||
|
rustcVersionPattern = regexp.MustCompile(`rustc version (\d+\.\d+\.\d+)`)
|
||||||
|
// LLVM lld writes "Linker: LLD <version>" into .comment (see https://lld.llvm.org/).
|
||||||
|
lldVersionPattern = regexp.MustCompile(`Linker: LLD (\d+\.\d+\.\d+)`)
|
||||||
|
// mold writes "mold <version> (...; compatible with GNU ld)" into .comment.
|
||||||
|
moldVersionPattern = regexp.MustCompile(`mold (\d+\.\d+\.\d+)`)
|
||||||
|
// GNU gold writes a "gold <version>" descriptor into the .note.gnu.gold-version note section.
|
||||||
|
goldVersionPattern = regexp.MustCompile(`gold (\d+\.\d+)`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// golangToolchainEvidence attempts to extract Go toolchain information from the binary build info.
|
||||||
|
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,
|
||||||
|
Component: file.ToolchainComponentCompiler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cToolchainEvidence attempts to extract C/C++/Fortran compiler information from the ELF .comment section.
|
||||||
|
// This detects GCC, Clang, and gfortran compilers based on their version strings.
|
||||||
|
func cToolchainEvidence(comments []string, symbols *strset.Set) *file.Toolchain {
|
||||||
|
for _, comment := range comments {
|
||||||
|
// check for clang first since clang binaries often have both GCC and clang entries
|
||||||
|
// (clang includes GCC compatibility info)
|
||||||
|
if match := clangVersionPattern.FindStringSubmatch(comment); match != nil {
|
||||||
|
return &file.Toolchain{
|
||||||
|
Name: "clang",
|
||||||
|
Version: match[1],
|
||||||
|
Component: file.ToolchainComponentCompiler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, comment := range comments {
|
||||||
|
if match := gccVersionPattern.FindStringSubmatch(comment); match != nil {
|
||||||
|
// gfortran is a GCC frontend and shares the GCC version string in .comment, so the only way
|
||||||
|
// to distinguish a Fortran build from a C build is by the presence of libgfortran runtime symbols.
|
||||||
|
name := "gcc"
|
||||||
|
if hasFortranEvidence(symbols) {
|
||||||
|
name = "gfortran"
|
||||||
|
}
|
||||||
|
return &file.Toolchain{
|
||||||
|
Name: name,
|
||||||
|
Version: match[1],
|
||||||
|
Component: file.ToolchainComponentCompiler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rustToolchainEvidence attempts to extract Rust compiler information from the ELF .comment section.
|
||||||
|
func rustToolchainEvidence(comments []string) *file.Toolchain {
|
||||||
|
for _, comment := range comments {
|
||||||
|
if match := rustcVersionPattern.FindStringSubmatch(comment); match != nil {
|
||||||
|
return &file.Toolchain{
|
||||||
|
Name: "rust",
|
||||||
|
Version: match[1],
|
||||||
|
Component: file.ToolchainComponentCompiler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// linkerToolchainEvidence attempts to extract linker information from the binary. lld and mold leave a
|
||||||
|
// version string in the ELF .comment section, while gold writes a dedicated .note.gnu.gold-version note.
|
||||||
|
// GNU ld (BFD) leaves no self-identifying marker, so it cannot be detected here.
|
||||||
|
func linkerToolchainEvidence(f *elf.File, comments []string) *file.Toolchain {
|
||||||
|
for _, comment := range comments {
|
||||||
|
if match := lldVersionPattern.FindStringSubmatch(comment); match != nil {
|
||||||
|
return &file.Toolchain{
|
||||||
|
Name: "lld",
|
||||||
|
Version: match[1],
|
||||||
|
Component: file.ToolchainComponentLinker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if match := moldVersionPattern.FindStringSubmatch(comment); match != nil {
|
||||||
|
return &file.Toolchain{
|
||||||
|
Name: "mold",
|
||||||
|
Version: match[1],
|
||||||
|
Component: file.ToolchainComponentLinker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if section := f.Section(".note.gnu.gold-version"); section != nil {
|
||||||
|
data, err := section.Data()
|
||||||
|
if err == nil {
|
||||||
|
if match := goldVersionPattern.FindStringSubmatch(string(data)); match != nil {
|
||||||
|
return &file.Toolchain{
|
||||||
|
Name: "gold",
|
||||||
|
Version: match[1],
|
||||||
|
Component: file.ToolchainComponentLinker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// elfComments returns the null-delimited strings within the ELF .comment section, which may contain
|
||||||
|
// multiple producer entries (compiler, assembler, linker, etc.).
|
||||||
|
func elfComments(f *elf.File) []string {
|
||||||
|
section := f.Section(".comment")
|
||||||
|
if section == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := section.Data()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// the .comment section contains null-terminated strings
|
||||||
|
return strings.Split(string(data), "\x00")
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasFortranEvidence reports whether the binary references the libgfortran runtime, which indicates a
|
||||||
|
// gfortran build (MAIN__ is the real Fortran program entry, and _gfortran_* are runtime calls).
|
||||||
|
func hasFortranEvidence(symbols *strset.Set) bool {
|
||||||
|
return symbols.HasAny("MAIN__", "_gfortran_set_args", "_gfortran_set_options", "_gfortran_st_write")
|
||||||
|
}
|
||||||
113
syft/file/cataloger/executable/toolchains_test.go
Normal file
113
syft/file/cataloger/executable/toolchains_test.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hasGoToolchain is a test helper to check whether the go toolchain was detected.
|
||||||
|
func hasGoToolchain(toolchains []file.Toolchain) bool {
|
||||||
|
for _, tc := range toolchains {
|
||||||
|
if tc.Name == "go" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_elfToolchains(t *testing.T) {
|
||||||
|
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||||
|
t.Helper()
|
||||||
|
f, err := os.Open(filepath.Join("testdata/toolchains", fixture))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
compiler := file.ToolchainComponentCompiler
|
||||||
|
linker := file.ToolchainComponentLinker
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fixture string
|
||||||
|
want []file.Toolchain
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "gcc: compiler only",
|
||||||
|
fixture: "gcc/bin/hello_gcc",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "gcc", Version: "13.4.0", Component: compiler},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "clang: compiler only",
|
||||||
|
fixture: "clang/bin/hello_clang",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "clang", Version: "18.1.8", Component: compiler},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "lld: clang compiler + lld linker",
|
||||||
|
fixture: "lld/bin/hello_lld",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "clang", Version: "18.1.8", Component: compiler},
|
||||||
|
{Name: "lld", Version: "19.1.4", Component: linker},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mold: gcc compiler + mold linker",
|
||||||
|
fixture: "mold/bin/hello_mold",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "gcc", Version: "14.2.0", Component: compiler},
|
||||||
|
{Name: "mold", Version: "2.34.1", Component: linker},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gold: gcc compiler + gold linker",
|
||||||
|
fixture: "gold/bin/hello_gold",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "gcc", Version: "14.2.0", Component: compiler},
|
||||||
|
{Name: "gold", Version: "1.16", Component: linker},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// rust binaries also carry a GCC producer string from the gcc-compiled C runtime glue that is
|
||||||
|
// linked in, so both compilers are reported (akin to how cgo binaries report go and gcc).
|
||||||
|
name: "rust: gcc glue + rustc",
|
||||||
|
fixture: "rust/bin/hello_rust",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "gcc", Version: "12.2.0", Component: compiler},
|
||||||
|
{Name: "rust", Version: "1.83.0", Component: compiler},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// gfortran shares the GCC version string in .comment, so it is only distinguishable from a
|
||||||
|
// C build by the presence of libgfortran runtime symbols (gcc gets relabeled to gfortran).
|
||||||
|
name: "fortran: gfortran compiler only",
|
||||||
|
fixture: "fortran/bin/hello_fortran",
|
||||||
|
want: []file.Toolchain{
|
||||||
|
{Name: "gfortran", Version: "13.4.0", Component: compiler},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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 := elfToolchains(reader, f)
|
||||||
|
|
||||||
|
if d := cmp.Diff(tt.want, got); d != "" {
|
||||||
|
t.Errorf("elfToolchains() mismatch (-want +got):\n%s", d)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,9 +6,16 @@ type (
|
|||||||
|
|
||||||
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
||||||
RelocationReadOnly string
|
RelocationReadOnly string
|
||||||
|
|
||||||
|
// ToolchainComponent represents which part of the toolchain an entry describes (e.g. compiler, linker).
|
||||||
|
// This can be expanded in the future to include assemblers, runtimes, and other toolchain components.
|
||||||
|
ToolchainComponent string
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
ToolchainComponentCompiler ToolchainComponent = "compiler"
|
||||||
|
ToolchainComponentLinker ToolchainComponent = "linker"
|
||||||
|
|
||||||
ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems
|
ELF ExecutableFormat = "elf" // Executable and Linkable Format used on Unix-like systems
|
||||||
MachO ExecutableFormat = "macho" // Mach object file format used on macOS and iOS
|
MachO ExecutableFormat = "macho" // Mach object file format used on macOS and iOS
|
||||||
PE ExecutableFormat = "pe" // Portable Executable format used on Windows
|
PE ExecutableFormat = "pe" // Portable Executable format used on Windows
|
||||||
@ -34,6 +41,20 @@ type Executable struct {
|
|||||||
|
|
||||||
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
||||||
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||||
|
|
||||||
|
// 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", "rust", "lld", "GNU AS", 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"`
|
||||||
|
|
||||||
|
// Component indicates which part of the toolchain this represents (e.g., compiler, linker, assembler).
|
||||||
|
Component ToolchainComponent `json:"component" yaml:"component" mapstructure:"component"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user