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 ( const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // 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. // 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", "$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", "$ref": "#/$defs/Document",
"$defs": { "$defs": {
"AlpmDbEntry": { "AlpmDbEntry": {
@ -647,12 +647,24 @@
"type": { "type": {
"type": "string" "type": "string"
}, },
"vendor": { "architecture": {
"type": "string"
},
"osCPE": {
"type": "string"
},
"os": {
"type": "string"
},
"osVersion": {
"type": "string" "type": "string"
}, },
"system": { "system": {
"type": "string" "type": "string"
}, },
"vendor": {
"type": "string"
},
"sourceRepo": { "sourceRepo": {
"type": "string" "type": "string"
}, },

View File

@ -15,9 +15,34 @@ type ClassifierMatch struct {
// ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of the binary // ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of the binary
type ELFBinaryPackageNoteJSONPayload struct { type ELFBinaryPackageNoteJSONPayload struct {
// 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"` Type string `json:"type,omitempty"`
Vendor string `json:"vendor,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"` 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"` SourceRepo string `json:"sourceRepo,omitempty"`
// Commit is the commit hash of the source repository for which the binary was built from
Commit string `json:"commit,omitempty"` Commit string `json:"commit,omitempty"`
} }

View File

@ -2,6 +2,7 @@ package binary
import ( import (
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
@ -23,13 +24,59 @@ func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) p
} }
func packageURL(metadata elfBinaryPackageNotes) string { 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( return packageurl.NewPackageURL(
packageurl.TypeGeneric, ty,
metadata.System, namespace,
metadata.Name, metadata.Name,
metadata.Version, metadata.Version,
nil, qualifiers,
"", "",
).ToString() ).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 package binary
import ( import (
"bytes"
"context" "context"
"debug/elf" "debug/elf"
"encoding/binary"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -154,11 +156,42 @@ func getELFNotes(r file.LocationReadCloser) (*elfBinaryPackageNotes, error) {
return nil, err return nil, err
} }
var metadata elfBinaryPackageNotes if len(notes) == 0 {
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")
return nil, nil 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,12 +9,20 @@ import (
) )
func Test_ELF_Package_Cataloger(t *testing.T) { func Test_ELF_Package_Cataloger(t *testing.T) {
expectedPkgs := []pkg.Package{
cases := []struct {
name string
fixture string
expected []pkg.Package
}{
{
name: "go case",
fixture: "elf-test-fixtures",
expected: []pkg.Package{
{ {
Name: "libhello_world.so", Name: "libhello_world.so",
Version: "0.01", Version: "0.01",
PURL: "pkg:generic/syftsys/libhello_world.so@0.01", PURL: "pkg:generic/syftsys/libhello_world.so@0.01",
FoundBy: "",
Locations: file.NewLocationSet( 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/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_world.so", "/usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so"),
@ -24,7 +32,6 @@ func Test_ELF_Package_Cataloger(t *testing.T) {
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"}, pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
), ),
Language: "",
Type: pkg.BinaryPkg, Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture", Type: "testfixture",
@ -38,7 +45,6 @@ func Test_ELF_Package_Cataloger(t *testing.T) {
Name: "syfttestfixture", Name: "syfttestfixture",
Version: "0.01", Version: "0.01",
PURL: "pkg:generic/syftsys/syfttestfixture@0.01", PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
FoundBy: "",
Locations: file.NewLocationSet( Locations: file.NewLocationSet(
file.NewLocation("/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), 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/elfwithparallellibbin1").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
@ -47,7 +53,6 @@ func Test_ELF_Package_Cataloger(t *testing.T) {
Licenses: pkg.NewLicenseSet( Licenses: pkg.NewLicenseSet(
pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"}, pkg.License{Value: "MIT", SPDXExpression: "MIT", Type: "declared"},
), ),
Language: "",
Type: pkg.BinaryPkg, Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture", Type: "testfixture",
@ -57,12 +62,62 @@ func Test_ELF_Package_Cataloger(t *testing.T) {
Commit: "5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb", 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",
},
},
},
},
} }
for _, v := range cases {
t.Run(v.name, func(t *testing.T) {
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
WithImageResolver(t, "elf-test-fixtures"). WithImageResolver(t, v.fixture).
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
Expects(expectedPkgs, nil). Expects(v.expected, nil).
TestCataloger(t, NewELFPackageCataloger()) TestCataloger(t, NewELFPackageCataloger())
})
}
} }

View File

@ -14,36 +14,110 @@ import (
func Test_packageURL(t *testing.T) { func Test_packageURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
notes elfBinaryPackageNotes metadata elfBinaryPackageNotes
expected string want string
}{ }{
{ {
name: "elf-binary-package-cataloger", name: "elf-binary-package-cataloger",
notes: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
Name: "github.com/anchore/syft", Name: "github.com/anchore/syft",
Version: "v0.1.0", Version: "v0.1.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "syftsys", 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", name: "elf binary package short name",
notes: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
Name: "go.opencensus.io", Name: "go.opencensus.io",
Version: "v0.23.0", Version: "v0.23.0",
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "syftsys", 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 { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { 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