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

nordisk/dwarves

xtask/src/build_history.rs

//! **`cargo xtask build-history`** — step 3 of the four, made runnable.
//!
//! ```text
//! 1  the recipe crate is published    wave-0, before the thing it builds
//! 2  the build runs                   the artifact gets its receipt
//! 3  znippy-build-thing is created    history / audit log, BEFORE release
//! 4  release                          the artifact goes out carrying itself
//! ```
//!
//! Step 3 before step 4 is the point: the history exists when the release
//! happens, not afterwards. And the package is always sealed **after** the
//! build, because the artifact's own hash does not exist until the artifact is
//! finished.
//!
//! ```text
//! seal        <artifacts>  ->  the .znippy. Refuses on any artifact it cannot attest.
//! verify      <artifacts>  ->  the release gate: every artifact covered and matching.
//! regenerate  <artifacts>  ->  reseal from the artifacts and diff against the sealed
//!                              package. "Derived, never a source of truth" as a check.
//! show                     ->  what did this recipe build?
//! ```
//!
//! # Where the work is not
//!
//! Nothing here computes an attestation or decides a layout — that is
//! `nornir_build_thing::history`, the dep-light leaf every member repo already
//! links. This module is the two things the leaf deliberately does not do: it
//! links znippy and seals the archive, and it turns outcomes into exit codes.
//!
//! # Exit codes
//!
//! `0` only when the thing asked for actually held. An artifact that could not
//! be measured exits non-zero exactly like one that failed: an unmeasured check
//! is never an approved check, and a release gate that passes on "could not
//! tell" is not a gate.

use std::path::{Path, PathBuf};

use nornir_build_thing::history::{HistoryPackage, Unattestable, PACKAGE_KIND};

/// znippy compression level for a sealed history. The package is a few kB of
/// JSON; this is not a throughput decision.
const LEVEL: i32 = 3;

/// What the verb was asked to do.
struct Args {
    verb: String,
    package: PathBuf,
    artifacts: Vec<PathBuf>,
    recipe: Option<String>,
    allow_unattested: bool,
}

fn usage() -> i32 {
    eprintln!("dwarves xtask build-history — the forge's build history, sealed as a znippy");
    eprintln!();
    eprintln!("usage: cargo xtask build-history <verb> --package <file.znippy> [artifacts…]");
    eprintln!();
    eprintln!("verbs:");
    eprintln!("  seal        attest every artifact and seal the package. Step 3: run it");
    eprintln!("              AFTER the build and BEFORE the release.");
    eprintln!("  verify      the release gate — every artifact must be covered by the");
    eprintln!("              sealed package and hash to what it attests.");
    eprintln!("  regenerate  reseal from the artifacts and diff against the sealed");
    eprintln!("              package. The package is derived; this is the proof.");
    eprintln!("  show        what did this recipe build?  --recipe <krate>[:<version>]");
    eprintln!();
    eprintln!("flags:");
    eprintln!("  --package <file.znippy>   the history package (required)");
    eprintln!("  --recipe <krate>[:<ver>]  narrow `show` to one recipe");
    eprintln!("  --allow-unattested        seal anyway when an artifact carries no receipt.");
    eprintln!("                            Off by default: a history that quietly covers");
    eprintln!("                            four of five artifacts looks complete.");
    2
}

fn parse(args: &[String]) -> Result<Args, i32> {
    let mut verb = String::new();
    let mut package = None;
    let mut recipe = None;
    let mut allow_unattested = false;
    let mut artifacts = Vec::new();
    let mut it = args.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "--package" => package = it.next().map(PathBuf::from),
            "--recipe" => recipe = it.next().cloned(),
            "--allow-unattested" => allow_unattested = true,
            "-h" | "--help" => return Err(usage()),
            other if other.starts_with('-') => {
                eprintln!("build-history: unknown flag {other}");
                return Err(usage());
            }
            other if verb.is_empty() => verb = other.to_string(),
            other => artifacts.push(PathBuf::from(other)),
        }
    }
    let Some(package) = package else {
        eprintln!("build-history: --package <file.znippy> is required");
        return Err(usage());
    };
    Ok(Args {
        verb,
        package,
        artifacts,
        recipe,
        allow_unattested,
    })
}

/// Seal a package's files into a `.znippy`.
fn seal_files(path: &Path, files: &[(String, Vec<u8>)]) -> anyhow::Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    znippy_common::meta_sink_append::create_archive(path, files, LEVEL)?;
    Ok(())
}

