diff --git a/syft/pkg/cataloger/rpmdb/cataloger.go b/syft/pkg/cataloger/rpmdb/cataloger.go index 4571e67e4..74fdbfebb 100644 --- a/syft/pkg/cataloger/rpmdb/cataloger.go +++ b/syft/pkg/cataloger/rpmdb/cataloger.go @@ -49,5 +49,27 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti pkgs = append(pkgs, discoveredPkgs...) } + + // Additionally look for RPM manifest files to detect packages in CBL-Mariner distroless images + manifestFileMatches, err := resolver.FilesByGlob(pkg.RpmManifestGlob) + if err != nil { + return nil, nil, fmt.Errorf("failed to find rpm manifests by glob: %w", err) + } + + for _, location := range manifestFileMatches { + reader, err := resolver.FileContentsByLocation(location) + if err != nil { + return nil, nil, err + } + + discoveredPkgs, err := parseRpmManifest(location, reader) + internal.CloseAndLogError(reader, location.VirtualPath) + if err != nil { + return nil, nil, fmt.Errorf("unable to catalog rpm manifest=%+v: %w", location.RealPath, err) + } + + pkgs = append(pkgs, discoveredPkgs...) + } + return pkgs, nil, nil } diff --git a/syft/pkg/cataloger/rpmdb/parse_rpmdb.go b/syft/pkg/cataloger/rpmdb/parse_rpmdb.go index bc3e72f51..613bf22d9 100644 --- a/syft/pkg/cataloger/rpmdb/parse_rpmdb.go +++ b/syft/pkg/cataloger/rpmdb/parse_rpmdb.go @@ -14,7 +14,7 @@ import ( rpmdb "github.com/knqyf263/go-rpmdb/pkg" ) -// parseApkDb parses an "Packages" RPM DB and returns the Packages listed within it. +// parseRpmDb parses an "Packages" RPM DB and returns the Packages listed within it. func parseRpmDB(resolver source.FilePathResolver, dbLocation source.Location, reader io.Reader) ([]pkg.Package, error) { f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb") if err != nil { diff --git a/syft/pkg/cataloger/rpmdb/parse_rpmmanifest.go b/syft/pkg/cataloger/rpmdb/parse_rpmmanifest.go new file mode 100644 index 000000000..5897ba8be --- /dev/null +++ b/syft/pkg/cataloger/rpmdb/parse_rpmmanifest.go @@ -0,0 +1,104 @@ +package rpmdb + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" + + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" +) + +// Parses an entry in an RPM manifest file as used in Mariner distroless containers +// Each line is the output of : +// rpm --query --all --query-format "%{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t%{EPOCH}\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}\n" +// https://github.com/microsoft/CBL-Mariner/blob/3df18fac373aba13a54bd02466e64969574f13af/toolkit/docs/how_it_works/5_misc.md?plain=1#L150 +func parseRpmManifestEntry(entry string, location source.Location) (*pkg.Package, error) { + parts := strings.Split(entry, "\t") + if len(parts) < 10 { + return nil, fmt.Errorf("unexpected number of fields in line: %s", entry) + } + + versionParts := strings.Split(parts[1], "-") + if len(versionParts) != 2 { + return nil, fmt.Errorf("unexpected version field: %s", parts[1]) + } + version := versionParts[0] + release := versionParts[1] + + converted, err := strconv.Atoi(parts[8]) + var epoch *int + if err != nil || parts[5] == "(none)" { + epoch = nil + } else { + epoch = &converted + } + + converted, err = strconv.Atoi(parts[6]) + var size int + if err == nil { + size = converted + } + + metadata := pkg.RpmdbMetadata{ + Name: parts[0], + Version: version, + Epoch: epoch, + Arch: parts[7], + Release: release, + SourceRpm: parts[9], + Vendor: parts[4], + Size: size, + } + + p := pkg.Package{ + Name: parts[0], + Version: toELVersion(metadata), + Locations: source.NewLocationSet(location), + FoundBy: catalogerName, + Type: pkg.RpmPkg, + MetadataType: pkg.RpmdbMetadataType, + Metadata: metadata, + } + + p.SetID() + return &p, nil +} + +// Parses an RPM manifest file, as used in Mariner distroless containers, and returns the Packages listed +func parseRpmManifest(dbLocation source.Location, reader io.Reader) ([]pkg.Package, error) { + r := bufio.NewReader(reader) + allPkgs := make([]pkg.Package, 0) + + for { + line, err := r.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + if line == "" { + continue + } + + p, err := parseRpmManifestEntry(strings.TrimSuffix(line, "\n"), dbLocation) + if err != nil { + log.Warnf("unable to parse RPM manifest entry: %w", err) + continue + } + + if !pkg.IsValid(p) { + continue + } + + p.SetID() + allPkgs = append(allPkgs, *p) + } + + return allPkgs, nil +} diff --git a/syft/pkg/cataloger/rpmdb/parse_rpmmanifest_test.go b/syft/pkg/cataloger/rpmdb/parse_rpmmanifest_test.go new file mode 100644 index 000000000..13ca05abd --- /dev/null +++ b/syft/pkg/cataloger/rpmdb/parse_rpmmanifest_test.go @@ -0,0 +1,117 @@ +package rpmdb + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" + "github.com/go-test/deep" +) + +func TestParseRpmManifest(t *testing.T) { + location := source.NewLocation("test-path") + + fixture_path := "test-fixtures/container-manifest-2" + expected := map[string]pkg.Package{ + "mariner-release": { + Name: "mariner-release", + Version: "2.0-12.cm2", + Locations: source.NewLocationSet(location), + FoundBy: catalogerName, + Type: pkg.RpmPkg, + MetadataType: pkg.RpmdbMetadataType, + Metadata: pkg.RpmdbMetadata{ + Name: "mariner-release", + Epoch: nil, + Arch: "noarch", + Release: "12.cm2", + Version: "2.0", + SourceRpm: "mariner-release-2.0-12.cm2.src.rpm", + Size: 580, + Vendor: "Microsoft Corporation", + }, + }, + "filesystem": { + Name: "filesystem", + Version: "1.1-9.cm2", + Locations: source.NewLocationSet(location), + FoundBy: catalogerName, + Type: pkg.RpmPkg, + MetadataType: pkg.RpmdbMetadataType, + Metadata: pkg.RpmdbMetadata{ + Name: "filesystem", + Epoch: nil, + Arch: "x86_64", + Release: "9.cm2", + Version: "1.1", + SourceRpm: "filesystem-1.1-9.cm2.src.rpm", + Size: 7596, + Vendor: "Microsoft Corporation", + }, + }, + "glibc": { + Name: "glibc", + Version: "2.35-2.cm2", + Locations: source.NewLocationSet(location), + FoundBy: catalogerName, + Type: pkg.RpmPkg, + MetadataType: pkg.RpmdbMetadataType, + Metadata: pkg.RpmdbMetadata{ + Name: "glibc", + Epoch: nil, + Arch: "x86_64", + Release: "2.cm2", + Version: "2.35", + SourceRpm: "glibc-2.35-2.cm2.src.rpm", + Size: 10855265, + Vendor: "Microsoft Corporation", + }, + }, + "openssl-libs": { + Name: "openssl-libs", + Version: "1.1.1k-15.cm2", + Locations: source.NewLocationSet(location), + FoundBy: catalogerName, + Type: pkg.RpmPkg, + MetadataType: pkg.RpmdbMetadataType, + Metadata: pkg.RpmdbMetadata{ + Name: "openssl-libs", + Epoch: nil, + Arch: "x86_64", + Release: "15.cm2", + Version: "1.1.1k", + SourceRpm: "openssl-1.1.1k-15.cm2.src.rpm", + Size: 4365048, + Vendor: "Microsoft Corporation", + }, + }, + } + + fixture, err := os.Open(fixture_path) + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + actual, err := parseRpmManifest(location, fixture) + if err != nil { + t.Fatalf("failed to parse rpm manifest: %+v", err) + } + + if len(actual) != 12 { + for _, a := range actual { + t.Log(" ", a) + } + t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expected)) + } + + for _, a := range actual[0:4] { + e := expected[a.Name] + diffs := deep.Equal(a, e) + if len(diffs) > 0 { + for _, d := range diffs { + t.Errorf("diff: %+v", d) + } + } + } +} diff --git a/syft/pkg/cataloger/rpmdb/test-fixtures/container-manifest-2 b/syft/pkg/cataloger/rpmdb/test-fixtures/container-manifest-2 new file mode 100644 index 000000000..576e121ac --- /dev/null +++ b/syft/pkg/cataloger/rpmdb/test-fixtures/container-manifest-2 @@ -0,0 +1,12 @@ +mariner-release 2.0-12.cm2 1653816591 1653753130 Microsoft Corporation (none) 580 noarch 0 mariner-release-2.0-12.cm2.src.rpm +filesystem 1.1-9.cm2 1653816591 1653628924 Microsoft Corporation (none) 7596 x86_64 0 filesystem-1.1-9.cm2.src.rpm +glibc 2.35-2.cm2 1653816591 1653628955 Microsoft Corporation (none) 10855265 x86_64 0 glibc-2.35-2.cm2.src.rpm +openssl-libs 1.1.1k-15.cm2 1653816591 1653631609 Microsoft Corporation (none) 4365048 x86_64 0 openssl-1.1.1k-15.cm2.src.rpm +libgcc 11.2.0-2.cm2 1653816591 1650702349 Microsoft Corporation (none) 103960 x86_64 0 gcc-11.2.0-2.cm2.src.rpm +openssl 1.1.1k-15.cm2 1653816591 1653631609 Microsoft Corporation (none) 1286337 x86_64 0 openssl-1.1.1k-15.cm2.src.rpm +glibc-iconv 2.35-2.cm2 1653816591 1653628955 Microsoft Corporation (none) 8397230 x86_64 0 glibc-2.35-2.cm2.src.rpm +iana-etc 20211115-1.cm2 1653816591 1650711959 Microsoft Corporation (none) 4380680 noarch 0 iana-etc-20211115-1.cm2.src.rpm +tzdata 2022a-1.cm2 1653816591 1653752882 Microsoft Corporation (none) 1535764 noarch 0 tzdata-2022a-1.cm2.src.rpm +prebuilt-ca-certificates-base 2.0.0-3.cm2 1653816591 1653771776 Microsoft Corporation (none) 65684 noarch 1 prebuilt-ca-certificates-base-2.0.0-3.cm2.src.rpm +distroless-packages-minimal 0.1-2.cm2 1653816591 1650712132 Microsoft Corporation (none) 0 x86_64 0 distroless-packages-0.1-2.cm2.src.rpm +distroless-packages-base 0.1-2.cm2 1653816591 1650712132 Microsoft Corporation (none) 0 x86_64 0 distroless-packages-0.1-2.cm2.src.rpm diff --git a/syft/pkg/cataloger/rpmdb/test-fixtures/generate-fixture.sh b/syft/pkg/cataloger/rpmdb/test-fixtures/generate-fixture.sh index 4356173e5..908a82ad3 100755 --- a/syft/pkg/cataloger/rpmdb/test-fixtures/generate-fixture.sh +++ b/syft/pkg/cataloger/rpmdb/test-fixtures/generate-fixture.sh @@ -21,3 +21,9 @@ docker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF EOF docker cp generate-rpmdb-fixture:/scratch/Packages . + +docker build -o . - <