Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading