[wip] add elf note dependencies

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-01-17 22:16:32 -05:00
parent 254a915592
commit bd131d78f1
5 changed files with 148 additions and 78 deletions

View File

@ -23,7 +23,7 @@ type ELFBinaryPackageNoteJSONPayload struct {
// Architecture of the binary package (e.g. "amd64", "arm", etc.) // Architecture of the binary package (e.g. "amd64", "arm", etc.)
Architecture string `json:"architecture,omitempty"` 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) // OS CPE is the common platform enumeration for the OS environment for the package, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)
OSCPE string `json:"osCPE,omitempty"` OSCPE string `json:"osCPE,omitempty"`
// OS is the OS name, typically corresponding to ID in os-release (e.g. "fedora") // OS is the OS name, typically corresponding to ID in os-release (e.g. "fedora")

View File

@ -3,18 +3,47 @@ package binary
import ( import (
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe" "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"
) )
func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) pkg.Package { func newELFPackages(metadata elfBinaryPackageNotes, locations file.LocationSet) ([]pkg.Package, []artifact.Relationship) {
parentPkg := newELFPackage(metadata.elfPackageCore, locations)
pkgs := []pkg.Package{parentPkg}
var relationships []artifact.Relationship
for _, depMetadata := range metadata.Dependencies {
dep := newELFPackage(depMetadata, locations)
pkgs = append(pkgs, dep)
relationships = append(relationships, artifact.Relationship{
From: dep,
To: parentPkg,
Type: artifact.DependencyOfRelationship,
})
}
return pkgs, relationships
}
func newELFPackage(metadata elfPackageCore, locations file.LocationSet) pkg.Package {
var cpes []cpe.CPE
if metadata.CPE != "" {
c, err := cpe.New(metadata.CPE, cpe.DeclaredSource)
if err != nil {
log.WithFields("error", err, "cpe", metadata.CPE).Trace("unable to parse cpe for elf binary package")
} else {
cpes = append(cpes, c)
}
}
p := pkg.Package{ p := pkg.Package{
Name: metadata.Name, Name: metadata.Name,
Version: metadata.Version, Version: metadata.Version,
Licenses: pkg.NewLicenseSet(pkg.NewLicense(metadata.License)), Licenses: pkg.NewLicenseSet(pkg.NewLicense(metadata.License)),
PURL: packageURL(metadata), PURL: packageURL(metadata),
Type: pkgType(metadata.Type), Type: pkgType(metadata.Type),
CPEs: cpes,
Locations: locations, Locations: locations,
Metadata: metadata.ELFBinaryPackageNoteJSONPayload, Metadata: metadata.ELFBinaryPackageNoteJSONPayload,
} }
@ -24,7 +53,7 @@ func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) p
return p return p
} }
func packageURL(metadata elfBinaryPackageNotes) string { func packageURL(metadata elfPackageCore) string {
var qualifiers []packageurl.Qualifier var qualifiers []packageurl.Qualifier
os, osVersion := osNameAndVersionFromMetadata(metadata) os, osVersion := osNameAndVersionFromMetadata(metadata)
@ -58,7 +87,7 @@ func packageURL(metadata elfBinaryPackageNotes) string {
).ToString() ).ToString()
} }
func osNameAndVersionFromMetadata(metadata elfBinaryPackageNotes) (string, string) { func osNameAndVersionFromMetadata(metadata elfPackageCore) (string, string) {
os := metadata.OS os := metadata.OS
osVersion := metadata.OSVersion osVersion := metadata.OSVersion

View File

@ -28,20 +28,22 @@ type elfPackageCataloger struct {
// For example, fedora includes an ELF section header as a prefix to the JSON payload: https://github.com/anchore/syft/issues/2713 // For example, fedora includes an ELF section header as a prefix to the JSON payload: https://github.com/anchore/syft/issues/2713
type elfBinaryPackageNotes struct { type elfBinaryPackageNotes struct {
Name string `json:"name"` elfPackageCore `json:",inline"`
Version string `json:"version"` Dependencies []elfPackageCore `json:"dependencies"`
PURL string `json:"purl"` }
CPE string `json:"cpe"`
type elfPackageCore struct {
elfPackageKey `json:",inline"`
License string `json:"license"` License string `json:"license"`
pkg.ELFBinaryPackageNoteJSONPayload `json:",inline"` pkg.ELFBinaryPackageNoteJSONPayload `json:",inline"`
Location file.Location `json:"-"` Location file.Location `json:"-"`
} }
type elfPackageKey struct { type elfPackageKey struct {
Name string Name string `json:"name"`
Version string Version string `json:"version"`
PURL string PURL string `json:"purl"`
CPE string CPE string `json:"cpe"`
} }
func NewELFPackageCataloger() pkg.Cataloger { func NewELFPackageCataloger() pkg.Cataloger {
@ -77,6 +79,7 @@ func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver)
// we do this in a second pass since it is possible that we have multiple ELF binaries with the same name and version // we do this in a second pass since it is possible that we have multiple ELF binaries with the same name and version
// which means the set of binaries collectively represent a single logical package. // which means the set of binaries collectively represent a single logical package.
var pkgs []pkg.Package var pkgs []pkg.Package
var relationships []artifact.Relationship
for _, notes := range notesByLocation { for _, notes := range notesByLocation {
noteLocations := file.NewLocationSet() noteLocations := file.NewLocationSet()
for _, note := range notes { for _, note := range notes {
@ -84,13 +87,15 @@ func (c *elfPackageCataloger) Catalog(_ context.Context, resolver file.Resolver)
} }
// create a package for each unique name/version pair (based on the first note found) // create a package for each unique name/version pair (based on the first note found)
pkgs = append(pkgs, newELFPackage(notes[0], noteLocations)) ps, rs := newELFPackages(notes[0], noteLocations)
pkgs = append(pkgs, ps...)
relationships = append(relationships, rs...)
} }
// why not return relationships? We have an executable cataloger that will note the dynamic libraries imported by // why not return relationships? We have an executable cataloger that will note the dynamic libraries imported by
// each binary. After all files and packages are processed there is a final task that creates package-to-package // each binary. After all files and packages are processed there is a final task that creates package-to-package
// and package-to-file relationships based on the dynamic libraries imported by each binary. // and package-to-file relationships based on the dynamic libraries imported by each binary.
return pkgs, nil, errs return pkgs, relationships, errs
} }
func parseElfPackageNotes(resolver file.Resolver, location file.Location, c *elfPackageCataloger) (*elfBinaryPackageNotes, elfPackageKey, error) { func parseElfPackageNotes(resolver file.Resolver, location file.Location, c *elfPackageCataloger) (*elfBinaryPackageNotes, elfPackageKey, error) {
@ -115,13 +120,7 @@ func parseElfPackageNotes(resolver file.Resolver, location file.Location, c *elf
} }
notes.Location = location notes.Location = location
key := elfPackageKey{ return notes, notes.elfPackageKey, nil
Name: notes.Name,
Version: notes.Version,
PURL: notes.PURL,
CPE: notes.CPE,
}
return notes, key, nil
} }
func (c *elfPackageCataloger) parseElfNotes(reader file.LocationReadCloser) (*elfBinaryPackageNotes, error) { func (c *elfPackageCataloger) parseElfNotes(reader file.LocationReadCloser) (*elfBinaryPackageNotes, error) {

View File

@ -5,13 +5,13 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"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"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
) )
func Test_ELF_Package_Cataloger(t *testing.T) { func Test_ELF_Package_Cataloger(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
fixture string fixture string
@ -34,7 +34,7 @@ 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"},
), ),
CPEs: []cpe.CPE{cpe.Must("cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01", cpe.DeclaredSource)},
Type: pkg.BinaryPkg, Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture", Type: "testfixture",
@ -56,6 +56,7 @@ 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"},
), ),
CPEs: []cpe.CPE{cpe.Must("cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01", cpe.DeclaredSource)},
Type: pkg.BinaryPkg, Type: pkg.BinaryPkg,
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "testfixture", Type: "testfixture",

