auditable-serde-0.8.0/.cargo_vcs_info.json0000644000000001550000000000100140740ustar { "git": { "sha1": "ceb4475d237b0296a3ddb946e0337fb658743ccc" }, "path_in_vcs": "auditable-serde" }auditable-serde-0.8.0/CHANGELOG.md000064400000000000000000000030001046102023000144650ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.8.0] - 2024-11-11 ### Removed - Removed the conversion from `cargo_metadata` structures. The `cargo_metadata` crate makes breaking changes quite frequently, and we need to be able to upgrade it without breaking semver on this crate. ## [0.7.0] - 2024-07-30 ### Changed - Removed the disabled-by-default conversion from the internal format to Cargo.lock. The Cargo.lock format is unstable, and the conversion to CycloneDX is a better idea these days. ## [0.6.1] - 2024-02-19 ### Fixed - `from_metadata` feature: Fixed creating a cyclic dependency graph under [certain conditions](https://github.com/rustsec/rustsec/issues/1043). ## [0.6.0] - 2023-04-27 ### Changed - `toml` feature: upgraded to `cargo-lock` crate v9.x ### Fixed - Fixed changelog formatting ## [0.5.2] - 2022-10-24 ### Changed - `toml` feature: Versions are no longer roundtripped through `&str`, resulting in faster conversion. - `toml` feature: `cargo_lock::Dependency.source` field is now populated when when converting into `cargo-lock` crate format. ### Added - This changelog file ## [0.5.1] - 2022-10-02 ### Added - JSON schema (thanks to @tofay) - A mention of the `auditable-info` crate in the crate documentation ## [0.5.0] - 2022-08-08 ### Changed - This is the first feature-complete release auditable-serde-0.8.0/Cargo.toml0000644000000024650000000000100121000ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "auditable-serde" version = "0.8.0" authors = ['Sergey "Shnatsel" Davidoff '] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Serialize/deserialize data encoded by `cargo auditable`" readme = "README.md" categories = ["encoding"] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" [package.metadata.docs.rs] all-features = true [lib] name = "auditable_serde" path = "src/lib.rs" [dependencies.schemars] version = "0.8.10" optional = true [dependencies.semver] version = "1.0" features = ["serde"] [dependencies.serde] version = "1" features = ["serde_derive"] [dependencies.serde_json] version = "1.0.57" [dependencies.topological-sort] version = "0.2.2" [features] default = [] schema = ["schemars"] auditable-serde-0.8.0/Cargo.toml.orig000064400000000000000000000012051046102023000155500ustar 00000000000000[package] name = "auditable-serde" version = "0.8.0" authors = ["Sergey \"Shnatsel\" Davidoff "] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" description = "Serialize/deserialize data encoded by `cargo auditable`" categories = ["encoding"] edition = "2018" [package.metadata.docs.rs] all-features = true [features] default = [] schema = ["schemars"] [dependencies] serde = { version = "1", features = ["serde_derive"] } serde_json = "1.0.57" semver = { version = "1.0", features = ["serde"] } topological-sort = "0.2.2" schemars = {version = "0.8.10", optional = true } auditable-serde-0.8.0/README.md000064400000000000000000000035231046102023000141450ustar 00000000000000Parses and serializes the JSON dependency tree embedded in executables by the [`cargo auditable`](https://github.com/rust-secure-code/cargo-auditable). This crate defines the data structures that a serialized to/from JSON and implements the serialization/deserialization routines via `serde`. The [`VersionInfo`] struct is where all the magic happens, see the docs on it for more info. ## Basic usage **Note:** this is a low-level crate that only implements JSON parsing. It rarely should be used directly. You probably want the higher-level [`auditable-info`](https://docs.rs/auditable-info) crate instead. The following snippet demonstrates full extraction pipeline using this crate, including platform-specific executable handling via [`auditable-extract`](http://docs.rs/auditable-serde/) and decompression using the safe-Rust [`miniz_oxide`](http://docs.rs/miniz_oxide/): ```rust,ignore use std::io::{Read, BufReader}; use std::{error::Error, fs::File, str::FromStr}; fn main() -> Result<(), Box> { // Read the input let f = File::open("target/release/hello-world")?; let mut f = BufReader::new(f); let mut input_binary = Vec::new(); f.read_to_end(&mut input_binary)?; // Extract the compressed audit data let compressed_audit_data = auditable_extract::raw_auditable_data(&input_binary)?; // Decompress it with your Zlib implementation of choice. We recommend miniz_oxide use miniz_oxide::inflate::decompress_to_vec_zlib; let decompressed_data = decompress_to_vec_zlib(&compressed_audit_data) .map_err(|_| "Failed to decompress audit data")?; let decompressed_data = String::from_utf8(decompressed_data)?; println!("{}", decompressed_data); // Parse the audit data to Rust data structures let dependency_tree = auditable_serde::VersionInfo::from_str(&decompressed_data); Ok(()) } ``` auditable-serde-0.8.0/src/lib.rs000064400000000000000000000143061046102023000145720ustar 00000000000000#![forbid(unsafe_code)] #![allow(clippy::redundant_field_names)] #![doc = include_str!("../README.md")] mod validation; use validation::RawVersionInfo; use serde::{Deserialize, Serialize}; use std::str::FromStr; /// Dependency tree embedded in the binary. /// /// Implements `Serialize` and `Deserialize` traits from `serde`, so you can use /// [all the usual methods from serde-json](https://docs.rs/serde_json/1.0.57/serde_json/#functions) /// to read and write it. /// /// `from_str()` that parses JSON is also implemented for your convenience: /// ```rust /// use auditable_serde::VersionInfo; /// use std::str::FromStr; /// let json_str = r#"{"packages":[{ /// "name":"adler", /// "version":"0.2.3", /// "source":"registry" /// }]}"#; /// let info = VersionInfo::from_str(json_str).unwrap(); /// assert_eq!(&info.packages[0].name, "adler"); /// ``` /// /// If deserialization succeeds, it is guaranteed that there is only one root package, /// and that are no cyclic dependencies. #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[serde(try_from = "RawVersionInfo")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct VersionInfo { pub packages: Vec, } /// A single package in the dependency tree #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub struct Package { /// Crate name specified in the `name` field in Cargo.toml file. Examples: "libc", "rand" pub name: String, /// The package's version in the [semantic version](https://semver.org) format. #[cfg_attr(feature = "schema", schemars(with = "String"))] pub version: semver::Version, /// Currently "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc. pub source: Source, /// "build" or "runtime". May be omitted if set to "runtime". /// If it's both a build and a runtime dependency, "runtime" is recorded. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub kind: DependencyKind, /// Packages are stored in an ordered array both in the `VersionInfo` struct and in JSON. /// Here we refer to each package by its index in the array. /// May be omitted if the list is empty. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub dependencies: Vec, /// Whether this is the root package in the dependency tree. /// There should only be one root package. /// May be omitted if set to `false`. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub root: bool, } /// Serializes to "git", "local", "crates.io" or "registry". Designed to be extensible with other revision control systems, etc. #[non_exhaustive] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] #[serde(from = "&str")] #[serde(into = "String")] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum Source { CratesIo, Git, Local, Registry, Other(String), } impl From<&str> for Source { fn from(s: &str) -> Self { match s { "crates.io" => Self::CratesIo, "git" => Self::Git, "local" => Self::Local, "registry" => Self::Registry, other_str => Self::Other(other_str.to_string()), } } } impl From for String { fn from(s: Source) -> String { match s { Source::CratesIo => "crates.io".to_owned(), Source::Git => "git".to_owned(), Source::Local => "local".to_owned(), Source::Registry => "registry".to_owned(), Source::Other(string) => string, } } } #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Default)] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] pub enum DependencyKind { // The values are ordered from weakest to strongest so that casting to integer would make sense #[serde(rename = "build")] Build, #[default] #[serde(rename = "runtime")] Runtime, } fn is_default(value: &T) -> bool { let default_value = T::default(); value == &default_value } impl FromStr for VersionInfo { type Err = serde_json::Error; fn from_str(s: &str) -> Result { serde_json::from_str(s) } } #[cfg(test)] mod tests { #![allow(unused_imports)] // otherwise conditional compilation emits warnings use super::*; use std::fs; use std::{ convert::TryInto, path::{Path, PathBuf}, }; #[cfg(feature = "schema")] /// Generate a JsonSchema for VersionInfo fn generate_schema() -> schemars::schema::RootSchema { let mut schema = schemars::schema_for!(VersionInfo); let mut metadata = *schema.schema.metadata.clone().unwrap(); let title = "cargo-auditable schema".to_string(); metadata.title = Some(title); metadata.id = Some("https://rustsec.org/schemas/cargo-auditable.json".to_string()); metadata.examples = [].to_vec(); metadata.description = Some( "Describes the `VersionInfo` JSON data structure that cargo-auditable embeds into Rust binaries." .to_string(), ); schema.schema.metadata = Some(Box::new(metadata)); schema } #[test] #[cfg(feature = "schema")] fn verify_schema() { use schemars::schema::RootSchema; let expected = generate_schema(); // Printing here makes it easier to update the schema when required println!( "expected schema:\n{}", serde_json::to_string_pretty(&expected).unwrap() ); let contents = fs::read_to_string( // `CARGO_MANIFEST_DIR` env is path to dir containing auditable-serde's Cargo.toml PathBuf::from(env!("CARGO_MANIFEST_DIR")) .parent() .unwrap() .join("cargo-auditable.schema.json"), ) .expect("error reading existing schema"); let actual: RootSchema = serde_json::from_str(&contents).expect("error deserializing existing schema"); assert_eq!(expected, actual); } } auditable-serde-0.8.0/src/validation.rs000064400000000000000000000064751046102023000161660ustar 00000000000000use crate::{Package, VersionInfo}; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fmt::Display}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub struct RawVersionInfo { pub packages: Vec, } pub enum ValidationError { MultipleRoots, CyclicDependency, } impl Display for ValidationError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ValidationError::MultipleRoots => { write!(f, "Multiple root packages specified in the input JSON") } ValidationError::CyclicDependency => { write!(f, "The input JSON specifies a cyclic dependency graph") } } } } impl TryFrom for VersionInfo { type Error = ValidationError; fn try_from(v: RawVersionInfo) -> Result { if has_multiple_root_packages(&v) { Err(ValidationError::MultipleRoots) } else if has_cylic_dependencies(&v) { Err(ValidationError::CyclicDependency) } else { Ok(VersionInfo { packages: v.packages, }) } } } fn has_multiple_root_packages(v: &RawVersionInfo) -> bool { let mut seen_a_root = false; for package in &v.packages { if package.root { if seen_a_root { return true; } else { seen_a_root = true; } } } false } fn has_cylic_dependencies(v: &RawVersionInfo) -> bool { // I've reviewed the `topological_sort` crate and it appears to be high-quality, // so I'm not concerned about having it exposed to untrusted input. // It's better than my hand-rolled version would have been. // populate the topological sorting map let mut ts = topological_sort::TopologicalSort::::new(); for (index, package) in v.packages.iter().enumerate() { for dep in &package.dependencies { ts.add_dependency(*dep, index); } } // drain all elements that are not part of a cycle while ts.pop().is_some() {} // if the set isn't empty, the graph has cycles !ts.is_empty() } #[cfg(test)] mod tests { use std::str::FromStr; use super::*; use crate::*; fn dummy_package(pkg_counter: u32, root: bool, deps: Vec) -> Package { Package { name: format!("test_{pkg_counter}"), version: semver::Version::from_str("0.0.0").unwrap(), source: Source::Local, kind: DependencyKind::Build, dependencies: deps, root: root, } } // these tests are very basic because `topological_sort` crate is already tested extensively #[test] fn cyclic_dependencies() { let pkg0 = dummy_package(0, true, vec![1]); let pkg1 = dummy_package(1, false, vec![0]); let raw = RawVersionInfo { packages: vec![pkg0, pkg1], }; assert!(VersionInfo::try_from(raw).is_err()); } #[test] fn no_cyclic_dependencies() { let pkg0 = dummy_package(0, true, vec![1]); let pkg1 = dummy_package(1, false, vec![]); let raw = RawVersionInfo { packages: vec![pkg0, pkg1], }; assert!(VersionInfo::try_from(raw).is_ok()); } }