auditable-info-0.10.0/.cargo_vcs_info.json0000644000000001540000000000100137750ustar { "git": { "sha1": "65266184d1f37b3a04db5423108323c6e75fbe01" }, "path_in_vcs": "auditable-info" }auditable-info-0.10.0/CHANGELOG.md000064400000000000000000000020221046102023000143720ustar 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.9.0] - 2024-11-11 ### Changed - Upgraded to `auditable-serde` v0.8.x - Upgraded to `miniz_oxide` v0.8.x and removed its types from the public API to simplify future upgrades ## [0.8.0] - 2024-07-30 ### Changed - Upgraded to `auditable-serde` v0.7.x ## [0.7.2] - 2024-05-08 ### Changed - Upgraded to `auditable-extract` v0.3.4, removing nearly all dependencies with `unsafe` code in them pulled in by `wasm` feature - Enabled the `wasm` feature by default now that it doesn't pull in `unsafe` code ## [0.7.1] - 2024-05-03 ### Added - Added WebAssembly support, gated behind the non-default `wasm` feature ## [0.7.0] - 2023-04-27 ### Changed - Upgraded to `auditable-serde` v0.6.x, the only change is an upgrade to `cargo-lock` v9.x ### Added - This changelog file auditable-info-0.10.0/Cargo.lock0000644000000105120000000000100117470ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "auditable-extract" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44371e9f9759dea49c42b6c6fe4c64ea216ee2af325a4524a7180823e00d3e7a" dependencies = [ "binfarce", "wasmparser", ] [[package]] name = "auditable-info" version = "0.10.0" dependencies = [ "auditable-extract", "auditable-serde", "miniz_oxide", "serde_json", ] [[package]] name = "auditable-serde" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d026218ae25ba5c72834245412dd1338f6d270d2c5109ee03a4badec288d4056" dependencies = [ "semver", "serde", "serde_json", "topological-sort", ] [[package]] name = "binfarce" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "18464ccbb85e5dede30d70cc7676dc9950a0fb7dbf595a43d765be9123c616a2" [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[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 = "miniz_oxide" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ "adler2", ] [[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 = "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_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" [[package]] name = "wasmparser" version = "0.207.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e19bb9f8ab07616da582ef8adb24c54f1424c7ec876720b7da9db8ec0626c92c" dependencies = [ "bitflags", ] auditable-info-0.10.0/Cargo.toml0000644000000026100000000000100117720ustar # 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-info" version = "0.10.0" authors = ['Sergey "Shnatsel" Davidoff '] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "High-level crate to extract the dependency trees embedded in binaries by `cargo auditable`." readme = "README.md" categories = ["encoding"] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" [features] default = [ "serde", "wasm", ] serde = [ "serde_json", "auditable-serde", ] wasm = ["auditable-extract/wasm"] [lib] name = "auditable_info" path = "src/lib.rs" [dependencies.auditable-extract] version = "0.3.4" default-features = false [dependencies.auditable-serde] version = "0.9.0" optional = true [dependencies.miniz_oxide] version = "0.8.0" features = ["std"] [dependencies.serde_json] version = "1.0.57" optional = true auditable-info-0.10.0/Cargo.toml.orig000064400000000000000000000015661046102023000154640ustar 00000000000000[package] name = "auditable-info" version = "0.10.0" authors = ["Sergey \"Shnatsel\" Davidoff "] license = "MIT OR Apache-2.0" repository = "https://github.com/rust-secure-code/cargo-auditable" description = "High-level crate to extract the dependency trees embedded in binaries by `cargo auditable`." categories = ["encoding"] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] auditable-extract = {version = "0.3.4", path = "../auditable-extract", default-features = false } miniz_oxide = { version = "0.8.0", features = ["std"] } auditable-serde = {version = "0.9.0", path = "../auditable-serde", optional = true} serde_json = { version = "1.0.57", optional = true } [features] default = ["serde", "wasm"] serde = ["serde_json", "auditable-serde"] wasm = ["auditable-extract/wasm"] auditable-info-0.10.0/README.md000064400000000000000000000030221046102023000140410ustar 00000000000000High-level crate to extract the dependency trees embedded in binaries by [`cargo auditable`](https://crates.io/crates/cargo-auditable). Deserializes them to a JSON string or Rust data structures, at your option. ### Features - Binary parsing designed from the ground up for resilience to malicious inputs. - 100% memory-safe Rust, including all dependencies. (There is some `unsafe` in `serde_json` and its dependencies, but only in serialization, which isn't used here). - Cross-platform, portable, easy to cross-compile. Runs on [any Rust target with `std`](https://doc.rust-lang.org/stable/rustc/platform-support.html). - Parses binaries from any supported platform, not just the platform it's running on. - Supports setting size limits for both input and output, to protect against [OOMs](https://en.wikipedia.org/wiki/Out_of_memory) and [zip bombs](https://en.wikipedia.org/wiki/Zip_bomb). ### Usage ```rust // Uses the default limits: 1GiB input file size, 8MiB audit data size let info = audit_info_from_file(&PathBuf::from("path/to/file"), Default::default())?; ``` Functions to load the data from a `Read` instance or from `&[u8]` are also provided, see the [documentation](https://docs.rs/auditable-info). ### Alternatives [`rust-audit-info`](https://crates.io/crates/rust-audit-info) is a command-line interface to this crate. If you need a lower-level interface than the one provided by this crate, use the [`auditable-extract`](http://docs.rs/auditable-extract/) and [`auditable-serde`](http://docs.rs/auditable-serde/) crates. auditable-info-0.10.0/src/error.rs000064400000000000000000000115321046102023000150550ustar 00000000000000#[derive(Debug)] pub enum Error { NoAuditData, InputLimitExceeded, OutputLimitExceeded, Io(std::io::Error), BinaryParsing(auditable_extract::Error), Decompression(DecompressError), #[cfg(feature = "serde")] Json(serde_json::Error), Utf8(std::str::Utf8Error), } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::NoAuditData => write!(f, "No audit data found in the binary! Was it built with 'cargo auditable'?"), Error::InputLimitExceeded => write!(f, "The input file is too large. Increase the input size limit to scan it."), Error::OutputLimitExceeded => write!(f, "Audit data size is over the specified limit. Increase the output size limit to scan it."), Error::Io(e) => write!(f, "Failed to read the binary: {e}"), Error::BinaryParsing(e) => write!(f, "Failed to parse the binary: {e}"), Error::Decompression(e) => write!(f, "Failed to decompress audit data: {e}"), #[cfg(feature = "serde")] Error::Json(e) => write!(f, "Failed to deserialize audit data from JSON: {e}"), Error::Utf8(e) => write!(f, "Invalid UTF-8 in audit data: {e}"), } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Error::NoAuditData => None, Error::InputLimitExceeded => None, Error::OutputLimitExceeded => None, Error::Io(e) => Some(e), Error::BinaryParsing(e) => Some(e), Error::Decompression(e) => Some(e), #[cfg(feature = "serde")] Error::Json(e) => Some(e), Error::Utf8(e) => Some(e), } } } impl From for Error { fn from(e: std::io::Error) -> Self { Self::Io(e) } } impl From for Error { fn from(e: auditable_extract::Error) -> Self { match e { auditable_extract::Error::NoAuditData => Error::NoAuditData, other_err => Self::BinaryParsing(other_err), } } } impl From for Error { fn from(e: DecompressError) -> Self { match e.status { TINFLStatus::HasMoreOutput => Error::OutputLimitExceeded, _ => Error::Decompression(e), } } } impl From for Error { fn from(e: std::string::FromUtf8Error) -> Self { Self::Utf8(e.utf8_error()) } } #[cfg(feature = "serde")] impl From for Error { fn from(e: serde_json::Error) -> Self { Self::Json(e) } } /// A copy of [miniz_oxide::inflate::DecompressError]. /// /// We use our copy instead of the miniz_oxide type directly /// so that we don't have to bump semver every time `miniz_oxide` does. #[derive(Debug)] pub struct DecompressError { /// Decompressor status on failure. See [TINFLStatus] for details. pub status: TINFLStatus, /// The currently decompressed data if any. pub output: Vec, } impl std::fmt::Display for DecompressError { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { f.write_str(match self.status { TINFLStatus::FailedCannotMakeProgress => "Truncated input stream", TINFLStatus::BadParam => "Invalid output buffer size", TINFLStatus::Adler32Mismatch => "Adler32 checksum mismatch", TINFLStatus::Failed => "Invalid input data", TINFLStatus::Done => unreachable!(), TINFLStatus::NeedsMoreInput => "Truncated input stream", TINFLStatus::HasMoreOutput => "Output size exceeded the specified limit", }) } } impl std::error::Error for DecompressError {} impl DecompressError { pub(crate) fn from_miniz(err: miniz_oxide::inflate::DecompressError) -> Self { Self { status: TINFLStatus::from_miniz(err.status), output: err.output, } } } #[repr(i8)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum TINFLStatus { FailedCannotMakeProgress, BadParam, Adler32Mismatch, Failed, Done, NeedsMoreInput, HasMoreOutput, } impl TINFLStatus { pub(crate) fn from_miniz(status: miniz_oxide::inflate::TINFLStatus) -> Self { use miniz_oxide::inflate; match status { inflate::TINFLStatus::FailedCannotMakeProgress => Self::FailedCannotMakeProgress, inflate::TINFLStatus::BadParam => Self::BadParam, inflate::TINFLStatus::Adler32Mismatch => Self::Adler32Mismatch, inflate::TINFLStatus::Failed => Self::Failed, inflate::TINFLStatus::Done => Self::Done, inflate::TINFLStatus::NeedsMoreInput => Self::NeedsMoreInput, inflate::TINFLStatus::HasMoreOutput => Self::HasMoreOutput, } } } auditable-info-0.10.0/src/lib.rs000064400000000000000000000207051046102023000144740ustar 00000000000000#![forbid(unsafe_code)] //! High-level crate to extract the dependency trees embedded in binaries by [`cargo auditable`](https://crates.io/crates/cargo-auditable). //! //! Deserializes them to a JSON string or Rust data structures, at your option. //! //! ```rust, ignore //! // Uses the default limits: 1GiB input file size, 8MiB audit data size //! let info = audit_info_from_file(&PathBuf::from("path/to/file"), Default::default())?; //! ``` //! Functions to load the data from a `Read` instance or from `&[u8]` are also provided. //! //! The supported formats are [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format), //! [PE](https://en.wikipedia.org/wiki/Portable_Executable), //! [Mach-O](https://en.wikipedia.org/wiki/Mach-O) and [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly). //! //! If you need an even lower-level interface than the one provided by this crate, //! use the [`auditable-extract`](http://docs.rs/auditable-extract/) and //! [`auditable-serde`](http://docs.rs/auditable-serde/) crates. use auditable_extract::raw_auditable_data; #[cfg(feature = "serde")] use auditable_serde::VersionInfo; use miniz_oxide::inflate::decompress_to_vec_zlib_with_limit; use std::fs::File; use std::io::{BufRead, BufReader, Read}; use std::path::Path; mod error; pub use crate::error::*; /// Loads audit info from the specified binary compiled with `cargo auditable`. /// /// The entire file is loaded into memory. The RAM usage limit can be configured using the [`Limits`] struct. /// /// ```rust, ignore /// // Uses the default limits: 1GiB input file size, 8MiB audit data size /// let info = audit_info_from_file(&PathBuf::from("path/to/file"), Default::default())?; /// ``` /// /// The data is validated to only have a single root package and not contain any circular dependencies. #[cfg(feature = "serde")] pub fn audit_info_from_file(path: &Path, limits: Limits) -> Result { Ok(serde_json::from_str(&json_from_file(path, limits)?)?) } /// Extracts the audit data from the specified binary and returns the JSON string. /// This is useful if you want to forward the data somewhere instead of parsing it to Rust data structures. /// /// If you want to obtain the Zlib-compressed data instead, /// use the [`auditable-extract`](https://docs.rs/auditable-extract/) crate directly. pub fn json_from_file(path: &Path, limits: Limits) -> Result { let file = File::open(path)?; let mut reader = BufReader::new(file); json_from_reader(&mut reader, limits) } /// Loads audit info from the binary loaded from an arbitrary reader, e.g. the standard input. /// /// ```rust, ignore /// let stdin = io::stdin(); /// let mut handle = stdin.lock(); /// // Uses the default limits: 1GiB input file size, 8MiB audit data size /// let info = audit_info_from_reader(&mut handle, Default::default())?; /// ``` /// /// The data is validated to only have a single root package and not contain any circular dependencies. #[cfg(feature = "serde")] pub fn audit_info_from_reader( reader: &mut T, limits: Limits, ) -> Result { Ok(serde_json::from_str(&json_from_reader(reader, limits)?)?) } /// Extracts the audit data and returns the JSON string. /// This is useful if you want to forward the data somewhere instead of parsing it to Rust data structures. /// /// If you want to obtain the Zlib-compressed data instead, /// use the [`auditable-extract`](https://docs.rs/auditable-extract/) crate directly. pub fn json_from_reader(reader: &mut T, limits: Limits) -> Result { let compressed_data = get_compressed_audit_data(reader, limits)?; let decompressed_data = decompress_to_vec_zlib_with_limit(&compressed_data, limits.decompressed_json_size) .map_err(DecompressError::from_miniz)?; Ok(String::from_utf8(decompressed_data)?) } // Factored into its own function for ease of unit testing, // and also so that the large allocation of the input file is dropped // before we start decompressing the data to minimize peak memory usage fn get_compressed_audit_data(reader: &mut T, limits: Limits) -> Result, Error> { // In case you're wondering why the check for the limit is weird like that: // When .take() returns EOF, it doesn't tell you if that's because it reached the limit // or because the underlying reader ran out of data. // And we need to return an error when the reader is over limit, else we'll truncate the audit data. // So it would be reasonable to run `into_inner()` and check if that reader has any data remaining... // But readers can return EOF sporadically - a reader may return EOF, // then get more data and return bytes again instead of EOF! // So instead we read as many bytes as the limit allows, plus one. // If we've read the limit-plus-one bytes, that means the underlying reader was at least one byte over the limit. // That way we avoid any time-of-check/time-of-use issues. let incremented_limit = u64::saturating_add(limits.input_file_size as u64, 1); let mut f = reader.take(incremented_limit); let mut input_binary = Vec::new(); f.read_to_end(&mut input_binary)?; if input_binary.len() as u64 == incremented_limit { Err(Error::InputLimitExceeded)? } let compressed_audit_data = raw_auditable_data(&input_binary)?; if compressed_audit_data.len() > limits.decompressed_json_size { Err(Error::OutputLimitExceeded)?; } Ok(compressed_audit_data.to_owned()) } /// The input slice should contain the entire binary. /// This function is useful if you have already loaded the binary to memory, e.g. via memory-mapping. #[cfg(feature = "serde")] pub fn audit_info_from_slice( input_binary: &[u8], decompressed_json_size_limit: usize, ) -> Result { Ok(serde_json::from_str(&json_from_slice( input_binary, decompressed_json_size_limit, )?)?) } /// The input slice should contain the entire binary. /// This function is useful if you have already loaded the binary to memory, e.g. via memory-mapping. /// /// Returns the decompressed audit data. /// This is useful if you want to forward the data somewhere instead of parsing it to Rust data structures. /// /// If you want to obtain the Zlib-compressed data instead, /// use the [`auditable-extract`](https://docs.rs/auditable-extract/) crate directly. pub fn json_from_slice( input_binary: &[u8], decompressed_json_size_limit: usize, ) -> Result { let compressed_audit_data = raw_auditable_data(input_binary)?; if compressed_audit_data.len() > decompressed_json_size_limit { Err(Error::OutputLimitExceeded)?; } let decompressed_data = decompress_to_vec_zlib_with_limit(compressed_audit_data, decompressed_json_size_limit) .map_err(DecompressError::from_miniz)?; Ok(String::from_utf8(decompressed_data)?) } /// Protects against [denial-of-service attacks](https://en.wikipedia.org/wiki/Denial-of-service_attack) /// via infinite input streams or [zip bombs](https://en.wikipedia.org/wiki/Zip_bomb), /// which would otherwise use up all your memory and crash your machine. /// /// If the limit is exceeded, an error is returned and no further deserialization is attempted. /// /// The default limits are **1 GiB** for the `input_file_size` and **8 MiB** for `decompressed_json_size`. /// /// Note that the `decompressed_json_size` is only enforced on the level of the *serialized* JSON, i.e. a string. /// We do not enforce that `serde_json` does not consume more memory when deserializing JSON to Rust data structures. /// Unfortunately Rust does not provide APIs for that. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct Limits { pub input_file_size: usize, pub decompressed_json_size: usize, } impl Default for Limits { fn default() -> Self { Self { input_file_size: 1024 * 1024 * 1024, // 1GiB decompressed_json_size: 1024 * 1024 * 8, // 8MiB } } } #[cfg(test)] mod tests { use super::*; #[test] fn input_file_limits() { let limits = Limits { input_file_size: 128, decompressed_json_size: 99999, }; let fake_data = vec![0; 1024]; let mut reader = std::io::Cursor::new(fake_data); let result = get_compressed_audit_data(&mut reader, limits); assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("The input file is too large")); } }