/// Read a sealed package's files back out.
fn read_files(path: &Path) -> anyhow::Result<Vec<(String, Vec<u8>)>> {
    use znippy_common::{ZnippyArchive, ZnippyReader};
    let archive = ZnippyArchive::open(path)?;
    let mut out = Vec::new();
    for p in archive.list_files()? {
        let bytes = archive.extract_file(&p)?;
        out.push((p, bytes));
    }
    out.sort_by(|a, b| a.0.cmp(&b.0));
    Ok(out)
}

/// Report every artifact that could not be attested, and say whether that is
/// fatal.
fn report_refusals(refused: &[Unattestable], allow: bool) -> bool {
    if refused.is_empty() {
        return true;
    }
    eprintln!(
        "build-history: {} artifact(s) could not be attested:",
        refused.len()
    );
    for r in refused {
        eprintln!("  {r}");
    }
    if allow {
        eprintln!(
            "build-history: --allow-unattested was given, so the history is sealed WITHOUT them"
        );
        return true;
    }
    eprintln!(
        "build-history: refusing to seal a partial history. A history that covers some of the \
         artifacts looks exactly like one that covers all of them, and the difference only \
         surfaces when somebody asks about the one that is missing. Pass --allow-unattested to \
         seal anyway."
    );
    false
}

fn seal(args: &Args) -> i32 {
    if args.artifacts.is_empty() {
        eprintln!("build-history seal: no artifacts given");
        return 2;
    }
    let (package, refused) = HistoryPackage::from_artifacts(&args.artifacts);
    if !report_refusals(&refused, args.allow_unattested) {
        return 1;
    }
    let files = match package.files() {
        Ok(f) => f,
        Err(e) => {
            eprintln!("build-history seal: {e}");
            return 1;
        }
    };
    if let Err(e) = seal_files(&args.package, &files) {
        eprintln!("build-history seal: {e:#}");
        return 1;
    }
    let bytes = std::fs::metadata(&args.package)
        .map(|m| m.len())
        .unwrap_or(0);
    println!(
        "build-history: sealed {} attestation(s) into {} ({bytes} bytes, {} files)",
        package.len(),
        args.package.display(),
        files.len()
    );
    for a in package.attestations() {
        println!(
            "  {} {} {} <- {}",
            &a.artifact.sha256[..16],
            a.receipt.atom,
            a.receipt.target,
            a.receipt.build_thing
        );
    }
    0
}

/// Open a sealed package, or explain why not.
fn open(path: &Path) -> Result<HistoryPackage, i32> {
    let files = match read_files(path) {
        Ok(f) => f,
        Err(e) => {
            eprintln!("build-history: cannot read {}: {e:#}", path.display());
            return Err(1);
        }
    };
    HistoryPackage::from_files(&files).map_err(|e| {
        eprintln!(
            "build-history: {} is not a usable {PACKAGE_KIND}: {e}",
            path.display()
        );
        1
    })
}

fn verify(args: &Args) -> i32 {
    if args.artifacts.is_empty() {
        eprintln!(
            "build-history verify: no artifacts given — a gate with nothing to check is not a gate"
        );
        return 2;
    }
    let package = match open(&args.package) {
        Ok(p) => p,
        Err(c) => return c,
    };
    let mut failed = 0usize;
    for a in &args.artifacts {
        let verdict = package.verify_artifact(a);
        if verdict.holds() {
            println!("  ATTESTED  {}", a.display());
        } else {
            failed += 1;
            eprintln!("  FAILED    {}: {verdict}", a.display());
        }
    }
    if failed == 0 {
        println!(
            "build-history: {} artifact(s) attested by {}",
            args.artifacts.len(),
            args.package.display()
        );
        0
    } else {
        eprintln!(
            "build-history: {failed} of {} artifact(s) are NOT attested — the history must be \
             sealed after the build and before the release, so this means the order was not kept",
            args.artifacts.len()
        );
        1
    }
}

fn regenerate(args: &Args) -> i32 {
    if args.artifacts.is_empty() {
        eprintln!("build-history regenerate: no artifacts given");
        return 2;
    }
    let sealed = match read_files(&args.package) {
        Ok(f) => f,
        Err(e) => {
            eprintln!(
                "build-history: cannot read {}: {e:#}",
                args.package.display()
            );
            let _ = e;
            return 1;
        }
    };
    let (package, refused) = HistoryPackage::from_artifacts(&args.artifacts);
    if !report_refusals(&refused, args.allow_unattested) {
        return 1;
    }
    let fresh = match package.files() {
        Ok(f) => f,
        Err(e) => {
            eprintln!("build-history regenerate: {e}");
            return 1;
        }
    };
    if fresh == sealed {
        println!(
            "build-history: {} regenerates byte-identically from {} artifact(s) — it is derived, \
             not a source of truth",
            args.package.display(),
            args.artifacts.len()
        );
        return 0;
    }

    eprintln!(
        "build-history: {} does NOT match what these artifacts regenerate to",
        args.package.display()
    );
    let names =
        |v: &[(String, Vec<u8>)]| -> Vec<String> { v.iter().map(|(p, _)| p.clone()).collect() };
    let (sn, fnames) = (names(&sealed), names(&fresh));
    for p in &fnames {
        if !sn.contains(p) {
            eprintln!("  only in the regenerated package: {p}");
        }
    }
    for p in &sn {
        if !fnames.contains(p) {
            eprintln!("  only in the sealed package:      {p}");
        }
    }
    for (p, b) in &fresh {
        if let Some((_, s)) = sealed.iter().find(|(sp, _)| sp == p) {
            if s != b {
                eprintln!(
                    "  differs: {p} ({} sealed vs {} regenerated bytes)",
                    s.len(),
                    b.len()
                );
            }
        }
    }
    1
}

