diff --git a/pkg/fanal/analyzer/language/rust/cargo/cargo.go b/pkg/fanal/analyzer/language/rust/cargo/cargo.go index 4e75217a90..d36e71ea4c 100644 --- a/pkg/fanal/analyzer/language/rust/cargo/cargo.go +++ b/pkg/fanal/analyzer/language/rust/cargo/cargo.go @@ -74,7 +74,7 @@ func (a cargoAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnaly // Parse Cargo.toml alongside Cargo.lock to identify the direct dependencies if err = a.removeDevDependencies(input.FS, path.Dir(filePath), app); err != nil { - a.logger.Warn("Unable to parse Cargo.toml q to identify direct dependencies", + a.logger.Warn("Unable to parse Cargo.toml to identify direct dependencies", log.FilePath(path.Join(path.Dir(filePath), types.CargoToml)), log.Err(err)) } sort.Sort(app.Packages) @@ -199,12 +199,15 @@ type cargoToml struct { type Package struct { Name string `toml:"name"` - Version string `toml:"version"` + Version any `toml:"version"` } type cargoTomlWorkspace struct { Dependencies Dependencies `toml:"dependencies"` Members []string `toml:"members"` + Package struct { + Version string `toml:"version"` + } `toml:"package"` } type Dependencies map[string]any @@ -212,7 +215,7 @@ type Dependencies map[string]any // parseRootCargoTOML parses top-level Cargo.toml and returns dependencies. // It also parses workspace members and their dependencies. func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (string, []string, map[string]string, error) { - rootPkg, dependencies, members, err := a.parseCargoTOML(fsys, filePath) + rootPkg, dependencies, members, rootWorkspaceVersion, err := a.parseCargoTOML(fsys, filePath, "") if err != nil { return "", nil, nil, xerrors.Errorf("unable to parse %s: %w", filePath, err) } @@ -237,7 +240,7 @@ func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (string, } for _, pkg := range resolvedPaths { - memberPkg, memberDeps, _, err := a.parseCargoTOML(fsys, pkg) + memberPkg, memberDeps, _, _, err := a.parseCargoTOML(fsys, pkg, rootWorkspaceVersion) if err != nil { a.logger.Warn("Unable to parse Cargo.toml", log.String("member_path", pkg), log.Err(err)) continue @@ -314,24 +317,49 @@ func (a cargoAnalyzer) matchVersion(currentVersion, constraint string) (bool, er return c.Check(ver), nil } -func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, filePath string) (string, Dependencies, []string, error) { +func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, filePath, workspaceVersion string) (string, Dependencies, []string, string, error) { // Parse Cargo.toml f, err := fsys.Open(filePath) if err != nil { - return "", nil, nil, xerrors.Errorf("file open error: %w", err) + return "", nil, nil, "", xerrors.Errorf("file open error: %w", err) } defer func() { _ = f.Close() }() var tomlFile cargoToml + var pkgVersion string // There are cases when toml file doesn't include `Dependencies` field (then map will be nil). // e.g. when only `workspace.Dependencies` are used // declare `dependencies` to avoid panic dependencies := Dependencies{} if _, err = toml.NewDecoder(f).Decode(&tomlFile); err != nil { - return "", nil, nil, xerrors.Errorf("toml decode error: %w", err) + return "", nil, nil, "", xerrors.Errorf("toml decode error: %w", err) } - pkgID := a.packageID(tomlFile) + // https://rust-lang.github.io/rfcs/2906-cargo-workspace-deduplicate.html + if workspaceVersion == "" { + workspaceVersion = tomlFile.Workspace.Package.Version + } + + switch ver := tomlFile.Package.Version.(type) { + // In case of purely virtual cargo workspace version only lives in `workspace.package.version` + case nil: + pkgVersion = workspaceVersion + // We assume a proper version string was used, like: `0.1.0` + // Empty version is not allowed in Cargo.toml + // cf. https://github.com/aquasecurity/trivy/pull/10011#discussion_r2740743095 + case string: + pkgVersion = ver + // There are cases when `package.version` uses `version.workspace = true`, + // which must inherit the version from `workspace.version` or workspaceVersion (from root Cargo.toml) + case map[string]any: + if verWorkspace, found := ver["workspace"]; found { + if wv, ok := verWorkspace.(bool); ok && wv { + pkgVersion = workspaceVersion + } + } + } + + pkgID := a.packageID(tomlFile, pkgVersion) maps.Copy(dependencies, tomlFile.Dependencies) @@ -343,14 +371,14 @@ func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, filePath string) (string, Depe // https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace maps.Copy(dependencies, tomlFile.Workspace.Dependencies) // https://doc.rust-lang.org/cargo/reference/workspaces.html#the-members-and-exclude-fields - return pkgID, dependencies, tomlFile.Workspace.Members, nil + return pkgID, dependencies, tomlFile.Workspace.Members, workspaceVersion, nil } // packageID builds PackageID by Package name and version. // If name is empty - use hash of cargoToml. -func (a cargoAnalyzer) packageID(cargoToml cargoToml) string { +func (a cargoAnalyzer) packageID(cargoToml cargoToml, pkgVersion string) string { if cargoToml.Package.Name != "" { - return dependency.ID(types.Cargo, cargoToml.Package.Name, cargoToml.Package.Version) + return dependency.ID(types.Cargo, cargoToml.Package.Name, pkgVersion) } hash, err := hashstructure.Hash(cargoToml, hashstructure.FormatV2, &hashstructure.HashOptions{ diff --git a/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go b/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go index b6e5252117..bb25f8f904 100644 --- a/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go +++ b/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go @@ -436,6 +436,55 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) { dir: "testdata/sad", want: &analyzer.AnalysisResult{}, }, + { + name: "version.workspace = true inherits from workspace.package.version", + dir: "testdata/version-workspace-inherit", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "afc012b7e68b6638", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "myapp@2.0.0", + }, + }, + { + ID: "myapp@2.0.0", + Name: "myapp", + Version: "2.0.0", + Relationship: types.RelationshipWorkspace, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + DependsOn: []string{ + "serde@1.0.195", + }, + }, + { + ID: "serde@1.0.195", + Name: "serde", + Version: "1.0.195", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 16, + }, + }, + }, + }, + }, + }, + }, + }, { name: "workspace members", dir: "testdata/toml-workspace-members", @@ -446,7 +495,7 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) { FilePath: "Cargo.lock", Packages: types.Packages{ { - ID: "d0e1231acd612a0f", + ID: "eeae9dc40f83d7dd", Relationship: types.RelationshipRoot, DependsOn: []string{ "member@0.1.0", @@ -618,7 +667,7 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) { FilePath: "Cargo.lock", Packages: types.Packages{ { - ID: "18164bd748b1f49e", + ID: "fbcbfc8e1114c435", Relationship: types.RelationshipRoot, DependsOn: []string{ "member1@0.1.0", diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/Cargo.lock b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/Cargo.lock new file mode 100644 index 0000000000..dcff1a21f0 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "myapp" +version = "2.0.0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/Cargo.toml b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/Cargo.toml new file mode 100644 index 0000000000..8a8e35b7b4 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/Cargo.toml @@ -0,0 +1,5 @@ +[workspace.package] +version = "2.0.0" + +[workspace] +members = ["app"] \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/app/Cargo.toml b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/app/Cargo.toml new file mode 100644 index 0000000000..9777d4b56c --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit/app/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "myapp" +version.workspace = true +edition = "2021" + +[dependencies] +serde = "1.0" \ No newline at end of file