From cd68818281b7d307d5d4742166b5c579304bcb4d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Thu, 12 Sep 2024 17:36:53 -0400 Subject: [PATCH] add dpkg evidence support Signed-off-by: Alex Goodman --- internal/relationship/evident_by.go | 11 ++++--- internal/task/file_tasks.go | 16 ++++++--- syft/pkg/cataloger/debian/cataloger_test.go | 28 ++++++++-------- syft/pkg/cataloger/debian/package.go | 18 ++++++---- syft/pkg/cataloger/debian/parse_dpkg_db.go | 33 ++++++++++++++++++- .../lib/dpkg/status.d/libsqlite3-0.preinst | 1 + .../var/lib/dpkg/info/libpam-runtime.preinst | 1 + syft/pkg/cataloger/ocaml/parse_opam_test.go | 3 +- 8 files changed, 81 insertions(+), 30 deletions(-) create mode 100644 syft/pkg/cataloger/debian/test-fixtures/image-distroless-deb/var/lib/dpkg/status.d/libsqlite3-0.preinst create mode 100644 syft/pkg/cataloger/debian/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.preinst diff --git a/internal/relationship/evident_by.go b/internal/relationship/evident_by.go index 4050e96fdab..ba4191325a8 100644 --- a/internal/relationship/evident_by.go +++ b/internal/relationship/evident_by.go @@ -9,15 +9,18 @@ func EvidentBy(catalog *pkg.Collection) []artifact.Relationship { var edges []artifact.Relationship for _, p := range catalog.Sorted() { for _, l := range p.Locations.ToSlice() { - if v, exists := l.Annotations[pkg.EvidenceAnnotationKey]; !exists || v != pkg.PrimaryEvidenceAnnotation { - // skip non-primary evidence from being expressed as a relationship. - // note: this may be configurable in the future. - continue + kind := pkg.SupportingEvidenceAnnotation + if v, exists := l.Annotations[pkg.EvidenceAnnotationKey]; exists { + kind = v } + edges = append(edges, artifact.Relationship{ From: p, To: l.Coordinates, Type: artifact.EvidentByRelationship, + Data: map[string]string{ + "kind": kind, + }, }) } } diff --git a/internal/task/file_tasks.go b/internal/task/file_tasks.go index 857f6e4238e..5369db32720 100644 --- a/internal/task/file_tasks.go +++ b/internal/task/file_tasks.go @@ -134,9 +134,10 @@ func coordinatesForSelection(selection file.Selection, accessor sbomsync.Accesso } if selection == file.FilesOwnedByPackageSelection { - var coordinates []file.Coordinates + var coordinates file.CoordinateSet accessor.ReadFromSBOM(func(sbom *sbom.SBOM) { + // get any file coordinates that are owned by a package for _, r := range sbom.Relationships { if r.Type != artifact.ContainsRelationship { continue @@ -145,16 +146,23 @@ func coordinatesForSelection(selection file.Selection, accessor sbomsync.Accesso continue } if c, ok := r.To.(file.Coordinates); ok { - coordinates = append(coordinates, c) + coordinates.Add(c) } } + + // get any file coordinates referenced by a package directly + for p := range sbom.Artifacts.Packages.Enumerate() { + coordinates.Add(p.Locations.CoordinateSet().ToSlice()...) + } }) - if len(coordinates) == 0 { + coords := coordinates.ToSlice() + + if len(coords) == 0 { return nil, false } - return coordinates, true + return coords, true } return nil, false diff --git a/syft/pkg/cataloger/debian/cataloger_test.go b/syft/pkg/cataloger/debian/cataloger_test.go index 0f8a70a9af4..629019954fe 100644 --- a/syft/pkg/cataloger/debian/cataloger_test.go +++ b/syft/pkg/cataloger/debian/cataloger_test.go @@ -25,15 +25,16 @@ func TestDpkgCataloger(t *testing.T) { Version: "1.1.8-3.6", FoundBy: "dpkg-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("GPL-1", file.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright")), - pkg.NewLicenseFromLocations("GPL-2", file.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright")), - pkg.NewLicenseFromLocations("LGPL-2.1", file.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright")), + pkg.NewLicenseFromLocations("GPL-1", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), + pkg.NewLicenseFromLocations("GPL-2", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), + pkg.NewLicenseFromLocations("LGPL-2.1", file.NewLocation("/usr/share/doc/libpam-runtime/copyright")), ), Locations: file.NewLocationSet( - file.NewVirtualLocation("/var/lib/dpkg/status", "/var/lib/dpkg/status"), - file.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.md5sums", "/var/lib/dpkg/info/libpam-runtime.md5sums"), - file.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.conffiles", "/var/lib/dpkg/info/libpam-runtime.conffiles"), - file.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright"), + file.NewLocation("/var/lib/dpkg/status"), + file.NewLocation("/var/lib/dpkg/info/libpam-runtime.preinst"), + file.NewLocation("/var/lib/dpkg/info/libpam-runtime.md5sums"), + file.NewLocation("/var/lib/dpkg/info/libpam-runtime.conffiles"), + file.NewLocation("/usr/share/doc/libpam-runtime/copyright"), ), Type: pkg.DebPkg, Metadata: pkg.DpkgDBEntry{ @@ -98,14 +99,15 @@ func TestDpkgCataloger(t *testing.T) { Version: "3.34.1-3", FoundBy: "dpkg-db-cataloger", Licenses: pkg.NewLicenseSet( - pkg.NewLicenseFromLocations("public-domain", file.NewVirtualLocation("/usr/share/doc/libsqlite3-0/copyright", "/usr/share/doc/libsqlite3-0/copyright")), - pkg.NewLicenseFromLocations("GPL-2+", file.NewVirtualLocation("/usr/share/doc/libsqlite3-0/copyright", "/usr/share/doc/libsqlite3-0/copyright")), - pkg.NewLicenseFromLocations("GPL-2", file.NewVirtualLocation("/usr/share/doc/libsqlite3-0/copyright", "/usr/share/doc/libsqlite3-0/copyright")), + pkg.NewLicenseFromLocations("public-domain", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), + pkg.NewLicenseFromLocations("GPL-2+", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), + pkg.NewLicenseFromLocations("GPL-2", file.NewLocation("/usr/share/doc/libsqlite3-0/copyright")), ), Locations: file.NewLocationSet( - file.NewVirtualLocation("/var/lib/dpkg/status.d/libsqlite3-0", "/var/lib/dpkg/status.d/libsqlite3-0"), - file.NewVirtualLocation("/var/lib/dpkg/status.d/libsqlite3-0.md5sums", "/var/lib/dpkg/status.d/libsqlite3-0.md5sums"), - file.NewVirtualLocation("/usr/share/doc/libsqlite3-0/copyright", "/usr/share/doc/libsqlite3-0/copyright"), + file.NewLocation("/var/lib/dpkg/status.d/libsqlite3-0"), + file.NewLocation("/var/lib/dpkg/status.d/libsqlite3-0.md5sums"), + file.NewLocation("/var/lib/dpkg/status.d/libsqlite3-0.preinst"), + file.NewLocation("/usr/share/doc/libsqlite3-0/copyright"), ), Type: pkg.DebPkg, Metadata: pkg.DpkgDBEntry{ diff --git a/syft/pkg/cataloger/debian/package.go b/syft/pkg/cataloger/debian/package.go index d31830e87fd..db93ba8ebd1 100644 --- a/syft/pkg/cataloger/debian/package.go +++ b/syft/pkg/cataloger/debian/package.go @@ -22,14 +22,18 @@ const ( docsPath = "/usr/share/doc" ) -func newDpkgPackage(d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.Resolver, release *linux.Release) pkg.Package { +func newDpkgPackage(d pkg.DpkgDBEntry, dbLocation file.Location, resolver file.Resolver, release *linux.Release, evidence ...file.Location) pkg.Package { // TODO: separate pr to license refactor, but explore extracting dpkg-specific license parsing into a separate function licenses := make([]pkg.License, 0) + + locations := file.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)) + locations.Add(evidence...) + p := pkg.Package{ Name: d.Package, Version: d.Version, Licenses: pkg.NewLicenseSet(licenses...), - Locations: file.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), + Locations: locations, PURL: packageURL(d, release), Type: pkg.DebPkg, Metadata: d, @@ -88,7 +92,7 @@ func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string { func addLicenses(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) { metadata, ok := p.Metadata.(pkg.DpkgDBEntry) if !ok { - log.WithFields("package", p).Warn("unable to extract DPKG metadata to add licenses") + log.WithFields("package", p).Trace("unable to extract DPKG metadata to add licenses") return } @@ -110,7 +114,7 @@ func addLicenses(resolver file.Resolver, dbLocation file.Location, p *pkg.Packag func mergeFileListing(resolver file.Resolver, dbLocation file.Location, p *pkg.Package) { metadata, ok := p.Metadata.(pkg.DpkgDBEntry) if !ok { - log.WithFields("package", p).Warn("unable to extract DPKG metadata to file listing") + log.WithFields("package", p).Trace("unable to extract DPKG metadata to file listing") return } @@ -204,7 +208,7 @@ func fetchMd5Contents(resolver file.Resolver, dbLocation file.Location, m pkg.Dp // this is unexpected, but not a show-stopper md5Reader, err = resolver.FileContentsByLocation(*location) if err != nil { - log.Warnf("failed to fetch deb md5 contents (package=%s): %+v", m.Package, err) + log.Tracef("failed to fetch deb md5 contents (package=%s): %+v", m.Package, err) } l := location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation) @@ -239,7 +243,7 @@ func fetchConffileContents(resolver file.Resolver, dbLocation file.Location, m p // this is unexpected, but not a show-stopper reader, err = resolver.FileContentsByLocation(*location) if err != nil { - log.Warnf("failed to fetch deb conffiles contents (package=%s): %+v", m.Package, err) + log.Tracef("failed to fetch deb conffiles contents (package=%s): %+v", m.Package, err) } l := location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation) @@ -263,7 +267,7 @@ func fetchCopyrightContents(resolver file.Resolver, dbLocation file.Location, m reader, err := resolver.FileContentsByLocation(*location) if err != nil { - log.Warnf("failed to fetch deb copyright contents (package=%s): %s", m.Package, err) + log.Tracef("failed to fetch deb copyright contents (package=%s): %s", m.Package, err) } defer internal.CloseAndLogError(reader, location.RealPath) diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db.go b/syft/pkg/cataloger/debian/parse_dpkg_db.go index 87428d608fb..6c5797c413b 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "io" + "path" + "path/filepath" "regexp" "strings" @@ -34,12 +36,41 @@ func parseDpkgDB(_ context.Context, resolver file.Resolver, env *generic.Environ var pkgs []pkg.Package for _, m := range metadata { - pkgs = append(pkgs, newDpkgPackage(m, reader.Location, resolver, env.LinuxRelease)) + p := newDpkgPackage(m, reader.Location, resolver, env.LinuxRelease, findDpkgInfoFiles(m.Package, resolver, reader.Location)...) + pkgs = append(pkgs, p) } return pkgs, nil, nil } +func findDpkgInfoFiles(name string, resolver file.Resolver, dbLocation file.Location) []file.Location { + if resolver == nil { + return nil + } + if strings.TrimSpace(name) == "" { + return nil + } + + // for typical debian-base distributions, the installed package info is at /var/lib/dpkg/status + // and the md5sum information is under /var/lib/dpkg/info/; however, for distroless the installed + // package info is across multiple files under /var/lib/dpkg/status.d/ and the md5sums are contained in + // the same directory + searchPath := filepath.Dir(dbLocation.RealPath) + + if !strings.HasSuffix(searchPath, "status.d") { + searchPath = path.Join(searchPath, "info") + } + + // look for /var/lib/dpkg/info/NAME.* + locations, err := resolver.FilesByGlob(path.Join(searchPath, name+".*")) + if err != nil { + log.WithFields("error", err, "pkg", name).Trace("failed to fetch related dpkg info files") + return nil + } + + return locations +} + // parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed. func parseDpkgStatus(reader io.Reader) ([]pkg.DpkgDBEntry, error) { buffedReader := bufio.NewReader(reader) diff --git a/syft/pkg/cataloger/debian/test-fixtures/image-distroless-deb/var/lib/dpkg/status.d/libsqlite3-0.preinst b/syft/pkg/cataloger/debian/test-fixtures/image-distroless-deb/var/lib/dpkg/status.d/libsqlite3-0.preinst new file mode 100644 index 00000000000..52b07edd8d3 --- /dev/null +++ b/syft/pkg/cataloger/debian/test-fixtures/image-distroless-deb/var/lib/dpkg/status.d/libsqlite3-0.preinst @@ -0,0 +1 @@ +# some shell script... \ No newline at end of file diff --git a/syft/pkg/cataloger/debian/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.preinst b/syft/pkg/cataloger/debian/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.preinst new file mode 100644 index 00000000000..52b07edd8d3 --- /dev/null +++ b/syft/pkg/cataloger/debian/test-fixtures/image-dpkg/var/lib/dpkg/info/libpam-runtime.preinst @@ -0,0 +1 @@ +# some shell script... \ No newline at end of file diff --git a/syft/pkg/cataloger/ocaml/parse_opam_test.go b/syft/pkg/cataloger/ocaml/parse_opam_test.go index f25563be4d8..63b5fc9d37b 100644 --- a/syft/pkg/cataloger/ocaml/parse_opam_test.go +++ b/syft/pkg/cataloger/ocaml/parse_opam_test.go @@ -3,11 +3,12 @@ package ocaml import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" - "github.com/stretchr/testify/assert" ) func TestParseOpamPackage(t *testing.T) {