Skip to content

Commit 3634e94

Browse files
committed
cargo sbom integration
If cargo sbom function is enabled, cargo-auditable will read the SBOM precursor file and use it to generate dependency information rather than trying to use the `cargo metadata` command.
1 parent 4edf5c7 commit 3634e94

File tree

11 files changed

+623
-36
lines changed

11 files changed

+623
-36
lines changed

Cargo.lock

Lines changed: 383 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,4 @@ Do not rely on SBOMs when dealing with supply chain attacks!
104104

105105
### What is blocking uplifting this into Cargo?
106106

107-
The [RFC for this functionality in Cargo itself](https://github.com/rust-lang/rfcs/pull/2801) has been [postponed](https://github.com/rust-lang/rfcs/pull/2801#issuecomment-2122880841) by the Cargo team until the [more foundational SBOM RFC](https://github.com/rust-lang/rfcs/pull/3553) is implemented.
107+
The [RFC for this functionality in Cargo itself](https://github.com/rust-lang/rfcs/pull/2801) has been [postponed](https://github.com/rust-lang/rfcs/pull/2801#issuecomment-2122880841) by the Cargo team until the [more foundational SBOM RFC](https://github.com/rust-lang/rfcs/pull/3553) is implemented. That RFC has now been implemented and is available via an [unstable feature](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom). cargo-auditable integrates with this: if you enable that feature and build with cargo auditable, e.g with `CARGO_BUILD_SBOM=true cargo auditable -Z sbom build` and a nightly Rust toolchain, then cargo auditable will use the SBOM precursor files generated by cargo.

cargo-auditable/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ cargo_metadata = "0.18"
2121
pico-args = { version = "0.5", features = ["eq-separator", "short-space-opt"] }
2222
serde = "1.0.147"
2323
wasm-gen = "0.1.4"
24+
cargo-util-schemas = "0.8.1"
2425

2526
[dev-dependencies]
2627
cargo_metadata = "0.18"

cargo-auditable/src/collect_audit_data.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,33 @@ use std::str::from_utf8;
44

55
use crate::{
66
auditable_from_metadata::encode_audit_data, cargo_arguments::CargoArgs,
7-
rustc_arguments::RustcArgs,
7+
rustc_arguments::RustcArgs, sbom_precursor,
88
};
99

1010
/// Calls `cargo metadata` to obtain the dependency tree, serializes it to JSON and compresses it
1111
pub fn compressed_dependency_list(rustc_args: &RustcArgs, target_triple: &str) -> Vec<u8> {
12-
let metadata = get_metadata(rustc_args, target_triple);
13-
let version_info = encode_audit_data(&metadata).unwrap();
12+
let sbom_path = std::env::var_os("CARGO_SBOM_PATH");
13+
14+
// If cargo has created precursor SBOM files, use them instead of `cargo metadata`.
15+
let version_info = if sbom_path.as_ref().map(|p| !p.is_empty()).unwrap_or(false) {
16+
let sbom_paths = std::env::split_paths(&sbom_path.unwrap()).collect::<Vec<_>>();
17+
// Cargo may create multiple SBOM precursor files.
18+
// We can't control per-binary (or cdylib) dependency information, just grab the first non-rlib SBOM we find.
19+
let sbom_path = sbom_paths
20+
.iter()
21+
.find(|p| !p.ends_with(".rlib.cargo-sbom.json"))
22+
.unwrap_or_else(|| &sbom_paths[0]);
23+
let sbom_data: Vec<u8> = std::fs::read(sbom_path)
24+
.unwrap_or_else(|_| panic!("Failed to read SBOM file at {}", sbom_path.display()));
25+
let sbom_precursor: sbom_precursor::SbomPrecursor = serde_json::from_slice(&sbom_data)
26+
.unwrap_or_else(|_| panic!("Failed to parse SBOM file at {}", sbom_path.display()));
27+
sbom_precursor.into()
28+
} else {
29+
// If no SBOM files are available, fall back to `cargo metadata`
30+
let metadata = get_metadata(rustc_args, target_triple);
31+
encode_audit_data(&metadata).unwrap()
32+
};
33+
1434
let json = serde_json::to_string(&version_info).unwrap();
1535
// compression level 7 makes this complete in a few milliseconds, so no need to drop to a lower level in debug mode
1636
let compressed_json = compress_to_vec_zlib(json.as_bytes(), 7);

cargo-auditable/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod object_file;
99
mod platform_detection;
1010
mod rustc_arguments;
1111
mod rustc_wrapper;
12+
mod sbom_precursor;
1213
mod target_info;
1314

1415
use std::process::exit;

cargo-auditable/src/sbom_precursor.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use std::collections::HashMap;
2+
3+
use auditable_serde::{Package, Source, VersionInfo};
4+
use cargo_metadata::DependencyKind;
5+
use cargo_util_schemas::core::{PackageIdSpec, SourceKind};
6+
use serde::{Deserialize, Serialize};
7+
8+
/// Cargo SBOM precursor format.
9+
#[derive(Debug, Clone, Serialize, Deserialize)]
10+
pub struct SbomPrecursor {
11+
/// Schema version
12+
pub version: u32,
13+
/// Index into the crates array for the root crate
14+
pub root: usize,
15+
/// Array of all crates
16+
pub crates: Vec<Crate>,
17+
/// Information about rustc used to perform the compilation
18+
pub rustc: RustcInfo,
19+
}
20+
21+
impl From<SbomPrecursor> for VersionInfo {
22+
fn from(sbom: SbomPrecursor) -> Self {
23+
// cargo sbom data format has more nodes than the auditable info format - if a crate is both a build
24+
// and runtime dependency it will appear twice in the `crates` array.
25+
// The `VersionInfo` format lists each package only once, with a single `kind` field
26+
// (Runtime having precence over other kinds).
27+
28+
// Firstly, we deduplicate the (name, version) pairs and create a mapping from the
29+
// original indices in the cargo sbom array to the new index in the auditable info package array.
30+
let (_, mut packages, indices) = sbom.crates.iter().enumerate().fold(
31+
(HashMap::new(), Vec::new(), Vec::new()),
32+
|(mut id_to_index_map, mut packages, mut indices), (index, crate_)| {
33+
match id_to_index_map.entry(crate_.id.clone()) {
34+
std::collections::hash_map::Entry::Occupied(entry) => {
35+
// Just store the new index in the indices array
36+
indices.push(*entry.get());
37+
}
38+
std::collections::hash_map::Entry::Vacant(entry) => {
39+
// If the entry does not exist, we create it
40+
packages.push(Package {
41+
name: crate_.id.name().to_string(),
42+
version: crate_.id.version().expect("Package to have version"),
43+
source: match crate_.id.kind() {
44+
Some(SourceKind::Path) => Source::Local,
45+
Some(SourceKind::Git(_)) => Source::Git,
46+
Some(_) => Source::Registry,
47+
None => Source::CratesIo,
48+
},
49+
// Assume build, if we determine this is a runtime dependency we'll update later
50+
kind: auditable_serde::DependencyKind::Build,
51+
// We will fill this in later
52+
dependencies: Vec::new(),
53+
root: index == sbom.root,
54+
});
55+
entry.insert(packages.len() - 1);
56+
indices.push(packages.len() - 1);
57+
}
58+
}
59+
(id_to_index_map, packages, indices)
60+
},
61+
);
62+
63+
// Traverse the graph as given by the sbom to fill in the dependencies with the new indices.
64+
//
65+
// Keep track of whether the dependency is a runtime dependency.
66+
// If we ever encounter a non-runtime dependency, all deps in the remaining subtree
67+
// are not runtime dependencies, i.e a runtime dep of a build dep is not recognized as a runtime dep.
68+
let mut stack = Vec::new();
69+
stack.push((sbom.root, true));
70+
while let Some((old_index, is_runtime)) = stack.pop() {
71+
let crate_ = &sbom.crates[old_index];
72+
for dep in &crate_.dependencies {
73+
stack.push((dep.index, dep.kind == DependencyKind::Normal && is_runtime));
74+
}
75+
76+
let package = &mut packages[indices[old_index]];
77+
if is_runtime {
78+
package.kind = auditable_serde::DependencyKind::Runtime
79+
};
80+
81+
for dep in &crate_.dependencies {
82+
let new_dep_index = indices[dep.index];
83+
if package.dependencies.contains(&new_dep_index) {
84+
continue; // Already added this dependency
85+
} else if new_dep_index == indices[old_index] {
86+
// If the dependency is the same as the package itself, skip it
87+
continue;
88+
} else {
89+
package.dependencies.push(new_dep_index);
90+
}
91+
}
92+
}
93+
94+
VersionInfo { packages }
95+
}
96+
}
97+
98+
#[derive(Debug, Clone, Serialize, Deserialize)]
99+
pub struct Crate {
100+
/// Package ID specification
101+
pub id: PackageIdSpec,
102+
/// List of target kinds
103+
pub kind: Vec<String>,
104+
/// Enabled feature flags
105+
pub features: Vec<String>,
106+
/// Dependencies for this crate
107+
pub dependencies: Vec<Dependency>,
108+
}
109+
110+
#[derive(Debug, Clone, Serialize, Deserialize)]
111+
pub struct Dependency {
112+
/// Index into the crates array
113+
pub index: usize,
114+
/// Dependency kind: "normal", "build", or "dev"
115+
pub kind: DependencyKind,
116+
}
117+
118+
#[derive(Debug, Clone, Serialize, Deserialize)]
119+
pub struct RustcInfo {
120+
/// Compiler version
121+
pub version: String,
122+
/// Compiler wrapper
123+
pub wrapper: Option<String>,
124+
/// Compiler workspace wrapper
125+
pub workspace_wrapper: Option<String>,
126+
/// Commit hash for rustc
127+
pub commit_hash: String,
128+
/// Host target triple
129+
pub host: String,
130+
/// Verbose version string: `rustc -vV`
131+
pub verbose_version: String,
132+
}

cargo-auditable/tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cargo.lock
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() {}

0 commit comments

Comments
 (0)