port rust cataloger to new generic cataloger pattern (#1323)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2022-11-04 12:07:36 -04:00 committed by GitHub
parent 41464bbd7f
commit f319713821
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 282 additions and 220 deletions

View file

@ -1,19 +0,0 @@
package pkg
type CargoMetadata struct {
Packages []CargoPackageMetadata `toml:"package"`
}
// Pkgs returns all of the packages referenced within the Cargo.lock metadata.
func (m CargoMetadata) Pkgs() []*Package {
pkgs := make([]*Package, 0)
for _, p := range m.Packages {
if p.Dependencies == nil {
p.Dependencies = make([]string, 0)
}
pkgs = append(pkgs, p.Pkg())
}
return pkgs
}

View file

@ -1,12 +1,5 @@
package pkg
import (
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux"
)
var _ urlIdentifier = (*CargoPackageMetadata)(nil)
type CargoPackageMetadata struct {
Name string `toml:"name" json:"name"`
Version string `toml:"version" json:"version"`
@ -14,27 +7,3 @@ type CargoPackageMetadata struct {
Checksum string `toml:"checksum" json:"checksum"`
Dependencies []string `toml:"dependencies" json:"dependencies"`
}
// Pkg returns the standard `pkg.Package` representation of the package referenced within the Cargo.lock metadata.
func (p CargoPackageMetadata) Pkg() *Package {
return &Package{
Name: p.Name,
Version: p.Version,
Language: Rust,
Type: RustPkg,
MetadataType: RustCargoPackageMetadataType,
Metadata: p,
}
}
// PackageURL returns the PURL for the specific rust package (see https://github.com/package-url/purl-spec)
func (p CargoPackageMetadata) PackageURL(_ *linux.Release) string {
return packageurl.NewPackageURL(
"cargo",
"",
p.Name,
p.Version,
nil,
"",
).ToString()
}

View file

@ -98,7 +98,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
golang.NewGoModuleBinaryCataloger(),
golang.NewGoModFileCataloger(),
rust.NewCargoLockCataloger(),
rust.NewRustAuditBinaryCataloger(),
rust.NewAuditBinaryCataloger(),
dart.NewPubspecLockCataloger(),
dotnet.NewDotnetDepsCataloger(),
php.NewPHPComposerInstalledCataloger(),

View file

@ -1,127 +0,0 @@
package rust
import (
"fmt"
rustaudit "github.com/microsoft/go-rustaudit"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
"github.com/anchore/syft/syft/source"
)
const catalogerName = "cargo-auditable-binary-cataloger"
type Cataloger struct{}
// NewRustAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies
// in binaries produced with https://github.com/Shnatsel/rust-audit
func NewRustAuditBinaryCataloger() *Cataloger {
return &Cataloger{}
}
// Name returns a string that uniquely describes a cataloger
func (c *Cataloger) Name() string {
return catalogerName
}
// Catalog identifies executables then attempts to read Rust dependency information from them
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
fileMatches, err := resolver.FilesByMIMEType(internal.ExecutableMIMETypeSet.List()...)
if err != nil {
return pkgs, nil, fmt.Errorf("failed to find bin by mime types: %w", err)
}
for _, location := range fileMatches {
readerCloser, err := resolver.FileContentsByLocation(location)
if err != nil {
log.Warnf("rust cataloger: opening file: %v", err)
continue
}
reader, err := unionreader.GetUnionReader(readerCloser)
if err != nil {
return nil, nil, err
}
versionInfos := scanFile(reader, location.RealPath)
internal.CloseAndLogError(readerCloser, location.RealPath)
for _, versionInfo := range versionInfos {
pkgs = append(pkgs, buildRustPkgInfo(location, versionInfo)...)
}
}
return pkgs, nil, nil
}
// scanFile scans file to try to report the Rust crate dependencies
func scanFile(reader unionreader.UnionReader, filename string) []rustaudit.VersionInfo {
// NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.Warnf("rust cataloger: failed to open a binary: %v", err)
return nil
}
var versionInfos []rustaudit.VersionInfo
for _, r := range readers {
versionInfo, err := rustaudit.GetDependencyInfo(r)
if err != nil {
if err == rustaudit.ErrNoRustDepInfo {
// since the cataloger can only select executables and not distinguish if they are a Rust-compiled
// binary, we should not show warnings/logs in this case.
return nil
}
// Use an Info level log here like golang/scan_bin.go
log.Infof("rust cataloger: unable to read dependency information (file=%q): %v", filename, err)
return nil
}
versionInfos = append(versionInfos, versionInfo)
}
return versionInfos
}
func buildRustPkgInfo(location source.Location, versionInfo rustaudit.VersionInfo) []pkg.Package {
var pkgs []pkg.Package
for _, dep := range versionInfo.Packages {
dep := dep
p := newRustPackage(&dep, location)
if pkg.IsValid(&p) && dep.Kind == rustaudit.Runtime {
pkgs = append(pkgs, p)
}
}
return pkgs
}
func newRustPackage(dep *rustaudit.Package, location source.Location) pkg.Package {
p := pkg.Package{
FoundBy: catalogerName,
Name: dep.Name,
Version: dep.Version,
Language: pkg.Rust,
Type: pkg.RustPkg,
Locations: source.NewLocationSet(location),
MetadataType: pkg.RustCargoPackageMetadataType,
Metadata: pkg.CargoPackageMetadata{
Name: dep.Name,
Version: dep.Version,
Source: dep.Source,
},
}
p.SetID()
return p
}

