add a cataloger for binaries built with rust-audit (#1116)

* add a cataloger for binaries built with rust-audit

Signed-off-by: Tom Fay <tomfay@microsoft.com>
This commit is contained in:
Tom Fay 2022-07-28 19:17:38 +01:00 committed by GitHub
parent 62897fbc89
commit 9896ff1b1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 246 additions and 80 deletions

View File

@ -412,6 +412,8 @@ platform: ""
# - dartlang-lock # - dartlang-lock
# - rust # - rust
# - dotnet-deps # - dotnet-deps
# rust-audit-binary scans Rust binaries built with https://github.com/Shnatsel/rust-audit
# - rust-audit-binary
catalogers: catalogers:
# cataloging packages is exposed through the packages and power-user subcommands # cataloging packages is exposed through the packages and power-user subcommands

1
go.mod
View File

@ -26,6 +26,7 @@ require (
github.com/jinzhu/copier v0.3.2 github.com/jinzhu/copier v0.3.2
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.1 github.com/mholt/archiver/v3 v3.5.1
github.com/microsoft/go-rustaudit v0.0.0-20220722052050-3b1735710a8e
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0

2
go.sum
View File

@ -1373,6 +1373,8 @@ github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQ
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
github.com/microsoft/go-rustaudit v0.0.0-20220722052050-3b1735710a8e h1:dMQrsCOQEkVsvtzvg4jH0I/AN4+f14E5emA2BdUwy50=
github.com/microsoft/go-rustaudit v0.0.0-20220722052050-3b1735710a8e/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=

View File

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

View File

@ -4,15 +4,13 @@ Package golang provides a concrete Cataloger implementation for go.mod files.
package golang package golang
import ( import (
"bytes"
"fmt" "fmt"
"io"
"io/ioutil"
"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/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
@ -46,7 +44,7 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
continue continue
} }
reader, err := getUnionReader(readerCloser) reader, err := unionreader.GetUnionReader(readerCloser)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -61,30 +59,3 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
return pkgs, nil, nil return pkgs, nil, nil
} }
func getUnionReader(readerCloser io.ReadCloser) (unionReader, error) {
reader, ok := readerCloser.(unionReader)
if ok {
return reader, nil
}
log.Debugf("golang cataloger: unable to use stereoscope file, reading entire contents")
b, err := ioutil.ReadAll(readerCloser)
if err != nil {
return nil, fmt.Errorf("unable to read contents from go binary: %w", err)
}
bytesReader := bytes.NewReader(b)
reader = struct {
io.ReadCloser
io.ReaderAt
io.Seeker
}{
ReadCloser: io.NopCloser(bytesReader),
ReaderAt: bytesReader,
Seeker: bytesReader,
}
return reader, nil
}

View File

