Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
400 changes: 383 additions & 17 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,4 @@ Do not rely on SBOMs when dealing with supply chain attacks!

### What is blocking uplifting this into Cargo?

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.
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.
1 change: 1 addition & 0 deletions cargo-auditable/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ cargo_metadata = "0.18"
pico-args = { version = "0.5", features = ["eq-separator", "short-space-opt"] }
serde = "1.0.147"
wasm-gen = "0.1.4"
cargo-util-schemas = "0.8.1"

[dev-dependencies]
cargo_metadata = "0.18"
Expand Down
26 changes: 23 additions & 3 deletions cargo-auditable/src/collect_audit_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,33 @@ use std::str::from_utf8;

use crate::{
auditable_from_metadata::encode_audit_data, cargo_arguments::CargoArgs,
rustc_arguments::RustcArgs,
rustc_arguments::RustcArgs, sbom_precursor,
};

/// Calls `cargo metadata` to obtain the dependency tree, serializes it to JSON and compresses it
pub fn compressed_dependency_list(rustc_args: &RustcArgs, target_triple: &str) -> Vec<u8> {
let metadata = get_metadata(rustc_args, target_triple);
let version_info = encode_audit_data(&metadata).unwrap();
let sbom_path = std::env::var_os("CARGO_SBOM_PATH");

// If cargo has created precursor SBOM files, use them instead of `cargo metadata`.
let version_info = if sbom_path.as_ref().map(|p| !p.is_empty()).unwrap_or(false) {
let sbom_paths = std::env::split_paths(&sbom_path.unwrap()).collect::<Vec<_>>();
// Cargo may create multiple SBOM precursor files.
// We can't control per-binary (or cdylib) dependency information, just grab the first non-rlib SBOM we find.
let sbom_path = sbom_paths
.iter()
.find(|p| !p.ends_with(".rlib.cargo-sbom.json"))
.unwrap_or_else(|| &sbom_paths[0]);
let sbom_data: Vec<u8> = std::fs::read(sbom_path)
.unwrap_or_else(|_| panic!("Failed to read SBOM file at {}", sbom_path.display()));
let sbom_precursor: sbom_precursor::SbomPrecursor = serde_json::from_slice(&sbom_data)
.unwrap_or_else(|_| panic!("Failed to parse SBOM file at {}", sbom_path.display()));
sbom_precursor.into()
} else {
// If no SBOM files are available, fall back to `cargo metadata`
let metadata = get_metadata(rustc_args, target_triple);
encode_audit_data(&metadata).unwrap()
};

let json = serde_json::to_string(&version_info).unwrap();
// compression level 7 makes this complete in a few milliseconds, so no need to drop to a lower level in debug mode
let compressed_json = compress_to_vec_zlib(json.as_bytes(), 7);
Expand Down
1 change: 1 addition & 0 deletions cargo-auditable/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod object_file;
mod platform_detection;
mod rustc_arguments;
mod rustc_wrapper;
mod sbom_precursor;
mod target_info;

use std::process::exit;
Expand Down
132 changes: 132 additions & 0 deletions cargo-auditable/src/sbom_precursor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use std::collections::HashMap;

use auditable_serde::{Package, Source, VersionInfo};
use cargo_metadata::DependencyKind;
use cargo_util_schemas::core::{PackageIdSpec, SourceKind};
use serde::{Deserialize, Serialize};

/// Cargo SBOM precursor format.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SbomPrecursor {
/// Schema version
pub version: u32,
/// Index into the crates array for the root crate
pub root: usize,
/// Array of all crates
pub crates: Vec<Crate>,
/// Information about rustc used to perform the compilation
pub rustc: RustcInfo,
}

impl From<SbomPrecursor> for VersionInfo {
fn from(sbom: SbomPrecursor) -> Self {
// cargo sbom data format has more nodes than the auditable info format - if a crate is both a build
// and runtime dependency it will appear twice in the `crates` array.
// The `VersionInfo` format lists each package only once, with a single `kind` field
// (Runtime having precence over other kinds).

// Firstly, we deduplicate the (name, version) pairs and create a mapping from the
// original indices in the cargo sbom array to the new index in the auditable info package array.
let (_, mut packages, indices) = sbom.crates.iter().enumerate().fold(
(HashMap::new(), Vec::new(), Vec::new()),
|(mut id_to_index_map, mut packages, mut indices), (index, crate_)| {
match id_to_index_map.entry(crate_.id.clone()) {
std::collections::hash_map::Entry::Occupied(entry) => {
// Just store the new index in the indices array
indices.push(*entry.get());
}
std::collections::hash_map::Entry::Vacant(entry) => {
// If the entry does not exist, we create it
packages.push(Package {
name: crate_.id.name().to_string(),
version: crate_.id.version().expect("Package to have version"),
source: match crate_.id.kind() {
Some(SourceKind::Path) => Source::Local,
Some(SourceKind::Git(_)) => Source::Git,
Some(_) => Source::Registry,
None => Source::CratesIo,
},
// Assume build, if we determine this is a runtime dependency we'll update later
kind: auditable_serde::DependencyKind::Build,
// We will fill this in later
dependencies: Vec::new(),
root: index == sbom.root,
});
entry.insert(packages.len() - 1);
indices.push(packages.len() - 1);
}
}
(id_to_index_map, packages, indices)
},
);

// Traverse the graph as given by the sbom to fill in the dependencies with the new indices.
//
// Keep track of whether the dependency is a runtime dependency.
// If we ever encounter a non-runtime dependency, all deps in the remaining subtree
// are not runtime dependencies, i.e a runtime dep of a build dep is not recognized as a runtime dep.
let mut stack = Vec::new();
stack.push((sbom.root, true));
while let Some((old_index, is_runtime)) = stack.pop() {
let crate_ = &sbom.crates[old_index];
for dep in &crate_.dependencies {
stack.push((dep.index, dep.kind == DependencyKind::Normal && is_runtime));
}

let package = &mut packages[indices[old_index]];
if is_runtime {
package.kind = auditable_serde::DependencyKind::Runtime
};

for dep in &crate_.dependencies {
let new_dep_index = indices[dep.index];
if package.dependencies.contains(&new_dep_index) {
continue; // Already added this dependency
} else if new_dep_index == indices[old_index] {
// If the dependency is the same as the package itself, skip it
continue;
} else {
package.dependencies.push(new_dep_index);
}
}
}

VersionInfo { packages }
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Crate {
/// Package ID specification
pub id: PackageIdSpec,
/// List of target kinds
pub kind: Vec<String>,
/// Enabled feature flags
pub features: Vec<String>,
/// Dependencies for this crate
pub dependencies: Vec<Dependency>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
/// Index into the crates array
pub index: usize,
/// Dependency kind: "normal", "build", or "dev"
pub kind: DependencyKind,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustcInfo {
/// Compiler version
pub version: String,
/// Compiler wrapper
pub wrapper: Option<String>,
/// Compiler workspace wrapper
pub workspace_wrapper: Option<String>,
/// Commit hash for rustc
pub commit_hash: String,
/// Host target triple
pub host: String,
/// Verbose version string: `rustc -vV`
pub verbose_version: String,
}
1 change: 1 addition & 0 deletions cargo-auditable/tests/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cargo.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
Loading
Loading