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

nordisk/dwarves

crates/dwarves-slsa/tests/pipeline_run.rs

//! A release build end to end as a Runa pipeline (lane RELBUILD-RUNA), with a stand-in
//! executor: the forge's own actions run for real (the checkout of a real git repo, the
//! seal, the vendor check, the staging of a packaged crate), the sealed containers are
//! simulated, and the provenance is written from the run's record.

use std::collections::BTreeMap;
use std::path::Path;
use std::process::Command;
use std::sync::Arc;

use dwarves_runa::{Pipeline, Step};
use dwarves_slsa::runner::{self, Executor, ReleaseRun, RunOptions, RunRecord, StepRecord};
use dwarves_slsa::spec::ReleaseSpec;
use dwarves_slsa::statement::BUILDER_ID;
use dwarves_slsa::Result;

const IMAGE: &str = "localhost/nordisk-cargo-toolchain@sha256:875a7a325dfe7f6b838535d085a2a502e8ad250076c947f6d023b1e224f03334";

fn git(dir: &Path, args: &[&str]) -> String {
    let o = Command::new("git")
        .arg("-C")
        .arg(dir)
        .args(["-c", "user.name=t", "-c", "user.email=t@example.invalid", "-c", "commit.gpgsign=false"])
        .args(args)
        .output()
        .unwrap();
    assert!(o.status.success(), "git {args:?}: {}", String::from_utf8_lossy(&o.stderr));
    String::from_utf8_lossy(&o.stdout).trim().to_string()
}

/// A repository `demo` with one crate at `crates/demo` and its lock; returns the commit.
fn repo(from: &Path) -> String {
    let r = from.join("demo");
    std::fs::create_dir_all(r.join("crates/demo/src")).unwrap();
    std::fs::write(
        r.join("crates/demo/Cargo.toml"),
        "[package]\nname = \"demo\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
    )
    .unwrap();
    std::fs::write(r.join("crates/demo/src/lib.rs"), "pub fn x() {}\n").unwrap();
    std::fs::write(r.join("crates/demo/Cargo.lock"), "version = 4\n\n[[package]]\nname = \"demo\"\nversion = \"0.1.0\"\n").unwrap();
    git(&r, &["init", "-q"]);
    git(&r, &["add", "-A"]);
    git(&r, &["commit", "-q", "-m", "demo"]);
    git(&r, &["rev-parse", "HEAD"])
}

fn spec(commit: &str) -> ReleaseSpec {
    ReleaseSpec::parse(&format!(
        r#"
name = "demo-0.1.0"
image = "{IMAGE}"
lock = ["demo/crates/demo/Cargo.toml"]
[[repo]]
name = "demo"
org = "nordisk"
commit = "{commit}"
[[crate]]
name = "demo"
version = "0.1.0"
repo = "demo"
path = "crates/demo"
"#
    ))
    .unwrap()
}

/// `name-version/` as a `.crate` (tar.gz).
fn dot_crate(to: &Path, name: &str, version: &str) {
    let f = std::fs::File::create(to).unwrap();
    let mut t = tar::Builder::new(flate2::write::GzEncoder::new(f, flate2::Compression::default()));
    for (p, body) in [("Cargo.toml", format!("[package]\nname = \"{name}\"\nversion = \"{version}\"\n")), ("src/lib.rs", "pub fn x() {}\n".to_string())] {
        let mut h = tar::Header::new_gnu();
        h.set_size(body.len() as u64);
        h.set_mode(0o644);
        h.set_cksum();
        t.append_data(&mut h, format!("{name}-{version}/{p}"), body.as_bytes()).unwrap();
    }
    t.into_inner().unwrap().finish().unwrap();
}

/// Runs each stage in order: `Forge` steps through the release run for real, `Sandbox`
/// steps simulated by what their cargo would leave behind.
struct StandIn {
    red_at: Option<&'static str>,
}

impl Executor for StandIn {
    fn execute(&self, p: &Pipeline, run: Arc<ReleaseRun>, run_id: &str) -> Result<RunRecord> {
        let l = &run.layout;
        let mut steps = Vec::new();
        for st in &p.stages {
            for s in &st.steps {
                let (label, ok) = match s {
                    Step::Forge { action, args } => {
                        let r = run.forge(action, args, &mut |m| eprintln!("  [{}] {m}", st.name));
                        if let Err(e) = &r {
                            eprintln!("{}: {e}", st.name);
                        }
                        ("Forge", r.is_ok())
                    }
                    Step::Sandbox { cmd, network, .. } => {
                        match cmd[1].as_str() {
                            "vendor" => std::fs::create_dir_all(l.vendor().join("crates")).unwrap(),
                            "generate-lockfile" => std::fs::write(
                                l.probe().join("Cargo.lock"),
                                "version = 4\n\n[[package]]\nname = \"dwarves-slsa-probe\"\nversion = \"0.0.0\"\n",
                            )
                            .unwrap(),
                            "package" => {
                                assert!(!network, "a package step has no network");
                                let d = l.target().join("demo/package");
                                std::fs::create_dir_all(&d).unwrap();
                                dot_crate(&d.join("demo-0.1.0.crate"), "demo", "0.1.0");
                            }
                            other => panic!("{other}"),
                        }
                        ("Sandbox", self.red_at != Some(st.name.as_str()))
                    }
                    other => panic!("{other:?}"),
                };
                steps.push(StepRecord {
                    stage: st.name.clone(),
                    step: label.into(),
                    status: if ok { "passed" } else { "failed" }.into(),
                    duration_ms: 1,
                    log: None,
                });
                if !ok {
                    return Ok(RunRecord {
                        run_id: run_id.into(),
                        pipeline: p.name.clone(),
                        green: false,
                        steps,
                        reason: Some(format!("{} failed", st.name)),
                    });
                }
            }
        }
        Ok(RunRecord { run_id: run_id.into(), pipeline: p.name.clone(), green: true, steps, reason: None })
    }
}

