diff --git a/syft/pkg/binary.go b/syft/pkg/binary.go index 70b72a977..7a586cea3 100644 --- a/syft/pkg/binary.go +++ b/syft/pkg/binary.go @@ -23,7 +23,7 @@ type ELFBinaryPackageNoteJSONPayload struct { // 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) + // 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"` // OS is the OS name, typically corresponding to ID in os-release (e.g. "fedora") diff --git a/syft/pkg/cataloger/binary/elf_package.go b/syft/pkg/cataloger/binary/elf_package.go index 81e4384d1..3f0f88fa9 100644 --- a/syft/pkg/cataloger/binary/elf_package.go +++ b/syft/pkg/cataloger/binary/elf_package.go @@ -3,18 +3,47 @@ package binary import ( "github.com/anchore/packageurl-go" "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "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{ Name: metadata.Name, Version: metadata.Version, Licenses: pkg.NewLicenseSet(pkg.NewLicense(metadata.License)), PURL: packageURL(metadata), Type: pkgType(metadata.Type), + CPEs: cpes, Locations: locations, Metadata: metadata.ELFBinaryPackageNoteJSONPayload, } @@ -24,7 +53,7 @@ func newELFPackage(metadata elfBinaryPackageNotes, locations file.LocationSet) p return p } -func packageURL(metadata elfBinaryPackageNotes) string { +func packageURL(metadata elfPackageCore) string { var qualifiers []packageurl.Qualifier os, osVersion := osNameAndVersionFromMetadata(metadata) @@ -58,7 +87,7 @@ func packageURL(metadata elfBinaryPackageNotes) string { ).ToString() } -func osNameAndVersionFromMetadata(metadata elfBinaryPackageNotes) (string, string) { +func osNameAndVersionFromMetadata(metadata elfPackageCore) (string, string) { os := metadata.OS osVersion := metadata.OSVersion diff --git a/syft/pkg/cataloger/binary/elf_package_cataloger.go b/syft/pkg/cataloger/binary/elf_package_cataloger.go index 24b4c0b08..379b66834 100644 --- a/syft/pkg/cataloger/binary/elf_package_cataloger.go +++ b/syft/pkg/cataloger/binary/elf_package_cataloger.go @@ -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 type elfBinaryPackageNotes struct { - Name string `json:"name"` - Version string `json:"version"` - PURL string `json:"purl"` - CPE string `json:"cpe"` + elfPackageCore `json:",inline"` + Dependencies []elfPackageCore `json:"dependencies"` +} + +type elfPackageCore struct { + elfPackageKey `json:",inline"` License string `json:"license"` pkg.ELFBinaryPackageNoteJSONPayload `json:",inline"` Location file.Location `json:"-"` } type elfPackageKey struct { - Name string - Version string - PURL string - CPE string + Name string `json:"name"` + Version string `json:"version"` + PURL string `json:"purl"` + CPE string `json:"cpe"` } 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 // which means the set of binaries collectively represent a single logical package. var pkgs []pkg.Package + var relationships []artifact.Relationship for _, notes := range notesByLocation { noteLocations := file.NewLocationSet() 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) - 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 // 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. - return pkgs, nil, errs + return pkgs, relationships, errs } 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 - key := elfPackageKey{ - Name: notes.Name, - Version: notes.Version, - PURL: notes.PURL, - CPE: notes.CPE, - } - return notes, key, nil + return notes, notes.elfPackageKey, nil } func (c *elfPackageCataloger) parseElfNotes(reader file.LocationReadCloser) (*elfBinaryPackageNotes, error) { diff --git a/syft/pkg/cataloger/binary/elf_package_cataloger_test.go b/syft/pkg/cataloger/binary/elf_package_cataloger_test.go index 0325b3083..e7263635c 100644 --- a/syft/pkg/cataloger/binary/elf_package_cataloger_test.go +++ b/syft/pkg/cataloger/binary/elf_package_cataloger_test.go @@ -5,13 +5,13 @@ import ( "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" ) func Test_ELF_Package_Cataloger(t *testing.T) { - cases := []struct { name string fixture string @@ -34,7 +34,7 @@ func Test_ELF_Package_Cataloger(t *testing.T) { Licenses: pkg.NewLicenseSet( 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, Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Type: "testfixture", @@ -56,6 +56,7 @@ func Test_ELF_Package_Cataloger(t *testing.T) { Licenses: pkg.NewLicenseSet( 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, Metadata: pkg.ELFBinaryPackageNoteJSONPayload{ Type: "testfixture", diff --git a/syft/pkg/cataloger/binary/elf_package_test.go b/syft/pkg/cataloger/binary/elf_package_test.go index 689e259d1..4600129ee 100644 --- a/syft/pkg/cataloger/binary/elf_package_test.go +++ b/syft/pkg/cataloger/binary/elf_package_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" ) @@ -20,10 +21,14 @@ func Test_packageURL(t *testing.T) { { name: "elf-binary-package-cataloger", metadata: elfBinaryPackageNotes{ - Name: "github.com/anchore/syft", - Version: "v0.1.0", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - System: "syftsys", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "github.com/anchore/syft", + Version: "v0.1.0", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + System: "syftsys", + }, }, }, want: "pkg:generic/syftsys/github.com/anchore/syft@v0.1.0", @@ -31,10 +36,14 @@ func Test_packageURL(t *testing.T) { { name: "elf binary package short name", metadata: elfBinaryPackageNotes{ - Name: "go.opencensus.io", - Version: "v0.23.0", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - System: "syftsys", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "go.opencensus.io", + Version: "v0.23.0", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + System: "syftsys", + }, }, }, want: "pkg:generic/syftsys/go.opencensus.io@v0.23.0", @@ -42,10 +51,14 @@ func Test_packageURL(t *testing.T) { { name: "no info", metadata: elfBinaryPackageNotes{ - Name: "test", - Version: "1.0", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - Type: "rpm", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "test", + Version: "1.0", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + Type: "rpm", + }, }, }, want: "pkg:rpm/test@1.0", @@ -53,11 +66,15 @@ func Test_packageURL(t *testing.T) { { name: "with system", metadata: elfBinaryPackageNotes{ - Name: "test", - Version: "1.0", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - Type: "rpm", - System: "system", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "test", + Version: "1.0", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + Type: "rpm", + System: "system", + }, }, }, want: "pkg:rpm/system/test@1.0", @@ -65,13 +82,17 @@ func Test_packageURL(t *testing.T) { { 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", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + 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", @@ -79,12 +100,16 @@ func Test_packageURL(t *testing.T) { { 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", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + 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", @@ -92,12 +117,16 @@ func Test_packageURL(t *testing.T) { { 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", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + 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", @@ -105,10 +134,14 @@ func Test_packageURL(t *testing.T) { { name: "missing type", metadata: elfBinaryPackageNotes{ - Name: "test", - Version: "1.0", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - System: "system", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "test", + Version: "1.0", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + System: "system", + }, }, }, want: "pkg:generic/system/test@1.0", @@ -116,11 +149,15 @@ func Test_packageURL(t *testing.T) { { name: "bad or missing OSCPE data cannot be parsed allows for correct string", metadata: elfBinaryPackageNotes{ - Name: "test", - Version: "1.0", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - System: "system", - OSCPE: "%$#*(#*@&$(", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "test", + Version: "1.0", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + System: "system", + OSCPE: "%$#*(#*@&$(", + }, }, }, want: "pkg:generic/system/test@1.0", @@ -129,7 +166,7 @@ func Test_packageURL(t *testing.T) { for _, test := range tests { 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", metadata: elfBinaryPackageNotes{ - Name: "syfttestfixture", - Version: "0.01", - PURL: "pkg:generic/syftsys/syfttestfixture@0.01", - CPE: "cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01", - ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ - Type: "binary", - System: "syftsys", + elfPackageCore: elfPackageCore{ + elfPackageKey: elfPackageKey{ + Name: "syfttestfixture", + Version: "0.01", + PURL: "pkg:generic/syftsys/syfttestfixture@0.01", + CPE: "cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01", + }, + ELFBinaryPackageNoteJSONPayload: pkg.ELFBinaryPackageNoteJSONPayload{ + Type: "binary", + System: "syftsys", + }, }, }, - expected: pkg.Package{ Name: "syfttestfixture", Version: "0.01", Type: "binary", 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{ Type: "binary", System: "syftsys", @@ -168,7 +209,7 @@ func Test_newELFPackage(t *testing.T) { for _, test := range tests { 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 != "" { t.Errorf("newELFPackage() mismatch (-want +got):\n%s", diff) }