Detect ELF security features (#2443)

* add detection of ELF security features

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

* fix linting

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

* update json schema with file executable data

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

* update expected fixure when no tty present

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

* more detailed differ

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

* use json differ

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

* fix tests

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

* remove json schema addition

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

* regenerate json schema

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

* fix mimtype set ref

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

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-02-02 11:51:18 -05:00 committed by GitHub
parent 3da679066e
commit 3023a5a7bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 3377 additions and 46 deletions

View File

@ -36,6 +36,12 @@ jobs:
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Restore file executable test-fixture cache
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
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' ) }}
- name: Restore Java test-fixture cache
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2 #v4.0.0
with:

View File

@ -266,6 +266,7 @@ 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/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
@ -274,17 +275,19 @@ tasks:
- test/install/cache.fingerprint
- test/cli/test-fixtures/cache.fingerprint
cmds:
# for EXECUTABLE unit test fixtures
- "cd syft/file/cataloger/executable/test-fixtures && make cache.fingerprint"
# for IMAGE integration test fixtures
- "cd cmd/syft/internal/test/integration/test-fixtures && make cache.fingerprint"
# for BINARY test fixtures
# for BINARY unit test fixtures
- "cd syft/pkg/cataloger/binary/test-fixtures && make cache.fingerprint"
# for JAVA BUILD test fixtures
# for JAVA BUILD unit test fixtures
- "cd syft/pkg/cataloger/java/test-fixtures/java-builds && make cache.fingerprint"
# for GO BINARY test fixtures
# for GO BINARY unit test fixtures
- "cd syft/pkg/cataloger/golang/test-fixtures/archs && make binaries.fingerprint"
# for RPM test fixtures
# for RPM unit test fixtures
- "cd syft/pkg/cataloger/redhat/test-fixtures && make rpms.fingerprint"
# for Kernel test fixtures
# for Kernel unit test fixtures
- "cd syft/pkg/cataloger/kernel/test-fixtures && make cache.fingerprint"
# for INSTALL test fixtures
- "cd test/install && make cache.fingerprint"
@ -294,6 +297,7 @@ tasks:
fixtures:
desc: Generate test fixtures
cmds:
- "cd syft/file/cataloger/executable/test-fixtures && 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"

View File

@ -15,6 +15,7 @@ import (
"github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/cataloging/filecataloging"
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
@ -111,6 +112,10 @@ func (cfg Catalog) ToFilesConfig() filecataloging.Config {
Globs: cfg.File.Content.Globs,
SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize,
},
Executable: executable.Config{
MIMETypes: executable.DefaultConfig().MIMETypes,
Globs: cfg.File.Executable.Globs,
},
}
}

View File

@ -11,8 +11,9 @@ import (
)
type fileConfig struct {
Metadata fileMetadata `yaml:"metadata" json:"metadata" mapstructure:"metadata"`
Content fileContent `yaml:"content" json:"content" mapstructure:"content"`
Metadata fileMetadata `yaml:"metadata" json:"metadata" mapstructure:"metadata"`
Content fileContent `yaml:"content" json:"content" mapstructure:"content"`
Executable fileExecutable `yaml:"executable" json:"executable" mapstructure:"executable"`
}
type fileMetadata struct {
@ -25,6 +26,10 @@ type fileContent struct {
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
type fileExecutable struct {
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
}
func defaultFileConfig() fileConfig {
return fileConfig{
Metadata: fileMetadata{
@ -34,6 +39,9 @@ func defaultFileConfig() fileConfig {
Content: fileContent{
SkipFilesAboveSize: 250 * intFile.KB,
},
Executable: fileExecutable{
Globs: nil,
},
}
}

View File

@ -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.0"
JSONSchemaVersion = "16.0.1"
)

View File

@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/file/cataloger/filedigest"
"github.com/anchore/syft/syft/file/cataloger/filemetadata"
@ -100,6 +101,31 @@ func NewFileContentCatalogerTask(cfg filecontent.Config) Task {
return NewTask("file-content-cataloger", fn)
}
func NewExecutableCatalogerTask(selection file.Selection, cfg executable.Config) Task {
if selection == file.NoFilesSelection {
return nil
}
cat := executable.NewCataloger(cfg)
fn := func(ctx context.Context, resolver file.Resolver, builder sbomsync.Builder) error {
accessor := builder.(sbomsync.Accessor)
result, err := cat.Catalog(resolver)
if err != nil {
return err
}
accessor.WriteToSBOM(func(sbom *sbom.SBOM) {
sbom.Artifacts.Executables = result
})
return nil
}
return NewTask("file-executable-cataloger", fn)
}
// TODO: this should be replaced with a fix that allows passing a coordinate or location iterator to the cataloger
// Today internal to both cataloger this functions differently: a slice of coordinates vs a channel of locations
func coordinatesForSelection(selection file.Selection, accessor sbomsync.Accessor) ([]file.Coordinates, bool) {

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,15 @@ import (
intFile "github.com/anchore/syft/internal/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
)
type Config struct {
Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"`
Hashers []crypto.Hash `yaml:"hashers" json:"hashers" mapstructure:"hashers"`
Content filecontent.Config `yaml:"content" json:"content" mapstructure:"content"`
Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"`
Hashers []crypto.Hash `yaml:"hashers" json:"hashers" mapstructure:"hashers"`
Content filecontent.Config `yaml:"content" json:"content" mapstructure:"content"`
Executable executable.Config `yaml:"executable" json:"executable" mapstructure:"executable"`
}
type configMarshaledForm struct {
@ -30,9 +32,10 @@ func DefaultConfig() Config {
log.WithFields("error", err).Warn("unable to create file hashers")
}
return Config{
Selection: file.FilesOwnedByPackageSelection,
Hashers: hashers,
Content: filecontent.DefaultConfig(),
Selection: file.FilesOwnedByPackageSelection,
Hashers: hashers,
Content: filecontent.DefaultConfig(),
Executable: executable.DefaultConfig(),
}
}

View File

@ -185,6 +185,9 @@ func (c *CreateSBOMConfig) fileTasks() []task.Task {
if t := task.NewFileContentCatalogerTask(c.Files.Content); t != nil {
tsks = append(tsks, t)
}
if t := task.NewExecutableCatalogerTask(c.Files.Selection, c.Files.Executable); t != nil {
tsks = append(tsks, t)
}
return tsks
}

View File

@ -88,7 +88,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -106,7 +106,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "directory"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -125,7 +125,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "directory"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -143,7 +143,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames(false, true), // note: the digest cataloger is not included
fileCatalogerNames(false, true, true), // note: the digest cataloger is not included
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -181,7 +181,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
// note: there is a single group of catalogers for pkgs and files
append(
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames(true, true)...,
fileCatalogerNames(true, true, true)...,
),
relationshipCatalogerNames(),
},
@ -202,7 +202,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "persistent"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -222,7 +222,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgCatalogerNamesWithTagOrName(t, "directory"), "persistent"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -242,7 +242,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgIntersect("image", "javascript"), "persistent"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -263,7 +263,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
addTo(pkgCatalogerNamesWithTagOrName(t, "image"), "user-provided"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -283,7 +283,7 @@ func TestCreateSBOMConfig_makeTaskGroups(t *testing.T) {
wantTaskNames: [][]string{
environmentCatalogerNames(),
pkgCatalogerNamesWithTagOrName(t, "image"),
fileCatalogerNames(true, true),
fileCatalogerNames(true, true, true),
relationshipCatalogerNames(),
},
wantManifest: &catalogerManifest{
@ -367,11 +367,14 @@ func pkgCatalogerNamesWithTagOrName(t *testing.T, token string) []string {
return names
}
func fileCatalogerNames(digest, metadata bool) []string {
func fileCatalogerNames(digest, metadata, executable bool) []string {
var names []string
if digest {
names = append(names, "file-digest-cataloger")
}
if executable {
names = append(names, "file-executable-cataloger")
}
if metadata {
names = append(names, "file-metadata-cataloger")
}

View File

@ -0,0 +1,234 @@
package executable
import (
"bytes"
"debug/elf"
"debug/macho"
"encoding/binary"
"fmt"
"github.com/bmatcuk/doublestar/v4"
"github.com/dustin/go-humanize"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
type Config struct {
MIMETypes []string `json:"mime-types" yaml:"mime-types" mapstructure:"mime-types"`
Globs []string `json:"globs" yaml:"globs" mapstructure:"globs"`
}
type Cataloger struct {
config Config
}
func DefaultConfig() Config {
return Config{
MIMETypes: mimetype.ExecutableMIMETypeSet.List(),
Globs: nil,
}
}
func NewCataloger(cfg Config) *Cataloger {
return &Cataloger{
config: cfg,
}
}
func (i *Cataloger) Catalog(resolver file.Resolver) (map[file.Coordinates]file.Executable, error) {
locs, err := resolver.FilesByMIMEType(i.config.MIMETypes...)
if err != nil {
return nil, fmt.Errorf("unable to get file locations for binaries: %w", err)
}
locs, err = filterByGlobs(locs, i.config.Globs)
if err != nil {
return nil, err
}
prog := catalogingProgress(int64(len(locs)))
results := make(map[file.Coordinates]file.Executable)
for _, loc := range locs {
prog.AtomicStage.Set(loc.Path())
reader, err := resolver.FileContentsByLocation(loc)
if err != nil {
// TODO: known-unknowns
log.WithFields("error", err).Warnf("unable to get file contents for %q", loc.RealPath)
continue
}
exec, err := processExecutable(loc, reader.(unionreader.UnionReader))
if err != nil {
log.WithFields("error", err).Warnf("unable to process executable %q", loc.RealPath)
}
if exec != nil {
prog.Increment()
results[loc.Coordinates] = *exec
}
}
log.Debugf("executable cataloger processed %d files", len(results))
prog.AtomicStage.Set(fmt.Sprintf("%s executables", humanize.Comma(prog.Current())))
prog.SetCompleted()
return results, nil
}
func catalogingProgress(locations int64) *monitor.CatalogerTaskProgress {
info := monitor.GenericTask{
Title: monitor.Title{
Default: "Executables",
},
ParentID: monitor.TopLevelCatalogingTaskID,
}
return bus.StartCatalogerTask(info, locations, "")
}
func filterByGlobs(locs []file.Location, globs []string) ([]file.Location, error) {
if len(globs) == 0 {
return locs, nil
}
var filteredLocs []file.Location
for _, loc := range locs {
matches, err := locationMatchesGlob(loc, globs)
if err != nil {
return nil, err
}
if matches {
filteredLocs = append(filteredLocs, loc)
}
}
return filteredLocs, nil
}
func locationMatchesGlob(loc file.Location, globs []string) (bool, error) {
for _, glob := range globs {
for _, path := range []string{loc.RealPath, loc.AccessPath} {
if path == "" {
continue
}
matches, err := doublestar.Match(glob, path)
if err != nil {
return false, fmt.Errorf("unable to match glob %q to path %q: %w", glob, path, err)
}
if matches {
return true, 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 {
return nil, fmt.Errorf("unable to determine executable kind: %w", err)
}
if format == "" {
log.Debugf("unable to determine executable format for %q", loc.RealPath)
return nil, nil
}
data.Format = format
securityFeatures, err := findSecurityFeatures(format, reader)
if err != nil {
log.WithFields("error", err).Warnf("unable to determine security features for %q", loc.RealPath)
return nil, nil
}
data.SecurityFeatures = securityFeatures
return &data, nil
}
func findExecutableFormat(reader unionreader.UnionReader) (file.ExecutableFormat, error) {
// read the first sector of the file
buf := make([]byte, 512)
n, err := reader.ReadAt(buf, 0)
if err != nil {
return "", fmt.Errorf("unable to read first sector of file: %w", err)
}
if n < 512 {
return "", fmt.Errorf("unable to read enough bytes to determine executable format")
}
switch {
case isMacho(buf):
return file.MachO, nil
case isPE(buf):
return file.PE, nil
case isELF(buf):
return file.ELF, nil
}
return "", nil
}
func isMacho(by []byte) bool {
// sourced from https://github.com/gabriel-vasile/mimetype/blob/02af149c0dfd1444d9256fc33c2012bb3153e1d2/internal/magic/binary.go#L44
if classOrMachOFat(by) && by[7] < 20 {
return true
}
if len(by) < 4 {
return false
}
be := binary.BigEndian.Uint32(by)
le := binary.LittleEndian.Uint32(by)
return be == macho.Magic32 ||
le == macho.Magic32 ||
be == macho.Magic64 ||
le == macho.Magic64
}
// Java bytecode and Mach-O binaries share the same magic number.
// More info here https://github.com/threatstack/libmagic/blob/master/magic/Magdir/cafebabe
func classOrMachOFat(in []byte) bool {
// sourced from https://github.com/gabriel-vasile/mimetype/blob/02af149c0dfd1444d9256fc33c2012bb3153e1d2/internal/magic/binary.go#L44
// There should be at least 8 bytes for both of them because the only way to
// quickly distinguish them is by comparing byte at position 7
if len(in) < 8 {
return false
}
return bytes.HasPrefix(in, []byte{0xCA, 0xFE, 0xBA, 0xBE})
}
func isPE(by []byte) bool {
return bytes.HasPrefix(by, []byte("MZ"))
}
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)
// case file.MachO:
// return findMachOSecurityFeatures(reader)
}
return nil, fmt.Errorf("unsupported executable format: %q", format)
}

View File

@ -0,0 +1,221 @@
package executable
import (
"debug/elf"
"regexp"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
func findELFSecurityFeatures(reader unionreader.UnionReader) (*file.ELFSecurityFeatures, error) {
f, err := elf.NewFile(reader)
if err != nil {
return nil, nil
}
features := file.ELFSecurityFeatures{
SymbolTableStripped: isElfSymbolTableStripped(f),
StackCanary: checkElfStackCanary(f),
NoExecutable: checkElfNXProtection(f),
RelocationReadOnly: checkElfRelROProtection(f),
PositionIndependentExecutable: isELFPIE(f),
DynamicSharedObject: isELFDSO(f),
LlvmSafeStack: checkLLVMSafeStack(f),
LlvmControlFlowIntegrity: checkLLVMControlFlowIntegrity(f),
ClangFortifySource: checkClangFortifySource(f),
}
return &features, nil
}
func isElfSymbolTableStripped(file *elf.File) bool {
return file.Section(".symtab") == nil
}
func checkElfStackCanary(file *elf.File) *bool {
return hasAnyDynamicSymbols(file, "__stack_chk_fail", "__stack_chk_guard")
}
func hasAnyDynamicSymbols(file *elf.File, symbolNames ...string) *bool {
dynSyms, err := file.DynamicSymbols()
if err != nil {
// TODO: known-unknowns
log.WithFields("error", err).Warn("unable to read dynamic symbols from elf file")
return nil
}
nameSet := strset.New(symbolNames...)
for _, sym := range dynSyms {
if nameSet.Has(sym.Name) {
return boolRef(true)
}
}
return boolRef(false)
}
func boolRef(b bool) *bool {
return &b
}
func checkElfNXProtection(file *elf.File) bool {
// find the program headers until you find the GNU_STACK segment
for _, prog := range file.Progs {
if prog.Type == elf.PT_GNU_STACK {
// check if the GNU_STACK segment is executable
return prog.Flags&elf.PF_X == 0
}
}
return false
}
func checkElfRelROProtection(f *elf.File) file.RelocationReadOnly {
// background on relro https://www.redhat.com/en/blog/hardening-elf-binaries-using-relocation-read-only-relro
hasRelro := false
hasBindNow := hasBindNowDynTagOrFlag(f)
for _, prog := range f.Progs {
if prog.Type == elf.PT_GNU_RELRO {
hasRelro = true
break
}
}
switch {
case hasRelro && hasBindNow:
return file.RelocationReadOnlyFull
case hasRelro:
return file.RelocationReadOnlyPartial
default:
return file.RelocationReadOnlyNone
}
}
func hasBindNowDynTagOrFlag(f *elf.File) bool {
if hasElfDynTag(f, elf.DT_BIND_NOW) {
// support older binaries...
return true
}
// "DT_BIND_NOW ... use has been superseded by the DF_BIND_NOW flag"
// source: https://refspecs.linuxbase.org/elf/gabi4+/ch5.dynamic.html
return hasElfDynFlag(f, elf.DF_BIND_NOW)
}
func hasElfDynFlag(f *elf.File, flag elf.DynFlag) bool {
vals, err := f.DynValue(elf.DT_FLAGS)
if err != nil {
// TODO: known-unknowns
log.WithFields("error", err).Warn("unable to read DT_FLAGS from elf file")
return false
}
for _, val := range vals {
if val&uint64(flag) != 0 {
return true
}
}
return false
}
func hasElfDynFlag1(f *elf.File, flag elf.DynFlag1) bool {
vals, err := f.DynValue(elf.DT_FLAGS_1)
if err != nil {
// TODO: known-unknowns
log.WithFields("error", err).Warn("unable to read DT_FLAGS_1 from elf file")
return false
}
for _, val := range vals {
if val&uint64(flag) != 0 {
return true
}
}
return false
}
func hasElfDynTag(f *elf.File, tag elf.DynTag) bool {
// source https://github.com/golang/go/blob/9b4b3e5acca2dabe107fa2c3ed963097d78a4562/src/cmd/cgo/internal/testshared/shared_test.go#L280
ds := f.SectionByType(elf.SHT_DYNAMIC)
if ds == nil {
return false
}
d, err := ds.Data()
if err != nil {
return false
}
for len(d) > 0 {
var t elf.DynTag
switch f.Class {
case elf.ELFCLASS32:
t = elf.DynTag(f.ByteOrder.Uint32(d[0:4]))
d = d[8:]
case elf.ELFCLASS64:
t = elf.DynTag(f.ByteOrder.Uint64(d[0:8]))
d = d[16:]
}
if t == tag {
return true
}
}
return false
}
func isELFPIE(f *elf.File) bool {
// being a shared object is not sufficient to be a PIE, the explicit flag must be set also
return isELFDSO(f) && hasElfDynFlag1(f, elf.DF_1_PIE)
}
func isELFDSO(f *elf.File) bool {
return f.Type == elf.ET_DYN
}
func checkLLVMSafeStack(file *elf.File) *bool {
// looking for the presence of https://github.com/microsoft/compiler-rt/blob/30b3b8cb5c9a0854f2f40f187c6f6773561a35f2/lib/safestack/safestack.cc#L207
return hasAnyDynamicSymbols(file, "__safestack_init")
}
func checkLLVMControlFlowIntegrity(file *elf.File) *bool {
// look for any symbols that are functions and end with ".cfi"
dynSyms, err := file.Symbols()
if err != nil {
// TODO: known-unknowns
log.WithFields("error", err).Trace("unable to read symbols from elf file")
return nil
}
for _, sym := range dynSyms {
if isFunction(sym) && strings.HasSuffix(sym.Name, ".cfi") {
return boolRef(true)
}
}
return boolRef(false)
}
func isFunction(sym elf.Symbol) bool {
return elf.ST_TYPE(sym.Info) == elf.STT_FUNC
}
var fortifyPattern = regexp.MustCompile(`__\w+_chk@.+`)
func checkClangFortifySource(file *elf.File) *bool {
dynSyms, err := file.Symbols()
if err != nil {
// TODO: known-unknowns
log.WithFields("error", err).Trace("unable to read symbols from elf file")
return nil
}
for _, sym := range dynSyms {
if isFunction(sym) && fortifyPattern.MatchString(sym.Name) {
return boolRef(true)
}
}
return boolRef(false)
}

View File

@ -0,0 +1,162 @@
package executable
import (
"os"
"path/filepath"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
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))
require.NoError(t, err)
return f
}
tests := []struct {
name string
fixture string
want *file.ELFSecurityFeatures
wantStripped bool
wantErr require.ErrorAssertionFunc
}{
{
name: "detect canary",
fixture: "bin/with_canary",
want: &file.ELFSecurityFeatures{
StackCanary: boolRef(true), // ! important !
RelocationReadOnly: file.RelocationReadOnlyNone,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect nx",
fixture: "bin/with_nx",
want: &file.ELFSecurityFeatures{
StackCanary: boolRef(false),
NoExecutable: true, // ! important !
RelocationReadOnly: file.RelocationReadOnlyNone,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect relro",
fixture: "bin/with_relro",
want: &file.ELFSecurityFeatures{
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyFull, // ! important !
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect partial relro",
fixture: "bin/with_partial_relro",
want: &file.ELFSecurityFeatures{
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyPartial, // ! important !
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect pie",
fixture: "bin/with_pie",
want: &file.ELFSecurityFeatures{
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyNone,
PositionIndependentExecutable: true, // ! important !
DynamicSharedObject: true, // ! important !
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect dso",
fixture: "bin/pie_false_positive.so",
want: &file.ELFSecurityFeatures{
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyPartial,
NoExecutable: true,
PositionIndependentExecutable: false, // ! important !
DynamicSharedObject: true, // ! important !
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect safestack",
fixture: "bin/with_safestack",
want: &file.ELFSecurityFeatures{
NoExecutable: true,
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyPartial,
PositionIndependentExecutable: false,
DynamicSharedObject: false,
LlvmSafeStack: boolRef(true), // ! important !
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(false),
},
},
{
name: "detect cfi",
fixture: "bin/with_cfi",
want: &file.ELFSecurityFeatures{
NoExecutable: true,
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyPartial,
PositionIndependentExecutable: false,
DynamicSharedObject: false,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(true), // ! important !
ClangFortifySource: boolRef(false),
},
},
{
name: "detect fortify",
fixture: "bin/with_fortify",
want: &file.ELFSecurityFeatures{
NoExecutable: true,
StackCanary: boolRef(false),
RelocationReadOnly: file.RelocationReadOnlyPartial,
PositionIndependentExecutable: false,
DynamicSharedObject: false,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(false),
ClangFortifySource: boolRef(true), // ! important !
},
},
}
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
}
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf("findELFSecurityFeatures() mismatch (-want +got):\n%s", d)
}
})
}
}

View File

@ -0,0 +1,3 @@
bin
actual_verify
Dockerfile.sha256

View File

@ -0,0 +1,16 @@
FROM gcc:9.5.0
RUN apt update -y && apt install -y clang cmake git make m4 pkg-config zlib1g-dev
## from https://github.com/runsafesecurity/selfrando/blob/tb-v0.4.2/docs/linux-build-instructions.md
#RUN git clone https://github.com/runsafesecurity/selfrando.git && \
# export SR_ARCH=`uname -m | sed s/i686/x86/` && \
# cd selfrando && \
# cmake . -DSR_DEBUG_LEVEL=env -DCMAKE_BUILD_TYPE=Release -DSR_BUILD_LIBELF=1 \
# -DSR_ARCH=$SR_ARCH -DSR_LOG=console \
# -DSR_FORCE_INPLACE=1 -G "Unix Makefiles" \
# -DCMAKE_INSTALL_PREFIX:PATH=$PWD/out/$SR_ARCH
#RUN cd selfrando && make -j`nprocs --all`
#RUN cd selfrando && make install
RUN curl -o /bin/checksec https://raw.githubusercontent.com/slimm609/checksec.sh/2.6.0/checksec && chmod +x /bin/checksec

View File

@ -0,0 +1,34 @@
BIN=./bin
TOOL_IMAGE=localhost/syft-bin-build-tools:latest
VERIFY_FILE=actual_verify
all: build verify
tools-check:
@sha256sum -c Dockerfile.sha256 || (echo "Tools Dockerfile has changed" && exit 1)
# for selfrando...
# docker buildx build --platform linux/amd64 -t $(TOOL_IMAGE) .
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 -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make
verify: tools
@rm -f $(VERIFY_FILE)
docker run -i -v $(shell pwd):/mount -w /mount/project $(TOOL_IMAGE) make verify > $(VERIFY_FILE)
@python ./differ expected_verify $(VERIFY_FILE)
debug:
docker run -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

View File

@ -0,0 +1,53 @@
import json
import sys
def load_json(file_path):
with open(file_path, 'r') as file:
return json.load(file)
def diff_json(json1, json2):
differences_found = False
for key in json1:
if key not in json2:
print(f'missing key "{key}"')
continue
differences = []
for subkey in json1[key]:
if subkey not in json2[key]:
differences.append(f' - "{subkey}": expected "{json1[key][subkey]}" but was MISSING')
continue
if subkey in json2[key] and json1[key][subkey] != json2[key][subkey]:
differences.append(f' - "{subkey}": expected "{json1[key][subkey]}" got "{json2[key][subkey]}"')
if differences:
differences_found = True
print(f'{key}')
for diff in differences:
print(diff)
print()
return differences_found
def main():
if len(sys.argv) != 3:
print("Usage: python ./differ file1.json file2.json")
sys.exit(1)
json1 = load_json(sys.argv[1])
json2 = load_json(sys.argv[2])
if diff_json(json1, json2):
print("FAIL: unexpected security feature differences")
sys.exit(1)
else:
print("PASS: all security features accounted for")
sys.exit(0)
main()

View File

@ -0,0 +1 @@
{ "dir": { "name":"../bin" }, "../bin/no_protection": { "relro":"no","canary":"no","nx":"no","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"yes","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/no_protection"}, "../bin/with_nx": { "relro":"no","canary":"no","nx":"yes","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_nx"}, "../bin/with_rpath": { "relro":"no","canary":"no","nx":"no","pie":"no","clangcfi":"no","safestack":"no","rpath":"yes","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_rpath"}, "../bin/with_canary": { "relro":"no","canary":"yes","nx":"no","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_canary"}, "../bin/with_relro": { "relro":"full","canary":"no","nx":"no","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_relro"}, "../bin/with_pie": { "relro":"no","canary":"no","nx":"no","pie":"yes","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_pie"}, "../bin/with_runpath": { "relro":"no","canary":"no","nx":"no","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"yes","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_runpath"}, "../bin/with_safestack": { "relro":"partial","canary":"no","nx":"yes","pie":"no","clangcfi":"no","safestack":"yes","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"3", "filename":"../bin/with_safestack"}, "../bin/pie_false_positive.so": { "relro":"partial","canary":"no","nx":"yes","pie":"dso","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"0", "filename":"../bin/pie_false_positive.so"}, "../bin/with_cfi": { "relro":"partial","canary":"no","nx":"yes","pie":"no","clangcfi":"yes","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_cfi"}, "../bin/with_partial_relro": { "relro":"partial","canary":"no","nx":"no","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/with_partial_relro"}, "../bin/protected": { "relro":"full","canary":"yes","nx":"yes","pie":"yes","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"no","fortified":"0","fortify-able":"2", "filename":"../bin/protected"}, "../bin/with_fortify": { "relro":"partial","canary":"no","nx":"yes","pie":"no","clangcfi":"no","safestack":"no","rpath":"no","runpath":"no","symbols":"yes","fortify_source":"yes","fortified":"1","fortify-able":"2", "filename":"../bin/with_fortify"}}

View File

@ -0,0 +1,93 @@
### GCC Options ############################################
CANARY := -fstack-protector
NO_CANARY := -fno-stack-protector
SHARED_OBJ := -shared
RELRO := -z relro -z now
PARTIAL_RELRO := -z relro
NO_RELRO := -z norelro
NX := -z noexecstack
NO_NX := -z execstack
PIE := -fpic -pie
NO_PIE := -no-pie
# deprecated
RPATH := -Wl,--disable-new-dtags,-rpath,./libs
# replaces RPATH (thus us mutually exclusive with it)
RUNPATH := -Wl,-rpath,./libs
GCCFLAGS := -g
### Clang Options ############################################
SAFE_STACK := -fsanitize=safe-stack
CFI := -flto -fvisibility=hidden -fsanitize=cfi
FORTIFY := -O2 -D_FORTIFY_SOURCE=2
### Common Options ############################################
SRC := main.c
LIB_SRC := lib.c
BIN := ../bin
BINS := $(BIN)/no_protection $(BIN)/with_nx $(BIN)/pie_false_positive.so $(BIN)/with_pie $(BIN)/with_canary $(BIN)/with_relro $(BIN)/with_partial_relro $(BIN)/with_rpath $(BIN)/with_runpath $(BIN)/with_safestack $(BIN)/with_cfi $(BIN)/with_fortify $(BIN)/protected
#.PHONY: verify $(BIN)/no_protection $(BIN)/with_nx $(BIN)/pie_false_positive.so $(BIN)/with_pie $(BIN)/with_canary $(BIN)/with_relro $(BIN)/with_partial_relro $(BIN)/with_rpath $(BIN)/with_runpath $(BIN)/with_safestack $(BIN)/with_cfi $(BIN)/with_fortify $(BIN)/protected
.PHONY: verify clean all
all: $(BINS)
$(BIN)/no_protection : $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) $(RUNPATH)
$(BIN)/with_nx : $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NX) $(NO_RELRO) $(NO_PIE)
$(BIN)/pie_false_positive.so: $(LIB_SRC)
gcc $< -c -Wall -Werror -fpic $(LIB_SRC)
gcc -shared -o $@ lib.o ; rm lib.o
$(BIN)/with_pie: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(PIE)
$(BIN)/with_canary: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE)
$(BIN)/with_relro: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(RELRO) $(NO_PIE)
$(BIN)/with_partial_relro: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(PARTIAL_RELRO) $(NO_PIE)
$(BIN)/with_rpath: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) $(RPATH)
$(BIN)/with_runpath: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE) $(RUNPATH)
$(BIN)/with_safestack: $(SRC)
clang $< -o $@ $(SAFE_STACK)
$(BIN)/with_cfi: $(SRC)
clang $< -o $@ $(CFI)
$(BIN)/with_fortify: $(SRC)
clang $< -o $@ $(FORTIFY)
#$(BIN)/with_selfrando: $(SRC)
# srenv gcc $< -o $@ $(GCCFLAGS) $(NO_CANARY) $(NO_NX) $(NO_RELRO) $(NO_PIE)
$(BIN)/protected: $(SRC)
gcc $< -o $@ $(GCCFLAGS) $(CANARY) $(NX) $(RELRO) $(PIE)
verify:
@/bin/checksec --dir=$(BIN) --extended --output=json
clean:
rm -rf $(BINS)

View File

@ -0,0 +1,6 @@
#include <stdio.h>
void foo(void)
{
puts("Share me!");
}

View File

@ -0,0 +1,6 @@
#ifndef foo_h__
#define foo_h__
extern void foo(void);
#endif // foo_h__

View File

@ -0,0 +1,106 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
// source: https://github.com/trailofbits/clang-cfi-showcase/blob/master/cfi_icall.c
typedef int (*int_arg_fn)(int);
typedef int (*float_arg_fn)(float);
static int int_arg(int arg) {
printf("In %s: (%d)\n", __FUNCTION__, arg);
return 0;
}
static int float_arg(float arg) {
printf("CFI should protect transfer to here\n");
printf("In %s: (%f)\n", __FUNCTION__, (double)arg);
return 0;
}
static int bad_int_arg(int arg) {
printf("CFI will not protect transfer to here\n");
printf("In %s: (%d)\n", __FUNCTION__, arg);
return 0;
}
static int not_entry_point(int arg) {
// nop sled for x86 / x86-64
// these instructions act as a buffer
// for an indirect control flow transfer to skip
// a valid function entry point, but continue
// to execute normal code
__asm__ volatile (
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
"nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n" "nop\n"
);
printf("CFI ensures control flow only transfers to potentially valid destinations\n");
printf("In %s: (%d)\n", __FUNCTION__, arg);
// need to exit or the program will segfault anyway,
// since the indirect call skipped the function preamble
exit(arg);
}
struct foo {
int_arg_fn int_funcs[1];
int_arg_fn bad_int_funcs[1];
float_arg_fn float_funcs[1];
int_arg_fn not_entries[1];
};
// the struct aligns the function pointer arrays
// so indexing past the end will reliably
// call working function pointers
static struct foo f = {
.int_funcs = {int_arg},
.bad_int_funcs = {bad_int_arg},
.float_funcs = {float_arg},
.not_entries = {(int_arg_fn)((uintptr_t)(not_entry_point)+0x20)}
};
void simple1() {
char buf[16];
fgets(buf, sizeof(buf), stdin);
printf(buf);
}
void simple2() {
char buf[16];
scanf("%s", buf);
}
int main(int argc, char **argv) {
if(argc != 2) {
printf("Usage: %s <option>\n", argv[0]);
printf("Option values:\n");
printf("\t0\tCall correct function\n");
printf("\t1\tCall the wrong function but with the same signature\n");
printf("\t2\tCall a float function with an int function signature\n");
printf("\t3\tCall into the middle of a function\n");
printf("\n");
printf("\tAll other options are undefined, but should be caught by CFI :)\n");
printf("\n\n");
printf("Here are some pointers so clang doesn't optimize away members of `struct foo f`:\n");
printf("\tint_funcs: %p\n", (void*)f.int_funcs);
printf("\tbad_int_funcs: %p\n", (void*)f.bad_int_funcs);
printf("\tfloat_funcs: %p\n", (void*)f.float_funcs);
printf("\tnot_entries: %p\n", (void*)f.not_entries);
return 1;
}
simple1();
simple2();
printf("Calling a function:\n");
int idx = argv[1][0] - '0';
return f.int_funcs[idx](idx);
}

51
syft/file/executable.go Normal file
View File

@ -0,0 +1,51 @@
package file
type (
ExecutableFormat string
RelocationReadOnly string
)
const (
ELF ExecutableFormat = "elf"
MachO ExecutableFormat = "macho"
PE ExecutableFormat = "pe"
RelocationReadOnlyNone RelocationReadOnly = "none"
RelocationReadOnlyPartial RelocationReadOnly = "partial"
RelocationReadOnlyFull RelocationReadOnly = "full"
)
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"`
}
type ELFSecurityFeatures struct {
SymbolTableStripped bool `json:"symbolTableStripped" yaml:"symbolTableStripped" mapstructure:"symbolTableStripped"`
// classic protections
StackCanary *bool `json:"stackCanary,omitempty" yaml:"stackCanary" mapstructure:"stackCanary"`
NoExecutable bool `json:"nx" yaml:"nx" mapstructure:"nx"`
RelocationReadOnly RelocationReadOnly `json:"relRO" yaml:"relRO" mapstructure:"relRO"`
PositionIndependentExecutable bool `json:"pie" yaml:"pie" mapstructure:"pie"`
DynamicSharedObject bool `json:"dso" yaml:"dso" mapstructure:"dso"`
// LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors
// see https://clang.llvm.org/docs/SafeStack.html
LlvmSafeStack *bool `json:"safeStack,omitempty" yaml:"safeStack" mapstructure:"safeStack"`
// ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks
// see https://clang.llvm.org/docs/ControlFlowIntegrity.html
LlvmControlFlowIntegrity *bool `json:"cfi,omitempty" yaml:"cfi" mapstructure:"cfi"`
// ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions
// see https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md
ClangFortifySource *bool `json:"fortify,omitempty" yaml:"fortify" mapstructure:"fortify"`
//// Selfrando provides function order shuffling to defend against ROP and other types of code reuse
//// see https://github.com/runsafesecurity/selfrando
// Selfrando *bool `json:"selfrando,omitempty" yaml:"selfrando" mapstructure:"selfrando"`
}

View File

@ -221,6 +221,22 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
},
},
},
Executables: map[file.Coordinates]file.Executable{
c: {
Format: file.ELF,
SecurityFeatures: &file.ELFSecurityFeatures{
SymbolTableStripped: false,
StackCanary: boolRef(true),
NoExecutable: false,
RelocationReadOnly: "partial",
PositionIndependentExecutable: false,
DynamicSharedObject: false,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(true),
ClangFortifySource: boolRef(true),
},
},
},
LinuxDistribution: &linux.Release{
PrettyName: "some os",
Name: "os",

View File

@ -6,12 +6,13 @@ import (
)
type File struct {
ID string `json:"id"`
Location file.Coordinates `json:"location"`
Metadata *FileMetadataEntry `json:"metadata,omitempty"`
Contents string `json:"contents,omitempty"`
Digests []file.Digest `json:"digests,omitempty"`
Licenses []FileLicense `json:"licenses,omitempty"`
ID string `json:"id"`
Location file.Coordinates `json:"location"`
Metadata *FileMetadataEntry `json:"metadata,omitempty"`
Contents string `json:"contents,omitempty"`
Digests []file.Digest `json:"digests,omitempty"`
Licenses []FileLicense `json:"licenses,omitempty"`
Executable *file.Executable `json:"executable,omitempty"`
}
type FileMetadataEntry struct {

View File

@ -119,13 +119,19 @@ func toFile(s sbom.SBOM) []model.File {
})
}
var executable *file.Executable
if exec, exists := artifacts.Executables[coordinates]; exists {
executable = &exec
}
results = append(results, model.File{
ID: string(coordinates.ID()),
Location: coordinates,
Metadata: toFileMetadataEntry(coordinates, metadata),
Digests: digests,
Contents: contents,
Licenses: licenses,
ID: string(coordinates.ID()),
Location: coordinates,
Metadata: toFileMetadataEntry(coordinates, metadata),
Digests: digests,
Contents: contents,
Licenses: licenses,
Executable: executable,
})
}