@ -2,27 +2,17 @@ package golang
import ( import (
"debug/buildinfo" "debug/buildinfo"
"io"
"runtime/debug" "runtime/debug"
macho "github.com/anchore/go-macholibre"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
) )
// unionReader is a single interface with all reading functions used by golang bin
// cataloger.
type unionReader interface {
io.Reader
io.ReaderAt
io.Seeker
io.Closer
}
// scanFile scans file to try to report the Go and module versions. // scanFile scans file to try to report the Go and module versions.
func scanFile(reader unionReader, filename string) ([]*debug.BuildInfo, []string) { func scanFile(reader unionreader.UnionReader, filename string) ([]*debug.BuildInfo, []string) {
// NOTE: multiple readers are returned to cover universal binaries, which are files // NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary // with more than one binary
readers, err := getReaders(reader) readers, err := unionreader.GetReaders(reader)
if err != nil { if err != nil {
log.Warnf("golang cataloger: failed to open a binary: %v", err) log.Warnf("golang cataloger: failed to open a binary: %v", err)
return nil, nil return nil, nil
@ -56,23 +46,3 @@ func scanFile(reader unionReader, filename string) ([]*debug.BuildInfo, []string
return builds, archs return builds, archs
} }
// getReaders extracts one or more io.ReaderAt objects representing binaries that can be processed (multiple binaries in the case for multi-architecture binaries).
func getReaders(f unionReader) ([]io.ReaderAt, error) {
if macho.IsUniversalMachoBinary(f) {
machoReaders, err := macho.ExtractReaders(f)
if err != nil {
log.Debugf("extracting readers: %v", err)
return nil, err
}
var readers []io.ReaderAt
for _, e := range machoReaders {
readers = append(readers, e.Reader)
}
return readers, nil
}
return []io.ReaderAt{f}, nil
}

View File

@ -0,0 +1,66 @@
package unionreader
import (
"bytes"
"fmt"
"io"
"io/ioutil"
macho "github.com/anchore/go-macholibre"
"github.com/anchore/syft/internal/log"
)
// unionReader is a single interface with all reading functions needed by multi-arch binary catalogers
// cataloger.
type UnionReader interface {
io.Reader
io.ReaderAt
io.Seeker
io.Closer
}
// getReaders extracts one or more io.ReaderAt objects representing binaries that can be processed (multiple binaries in the case for multi-architecture binaries).
func GetReaders(f UnionReader) ([]io.ReaderAt, error) {
if macho.IsUniversalMachoBinary(f) {
machoReaders, err := macho.ExtractReaders(f)
if err != nil {
log.Debugf("extracting readers: %v", err)
return nil, err
}
var readers []io.ReaderAt
for _, e := range machoReaders {
readers = append(readers, e.Reader)
}
return readers, nil
}
return []io.ReaderAt{f}, nil
}
func GetUnionReader(readerCloser io.ReadCloser) (UnionReader, error) {
reader, ok := readerCloser.(UnionReader)
if ok {
return reader, nil
}
b, err := ioutil.ReadAll(readerCloser)
if err != nil {
return nil, fmt.Errorf("unable to read contents from binary: %w", err)
}
bytesReader := bytes.NewReader(b)
reader = struct {
io.ReadCloser
io.ReaderAt
io.Seeker
}{
ReadCloser: io.NopCloser(bytesReader),
ReaderAt: bytesReader,
Seeker: bytesReader,
}
return reader, nil
}

View File

@ -1,4 +1,4 @@
package golang package unionreader
import ( import (
"io" "io"
@ -14,13 +14,13 @@ func Test_getUnionReader_notUnionReader(t *testing.T) {
reader := io.NopCloser(strings.NewReader(expectedContents)) reader := io.NopCloser(strings.NewReader(expectedContents))
// make certain that the test fixture does not implement the union reader // make certain that the test fixture does not implement the union reader
_, ok := reader.(unionReader) _, ok := reader.(UnionReader)
require.False(t, ok) require.False(t, ok)
actual, err := getUnionReader(reader) actual, err := GetUnionReader(reader)
require.NoError(t, err) require.NoError(t, err)
_, ok = actual.(unionReader) _, ok = actual.(UnionReader)
require.True(t, ok) require.True(t, ok)
b, err := io.ReadAll(actual) b, err := io.ReadAll(actual)

View File

@ -0,0 +1,126 @@
package rust
import (
"fmt"
"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"
rustaudit "github.com/microsoft/go-rustaudit"
)
const catalogerName = "rust-audit-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

@ -54,7 +54,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
} }
func TestPkgCoverageImage(t *testing.T) { func TestPkgCoverageImage(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, false)
observedLanguages := internal.NewStringSet() observedLanguages := internal.NewStringSet()
definedLanguages := internal.NewStringSet() definedLanguages := internal.NewStringSet()

View File

@ -36,7 +36,7 @@ var convertibleFormats = []sbom.Format{
func TestConvertCmd(t *testing.T) { func TestConvertCmd(t *testing.T) {
for _, format := range convertibleFormats { for _, format := range convertibleFormats {
t.Run(format.ID().String(), func(t *testing.T) { t.Run(format.ID().String(), func(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-pkg-coverage", source.SquashedScope, false)
format := syft.FormatByID(syftjson.ID) format := syft.FormatByID(syftjson.ID)
f, err := ioutil.TempFile("", "test-convert-sbom-") f, err := ioutil.TempFile("", "test-convert-sbom-")

View File

@ -10,7 +10,7 @@ import (
) )
func TestDistroImage(t *testing.T) { func TestDistroImage(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-distro-id", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-distro-id", source.SquashedScope, false)
expected := &linux.Release{ expected := &linux.Release{
PrettyName: "BusyBox v1.31.1", PrettyName: "BusyBox v1.31.1",

View File

@ -64,7 +64,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(fmt.Sprintf("%s", test.formatOption), func(t *testing.T) { t.Run(fmt.Sprintf("%s", test.formatOption), func(t *testing.T) {
for _, image := range images { for _, image := range images {
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope) originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope, false)
format := syft.FormatByID(test.formatOption) format := syft.FormatByID(test.formatOption)
require.NotNil(t, format) require.NotNil(t, format)

View File

@ -8,7 +8,7 @@ import (
) )
func TestMarinerDistroless(t *testing.T) { func TestMarinerDistroless(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-mariner-distroless", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-mariner-distroless", source.SquashedScope, false)
expectedPkgs := 12 expectedPkgs := 12
actualPkgs := 0 actualPkgs := 0

View File

@ -56,7 +56,7 @@ func TestPackageDeduplication(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(string(tt.scope), func(t *testing.T) { t.Run(string(tt.scope), func(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-vertical-package-dups", tt.scope) sbom, _ := catalogFixtureImage(t, "image-vertical-package-dups", tt.scope, false)
assert.Equal(t, tt.packageCount, sbom.Artifacts.PackageCatalog.PackageCount()) assert.Equal(t, tt.packageCount, sbom.Artifacts.PackageCatalog.PackageCount())
for name, expectedInstanceCount := range tt.instanceCount { for name, expectedInstanceCount := range tt.instanceCount {

View File

@ -23,7 +23,7 @@ func TestPackageOwnershipRelationships(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) { t.Run(test.fixture, func(t *testing.T) {
sbom, _ := catalogFixtureImage(t, test.fixture, source.SquashedScope) sbom, _ := catalogFixtureImage(t, test.fixture, source.SquashedScope, false)
output := bytes.NewBufferString("") output := bytes.NewBufferString("")
err := syftjson.Format().Encode(output, sbom) err := syftjson.Format().Encode(output, sbom)

View File

@ -10,7 +10,7 @@ import (
func TestRegression212ApkBufferSize(t *testing.T) { func TestRegression212ApkBufferSize(t *testing.T) {
// This is a regression test for issue #212 (https://github.com/anchore/syft/issues/212) in which the apk db could // This is a regression test for issue #212 (https://github.com/anchore/syft/issues/212) in which the apk db could
// not be processed due to a scanner buffer that was too small // not be processed due to a scanner buffer that was too small
sbom, _ := catalogFixtureImage(t, "image-large-apk-data", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-large-apk-data", source.SquashedScope, false)
expectedPkgs := 58 expectedPkgs := 58
actualPkgs := 0 actualPkgs := 0

View File

@ -16,7 +16,7 @@ func TestRegressionGoArchDiscovery(t *testing.T) {
) )
// This is a regression test to make sure the way we detect go binary packages // This is a regression test to make sure the way we detect go binary packages
// stays consistent and reproducible as the tool chain evolves // stays consistent and reproducible as the tool chain evolves
sbom, _ := catalogFixtureImage(t, "image-go-bin-arch-coverage", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-go-bin-arch-coverage", source.SquashedScope, false)
var actualELF, actualWIN, actualMACOS int var actualELF, actualWIN, actualMACOS int

View File

@ -6,5 +6,5 @@ import (
) )
func TestRegressionJavaNoMainPackage(t *testing.T) { // Regression: https://github.com/anchore/syft/issues/252 func TestRegressionJavaNoMainPackage(t *testing.T) { // Regression: https://github.com/anchore/syft/issues/252
catalogFixtureImage(t, "image-java-no-main-package", source.SquashedScope) catalogFixtureImage(t, "image-java-no-main-package", source.SquashedScope, false)
} }

View File

@ -0,0 +1,22 @@
package integration
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func TestRustAudit(t *testing.T) {
sbom, _ := catalogFixtureImage(t, "image-rust-auditable", source.SquashedScope, true)
expectedPkgs := 2
actualPkgs := 0
for range sbom.Artifacts.PackageCatalog.Enumerate(pkg.RustPkg) {
actualPkgs += 1
}
if actualPkgs != expectedPkgs {
t.Errorf("unexpected number of Rust packages: %d != %d", expectedPkgs, actualPkgs)
}
}

View File

@ -11,7 +11,7 @@ import (
func TestSqliteRpm(t *testing.T) { func TestSqliteRpm(t *testing.T) {
// This is a regression test for issue #469 (https://github.com/anchore/syft/issues/469). Recent RPM // This is a regression test for issue #469 (https://github.com/anchore/syft/issues/469). Recent RPM
// based distribution store package data in an sqlite database // based distribution store package data in an sqlite database
sbom, _ := catalogFixtureImage(t, "image-sqlite-rpmdb", source.SquashedScope) sbom, _ := catalogFixtureImage(t, "image-sqlite-rpmdb", source.SquashedScope, false)
expectedPkgs := 139 expectedPkgs := 139
actualPkgs := 0 actualPkgs := 0

View File

@ -0,0 +1,2 @@
# An image containing the example hello-auditable binary from https://github.com/Shnatsel/rust-audit/tree/master/hello-auditable
FROM docker.io/tofay/hello-rust-auditable:latest

View File

@ -13,7 +13,7 @@ import (
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Scope) (sbom.SBOM, *source.Source) { func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Scope, allCatalogers bool) (sbom.SBOM, *source.Source) {
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
userInput := "docker-archive:" + tarPath userInput := "docker-archive:" + tarPath
@ -25,6 +25,9 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco
// TODO: this would be better with functional options (after/during API refactor) // TODO: this would be better with functional options (after/during API refactor)
c := cataloger.DefaultConfig() c := cataloger.DefaultConfig()
if allCatalogers {
c.Catalogers = []string{"all"}
}
c.Search.Scope = scope c.Search.Scope = scope
pkgCatalog, relationships, actualDistro, err := syft.CatalogPackages(theSource, c) pkgCatalog, relationships, actualDistro, err := syft.CatalogPackages(theSource, c)
if err != nil { if err != nil {