fix(rust): implement version inheritance for Cargo mono repos (#10011)

Signed-off-by: Máté Czékus <mate@picloud.hu>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
Czékus Máté
2026-01-29 11:51:02 +01:00
committed by GitHub
parent 676709de44
commit 47d3103c50
5 changed files with 118 additions and 13 deletions

View File

@@ -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{

View File

@@ -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",

View File

@@ -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"

View File

@@ -0,0 +1,5 @@
[workspace.package]
version = "2.0.0"
[workspace]
members = ["app"]

View File

@@ -0,0 +1,7 @@
[package]
name = "myapp"
version.workspace = true
edition = "2021"
[dependencies]
serde = "1.0"