Cargo analyzer drops direct dependencies declared without `version`
Description
When Cargo.toml is present next to Cargo.lock, Trivy uses it to identify direct dependencies and then keeps only packages reachable from them (this is how dev dependencies are excluded).
A direct dependency is recognized only if it has a string version in Cargo.toml: foo = "1" or foo = { version = "1" }.
Dependencies without version are not recognized as direct and are removed from the result together with every transitive dependency that is reachable only through them.
According to the Cargo documentation, such declarations are valid and common:
- git dependencies:
foo = { git = "..." }, includingbranch,tagorrev; - path dependencies outside the workspace:
foo = { path = "../foo" }; - dependencies inherited from
[workspace.dependencies]that are declared withgitorpath.
The dropped packages are missing from both vulnerability results and SBOMs.
Originally reported in #11236.
Reproduction
Cargo.toml:
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[dependencies]
log = "0.4"
tantivy-fst = { git = "https://github.com/paradedb/fst.git" }
local = { path = "local" }
[dev-dependencies]
pretty_assertions = "1"local/Cargo.toml:
[package]
name = "local"
version = "0.1.0"
edition = "2021"
[dependencies]
itoa = "1"Cargo.lock (generated by cargo generate-lockfile)# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "app"
version = "0.1.0"
dependencies = [
"local",
"log",
"pretty_assertions",
"tantivy-fst",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "local"
version = "0.1.0"
dependencies = [
"itoa",
]
[[package]]
name = "log"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "tantivy-fst"
version = "0.5.0"
source = "git+https://github.com/paradedb/fst.git#11e89334c578f26f9fbafbd1122ffb220ebbdbbf"
dependencies = [
"byteorder",
"regex-syntax",
"utf8-ranges",
]
[[package]]
name = "utf8-ranges"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba"
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"src/main.rs contains fn main() {}, local/src/lib.rs is empty.
$ trivy fs --format json --scanners vuln . | jq -r '.Results[] | select(.Type == "cargo") | .Packages[] | "\(.ID) \(.Relationship)"'Actual result (v0.74.0)
[email protected] root
[email protected] directExpected result
The same set that cargo tree -e normal reports: everything except pretty_assertions, diff and yansi.
[email protected] root
[email protected] indirect
[email protected] indirect
[email protected] direct
[email protected] direct
[email protected] indirect
[email protected] direct
[email protected] indirectRoot cause
parseRootCargoTOML keeps a dependency only if it has a string version.
removeDevDependencies then looks for a lock file package with the same name and a version that satisfies the constraint.
The version comparison was added in #3919 for a reason: Cargo.lock may contain several versions of the same crate (e.g. paradedb v0.25.6 has 110 such crates), and the name alone does not tell which version is the direct one.
However, dependencies without a version in Cargo.toml were not taken into account, and there are no tests for them.
In addition, the dependencies of all workspace members and [workspace.dependencies] are merged into a single map.
As a result, every entry of [workspace.dependencies] is treated as a direct dependency, even if a member uses it only in [dev-dependencies].
For example, in paradedb v0.25.6 this adds [email protected] with Relationship: direct, although it is a dev dependency of pg_search.
Proposed fix (needs verification)
The version selected by Cargo is already recorded in the lock file, so there is no need to check the constraint.
In Cargo.lock, the root package and every workspace member have a dependencies list with their direct dependencies, including dev and build ones.
When a crate has several versions in the lock file, the entry includes the version ("ordered-float 5.3.0"), so the parser already resolves it to an exact ID in DependsOn.
- Process the root package and each workspace member separately instead of merging their dependencies into one map.
- For each of them, take
DependsOnfrom the lock file and keep only the IDs whose name is declared in[dependencies]or[target.*.dependencies]of itsCargo.toml. Use the[workspace.dependencies]definition forworkspace = true. Theversion,gitandpathkeys are not needed for matching. - If the filtered
DependsOnstill contains several versions of the same crate, compare the version with the constraint as today. - Walk transitive dependencies as today.
This also stops [workspace.dependencies] from being reported as direct dependencies of every member.
Out of scope
- Renamed dependencies (
alias = { package = "real-name", version = "1" }) are dropped as well, even with a version, because the package is looked up by thealiaskey instead of thepackagevalue. [build-dependencies]stay excluded, consistent with scanning cargo-auditable binaries.- Packages with the same name and version from different sources (e.g. crates.io and a git fork) are already merged by the lock file parser, because the package ID does not include the source.
Source: aquasecurity/trivy