Skip to content

Commit

Permalink
add a cataloger for binaries built with rust-audit (#1116)
Browse files Browse the repository at this point in the history
* add a cataloger for binaries built with rust-audit

Signed-off-by: Tom Fay <tomfay@microsoft.com>
  • Loading branch information
tofay committed Jul 28, 2022
1 parent 62897fb commit 9896ff1
Show file tree
Hide file tree
Showing 23 changed files with 246 additions and 80 deletions.
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -412,6 +412,8 @@ platform: ""
# - dartlang-lock
# - rust
# - dotnet-deps
# rust-audit-binary scans Rust binaries built with https://github.com/Shnatsel/rust-audit
# - rust-audit-binary
catalogers:

# cataloging packages is exposed through the packages and power-user subcommands
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -26,6 +26,7 @@ require (
github.com/jinzhu/copier v0.3.2
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
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/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.5.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
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.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
Expand Down
1 change: 1 addition & 0 deletions syft/pkg/cataloger/cataloger.go
Expand Up @@ -105,6 +105,7 @@ func AllCatalogers(cfg Config) []Cataloger {
golang.NewGoModuleBinaryCataloger(),
golang.NewGoModFileCataloger(),
rust.NewCargoLockCataloger(),
rust.NewRustAuditBinaryCataloger(),
dart.NewPubspecLockCataloger(),
dotnet.NewDotnetDepsCataloger(),
php.NewPHPComposerInstalledCataloger(),
Expand Down
33 changes: 2 additions & 31 deletions syft/pkg/cataloger/golang/binary_cataloger.go
Expand Up @@ -4,15 +4,13 @@ Package golang provides a concrete Cataloger implementation for go.mod files.
package golang

import (
"bytes"
"fmt"
"io"
"io/ioutil"

"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"
)

Expand Down Expand Up @@ -46,7 +44,7 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti
continue
}

reader, err := getUnionReader(readerCloser)
reader, err := unionreader.GetUnionReader(readerCloser)
if err != nil {
return nil, nil, err
}
Expand All @@ -61,30 +59,3 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []arti

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
}
36 changes: 3 additions & 33 deletions syft/pkg/cataloger/golang/scan_bin.go
Expand Up @@ -2,27 +2,17 @@ package golang

import (
"debug/buildinfo"
"io"
"runtime/debug"

macho "github.com/anchore/go-macholibre"
"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.
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
// with more than one binary
readers, err := getReaders(reader)
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.Warnf("golang cataloger: failed to open a binary: %v", err)
return nil, nil
Expand Down Expand Up @@ -56,23 +46,3 @@ func scanFile(reader unionReader, filename string) ([]*debug.BuildInfo, []string

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
}
66 changes: 66 additions & 0 deletions syft/pkg/cataloger/internal/unionreader/union_reader.go
@@ -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
}
@@ -1,4 +1,4 @@
package golang
package unionreader

import (
"io"
Expand All @@ -14,13 +14,13 @@ func Test_getUnionReader_notUnionReader(t *testing.T) {
reader := io.NopCloser(strings.NewReader(expectedContents))

// make certain that the test fixture does not implement the union reader
_, ok := reader.(unionReader)
_, ok := reader.(UnionReader)
require.False(t, ok)

actual, err := getUnionReader(reader)
actual, err := GetUnionReader(reader)
require.NoError(t, err)

_, ok = actual.(unionReader)
_, ok = actual.(UnionReader)
require.True(t, ok)

b, err := io.ReadAll(actual)
Expand Down
126 changes: 126 additions & 0 deletions syft/pkg/cataloger/rust/audit_binary_cataloger.go
@@ -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
}
2 changes: 1 addition & 1 deletion test/integration/catalog_packages_test.go
Expand Up @@ -54,7 +54,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
}

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()
definedLanguages := internal.NewStringSet()
Expand Down
2 changes: 1 addition & 1 deletion test/integration/convert_test.go
Expand Up @@ -36,7 +36,7 @@ var convertibleFormats = []sbom.Format{
func TestConvertCmd(t *testing.T) {
for _, format := range convertibleFormats {
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)

f, err := ioutil.TempFile("", "test-convert-sbom-")
Expand Down
2 changes: 1 addition & 1 deletion test/integration/distro_test.go
Expand Up @@ -10,7 +10,7 @@ import (
)

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{
PrettyName: "BusyBox v1.31.1",
Expand Down
2 changes: 1 addition & 1 deletion test/integration/encode_decode_cycle_test.go
Expand Up @@ -64,7 +64,7 @@ func TestEncodeDecodeEncodeCycleComparison(t *testing.T) {
for _, test := range tests {
t.Run(fmt.Sprintf("%s", test.formatOption), func(t *testing.T) {
for _, image := range images {
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope)
originalSBOM, _ := catalogFixtureImage(t, image, source.SquashedScope, false)

format := syft.FormatByID(test.formatOption)
require.NotNil(t, format)
Expand Down

0 comments on commit 9896ff1

Please sign in to comment.