View File

@ -7,6 +7,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"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"
) )
@ -20,53 +21,72 @@ func Test_packageURL(t *testing.T) {
{ {
name: "elf-binary-package-cataloger", name: "elf-binary-package-cataloger",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
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",
}, },
}, },
},
want: "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",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
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",
}, },
}, },
},
want: "pkg:generic/syftsys/go.opencensus.io@v0.23.0", want: "pkg:generic/syftsys/go.opencensus.io@v0.23.0",
}, },
{ {
name: "no info", name: "no info",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm", Type: "rpm",
}, },
}, },
},
want: "pkg:rpm/test@1.0", want: "pkg:rpm/test@1.0",
}, },
{ {
name: "with system", name: "with system",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm", Type: "rpm",
System: "system", System: "system",
}, },
}, },
},
want: "pkg:rpm/system/test@1.0", want: "pkg:rpm/system/test@1.0",
}, },
{ {
name: "with os info preferred", name: "with os info preferred",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm", Type: "rpm",
OS: "fedora", OS: "fedora",
@ -74,62 +94,79 @@ func Test_packageURL(t *testing.T) {
OSCPE: "cpe:/o:someone:redhat:3.0", OSCPE: "cpe:/o:someone:redhat:3.0",
}, },
}, },
},
want: "pkg:rpm/fedora/test@1.0?distro=fedora-2.0", want: "pkg:rpm/fedora/test@1.0?distro=fedora-2.0",
}, },
{ {
name: "with os info fallback to CPE parsing (missing version)", name: "with os info fallback to CPE parsing (missing version)",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm", Type: "rpm",
OS: "fedora", OS: "fedora",
OSCPE: "cpe:/o:someone:redhat:3.0", OSCPE: "cpe:/o:someone:redhat:3.0",
}, },
}, },
},
want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0", want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0",
}, },
{ {
name: "with os info preferred (missing OS)", name: "with os info preferred (missing OS)",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "rpm", Type: "rpm",
OSVersion: "2.0", OSVersion: "2.0",
OSCPE: "cpe:/o:someone:redhat:3.0", OSCPE: "cpe:/o:someone:redhat:3.0",
}, },
}, },
},
want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0", want: "pkg:rpm/redhat/test@1.0?distro=redhat-3.0",
}, },
{ {
name: "missing type", name: "missing type",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "system", System: "system",
}, },
}, },
},
want: "pkg:generic/system/test@1.0", want: "pkg:generic/system/test@1.0",
}, },
{ {
name: "bad or missing OSCPE data cannot be parsed allows for correct string", name: "bad or missing OSCPE data cannot be parsed allows for correct string",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "test", Name: "test",
Version: "1.0", Version: "1.0",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
System: "system", System: "system",
OSCPE: "%$#*(#*@&$(", OSCPE: "%$#*(#*@&$(",
}, },
}, },
},
want: "pkg:generic/system/test@1.0", 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.want, packageURL(test.metadata)) assert.Equal(t, test.want, packageURL(test.metadata.elfPackageCore))
}) })
} }
} }
@ -143,21 +180,25 @@ func Test_newELFPackage(t *testing.T) {
{ {
name: "elf-binary-package-cataloger", name: "elf-binary-package-cataloger",
metadata: elfBinaryPackageNotes{ metadata: elfBinaryPackageNotes{
elfPackageCore: elfPackageCore{
elfPackageKey: elfPackageKey{
Name: "syfttestfixture", Name: "syfttestfixture",
Version: "0.01", Version: "0.01",
PURL: "pkg:generic/syftsys/syfttestfixture@0.01", PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
CPE: "cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01", CPE: "cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01",
},
ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "binary", Type: "binary",
System: "syftsys", System: "syftsys",
}, },
}, },
},
expected: pkg.Package{ expected: pkg.Package{
Name: "syfttestfixture", Name: "syfttestfixture",
Version: "0.01", Version: "0.01",
Type: "binary", Type: "binary",
PURL: "pkg:generic/syftsys/syfttestfixture@0.01", PURL: "pkg:generic/syftsys/syfttestfixture@0.01",
CPEs: []cpe.CPE{cpe.Must("cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01", cpe.DeclaredSource)},
Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Metadata: pkg.ELFBinaryPackageNoteJSONPayload{
Type: "binary", Type: "binary",
System: "syftsys", System: "syftsys",
@ -168,7 +209,7 @@ func Test_newELFPackage(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
actual := newELFPackage(test.metadata, file.NewLocationSet()) actual := newELFPackage(test.metadata.elfPackageCore, file.NewLocationSet())
if diff := cmp.Diff(test.expected, actual, cmpopts.IgnoreFields(pkg.Package{}, "id"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{})); diff != "" { if diff := cmp.Diff(test.expected, actual, cmpopts.IgnoreFields(pkg.Package{}, "id"), cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{})); diff != "" {
t.Errorf("newELFPackage() mismatch (-want +got):\n%s", diff) t.Errorf("newELFPackage() mismatch (-want +got):\n%s", diff)
} }