add rpmdb file info to cataloger

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2020-11-05 13:46:20 -05:00
parent ecfc471ce5
commit 8095cd9980
No known key found for this signature in database
GPG Key ID: 5CB45AE22BAB7EA7
6 changed files with 213 additions and 61 deletions

2
go.mod
View File

@ -11,7 +11,7 @@ require (
github.com/bmatcuk/doublestar v1.3.3 github.com/bmatcuk/doublestar v1.3.3
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0 github.com/dustin/go-humanize v1.0.0
github.com/go-test/deep v1.0.6 github.com/go-test/deep v1.0.7
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gookit/color v1.2.7 github.com/gookit/color v1.2.7
github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-multierror v1.1.0

2
go.sum
View File

@ -299,6 +299,8 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8= github.com/go-test/deep v1.0.6 h1:UHSEyLZUwX9Qoi99vVwvewiMC8mM2bf7XEM2nqvzEn8=
github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g=
github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4=
github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8=

View File

@ -4,13 +4,49 @@ Package rpmdb provides a concrete Cataloger implementation for RPM "Package" DB
package rpmdb package rpmdb
import ( import (
"github.com/anchore/syft/syft/cataloger/common" "fmt"
"strings"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
) )
const (
packagesGlob = "**/var/lib/rpm/Packages"
)
type Cataloger struct{}
// NewRpmdbCataloger returns a new RPM DB cataloger object. // NewRpmdbCataloger returns a new RPM DB cataloger object.
func NewRpmdbCataloger() *common.GenericCataloger { func NewRpmdbCataloger() *Cataloger {
globParsers := map[string]common.ParserFn{ return &Cataloger{}
"**/var/lib/rpm/Packages": parseRpmDB, }
}
return common.NewGenericCataloger(nil, globParsers, "rpmdb-cataloger") // Name returns a string that uniquely describes a cataloger
func (c *Cataloger) Name() string {
return "rpmdb-cataloger"
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing python egg and wheel installations.
func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
fileMatches, err := resolver.FilesByGlob(packagesGlob)
if err != nil {
return nil, fmt.Errorf("failed to find rpmdb's by glob")
}
var pkgs []pkg.Package
for _, ref := range fileMatches {
dbContents, err := resolver.FileContentsByRef(ref)
if err != nil {
return nil, err
}
pkgs, err = parseRpmDB(resolver, strings.NewReader(dbContents))
if err != nil {
return nil, fmt.Errorf("unable to catalog rpmdb package=%+v: %w", ref.Path, err)
}
}
return pkgs, nil
} }

View File

@ -6,18 +6,18 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/scope"
rpmdb "github.com/anchore/go-rpmdb/pkg" rpmdb "github.com/anchore/go-rpmdb/pkg"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
// integrity check
var _ common.ParserFn = parseRpmDB
// parseApkDb parses an "Packages" RPM DB and returns the Packages listed within it. // parseApkDb parses an "Packages" RPM DB and returns the Packages listed within it.
func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) { func parseRpmDB(resolver scope.FileResolver, reader io.Reader) ([]pkg.Package, error) {
f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb") f, err := ioutil.TempFile("", internal.ApplicationName+"-rpmdb")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create temp rpmdb file: %w", err) return nil, fmt.Errorf("failed to create temp rpmdb file: %w", err)
@ -48,6 +48,26 @@ func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
allPkgs := make([]pkg.Package, 0) allPkgs := make([]pkg.Package, 0)
for _, entry := range pkgList { for _, entry := range pkgList {
var records = make([]pkg.RpmdbFileRecord, 0)
for _, record := range entry.Files {
refs, err := resolver.FilesByPath(file.Path(record.Path))
if err != nil {
return nil, fmt.Errorf("failed to resolve path=%+v: %w", record.Path, err)
}
//only persist RPMDB file records which exist in the image/directory, otherwise ignore them
if len(refs) == 0 {
continue
}
records = append(records, pkg.RpmdbFileRecord{
Path: record.Path,
Mode: pkg.RpmdbFileMode(record.Mode),
Size: int(record.Size),
SHA256: record.SHA256,
})
}
p := pkg.Package{ p := pkg.Package{
Name: entry.Name, Name: entry.Name,
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does
@ -64,6 +84,7 @@ func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
Vendor: entry.Vendor, Vendor: entry.Vendor,
License: entry.License, License: entry.License,
Size: entry.Size, Size: entry.Size,
Files: records,
}, },
} }

View File

@ -1,58 +1,141 @@
package rpmdb package rpmdb
import ( import (
"fmt"
"os" "os"
"testing" "testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep" "github.com/go-test/deep"
) )
type rpmdbTestFileResolverMock struct {
ignorePaths bool
}
func newTestFileResolver(ignorePaths bool) *rpmdbTestFileResolverMock {
return &rpmdbTestFileResolverMock{
ignorePaths: ignorePaths,
}
}
func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
if r.ignorePaths {
// act as if no paths exist
return nil, nil
}
// act as if all files exist
var refs = make([]file.Reference, len(paths))
for i, p := range paths {
refs[i] = file.NewFileReference(p)
}
return refs, nil
}
func (r *rpmdbTestFileResolverMock) FilesByGlob(_ ...string) ([]file.Reference, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *rpmdbTestFileResolverMock) RelativeFileByPath(_ file.Reference, path string) (*file.Reference, error) {
return nil, fmt.Errorf("not implemented")
}
func TestParseRpmDB(t *testing.T) { func TestParseRpmDB(t *testing.T) {
expected := map[string]pkg.Package{ tests := []struct {
"dive": { fixture string
Name: "dive", expected map[string]pkg.Package
Version: "0.9.2-1", ignorePaths bool
Type: pkg.RpmPkg, }{
MetadataType: pkg.RpmdbMetadataType, {
Metadata: pkg.RpmdbMetadata{ fixture: "test-fixtures/Packages",
Name: "dive", // we only surface package paths for files that exist (here we DO NOT expect a path)
Epoch: 0, ignorePaths: true,
Arch: "x86_64", expected: map[string]pkg.Package{
Release: "1", "dive": {
Version: "0.9.2", Name: "dive",
SourceRpm: "dive-0.9.2-1.src.rpm", Version: "0.9.2-1",
Size: 12406784, Type: pkg.RpmPkg,
License: "MIT", MetadataType: pkg.RpmdbMetadataType,
Vendor: "", Metadata: pkg.RpmdbMetadata{
Name: "dive",
Epoch: 0,
Arch: "x86_64",
Release: "1",
Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
Files: []pkg.RpmdbFileRecord{},
},
},
},
},
{
fixture: "test-fixtures/Packages",
// we only surface package paths for files that exist (here we expect a path)
ignorePaths: false,
expected: map[string]pkg.Package{
"dive": {
Name: "dive",
Version: "0.9.2-1",
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{
Name: "dive",
Epoch: 0,
Arch: "x86_64",
Release: "1",
Version: "0.9.2",
SourceRpm: "dive-0.9.2-1.src.rpm",
Size: 12406784,
License: "MIT",
Vendor: "",
Files: []pkg.RpmdbFileRecord{
{
Path: "/usr/local/bin/dive",
Mode: 33261,
Size: 12406784,
SHA256: "81d29f327ba23096b3c52ff6fe1c425641e618bc87b5c05ee377edc650afaa55",
},
},
},
},
}, },
}, },
} }
fixture, err := os.Open("test-fixtures/Packages") for _, test := range tests {
if err != nil { t.Run(test.fixture, func(t *testing.T) {
t.Fatalf("failed to open fixture: %+v", err) fixture, err := os.Open(test.fixture)
} if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
actual, err := parseRpmDB(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse rpmdb: %+v", err)
}
if len(actual) != len(expected) {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expected))
}
for _, a := range actual {
e := expected[a.Name]
diffs := deep.Equal(a, e)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("diff: %+v", d)
} }
}
fileResolver := newTestFileResolver(test.ignorePaths)
actual, err := parseRpmDB(fileResolver, fixture)
if err != nil {
t.Fatalf("failed to parse rpmdb: %+v", err)
}
if len(actual) != len(test.expected) {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
}
for _, a := range actual {
e := test.expected[a.Name]
diffs := deep.Equal(a, e)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("diff: %+v", d)
}
}
}
})
} }
} }

