mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
Show binary exports, entrypoint, and imports (#2626)
show binary exports, entrypoint, and imports for macho, elf, and pe formats Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
2e2a9377ea
commit
47fc909700
34 changed files with 2959 additions and 48 deletions
10
.github/workflows/validations.yaml
vendored
10
.github/workflows/validations.yaml
vendored
|
@ -39,8 +39,14 @@ jobs:
|
|||
- name: Restore file executable test-fixture cache
|
||||
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 #v4.0.1
|
||||
with:
|
||||
path: syft/file/cataloger/executable/test-fixtures/bin
|
||||
key: ${{ runner.os }}-unit-file-executable-cache-${{ hashFiles( 'syft/file/cataloger/executable/test-fixtures/cache.fingerprint' ) }}
|
||||
path: syft/file/cataloger/executable/test-fixtures/elf/bin
|
||||
key: ${{ runner.os }}-unit-file-executable-elf-cache-${{ hashFiles( 'syft/file/cataloger/executable/test-fixtures/elf/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore file executable shared-info test-fixture cache
|
||||
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 #v4.0.1
|
||||
with:
|
||||
path: syft/file/cataloger/executable/test-fixtures/shared-info/bin
|
||||
key: ${{ runner.os }}-unit-file-executable-shared-info-cache-${{ hashFiles( 'syft/file/cataloger/executable/test-fixtures/shared-info/cache.fingerprint' ) }}
|
||||
|
||||
- name: Restore Java test-fixture cache
|
||||
uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 #v4.0.1
|
||||
|
|
|
@ -266,7 +266,8 @@ tasks:
|
|||
desc: Generate test fixture fingerprints
|
||||
generates:
|
||||
- cmd/syft/internal/test/integration/test-fixtures/cache.fingerprint
|
||||
- syft/file/cataloger/executable/test-fixtures/cache.fingerprint
|
||||
- syft/file/cataloger/executable/test-fixtures/elf/cache.fingerprint
|
||||
- syft/file/cataloger/executable/test-fixtures/shared-info/cache.fingerprint
|
||||
- syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint
|
||||
- syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint
|
||||
- syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint
|
||||
|
@ -276,7 +277,8 @@ tasks:
|
|||
- test/cli/test-fixtures/cache.fingerprint
|
||||
cmds:
|
||||
# for EXECUTABLE unit test fixtures
|
||||
- "cd syft/file/cataloger/executable/test-fixtures && make cache.fingerprint"
|
||||
- "cd syft/file/cataloger/executable/test-fixtures/elf && make cache.fingerprint"
|
||||
- "cd syft/file/cataloger/executable/test-fixtures/shared-info && make cache.fingerprint"
|
||||
# for IMAGE integration test fixtures
|
||||
- "cd cmd/syft/internal/test/integration/test-fixtures && make cache.fingerprint"
|
||||
# for BINARY unit test fixtures
|
||||
|
@ -297,7 +299,8 @@ tasks:
|
|||
fixtures:
|
||||
desc: Generate test fixtures
|
||||
cmds:
|
||||
- "cd syft/file/cataloger/executable/test-fixtures && make"
|
||||
- "cd syft/file/cataloger/executable/test-fixtures/elf && make"
|
||||
- "cd syft/file/cataloger/executable/test-fixtures/shared-info && make"
|
||||
- "cd syft/pkg/cataloger/java/test-fixtures/java-builds && make"
|
||||
- "cd syft/pkg/cataloger/redhat/test-fixtures && make"
|
||||
- "cd syft/pkg/cataloger/binary/test-fixtures && make"
|
||||
|
|
|
@ -3,5 +3,5 @@ package internal
|
|||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.0.4"
|
||||
JSONSchemaVersion = "16.0.5"
|
||||
)
|
||||
|
|
2319
schema/json/schema-16.0.5.json
Normal file
2319
schema/json/schema-16.0.5.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.4/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.5/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
|
@ -681,13 +681,28 @@
|
|||
"format": {
|
||||
"type": "string"
|
||||
},
|
||||
"hasExports": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hasEntrypoint": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"importedLibraries": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"elfSecurityFeatures": {
|
||||
"$ref": "#/$defs/ELFSecurityFeatures"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"format"
|
||||
"format",
|
||||
"hasExports",
|
||||
"hasEntrypoint",
|
||||
"importedLibraries"
|
||||
]
|
||||
},
|
||||
"File": {
|
||||
|
|
|
@ -155,13 +155,25 @@ func processExecutable(loc file.Location, reader unionreader.UnionReader) (*file
|
|||
|
||||
data.Format = format
|
||||
|
||||
securityFeatures, err := findSecurityFeatures(format, reader)
|
||||
if err != nil {
|
||||
log.WithFields("error", err).Tracef("unable to determine security features for %q", loc.RealPath)
|
||||
return nil, nil
|
||||
switch format {
|
||||
case file.ELF:
|
||||
if err := findELFFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err).Tracef("unable to determine ELF features for %q", loc.RealPath)
|
||||
}
|
||||
case file.PE:
|
||||
if err := findPEFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err).Tracef("unable to determine PE features for %q", loc.RealPath)
|
||||
}
|
||||
case file.MachO:
|
||||
if err := findMachoFeatures(&data, reader); err != nil {
|
||||
log.WithFields("error", err).Tracef("unable to determine Macho features for %q", loc.RealPath)
|
||||
}
|
||||
}
|
||||
|
||||
data.SecurityFeatures = securityFeatures
|
||||
// always allocate collections for presentation
|
||||
if data.ImportedLibraries == nil {
|
||||
data.ImportedLibraries = []string{}
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
@ -230,18 +242,3 @@ func isPE(by []byte) bool {
|
|||
func isELF(by []byte) bool {
|
||||
return bytes.HasPrefix(by, []byte(elf.ELFMAG))
|
||||
}
|
||||
|
||||
func findSecurityFeatures(format file.ExecutableFormat, reader unionreader.UnionReader) (*file.ELFSecurityFeatures, error) {
|
||||
// TODO: add support for PE and MachO
|
||||
switch format { //nolint: gocritic
|
||||
case file.ELF:
|
||||
return findELFSecurityFeatures(reader) //nolint: gocritic
|
||||
case file.PE:
|
||||
// return findPESecurityFeatures(reader)
|
||||
return nil, nil
|
||||
case file.MachO:
|
||||
// return findMachOSecurityFeatures(reader)
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported executable format: %q", format)
|
||||
}
|
||||
|
|
|
@ -12,13 +12,33 @@ import (
|
|||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func findELFSecurityFeatures(reader unionreader.UnionReader) (*file.ELFSecurityFeatures, error) {
|
||||
func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
f, err := elf.NewFile(reader)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return err
|
||||
}
|
||||
|
||||
features := file.ELFSecurityFeatures{
|
||||
libs, err := f.ImportedLibraries()
|
||||
if err != nil {
|
||||
// TODO: known-unknowns
|
||||
log.WithFields("error", err).Trace("unable to read imported libraries from elf file")
|
||||
libs = nil
|
||||
}
|
||||
|
||||
if libs == nil {
|
||||
libs = []string{}
|
||||
}
|
||||
|
||||
data.ImportedLibraries = libs
|
||||
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
||||
data.HasEntrypoint = elfHasEntrypoint(f)
|
||||
data.HasExports = elfHasExports(f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
||||
return &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: isElfSymbolTableStripped(f),
|
||||
StackCanary: checkElfStackCanary(f),
|
||||
NoExecutable: checkElfNXProtection(f),
|
||||
|
@ -29,8 +49,6 @@ func findELFSecurityFeatures(reader unionreader.UnionReader) (*file.ELFSecurityF
|
|||
LlvmControlFlowIntegrity: checkLLVMControlFlowIntegrity(f),
|
||||
ClangFortifySource: checkClangFortifySource(f),
|
||||
}
|
||||
|
||||
return &features, nil
|
||||
}
|
||||
|
||||
func isElfSymbolTableStripped(file *elf.File) bool {
|
||||
|
@ -219,3 +237,34 @@ func checkClangFortifySource(file *elf.File) *bool {
|
|||
}
|
||||
return boolRef(false)
|
||||
}
|
||||
|
||||
func elfHasEntrypoint(f *elf.File) bool {
|
||||
// this is akin to
|
||||
// readelf -h ./path/to/bin | grep "Entry point address"
|
||||
return f.Entry > 0
|
||||
}
|
||||
|
||||
func elfHasExports(f *elf.File) bool {
|
||||
// this is akin to:
|
||||
// nm -D --defined-only ./path/to/bin | grep ' T \| W \| B '
|
||||
// where:
|
||||
// T - symbol in the text section
|
||||
// W - weak symbol that might be overwritten
|
||||
// B - variable located in the uninitialized data section
|
||||
// really anything that is not marked with 'U' (undefined) is considered an export.
|
||||
symbols, err := f.DynamicSymbols()
|
||||
if err != nil {
|
||||
// TODO: known-unknowns?
|
||||
return false
|
||||
}
|
||||
|
||||
for _, s := range symbols {
|
||||
// check if the section is SHN_UNDEF, which is the "U" output type from "nm -D" meaning that the symbol
|
||||
// is undefined, meaning it is not an export. Any entry that is not undefined is considered an export.
|
||||
if s.Section != elf.SHN_UNDEF {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package executable
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
@ -16,7 +18,7 @@ func Test_findELFSecurityFeatures(t *testing.T) {
|
|||
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures", fixture))
|
||||
f, err := os.Open(filepath.Join("test-fixtures/elf", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
@ -26,7 +28,6 @@ func Test_findELFSecurityFeatures(t *testing.T) {
|
|||
fixture string
|
||||
want *file.ELFSecurityFeatures
|
||||
wantStripped bool
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "detect canary",
|
||||
|
@ -145,14 +146,10 @@ func Test_findELFSecurityFeatures(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.wantErr == nil {
|
||||
tt.wantErr = require.NoError
|
||||
}
|
||||
got, err := findELFSecurityFeatures(readerForFixture(t, tt.fixture))
|
||||
tt.wantErr(t, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f, err := elf.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
|
||||
got := findELFSecurityFeatures(f)
|
||||
|
||||
if d := cmp.Diff(tt.want, got); d != "" {
|
||||
t.Errorf("findELFSecurityFeatures() mismatch (-want +got):\n%s", d)
|
||||
|
@ -160,3 +157,70 @@ func Test_findELFSecurityFeatures(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfHasEntrypoint(t *testing.T) {
|
||||
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shared lib",
|
||||
fixture: "bin/libhello.so",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "application",
|
||||
fixture: "bin/hello_linux",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := elf.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, elfHasEntrypoint(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_elfHasExports(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shared lib",
|
||||
fixture: "bin/libhello.so",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "application",
|
||||
fixture: "bin/hello_linux",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := elf.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, elfHasExports(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
92
syft/file/cataloger/executable/macho.go
Normal file
92
syft/file/cataloger/executable/macho.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
package executable
|
||||
|
||||
import (
|
||||
"debug/macho"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
// source http://www.cilinder.be/docs/next/NeXTStep/3.3/nd/DevTools/14_MachO/MachO.htmld/index.html
|
||||
const (
|
||||
machoNPExt uint8 = 0x10 /* N_PEXT: private external symbol bit */
|
||||
machoNExt uint8 = 0x01 /* N_EXT: external symbol bit, set for external symbols */
|
||||
// > #define LC_REQ_DYLD 0x80000000
|
||||
// > #define LC_MAIN (0x28|LC_REQ_DYLD) /* replacement for LC_UNIXTHREAD */
|
||||
lcMain = 0x28 | 0x80000000
|
||||
)
|
||||
|
||||
func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
// TODO: support security features
|
||||
|
||||
// TODO: support multi-architecture binaries
|
||||
f, err := macho.NewFile(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
libs, err := f.ImportedLibraries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data.ImportedLibraries = libs
|
||||
data.HasEntrypoint = machoHasEntrypoint(f)
|
||||
data.HasExports = machoHasExports(f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func machoHasEntrypoint(f *macho.File) bool {
|
||||
// derived from struct entry_point_command found from which explicitly calls out LC_MAIN:
|
||||
// https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h
|
||||
// we need to look for both LC_MAIN and LC_UNIXTHREAD commands to determine if the file is an executable
|
||||
//
|
||||
// this is akin to:
|
||||
// otool -l ./path/to/bin | grep -A4 LC_MAIN
|
||||
// otool -l ./path/to/bin | grep -A4 LC_UNIXTHREAD
|
||||
for _, l := range f.Loads {
|
||||
data := l.Raw()
|
||||
cmd := f.ByteOrder.Uint32(data)
|
||||
|
||||
if macho.LoadCmd(cmd) == macho.LoadCmdUnixThread || macho.LoadCmd(cmd) == lcMain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func machoHasExports(f *macho.File) bool {
|
||||
for _, sym := range f.Symtab.Syms {
|
||||
// look for symbols that are:
|
||||
// - not private and are external
|
||||
// - do not have an N_TYPE value of N_UNDF (undefined symbol)
|
||||
//
|
||||
// here's the bit layout for the n_type field:
|
||||
// 0000 0000
|
||||
// ─┬─│ ─┬─│
|
||||
// │ │ │ └─ N_EXT (external symbol)
|
||||
// │ │ └─ N_TYPE (N_UNDF, N_ABS, N_SECT, N_PBUD, N_INDR)
|
||||
// │ └─ N_PEXT (private external symbol)
|
||||
// └─ N_STAB (debugging symbol)
|
||||
//
|
||||
isExternal := sym.Type&machoNExt == machoNExt
|
||||
isPrivate := sym.Type&machoNPExt == machoNPExt
|
||||
nTypeIsUndefined := sym.Type&0x0e == 0
|
||||
|
||||
if isExternal && !isPrivate {
|
||||
if sym.Name == "_main" || sym.Name == "__mh_execute_header" {
|
||||
// ...however there are some symbols that are not exported but are still important
|
||||
// for debugging or as an entrypoint, so we need to explicitly check for them
|
||||
continue
|
||||
}
|
||||
if nTypeIsUndefined {
|
||||
continue
|
||||
}
|
||||
// we have a symbol that is not private and is external
|
||||
// and is not undefined, so it is an export
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
80
syft/file/cataloger/executable/macho_test.go
Normal file
80
syft/file/cataloger/executable/macho_test.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package executable
|
||||
|
||||
import (
|
||||
"debug/macho"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func Test_machoHasEntrypoint(t *testing.T) {
|
||||
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shared lib",
|
||||
fixture: "bin/libhello.dylib",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "application",
|
||||
fixture: "bin/hello_mac",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := macho.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, machoHasEntrypoint(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_machoHasExports(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shared lib",
|
||||
fixture: "bin/libhello.dylib",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "application",
|
||||
fixture: "bin/hello_mac",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := macho.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, machoHasExports(f))
|
||||
})
|
||||
}
|
||||
}
|
84
syft/file/cataloger/executable/pe.go
Normal file
84
syft/file/cataloger/executable/pe.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package executable
|
||||
|
||||
import (
|
||||
"debug/pe"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func findPEFeatures(data *file.Executable, reader unionreader.UnionReader) error {
|
||||
// TODO: support security features
|
||||
|
||||
f, err := pe.NewFile(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
libs, err := f.ImportedLibraries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data.ImportedLibraries = libs
|
||||
data.HasEntrypoint = peHasEntrypoint(f)
|
||||
data.HasExports = peHasExports(f)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
windowsExeEntrypoints = strset.New("main", "WinMain", "wWinMain")
|
||||
windowsDllEntrypoints = strset.New("DllMain", "_DllMainCRTStartup@12", "CRT_INIT")
|
||||
)
|
||||
|
||||
func peHasEntrypoint(f *pe.File) bool {
|
||||
// DLLs can have entrypoints, but they are not "executables" in the traditional sense,
|
||||
// but instead point to an initialization function (DLLMain).
|
||||
// The PE format does not require an entrypoint, so it is possible to not have one, however,
|
||||
// the microsoft C runtime does: https://learn.microsoft.com/en-US/troubleshoot/developer/visualstudio/cpp/libraries/use-c-run-time
|
||||
//
|
||||
// > When building a DLL which uses any of the C Run-time libraries, in order to ensure that the CRT is properly initialized, either
|
||||
// > 1. the initialization function must be named DllMain() and the entry point must be specified with the linker option -entry:_DllMainCRTStartup@12 - or -
|
||||
// > 2. the DLL's entry point must explicitly call CRT_INIT() on process attach and process detach
|
||||
//
|
||||
// This isn't really helpful from a user perspective when it comes to indicating if there is an entrypoint or not
|
||||
// since it will always effectively be true for DLLs! All DLLs and Executables (aka "modules") have a single
|
||||
// entrypoint, _GetPEImageBase, but we're more interested in the logical idea of an entrypoint.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/psapi/module-information for more details.
|
||||
|
||||
var hasLibEntrypoint, hasExeEntrypoint bool
|
||||
for _, s := range f.Symbols {
|
||||
if windowsExeEntrypoints.Has(s.Name) {
|
||||
hasExeEntrypoint = true
|
||||
}
|
||||
if windowsDllEntrypoints.Has(s.Name) {
|
||||
hasLibEntrypoint = true
|
||||
}
|
||||
}
|
||||
|
||||
switch v := f.OptionalHeader.(type) {
|
||||
case *pe.OptionalHeader32:
|
||||
return v.AddressOfEntryPoint > 0 && !hasLibEntrypoint && hasExeEntrypoint
|
||||
case *pe.OptionalHeader64:
|
||||
return v.AddressOfEntryPoint > 0 && !hasLibEntrypoint && hasExeEntrypoint
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func peHasExports(f *pe.File) bool {
|
||||
if f.OptionalHeader == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch v := f.OptionalHeader.(type) {
|
||||
case *pe.OptionalHeader32:
|
||||
return v.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].Size > 0
|
||||
case *pe.OptionalHeader64:
|
||||
return v.DataDirectory[pe.IMAGE_DIRECTORY_ENTRY_EXPORT].Size > 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
80
syft/file/cataloger/executable/pe_test.go
Normal file
80
syft/file/cataloger/executable/pe_test.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package executable
|
||||
|
||||
import (
|
||||
"debug/pe"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func Test_peHasEntrypoint(t *testing.T) {
|
||||
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shared lib",
|
||||
fixture: "bin/hello.dll", // though this is a shared lib, it has an entrypoint (DLLMain)
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "application",
|
||||
fixture: "bin/hello.exe",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := pe.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, peHasEntrypoint(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_peHasExports(t *testing.T) {
|
||||
readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader {
|
||||
t.Helper()
|
||||
f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture))
|
||||
require.NoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fixture string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "shared lib",
|
||||
fixture: "bin/hello.dll",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "application",
|
||||
fixture: "bin/hello.exe",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := pe.NewFile(readerForFixture(t, tt.fixture))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, peHasExports(f))
|
||||
})
|
||||
}
|
||||
}
|
3
syft/file/cataloger/executable/test-fixtures/elf/.gitignore
vendored
Normal file
3
syft/file/cataloger/executable/test-fixtures/elf/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
../bin
|
||||
actual_verify
|
||||
Dockerfile.sha256
|
1
syft/file/cataloger/executable/test-fixtures/shared-info/.gitignore
vendored
Normal file
1
syft/file/cataloger/executable/test-fixtures/shared-info/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
bin
|
|
@ -0,0 +1,16 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG OSXCROSS_VERSION=13.1
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
FROM --platform=$BUILDPLATFORM crazymax/osxcross:${OSXCROSS_VERSION}-ubuntu AS osxcross
|
||||
|
||||
FROM --platform=$BUILDPLATFORM ubuntu:22.04
|
||||
|
||||
RUN apt update -y && apt install -y \
|
||||
curl wget \
|
||||
make gcc clang \
|
||||
git mingw-w64
|
||||
|
||||
COPY --from=osxcross /osxcross /osxcross
|
||||
ENV PATH="/osxcross/bin:$PATH"
|
||||
ENV LD_LIBRARY_PATH="/osxcross/lib:$LD_LIBRARY_PATH"
|
|
@ -0,0 +1,25 @@
|
|||
BIN=./bin
|
||||
TOOL_IMAGE=localhost/syft-shared-info-build-tools:latest
|
||||
VERIFY_FILE=actual_verify
|
||||
|
||||
all: build
|
||||
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 -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
|
||||
|
||||
cache.fingerprint:
|
||||
@find project Dockerfile Makefile -type f -exec md5sum {} + | awk '{print $1}' | sort | tee cache.fingerprint
|
||||
|
||||
clean:
|
||||
rm -f $(BIN)/*
|
||||
|
||||
.PHONY: build verify debug build-image build-bins clean dockerfile-check cache.fingerprint
|
|
@ -0,0 +1,10 @@
|
|||
# invoke all make files in subdirectories
|
||||
.PHONY: all hello libhello
|
||||
|
||||
all: hello libhello
|
||||
|
||||
hello:
|
||||
$(MAKE) -C hello
|
||||
|
||||
libhello:
|
||||
$(MAKE) -C libhello
|
|
@ -0,0 +1,23 @@
|
|||
.PHONY: all linux windows mac
|
||||
|
||||
BIN=../../bin
|
||||
|
||||
all: $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac
|
||||
|
||||
linux: $(BIN)/libhello.so
|
||||
|
||||
windows: $(BIN)/libhello.dll
|
||||
|
||||
mac: $(BIN)/libhello.dylib
|
||||
|
||||
$(BIN)/hello_linux:
|
||||
gcc hello.c -o $(BIN)/hello_linux
|
||||
|
||||
$(BIN)/hello.exe:
|
||||
x86_64-w64-mingw32-gcc hello.c -o $(BIN)/hello.exe
|
||||
|
||||
$(BIN)/hello_mac:
|
||||
o64-clang hello.c -o $(BIN)/hello_mac
|
||||
|
||||
clean:
|
||||
rm -f $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac
|
|
@ -0,0 +1,6 @@
|
|||
#include <stdio.h>
|
||||
|
||||
int main() {
|
||||
printf("Hello, World!\n");
|
||||
return 0;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
.PHONY: all linux windows mac
|
||||
|
||||
BIN=../../bin
|
||||
|
||||
all: $(BIN)/libhello.so $(BIN)/libhello.dll $(BIN)/libhello.dylib
|
||||
|
||||
linux: $(BIN)/libhello.so
|
||||
|
||||
windows: $(BIN)/libhello.dll
|
||||
|
||||
mac: $(BIN)/libhello.dylib
|
||||
|
||||
$(BIN)/libhello.so:
|
||||
gcc -shared -fPIC -o $(BIN)/libhello.so hello.c
|
||||
|
||||
$(BIN)/libhello.dll:
|
||||
x86_64-w64-mingw32-gcc -shared -o $(BIN)/hello.dll hello.c -Wl,--out-implib,$(BIN)/libhello.a
|
||||
|
||||
$(BIN)/libhello.dylib:
|
||||
o64-clang -dynamiclib -o $(BIN)/libhello.dylib hello.c
|
||||
|
||||
clean:
|
||||
rm -f $(BIN)/libhello.so $(BIN)/hello.dll $(BIN)/libhello.dylib $(BIN)/libhello.a
|
|
@ -0,0 +1,5 @@
|
|||
#include <stdio.h>
|
||||
|
||||
void hello() {
|
||||
printf("Hello from shared library!\n");
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
#ifndef HELLO_H
|
||||
#define HELLO_H
|
||||
|
||||
void hello();
|
||||
|
||||
#endif
|
|
@ -19,7 +19,10 @@ type Executable struct {
|
|||
// Format denotes either ELF, Mach-O, or PE
|
||||
Format ExecutableFormat `json:"format" yaml:"format" mapstructure:"format"`
|
||||
|
||||
SecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||
HasExports bool `json:"hasExports" yaml:"hasExports" mapstructure:"hasExports"`
|
||||
HasEntrypoint bool `json:"hasEntrypoint" yaml:"hasEntrypoint" mapstructure:"hasEntrypoint"`
|
||||
ImportedLibraries []string `json:"importedLibraries" yaml:"importedLibraries" mapstructure:"importedLibraries"`
|
||||
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||
}
|
||||
|
||||
type ELFSecurityFeatures struct {
|
||||
|
|
|
@ -224,7 +224,7 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
|||
Executables: map[file.Coordinates]file.Executable{
|
||||
c: {
|
||||
Format: file.ELF,
|
||||
SecurityFeatures: &file.ELFSecurityFeatures{
|
||||
ELFSecurityFeatures: &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: false,
|
||||
StackCanary: boolRef(true),
|
||||
NoExecutable: false,
|
||||
|
|
|
@ -288,7 +288,7 @@ func Test_toSyftFiles(t *testing.T) {
|
|||
},
|
||||
Executable: &file.Executable{
|
||||
Format: file.ELF,
|
||||
SecurityFeatures: &file.ELFSecurityFeatures{
|
||||
ELFSecurityFeatures: &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: false,
|
||||
StackCanary: boolRef(true),
|
||||
NoExecutable: false,
|
||||
|
@ -329,7 +329,7 @@ func Test_toSyftFiles(t *testing.T) {
|
|||
Executables: map[file.Coordinates]file.Executable{
|
||||
coord: {
|
||||
Format: file.ELF,
|
||||
SecurityFeatures: &file.ELFSecurityFeatures{
|
||||
ELFSecurityFeatures: &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: false,
|
||||
StackCanary: boolRef(true),
|
||||
NoExecutable: false,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package dotnet
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_getDepsJSONFilePrefix(t *testing.T) {
|
||||
|
|
Loading…
Reference in a new issue