add file-based toolchain detection

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-29 15:05:31 -04:00
parent c0072873c9
commit d7875ca014
66 changed files with 6163 additions and 64 deletions
+8 -14
View File
@@ -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/cpp" "github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet" "github.com/anchore/syft/syft/pkg/cataloger/dotnet"
@@ -147,18 +145,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 {
+1 -1
View File
@@ -64,7 +64,7 @@ func (c *fileConfig) PostLoad() error {
} }
func (c *fileConfig) DescribeFields(descriptions clio.FieldDescriptionSet) { 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: Options include:
- "all": capture all files from the search space - "all": capture all files from the search space
- "owned-by-package": capture only files owned by packages - "owned-by-package": capture only files owned by packages
+2 -1
View File
@@ -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.10" JSONSchemaVersion = "16.1.11"
// 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).
@@ -17,4 +17,5 @@ const (
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support // 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
// 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata // 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata
// 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger // 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
// 16.1.11 - add file executable toolchain information
) )
File diff suppressed because it is too large Load Diff
+29 -1
View File
@@ -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.10/document", "$id": "anchore.io/schema/syft/json/16.1.11/document",
"$ref": "#/$defs/Document", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@@ -1398,6 +1398,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",
@@ -4450,6 +4457,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"
]
},
"VcpkgManifest": { "VcpkgManifest": {
"properties": { "properties": {
"description": { "description": {
+49 -46
View File
@@ -26,8 +26,11 @@ 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 []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 { type Cataloger struct {
@@ -116,6 +119,51 @@ func processExecutableLocation(loc file.Location, resolver file.Resolver) (exec
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{
@@ -162,51 +210,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)
+45
View File
@@ -35,10 +35,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),
@@ -82,6 +105,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 {
@@ -9,6 +9,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"
@@ -314,3 +315,73 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
buf.Write(dynamic) buf.Write(dynamic)
return buf.Bytes() return buf.Bytes()
} }
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)
}
})
}
+8
View File
@@ -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)
}
})
}
}
+7
View File
@@ -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),
)
}
+37
View File
@@ -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
@@ -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
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
@@ -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
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
)
+4
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=
+28
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))
}
@@ -0,0 +1,3 @@
bin/
Dockerfile.sha256
*.fingerprint
@@ -0,0 +1,39 @@
# invoke all make files in subdirectories
.PHONY: all gcc clang lld mold gold rust fortran gdc gccgo gnat
all: gcc clang lld mold gold rust fortran gdc gccgo gnat
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
gdc:
$(MAKE) -C gdc
gccgo:
$(MAKE) -C gccgo
gnat:
$(MAKE) -C gnat
%:
@for dir in gcc clang lld mold gold rust fortran gdc gccgo gnat; do \
$(MAKE) -C $$dir $@; \
done
@@ -0,0 +1,6 @@
FROM alpine:3.21.7@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
RUN apk add --no-cache clang18 musl-dev make
# create symlink so 'clang' command works (Alpine installs as clang-18)
RUN ln -s /usr/bin/clang-18 /usr/bin/clang
@@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-clang-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean
@@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_clang
$(BIN)/hello_clang: hello.c
clang hello.c -o $(BIN)/hello_clang
clean:
rm -f $(BIN)/hello_clang
@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
@@ -0,0 +1 @@
FROM gcc:13.4.0@sha256:d49302cbe2faf1a2dece312e258325e0d39e32874ea4dec122b4ac86f45d5b50
@@ -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
@@ -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
@@ -0,0 +1,3 @@
program hello
print *, "Hello, World!"
end program hello
@@ -0,0 +1 @@
FROM gcc:13.4.0@sha256:d49302cbe2faf1a2dece312e258325e0d39e32874ea4dec122b4ac86f45d5b50
@@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-gcc-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean
@@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_gcc
$(BIN)/hello_gcc: hello.c
gcc hello.c -o $(BIN)/hello_gcc
clean:
rm -f $(BIN)/hello_gcc
@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
@@ -0,0 +1,3 @@
FROM debian:12.14@sha256:49ba348354a28e39c70beffd6cf43bdb8d55d81ce4b746b0428717d054b8bbc4
RUN apt-get update && apt-get install -y --no-install-recommends gccgo make && rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-gccgo-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean
@@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_gccgo
$(BIN)/hello_gccgo: hello.go
gccgo hello.go -o $(BIN)/hello_gccgo
clean:
rm -f $(BIN)/hello_gccgo
@@ -0,0 +1,5 @@
package main
func main() {
println("Hello, World!")
}
@@ -0,0 +1,3 @@
FROM debian:12.14@sha256:49ba348354a28e39c70beffd6cf43bdb8d55d81ce4b746b0428717d054b8bbc4
RUN apt-get update && apt-get install -y --no-install-recommends gdc make && rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-gdc-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean
@@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_gdc
$(BIN)/hello_gdc: hello.d
gdc hello.d -o $(BIN)/hello_gdc
clean:
rm -f $(BIN)/hello_gdc
@@ -0,0 +1,5 @@
import std.stdio;
void main() {
writeln("Hello, World!");
}
@@ -0,0 +1,3 @@
FROM debian:12.14@sha256:49ba348354a28e39c70beffd6cf43bdb8d55d81ce4b746b0428717d054b8bbc4
RUN apt-get update && apt-get install -y --no-install-recommends gnat make && rm -rf /var/lib/apt/lists/*
@@ -0,0 +1,39 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-toolchain-gnat-build-tools:latest
FINGERPRINT_FILE=$(BIN).fingerprint
ifndef BIN
$(error BIN is not set)
endif
.DEFAULT_GOAL := fixtures
# requirement 1: 'fixtures' goal to generate any and all test fixtures
fixtures: build
# requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted
fingerprint: $(FINGERPRINT_FILE)
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
tools:
@(docker inspect $(TOOL_IMAGE) > /dev/null && make tools-check) || (docker build --platform linux/amd64 -t $(TOOL_IMAGE) . && sha256sum Dockerfile > Dockerfile.sha256)
build: tools
@mkdir -p $(BIN)
docker run --platform linux/amd64 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
debug:
docker run --platform linux/amd64 -i --rm -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) bash
# requirement 3: we always need to recalculate the fingerprint based on source regardless of any existing fingerprint
.PHONY: $(FINGERPRINT_FILE)
$(FINGERPRINT_FILE):
@find project Dockerfile Makefile -type f -exec sha256sum {} \; | sort -k2 > $(FINGERPRINT_FILE)
# requirement 4: 'clean' goal to remove all generated test fixtures
clean:
rm -rf $(BIN) Dockerfile.sha256 $(FINGERPRINT_FILE)
.PHONY: tools tools-check build debug clean
@@ -0,0 +1,9 @@
BIN=../bin
all: $(BIN)/hello_gnat
$(BIN)/hello_gnat: hello.adb
gnatmake hello.adb -o $(BIN)/hello_gnat
clean:
rm -f $(BIN)/hello_gnat *.o *.ali
@@ -0,0 +1,6 @@
with Ada.Text_IO;
procedure Hello is
begin
Ada.Text_IO.Put_Line ("Hello, World!");
end Hello;
@@ -0,0 +1,51 @@
V "GNAT Lib v12"
M P W=b
P ZX
RN
RV NO_IO
U hello%b hello.adb b6dbb388 NE OO SU
W ada%s ada.ads ada.ali
Z ada.strings.text_buffers%s a-sttebu.adb a-sttebu.ali
W ada.text_io%s a-textio.adb a-textio.ali
D ada.ads 20220819080952 76789da1 ada%s
D a-except.ads 20220819080952 2a7d8d70 ada.exceptions%s
D a-ioexce.ads 20220819080952 40018c65 ada.io_exceptions%s
D a-stream.ads 20220819080952 119b8fb3 ada.streams%s
D a-string.ads 20220819080952 90ac6797 ada.strings%s
D a-sttebu.ads 20220819080952 18209c64 ada.strings.text_buffers%s
D a-stuten.ads 20220819080952 33c54060 ada.strings.utf_encoding%s
D a-tags.ads 20220819080952 9eaa38c6 ada.tags%s
D a-textio.ads 20220819080952 deada98a ada.text_io%s
D a-unccon.ads 20220819080952 0e9b276f ada.unchecked_conversion%s
D hello.adb 20260618132941 68761a02 hello%b
D interfac.ads 20220819080952 edec285f interfaces%s
D i-cstrea.ads 20220819080952 e53d8b8e interfaces.c_streams%s
D system.ads 20220819080952 a2da961d system%s
D s-crtl.ads 20220819080952 8a0692f8 system.crtl%s
D s-exctab.ads 20220819080952 91bef6ef system.exception_table%s
D s-ficobl.ads 20220819080952 dc5161d4 system.file_control_block%s
D s-parame.ads 20220819080952 d494a4a6 system.parameters%s
D s-putima.ads 20220819080952 4e095881 system.put_images%s
D s-secsta.ads 20220819080952 62cd9202 system.secondary_stack%s
D s-soflin.ads 20220819080952 df77073e system.soft_links%s
D s-stache.ads 20220819080952 0b81c1fe system.stack_checking%s
D s-stalib.ads 20220819080952 1c9580f6 system.standard_library%s
D s-stoele.ads 20220819080952 6d8e1ff4 system.storage_elements%s
D s-stoele.adb 20220819080952 8f5a9db6 system.storage_elements%b
D s-traent.ads 20220819080952 c81cbf8c system.traceback_entries%s
D s-unstyp.ads 20220819080952 fa2a7f59 system.unsigned_types%s
D s-wchcon.ads 20220819080952 d9032363 system.wch_con%s
D unchconv.ads 20220819080952 ca2f9e18 unchecked_conversion%s
G a e
G c Z b b [hello standard 3 11 none]
X 1 ada.ads
16K9*Ada 20e8 11|1r6 5r4
X 9 a-textio.ads
58K13*Text_IO 727e16 11|1w10 5r8
507U14*Put_Line 11|5s16
X 11 hello.adb
3U11*Hello 3b11 6l5 6t10
@@ -0,0 +1,3 @@
FROM alpine:3.21.7@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
RUN apk add --no-cache gcc binutils-gold musl-dev make
@@ -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
@@ -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
@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
@@ -0,0 +1,6 @@
FROM alpine:3.21.7@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
@@ -0,0 +1,3 @@
FROM alpine:3.21.7@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d
RUN apk add --no-cache gcc mold musl-dev make
@@ -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
@@ -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
@@ -0,0 +1,6 @@
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
@@ -0,0 +1 @@
FROM rust:1.83.0@sha256:a45bf1f5d9af0a23b26703b3500d70af1abff7f984a7abef5a104b42c02a292b
@@ -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
@@ -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
@@ -0,0 +1,3 @@
fn main() {
println!("Hello, World!");
}
@@ -0,0 +1,248 @@
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. more clang forks: AMD AOCC and Arm Compiler. their producer strings are vendor-prefixed clang
// versions (e.g. "AMD clang version 14.0.6 ...") that clangVersionPattern already matches, so today
// they are mislabeled as plain "clang"; each needs a vendor pattern matched before clangVersionPattern
// (the same ordering used for icx). not yet added because neither ships a freely buildable linux/amd64
// image to anchor a real fixture (AOCC is proprietary; Arm Compiler is AArch64-only). 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_*), and the LLVM-based D compilers ldc/dmd (_Dmain). the
// GCC D frontend (gdc) is already handled via the shared GCC .comment path. 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.
//
// not feasible via this mechanism (recorded so they aren't re-investigated):
// - TinyCC (tcc): emits no .comment section, no producer string, and no identifying symbols (stripped
// by default), so a tcc-built ELF carries no usable signal here.
// - Intel classic icc/ifort, IBM XL, NVIDIA/PGI: distinct producer strings exist but the toolchains are
// proprietary/deprecated and not freely dockerizable on linux/amd64 to anchor a real fixture.
//
// 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+)`)
// the Intel oneAPI DPC++/C++ compiler (icx/icpx) is an LLVM/clang fork that writes its own producer
// string into .comment, e.g. "Intel(R) oneAPI DPC++/C++ Compiler 2024.0.2 (2024.0.2.20231223)" (see
// https://briancallahan.net/blog/20240306.html for a real `readelf .comment` dump). since it is a clang
// fork it may also emit a plain "clang version" entry, so it is matched before clangVersionPattern to
// avoid mislabeling as upstream clang. evidence for the producer string and version scheme:
// - https://briancallahan.net/blog/20240306.html
// - https://www.intel.com/content/www/us/en/developer/articles/release-notes/oneapi-dpcpp-cpp-compiler-release-notes.html
intelVersionPattern = regexp.MustCompile(`Intel\(R\) oneAPI[^0-9]*(\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/). distros often
// inject a vendor prefix (e.g. Debian/Ubuntu emit "Linker: Ubuntu LLD 18.1.3"), so allow one.
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 compiler information from the ELF .comment section for the
// GCC and Clang families. This covers upstream GCC/Clang, GCC frontends that share the GCC producer
// string but pull in distinct runtime symbols (gfortran, gdc, gccgo, gnat), and the Intel oneAPI
// (icx/icpx) clang fork.
func cToolchainEvidence(comments []string, symbols *strset.Set) *file.Toolchain {
for _, comment := range comments {
// Intel oneAPI (icx) is a clang fork whose binaries carry both an "Intel(R) oneAPI ..." producer
// string and a plain "clang version" entry, so it must be checked before the generic clang match.
if match := intelVersionPattern.FindStringSubmatch(comment); match != nil {
return &file.Toolchain{
Name: "icx",
Version: match[1],
Component: file.ToolchainComponentCompiler,
}
}
}
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 {
return &file.Toolchain{
Name: gccFrontendName(symbols),
Version: match[1],
Component: file.ToolchainComponentCompiler,
}
}
}
return nil
}
// gccFrontendName disambiguates which GCC frontend produced a binary. gfortran, gdc, gccgo, and gnat
// all share the same "GCC: (...) <version>" string in .comment, so the only way to tell them apart from
// a plain C/C++ build is by the language-specific runtime symbols they pull in. defaults to "gcc".
func gccFrontendName(symbols *strset.Set) string {
switch {
case hasFortranEvidence(symbols):
return "gfortran"
case hasDEvidence(symbols):
return "gdc"
case hasGccgoEvidence(symbols):
return "gccgo"
case hasAdaEvidence(symbols):
return "gnat"
default:
return "gcc"
}
}
// 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")
}
// hasDEvidence reports whether the binary references the gdc (GNU D) runtime. _Dmain is the D program
// entry the compiler emits, and __gdc_personality_v0 is gdc's exception-handling personality routine.
// see the gdc frontend in gcc: https://github.com/gcc-mirror/gcc/tree/master/gcc/d
func hasDEvidence(symbols *strset.Set) bool {
return symbols.HasAny("_Dmain", "__gdc_personality_v0")
}
// hasGccgoEvidence reports whether the binary was built by gccgo. the libgo runtime exports __go_*
// helpers (e.g. __go_go to start a goroutine) that the gc toolchain does not emit, so these
// distinguish gccgo both from a plain gcc C build and from a cgo-enabled gc binary (which also carries
// a gcc .comment). see libgo: https://github.com/gcc-mirror/gcc/tree/master/libgo/runtime
func hasGccgoEvidence(symbols *strset.Set) bool {
return symbols.HasAny("__go_go", "__go_init_main", "__go_register_gc_roots")
}
// hasAdaEvidence reports whether the binary references the GNAT Ada runtime. adainit/adafinal are the
// elaboration entry points and __gnat_* are runtime support calls.
// see the GNAT runtime (libgnat): https://github.com/gcc-mirror/gcc/tree/master/gcc/ada
func hasAdaEvidence(symbols *strset.Set) bool {
return symbols.HasAny("__gnat_initialize", "__gnat_runtime_initialize", "adainit", "adafinal")
}
@@ -0,0 +1,223 @@
package executable
import (
"debug/elf"
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/scylladb/go-set/strset"
"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},
},
},
{
// gdc is a GCC frontend that shares the GCC version string, so it is distinguished from a C
// build only by the D runtime symbols (_Dmain, __gdc_personality_v0).
name: "gdc: GNU D compiler only",
fixture: "gdc/bin/hello_gdc",
want: []file.Toolchain{
{Name: "gdc", Version: "12.2.0", Component: compiler},
},
},
{
// gccgo is a GCC frontend (not the gc toolchain, so it has no Go build info); it shares the
// GCC version string and is distinguished only by the libgo __go_* runtime symbols.
name: "gccgo: GNU Go compiler only",
fixture: "gccgo/bin/hello_gccgo",
want: []file.Toolchain{
{Name: "gccgo", Version: "12.2.0", Component: compiler},
},
},
{
// gnat is the GCC Ada frontend; it shares the GCC version string and is distinguished only by
// the GNAT runtime symbols (adainit, __gnat_*).
name: "gnat: GNU Ada compiler only",
fixture: "gnat/bin/hello_gnat",
want: []file.Toolchain{
{Name: "gnat", Version: "12.2.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)
}
})
}
}
func Test_cToolchainEvidence(t *testing.T) {
compiler := file.ToolchainComponentCompiler
tests := []struct {
name string
comments []string
symbols []string
want *file.Toolchain
}{
{
// icx (Intel oneAPI DPC++/C++) is an LLVM/clang fork. there is no freely buildable linux/amd64
// image to anchor a real fixture, so the literal .comment producer string is asserted here. the
// exact form is confirmed by a real `readelf -x .comment` dump (which yielded
// "Intel(R) oneAPI DPC++/C++ Compiler 2024.0.2 (2024.0.2.20231223)"):
// - https://briancallahan.net/blog/20240306.html
// - https://www.intel.com/content/www/us/en/developer/articles/release-notes/oneapi-dpcpp/2025.html
// because it is a clang fork, the Intel string must be matched before the generic clang pattern;
// the second "clang version" entry below guards that ordering (a generic clang match must not win).
name: "intel oneAPI icx is matched before clang",
comments: []string{
"Intel(R) oneAPI DPC++/C++ Compiler 2024.0.2 (2024.0.2.20231223)",
"clang version 18.1.0",
},
want: &file.Toolchain{Name: "icx", Version: "2024.0.2", Component: compiler},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := cToolchainEvidence(tt.comments, strset.New(tt.symbols...))
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf("cToolchainEvidence() mismatch (-want +got):\n%s", d)
}
})
}
}
func Test_linkerToolchainEvidence(t *testing.T) {
linker := file.ToolchainComponentLinker
tests := []struct {
name string
comments []string
want *file.Toolchain
}{
{
// lld writes "Linker: " + getLLDVersion() into .comment. docs and source:
// - https://lld.llvm.org/ ("If the string \"Linker: LLD\" is included ... you are using LLD")
// - https://github.com/llvm/llvm-project/blob/main/lld/ELF/SyntheticSections.cpp (Twine("Linker: ") + getLLDVersion())
name: "vanilla lld",
comments: []string{"Linker: LLD 19.1.4"},
want: &file.Toolchain{Name: "lld", Version: "19.1.4", Component: linker},
},
{
// debian/ubuntu prepend a vendor to getLLDVersion() (the same packaging mechanism behind clang's
// "Ubuntu clang version ..."), so .comment reads "Linker: Ubuntu LLD <version>". reproduced on
// ubuntu:24.04: `readelf -p .comment <bin>` => "Linker: Ubuntu LLD 18.1.3".
name: "vendor-prefixed lld",
comments: []string{"Linker: Ubuntu LLD 18.1.3"},
want: &file.Toolchain{Name: "lld", Version: "18.1.3", Component: linker},
},
{
// mold writes "mold <version> (<commit>; compatible with GNU ld)" into .comment. this format is
// documented by mold and is also exercised end-to-end by the real fixture in Test_elfToolchains:
// - https://github.com/rui314/mold (README: "mold leaves its identification string in .comment")
name: "mold",
comments: []string{"mold 2.34.1 (compatible with GNU ld)"},
want: &file.Toolchain{Name: "mold", Version: "2.34.1", Component: linker},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// f is only consulted for the gold note, which these comment-based cases match before reaching.
got := linkerToolchainEvidence(nil, tt.comments)
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf("linkerToolchainEvidence() mismatch (-want +got):\n%s", d)
}
})
}
}
+21
View File
@@ -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.