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