auditable-serde-0.9.0/.cargo_vcs_info.json0000644000000001550000000000100140750ustar { "git": { "sha1": "65266184d1f37b3a04db5423108323c6e75fbe01" }, "path_in_vcs": "auditable-serde" }auditable-serde-0.9.0/CHANGELOG.md000064400000000000000000000030001046102023000144660ustar 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.9.0/Cargo.lock0000644000000074460000000000100120620ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "auditable-serde" version = "0.9.0" dependencies = [ "schemars", "semver", "serde", "serde_json", "topological-sort", ] [[package]] name = "dyn-clone" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "itoa" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "ryu" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schemars" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" dependencies = [ "dyn-clone", "schemars_derive", "serde", "serde_json", ] [[package]] name = "schemars_derive" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", "syn", ] [[package]] name = "semver" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" dependencies = [ "serde", ] [[package]] name = "serde" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_derive_internals" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", "memchr", "ryu", "serde", ] [[package]] name = "syn" version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" auditable-serde-0.9.0/Cargo.toml0000644000000025050000000000100120740ustar # 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.9.0" authors = ['Sergey "Shnatsel" Davidoff '] build = false autolib = 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 [features] default = [] schema = ["schemars"] [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" auditable-serde-0.9.0/Cargo.toml.orig000064400000000000000000000012051046102023000155510ustar 00000000000000[package] name = "auditable-serde" version = "0.9.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.9.0/README.md000064400000000000000000000035231046102023000141460ustar 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.9.0/src/lib.rs000064400000000000000000000177531046102023000146040ustar 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, /// Format revision. Identifies the data source for the audit data. /// /// Format revisions are **backwards compatible.** /// If an unknown format is encountered, it should be treated as the highest known preceding format. /// For example, if formats `0`, `1` and `8` are known, format `4` should be treated as if it's `1`. /// /// # Known formats /// /// ## 0 (or the field is absent) /// /// Generated based on the data provided by [`cargo metadata`](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html). /// /// There are multiple [known](https://github.com/rust-lang/cargo/issues/7754) /// [issues](https://github.com/rust-lang/cargo/issues/10718) with this data source, /// leading to the audit data sometimes including more dependencies than are really used in the build. /// /// However, is the only machine-readable data source available on stable Rust as of v1.88. /// /// Additionally, this format incorrectly includes [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html) /// and their dependencies as runtime dependencies while in reality they are build-time dependencies. /// /// ## 1 /// /// Same as 0, but correctly records proc-macros and their dependencies as build-time dependencies. /// /// May still include slightly more dependencies than are actually used, especially in workspaces. /// /// ## 8 /// /// Generated using Cargo's [SBOM precursor](https://doc.rust-lang.org/cargo/reference/unstable.html#sbom) as the data source. /// /// This data is highly accurate, but as of Rust v1.88 can only be generated using a nightly build of Cargo. #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub format: u32, } /// 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, } pub(crate) 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.9.0/src/validation.rs000064400000000000000000000067741046102023000161710ustar 00000000000000use crate::{is_default, Package, VersionInfo}; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fmt::Display}; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] pub(crate) struct RawVersionInfo { pub packages: Vec, #[serde(default)] #[serde(skip_serializing_if = "is_default")] pub format: u32, } 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, format: v.format, }) } } } 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], format: 0, }; 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], format: 0, }; assert!(VersionInfo::try_from(raw).is_ok()); } }