fn show(args: &Args) -> i32 {
    let package = match open(&args.package) {
        Ok(p) => p,
        Err(c) => return c,
    };
    let selected = match &args.recipe {
        None => package.attestations().iter().collect::<Vec<_>>(),
        Some(spec) => {
            let (krate, version) = match spec.split_once(':') {
                Some((k, v)) => (k, Some(v)),
                None => (spec.as_str(), None),
            };
            package.by_recipe(krate, version)
        }
    };
    if selected.is_empty() {
        // Not an error: "this recipe built nothing" is an answer, and it is the
        // answer a CVE sweep is hoping for.
        println!(
            "build-history: nothing in {} matches",
            args.package.display()
        );
        return 0;
    }
    println!(
        "build-history: {} of {} attestation(s) in {}",
        selected.len(),
        package.len(),
        args.package.display()
    );
    for a in selected {
        println!(
            "  {}  {} {}  <- {}  ({} bytes)",
            a.artifact.sha256,
            a.receipt.atom,
            a.receipt.target,
            a.receipt.build_thing,
            a.artifact.bytes
        );
    }
    0
}

/// Dispatch. Returns the process exit code.
pub fn run(args: &[String]) -> i32 {
    let args = match parse(args) {
        Ok(a) => a,
        Err(c) => return c,
    };
    match args.verb.as_str() {
        "seal" => seal(&args),
        "verify" => verify(&args),
        "regenerate" => regenerate(&args),
        "show" => show(&args),
        "" => usage(),
        other => {
            eprintln!("build-history: unknown verb `{other}`");
            usage()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use nornir_build_thing::history::AttestationVerdict;

    /// `--package` is not optional, and its absence is a usage error rather than
    /// a default path nobody meant.
    #[test]
    fn a_missing_package_flag_is_refused_rather_than_defaulted() {
        assert!(parse(&["seal".to_string()]).is_err());
        let ok = parse(&[
            "seal".to_string(),
            "--package".to_string(),
            "h.znippy".to_string(),
            "a".to_string(),
            "b".to_string(),
        ])
        .expect("parses");
        assert_eq!(ok.verb, "seal");
        assert_eq!(ok.artifacts.len(), 2, "artifacts are positional");
        assert!(!ok.allow_unattested);
    }

    /// A verb with nothing to check exits non-zero. A gate that passes when it
    /// was handed nothing is the shape of every hollow green in this tree.
    #[test]
    fn a_gate_with_no_artifacts_does_not_pass() {
        let args = parse(&[
            "verify".to_string(),
            "--package".to_string(),
            "h.znippy".to_string(),
        ])
        .expect("parses");
        assert_ne!(verify(&args), 0);
        assert_ne!(seal(&args), 0);
        assert_ne!(regenerate(&args), 0);
    }

    /// Refusals are fatal by default and only waived when asked.
    #[test]
    fn an_unattestable_artifact_stops_the_seal_unless_it_is_waived() {
        let refused = vec![Unattestable::NoReceipt(PathBuf::from("a.bin"))];
        assert!(!report_refusals(&refused, false));
        assert!(report_refusals(&refused, true));
        assert!(
            report_refusals(&[], false),
            "no refusals must not be reported as a refusal"
        );
    }

    /// The verdict type's `holds` is what the exit code turns on, so it gets an
    /// assertion of its own rather than being trusted.
    #[test]
    fn only_an_attested_verdict_holds() {
        assert!(AttestationVerdict::Attested.holds());
        assert!(!AttestationVerdict::ReceiptMismatch.holds());
        assert!(!AttestationVerdict::Unreadable("x".into()).holds());
        assert!(!AttestationVerdict::ArtifactMismatch {
            attested: "a".into(),
            actual: "b".into(),
        }
        .holds());
    }
}