Follow convention for naming catalogers (#2277)

* follow convention for naming catalogers

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix cataloger name example

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
2023-11-02 12:39:42 +00:00
committed by GitHub
parent 6c41f15975
commit b2f4d7eda2
144 changed files with 294 additions and 273 deletions
+35
View File
@@ -0,0 +1,35 @@
/*
Package redhat provides a concrete DBCataloger implementation relating to packages within the RedHat linux distribution.
*/
package redhat
import (
"database/sql"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// NewDBCataloger returns a new RPM DB cataloger object.
func NewDBCataloger() *generic.Cataloger {
// check if a sqlite driver is available
if !isSqliteDriverAvailable() {
log.Warnf("sqlite driver is not available, newer RPM databases might not be cataloged")
}
return generic.NewCataloger("rpm-db-cataloger").
WithParserByGlobs(parseRpmDB, pkg.RpmDBGlob).
WithParserByGlobs(parseRpmManifest, pkg.RpmManifestGlob)
}
// NewArchiveCataloger returns a new RPM file cataloger object.
func NewArchiveCataloger() *generic.Cataloger {
return generic.NewCataloger("rpm-archive-cataloger").
WithParserByGlobs(parseRpmArchive, "**/*.rpm")
}
func isSqliteDriverAvailable() bool {
_, err := sql.Open("sqlite", ":memory:")
return err == nil
}
@@ -0,0 +1,66 @@
package redhat
import (
"testing"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func Test_DBCataloger_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
expected []string
}{
{
name: "obtain DB files",
fixture: "test-fixtures/glob-paths",
expected: []string{
"usr/share/rpm/Packages",
"usr/share/rpm/Packages.db",
"usr/share/rpm/rpmdb.sqlite",
"var/lib/rpm/Packages",
"var/lib/rpm/Packages.db",
"var/lib/rpm/rpmdb.sqlite",
"var/lib/rpmmanifest/container-manifest-2",
"usr/lib/sysimage/rpm/Packages",
"usr/lib/sysimage/rpm/Packages.db",
"usr/lib/sysimage/rpm/rpmdb.sqlite",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
ExpectsResolverContentQueries(test.expected).
TestCataloger(t, NewDBCataloger())
})
}
}
func Test_RPMFileCataloger_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
expected []string
}{
{
name: "obtain rpm files",
fixture: "test-fixtures/glob-paths",
expected: []string{
"dive-0.10.0.rpm",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
ExpectsResolverContentQueries(test.expected).
TestCataloger(t, NewArchiveCataloger())
})
}
}
+120
View File
@@ -0,0 +1,120 @@
package redhat
import (
"fmt"
"strconv"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
)
func newDBPackage(dbOrRpmLocation file.Location, m pkg.RpmDBEntry, distro *linux.Release, licenses []string) pkg.Package {
p := pkg.Package{
Name: m.Name,
Version: toELVersion(m.Epoch, m.Version, m.Release),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(dbOrRpmLocation, licenses...)...),
PURL: packageURL(m.Name, m.Arch, m.Epoch, m.SourceRpm, m.Version, m.Release, distro),
Locations: file.NewLocationSet(dbOrRpmLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Type: pkg.RpmPkg,
Metadata: m,
}
p.SetID()
return p
}
func newArchivePackage(archiveLocation file.Location, m pkg.RpmArchive, licenses []string) pkg.Package {
p := pkg.Package{
Name: m.Name,
Version: toELVersion(m.Epoch, m.Version, m.Release),
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromLocation(archiveLocation, licenses...)...),
PURL: packageURL(m.Name, m.Arch, m.Epoch, m.SourceRpm, m.Version, m.Release, nil),
Locations: file.NewLocationSet(archiveLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
Type: pkg.RpmPkg,
Metadata: m,
}
p.SetID()
return p
}
// newMetadataFromManifestLine parses an entry in an RPM manifest file as used in Mariner distroless containers.
// Each line is the output from:
// - 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 newMetadataFromManifestLine(entry string) (*pkg.RpmDBEntry, 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
}
return &pkg.RpmDBEntry{
Name: parts[0],
Version: version,
Epoch: epoch,
Arch: parts[7],
Release: release,
SourceRpm: parts[9],
Vendor: parts[4],
Size: size,
}, nil
}
// packageURL returns the PURL for the specific RHEL package (see https://github.com/package-url/purl-spec)
func packageURL(name, arch string, epoch *int, srpm string, version, release string, distro *linux.Release) string {
var namespace string
if distro != nil {
namespace = distro.ID
}
qualifiers := map[string]string{}
if arch != "" {
qualifiers[pkg.PURLQualifierArch] = arch
}
if epoch != nil {
qualifiers[pkg.PURLQualifierEpoch] = strconv.Itoa(*epoch)
}
if srpm != "" {
qualifiers[pkg.PURLQualifierUpstream] = srpm
}
return packageurl.NewPackageURL(
packageurl.TypeRPM,
namespace,
name,
// for purl the epoch is a qualifier, not part of the version
// see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst under the RPM section
fmt.Sprintf("%s-%s", version, release),
pkg.PURLQualifiers(
qualifiers,
distro,
),
"",
).ToString()
}
+92
View File
@@ -0,0 +1,92 @@
package redhat
import (
"testing"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
)
func Test_packageURL(t *testing.T) {
tests := []struct {
name string
distro *linux.Release
metadata pkg.RpmDBEntry
expected string
}{
{
name: "go case",
distro: &linux.Release{
ID: "rhel",
VersionID: "8.4",
},
metadata: pkg.RpmDBEntry{
Name: "p",
Version: "v",
Release: "r",
Epoch: nil,
},
expected: "pkg:rpm/rhel/p@v-r?distro=rhel-8.4",
},
{
name: "with arch and epoch",
distro: &linux.Release{
ID: "centos",
VersionID: "7",
},
metadata: pkg.RpmDBEntry{
Name: "p",
Version: "v",
Arch: "a",
Release: "r",
Epoch: intRef(1),
},
expected: "pkg:rpm/centos/p@v-r?arch=a&epoch=1&distro=centos-7",
},
{
name: "missing distro",
metadata: pkg.RpmDBEntry{
Name: "p",
Version: "v",
Release: "r",
Epoch: nil,
},
expected: "pkg:rpm/p@v-r",
},
{
name: "with upstream source rpm info",
distro: &linux.Release{
ID: "rhel",
VersionID: "8.4",
},
metadata: pkg.RpmDBEntry{
Name: "p",
Version: "v",
Release: "r",
SourceRpm: "sourcerpm",
},
expected: "pkg:rpm/rhel/p@v-r?upstream=sourcerpm&distro=rhel-8.4",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := packageURL(
test.metadata.Name,
test.metadata.Arch,
test.metadata.Epoch,
test.metadata.SourceRpm,
test.metadata.Version,
test.metadata.Release,
test.distro,
)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
@@ -0,0 +1,92 @@
package redhat
import (
"fmt"
"strconv"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
"github.com/sassoftware/go-rpmutils"
"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/generic"
)
// parseRpmArchive parses a single RPM
func parseRpmArchive(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
rpm, err := rpmutils.ReadRpm(reader)
if err != nil {
return nil, nil, fmt.Errorf("RPM file found but unable to read: %s (%w)", reader.Location.RealPath, err)
}
nevra, err := rpm.Header.GetNEVRA()
if err != nil {
return nil, nil, err
}
licenses, _ := rpm.Header.GetStrings(rpmutils.LICENSE)
sourceRpm, _ := rpm.Header.GetString(rpmutils.SOURCERPM)
vendor, _ := rpm.Header.GetString(rpmutils.VENDOR)
digestAlgorithm := getDigestAlgorithm(rpm.Header)
size, _ := rpm.Header.InstalledSize()
files, _ := rpm.Header.GetFiles()
metadata := pkg.RpmArchive{
Name: nevra.Name,
Version: nevra.Version,
Epoch: parseEpoch(nevra.Epoch),
Arch: nevra.Arch,
Release: nevra.Release,
SourceRpm: sourceRpm,
Vendor: vendor,
Size: int(size),
Files: mapFiles(files, digestAlgorithm),
}
return []pkg.Package{newArchivePackage(reader.Location, metadata, licenses)}, nil, nil
}
func getDigestAlgorithm(header *rpmutils.RpmHeader) string {
digestAlgorithm, _ := header.GetString(rpmutils.FILEDIGESTALGO)
if digestAlgorithm != "" {
return digestAlgorithm
}
digestAlgorithms, _ := header.GetUint32s(rpmutils.FILEDIGESTALGO)
if len(digestAlgorithms) > 0 {
digestAlgo := int(digestAlgorithms[0])
return rpmutils.GetFileAlgoName(digestAlgo)
}
return ""
}
func mapFiles(files []rpmutils.FileInfo, digestAlgorithm string) []pkg.RpmFileRecord {
var out []pkg.RpmFileRecord
for _, f := range files {
digest := file.Digest{}
if f.Digest() != "" {
digest = file.Digest{
Algorithm: digestAlgorithm,
Value: f.Digest(),
}
}
out = append(out, pkg.RpmFileRecord{
Path: f.Name(),
Mode: pkg.RpmFileMode(f.Mode()),
Size: int(f.Size()),
Digest: digest,
UserName: f.UserName(),
GroupName: f.GroupName(),
Flags: rpmdb.FileFlags(f.Flags()).String(),
})
}
return out
}
func parseEpoch(epoch string) *int {
i, err := strconv.Atoi(epoch)
if err != nil {
return nil
}
return &i
}
@@ -0,0 +1,96 @@
package redhat
import (
"testing"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParseRpmFiles(t *testing.T) {
abcRpmLocation := file.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm")
zorkRpmLocation := file.NewLocation("zork-1.0.3-1.el7.x86_64.rpm")
tests := []struct {
fixture string
expected []pkg.Package
}{
{
fixture: "test-fixtures/rpms",
expected: []pkg.Package{
{
Name: "abc",
Version: "0:1.01-9.hg20160905.el7",
PURL: "pkg:rpm/abc@1.01-9.hg20160905.el7?arch=x86_64&epoch=0&upstream=abc-1.01-9.hg20160905.el7.src.rpm",
Locations: file.NewLocationSet(file.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm")),
FoundBy: "rpm-archive-cataloger",
Type: pkg.RpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", abcRpmLocation),
),
Metadata: pkg.RpmArchive{
Name: "abc",
Epoch: intRef(0),
Arch: "x86_64",
Release: "9.hg20160905.el7",
Version: "1.01",
SourceRpm: "abc-1.01-9.hg20160905.el7.src.rpm",
Size: 17396,
Vendor: "Fedora Project",
Files: []pkg.RpmFileRecord{
{"/usr/bin/abc", 33261, 7120, file.Digest{"sha256", "8f8495a65c66762b60afa0c3949d81b275ca6fa0601696caba5af762f455d0b9"}, "root", "root", ""},
{"/usr/share/doc/abc-1.01", 16877, 4096, file.Digest{}, "root", "root", ""},
{"/usr/share/doc/abc-1.01/readme.md", 33188, 4984, file.Digest{"sha256", "808af8a28391e96ca0d91086789488dda3724fe7c8b2859efd464fb04b94b2d4"}, "root", "root", "d"},
{"/usr/share/doc/abc-1.01/readmeaig", 33188, 3324, file.Digest{"sha256", "530ec6175cf7fbeb7b595cbe7a50994429c4e62cae6666fb3a1d5745f3127b19"}, "root", "root", "d"},
{"/usr/share/man/man1/abc.1.gz", 33188, 1968, file.Digest{"sha256", "cf2cfe25b29087e60ffd5f31f974a0762172fc2f009704951f12ff750ea77ed6"}, "root", "root", "d"},
},
},
},
{
Name: "zork",
Version: "0:1.0.3-1.el7",
PURL: "pkg:rpm/zork@1.0.3-1.el7?arch=x86_64&epoch=0&upstream=zork-1.0.3-1.el7.src.rpm",
Locations: file.NewLocationSet(zorkRpmLocation),
FoundBy: "rpm-archive-cataloger",
Type: pkg.RpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Public Domain", zorkRpmLocation),
),
Metadata: pkg.RpmArchive{
Name: "zork",
Epoch: intRef(0),
Arch: "x86_64",
Release: "1.el7",
Version: "1.0.3",
SourceRpm: "zork-1.0.3-1.el7.src.rpm",
Size: 262367,
Vendor: "Fedora Project",
Files: []pkg.RpmFileRecord{
{"/usr/bin/zork", 33261, 115440, file.Digest{"sha256", "31b2ffc20b676a8fff795a45308f584273b9c47e8f7e196b4f36220b2734b472"}, "root", "root", ""},
{"/usr/share/doc/zork-1.0.3", 16877, 38, file.Digest{}, "root", "root", ""},
{"/usr/share/doc/zork-1.0.3/README.md", 33188, 5123, file.Digest{"sha256", "0013d67610a80c9f62d151a952f18d520b15b4c505b3ec2af34b96ab824654a4"}, "root", "root", "d"},
{"/usr/share/doc/zork-1.0.3/history", 33188, 4816, file.Digest{"sha256", "6949044a65adefca6ac0132c18cfccc4ba8fdaec948424b6ccb60afd8a6ac82f"}, "root", "root", "d"},
{"/usr/share/licenses/zork-1.0.3", 16877, 24, file.Digest{}, "root", "root", ""},
{"/usr/share/licenses/zork-1.0.3/readme.txt", 33188, 146, file.Digest{"sha256", "9d6f7500555a2ecc3cb289dcca1e37fb96894dab1e4ba692b4d36fd6c3bdf939"}, "root", "root", "l"},
{"/usr/share/man/man6/dungeon.6.gz", 33188, 3800, file.Digest{"sha256", "9b065d6a6f65b4d2d038fcca0af47a38e8723c32008d08659739ac34abe018da"}, "root", "root", "d"},
{"/usr/share/man/man6/zork.6.gz", 33188, 34, file.Digest{"sha256", "18fbcb598bc40a25befe26256e29366984d2288dd154f877b8ac5fc138dd0884"}, "root", "root", "d"},
{"/usr/share/zork/dtextc.dat", 33188, 133008, file.Digest{"sha256", "25ca42857c2b32054916d9258152293ead644023d5e03bec039ea92014e2ef91"}, "root", "root", ""},
},
},
},
},
},
{
fixture: "test-fixtures/bad",
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
Expects(test.expected, nil).
TestCataloger(t, NewArchiveCataloger())
})
}
}
+132
View File
@@ -0,0 +1,132 @@
package redhat
import (
"fmt"
"io"
"os"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// parseRpmDb parses an "Packages" RPM DB and returns the Packages listed within it.
func parseRpmDB(resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
f, err := os.CreateTemp("", "rpmdb")
if err != nil {
return nil, nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)
}
defer func() {
err = os.Remove(f.Name())
if err != nil {
log.Errorf("failed to remove temp rpmdb file: %+v", err)
}
}()
_, err = io.Copy(f, reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to copy rpmdb contents to temp file: %w", err)
}
db, err := rpmdb.Open(f.Name())
if err != nil {
return nil, nil, err
}
pkgList, err := db.ListPackages()
if err != nil {
return nil, nil, err
}
var allPkgs []pkg.Package
var distro *linux.Release
if env != nil {
distro = env.LinuxRelease
}
for _, entry := range pkgList {
if entry == nil {
continue
}
metadata := pkg.RpmDBEntry{
Name: entry.Name,
Version: entry.Version,
Epoch: entry.Epoch,
Arch: entry.Arch,
Release: entry.Release,
SourceRpm: entry.SourceRpm,
Vendor: entry.Vendor,
Size: entry.Size,
ModularityLabel: entry.Modularitylabel,
Files: extractRpmFileRecords(resolver, *entry),
}
p := newDBPackage(
reader.Location,
metadata,
distro,
[]string{entry.License},
)
if !pkg.IsValid(&p) {
log.WithFields("location", reader.RealPath, "pkg", fmt.Sprintf("%s@%s", entry.Name, entry.Version)).
Warn("ignoring invalid package found in RPM DB")
continue
}
p.SetID()
allPkgs = append(allPkgs, p)
}
return allPkgs, nil, nil
}
// The RPM naming scheme is [name]-[version]-[release]-[arch], where version is implicitly expands to [epoch]:[version].
// RPM version comparison depends on comparing at least the version and release fields together as a subset of the
// naming scheme. This toELVersion function takes a RPM DB package information and converts it into a minimally comparable
// version string, containing epoch (optional), version, and release information. Epoch is an optional field and can be
// assumed to be 0 when not provided for comparison purposes, however, if the underlying RPM DB entry does not have
// an epoch specified it would be slightly disingenuous to display a value of 0.
func toELVersion(epoch *int, version, release string) string {
if epoch != nil {
return fmt.Sprintf("%d:%s-%s", *epoch, version, release)
}
return fmt.Sprintf("%s-%s", version, release)
}
func extractRpmFileRecords(resolver file.PathResolver, entry rpmdb.PackageInfo) []pkg.RpmFileRecord {
var records = make([]pkg.RpmFileRecord, 0)
files, err := entry.InstalledFiles()
if err != nil {
log.Warnf("unable to parse listing of installed files for RPM DB entry: %s", err.Error())
return records
}
for _, record := range files {
// only persist RPMDB file records which exist in the image/directory, otherwise ignore them
if resolver.HasPath(record.Path) {
records = append(records, pkg.RpmFileRecord{
Path: record.Path,
Mode: pkg.RpmFileMode(record.Mode),
Size: int(record.Size),
Digest: file.Digest{
Value: record.Digest,
Algorithm: entry.DigestAlgorithm.String(),
},
UserName: record.Username,
GroupName: record.Groupname,
Flags: record.Flags.String(),
})
}
}
return records
}
@@ -0,0 +1,214 @@
package redhat
import (
"fmt"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
var _ file.Resolver = (*rpmdbTestFileResolverMock)(nil)
type rpmdbTestFileResolverMock struct {
ignorePaths bool
}
func (r rpmdbTestFileResolverMock) FilesByExtension(extensions ...string) ([]file.Location, error) {
panic("not implemented")
}
func (r rpmdbTestFileResolverMock) FilesByBasename(filenames ...string) ([]file.Location, error) {
panic("not implemented")
}
func (r rpmdbTestFileResolverMock) FilesByBasenameGlob(globs ...string) ([]file.Location, error) {
panic("not implemented")
}
func (r rpmdbTestFileResolverMock) FileContentsByLocation(location file.Location) (io.ReadCloser, error) {
panic("not implemented")
}
func (r rpmdbTestFileResolverMock) AllLocations() <-chan file.Location {
panic("not implemented")
}
func (r rpmdbTestFileResolverMock) FileMetadataByLocation(location file.Location) (file.Metadata, error) {
panic("not implemented")
}
func newTestFileResolver(ignorePaths bool) *rpmdbTestFileResolverMock {
return &rpmdbTestFileResolverMock{
ignorePaths: ignorePaths,
}
}
func (r rpmdbTestFileResolverMock) HasPath(path string) bool {
return !r.ignorePaths
}
func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]file.Location, error) {
if r.ignorePaths {
// act as if no paths exist
return nil, nil
}
// act as if all files exist
var locations = make([]file.Location, len(paths))
for i, p := range paths {
locations[i] = file.NewLocation(p)
}
return locations, nil
}
func (r *rpmdbTestFileResolverMock) FilesByGlob(...string) ([]file.Location, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *rpmdbTestFileResolverMock) RelativeFileByPath(file.Location, string) *file.Location {
panic(fmt.Errorf("not implemented"))
return nil
}
func (r *rpmdbTestFileResolverMock) FilesByMIMEType(...string) ([]file.Location, error) {
return nil, fmt.Errorf("not implemented")
}
func TestParseRpmDB(t *testing.T) {
packagesLocation := file.NewLocation("test-fixtures/Packages")
tests := []struct {
fixture string
expected []pkg.Package
ignorePaths bool
}{
{
fixture: "test-fixtures/Packages",
// we only surface package paths for files that exist (here we DO NOT expect a path)
ignorePaths: true,
expected: []pkg.Package{
{
Name: "dive",
Version: "0.9.2-1",
PURL: "pkg:rpm/dive@0.9.2-1?arch=x86_64&upstream=dive-0.9.2-1.src.rpm",
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/Packages")),
Type: pkg.RpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", packagesLocation),
),
Metadata: pkg.RpmDBEntry{
Name: "dive",
Epoch: nil,
Arch: "x86_64",
Release: "1",
Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784,
Vendor: "",
Files: []pkg.RpmFileRecord{},
},
},
},
},
{
fixture: "test-fixtures/Packages",
// we only surface package paths for files that exist (here we expect a path)
ignorePaths: false,
expected: []pkg.Package{
{
Name: "dive",
Version: "0.9.2-1",
PURL: "pkg:rpm/dive@0.9.2-1?arch=x86_64&upstream=dive-0.9.2-1.src.rpm",
Locations: file.NewLocationSet(packagesLocation),
Type: pkg.RpmPkg,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", packagesLocation),
),
Metadata: pkg.RpmDBEntry{
Name: "dive",
Epoch: nil,
Arch: "x86_64",
Release: "1",
Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784,
Vendor: "",
Files: []pkg.RpmFileRecord{
{
Path: "/usr/local/bin/dive",
Mode: 33261,
Size: 12406784,
Digest: file.Digest{
Algorithm: "sha256",
Value: "81d29f327ba23096b3c52ff6fe1c425641e618bc87b5c05ee377edc650afaa55",
},
// note: there is no username, groupname, or flags for this RPM
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
pkgtest.NewCatalogTester().
WithResolver(newTestFileResolver(test.ignorePaths)).
FromFile(t, test.fixture).
Expects(test.expected, nil).
TestParser(t, parseRpmDB)
})
}
}
func TestToElVersion(t *testing.T) {
tests := []struct {
name string
entry pkg.RpmDBEntry
expected string
}{
{
name: "no epoch",
entry: pkg.RpmDBEntry{
Version: "1.2.3-4",
Release: "el7",
Arch: "x86-64",
},
expected: "1.2.3-4-el7",
},
{
name: "with 0 epoch",
entry: pkg.RpmDBEntry{
Version: "1.2.3-4",
Release: "el7",
Arch: "x86-64",
Epoch: intRef(0),
},
expected: "0:1.2.3-4-el7",
},
{
name: "with non-zero epoch",
entry: pkg.RpmDBEntry{
Version: "1.2.3-4",
Release: "el7",
Arch: "x86-64",
Epoch: intRef(12),
},
expected: "12:1.2.3-4-el7",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, toELVersion(test.entry.Epoch, test.entry.Version, test.entry.Release))
})
}
}
func intRef(i int) *int {
return &i
}
@@ -0,0 +1,56 @@
package redhat
import (
"bufio"
"errors"
"io"
"strings"
"github.com/anchore/syft/internal/log"
"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/generic"
)
// Parses an RPM manifest file, as used in Mariner distroless containers, and returns the Packages listed
func parseRpmManifest(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
r := bufio.NewReader(reader)
allPkgs := make([]pkg.Package, 0)
for {
line, err := r.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, nil, err
}
if line == "" {
continue
}
metadata, err := newMetadataFromManifestLine(strings.TrimSuffix(line, "\n"))
if err != nil {
log.Warnf("unable to parse RPM manifest entry: %+v", err)
continue
}
if metadata == nil {
log.Warn("unable to parse RPM manifest entry: no metadata found")
continue
}
p := newDBPackage(reader.Location, *metadata, nil, nil)
if !pkg.IsValid(&p) {
continue
}
p.SetID()
allPkgs = append(allPkgs, p)
}
return allPkgs, nil, nil
}
@@ -0,0 +1,90 @@
package redhat
import (
"testing"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParseRpmManifest(t *testing.T) {
fixture := "test-fixtures/container-manifest-2"
location := file.NewLocation(fixture)
expected := []pkg.Package{
{
Name: "mariner-release",
Version: "2.0-12.cm2",
PURL: "pkg:rpm/mariner-release@2.0-12.cm2?arch=noarch&upstream=mariner-release-2.0-12.cm2.src.rpm",
Locations: file.NewLocationSet(location),
Type: pkg.RpmPkg,
Metadata: pkg.RpmDBEntry{
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",
},
},
{
Name: "filesystem",
Version: "1.1-9.cm2",
PURL: "pkg:rpm/filesystem@1.1-9.cm2?arch=x86_64&upstream=filesystem-1.1-9.cm2.src.rpm",
Locations: file.NewLocationSet(location),
Type: pkg.RpmPkg,
Metadata: pkg.RpmDBEntry{
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",
},
},
{
Name: "glibc",
Version: "2.35-2.cm2",
PURL: "pkg:rpm/glibc@2.35-2.cm2?arch=x86_64&upstream=glibc-2.35-2.cm2.src.rpm",
Locations: file.NewLocationSet(location),
Type: pkg.RpmPkg,
Metadata: pkg.RpmDBEntry{
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",
},
},
{
Name: "openssl-libs",
Version: "1.1.1k-15.cm2",
PURL: "pkg:rpm/openssl-libs@1.1.1k-15.cm2?arch=x86_64&upstream=openssl-1.1.1k-15.cm2.src.rpm",
Locations: file.NewLocationSet(location),
Type: pkg.RpmPkg,
Metadata: pkg.RpmDBEntry{
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",
},
},
}
pkgtest.NewCatalogTester().
FromFile(t, fixture).
Expects(expected, nil).
TestParser(t, parseRpmManifest)
}
@@ -0,0 +1,2 @@
/rpms/*
*.fingerprint
@@ -0,0 +1,21 @@
RPMSDIR=rpms
ifndef RPMSDIR
$(error RPMSDIR is not set)
endif
all: rpms
clean:
rm -rf $(RPMSDIR)
rpms:
mkdir -p $(RPMSDIR)
cd $(RPMSDIR) && curl https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/a/abc-1.01-9.hg20160905.el7.x86_64.rpm -O
cd $(RPMSDIR) && curl https://dl.fedoraproject.org/pub/epel/7/x86_64/Packages/z/zork-1.0.3-1.el7.x86_64.rpm -O
# we need a way to determine if CI should bust the test cache based on the source material
.PHONY: $(RPMSDIR).fingerprint
$(RPMSDIR).fingerprint:
find Makefile -type f -exec sha256sum {} \; | sort | tee /dev/stderr | tee $(RPMSDIR).fingerprint
sha256sum $(RPMSDIR).fingerprint
Binary file not shown.
@@ -0,0 +1 @@
this is not a real RPM file
@@ -0,0 +1,5 @@
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
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -eux
docker create --name generate-rpmdb-fixture centos:8 sh -c 'tail -f /dev/null'
function cleanup {
docker kill generate-rpmdb-fixture
docker rm generate-rpmdb-fixture
}
trap cleanup EXIT
docker start generate-rpmdb-fixture
docker exec -i --tty=false generate-rpmdb-fixture bash <<-EOF
mkdir -p /scratch
cd /scratch
rpm --initdb --dbpath /scratch
curl -sSLO https://github.com/wagoodman/dive/releases/download/v0.9.2/dive_0.9.2_linux_amd64.rpm
rpm --dbpath /scratch -ivh dive_0.9.2_linux_amd64.rpm
rm dive_0.9.2_linux_amd64.rpm
rpm --dbpath /scratch -qa
EOF
docker cp generate-rpmdb-fixture:/scratch/Packages .
docker build -o . - <<EOF
FROM mcr.microsoft.com/cbl-mariner/distroless/base:2.0 as base
FROM scratch
COPY --from=base /var/lib/rpmmanifest/container-manifest-2 .
EOF
@@ -0,0 +1 @@
bogus