mirror of
https://github.com/anchore/syft.git
synced 2026-11-04 00:16:38 +01:00
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:
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
bogus
|
||||
@@ -0,0 +1 @@
|
||||
bogus
|
||||
@@ -0,0 +1 @@
|
||||
bogus
|
||||
@@ -0,0 +1 @@
|
||||
bogus
|
||||
@@ -0,0 +1 @@
|
||||
bogus
|
||||
@@ -0,0 +1 @@
|
||||
bogus
|
||||
Reference in New Issue
Block a user