View file

@ -4,14 +4,19 @@ Package rust provides a concrete Cataloger implementation for Cargo.lock files.
package rust
import (
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object.
func NewCargoLockCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/Cargo.lock": parseCargoLock,
}
return common.NewGenericCataloger(nil, globParsers, "rust-cargo-lock-cataloger")
func NewCargoLockCataloger() *generic.Cataloger {
return generic.NewCataloger("rust-cargo-lock-cataloger").
WithParserByGlobs(parseCargoLock, "**/Cargo.lock")
}
// NewAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies
// in binaries produced with https://github.com/Shnatsel/rust-audit
func NewAuditBinaryCataloger() *generic.Cataloger {
return generic.NewCataloger("cargo-auditable-binary-cataloger").
WithParserByMimeTypes(parseAuditBinary, internal.ExecutableMIMETypeSet.List()...)
}

View file

@ -0,0 +1,51 @@
package rust
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/source"
)
func TestNewAuditBinaryCataloger(t *testing.T) {
expectedPkgs := []pkg.Package{
{
Name: "auditable",
Version: "0.1.0",
PURL: "pkg:cargo/auditable@0.1.0",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: source.NewLocationSet(source.NewVirtualLocation("/hello-auditable", "/hello-auditable")),
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
Metadata: pkg.CargoPackageMetadata{
Name: "auditable",
Version: "0.1.0",
Source: "local",
},
},
{
Name: "hello-auditable",
Version: "0.1.0",
PURL: "pkg:cargo/hello-auditable@0.1.0",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: source.NewLocationSet(source.NewVirtualLocation("/hello-auditable", "/hello-auditable")),
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
Metadata: pkg.CargoPackageMetadata{
Name: "hello-auditable",
Version: "0.1.0",
Source: "local",
},
},
}
pkgtest.NewCatalogTester().
WithImageResolver(t, "image-audit").
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
Expects(expectedPkgs, nil).
TestCataloger(t, NewAuditBinaryCataloger())
}

View file