View File

@ -9,17 +9,27 @@ import (
// RpmdbMetadata represents all captured data for a RPM DB package entry. // RpmdbMetadata represents all captured data for a RPM DB package entry.
type RpmdbMetadata struct { type RpmdbMetadata struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
Epoch int `json:"epoch"` Epoch int `json:"epoch"`
Arch string `json:"architecture"` Arch string `json:"architecture"`
Release string `json:"release"` Release string `json:"release"`
SourceRpm string `json:"sourceRpm"` SourceRpm string `json:"sourceRpm"`
Size int `json:"size"` Size int `json:"size"`
License string `json:"license"` License string `json:"license"`
Vendor string `json:"vendor"` Vendor string `json:"vendor"`
Files []RpmdbFileRecord `json:"files"`
} }
type RpmdbFileRecord struct {
Path string `json:"path"`
Mode RpmdbFileMode `json:"mode"`
Size int `json:"size"`
SHA256 string `json:"sha256"`
}
type RpmdbFileMode uint16
func (m RpmdbMetadata) PackageURL(d distro.Distro) string { func (m RpmdbMetadata) PackageURL(d distro.Distro) string {
pURL := packageurl.NewPackageURL( pURL := packageurl.NewPackageURL(
packageurl.TypeRPM, packageurl.TypeRPM,