Skip to content

Cargo analyzer drops direct dependencies declared without version #11247

Description

@DmitriyLewen

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 = "..." }, including branch, tag or rev;
  • path dependencies outside the workspace: foo = { path = "../foo" };
  • dependencies inherited from [workspace.dependencies] that are declared with git or path.

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)

app@0.1.0 root
log@0.4.34 direct

Expected result

The same set that cargo tree -e normal reports: everything except pretty_assertions, diff and yansi.

app@0.1.0 root
byteorder@1.5.0 indirect
itoa@1.0.18 indirect
local@0.1.0 direct
log@0.4.34 direct
regex-syntax@0.8.11 indirect
tantivy-fst@0.5.0 direct
utf8-ranges@1.0.5 indirect

Root 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 pgrx-tests@0.19.0 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.

  1. Process the root package and each workspace member separately instead of merging their dependencies into one map.
  2. For each of them, take DependsOn from the lock file and keep only the IDs whose name is declared in [dependencies] or [target.*.dependencies] of its Cargo.toml.
    Use the [workspace.dependencies] definition for workspace = true.
    The version, git and path keys are not needed for matching.
  3. If the filtered DependsOn still contains several versions of the same crate, compare the version with the constraint as today.
  4. 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 the alias key instead of the package value.
  • [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.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug.
    scan/vulnerabilityIssues relating to vulnerability scanning
    scan/sbomIssues relating to SBOM
    on Sep 14, 2026
  2. added a commit that references this issue on Oct 7, 2026
    f4f4821
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugCategorizes issue or PR as related to a bug.scan/sbomIssues relating to SBOMscan/vulnerabilityIssues relating to vulnerability scanning

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions