Trivy’s Rust Cargo scanner doesn’t just check your Cargo.lock file; it actually builds your project in a sandboxed environment to accurately determine which transitive dependencies are actually being pulled in, then scans that dependency graph for CVEs.
Let’s see it in action. Imagine you have a simple Rust project with a Cargo.toml and Cargo.lock.
# Cargo.toml
[package]
name = "my-rust-app"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web = "4.0.0"
serde = { version = "1.0", features = ["derive"] }
When you run trivy fs . in the root of this project, Trivy will:
- Parse
Cargo.tomlandCargo.lock: It identifies the top-level dependencies. - Simulate a
cargo build: Trivy uses a minimal Rust toolchain to perform a "dry run" build. This is crucial becauseCargo.lockmight list versions of dependencies that are not actually used by the current Rust compiler version or the specified features. The build simulation resolves the exact dependency tree. - Extract the resolved dependency graph: Trivy captures the precise list of all crates and their versions, including transitive dependencies, that the build process would use.
- Scan the extracted graph: Each identified crate and its version is then checked against Trivy’s vulnerability database.
The output will look something like this, detailing any found CVEs for your direct and indirect dependencies:
$ trivy fs .
2023-10-27T10:30:00.123Z [INFO] Vulnerability scanning...
[
{
"Target": "my-rust-app",
"Type": "rustc",
"Vulnerabilities": [
{
"PkgName": "actix-web",
"InstalledVersion": "4.0.0",
"PkgID": "actix-web@4.0.0",
"Severity": "HIGH",
"Title": "Denial of Service vulnerability in actix-web",
"Description": "A vulnerability in actix-web allows an attacker to cause a denial of service...",
"FixedVersion": "4.3.1",
"References": [
"CVE-2022-XXXX",
"..."
]
},
{
"PkgName": "serde",
"InstalledVersion": "1.0.147",
"PkgID": "serde@1.0.147",
"Severity": "MEDIUM",
"Title": "Deserialization vulnerability in serde",
"Description": "Deserializing untrusted data in serde can lead to arbitrary code execution...",
"FixedVersion": "1.0.150",
"References": [
"CVE-2022-YYYY",
"..."
]
}
// ... more vulnerabilities
]
}
]
This process is powerful because it bypasses the common pitfall of relying solely on Cargo.lock. Cargo.lock represents a potential dependency resolution, but not necessarily the active one for your specific build environment. Trivy’s build simulation ensures you’re scanning what’s actually being compiled.
The core problem this solves is the "dependency hell" that arises from nested dependencies. A vulnerability in a deeply nested crate, one you didn’t even explicitly add, can still compromise your entire application. Trivy’s ability to reconstruct the full dependency graph, including transitive dependencies, makes these hidden risks visible.
The levers you control are primarily through your Cargo.toml and Cargo.lock files. By updating dependencies in Cargo.toml and running cargo build (or cargo update), you generate a new Cargo.lock that reflects newer, potentially more secure versions. Trivy then scans this updated lock file. For more granular control or to scan specific sub-projects within a larger workspace, you can point Trivy at the relevant directory containing the Cargo.toml and Cargo.lock.
What most people don’t realize is that the actix-web = "4.0.0" in your Cargo.toml doesn’t just pull in actix-web version 4.0.0. It pulls in actix-web 4.0.0, which in turn might depend on tokio 1.20.0, which might depend on bytes 1.1.0. Trivy’s build simulation meticulously traces this entire chain, ensuring that even if bytes 1.1.0 has a vulnerability, and you never explicitly added bytes to your project, Trivy will find it.
The next concept you’ll run into is understanding how Trivy handles different package managers and languages, as the underlying principles of dependency graph resolution and vulnerability scanning apply broadly.