@ -0,0 +1,74 @@
package rust
import (
"github.com/microsoft/go-rustaudit"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// Pkg returns the standard `pkg.Package` representation of the package referenced within the Cargo.lock metadata.
func newPackageFromCargoMetadata(m pkg.CargoPackageMetadata, locations ...source.Location) pkg.Package {
p := pkg.Package{
Name: m.Name,
Version: m.Version,
Locations: source.NewLocationSet(locations...),
PURL: packageURL(m.Name, m.Version),
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
Metadata: m,
}
p.SetID()
return p
}
func newPackagesFromAudit(location source.Location, versionInfo rustaudit.VersionInfo) []pkg.Package {
var pkgs []pkg.Package
for _, dep := range versionInfo.Packages {
dep := dep
p := newPackageFromAudit(&dep, location)
if pkg.IsValid(&p) && dep.Kind == rustaudit.Runtime {
pkgs = append(pkgs, p)
}
}
return pkgs
}
func newPackageFromAudit(dep *rustaudit.Package, locations ...source.Location) pkg.Package {
p := pkg.Package{
Name: dep.Name,
Version: dep.Version,
PURL: packageURL(dep.Name, dep.Version),
Language: pkg.Rust,
Type: pkg.RustPkg,
Locations: source.NewLocationSet(locations...),
MetadataType: pkg.RustCargoPackageMetadataType,
Metadata: pkg.CargoPackageMetadata{
Name: dep.Name,
Version: dep.Version,
Source: dep.Source,
},
}
p.SetID()
return p
}
// packageURL returns the PURL for the specific rust package (see https://github.com/package-url/purl-spec)
func packageURL(name, version string) string {
return packageurl.NewPackageURL(
packageurl.TypeCargo,
"",
name,
version,
nil,
"",
).ToString()
}

View file

@ -0,0 +1,33 @@
package rust
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_packageURL(t *testing.T) {
type args struct {
name string
version string
}
tests := []struct {
name string
args args
want string
}{
{
name: "go case",
args: args{
name: "name",
version: "v0.1.0",
},
want: "pkg:cargo/name@v0.1.0",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, packageURL(tt.args.name, tt.args.version))
})
}
}

View file

@ -0,0 +1,59 @@
package rust
import (
rustaudit "github.com/microsoft/go-rustaudit"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
"github.com/anchore/syft/syft/source"
)
// Catalog identifies executables then attempts to read Rust dependency information from them
func parseAuditBinary(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
unionReader, err := unionreader.GetUnionReader(reader.ReadCloser)
if err != nil {
return nil, nil, err
}
for _, versionInfo := range parseAuditBinaryEntry(unionReader, reader.RealPath) {
pkgs = append(pkgs, newPackagesFromAudit(reader.Location, versionInfo)...)
}
return pkgs, nil, nil
}
// scanFile scans file to try to report the Rust crate dependencies
func parseAuditBinaryEntry(reader unionreader.UnionReader, filename string) []rustaudit.VersionInfo {
// NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.Warnf("rust cataloger: failed to open a binary: %v", err)
return nil
}
var versionInfos []rustaudit.VersionInfo
for _, r := range readers {
versionInfo, err := rustaudit.GetDependencyInfo(r)
if err != nil {
if err == rustaudit.ErrNoRustDepInfo {
// since the cataloger can only select executables and not distinguish if they are a Rust-compiled
// binary, we should not show warnings/logs in this case.
return nil
}
// Use an Info level log here like golang/scan_bin.go
log.Infof("rust cataloger: unable to read dependency information (file=%q): %v", filename, err)
return nil
}
versionInfos = append(versionInfos, versionInfo)
}
return versionInfos
}

View file

@ -2,30 +2,42 @@ package rust
import (
"fmt"
"io"
"github.com/pelletier/go-toml"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source"
)
// integrity check
var _ common.ParserFn = parseCargoLock
var _ generic.Parser = parseCargoLock
type cargoLockFile struct {
Packages []pkg.CargoPackageMetadata `toml:"package"`
}
// parseCargoLock is a parser function for Cargo.lock contents, returning all rust cargo crates discovered.
func parseCargoLock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
func parseCargoLock(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
tree, err := toml.LoadReader(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err)
}
metadata := pkg.CargoMetadata{}
err = tree.Unmarshal(&metadata)
m := cargoLockFile{}
err = tree.Unmarshal(&m)
if err != nil {
return nil, nil, fmt.Errorf("unable to parse Cargo.lock: %v", err)
}
return metadata.Pkgs(), nil, nil
var pkgs []pkg.Package
for _, p := range m.Packages {
if p.Dependencies == nil {
p.Dependencies = make([]string, 0)
}
pkgs = append(pkgs, newPackageFromCargoMetadata(p, reader.Location))
}
return pkgs, nil, nil
}

View file

@ -1,19 +1,23 @@
package rust
import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/source"
)
func TestParseCargoLock(t *testing.T) {
expected := []*pkg.Package{
fixture := "test-fixtures/Cargo.lock"
locations := source.NewLocationSet(source.NewLocation(fixture))
expectedPkgs := []pkg.Package{
{
Name: "ansi_term",
Version: "0.12.1",
PURL: "pkg:cargo/ansi_term@0.12.1",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -31,6 +35,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "matches",
Version: "0.1.8",
PURL: "pkg:cargo/matches@0.1.8",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -46,6 +52,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "memchr",
Version: "2.3.3",
PURL: "pkg:cargo/memchr@2.3.3",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -61,6 +69,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "natord",
Version: "1.0.9",
PURL: "pkg:cargo/natord@1.0.9",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -76,6 +86,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "nom",
Version: "4.2.3",
PURL: "pkg:cargo/nom@4.2.3",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -94,6 +106,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "unicode-bidi",
Version: "0.3.4",
PURL: "pkg:cargo/unicode-bidi@0.3.4",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -111,6 +125,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "version_check",
Version: "0.1.5",
PURL: "pkg:cargo/version_check@0.1.5",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -126,6 +142,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "winapi",
Version: "0.3.9",
PURL: "pkg:cargo/winapi@0.3.9",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -144,6 +162,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "winapi-i686-pc-windows-gnu",
Version: "0.4.0",
PURL: "pkg:cargo/winapi-i686-pc-windows-gnu@0.4.0",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -159,6 +179,8 @@ func TestParseCargoLock(t *testing.T) {
{
Name: "winapi-x86_64-pc-windows-gnu",
Version: "0.4.0",
PURL: "pkg:cargo/winapi-x86_64-pc-windows-gnu@0.4.0",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
MetadataType: pkg.RustCargoPackageMetadataType,
@ -173,19 +195,9 @@ func TestParseCargoLock(t *testing.T) {
},
}
fixture, err := os.Open("test-fixtures/Cargo.lock")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
// TODO: no relationships are under test yet
actual, _, err := parseCargoLock(fixture.Name(), fixture)
if err != nil {
t.Error(err)
}
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships)
differences := deep.Equal(expected, actual)
if differences != nil {
t.Errorf("returned package list differed from expectation: %+v", differences)
}
}

View file

@ -0,0 +1 @@
FROM docker.io/tofay/hello-rust-auditable:latest@sha256:1d35d1e007180b3f7500aae5e27560697909132ca9a6d480c4c825534c1c47a9

View file

@ -68,7 +68,7 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
DartPubMetadataType: reflect.TypeOf(DartPubMetadata{}),
DotnetDepsMetadataType: reflect.TypeOf(DotnetDepsMetadata{}),
PythonPackageMetadataType: reflect.TypeOf(PythonPackageMetadata{}),
RustCargoPackageMetadataType: reflect.TypeOf(CargoMetadata{}),
RustCargoPackageMetadataType: reflect.TypeOf(CargoPackageMetadata{}),
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),

View file

@ -17,15 +17,6 @@ func TestPackageURL(t *testing.T) {
distro *linux.Release
expected string
}{
{
name: "cargo",
pkg: Package{
Name: "name",
Version: "v0.1.0",
Type: RustPkg,
},
expected: "pkg:cargo/name@v0.1.0",
},
{
name: "java",
pkg: Package{
@ -93,6 +84,7 @@ func TestPackageURL(t *testing.T) {
expectedTypes.Remove(string(RpmPkg))
expectedTypes.Remove(string(GemPkg))
expectedTypes.Remove(string(NpmPkg))
expectedTypes.Remove(string(RustPkg))
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {