feat(executable): capture GNU ELF build IDs (#5426)

* feat(executable): capture GNU ELF build IDs

Capture GNU ELF build IDs in Syft JSON so consumers can correlate
otherwise unidentified binaries with known artifacts or build records.

Read GNU build-ID notes from .note.gnu.build-id, with a 64 KiB read limit.
Validate the note owner and type before recording the identifier.

Add focused extraction and JSON round-trip tests and update the schema.

Signed-off-by: Mario Corchero <mcorcherojim@bloomberg.net>

* chore: bump JSON schema to 16.1.13 for executable identifiers

16.1.12 landed on main for coordinate fullPath, so the identifiers
addition moves to the next version.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* feat(executable): capture go build IDs from .note.go.buildid

The go linker only writes a GNU build ID with -B or when linking
externally, so most pure-Go binaries had no identifier. Every go ELF
carries .note.go.buildid, captured as kind `go-build-id` in the form
`go tool buildid` prints.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* docs(executable): describe identifiers generically

Identifier field docs (and the published schema) now give GNU hex and
Go build IDs as examples instead of stating every value is a GNU build
ID in lowercase hex, so the contract holds as more kinds are added.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix(executable): keep identifier notes inside elfutil bounds

Register `.note.gnu.build-id` and `.note.go.buildid` in elfutil's
sectionsReadByName as that list asks, and never open a compressed
identifier note. Real ones are SHF_ALLOC and cannot be compressed, so
this drops the only decompression path into these reads.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* test(executable): check identifiers from real linker output

Cross-compiles a tiny go program with `-B` so the test sees a GNU
build ID and a go build ID written by an actual linker and read through
the cataloger entrypoint.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Mario Corchero <mcorcherojim@bloomberg.net>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Mario Corchero <mcorcherojim@bloomberg.net>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Aditya Sirish A Yelgundhalli
2026-11-04 22:42:03 +00:00
committed by GitHub
co-authored by Mario Corchero Alex Goodman
parent fd712ee71a
commit fa02ff2d26
8 changed files with 5081 additions and 5 deletions
+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.12" JSONSchemaVersion = "16.1.13"
// 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).
@@ -19,4 +19,5 @@ const (
// 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 CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers // 16.1.11 - add CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers
// 16.1.12 - add fullPath to file coordinates, naming the nested archive chain a file was found in (blank for files not within an archive) // 16.1.12 - add fullPath to file coordinates, naming the nested archive chain a file was found in (blank for files not within an archive)
// 16.1.13 - add executable identifiers (ELF GNU and Go build IDs)
) )
File diff suppressed because it is too large Load Diff
+26 -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.12/document", "$id": "anchore.io/schema/syft/json/16.1.13/document",
"$ref": "#/$defs/Document", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@@ -1468,6 +1468,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."
},
"identifiers": {
"items": {
"$ref": "#/$defs/ExecutableIdentifier"
},
"type": "array",
"description": "Identifiers lists self-declared build identifiers found in the binary (e.g. ELF GNU build IDs or Go build IDs)."
} }
}, },
"type": "object", "type": "object",
@@ -1479,6 +1486,24 @@
], ],
"description": "Executable contains metadata about binary files and their security features." "description": "Executable contains metadata about binary files and their security features."
}, },
"ExecutableIdentifier": {
"properties": {
"kind": {
"type": "string",
"description": "Kind identifies the type and source of the identifier."
},
"value": {
"type": "string",
"description": "Value is the identifier in the conventional text form for its Kind (e.g. lowercase hex for an ELF GNU build ID)."
}
},
"type": "object",
"required": [
"kind",
"value"
],
"description": "ExecutableIdentifier is a self-declared build identifier used to correlate a binary with its build or debug information."
},
"File": { "File": {
"properties": { "properties": {
"id": { "id": {
+127
View File
@@ -2,11 +2,16 @@ package executable
import ( import (
"debug/elf" "debug/elf"
"encoding/binary"
"encoding/hex"
"io"
"regexp" "regexp"
"strings" "strings"
"unicode/utf8"
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown" "github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
@@ -35,10 +40,132 @@ 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.Identifiers = findELFIdentifiers(f)
return err return err
} }
const (
// NT_GNU_BUILD_ID is not defined by debug/elf.
ntGNUBuildID = 3
// ELF_NOTE_GOBUILDID_TAG from cmd/link, the note type the go linker uses for the go build ID.
ntGoBuildID = 4
elfNoteOwnerGNU = "GNU"
elfNoteOwnerGo = "Go"
// bound decoded bytes read from any one identifier note section.
maxELFNoteBytes = 64 * intFile.KB
elfNoteHeaderSize = 12
)
type elfNote struct {
owner string
typ uint32
desc []byte
}
// elfIdentifierNote describes where one kind of identifier lives and how its descriptor becomes a value.
type elfIdentifierNote struct {
section string
owner string
typ uint32
kind file.ExecutableIdentifierKind
// encode returns the value for a descriptor, or "" to skip it.
encode func([]byte) string
}
var elfIdentifierNotes = []elfIdentifierNote{
{section: ".note.gnu.build-id", owner: elfNoteOwnerGNU, typ: ntGNUBuildID, kind: file.ELFGNUBuildID, encode: hex.EncodeToString},
// the go linker always writes this note, while it only writes a GNU build ID when asked (-B) or when linking externally
{section: ".note.go.buildid", owner: elfNoteOwnerGo, typ: ntGoBuildID, kind: file.GoBuildID, encode: goBuildIDValue},
}
func findELFIdentifiers(f *elf.File) []file.ExecutableIdentifier {
var ids []file.ExecutableIdentifier
for _, in := range elfIdentifierNotes {
ids = append(ids, findELFNoteIdentifiers(f, in)...)
}
return ids
}
func findELFNoteIdentifiers(f *elf.File, in elfIdentifierNote) []file.ExecutableIdentifier {
s := f.Section(in.section)
// real identifier notes are SHF_ALLOC, which the gABI forbids compressing, so a compressed one is
// hostile or broken and is never decompressed
if s == nil || s.Type != elf.SHT_NOTE || s.Flags&elf.SHF_COMPRESSED != 0 {
return nil
}
data, err := io.ReadAll(io.LimitReader(s.Open(), maxELFNoteBytes))
if err != nil {
log.WithFields("error", err, "section", in.section).Trace("unable to read ELF identifier note")
return nil
}
var ids []file.ExecutableIdentifier
seen := strset.New()
for _, n := range parseELFNotes(data, s.Addralign, f.ByteOrder) {
if n.owner != in.owner || n.typ != in.typ || len(n.desc) == 0 {
continue
}
value := in.encode(n.desc)
if value == "" || seen.Has(value) {
continue
}
seen.Add(value)
ids = append(ids, file.ExecutableIdentifier{Kind: in.kind, Value: value})
}
return ids
}
// goBuildIDValue keeps the go build ID as the text the go toolchain prints for it (e.g. "go tool buildid").
func goBuildIDValue(desc []byte) string {
if !utf8.Valid(desc) {
return ""
}
return string(desc)
}
// parseELFNotes decodes the Elf_Nhdr records in a note region, stopping at the first truncated record.
func parseELFNotes(data []byte, align uint64, order binary.ByteOrder) []elfNote {
// Some GNU property note regions use 8-byte alignment instead of 4.
if align != 8 {
align = 4
}
var notes []elfNote
for len(data) >= elfNoteHeaderSize {
nameSize := uint64(order.Uint32(data[0:4]))
descSize := uint64(order.Uint32(data[4:8]))
typ := order.Uint32(data[8:12])
nameEnd := elfNoteHeaderSize + nameSize
descStart := alignUp(nameEnd, align)
descEnd := descStart + descSize
if descEnd > uint64(len(data)) {
break
}
notes = append(notes, elfNote{
owner: strings.TrimRight(string(data[elfNoteHeaderSize:nameEnd]), "\x00"),
typ: typ,
desc: data[descStart:descEnd],
})
next := alignUp(descEnd, align)
if next >= uint64(len(data)) {
break
}
data = data[next:]
}
return notes
}
func alignUp(n, align uint64) uint64 {
return (n + align - 1) &^ (align - 1)
}
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),
+214 -3
View File
@@ -4,8 +4,12 @@ import (
"bytes" "bytes"
"debug/elf" "debug/elf"
"encoding/binary" "encoding/binary"
"encoding/hex"
"io"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
@@ -13,6 +17,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/elfutil"
"github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/internal/unionreader"
) )
@@ -285,6 +290,12 @@ func Test_hasElfDynTag(t *testing.T) {
// elfWithDynamicSection builds a little-endian ELF holding only a null section and a SHT_DYNAMIC section. // elfWithDynamicSection builds a little-endian ELF holding only a null section and a SHT_DYNAMIC section.
func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte { func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte {
t.Helper()
return elfWithSection(t, class, elf.SHT_DYNAMIC, 1, dynamic)
}
// elfWithSection builds a little-endian ELF holding only a null section and one section of the given type.
func elfWithSection(t *testing.T, class elf.Class, typ elf.SectionType, align uint64, data []byte) []byte {
t.Helper() t.Helper()
ident := [elf.EI_NIDENT]byte{0x7f, 'E', 'L', 'F', byte(class), byte(elf.ELFDATA2LSB), byte(elf.EV_CURRENT)} ident := [elf.EI_NIDENT]byte{0x7f, 'E', 'L', 'F', byte(class), byte(elf.ELFDATA2LSB), byte(elf.EV_CURRENT)}
var buf bytes.Buffer var buf bytes.Buffer
@@ -300,7 +311,7 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
Shoff: uint32(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2, Shoff: uint32(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
}) })
write(elf.Section32{}) write(elf.Section32{})
write(elf.Section32{Type: uint32(elf.SHT_DYNAMIC), Off: uint32(dataOff), Size: uint32(len(dynamic)), Addralign: 1}) write(elf.Section32{Type: uint32(typ), Off: uint32(dataOff), Size: uint32(len(data)), Addralign: uint32(align)})
} else { } else {
ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{}) ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{})
dataOff := ehsize + 2*shentsize dataOff := ehsize + 2*shentsize
@@ -309,8 +320,208 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
Shoff: uint64(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2, Shoff: uint64(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
}) })
write(elf.Section64{}) write(elf.Section64{})
write(elf.Section64{Type: uint32(elf.SHT_DYNAMIC), Off: uint64(dataOff), Size: uint64(len(dynamic)), Addralign: 1}) write(elf.Section64{Type: uint32(typ), Off: uint64(dataOff), Size: uint64(len(data)), Addralign: align})
} }
buf.Write(dynamic) buf.Write(data)
return buf.Bytes() return buf.Bytes()
} }
// encodeELFNote encodes a single little-endian Elf_Nhdr record, padding name and descriptor to align.
func encodeELFNote(owner string, typ uint32, desc []byte, align int) []byte {
pad := func(b []byte) []byte {
for len(b)%align != 0 {
b = append(b, 0)
}
return b
}
name := append([]byte(owner), 0)
out := binary.LittleEndian.AppendUint32(nil, uint32(len(name)))
out = binary.LittleEndian.AppendUint32(out, uint32(len(desc)))
out = binary.LittleEndian.AppendUint32(out, typ)
out = pad(append(out, name...))
return pad(append(out, desc...))
}
func Test_findELFIdentifiers(t *testing.T) {
sha1ID := bytes.Repeat([]byte{0xab}, 20)
customID := []byte{0xde, 0xad, 0xbe, 0xef}
longID := bytes.Repeat([]byte{0x01}, 32)
buildID := func(v []byte) file.ExecutableIdentifier {
return file.ExecutableIdentifier{Kind: file.ELFGNUBuildID, Value: hex.EncodeToString(v)}
}
const ntGNUABITag, ntGNUProperty = 1, 5
tests := []struct {
name string
elf []byte
sectionName string
want []file.ExecutableIdentifier
}{
{
name: "sha1-length build id",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
sectionName: ".note.gnu.build-id",
want: []file.ExecutableIdentifier{buildID(sha1ID)},
},
{
name: "caller-chosen short build id",
elf: elfWithSection(t, elf.ELFCLASS32, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, customID, 4)),
sectionName: ".note.gnu.build-id",
want: []file.ExecutableIdentifier{buildID(customID)},
},
{
name: "build id after other notes in the same section",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, append(
encodeELFNote("GNU", ntGNUABITag, make([]byte, 16), 4),
encodeELFNote("GNU", ntGNUBuildID, longID, 4)...,
)),
sectionName: ".note.gnu.build-id",
want: []file.ExecutableIdentifier{buildID(longID)},
},
{
name: "8-byte aligned note section",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 8, append(
encodeELFNote("GNU", ntGNUProperty, make([]byte, 12), 8),
encodeELFNote("GNU", ntGNUBuildID, customID, 8)...,
)),
sectionName: ".note.gnu.build-id",
want: []file.ExecutableIdentifier{buildID(customID)},
},
{
name: "duplicate build ids",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, bytes.Repeat(
encodeELFNote("GNU", ntGNUBuildID, customID, 4), 2,
)),
sectionName: ".note.gnu.build-id",
want: []file.ExecutableIdentifier{buildID(customID)},
},
{
name: "build id in another section is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
sectionName: ".note.other",
},
{
name: "build-id section with another section type is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_PROGBITS, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
sectionName: ".note.gnu.build-id",
},
{
name: "build-id section with another owner is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("FDO", ntGNUBuildID, sha1ID, 4)),
sectionName: ".note.gnu.build-id",
},
{
name: "build-id section with another note type is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUABITag, sha1ID, 4)),
sectionName: ".note.gnu.build-id",
},
{
name: "empty descriptor is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, nil, 4)),
sectionName: ".note.gnu.build-id",
},
{
name: "truncated descriptor is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)[:20]),
sectionName: ".note.gnu.build-id",
},
{
name: "go build id",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("Go\x00", ntGoBuildID, []byte("action/content"), 4)),
sectionName: ".note.go.buildid",
want: []file.ExecutableIdentifier{{Kind: file.GoBuildID, Value: "action/content"}},
},
{
name: "go build id that is not utf-8 is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("Go\x00", ntGoBuildID, []byte{0xff, 0xfe}, 4)),
sectionName: ".note.go.buildid",
},
{
name: "gnu build id note in the go section is ignored",
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
sectionName: ".note.go.buildid",
},
{
name: "no build-id section",
elf: elfWithDynamicSection(t, elf.ELFCLASS64, nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := elfutil.NewFile(bytes.NewReader(tt.elf))
require.NoError(t, err)
f.Sections[1].Name = tt.sectionName
assert.Equal(t, tt.want, findELFIdentifiers(f))
})
}
}
func Test_findELFIdentifiers_compressedSectionIgnored(t *testing.T) {
f, err := elfutil.NewFile(bytes.NewReader(elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, []byte{0xde, 0xad}, 4))))
require.NoError(t, err)
f.Sections[1].Name = ".note.gnu.build-id"
f.Sections[1].Flags |= elf.SHF_COMPRESSED
assert.Nil(t, findELFIdentifiers(f))
}
type countingNoteReader struct {
io.ReaderAt
bytesRead int
}
func (r *countingNoteReader) ReadAt(p []byte, off int64) (int, error) {
n, err := r.ReaderAt.ReadAt(p, off)
r.bytesRead += n
return n, err
}
func Test_findELFIdentifiers_readLimit(t *testing.T) {
id := encodeELFNote("GNU", ntGNUBuildID, []byte{0xde, 0xad, 0xbe, 0xef}, 4)
data := append(id, make([]byte, maxELFNoteBytes)...)
r := &countingNoteReader{ReaderAt: bytes.NewReader(elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, data))}
f, err := elfutil.NewFile(r)
require.NoError(t, err)
f.Sections[1].Name = ".note.gnu.build-id"
r.bytesRead = 0
assert.Equal(t, []file.ExecutableIdentifier{{Kind: file.ELFGNUBuildID, Value: "deadbeef"}}, findELFIdentifiers(f))
assert.Equal(t, maxELFNoteBytes, r.bytesRead)
}
func Test_parseELFNotes_bigEndian(t *testing.T) {
data := []byte{0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 'G', 'N', 'U', 0, 0xde, 0xad, 0xbe, 0xef}
assert.Equal(t, []elfNote{{owner: "GNU", typ: ntGNUBuildID, desc: data[16:]}}, parseELFNotes(data, 4, binary.BigEndian))
}
// Test_findELFFeatures_realLinkerIdentifiers proves identifiers are picked up from real linker output, not just
// hand-built notes. The go toolchain is the one linker every test environment has, and -B makes it emit a GNU
// build ID next to the go build ID it always writes.
func Test_findELFFeatures_realLinkerIdentifiers(t *testing.T) {
if testing.Short() {
t.Skip("builds a go binary")
}
dir := t.TempDir()
src := filepath.Join(dir, "main.go")
bin := filepath.Join(dir, "main")
require.NoError(t, os.WriteFile(src, []byte("package main\n\nfunc main() {}\n"), 0o600))
build := exec.Command("go", "build", "-trimpath", "-ldflags=-B 0x0123456789abcdef", "-o", bin, src)
build.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off")
out, err := build.CombinedOutput()
require.NoError(t, err, string(out))
goBuildID, err := exec.Command("go", "tool", "buildid", bin).Output()
require.NoError(t, err)
f, err := os.Open(bin)
require.NoError(t, err)
t.Cleanup(func() { _ = f.Close() })
var data file.Executable
require.NoError(t, findELFFeatures(&data, f))
assert.Equal(t, []file.ExecutableIdentifier{
{Kind: file.ELFGNUBuildID, Value: "0123456789abcdef"},
{Kind: file.GoBuildID, Value: strings.TrimSpace(string(goBuildID))},
}, data.Identifiers)
}
+19
View File
@@ -6,6 +6,9 @@ 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
// ExecutableIdentifierKind identifies the type and source of an executable identifier.
ExecutableIdentifierKind string
) )
const ( const (
@@ -16,6 +19,9 @@ const (
RelocationReadOnlyNone RelocationReadOnly = "none" // no RELRO protection RelocationReadOnlyNone RelocationReadOnly = "none" // no RELRO protection
RelocationReadOnlyPartial RelocationReadOnly = "partial" // partial RELRO protection RelocationReadOnlyPartial RelocationReadOnly = "partial" // partial RELRO protection
RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection
ELFGNUBuildID ExecutableIdentifierKind = "elf-gnu-build-id" // GNU build ID from an ELF .note.gnu.build-id note
GoBuildID ExecutableIdentifierKind = "go-build-id" // build ID written by the go linker
) )
// Executable contains metadata about binary files and their security features. // Executable contains metadata about binary files and their security features.
@@ -34,6 +40,19 @@ 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"`
// Identifiers lists self-declared build identifiers found in the binary (e.g. ELF GNU build IDs or Go build IDs).
Identifiers []ExecutableIdentifier `json:"identifiers,omitempty" yaml:"identifiers" mapstructure:"identifiers"`
}
// ExecutableIdentifier is a self-declared build identifier used to correlate a binary with its build or debug information.
// It can survive stripping; use file digests to verify content.
type ExecutableIdentifier struct {
// Kind identifies the type and source of the identifier.
Kind ExecutableIdentifierKind `json:"kind" yaml:"kind" mapstructure:"kind"`
// Value is the identifier in the conventional text form for its Kind (e.g. lowercase hex for an ELF GNU build ID).
Value string `json:"value" yaml:"value" mapstructure:"value"`
} }
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries. // ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
+10
View File
@@ -219,6 +219,9 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
Executables: map[file.Coordinates]file.Executable{ Executables: map[file.Coordinates]file.Executable{
c: { c: {
Format: file.ELF, Format: file.ELF,
Identifiers: []file.ExecutableIdentifier{
{Kind: file.ELFGNUBuildID, Value: "0123456789abcdef"},
},
ELFSecurityFeatures: &file.ELFSecurityFeatures{ ELFSecurityFeatures: &file.ELFSecurityFeatures{
SymbolTableStripped: false, SymbolTableStripped: false,
StackCanary: boolRef(true), StackCanary: boolRef(true),
@@ -288,4 +291,11 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion) assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
require.Equal(t, s, *got) require.Equal(t, s, *got)
executable := s.Artifacts.Executables[c]
executable.Identifiers = nil
s.Artifacts.Executables[c] = executable
buf.Reset()
require.NoError(t, enc.Encode(buf, s))
assert.NotContains(t, buf.String(), `"identifiers"`)
} }
+3
View File
@@ -82,6 +82,9 @@ var sectionsReadByName = map[string]struct{}{
".gopclntab": {}, ".gopclntab": {},
".note.package": {}, ".note.package": {},
".modinfo": {}, ".modinfo": {},
// identifier notes read by the executable cataloger
".note.gnu.build-id": {},
".note.go.buildid": {},
} }
// wire sizes of the structures we decode, which differ between the two ELF classes // wire sizes of the structures we decode, which differ between the two ELF classes