crates/nornir-introspect/src/binsbom.rs
//! `binsbom` — the **binary-derived SBOM** producer (task #65 slice 4a): the
//! components actually present in the *shipped bytes*, distinct from the
//! source-side `cargo metadata` SBOM (which lists the resolved dep tree, missing
//! what dead-code-elimination pruned out and what a vendored `.a` linked in).
//!
//! Pure leaf algorithm: it reads the per-crate roll-up the size analyzer already
//! attributed from the artifact's symbols + DWARF ([`crate::binscan::BinaryReport`])
//! and emits one component per crate that carries code in the binary. The nornir
//! side maps these into `sbom_components` rows with `derived_from = "binary"`
//! (reusing `security.rs`'s CycloneDX / OSV machinery) and CVE-matches them — a
//! CVE found only in the binary SBOM is the high-value catch the source SBOM
//! misses. The `version` is best-effort (empty when the bytes carry no embedded
//! version string); CVE matching keys on the crate name.
//!
//! ROOT-LAW #0: trivial serial map over an already-computed roll-up — no fan-out.
use serde::{Deserialize, Serialize};
use crate::binscan::BinaryReport;
/// One component derived from the shipped bytes. `name` is the crate the symbols
/// were attributed to; `version` is best-effort (empty when unknown); `size_bytes`
/// is the code this component contributes (the bloat weight).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct BinComponent {
/// The component / crate name (the DWARF `krate` or demangled path root).
pub name: String,
/// Best-effort version (empty when the bytes carry no version string).
pub version: String,
/// Bytes of code this component contributes to the artifact.
pub size_bytes: u64,
/// Number of symbols attributed to this component.
pub symbol_count: u64,
}
/// Derive the binary SBOM from an analysed artifact: one [`BinComponent`] per
/// attributed crate present in the bytes, largest first. Unattributed symbols
/// (empty crate name) are dropped — a nameless component is not a supply-chain
/// fact worth shipping. Pure; deterministic (the roll-up is already size-sorted).
pub fn derive_components(report: &BinaryReport) -> Vec<BinComponent> {
report
.crates
.iter()
.filter(|c| !c.crate_name.is_empty())
.map(|c| BinComponent {
name: c.crate_name.clone(),
version: String::new(),
size_bytes: c.size_bytes,
symbol_count: c.symbol_count,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
/// RED-WHEN-BROKEN: derive the binary SBOM from the **test binary itself** (the
/// self-exe pattern) and assert it yields named, code-bearing components — i.e.
/// the shipped bytes carry attributable crates. Any regression in the crate
/// roll-up or the derivation turns this red.
#[test]
fn derive_components_from_selfexe() {
let bin = std::env::current_exe().expect("current test exe");
let report = crate::binscan::analyze(&bin, Some(Path::new("/"))).expect("analyze");
let comps = derive_components(&report);
assert!(!comps.is_empty(), "≥1 binary-derived component");
assert!(
comps.iter().all(|c| !c.name.is_empty()),
"no nameless components"
);
assert!(
comps.iter().all(|c| c.size_bytes > 0),
"every component carries code"
);
// Size-sorted (roll-up order preserved).
assert!(
comps.windows(2).all(|w| w[0].size_bytes >= w[1].size_bytes),
"components are largest-first"
);
}
}