diff --git a/docs/docs/vulnerability/detection/language.md b/docs/docs/vulnerability/detection/language.md index bcfcfe8f26fd..c8737d65a251 100644 --- a/docs/docs/vulnerability/detection/language.md +++ b/docs/docs/vulnerability/detection/language.md @@ -24,6 +24,7 @@ | Go | Binaries built by Go[^6] | ✅ | ✅ | - | - | excluded | | | go.mod[^7] | - | - | ✅ | ✅ | included | | Rust | Cargo.lock | ✅ | ✅ | ✅ | ✅ | included | +| | Binaries built with [cargo-auditable](https://github.com/rust-secure-code/cargo-auditable) | ✅ | ✅ | - | - | excluded The path of these files does not matter. diff --git a/go.mod b/go.mod index 3621bb867045..62d2d59f6e31 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/NYTimes/gziphandler v1.1.1 github.com/alicebob/miniredis/v2 v2.22.0 github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 - github.com/aquasecurity/go-dep-parser v0.0.0-20220626060741-179d0b167e5f + github.com/aquasecurity/go-dep-parser v0.0.0-20220807122629-b5a21d267b03 github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 @@ -201,6 +201,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/microsoft/go-rustaudit v0.0.0-20220805122630-097fff025e34 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.0.0 // indirect diff --git a/go.sum b/go.sum index 374e48f748ae..a492a50cad1b 100644 --- a/go.sum +++ b/go.sum @@ -206,8 +206,8 @@ github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30 github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= github.com/aquasecurity/defsec v0.70.0 h1:tzmKrnR/OssRC/0RwnmmPwnoWOCOY7rPc+3ZaqLumg0= github.com/aquasecurity/defsec v0.70.0/go.mod h1:ZMuvHCXmvdL6EM3ckt/qY/qIJ6WEr5GNeGhNDFgVrcw= -github.com/aquasecurity/go-dep-parser v0.0.0-20220626060741-179d0b167e5f h1:ObiLf3DY/Mr3hfqWHNgQ4vjVo/fFni216otahWzQXIE= -github.com/aquasecurity/go-dep-parser v0.0.0-20220626060741-179d0b167e5f/go.mod h1:MDQj3aeTQHSRbM1ZOGQVFziHvJtwf7moK+f9gYlUdeE= +github.com/aquasecurity/go-dep-parser v0.0.0-20220807122629-b5a21d267b03 h1:Axx5KwV0c83IlPLIIsi/Ht6sGsSJBzABUngXjFHFg4I= +github.com/aquasecurity/go-dep-parser v0.0.0-20220807122629-b5a21d267b03/go.mod h1:SONYN1M+sYu6VIJsZnltmVfcGOCvp09HWbhpnHDn3aY= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 h1:eveqE9ivrt30CJ7dOajOfBavhZ4zPqHcZe/4tKp0alc= @@ -1089,6 +1089,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/microsoft/go-rustaudit v0.0.0-20220805122630-097fff025e34 h1:W/tuIksfbU5I1xVm2zxi0afcIhDvmnebpdq+tA3OPAE= +github.com/microsoft/go-rustaudit v0.0.0-20220805122630-097fff025e34/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= diff --git a/pkg/detector/library/driver.go b/pkg/detector/library/driver.go index 561b83d22e83..5f1e5652fb2d 100644 --- a/pkg/detector/library/driver.go +++ b/pkg/detector/library/driver.go @@ -28,7 +28,7 @@ func NewDriver(libType string) (Driver, error) { case ftypes.Bundler, ftypes.GemSpec: ecosystem = vulnerability.RubyGems comparer = rubygems.Comparer{} - case ftypes.Cargo: + case ftypes.RustBinary, ftypes.Cargo: ecosystem = vulnerability.Cargo comparer = compare.GenericComparer{} case ftypes.Composer: diff --git a/pkg/fanal/analyzer/all/import.go b/pkg/fanal/analyzer/all/import.go index bf04d173fc34..3fd1b0078658 100644 --- a/pkg/fanal/analyzer/all/import.go +++ b/pkg/fanal/analyzer/all/import.go @@ -20,6 +20,7 @@ import ( _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/poetry" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/gemspec" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/rust/binary" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/rust/cargo" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/licensing" _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine" diff --git a/pkg/fanal/analyzer/const.go b/pkg/fanal/analyzer/const.go index 0ce24c7e22d5..8ebdbf311be7 100644 --- a/pkg/fanal/analyzer/const.go +++ b/pkg/fanal/analyzer/const.go @@ -40,7 +40,8 @@ const ( TypeGemSpec Type = "gemspec" // Rust - TypeCargo Type = "cargo" + TypeRustBinary Type = "rustbinary" + TypeCargo Type = "cargo" // PHP TypeComposer Type = "composer" @@ -114,7 +115,7 @@ var ( TypeLanguages = []Type{ TypeBundler, TypeGemSpec, TypeCargo, TypeComposer, TypeJar, TypePom, TypeNpmPkgLock, TypeNodePkg, TypeYarn, TypePnpm, TypeNuget, TypeDotNetDeps, - TypePythonPkg, TypePip, TypePipenv, TypePoetry, TypeGoBinary, TypeGoMod, + TypePythonPkg, TypePip, TypePipenv, TypePoetry, TypeGoBinary, TypeGoMod, TypeRustBinary, } // TypeLockfiles has all lock file analyzers @@ -124,7 +125,7 @@ var ( } // TypeIndividualPkgs has all analyzers for individual packages - TypeIndividualPkgs = []Type{TypeGemSpec, TypeNodePkg, TypePythonPkg, TypeGoBinary, TypeJar} + TypeIndividualPkgs = []Type{TypeGemSpec, TypeNodePkg, TypePythonPkg, TypeGoBinary, TypeJar, TypeRustBinary} // TypeConfigFiles has all config file analyzers TypeConfigFiles = []Type{TypeYaml, TypeJSON, TypeDockerfile, TypeTerraform, TypeCloudFormation, TypeHelm} diff --git a/pkg/fanal/analyzer/language/golang/binary/binary.go b/pkg/fanal/analyzer/language/golang/binary/binary.go index 68f9854c2a86..474543bc3c3c 100644 --- a/pkg/fanal/analyzer/language/golang/binary/binary.go +++ b/pkg/fanal/analyzer/language/golang/binary/binary.go @@ -11,6 +11,7 @@ import ( "github.com/aquasecurity/trivy/pkg/fanal/analyzer" "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/utils" ) func init() { @@ -34,16 +35,7 @@ func (a gobinaryLibraryAnalyzer) Analyze(_ context.Context, input analyzer.Analy } func (a gobinaryLibraryAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { - mode := fileInfo.Mode() - if !mode.IsRegular() { - return false - } - - // Check executable file - if mode.Perm()&0111 != 0 { - return true - } - return false + return utils.IsExecutable(fileInfo) } func (a gobinaryLibraryAnalyzer) Type() analyzer.Type { diff --git a/pkg/fanal/analyzer/language/rust/binary/binary.go b/pkg/fanal/analyzer/language/rust/binary/binary.go new file mode 100644 index 000000000000..116f0000fad3 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/binary.go @@ -0,0 +1,47 @@ +package binary + +import ( + "context" + "errors" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-dep-parser/pkg/rust/binary" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +func init() { + analyzer.RegisterAnalyzer(&rustBinaryLibraryAnalyzer{}) +} + +const version = 1 + +type rustBinaryLibraryAnalyzer struct{} + +func (a rustBinaryLibraryAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + p := binary.NewParser() + libs, deps, err := p.Parse(input.Content) + if errors.Is(err, binary.ErrUnrecognizedExe) || errors.Is(err, binary.ErrNonRustBinary) { + return nil, nil + } else if err != nil { + return nil, xerrors.Errorf("rust binary parse error: %w", err) + } + + return language.ToAnalysisResult(types.RustBinary, input.FilePath, "", libs, deps), nil +} + +func (a rustBinaryLibraryAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { + return utils.IsExecutable(fileInfo) +} + +func (a rustBinaryLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeRustBinary +} + +func (a rustBinaryLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/rust/binary/binary_test.go b/pkg/fanal/analyzer/language/rust/binary/binary_test.go new file mode 100644 index 000000000000..4c49d376de82 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/binary_test.go @@ -0,0 +1,96 @@ +package binary + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_rustBinaryLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + inputFile: "testdata/executable_rust", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.RustBinary, + FilePath: "testdata/executable_rust", + Libraries: []types.Package{ + {Name: "crate_with_features", Version: "0.1.0"}, + {Name: "library_crate", Version: "0.1.0"}, + }, + }, + }, + }, + }, + { + name: "not rust binary", + inputFile: "testdata/executable_bash", + }, + { + name: "broken elf", + inputFile: "testdata/broken_elf", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := rustBinaryLibraryAnalyzer{} + ctx := context.Background() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_rustBinaryLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "file perm 0755", + filePath: "testdata/0755", + want: true, + }, + { + name: "file perm 0644", + filePath: "testdata/0644", + want: false, + }, + { + name: "symlink", + filePath: "testdata/symlink", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := rustBinaryLibraryAnalyzer{} + fileInfo, err := os.Lstat(tt.filePath) + require.NoError(t, err) + got := a.Required(tt.filePath, fileInfo) + assert.Equal(t, tt.want, got, fileInfo.Mode().Perm()) + }) + } +} diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/0644 b/pkg/fanal/analyzer/language/rust/binary/testdata/0644 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/0755 b/pkg/fanal/analyzer/language/rust/binary/testdata/0755 new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/broken_elf b/pkg/fanal/analyzer/language/rust/binary/testdata/broken_elf new file mode 100755 index 000000000000..1a736bf298d6 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/broken_elf @@ -0,0 +1 @@ +ELF diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/executable_bash b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_bash new file mode 100755 index 000000000000..ca3b9f0d2cda --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_bash @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "hello" diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/executable_rust b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_rust new file mode 100755 index 000000000000..cadfee87b008 Binary files /dev/null and b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_rust differ diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/foo b/pkg/fanal/analyzer/language/rust/binary/testdata/foo new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/symlink b/pkg/fanal/analyzer/language/rust/binary/testdata/symlink new file mode 120000 index 000000000000..19102815663d --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/symlink @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go index 4be722b51692..5b1291097f7e 100644 --- a/pkg/fanal/types/const.go +++ b/pkg/fanal/types/const.go @@ -26,6 +26,7 @@ const ( GoBinary = "gobinary" GoModule = "gomod" JavaScript = "javascript" + RustBinary = "rustbinary" // Config files YAML = "yaml" diff --git a/pkg/fanal/utils/utils.go b/pkg/fanal/utils/utils.go index 0e85fa5c6c2a..1556d9100c7c 100644 --- a/pkg/fanal/utils/utils.go +++ b/pkg/fanal/utils/utils.go @@ -50,3 +50,16 @@ func Keys(m map[string]struct{}) []string { } return keys } + +func IsExecutable(fileInfo os.FileInfo) bool { + mode := fileInfo.Mode() + if !mode.IsRegular() { + return false + } + + // Check executable file + if mode.Perm()&0111 != 0 { + return true + } + return false +} diff --git a/pkg/purl/purl.go b/pkg/purl/purl.go index fa8637b08c1c..34c92b0e5021 100644 --- a/pkg/purl/purl.go +++ b/pkg/purl/purl.go @@ -90,6 +90,8 @@ func (p *PackageURL) AppType() string { return string(analyzer.TypeGoBinary) case packageurl.TypeNPM: return string(analyzer.TypeNodePkg) + case packageurl.TypeCargo: + return string(analyzer.TypeRustBinary) } return p.Type } diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go index b04d5cab62cd..538b07231c6b 100644 --- a/pkg/sbom/cyclonedx/marshal.go +++ b/pkg/sbom/cyclonedx/marshal.go @@ -241,7 +241,7 @@ func (e *Marshaler) marshalComponents(r types.Report, bomRef string) (*[]cdx.Com } if result.Type == ftypes.NodePkg || result.Type == ftypes.PythonPkg || result.Type == ftypes.GoBinary || - result.Type == ftypes.GemSpec || result.Type == ftypes.Jar { + result.Type == ftypes.GemSpec || result.Type == ftypes.Jar || result.Type == ftypes.RustBinary { // If a package is language-specific package that isn't associated with a lock file, // it will be a dependency of a component under "metadata". // e.g.