View File

@ -35,6 +35,7 @@ func toSyftModel(doc model.Document) *sbom.SBOM {
FileDigests: fileArtifacts.FileDigests,
FileContents: fileArtifacts.FileContents,
FileLicenses: fileArtifacts.FileLicenses,
Executables: fileArtifacts.Executables,
LinuxDistribution: toSyftLinuxRelease(doc.Distro),
},
Source: *toSyftSourceData(doc.Source),
@ -69,6 +70,7 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
FileDigests: make(map[file.Coordinates][]file.Digest),
FileContents: make(map[file.Coordinates]string),
FileLicenses: make(map[file.Coordinates][]file.License),
Executables: make(map[file.Coordinates]file.Executable),
}
for _, f := range files {
@ -124,6 +126,10 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
LicenseEvidence: evidence,
})
}
if f.Executable != nil {
ret.Executables[coord] = *f.Executable
}
}
return ret

View File

@ -230,6 +230,7 @@ func Test_toSyftFiles(t *testing.T) {
want: sbom.Artifacts{
FileMetadata: map[file.Coordinates]file.Metadata{},
FileDigests: map[file.Coordinates][]file.Digest{},
Executables: map[file.Coordinates]file.Executable{},
},
},
{
@ -245,6 +246,7 @@ func Test_toSyftFiles(t *testing.T) {
Value: "123",
},
},
Executable: nil,
},
},
want: sbom.Artifacts{
@ -257,6 +259,7 @@ func Test_toSyftFiles(t *testing.T) {
},
},
},
Executables: map[file.Coordinates]file.Executable{},
},
},
{
@ -280,6 +283,20 @@ func Test_toSyftFiles(t *testing.T) {
Value: "123",
},
},
Executable: &file.Executable{
Format: file.ELF,
SecurityFeatures: &file.ELFSecurityFeatures{
SymbolTableStripped: false,
StackCanary: boolRef(true),
NoExecutable: false,
RelocationReadOnly: "partial",
PositionIndependentExecutable: false,
DynamicSharedObject: false,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(true),
ClangFortifySource: boolRef(true),
},
},
},
},
want: sbom.Artifacts{
@ -306,6 +323,22 @@ func Test_toSyftFiles(t *testing.T) {
},
},
},
Executables: map[file.Coordinates]file.Executable{
coord: {
Format: file.ELF,
SecurityFeatures: &file.ELFSecurityFeatures{
SymbolTableStripped: false,
StackCanary: boolRef(true),
NoExecutable: false,
RelocationReadOnly: "partial",
PositionIndependentExecutable: false,
DynamicSharedObject: false,
LlvmSafeStack: boolRef(false),
LlvmControlFlowIntegrity: boolRef(true),
ClangFortifySource: boolRef(true),
},
},
},
},
},
}
@ -318,7 +351,11 @@ func Test_toSyftFiles(t *testing.T) {
}
}
func Test_toSyfRelationship(t *testing.T) {
func boolRef(b bool) *bool {
return &b
}
func Test_toSyftRelationship(t *testing.T) {
packageWithId := func(id string) *pkg.Package {
p := &pkg.Package{}
p.OverrideID(artifact.ID(id))

View File

@ -17,8 +17,8 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
// Classifier is a generic package classifier that can be used to match a package definition

View File

@ -19,10 +19,10 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/golang/internal/xcoff"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
const goArch = "GOARCH"

View File

@ -9,7 +9,7 @@ import (
"github.com/kastenhq/goversion/version"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
"github.com/anchore/syft/syft/internal/unionreader"
)
type extendedBuildInfo struct {

View File

@ -20,8 +20,8 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
type nativeImageCycloneDX struct {

View File

@ -13,8 +13,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
func TestParseNativeImage(t *testing.T) {

View File

@ -11,9 +11,9 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
const linuxKernelMagicName = "Linux kernel"

View File

@ -8,9 +8,9 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
const modinfoName = ".modinfo"

View File

@ -9,9 +9,9 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
)
// Catalog identifies executables then attempts to read Rust dependency information from them

View File

@ -25,6 +25,7 @@ type Artifacts struct {
FileDigests map[file.Coordinates][]file.Digest
FileContents map[file.Coordinates]string
FileLicenses map[file.Coordinates][]file.License
Executables map[file.Coordinates]file.Executable
LinuxDistribution *linux.Release
}