fn opts(run_dir: &Path, from: &Path) -> RunOptions {
    RunOptions {
        run_dir: run_dir.into(),
        from: Some(from.into()),
        jobs: 2,
        rebuild_published: true,
        keep: true,
        run_id: Some("demo-0.1.0-abcdef012345".into()),
        builder_version: BTreeMap::new(),
    }
}

#[test]
fn a_release_runs_as_a_runa_pipeline_and_its_statement_names_the_run() {
    let d = tempfile::tempdir().unwrap();
    let from = d.path().join("from");
    let commit = repo(&from);
    let run_dir = d.path().join("run");
    let st = runner::run(&spec(&commit), &opts(&run_dir, &from), &StandIn { red_at: None }).unwrap();
    let v: serde_json::Value = serde_json::from_slice(&std::fs::read(&st).unwrap()).unwrap();
    assert_eq!(v["subject"][0]["name"], "crates/demo-0.1.0.crate");
    let sha = dwarves_slsa::subjects::sha256_file(&run_dir.join("out/crates/demo-0.1.0.crate")).unwrap();
    assert_eq!(v["subject"][0]["digest"]["sha256"], sha);
    let pred = &v["predicate"];
    assert_eq!(pred["runDetails"]["builder"]["id"], BUILDER_ID);
    let ip = &pred["buildDefinition"]["internalParameters"];
    assert_eq!(ip["runa"]["pipeline"], "release-build");
    assert_eq!(ip["runa"]["runId"], "demo-0.1.0-abcdef012345");
    let steps: Vec<&str> = ip["runa"]["steps"].as_array().unwrap().iter().map(|s| s.as_str().unwrap()).collect();
    assert!(steps.contains(&"package-0-demo Sandbox passed"), "{steps:?}");
    assert!(ip.get("innerScriptSha256").is_none(), "no shell script any more");
    assert_eq!(ip["sandbox"]["shell"], false);
    assert!(pred["runDetails"]["builder"]["version"]["runnerSha256"].is_string());
    let res = pred["buildDefinition"]["resolvedDependencies"].as_array().unwrap();
    assert!(res.iter().any(|r| r["digest"]["gitCommit"] == commit.as_str()));
    // the pipeline as it ran is a byproduct, and the forge staged the package in Rust
    let by = pred["runDetails"]["byproducts"].as_array().unwrap();
    assert!(by.iter().any(|b| b["name"] == "logs/pipeline.runa.ron"));
    let ck: serde_json::Value =
        serde_json::from_slice(&std::fs::read(run_dir.join("stage/demo-0.1.0/.cargo-checksum.json")).unwrap()).unwrap();
    assert_eq!(ck["package"], sha);
    assert!(ck["files"]["src/lib.rs"].is_string() && ck["files"]["Cargo.toml"].is_string());
    // the snapshot is read-only
    let m = std::fs::metadata(run_dir.join("src/demo/crates/demo/Cargo.toml")).unwrap();
    assert!(m.permissions().readonly());
}

#[test]
fn a_red_run_writes_no_statement() {
    let d = tempfile::tempdir().unwrap();
    let from = d.path().join("from");
    let commit = repo(&from);
    let run_dir = d.path().join("run");
    let e = runner::run(&spec(&commit), &opts(&run_dir, &from), &StandIn { red_at: Some("package-0-demo") }).unwrap_err();
    assert!(e.0.contains("red") && e.0.contains("package-0-demo"), "{e}");
    assert!(!run_dir.join("release.statement.json").exists());
}

#[test]
fn the_forge_refuses_an_action_it_does_not_know_and_a_crate_staged_twice() {
    let d = tempfile::tempdir().unwrap();
    let l = runner::Layout::new(d.path());
    let rr = ReleaseRun::new(spec(&"a".repeat(40)), l.clone(), None, BTreeMap::new());
    let e = rr.forge("shell", &[], &mut |_| {}).unwrap_err();
    assert!(e.0.contains("not an action"), "{e}");
    std::fs::create_dir_all(l.stage()).unwrap();
    let f = d.path().join("x-1.0.0.crate");
    dot_crate(&f, "x", "1.0.0");
    runner::stage_crate(&l.stage(), "x", "1.0.0", &f).unwrap();
    let e = runner::stage_crate(&l.stage(), "x", "1.0.0", &f).unwrap_err();
    assert!(e.0.contains("twice"), "{e}");
    // a vendored crate of the same name and version (a link) gives way
    std::os::unix::fs::symlink("/vendor/crates/y-1.0.0", l.stage().join("y-1.0.0")).unwrap();
    let g = d.path().join("y-1.0.0.crate");
    dot_crate(&g, "y", "1.0.0");
    runner::stage_crate(&l.stage(), "y", "1.0.0", &g).unwrap();
    assert!(l.stage().join("y-1.0.0/Cargo.toml").is_file());
    // a .crate whose entries leave its directory is refused
    let h = d.path().join("z-1.0.0.crate");
    dot_crate(&h, "w", "1.0.0");
    assert!(runner::stage_crate(&l.stage(), "z", "1.0.0", &h).is_err());
}