From ba81bfe529e67410a94c4ef3f33a01e1bdf85b6a Mon Sep 17 00:00:00 2001 From: Weston Steimel Date: Sun, 28 Feb 2021 01:29:37 +0000 Subject: [PATCH] add cataloger for rust crates from Cargo.lock files Signed-off-by: Weston Steimel --- syft/cataloger/cataloger.go | 3 + syft/cataloger/package_url_test.go | 8 ++ syft/cataloger/rust/cargo_metadata.go | 18 ++++ syft/cataloger/rust/cargo_metadata_package.go | 21 ++++ syft/cataloger/rust/cataloger.go | 17 ++++ syft/cataloger/rust/parse_cargo_lock.go | 29 ++++++ syft/cataloger/rust/parse_cargo_lock_test.go | 99 +++++++++++++++++++ syft/cataloger/rust/test-fixtures/Cargo.lock | 76 ++++++++++++++ syft/pkg/language.go | 2 + syft/pkg/metadata.go | 1 + syft/pkg/type.go | 4 + test/integration/pkg_cases_test.go | 10 ++ .../image-pkg-coverage/rust/Cargo.lock | 25 +++++ 13 files changed, 313 insertions(+) create mode 100644 syft/cataloger/rust/cargo_metadata.go create mode 100644 syft/cataloger/rust/cargo_metadata_package.go create mode 100644 syft/cataloger/rust/cataloger.go create mode 100644 syft/cataloger/rust/parse_cargo_lock.go create mode 100644 syft/cataloger/rust/parse_cargo_lock_test.go create mode 100644 syft/cataloger/rust/test-fixtures/Cargo.lock create mode 100644 test/integration/test-fixtures/image-pkg-coverage/rust/Cargo.lock diff --git a/syft/cataloger/cataloger.go b/syft/cataloger/cataloger.go index aba71dfb9..28a418270 100644 --- a/syft/cataloger/cataloger.go +++ b/syft/cataloger/cataloger.go @@ -14,6 +14,7 @@ import ( "github.com/anchore/syft/syft/cataloger/python" "github.com/anchore/syft/syft/cataloger/rpmdb" "github.com/anchore/syft/syft/cataloger/ruby" + "github.com/anchore/syft/syft/cataloger/rust" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" ) @@ -39,6 +40,7 @@ func ImageCatalogers() []Cataloger { java.NewJavaCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModCataloger(), + rust.NewCargoLockCataloger(), } } @@ -54,5 +56,6 @@ func DirectoryCatalogers() []Cataloger { java.NewJavaCataloger(), apkdb.NewApkdbCataloger(), golang.NewGoModCataloger(), + rust.NewCargoLockCataloger(), } } diff --git a/syft/cataloger/package_url_test.go b/syft/cataloger/package_url_test.go index 6f0f9e003..db562442b 100644 --- a/syft/cataloger/package_url_test.go +++ b/syft/cataloger/package_url_test.go @@ -115,6 +115,14 @@ func TestPackageURL(t *testing.T) { }, expected: "pkg:deb/name@v0.1.0", }, + { + pkg: pkg.Package{ + Name: "name", + Version: "v0.1.0", + Type: pkg.RustPkg, + }, + expected: "pkg:cargo/name@v0.1.0", + }, } for _, test := range tests { diff --git a/syft/cataloger/rust/cargo_metadata.go b/syft/cataloger/rust/cargo_metadata.go new file mode 100644 index 000000000..23c85c8ba --- /dev/null +++ b/syft/cataloger/rust/cargo_metadata.go @@ -0,0 +1,18 @@ +package rust + +import "github.com/anchore/syft/syft/pkg" + +type CargoMetadata struct { + Packages []CargoMetadataPackage `toml:"package"` +} + +// Pkgs returns all of the packages referenced within the Cargo.lock metadata. +func (m CargoMetadata) Pkgs() []pkg.Package { + pkgs := make([]pkg.Package, 0) + + for _, p := range m.Packages { + pkgs = append(pkgs, p.Pkg()) + } + + return pkgs +} diff --git a/syft/cataloger/rust/cargo_metadata_package.go b/syft/cataloger/rust/cargo_metadata_package.go new file mode 100644 index 000000000..b3410f35b --- /dev/null +++ b/syft/cataloger/rust/cargo_metadata_package.go @@ -0,0 +1,21 @@ +package rust + +import "github.com/anchore/syft/syft/pkg" + +type CargoMetadataPackage struct { + Name string `toml:"name"` + Version string `toml:"version"` + Source string `toml:"source"` + Checksum string `toml:"checksum"` + Dependencies []string `toml:"dependencies"` +} + +// Pkg returns the standard `pkg.Package` representation of the package referenced within the Cargo.lock metadata. +func (p CargoMetadataPackage) Pkg() pkg.Package { + return pkg.Package{ + Name: p.Name, + Version: p.Version, + Language: pkg.Rust, + Type: pkg.RustPkg, + } +} diff --git a/syft/cataloger/rust/cataloger.go b/syft/cataloger/rust/cataloger.go new file mode 100644 index 000000000..bd2625c5d --- /dev/null +++ b/syft/cataloger/rust/cataloger.go @@ -0,0 +1,17 @@ +/* +Package rust provides a concrete Cataloger implementation for Cargo.lock files. +*/ +package rust + +import ( + "github.com/anchore/syft/syft/cataloger/common" +) + +// NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object. +func NewCargoLockCataloger() *common.GenericCataloger { + globParsers := map[string]common.ParserFn{ + "**/Cargo.lock": parseCargoLock, + } + + return common.NewGenericCataloger(nil, globParsers, "rust-cataloger") +} diff --git a/syft/cataloger/rust/parse_cargo_lock.go b/syft/cataloger/rust/parse_cargo_lock.go new file mode 100644 index 000000000..2ec0b834a --- /dev/null +++ b/syft/cataloger/rust/parse_cargo_lock.go @@ -0,0 +1,29 @@ +package rust + +import ( + "fmt" + "io" + + "github.com/anchore/syft/syft/cataloger/common" + "github.com/anchore/syft/syft/pkg" + "github.com/pelletier/go-toml" +) + +// integrity check +var _ common.ParserFn = parseCargoLock + +// parseCargoLock is a parser function for Cargo.lock contents, returning all rust cargo crates discovered. +func parseCargoLock(_ string, reader io.Reader) ([]pkg.Package, error) { + tree, err := toml.LoadReader(reader) + if err != nil { + return nil, fmt.Errorf("unable to load Cargo.lock for parsing: %v", err) + } + + metadata := CargoMetadata{} + err = tree.Unmarshal(&metadata) + if err != nil { + return nil, fmt.Errorf("unable to parse Cargo.lock: %v", err) + } + + return metadata.Pkgs(), nil +} diff --git a/syft/cataloger/rust/parse_cargo_lock_test.go b/syft/cataloger/rust/parse_cargo_lock_test.go new file mode 100644 index 000000000..284660169 --- /dev/null +++ b/syft/cataloger/rust/parse_cargo_lock_test.go @@ -0,0 +1,99 @@ +package rust + +import ( + "os" + "testing" + + "github.com/anchore/syft/syft/pkg" + "github.com/go-test/deep" +) + +func TestParseCargoLock(t *testing.T) { + expected := []pkg.Package{ + { + Name: "ansi_term", + Version: "0.12.1", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "matches", + Version: "0.1.8", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "memchr", + Version: "2.3.3", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "natord", + Version: "1.0.9", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "nom", + Version: "4.2.3", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "unicode-bidi", + Version: "0.3.4", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "version_check", + Version: "0.1.5", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "winapi", + Version: "0.3.9", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "winapi-i686-pc-windows-gnu", + Version: "0.4.0", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + { + Name: "winapi-x86_64-pc-windows-gnu", + Version: "0.4.0", + Language: pkg.Rust, + Type: pkg.RustPkg, + Licenses: nil, + }, + } + + fixture, err := os.Open("test-fixtures/Cargo.lock") + if err != nil { + t.Fatalf("failed to open fixture: %+v", err) + } + + actual, err := parseCargoLock(fixture.Name(), fixture) + if err != nil { + t.Error(err) + } + + differences := deep.Equal(expected, actual) + if differences != nil { + t.Errorf("returned package list differed from expectation: %+v", differences) + } +} diff --git a/syft/cataloger/rust/test-fixtures/Cargo.lock b/syft/cataloger/rust/test-fixtures/Cargo.lock new file mode 100644 index 000000000..5ee8fb738 --- /dev/null +++ b/syft/cataloger/rust/test-fixtures/Cargo.lock @@ -0,0 +1,76 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +dependencies = [ + "matches", +] + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + diff --git a/syft/pkg/language.go b/syft/pkg/language.go index 9f2ab7dae..96aa4a6f9 100644 --- a/syft/pkg/language.go +++ b/syft/pkg/language.go @@ -11,6 +11,7 @@ const ( Python Language = "python" Ruby Language = "ruby" Go Language = "go" + Rust Language = "rust" ) // AllLanguages is a set of all programming languages detected by syft. @@ -20,6 +21,7 @@ var AllLanguages = []Language{ Python, Ruby, Go, + Rust, } // String returns the string representation of the language. diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index 65d58724f..56432217d 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -13,4 +13,5 @@ const ( NpmPackageJSONMetadataType MetadataType = "NpmPackageJsonMetadata" RpmdbMetadataType MetadataType = "RpmdbMetadata" PythonPackageMetadataType MetadataType = "PythonPackageMetadata" + RustCrateMetadataType MetadataType = "RustCrateMetadata" ) diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 49d3d3b26..8d193b12a 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -17,6 +17,7 @@ const ( JavaPkg Type = "java-archive" JenkinsPluginPkg Type = "jenkins-plugin" GoModulePkg Type = "go-module" + RustPkg Type = "rust-crate" ) // AllPkgs represents all supported package types @@ -30,6 +31,7 @@ var AllPkgs = []Type{ JavaPkg, JenkinsPluginPkg, GoModulePkg, + RustPkg, } // PackageURLType returns the PURL package type for the current package. @@ -51,6 +53,8 @@ func (t Type) PackageURLType() string { return packageurl.TypeRPM case GoModulePkg: return packageurl.TypeGolang + case RustPkg: + return "cargo" default: // TODO: should this be a "generic" purl type instead? return "" diff --git a/test/integration/pkg_cases_test.go b/test/integration/pkg_cases_test.go index 81946aeac..b5c05e892 100644 --- a/test/integration/pkg_cases_test.go +++ b/test/integration/pkg_cases_test.go @@ -184,4 +184,14 @@ var commonTestCases = []testCase{ "github.com/bmatcuk/doublestar": "v1.3.1", }, }, + { + name: "find rust crates", + pkgType: pkg.RustPkg, + pkgLanguage: pkg.Rust, + pkgInfo: map[string]string{ + "memchr": "2.3.3", + "nom": "4.2.3", + "version_check": "0.1.5", + }, + }, } diff --git a/test/integration/test-fixtures/image-pkg-coverage/rust/Cargo.lock b/test/integration/test-fixtures/image-pkg-coverage/rust/Cargo.lock new file mode 100644 index 000000000..f2a838632 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/rust/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check", +] + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +