diff --git a/README.md b/README.md index f0dc6dc5280..c793e9e3916 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/go.mod b/go.mod index 47deb929ec0..750ab39f41b 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index dbc08b58aa2..e1c05e37bbd 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 98d98436d2d..57ddcf5cf33 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -105,6 +105,7 @@ func AllCatalogers(cfg Config) []Cataloger { golang.NewGoModuleBinaryCataloger(), golang.NewGoModFileCataloger(), rust.NewCargoLockCataloger(), + rust.NewRustAuditBinaryCataloger(), dart.NewPubspecLockCataloger(), dotnet.NewDotnetDepsCataloger(), php.NewPHPComposerInstalledCataloger(), diff --git a/syft/pkg/cataloger/golang/binary_cataloger.go b/syft/pkg/cataloger/golang/binary_cataloger.go index 7d51f63ce75..494c7da86c2 100644 --- a/syft/pkg/cataloger/golang/binary_cataloger.go +++ b/syft/pkg/cataloger/golang/binary_cataloger.go @@ -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" ) @@ -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 } @@ -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 -} diff --git a/syft/pkg/cataloger/golang/scan_bin.go b/syft/pkg/cataloger/golang/scan_bin.go index 5d84011a885..58ad1a3d489 100644 --- a/syft/pkg/cataloger/golang/scan_bin.go +++ b/syft/pkg/cataloger/golang/scan_bin.go @@ -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 @@ -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 -} diff --git a/syft/pkg/cataloger/internal/unionreader/union_reader.go b/syft/pkg/cataloger/internal/unionreader/union_reader.go new file mode 100644 index 00000000000..83bad171b1a --- /dev/null +++ b/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 +} diff --git a/syft/pkg/cataloger/golang/binary_cataloger_test.go b/syft/pkg/cataloger/internal/unionreader/union_reader_test.go similarity index 81% rename from syft/pkg/cataloger/golang/binary_cataloger_test.go rename to syft/pkg/cataloger/internal/unionreader/union_reader_test.go index 41ad714df2d..15c67459aa1 100644 --- a/syft/pkg/cataloger/golang/binary_cataloger_test.go +++ b/syft/pkg/cataloger/internal/unionreader/union_reader_test.go @@ -1,4 +1,4 @@ -package golang +package unionreader import ( "io" @@ -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) diff --git a/syft/pkg/cataloger/rust/audit_binary_cataloger.go b/syft/pkg/cataloger/rust/audit_binary_cataloger.go new file mode 100644 index 00000000000..8cc2960e980 --- /dev/null +++ b/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 +} diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 9cd047fc27e..3a8e5ed062b 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -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() diff --git a/test/integration/convert_test.go b/test/integration/convert_test.go index add2e1fccff..5371948861c 100644 --- a/test/integration/convert_test.go +++ b/test/integration/convert_test.go @@ -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-") diff --git a/test/integration/distro_test.go b/test/integration/distro_test.go index 5c9f044f052..4e25cd6949c 100644 --- a/test/integration/distro_test.go +++ b/test/integration/distro_test.go @@ -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", diff --git a/test/integration/encode_decode_cycle_test.go b/test/integration/encode_decode_cycle_test.go index 012715dc3cd..8a98c831adb 100644 --- a/test/integration/encode_decode_cycle_test.go +++ b/test/integration/encode_decode_cycle_test.go @@ -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) diff --git a/test/integration/mariner_distroless_test.go b/test/integration/mariner_distroless_test.go index c30b1dbd434..6df7a1f21f2 100644 --- a/test/integration/mariner_distroless_test.go +++ b/test/integration/mariner_distroless_test.go @@ -8,7 +8,7 @@ import ( ) func TestMarinerDistroless(t *testing.T) { - sbom, _ := catalogFixtureImage(t, "image-mariner-distroless", source.SquashedScope) + sbom, _ := catalogFixtureImage(t, "image-mariner-distroless", source.SquashedScope, false) expectedPkgs := 12 actualPkgs := 0 diff --git a/test/integration/package_deduplication_test.go b/test/integration/package_deduplication_test.go index 8798f9d9045..619e67ccdf7 100644 --- a/test/integration/package_deduplication_test.go +++ b/test/integration/package_deduplication_test.go @@ -56,7 +56,7 @@ func TestPackageDeduplication(t *testing.T) { for _, tt := range tests { 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()) for name, expectedInstanceCount := range tt.instanceCount { diff --git a/test/integration/package_ownership_relationship_test.go b/test/integration/package_ownership_relationship_test.go index 8335898b9b0..afaab7d0e92 100644 --- a/test/integration/package_ownership_relationship_test.go +++ b/test/integration/package_ownership_relationship_test.go @@ -23,7 +23,7 @@ func TestPackageOwnershipRelationships(t *testing.T) { for _, test := range tests { 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("") err := syftjson.Format().Encode(output, sbom) diff --git a/test/integration/regression_apk_scanner_buffer_size_test.go b/test/integration/regression_apk_scanner_buffer_size_test.go index d0e4d953e6d..ca166ed5154 100644 --- a/test/integration/regression_apk_scanner_buffer_size_test.go +++ b/test/integration/regression_apk_scanner_buffer_size_test.go @@ -10,7 +10,7 @@ import ( 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 // 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 actualPkgs := 0 diff --git a/test/integration/regression_go_bin_scanner_arch_test.go b/test/integration/regression_go_bin_scanner_arch_test.go index 295dd4f10c6..fdb5152fc38 100644 --- a/test/integration/regression_go_bin_scanner_arch_test.go +++ b/test/integration/regression_go_bin_scanner_arch_test.go @@ -16,7 +16,7 @@ func TestRegressionGoArchDiscovery(t *testing.T) { ) // This is a regression test to make sure the way we detect go binary packages // 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 diff --git a/test/integration/regression_java_no_main_package_test.go b/test/integration/regression_java_no_main_package_test.go index 417002dc923..8e8bba2cab8 100644 --- a/test/integration/regression_java_no_main_package_test.go +++ b/test/integration/regression_java_no_main_package_test.go @@ -6,5 +6,5 @@ import ( ) 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) } diff --git a/test/integration/rust_audit_binary_test.go b/test/integration/rust_audit_binary_test.go new file mode 100644 index 00000000000..fd3d57bc37e --- /dev/null +++ b/test/integration/rust_audit_binary_test.go @@ -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) + } +} diff --git a/test/integration/sqlite_rpmdb_test.go b/test/integration/sqlite_rpmdb_test.go index 3d58ec99da1..0eac12dc702 100644 --- a/test/integration/sqlite_rpmdb_test.go +++ b/test/integration/sqlite_rpmdb_test.go @@ -11,7 +11,7 @@ import ( func TestSqliteRpm(t *testing.T) { // 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 - sbom, _ := catalogFixtureImage(t, "image-sqlite-rpmdb", source.SquashedScope) + sbom, _ := catalogFixtureImage(t, "image-sqlite-rpmdb", source.SquashedScope, false) expectedPkgs := 139 actualPkgs := 0 diff --git a/test/integration/test-fixtures/image-rust-auditable/Dockerfile b/test/integration/test-fixtures/image-rust-auditable/Dockerfile new file mode 100644 index 00000000000..a35897003b9 --- /dev/null +++ b/test/integration/test-fixtures/image-rust-auditable/Dockerfile @@ -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 diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index f05bb100a32..b5d0b7f773a 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -13,7 +13,7 @@ import ( "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) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) 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) c := cataloger.DefaultConfig() + if allCatalogers { + c.Catalogers = []string{"all"} + } c.Search.Scope = scope pkgCatalog, relationships, actualDistro, err := syft.CatalogPackages(theSource, c) if err != nil {