Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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"
        );
    }
}