mirror of
https://github.com/anchore/syft.git
synced 2026-11-07 01:44:17 +01:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f4bce762 | ||
|
|
c581c84ff1 | ||
|
|
fa02ff2d26 | ||
|
|
fd712ee71a | ||
|
|
551442b1bc | ||
|
|
639de5db64 |
@@ -260,6 +260,11 @@ func getSource(ctx context.Context, opts *options.Catalog, userInput string, sou
|
||||
cfg = cfg.WithDigestAlgorithms(hashers...)
|
||||
}
|
||||
|
||||
if source.ParseScope(opts.Scope) != source.SquashedScope {
|
||||
// every other scope reads each layer's squash, which is cheaper to build while the image is read
|
||||
ctx = internal.WithEagerLayerSquashes(ctx)
|
||||
}
|
||||
|
||||
src, err := syft.GetSource(ctx, userInput, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not determine source: %w", err)
|
||||
|
||||
@@ -22,7 +22,7 @@ require (
|
||||
github.com/anchore/go-sync v0.1.2
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.2.0
|
||||
github.com/anchore/stereoscope v0.4.0
|
||||
github.com/anchore/stereoscope v0.4.1
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
@@ -39,7 +39,6 @@ require (
|
||||
github.com/dustin/go-humanize v1.1.0
|
||||
github.com/elliotchance/phpserialize v1.4.0
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.8.0
|
||||
github.com/gkampitakis/go-snaps v0.5.24
|
||||
github.com/go-git/go-billy/v5 v5.9.2
|
||||
github.com/go-git/go-git/v5 v5.19.3
|
||||
@@ -69,6 +68,7 @@ require (
|
||||
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
|
||||
github.com/olekukonko/tablewriter v1.1.5
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23
|
||||
github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c
|
||||
@@ -83,6 +83,7 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
|
||||
github.com/ulikunitz/xz v0.5.17
|
||||
github.com/vbatts/go-mtree v0.7.0
|
||||
github.com/vifraa/gopom v1.0.0
|
||||
@@ -95,17 +96,14 @@ require (
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
|
||||
golang.org/x/mod v0.42.0
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/sys v0.49.0
|
||||
golang.org/x/time v0.16.0
|
||||
golang.org/x/tools v0.52.0
|
||||
howett.net/plist v1.0.1
|
||||
modernc.org/sqlite v1.61.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
|
||||
golang.org/x/sys v0.49.0
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
require github.com/github/go-spdx/v2 v2.8.0
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.25.2 // indirect
|
||||
|
||||
@@ -150,8 +150,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
|
||||
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
|
||||
github.com/anchore/stereoscope v0.4.0 h1:MDL7YFRDaGzhCdjm/s+em1D/X9HwnZMYupfgg+mvqPI=
|
||||
github.com/anchore/stereoscope v0.4.0/go.mod h1:hYJsi3M8a8N1O66u+rMQSgv5TREGM7QIjKBjPPnwiD8=
|
||||
github.com/anchore/stereoscope v0.4.1 h1:7mxw24JCabbJIAcyvQDjBd2pbzo+0HSFVgjYlE8Zm+k=
|
||||
github.com/anchore/stereoscope v0.4.1/go.mod h1:hYJsi3M8a8N1O66u+rMQSgv5TREGM7QIjKBjPPnwiD8=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
|
||||
+3
-2
@@ -257,8 +257,9 @@ hash_sha256() (
|
||||
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command openssl; then
|
||||
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f a
|
||||
hash=$(openssl dgst -sha256 "$TARGET") || return 1
|
||||
# output is "SHA256(<file>)= <digest>" (or "SHA2-256(...)" on OpenSSL 3.x)
|
||||
echo "${hash##* }"
|
||||
else
|
||||
log_err "hash_sha256 unable to find command to compute sha-256 hash"
|
||||
return 1
|
||||
|
||||
@@ -3,7 +3,7 @@ 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.1.12"
|
||||
JSONSchemaVersion = "16.1.13"
|
||||
|
||||
// Changelog
|
||||
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
|
||||
@@ -19,4 +19,5 @@ const (
|
||||
// 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
|
||||
// 16.1.11 - add CpanDistribution and CpanUnpackedRelease metadata types for the perl catalogers
|
||||
// 16.1.12 - add fullPath to file coordinates, naming the nested archive chain a file was found in (blank for files not within an archive)
|
||||
// 16.1.13 - add executable identifiers (ELF GNU and Go build IDs)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package internal
|
||||
|
||||
import "context"
|
||||
|
||||
type eagerLayerSquashesKey struct{}
|
||||
|
||||
// WithEagerLayerSquashes has the image source provider build every layer's squashed tree while it reads the
|
||||
// image, instead of the first time each one is used. Set it when the scan will read lower layers (any scope but
|
||||
// squashed): eager squashing overlaps with fetching and indexing the layers, so those scans finish sooner.
|
||||
func WithEagerLayerSquashes(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, eagerLayerSquashesKey{}, true)
|
||||
}
|
||||
|
||||
func EagerLayerSquashes(ctx context.Context) bool {
|
||||
v, _ := ctx.Value(eagerLayerSquashesKey{}).(bool)
|
||||
return v
|
||||
}
|
||||
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.1.12/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.1.13/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@@ -1468,6 +1468,13 @@
|
||||
"elfSecurityFeatures": {
|
||||
"$ref": "#/$defs/ELFSecurityFeatures",
|
||||
"description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF."
|
||||
},
|
||||
"identifiers": {
|
||||
"items": {
|
||||
"$ref": "#/$defs/ExecutableIdentifier"
|
||||
},
|
||||
"type": "array",
|
||||
"description": "Identifiers lists self-declared build identifiers found in the binary (e.g. ELF GNU build IDs or Go build IDs)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
@@ -1479,6 +1486,24 @@
|
||||
],
|
||||
"description": "Executable contains metadata about binary files and their security features."
|
||||
},
|
||||
"ExecutableIdentifier": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "Kind identifies the type and source of the identifier."
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Value is the identifier in the conventional text form for its Kind (e.g. lowercase hex for an ELF GNU build ID)."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"value"
|
||||
],
|
||||
"description": "ExecutableIdentifier is a self-declared build identifier used to correlate a binary with its build or debug information."
|
||||
},
|
||||
"File": {
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -2,11 +2,16 @@ package executable
|
||||
|
||||
import (
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
intFile "github.com/anchore/syft/internal/file"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
@@ -35,10 +40,132 @@ func findELFFeatures(data *file.Executable, reader unionreader.UnionReader) erro
|
||||
data.ELFSecurityFeatures = findELFSecurityFeatures(f)
|
||||
data.HasEntrypoint = elfHasEntrypoint(f)
|
||||
data.HasExports = elfHasExports(f)
|
||||
data.Identifiers = findELFIdentifiers(f)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
// NT_GNU_BUILD_ID is not defined by debug/elf.
|
||||
ntGNUBuildID = 3
|
||||
// ELF_NOTE_GOBUILDID_TAG from cmd/link, the note type the go linker uses for the go build ID.
|
||||
ntGoBuildID = 4
|
||||
|
||||
elfNoteOwnerGNU = "GNU"
|
||||
elfNoteOwnerGo = "Go"
|
||||
|
||||
// bound decoded bytes read from any one identifier note section.
|
||||
maxELFNoteBytes = 64 * intFile.KB
|
||||
|
||||
elfNoteHeaderSize = 12
|
||||
)
|
||||
|
||||
type elfNote struct {
|
||||
owner string
|
||||
typ uint32
|
||||
desc []byte
|
||||
}
|
||||
|
||||
// elfIdentifierNote describes where one kind of identifier lives and how its descriptor becomes a value.
|
||||
type elfIdentifierNote struct {
|
||||
section string
|
||||
owner string
|
||||
typ uint32
|
||||
kind file.ExecutableIdentifierKind
|
||||
// encode returns the value for a descriptor, or "" to skip it.
|
||||
encode func([]byte) string
|
||||
}
|
||||
|
||||
var elfIdentifierNotes = []elfIdentifierNote{
|
||||
{section: ".note.gnu.build-id", owner: elfNoteOwnerGNU, typ: ntGNUBuildID, kind: file.ELFGNUBuildID, encode: hex.EncodeToString},
|
||||
// the go linker always writes this note, while it only writes a GNU build ID when asked (-B) or when linking externally
|
||||
{section: ".note.go.buildid", owner: elfNoteOwnerGo, typ: ntGoBuildID, kind: file.GoBuildID, encode: goBuildIDValue},
|
||||
}
|
||||
|
||||
func findELFIdentifiers(f *elf.File) []file.ExecutableIdentifier {
|
||||
var ids []file.ExecutableIdentifier
|
||||
for _, in := range elfIdentifierNotes {
|
||||
ids = append(ids, findELFNoteIdentifiers(f, in)...)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func findELFNoteIdentifiers(f *elf.File, in elfIdentifierNote) []file.ExecutableIdentifier {
|
||||
s := f.Section(in.section)
|
||||
// real identifier notes are SHF_ALLOC, which the gABI forbids compressing, so a compressed one is
|
||||
// hostile or broken and is never decompressed
|
||||
if s == nil || s.Type != elf.SHT_NOTE || s.Flags&elf.SHF_COMPRESSED != 0 {
|
||||
return nil
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(s.Open(), maxELFNoteBytes))
|
||||
if err != nil {
|
||||
log.WithFields("error", err, "section", in.section).Trace("unable to read ELF identifier note")
|
||||
return nil
|
||||
}
|
||||
|
||||
var ids []file.ExecutableIdentifier
|
||||
seen := strset.New()
|
||||
for _, n := range parseELFNotes(data, s.Addralign, f.ByteOrder) {
|
||||
if n.owner != in.owner || n.typ != in.typ || len(n.desc) == 0 {
|
||||
continue
|
||||
}
|
||||
value := in.encode(n.desc)
|
||||
if value == "" || seen.Has(value) {
|
||||
continue
|
||||
}
|
||||
seen.Add(value)
|
||||
ids = append(ids, file.ExecutableIdentifier{Kind: in.kind, Value: value})
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// goBuildIDValue keeps the go build ID as the text the go toolchain prints for it (e.g. "go tool buildid").
|
||||
func goBuildIDValue(desc []byte) string {
|
||||
if !utf8.Valid(desc) {
|
||||
return ""
|
||||
}
|
||||
return string(desc)
|
||||
}
|
||||
|
||||
// parseELFNotes decodes the Elf_Nhdr records in a note region, stopping at the first truncated record.
|
||||
func parseELFNotes(data []byte, align uint64, order binary.ByteOrder) []elfNote {
|
||||
// Some GNU property note regions use 8-byte alignment instead of 4.
|
||||
if align != 8 {
|
||||
align = 4
|
||||
}
|
||||
|
||||
var notes []elfNote
|
||||
for len(data) >= elfNoteHeaderSize {
|
||||
nameSize := uint64(order.Uint32(data[0:4]))
|
||||
descSize := uint64(order.Uint32(data[4:8]))
|
||||
typ := order.Uint32(data[8:12])
|
||||
|
||||
nameEnd := elfNoteHeaderSize + nameSize
|
||||
descStart := alignUp(nameEnd, align)
|
||||
descEnd := descStart + descSize
|
||||
if descEnd > uint64(len(data)) {
|
||||
break
|
||||
}
|
||||
|
||||
notes = append(notes, elfNote{
|
||||
owner: strings.TrimRight(string(data[elfNoteHeaderSize:nameEnd]), "\x00"),
|
||||
typ: typ,
|
||||
desc: data[descStart:descEnd],
|
||||
})
|
||||
|
||||
next := alignUp(descEnd, align)
|
||||
if next >= uint64(len(data)) {
|
||||
break
|
||||
}
|
||||
data = data[next:]
|
||||
}
|
||||
return notes
|
||||
}
|
||||
|
||||
func alignUp(n, align uint64) uint64 {
|
||||
return (n + align - 1) &^ (align - 1)
|
||||
}
|
||||
|
||||
func findELFSecurityFeatures(f *elf.File) *file.ELFSecurityFeatures {
|
||||
return &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: isElfSymbolTableStripped(f),
|
||||
|
||||
@@ -4,8 +4,12 @@ import (
|
||||
"bytes"
|
||||
"debug/elf"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -13,6 +17,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/elfutil"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
@@ -285,6 +290,12 @@ func Test_hasElfDynTag(t *testing.T) {
|
||||
|
||||
// elfWithDynamicSection builds a little-endian ELF holding only a null section and a SHT_DYNAMIC section.
|
||||
func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte {
|
||||
t.Helper()
|
||||
return elfWithSection(t, class, elf.SHT_DYNAMIC, 1, dynamic)
|
||||
}
|
||||
|
||||
// elfWithSection builds a little-endian ELF holding only a null section and one section of the given type.
|
||||
func elfWithSection(t *testing.T, class elf.Class, typ elf.SectionType, align uint64, data []byte) []byte {
|
||||
t.Helper()
|
||||
ident := [elf.EI_NIDENT]byte{0x7f, 'E', 'L', 'F', byte(class), byte(elf.ELFDATA2LSB), byte(elf.EV_CURRENT)}
|
||||
var buf bytes.Buffer
|
||||
@@ -300,7 +311,7 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
|
||||
Shoff: uint32(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
|
||||
})
|
||||
write(elf.Section32{})
|
||||
write(elf.Section32{Type: uint32(elf.SHT_DYNAMIC), Off: uint32(dataOff), Size: uint32(len(dynamic)), Addralign: 1})
|
||||
write(elf.Section32{Type: uint32(typ), Off: uint32(dataOff), Size: uint32(len(data)), Addralign: uint32(align)})
|
||||
} else {
|
||||
ehsize, shentsize := binary.Size(elf.Header64{}), binary.Size(elf.Section64{})
|
||||
dataOff := ehsize + 2*shentsize
|
||||
@@ -309,8 +320,208 @@ func elfWithDynamicSection(t *testing.T, class elf.Class, dynamic []byte) []byte
|
||||
Shoff: uint64(ehsize), Ehsize: uint16(ehsize), Shentsize: uint16(shentsize), Shnum: 2,
|
||||
})
|
||||
write(elf.Section64{})
|
||||
write(elf.Section64{Type: uint32(elf.SHT_DYNAMIC), Off: uint64(dataOff), Size: uint64(len(dynamic)), Addralign: 1})
|
||||
write(elf.Section64{Type: uint32(typ), Off: uint64(dataOff), Size: uint64(len(data)), Addralign: align})
|
||||
}
|
||||
buf.Write(dynamic)
|
||||
buf.Write(data)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// encodeELFNote encodes a single little-endian Elf_Nhdr record, padding name and descriptor to align.
|
||||
func encodeELFNote(owner string, typ uint32, desc []byte, align int) []byte {
|
||||
pad := func(b []byte) []byte {
|
||||
for len(b)%align != 0 {
|
||||
b = append(b, 0)
|
||||
}
|
||||
return b
|
||||
}
|
||||
name := append([]byte(owner), 0)
|
||||
out := binary.LittleEndian.AppendUint32(nil, uint32(len(name)))
|
||||
out = binary.LittleEndian.AppendUint32(out, uint32(len(desc)))
|
||||
out = binary.LittleEndian.AppendUint32(out, typ)
|
||||
out = pad(append(out, name...))
|
||||
return pad(append(out, desc...))
|
||||
}
|
||||
|
||||
func Test_findELFIdentifiers(t *testing.T) {
|
||||
sha1ID := bytes.Repeat([]byte{0xab}, 20)
|
||||
customID := []byte{0xde, 0xad, 0xbe, 0xef}
|
||||
longID := bytes.Repeat([]byte{0x01}, 32)
|
||||
|
||||
buildID := func(v []byte) file.ExecutableIdentifier {
|
||||
return file.ExecutableIdentifier{Kind: file.ELFGNUBuildID, Value: hex.EncodeToString(v)}
|
||||
}
|
||||
const ntGNUABITag, ntGNUProperty = 1, 5
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
elf []byte
|
||||
sectionName string
|
||||
want []file.ExecutableIdentifier
|
||||
}{
|
||||
{
|
||||
name: "sha1-length build id",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
want: []file.ExecutableIdentifier{buildID(sha1ID)},
|
||||
},
|
||||
{
|
||||
name: "caller-chosen short build id",
|
||||
elf: elfWithSection(t, elf.ELFCLASS32, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, customID, 4)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
want: []file.ExecutableIdentifier{buildID(customID)},
|
||||
},
|
||||
{
|
||||
name: "build id after other notes in the same section",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, append(
|
||||
encodeELFNote("GNU", ntGNUABITag, make([]byte, 16), 4),
|
||||
encodeELFNote("GNU", ntGNUBuildID, longID, 4)...,
|
||||
)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
want: []file.ExecutableIdentifier{buildID(longID)},
|
||||
},
|
||||
{
|
||||
name: "8-byte aligned note section",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 8, append(
|
||||
encodeELFNote("GNU", ntGNUProperty, make([]byte, 12), 8),
|
||||
encodeELFNote("GNU", ntGNUBuildID, customID, 8)...,
|
||||
)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
want: []file.ExecutableIdentifier{buildID(customID)},
|
||||
},
|
||||
{
|
||||
name: "duplicate build ids",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, bytes.Repeat(
|
||||
encodeELFNote("GNU", ntGNUBuildID, customID, 4), 2,
|
||||
)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
want: []file.ExecutableIdentifier{buildID(customID)},
|
||||
},
|
||||
{
|
||||
name: "build id in another section is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||
sectionName: ".note.other",
|
||||
},
|
||||
{
|
||||
name: "build-id section with another section type is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_PROGBITS, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
},
|
||||
{
|
||||
name: "build-id section with another owner is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("FDO", ntGNUBuildID, sha1ID, 4)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
},
|
||||
{
|
||||
name: "build-id section with another note type is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUABITag, sha1ID, 4)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
},
|
||||
{
|
||||
name: "empty descriptor is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, nil, 4)),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
},
|
||||
{
|
||||
name: "truncated descriptor is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)[:20]),
|
||||
sectionName: ".note.gnu.build-id",
|
||||
},
|
||||
{
|
||||
name: "go build id",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("Go\x00", ntGoBuildID, []byte("action/content"), 4)),
|
||||
sectionName: ".note.go.buildid",
|
||||
want: []file.ExecutableIdentifier{{Kind: file.GoBuildID, Value: "action/content"}},
|
||||
},
|
||||
{
|
||||
name: "go build id that is not utf-8 is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("Go\x00", ntGoBuildID, []byte{0xff, 0xfe}, 4)),
|
||||
sectionName: ".note.go.buildid",
|
||||
},
|
||||
{
|
||||
name: "gnu build id note in the go section is ignored",
|
||||
elf: elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, sha1ID, 4)),
|
||||
sectionName: ".note.go.buildid",
|
||||
},
|
||||
{
|
||||
name: "no build-id section",
|
||||
elf: elfWithDynamicSection(t, elf.ELFCLASS64, nil),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
f, err := elfutil.NewFile(bytes.NewReader(tt.elf))
|
||||
require.NoError(t, err)
|
||||
f.Sections[1].Name = tt.sectionName
|
||||
assert.Equal(t, tt.want, findELFIdentifiers(f))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_findELFIdentifiers_compressedSectionIgnored(t *testing.T) {
|
||||
f, err := elfutil.NewFile(bytes.NewReader(elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, encodeELFNote("GNU", ntGNUBuildID, []byte{0xde, 0xad}, 4))))
|
||||
require.NoError(t, err)
|
||||
f.Sections[1].Name = ".note.gnu.build-id"
|
||||
f.Sections[1].Flags |= elf.SHF_COMPRESSED
|
||||
assert.Nil(t, findELFIdentifiers(f))
|
||||
}
|
||||
|
||||
type countingNoteReader struct {
|
||||
io.ReaderAt
|
||||
bytesRead int
|
||||
}
|
||||
|
||||
func (r *countingNoteReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
n, err := r.ReaderAt.ReadAt(p, off)
|
||||
r.bytesRead += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
func Test_findELFIdentifiers_readLimit(t *testing.T) {
|
||||
id := encodeELFNote("GNU", ntGNUBuildID, []byte{0xde, 0xad, 0xbe, 0xef}, 4)
|
||||
data := append(id, make([]byte, maxELFNoteBytes)...)
|
||||
r := &countingNoteReader{ReaderAt: bytes.NewReader(elfWithSection(t, elf.ELFCLASS64, elf.SHT_NOTE, 4, data))}
|
||||
f, err := elfutil.NewFile(r)
|
||||
require.NoError(t, err)
|
||||
f.Sections[1].Name = ".note.gnu.build-id"
|
||||
r.bytesRead = 0
|
||||
|
||||
assert.Equal(t, []file.ExecutableIdentifier{{Kind: file.ELFGNUBuildID, Value: "deadbeef"}}, findELFIdentifiers(f))
|
||||
assert.Equal(t, maxELFNoteBytes, r.bytesRead)
|
||||
}
|
||||
|
||||
func Test_parseELFNotes_bigEndian(t *testing.T) {
|
||||
data := []byte{0, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 3, 'G', 'N', 'U', 0, 0xde, 0xad, 0xbe, 0xef}
|
||||
assert.Equal(t, []elfNote{{owner: "GNU", typ: ntGNUBuildID, desc: data[16:]}}, parseELFNotes(data, 4, binary.BigEndian))
|
||||
}
|
||||
|
||||
// Test_findELFFeatures_realLinkerIdentifiers proves identifiers are picked up from real linker output, not just
|
||||
// hand-built notes. The go toolchain is the one linker every test environment has, and -B makes it emit a GNU
|
||||
// build ID next to the go build ID it always writes.
|
||||
func Test_findELFFeatures_realLinkerIdentifiers(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("builds a go binary")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
src := filepath.Join(dir, "main.go")
|
||||
bin := filepath.Join(dir, "main")
|
||||
require.NoError(t, os.WriteFile(src, []byte("package main\n\nfunc main() {}\n"), 0o600))
|
||||
|
||||
build := exec.Command("go", "build", "-trimpath", "-ldflags=-B 0x0123456789abcdef", "-o", bin, src)
|
||||
build.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0", "GOFLAGS=", "GOWORK=off")
|
||||
out, err := build.CombinedOutput()
|
||||
require.NoError(t, err, string(out))
|
||||
|
||||
goBuildID, err := exec.Command("go", "tool", "buildid", bin).Output()
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := os.Open(bin)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = f.Close() })
|
||||
|
||||
var data file.Executable
|
||||
require.NoError(t, findELFFeatures(&data, f))
|
||||
assert.Equal(t, []file.ExecutableIdentifier{
|
||||
{Kind: file.ELFGNUBuildID, Value: "0123456789abcdef"},
|
||||
{Kind: file.GoBuildID, Value: strings.TrimSpace(string(goBuildID))},
|
||||
}, data.Identifiers)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ type (
|
||||
|
||||
// RelocationReadOnly indicates the RELRO security protection level applied to an ELF binary.
|
||||
RelocationReadOnly string
|
||||
|
||||
// ExecutableIdentifierKind identifies the type and source of an executable identifier.
|
||||
ExecutableIdentifierKind string
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,6 +19,9 @@ const (
|
||||
RelocationReadOnlyNone RelocationReadOnly = "none" // no RELRO protection
|
||||
RelocationReadOnlyPartial RelocationReadOnly = "partial" // partial RELRO protection
|
||||
RelocationReadOnlyFull RelocationReadOnly = "full" // full RELRO protection
|
||||
|
||||
ELFGNUBuildID ExecutableIdentifierKind = "elf-gnu-build-id" // GNU build ID from an ELF .note.gnu.build-id note
|
||||
GoBuildID ExecutableIdentifierKind = "go-build-id" // build ID written by the go linker
|
||||
)
|
||||
|
||||
// Executable contains metadata about binary files and their security features.
|
||||
@@ -34,6 +40,19 @@ type Executable struct {
|
||||
|
||||
// ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF.
|
||||
ELFSecurityFeatures *ELFSecurityFeatures `json:"elfSecurityFeatures,omitempty" yaml:"elfSecurityFeatures" mapstructure:"elfSecurityFeatures"`
|
||||
|
||||
// Identifiers lists self-declared build identifiers found in the binary (e.g. ELF GNU build IDs or Go build IDs).
|
||||
Identifiers []ExecutableIdentifier `json:"identifiers,omitempty" yaml:"identifiers" mapstructure:"identifiers"`
|
||||
}
|
||||
|
||||
// ExecutableIdentifier is a self-declared build identifier used to correlate a binary with its build or debug information.
|
||||
// It can survive stripping; use file digests to verify content.
|
||||
type ExecutableIdentifier struct {
|
||||
// Kind identifies the type and source of the identifier.
|
||||
Kind ExecutableIdentifierKind `json:"kind" yaml:"kind" mapstructure:"kind"`
|
||||
|
||||
// Value is the identifier in the conventional text form for its Kind (e.g. lowercase hex for an ELF GNU build ID).
|
||||
Value string `json:"value" yaml:"value" mapstructure:"value"`
|
||||
}
|
||||
|
||||
// ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries.
|
||||
|
||||
@@ -219,6 +219,9 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
||||
Executables: map[file.Coordinates]file.Executable{
|
||||
c: {
|
||||
Format: file.ELF,
|
||||
Identifiers: []file.ExecutableIdentifier{
|
||||
{Kind: file.ELFGNUBuildID, Value: "0123456789abcdef"},
|
||||
},
|
||||
ELFSecurityFeatures: &file.ELFSecurityFeatures{
|
||||
SymbolTableStripped: false,
|
||||
StackCanary: boolRef(true),
|
||||
@@ -288,4 +291,11 @@ func Test_encodeDecodeFileMetadata(t *testing.T) {
|
||||
assert.Equal(t, internal.JSONSchemaVersion, decodedVersion)
|
||||
|
||||
require.Equal(t, s, *got)
|
||||
|
||||
executable := s.Artifacts.Executables[c]
|
||||
executable.Identifiers = nil
|
||||
s.Artifacts.Executables[c] = executable
|
||||
buf.Reset()
|
||||
require.NoError(t, enc.Encode(buf, s))
|
||||
assert.NotContains(t, buf.String(), `"identifiers"`)
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ var sectionsReadByName = map[string]struct{}{
|
||||
".gopclntab": {},
|
||||
".note.package": {},
|
||||
".modinfo": {},
|
||||
// identifier notes read by the executable cataloger
|
||||
".note.gnu.build-id": {},
|
||||
".note.go.buildid": {},
|
||||
}
|
||||
|
||||
// wire sizes of the structures we decode, which differ between the two ELF classes
|
||||
|
||||
@@ -41,8 +41,8 @@ var (
|
||||
// inject the correct version into the main module of the build process
|
||||
|
||||
knownBuildFlagPatterns = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?m)\.[a-zA-Z0-9]*([rR]elease)?([gG]it)?([bB]uild)?[vV]er(sion)?=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||
regexp.MustCompile(`(?m)\.[a-zA-Z0-9]*([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*)`),
|
||||
regexp.MustCompile(`(?m)\.[a-zA-Z0-9]*([rR]elease)?([gG]it)?([bB]uild)?[vV]er(sion)?=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*(?:\.[-\w]+)*)`),
|
||||
regexp.MustCompile(`(?m)\.[a-zA-Z0-9]*([tT]ag)=(\S+/)*(?P<version>v?\d+.\d+.\d+[-\w]*(?:\.[-\w]+)*)`),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1285,6 +1285,20 @@ func Test_extractVersionFromLDFlags(t *testing.T) {
|
||||
wantMajorVersion: "2",
|
||||
wantFullVersion: "v2.4.5",
|
||||
},
|
||||
{
|
||||
name: "pre-release version with dot-separated identifiers",
|
||||
mainModule: "github.com/foo/bar",
|
||||
ldflags: ` build -ldflags="-s -w -X 'github.com/foo/bar/version.gitVersion=v1.28.0-rc.1' -X github.com/foo/bar/version.gitCommit=abc123"`,
|
||||
wantMajorVersion: "1",
|
||||
wantFullVersion: "v1.28.0-rc.1",
|
||||
},
|
||||
{
|
||||
name: "pre-release version with multiple dot-separated identifiers",
|
||||
mainModule: "github.com/foo/bar",
|
||||
ldflags: ` build -ldflags="-X main.version=2.0.0-beta.2.1"`,
|
||||
wantMajorVersion: "2",
|
||||
wantFullVersion: "v2.0.0-beta.2.1",
|
||||
},
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// don't know how to handle these... yet
|
||||
//{
|
||||
|
||||
@@ -234,6 +234,10 @@ func candidateTargetSw(p pkg.Package) []string {
|
||||
// NVD records CPAN distributions with perl in target_sw, e.g.
|
||||
// cpe:2.3:a:mojolicious:mojolicious:*:*:*:*:*:perl:*:*
|
||||
return []string{"perl"}
|
||||
case p.Type == pkg.TerraformPkg:
|
||||
// NVD records terraform providers with the provider type in target_sw, e.g.
|
||||
// cpe:2.3:a:hashicorp:terraform_provider:*:*:*:*:*:vault:*:*
|
||||
return candidateTargetSwForTerraformProvider(p)
|
||||
}
|
||||
|
||||
return []string{cpe.Any}
|
||||
@@ -332,6 +336,9 @@ func candidateVendorsByType(p pkg.Package, vendors fieldCandidateSet) fieldCandi
|
||||
case pkg.WordpressPluginEntry:
|
||||
vendors.clear()
|
||||
vendors.union(candidateVendorsForWordpressPlugin(p))
|
||||
case pkg.TerraformLockProviderEntry:
|
||||
vendors.clear()
|
||||
vendors.union(candidateVendorsForTerraformProvider(p))
|
||||
}
|
||||
return vendors
|
||||
}
|
||||
@@ -377,6 +384,9 @@ func candidateProductSet(p pkg.Package) fieldCandidateSet {
|
||||
case pkg.WordpressPluginEntry:
|
||||
products.clear()
|
||||
products.union(candidateProductsForWordpressPlugin(p))
|
||||
case pkg.TerraformLockProviderEntry:
|
||||
products.clear()
|
||||
products.union(candidateProductsForTerraformProvider(p))
|
||||
}
|
||||
|
||||
// it is never OK to have candidates with these values ["" and "*"] (since CPEs will match any other value)
|
||||
|
||||
@@ -902,6 +902,38 @@ func TestGeneratePackageCPEs(t *testing.T) {
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "terraform provider generates NVD terraform_provider CPEs not application CPEs",
|
||||
p: pkg.Package{
|
||||
Name: "registry.terraform.io/grafana/grafana",
|
||||
Version: "4.45.2",
|
||||
Type: pkg.TerraformPkg,
|
||||
Language: pkg.Go,
|
||||
Metadata: pkg.TerraformLockProviderEntry{
|
||||
URL: "registry.terraform.io/grafana/grafana",
|
||||
Version: "4.45.2",
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:grafana:terraform_provider:4.45.2:*:*:*:*:grafana:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "terraform provider with different namespace and type",
|
||||
p: pkg.Package{
|
||||
Name: "registry.terraform.io/hashicorp/aws",
|
||||
Version: "5.72.1",
|
||||
Type: pkg.TerraformPkg,
|
||||
Language: pkg.Go,
|
||||
Metadata: pkg.TerraformLockProviderEntry{
|
||||
URL: "registry.terraform.io/hashicorp/aws",
|
||||
Version: "5.72.1",
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"cpe:2.3:a:hashicorp:terraform_provider:5.72.1:*:*:*:*:aws:*:*",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rust package",
|
||||
p: pkg.Package{
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// NVD records terraform providers with a fixed "terraform_provider" product and the provider type in target_sw, e.g.
|
||||
// cpe:2.3:a:hashicorp:terraform_provider:*:*:*:*:*:vault:*:*
|
||||
const terraformProviderProduct = "terraform_provider"
|
||||
|
||||
func candidateVendorsForTerraformProvider(p pkg.Package) fieldCandidateSet {
|
||||
vendors := newFieldCandidateSet()
|
||||
|
||||
namespace, _ := parseTerraformProviderName(p.Name)
|
||||
if namespace != "" {
|
||||
vendors.addValue(namespace)
|
||||
}
|
||||
|
||||
return vendors
|
||||
}
|
||||
|
||||
func candidateProductsForTerraformProvider(p pkg.Package) fieldCandidateSet {
|
||||
products := newFieldCandidateSet()
|
||||
|
||||
_, providerType := parseTerraformProviderName(p.Name)
|
||||
if providerType != "" {
|
||||
products.add(fieldCandidate{
|
||||
value: terraformProviderProduct,
|
||||
disallowSubSelections: true,
|
||||
disallowDelimiterVariations: true,
|
||||
})
|
||||
}
|
||||
|
||||
return products
|
||||
}
|
||||
|
||||
func candidateTargetSwForTerraformProvider(p pkg.Package) []string {
|
||||
_, providerType := parseTerraformProviderName(p.Name)
|
||||
if providerType == "" {
|
||||
return []string{cpe.Any}
|
||||
}
|
||||
return []string{providerType}
|
||||
}
|
||||
|
||||
// parseTerraformProviderName extracts the namespace and type from a terraform provider
|
||||
// source address. The expected format is "registry.terraform.io/<namespace>/<type>".
|
||||
func parseTerraformProviderName(name string) (namespace, providerType string) {
|
||||
parts := strings.Split(name, "/")
|
||||
if len(parts) < 3 {
|
||||
return "", ""
|
||||
}
|
||||
return parts[len(parts)-2], parts[len(parts)-1]
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package cpegenerate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_candidateVendorsForTerraformProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "hashicorp/aws provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/hashicorp/aws",
|
||||
},
|
||||
expected: []string{"hashicorp"},
|
||||
},
|
||||
{
|
||||
name: "grafana/grafana provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/grafana/grafana",
|
||||
},
|
||||
expected: []string{"grafana"},
|
||||
},
|
||||
{
|
||||
name: "hashicorp/google provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/hashicorp/google",
|
||||
},
|
||||
expected: []string{"hashicorp"},
|
||||
},
|
||||
{
|
||||
name: "malformed name with fewer than 3 segments",
|
||||
pkg: pkg.Package{
|
||||
Name: "hashicorp/aws",
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := candidateVendorsForTerraformProvider(test.pkg).uniqueValues()
|
||||
assert.ElementsMatch(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_candidateProductsForTerraformProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "hashicorp/aws provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/hashicorp/aws",
|
||||
},
|
||||
expected: []string{"terraform_provider"},
|
||||
},
|
||||
{
|
||||
name: "grafana/grafana provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/grafana/grafana",
|
||||
},
|
||||
expected: []string{"terraform_provider"},
|
||||
},
|
||||
{
|
||||
name: "hashicorp/google provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/hashicorp/google",
|
||||
},
|
||||
expected: []string{"terraform_provider"},
|
||||
},
|
||||
{
|
||||
name: "malformed name with fewer than 3 segments",
|
||||
pkg: pkg.Package{
|
||||
Name: "hashicorp/aws",
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := candidateProductsForTerraformProvider(test.pkg).uniqueValues()
|
||||
assert.ElementsMatch(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_candidateTargetSwForTerraformProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkg pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "hashicorp/aws provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/hashicorp/aws",
|
||||
},
|
||||
expected: []string{"aws"},
|
||||
},
|
||||
{
|
||||
name: "grafana/grafana provider",
|
||||
pkg: pkg.Package{
|
||||
Name: "registry.terraform.io/grafana/grafana",
|
||||
},
|
||||
expected: []string{"grafana"},
|
||||
},
|
||||
{
|
||||
name: "malformed name with fewer than 3 segments",
|
||||
pkg: pkg.Package{
|
||||
Name: "hashicorp/aws",
|
||||
},
|
||||
expected: []string{cpe.Any},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, candidateTargetSwForTerraformProvider(test.pkg))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseTerraformProviderName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedNamespace string
|
||||
expectedType string
|
||||
}{
|
||||
{
|
||||
name: "standard provider",
|
||||
input: "registry.terraform.io/hashicorp/aws",
|
||||
expectedNamespace: "hashicorp",
|
||||
expectedType: "aws",
|
||||
},
|
||||
{
|
||||
name: "provider where namespace matches type",
|
||||
input: "registry.terraform.io/grafana/grafana",
|
||||
expectedNamespace: "grafana",
|
||||
expectedType: "grafana",
|
||||
},
|
||||
{
|
||||
name: "too few segments",
|
||||
input: "hashicorp/aws",
|
||||
expectedNamespace: "",
|
||||
expectedType: "",
|
||||
},
|
||||
{
|
||||
name: "single segment",
|
||||
input: "aws",
|
||||
expectedNamespace: "",
|
||||
expectedType: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
namespace, providerType := parseTerraformProviderName(test.input)
|
||||
assert.Equal(t, test.expectedNamespace, namespace)
|
||||
assert.Equal(t, test.expectedType, providerType)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/anchore/go-collections"
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
@@ -29,6 +30,10 @@ func (l stereoscopeImageSourceProvider) Name() string {
|
||||
}
|
||||
|
||||
func (l stereoscopeImageSourceProvider) Provide(ctx context.Context) (source.Source, error) {
|
||||
if !internal.EagerLayerSquashes(ctx) {
|
||||
// lower layer squashes are only built if something reads them, so a squashed scan never pays for them
|
||||
ctx = image.WithLazyLayerSquashes(ctx)
|
||||
}
|
||||
img, err := l.stereoscopeProvider.Provide(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package stereoscopesource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/random"
|
||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
func Test_stereoscopeImageSourceProvider_layerSquashes(t *testing.T) {
|
||||
img, err := random.Image(1024, 3)
|
||||
require.NoError(t, err)
|
||||
tag, err := name.NewTag("localhost/layer-squashes:latest")
|
||||
require.NoError(t, err)
|
||||
archive := filepath.Join(t.TempDir(), "image.tar")
|
||||
require.NoError(t, tarball.WriteToFile(archive, tag, img))
|
||||
|
||||
provide := func(t *testing.T, ctx context.Context) *stereoscopeImageSource {
|
||||
providers := Providers(ProviderConfig{
|
||||
StereoscopeImageProviderConfig: stereoscope.ImageProviderConfig{UserInput: archive},
|
||||
})
|
||||
for _, p := range providers {
|
||||
if p.Value.Name() != image.DockerTarballSource {
|
||||
continue
|
||||
}
|
||||
src, err := p.Value.Provide(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { require.NoError(t, src.Close()) })
|
||||
return src.(*stereoscopeImageSource)
|
||||
}
|
||||
t.Fatalf("no %s provider", image.DockerTarballSource)
|
||||
return nil
|
||||
}
|
||||
|
||||
lazy := provide(t, context.Background())
|
||||
eager := provide(t, internal.WithEagerLayerSquashes(context.Background()))
|
||||
|
||||
// lower layers are built on first use unless eager was asked for, the top is the image squash either way
|
||||
require.Len(t, lazy.image.Layers, 3)
|
||||
for idx := range lazy.image.Layers {
|
||||
_, isTree := lazy.image.Layers[idx].SquashedTree.(*filetree.FileTree)
|
||||
assert.Equalf(t, idx == 2, isTree, "lazy layer %d", idx)
|
||||
assert.IsTypef(t, &filetree.FileTree{}, eager.image.Layers[idx].SquashedTree, "eager layer %d", idx)
|
||||
}
|
||||
|
||||
// every scope sees the same files either way
|
||||
for _, scope := range []source.Scope{source.SquashedScope, source.AllLayersScope, source.DeepSquashedScope} {
|
||||
t.Run(string(scope), func(t *testing.T) {
|
||||
files := func(src *stereoscopeImageSource) []string {
|
||||
res, err := src.FileResolver(scope)
|
||||
require.NoError(t, err)
|
||||
locs, err := res.FilesByGlob("**/*")
|
||||
require.NoError(t, err)
|
||||
var out []string
|
||||
for _, l := range locs {
|
||||
out = append(out, l.RealPath+"@"+l.FileSystemID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
want := files(eager)
|
||||
require.NotEmpty(t, want)
|
||||
assert.ElementsMatch(t, want, files(lazy))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,20 @@ test_hash_sha256() {
|
||||
|
||||
run_test_case test_hash_sha256
|
||||
|
||||
# verify 256 digest of a file when openssl is the only available hash tool
|
||||
test_hash_sha256_openssl() {
|
||||
target=./testdata/assets/valid/syft_1.5.0_linux_arm64.tar.gz
|
||||
|
||||
# hide the other hash tools (within subshells) so that hash_sha256 falls back to openssl
|
||||
actual=$(is_command() { [ "$1" = "openssl" ]; }; hash_sha256 "${target}")
|
||||
assertEquals "8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d" "${actual}" "mismatched checksum"
|
||||
|
||||
actual=$(is_command() { [ "$1" = "openssl" ]; }; hash_sha256 < "${target}")
|
||||
assertEquals "8d57abb57a0dae3ff23c8f0df1f51951b7772822e0d560e860d6f68c24ef6d3d" "${actual}" "mismatched checksum (stdin)"
|
||||
}
|
||||
|
||||
run_test_case test_hash_sha256_openssl
|
||||
|
||||
# verify 256 digest of a file relative to the checksums file
|
||||
test_hash_sha256_verify() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user