mirror of
https://github.com/anchore/syft.git
synced 2026-02-12 02:26:42 +01:00
Add cataloging of macho multi-architecture binaries (#657)
* add cataloging within universal binaries Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * update json test fixtures Signed-off-by: Alex Goodman <alex.goodman@anchore.com> * add comments + correct 32 bit multi arch magic check Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
c7752f0f6c
commit
da0b17b719
@ -6,5 +6,5 @@ const (
|
|||||||
|
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
||||||
// 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 = "2.0.0"
|
JSONSchemaVersion = "2.0.1"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -60,8 +60,6 @@ func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
if !bytes.Equal(expected, actual) {
|
if !bytes.Equal(expected, actual) {
|
||||||
dmp := diffmatchpatch.New()
|
dmp := diffmatchpatch.New()
|
||||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||||
t.Logf("len: %d\nexpected: %v", len(expected), expected)
|
|
||||||
t.Logf("len: %d\nactual: %v", len(actual), actual)
|
|
||||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
|
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -179,7 +179,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
|
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.0.json"
|
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-2.0.1.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,7 @@ type artifactMetadataContainer struct {
|
|||||||
Python pkg.PythonPackageMetadata
|
Python pkg.PythonPackageMetadata
|
||||||
Rpm pkg.RpmdbMetadata
|
Rpm pkg.RpmdbMetadata
|
||||||
Cargo pkg.CargoPackageMetadata
|
Cargo pkg.CargoPackageMetadata
|
||||||
|
Go pkg.GolangBinMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
1025
schema/json/schema-2.0.1.json
Normal file
1025
schema/json/schema-2.0.1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@ import (
|
|||||||
"debug/pe"
|
"debug/pe"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF).
|
// An exe is a generic interface to an OS executable (ELF, Mach-O, PE, XCOFF).
|
||||||
@ -24,6 +25,9 @@ type exe interface {
|
|||||||
// ReadData reads and returns up to size byte starting at virtual address addr.
|
// ReadData reads and returns up to size byte starting at virtual address addr.
|
||||||
ReadData(addr, size uint64) ([]byte, error)
|
ReadData(addr, size uint64) ([]byte, error)
|
||||||
|
|
||||||
|
// ArchName returns a string that represents the CPU architecture of the executable.
|
||||||
|
ArchName() string
|
||||||
|
|
||||||
// DataStart returns the writable data segment start address.
|
// DataStart returns the writable data segment start address.
|
||||||
DataStart() uint64
|
DataStart() uint64
|
||||||
}
|
}
|
||||||
@ -32,7 +36,7 @@ type exe interface {
|
|||||||
// we changed this signature from accpeting a string
|
// we changed this signature from accpeting a string
|
||||||
// to a ReadCloser so we could adapt the code to the
|
// to a ReadCloser so we could adapt the code to the
|
||||||
// stereoscope api. We removed the file open methods.
|
// stereoscope api. We removed the file open methods.
|
||||||
func openExe(file io.ReadCloser) (exe, error) {
|
func openExe(file io.ReadCloser) ([]exe, error) {
|
||||||
/*
|
/*
|
||||||
f, err := os.Open(file)
|
f, err := os.Open(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -57,7 +61,7 @@ func openExe(file io.ReadCloser) (exe, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &elfExe{file, e}, nil
|
return []exe{&elfExe{file, e}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.HasPrefix(data, []byte("MZ")) {
|
if bytes.HasPrefix(data, []byte("MZ")) {
|
||||||
@ -65,7 +69,7 @@ func openExe(file io.ReadCloser) (exe, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &peExe{file, e}, nil
|
return []exe{&peExe{file, e}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
|
if bytes.HasPrefix(data, []byte("\xFE\xED\xFA")) || bytes.HasPrefix(data[1:], []byte("\xFA\xED\xFE")) {
|
||||||
@ -73,7 +77,20 @@ func openExe(file io.ReadCloser) (exe, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &machoExe{file, e}, nil
|
return []exe{&machoExe{file, e}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// adding macho multi-architecture support (both for 64bit and 32 bit)... this case is not in the stdlib yet
|
||||||
|
if bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBE")) || bytes.HasPrefix(data, []byte("\xCA\xFE\xBA\xBF")) {
|
||||||
|
fatExe, err := macho.NewFatFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var exes []exe
|
||||||
|
for _, arch := range fatExe.Arches {
|
||||||
|
exes = append(exes, &machoExe{file, arch.File})
|
||||||
|
}
|
||||||
|
return exes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("unrecognized executable format")
|
return nil, fmt.Errorf("unrecognized executable format")
|
||||||
@ -90,6 +107,14 @@ func (x *elfExe) Close() error {
|
|||||||
return x.os.Close()
|
return x.os.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *elfExe) ArchName() string {
|
||||||
|
return cleanElfArch(x.f.Machine)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanElfArch(machine elf.Machine) string {
|
||||||
|
return strings.TrimPrefix(strings.ToLower(machine.String()), "em_")
|
||||||
|
}
|
||||||
|
|
||||||
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
|
func (x *elfExe) ReadData(addr, size uint64) ([]byte, error) {
|
||||||
for _, prog := range x.f.Progs {
|
for _, prog := range x.f.Progs {
|
||||||
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
|
if prog.Vaddr <= addr && addr <= prog.Vaddr+prog.Filesz-1 {
|
||||||
@ -132,6 +157,56 @@ func (x *peExe) Close() error {
|
|||||||
return x.os.Close()
|
return x.os.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *peExe) ArchName() string {
|
||||||
|
// from: debug/pe/pe.go
|
||||||
|
switch x.f.Machine {
|
||||||
|
case pe.IMAGE_FILE_MACHINE_AM33:
|
||||||
|
return "amd33"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_AMD64:
|
||||||
|
return "amd64"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_ARM:
|
||||||
|
return "arm"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_ARMNT:
|
||||||
|
return "armnt"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_ARM64:
|
||||||
|
return "arm64"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_EBC:
|
||||||
|
return "ebc"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_I386:
|
||||||
|
return "i386"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_IA64:
|
||||||
|
return "ia64"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_M32R:
|
||||||
|
return "m32r"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_MIPS16:
|
||||||
|
return "mips16"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_MIPSFPU:
|
||||||
|
return "mipsfpu"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_MIPSFPU16:
|
||||||
|
return "mipsfpu16"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_POWERPC:
|
||||||
|
return "ppc"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_POWERPCFP:
|
||||||
|
return "ppcfp"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_R4000:
|
||||||
|
return "r4000"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_SH3:
|
||||||
|
return "sh3"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_SH3DSP:
|
||||||
|
return "sh3dsp"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_SH4:
|
||||||
|
return "sh4"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_SH5:
|
||||||
|
return "sh5"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_THUMB:
|
||||||
|
return "thumb"
|
||||||
|
case pe.IMAGE_FILE_MACHINE_WCEMIPSV2:
|
||||||
|
return "wcemipsv2"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("unknown-pe-machine-%d", x.f.Machine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (x *peExe) imageBase() uint64 {
|
func (x *peExe) imageBase() uint64 {
|
||||||
switch oh := x.f.OptionalHeader.(type) {
|
switch oh := x.f.OptionalHeader.(type) {
|
||||||
case *pe.OptionalHeader32:
|
case *pe.OptionalHeader32:
|
||||||
@ -193,6 +268,14 @@ func (x *machoExe) Close() error {
|
|||||||
return x.os.Close()
|
return x.os.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *machoExe) ArchName() string {
|
||||||
|
return cleanMachoArch(x.f.Cpu)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanMachoArch(cpu macho.Cpu) string {
|
||||||
|
return strings.TrimPrefix(strings.ToLower(cpu.String()), "cpu")
|
||||||
|
}
|
||||||
|
|
||||||
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
|
func (x *machoExe) ReadData(addr, size uint64) ([]byte, error) {
|
||||||
for _, load := range x.f.Loads {
|
for _, load := range x.f.Loads {
|
||||||
seg, ok := load.(*macho.Segment)
|
seg, ok := load.(*macho.Segment)
|
||||||
|
|||||||
43
syft/pkg/cataloger/golang/exe_test.go
Normal file
43
syft/pkg/cataloger/golang/exe_test.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package golang
|
||||||
|
|
||||||
|
import (
|
||||||
|
"debug/elf"
|
||||||
|
"debug/macho"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_cleanElfArch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
machine elf.Machine
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
machine: elf.EM_X86_64,
|
||||||
|
want: "x86_64",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.machine.String(), func(t *testing.T) {
|
||||||
|
assert.Equalf(t, test.want, cleanElfArch(test.machine), "cleanElfArch(%v)", test.machine)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_cleanMachoArch(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
cpu macho.Cpu
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cpu: macho.CpuAmd64,
|
||||||
|
want: "amd64",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.cpu.String(), func(t *testing.T) {
|
||||||
|
assert.Equalf(t, test.want, cleanMachoArch(test.cpu), "cleanMachoArch(%v)", test.cpu)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,17 +16,20 @@ const (
|
|||||||
|
|
||||||
func parseGoBin(location source.Location, reader io.ReadCloser) ([]pkg.Package, error) {
|
func parseGoBin(location source.Location, reader io.ReadCloser) ([]pkg.Package, error) {
|
||||||
// Identify if bin was compiled by go
|
// Identify if bin was compiled by go
|
||||||
x, err := openExe(reader)
|
exes, err := openExe(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pkgs []pkg.Package
|
||||||
|
for _, x := range exes {
|
||||||
goVersion, mod := findVers(x)
|
goVersion, mod := findVers(x)
|
||||||
|
pkgs = append(pkgs, buildGoPkgInfo(location, mod, goVersion, x.ArchName())...)
|
||||||
return buildGoPkgInfo(location, mod, goVersion), nil
|
}
|
||||||
|
return pkgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Package {
|
func buildGoPkgInfo(location source.Location, mod, goVersion, arch string) []pkg.Package {
|
||||||
pkgsSlice := make([]pkg.Package, 0)
|
pkgsSlice := make([]pkg.Package, 0)
|
||||||
scanner := bufio.NewScanner(strings.NewReader(mod))
|
scanner := bufio.NewScanner(strings.NewReader(mod))
|
||||||
|
|
||||||
@ -52,6 +55,7 @@ func buildGoPkgInfo(location source.Location, mod, goVersion string) []pkg.Packa
|
|||||||
Metadata: pkg.GolangBinMetadata{
|
Metadata: pkg.GolangBinMetadata{
|
||||||
GoCompiledVersion: goVersion,
|
GoCompiledVersion: goVersion,
|
||||||
H1Digest: fields[3],
|
H1Digest: fields[3],
|
||||||
|
Architecture: arch,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,11 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const goCompiledVersion = "1.17"
|
|
||||||
|
|
||||||
func TestBuildGoPkgInfo(t *testing.T) {
|
func TestBuildGoPkgInfo(t *testing.T) {
|
||||||
|
const (
|
||||||
|
goCompiledVersion = "1.17"
|
||||||
|
archDetails = "amd64"
|
||||||
|
)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
mod string
|
mod string
|
||||||
@ -43,6 +45,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
MetadataType: pkg.GolangBinMetadataType,
|
MetadataType: pkg.GolangBinMetadataType,
|
||||||
Metadata: pkg.GolangBinMetadata{
|
Metadata: pkg.GolangBinMetadata{
|
||||||
GoCompiledVersion: goCompiledVersion,
|
GoCompiledVersion: goCompiledVersion,
|
||||||
|
Architecture: archDetails,
|
||||||
H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
|
H1Digest: "h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -62,6 +65,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
MetadataType: pkg.GolangBinMetadataType,
|
MetadataType: pkg.GolangBinMetadataType,
|
||||||
Metadata: pkg.GolangBinMetadata{
|
Metadata: pkg.GolangBinMetadata{
|
||||||
GoCompiledVersion: goCompiledVersion,
|
GoCompiledVersion: goCompiledVersion,
|
||||||
|
Architecture: archDetails,
|
||||||
H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
|
H1Digest: "h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -92,6 +96,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
MetadataType: pkg.GolangBinMetadataType,
|
MetadataType: pkg.GolangBinMetadataType,
|
||||||
Metadata: pkg.GolangBinMetadata{
|
Metadata: pkg.GolangBinMetadata{
|
||||||
GoCompiledVersion: goCompiledVersion,
|
GoCompiledVersion: goCompiledVersion,
|
||||||
|
Architecture: archDetails,
|
||||||
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
|
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -111,6 +116,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
MetadataType: pkg.GolangBinMetadataType,
|
MetadataType: pkg.GolangBinMetadataType,
|
||||||
Metadata: pkg.GolangBinMetadata{
|
Metadata: pkg.GolangBinMetadata{
|
||||||
GoCompiledVersion: goCompiledVersion,
|
GoCompiledVersion: goCompiledVersion,
|
||||||
|
Architecture: archDetails,
|
||||||
H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=",
|
H1Digest: "h1:PjhxBct4MZii8FFR8+oeS7QOvxKOTZXgk63EU2XpfJE=",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -130,6 +136,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
MetadataType: pkg.GolangBinMetadataType,
|
MetadataType: pkg.GolangBinMetadataType,
|
||||||
Metadata: pkg.GolangBinMetadata{
|
Metadata: pkg.GolangBinMetadata{
|
||||||
GoCompiledVersion: goCompiledVersion,
|
GoCompiledVersion: goCompiledVersion,
|
||||||
|
Architecture: archDetails,
|
||||||
H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=",
|
H1Digest: "h1:Ihq/mm/suC88gF8WFcVwk+OV6Tq+wyA1O0E5UEvDglI=",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -146,7 +153,7 @@ func TestBuildGoPkgInfo(t *testing.T) {
|
|||||||
FileSystemID: "layer-id",
|
FileSystemID: "layer-id",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pkgs := buildGoPkgInfo(location, tt.mod, goCompiledVersion)
|
pkgs := buildGoPkgInfo(location, tt.mod, goCompiledVersion, archDetails)
|
||||||
assert.Equal(t, tt.expected, pkgs)
|
assert.Equal(t, tt.expected, pkgs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package pkg
|
|||||||
|
|
||||||
// GolangBinMetadata represents all captured data for a Golang Binary
|
// GolangBinMetadata represents all captured data for a Golang Binary
|
||||||
type GolangBinMetadata struct {
|
type GolangBinMetadata struct {
|
||||||
GoCompiledVersion string
|
GoCompiledVersion string `json:"goCompiledVersion"`
|
||||||
H1Digest string
|
Architecture string `json:"architecture"`
|
||||||
|
H1Digest string `json:"h1Digest"`
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user