Add support for reading ELF package notes with section header (#2939)

* add support for reading ELF package notes with section header

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

* add systemd elf package fields to json schema

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-06-07 14:38:54 -04:00 committed by GitHub
parent bc20e66d08
commit 254a562b4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 2862 additions and 69 deletions

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.13"
JSONSchemaVersion = "16.0.14"
)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.13/document",
"$id": "anchore.io/schema/syft/json/16.0.14/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -647,12 +647,24 @@
"type": {
"type": "string"
},
"vendor": {
"architecture": {
"type": "string"
},
"osCPE": {
"type": "string"
},
"os": {
"type": "string"
},
"osVersion": {
"type": "string"
},
"system": {
"type": "string"
},
"vendor": {
"type": "string"
},
"sourceRepo": {
"type": "string"
},

View file

@ -15,9 +15,34 @@ type ClassifierMatch struct {
// ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of the binary
type ELFBinaryPackageNoteJSONPayload struct {
Type string `json:"type,omitempty"`
Vendor string `json:"vendor,omitempty"`
System string `json:"system,omitempty"`
// these are well-known fields as defined by systemd ELF package metadata "spec" https://systemd.io/ELF_PACKAGE_METADATA/
// Type is the type of the package (e.g. "rpm", "deb", "apk", etc.)
Type string `json:"type,omitempty"`
// Architecture of the binary package (e.g. "amd64", "arm", etc.)
Architecture string `json:"architecture,omitempty"`
// OS CPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)
OSCPE string `json:"osCPE,omitempty"`
// OS is the OS name, typically corresponding to ID in os-release (e.g. "fedora")
OS string `json:"os,omitempty"`
// osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. "33")
OSVersion string `json:"osVersion,omitempty"`
// these are additional fields that are not part of the systemd spec
// System is a context-specific name for the system that the binary package is intended to run on or a part of
System string `json:"system,omitempty"`
// Vendor is the individual or organization that produced the source code for the binary
Vendor string `json:"vendor,omitempty"`
// SourceRepo is the URL to the source repository for which the binary was built from
SourceRepo string `json:"sourceRepo,omitempty"`
Commit string `json:"commit,omitempty"`
// Commit is the commit hash of the source repository for which the binary was built from
Commit string `json:"commit,omitempty"`
}

View file

@ -2,6 +2,7 @@ package binary
import (
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
@ -23,13 +24,59 @@ func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) p
}
func packageURL(metadata elfBinaryPackageNotes) string {
// TODO: what if the System value is not set?
var qualifiers []packageurl.Qualifier
os := metadata.OS
osVersion := metadata.OSVersion
atts, err := cpe.NewAttributes(metadata.OSCPE)
if err == nil {
// only "upgrade" the OS information if there is something more specific to use in it's place
if os == "" && osVersion == "" || os == "" && atts.Version != "" || atts.Product != "" && osVersion == "" {
os = atts.Product
osVersion = atts.Version
}
}
if os != "" {
osQualifier := os
if osVersion != "" {
osQualifier += "-" + osVersion
}
qualifiers = append(qualifiers, packageurl.Qualifier{
Key: "distro",
Value: osQualifier,
})
}
ty := purlDistroType(metadata.Type)
namespace := os
if ty == packageurl.TypeGeneric || os == "" {
namespace = metadata.System
}
return packageurl.NewPackageURL(
packageurl.TypeGeneric,
metadata.System,
ty,
namespace,
metadata.Name,
metadata.Version,
nil,
qualifiers,
"",
).ToString()
}
func purlDistroType(ty string) string {
switch ty {
case "rpm":
return packageurl.TypeRPM
case "deb":
return packageurl.TypeDebian
case "apk":
return packageurl.TypeAlpine
case "alpm":
return "alpm"
}
return packageurl.TypeGeneric
}

View file

@ -1,8 +1,10 @@
package binary
import (
"bytes"
"context"
"debug/elf"
"encoding/binary"
"encoding/json"
"fmt"
@ -154,11 +156,42 @@ func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
return nil, err
}
var metadata elfBinaryPackageNotes
if err := json.Unmarshal(notes, &metadata); err != nil {
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to unmarshal ELF package notes as JSON")
if len(notes) == 0 {
return nil, nil
}
return &metadata, err
{
var metadata elfBinaryPackageNotes
if err := json.Unmarshal(notes, &metadata); err == nil {
return &metadata, nil
}
}
{
var header elf64SectionHeader
headerSize := binary.Size(header) / 4
if len(notes) > headerSize {
var metadata elfBinaryPackageNotes
newPayload := bytes.TrimRight(notes[headerSize:], "\x00")
if err := json.Unmarshal(newPayload, &metadata); err == nil {
return &metadata, nil
}
log.WithFields("file", r.Location.Path(), "error", err).Trace("unable to unmarshal ELF package notes as JSON")
}
}
return nil, err
}
type elf64SectionHeader struct {
ShName uint32
ShType uint32
ShFlags uint64
ShAddr uint64
ShOffset uint64
ShSize uint64
ShLink uint32
ShInfo uint32
ShAddralign uint64
ShEntsize uint64
}

View file

@ -9,60 +9,115 @@ import (
)
func Test_ELF_Package_Cataloger(t *testing.T) {
expectedPkgs := []pkg.Package{
{
Name: "libhello_world.so",
Version: "0.01",
PURL: "pkg:generic/syftsys/libhello_world.so@0.01",
FoundBy: "",
Locations: file.NewLocationSet(
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so"),
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so"),
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so"),
),
Licenses: pkg.NewLicenseSet(
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
),
Language: "",
Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture",
Vendor: "syft",
System: "syftsys",
SourceRepo: "https://github.com/someone/somewhere.git",
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
cases := []struct {
name string
fixture string
expected []pkg.Package
}{
{
name: "go case",
fixture: "elf-test-fixtures",
expected: []pkg.Package{
{
Name: "libhello_world.so",
Version: "0.01",
PURL: "pkg:generic/syftsys/libhello_world.so@0.01",
Locations: file.NewLocationSet(
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so"),
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so"),
file.NewVirtualLocation("/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.so"),
),
Licenses: pkg.NewLicenseSet(
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
),
Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture",
Vendor: "syft",
System: "syftsys",
SourceRepo: "https://github.com/someone/somewhere.git",
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
},
},
{
Name: "syfttestfixture",
Version: "0.01",
PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
Locations: file.NewLocationSet(
file.NewLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
Licenses: pkg.NewLicenseSet(
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
),
Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture",
Vendor: "syft",
System: "syftsys",
SourceRepo: "https://github.com/someone/somewhere.git",
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
},
},
},
},
{
Name: "syfttestfixture",
Version: "0.01",
PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
FoundBy: "",
Locations: file.NewLocationSet(
file.NewLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
file.NewLocation("/usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
Licenses: pkg.NewLicenseSet(
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
),
Language: "",
Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture",
Vendor: "syft",
System: "syftsys",
SourceRepo: "https://github.com/someone/somewhere.git",
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb",
name: "fedora 64 bit binaries",
fixture: "image-fedora-64bit",
expected: []pkg.Package{
{
Name: "coreutils",
Version: "9.5-1.fc41",
PURL: "pkg:rpm/fedora/coreutils@9.5-1.fc41?distro=fedora-40",
Locations: file.NewLocationSet(
file.NewLocation("/sha256sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
file.NewLocation("/sha1sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
Licenses: pkg.NewLicenseSet(),
Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
Architecture: "x86_64",
OSCPE: "cpe:/o:fedoraproject:fedora:40",
},
},
},
},
{
name: "fedora 32 bit binaries",
fixture: "image-fedora-32bit",
expected: []pkg.Package{
{
Name: "coreutils",
Version: "9.0-5.fc36",
PURL: "pkg:rpm/fedora/coreutils@9.0-5.fc36?distro=fedora-36",
Locations: file.NewLocationSet(
file.NewLocation("/sha256sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
file.NewLocation("/sha1sum").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
),
Licenses: pkg.NewLicenseSet(),
Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
Architecture: "arm",
OSCPE: "cpe:/o:fedoraproject:fedora:36",
},
},
},
},
}
pkgtest.NewCatalogTester().
WithImageResolver(t, "elf-test-fixtures").
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
Expects(expectedPkgs, nil).
TestCataloger(t, NewELFPackageCataloger())
for _, v := range cases {
t.Run(v.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
WithImageResolver(t, v.fixture).
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
Expects(v.expected, nil).
TestCataloger(t, NewELFPackageCataloger())
})
}
}

View file

@ -14,36 +14,110 @@ import (
func Test_packageURL(t *testing.T) {
tests := []struct {
name string
notes elfBinaryPackageNotes
expected string
metadata elfBinaryPackageNotes
want string
}{
{
name: "elf-binary-package-cataloger",
notes: elfBinaryPackageNotes{
metadata: elfBinaryPackageNotes{
Name: "github.com/anchore/syft",
Version: "v0.1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "syftsys",
},
},
expected: "pkg:generic/syftsys/github.com/anchore/syft@v0.1.0",
want: "pkg:generic/syftsys/github.com/anchore/syft@v0.1.0",
},
{
name: "elf binary package short name",
notes: elfBinaryPackageNotes{
metadata: elfBinaryPackageNotes{
Name: "go.opencensus.io",
Version: "v0.23.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "syftsys",
},
},
expected: "pkg:generic/syftsys/go.opencensus.io@v0.23.0",
want: "pkg:generic/syftsys/go.opencensus.io@v0.23.0",
},
{
name: "no info",
metadata: elfBinaryPackageNotes{
Name: "test",
Version: "1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
},
},
want: "pkg:rpm/test@1.0",
},
{
name: "with system",
metadata: elfBinaryPackageNotes{
Name: "test",
Version: "1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
System: "system",
},
},
want: "pkg:rpm/system/test@1.0",
},
{
name: "with os info preferred",
metadata: elfBinaryPackageNotes{
Name: "test",
Version: "1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
OS: "fedora",
OSVersion: "2.0",
OSCPE: "cpe:/o:someone:redhat:3.0",
},
},
want: "pkg:rpm/fedora/test@1.0?distro=fedora-2.0",
},
{
name: "with os info fallback to CPE parsing (missing version)",
metadata: elfBinaryPackageNotes{
Name: "test",
Version: "1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
OS: "fedora",
OSCPE: "cpe:/o:someone:redhat:3.0",
},
},
want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0",
},
{
name: "with os info preferred (missing OS)",
metadata: elfBinaryPackageNotes{
Name: "test",
Version: "1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm",
OSVersion: "2.0",
OSCPE: "cpe:/o:someone:redhat:3.0",
},
},
want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0",
},
{
name: "missing type",
metadata: elfBinaryPackageNotes{
Name: "test",
Version: "1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "system",
},
},
want: "pkg:generic/system/test@1.0",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, packageURL(test.notes))
assert.Equal(t, test.want, packageURL(test.metadata))
})
}
}

View file

@ -0,0 +1,5 @@
FROM --platform=linux/arm arm32v7/fedora:36 as build
FROM scratch
COPY --from=build /bin/sha256sum /sha256sum
COPY --from=build /bin/sha1sum /sha1sum

View file

@ -0,0 +1,5 @@
FROM --platform=linux/amd64 fedora:41 as build
FROM scratch
COPY --from=build /bin/sha256sum /sha256sum
COPY --from=build /bin/sha1sum /sha1sum