add file-based toolchain detection

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-12-09 11:31:48 -05:00
parent 58e4dbbf01
commit 1c72d03da3
No known key found for this signature in database
52 changed files with 5487 additions and 64 deletions

View File

@ -17,8 +17,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"
@ -144,18 +142,14 @@ 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
return c
}
func (cfg Catalog) ToLicenseConfig() cataloging.LicenseConfig {

View File

@ -64,7 +64,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

View File

@ -3,7 +3,7 @@ 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.4"
JSONSchemaVersion = "16.1.5"
// Changelog
// 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.3 - add GGUFFileParts to GGUFFileHeader metadata
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
// 16.1.5 - add file executable toolchain information
)

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"$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",
"$defs": {
"AlpmDbEntry": {
@ -1291,6 +1291,13 @@
"elfSecurityFeatures": {
"$ref": "#/$defs/ELFSecurityFeatures",
"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",
@ -4269,6 +4276,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\", \"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": {
"properties": {
"pluginInstallDirectory": {

View File

@ -25,8 +25,11 @@ import (
)
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"`
}
type Cataloger struct {
@ -106,6 +109,51 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (*file
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 {
info := monitor.GenericTask{
Title: monitor.Title{
@ -152,51 +200,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)

View File

@ -34,10 +34,33 @@ 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)
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 {
return &file.ELFSecurityFeatures{
SymbolTableStripped: isElfSymbolTableStripped(f),
@ -81,6 +104,28 @@ func boolRef(b bool) *bool {
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 {
// find the program headers until you find the GNU_STACK segment
for _, prog := range file.Progs {

View File

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

View File

@ -48,6 +48,8 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) er
if !data.HasExports {
data.HasExports = machoHasExports(f)
}
data.Toolchains = machoToolchains(reader)
}
// de-duplicate libraries
@ -56,6 +58,12 @@ func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) er
return nil
}
func machoToolchains(reader unionreader.UnionReader) []file.Toolchain {
return includeNoneNil(
golangToolchainEvidence(reader),
)
}
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

View File

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

View File

@ -25,6 +25,7 @@ func findPEFeatures(data *file.Executable, reader unionreader.UnionReader) error
data.ImportedLibraries = libs
data.HasEntrypoint = peHasEntrypoint(f)
data.HasExports = peHasExports(f)
data.Toolchains = peToolchains(reader)
return nil
}
@ -82,3 +83,9 @@ func peHasExports(f *pe.File) bool {
return false
}
func peToolchains(reader unionreader.UnionReader) []file.Toolchain {
return includeNoneNil(
golangToolchainEvidence(reader),
)
}

View File

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

View File

@ -1,3 +1,4 @@
bin
actual_verify
Dockerfile.sha256
Dockerfile.sha256
*.fingerprint

View 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

View 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

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

View 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
)

View 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=

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

View File

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

View 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

View 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

View File

@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-clang-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean

View File

@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_clang
$(BIN)/hello_clang: hello.c
clang hello.c -o $(BIN)/hello_clang
clean:
rm -f $(BIN)/hello_clang

View File

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

View File

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

View File

@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-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

View 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

View File

@ -0,0 +1,3 @@
program hello
print *, "Hello, World!"
end program hello

View File

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

View File

@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-gcc-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean

View File

@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_gcc
$(BIN)/hello_gcc: hello.c
gcc hello.c -o $(BIN)/hello_gcc
clean:
rm -f $(BIN)/hello_gcc

View File

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

View File

@ -0,0 +1,3 @@
FROM alpine:3.21
RUN apk add --no-cache gcc binutils-gold musl-dev make

View 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

View 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

View File

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

View 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

View 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

View 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

View File

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

View File

@ -0,0 +1,3 @@
FROM alpine:3.21
RUN apk add --no-cache gcc mold musl-dev make

View 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

View 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

View File

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

View File

@ -0,0 +1 @@
FROM rust:1.83.0

View 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

View 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

View File

@ -0,0 +1,3 @@
fn main() {
println!("Hello, World!");
}

View 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")
}

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

View File

@ -6,9 +6,16 @@ type (
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
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 (
ToolchainComponentCompiler ToolchainComponent = "compiler"
ToolchainComponentLinker ToolchainComponent = "linker"
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 +41,20 @@ type Executable struct {
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
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.