xtask/src/demo.rs
//! **`cargo xtask demo`** — the Holger 1.0 demo as one command (lane G1).
//!
//! * `demo airgap` builds what the demo needs (`holger-server` with
//! `secure_update`, and `holger-cli` for its `bundle` verbs), runs the declared demo tests (`demo
//! tests`, below), then `scripts/demo-airgap.sh`: an online intake Holger and
//! a secure Holger in a podman pod with no network, every step of the intake
//! chain, the signed airgap export, the verified import and a real `pip
//! install` on the secure side. It reads the receipt the script wrote
//! (`receipt.json`, one check per step) and is green only when every check
//! passed. A check whose lane has not landed (NOT YET) makes the verb
//! INCOMPLETE (exit 2), never green.
//! * `demo tests` is the declared row for the demo tests that otherwise ran
//! only by hand: `intake_serves_pip` with its ignored real-pip arm,
//! `airgap_export_import`, and `pull_through_apk` with its ignored real
//! `apk add` (lane T8), under their own features.
//!
//! Flags: `--bin-dir <dir>` uses binaries built elsewhere (no build), `--out
//! <dir>` is where the receipt and the log go, `--no-tests` skips `demo tests`.
use std::path::{Path, PathBuf};
use std::process::Command;
/// The features the demo's server is built with: the intake chain, the DSSE
/// signer of the scan evidence (`sign`), the
/// `export`, `bundle`, `intel` and `intake` verbs, plus the `holger-server` name
/// the script calls.
pub const DEMO_FEATURES: &str = "holger/secure_update,holger/sign,holger/holger-server-alias";
/// The declared demo test row: (package, features, test target, include the ignored arms).
pub const DEMO_TESTS: &[(&str, &str, &str, bool)] = &[
(
"holger-server-lib",
"supply-chain",
"intake_serves_pip",
true,
),
(
"holger",
"supply-chain,secure_update,sign",
"airgap_export_import",
true,
),
// Lane T8: the APK pull-through, with its ignored real `apk add jq` in Wolfi against Holger.
(
"holger-server-lib",
"jobs,online,supply-chain",
"pull_through_apk",
true,
),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Sub {
Airgap,
Tests,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Args {
pub sub: Sub,
pub bin_dir: Option<PathBuf>,
pub out: Option<PathBuf>,
pub tests: bool,
}
pub fn parse(args: &[String]) -> Result<Args, String> {
let mut it = args.iter();
let sub = match it.next().map(String::as_str) {
Some("airgap") => Sub::Airgap,
Some("tests") => Sub::Tests,
Some(other) => {
return Err(format!(
"`demo {other}`: the demos are `airgap` and `tests`"
))
}
None => return Err(
"usage: cargo xtask demo <airgap|tests> [--bin-dir <dir>] [--out <dir>] [--no-tests]"
.into(),
),
};
let mut a = Args {
sub,
bin_dir: None,
out: None,
tests: true,
};
while let Some(flag) = it.next() {
let mut value = || {
it.next()
.cloned()
.ok_or_else(|| format!("{flag} needs a value"))
};
match flag.as_str() {
"--bin-dir" => a.bin_dir = Some(PathBuf::from(value()?)),
"--out" => a.out = Some(PathBuf::from(value()?)),
"--no-tests" => a.tests = false,
other => return Err(format!("`demo`: unknown flag {other}")),
}
}
Ok(a)
}
/// What a receipt says.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Pass {
passed: usize,
},
/// No failure, but checks whose lanes have not landed.
Incomplete {
passed: usize,
pending: Vec<String>,
},
Fail {
failed: Vec<String>,
},
}
/// Read a receipt's verdict from its checks (not from its `result` field alone:
/// an empty or truncated receipt is a failure).
pub fn verdict(receipt: &str) -> Result<Verdict, String> {
let v: serde_json::Value =
serde_json::from_str(receipt).map_err(|e| format!("the receipt is not JSON: {e}"))?;
if v["schema"] != "holger.demo-airgap-receipt/v1" {
return Err(format!(
"the receipt's schema is {}, not holger.demo-airgap-receipt/v1",
v["schema"]
));
}
let checks = v["checks"].as_array().ok_or("the receipt has no checks")?;
if checks.is_empty() {
return Ok(Verdict::Fail {
failed: vec!["the receipt has no checks".into()],
});
}
let named = |want: &str| -> Vec<String> {
checks
.iter()
.filter(|c| c["result"] == want)
.map(|c| {
format!(
"{}: {}",
c["step"].as_str().unwrap_or("?"),
c["detail"].as_str().unwrap_or("")
)
})
.collect()
};
let failed = named("FAIL");
if !failed.is_empty() || v["result"] == "fail" {
return Ok(Verdict::Fail { failed });
}
let passed = named("PASS").len();
let pending = named("NOT YET");
if pending.is_empty() && passed == checks.len() {
Ok(Verdict::Pass { passed })
} else {
Ok(Verdict::Incomplete { passed, pending })
}
}
fn cargo() -> std::ffi::OsString {
std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())
}
fn target_dir(root: &Path) -> PathBuf {
std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| root.join("target"))
}
fn status(cmd: &mut Command, what: &str) -> anyhow::Result<()> {
println!("\n=== {what} ===");
let st = cmd.status().map_err(|e| anyhow::anyhow!("{what}: {e}"))?;
anyhow::ensure!(st.success(), "{what}: {st}");
Ok(())
}
/// `demo tests`: the declared row.
pub fn tests(root: &Path) -> anyhow::Result<()> {
let mut red = Vec::new();
for (pkg, features, test, ignored) in DEMO_TESTS {
let mut cmd = Command::new(cargo());
cmd.current_dir(root).args([
"test",
"--release",
"-p",
pkg,
"--features",
features,
"--test",
test,
]);
if *ignored {
cmd.args(["--", "--include-ignored"]);
}
if let Err(e) = status(
&mut cmd,
&format!("demo test {pkg} --test {test} ({features})"),
) {
red.push(format!("{e:#}"));
}
}
anyhow::ensure!(red.is_empty(), "red demo tests: {}", red.join("; "));
Ok(())
}
/// `demo airgap`: build, the demo tests, the script, the receipt.
pub fn airgap(root: &Path, a: &Args) -> anyhow::Result<Verdict> {
let bin = match &a.bin_dir {
Some(d) => d.clone(),
None => {
status(
Command::new(cargo()).current_dir(root).args([
"build",
"--release",
"-p",
"holger",
"-p",
"holger-cli",
"--features",
DEMO_FEATURES,
"--bins",
]),
"build the demo binaries",
)?;
// The online side's SLSA provenance signer (the demo's build system).
status(
Command::new(cargo()).current_dir(root).args([
"build",
"--release",
"-p",
"holger-provenance",
"--example",
"sign_slsa",
]),
"build the demo's provenance signer",
)?;
target_dir(root).join("release")
}
};
if a.tests {
tests(root)?;
}
let out = a.out.clone().unwrap_or_else(|| {
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
target_dir(root).join("demo-airgap").join(stamp.to_string())
});
std::fs::create_dir_all(&out)?;
// The script's own exit is not the verdict; the receipt is.
let _ = status(
Command::new("bash")
.arg(root.join("scripts/demo-airgap.sh"))
.current_dir(root)
.env("HOLGER_BIN_DIR", &bin)
.env("DEMO_OUT", &out),
"scripts/demo-airgap.sh",
);
let path = out.join("receipt.json");
let body = std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("{}: {e} (the script wrote no receipt)", path.display()))?;
let v = verdict(&body).map_err(|e| anyhow::anyhow!("{}: {e}", path.display()))?;
println!(
"\nreceipt: {}\nlog: {}",
path.display(),
out.join("demo.log").display()
);
Ok(v)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|x| x.to_string()).collect()
}
#[test]
fn the_verb_reads_its_subcommand_and_flags() {
let a = parse(&s(&[
"airgap",
"--bin-dir",
"/b",
"--out",
"/o",
"--no-tests",
]))
.unwrap();
assert_eq!(
a,
Args {
sub: Sub::Airgap,
bin_dir: Some("/b".into()),
out: Some("/o".into()),
tests: false
}
);
assert_eq!(parse(&s(&["tests"])).unwrap().sub, Sub::Tests);
assert!(parse(&s(&[])).is_err());
assert!(parse(&s(&["usb"])).unwrap_err().contains("airgap"));
assert!(parse(&s(&["airgap", "--bin-dir"]))
.unwrap_err()
.contains("needs a value"));
assert!(parse(&s(&["airgap", "--fast"])).is_err());
}
fn receipt(checks: &[(&str, &str)], result: &str) -> String {
let checks: Vec<serde_json::Value> = checks
.iter()
.enumerate()
.map(|(n, (step, r))| serde_json::json!({"n": n + 1, "step": step, "result": r, "detail": "d"}))
.collect();
serde_json::json!({"schema": "holger.demo-airgap-receipt/v1", "result": result, "checks": checks}).to_string()
}
#[test]
fn only_a_receipt_where_every_check_passed_is_green() {
assert_eq!(
verdict(&receipt(&[("a", "PASS"), ("b", "PASS")], "pass")).unwrap(),
Verdict::Pass { passed: 2 }
);
assert_eq!(
verdict(&receipt(
&[("a", "PASS"), ("Provenance", "NOT YET")],
"incomplete"
))
.unwrap(),
Verdict::Incomplete {
passed: 1,
pending: vec!["Provenance: d".into()]
}
);
assert_eq!(
verdict(&receipt(&[("a", "PASS"), ("pip", "FAIL")], "fail")).unwrap(),
Verdict::Fail {
failed: vec!["pip: d".into()]
}
);
// A receipt that claims pass with a failed check, or with no checks, is not green.
assert!(matches!(
verdict(&receipt(&[("pip", "FAIL")], "pass")).unwrap(),
Verdict::Fail { .. }
));
assert!(matches!(
verdict(&receipt(&[], "pass")).unwrap(),
Verdict::Fail { .. }
));
assert!(verdict("{\"schema\":\"other\"}").is_err());
assert!(verdict("not json").is_err());
}
#[test]
fn the_demo_builds_the_gateway_and_declares_both_demo_tests() {
assert!(DEMO_FEATURES.contains("holger/secure_update"));
let names: Vec<&str> = DEMO_TESTS.iter().map(|t| t.2).collect();
assert_eq!(
names,
[
"intake_serves_pip",
"airgap_export_import",
"pull_through_apk"
]
);
assert!(
DEMO_TESTS.iter().all(|t| t.3),
"the real-pip and real-apk arms are ignored by default; the row includes them"
);
let apk = DEMO_TESTS
.iter()
.find(|t| t.2 == "pull_through_apk")
.unwrap();
assert_eq!(
(apk.0, apk.1),
("holger-server-lib", "jobs,online,supply-chain"),
"the APK pull-through's own features"
);
}
#[test]
fn the_script_builds_the_gateway_features_and_writes_the_receipt() {
let script = std::fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/demo-airgap.sh"),
)
.unwrap();
assert!(
script.contains("--features holger/secure_update"),
"the build line names secure_update"
);
assert!(
!script.contains("--features supply-chain,holger-server-alias"),
"the stale build line is gone"
);
assert!(
script.contains("holger.demo-airgap-receipt/v1") && script.contains("receipt.json")
);
assert!(
script.contains("--network none"),
"the secure side has no network"
);
assert!(
script.contains("EICAR_Test_File"),
"the planted EICAR file is refused by the rule's name"
);
assert!(
script.contains("NOT YET\" \"$1"),
"a pending lane is a NOT YET check, never a pass"
);
}
#[test]
fn the_script_verifies_signed_provenance_on_both_sides_and_refuses_a_tampered_one() {
let script = std::fs::read_to_string(
Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/demo-airgap.sh"),
)
.unwrap();
assert!(
!script.contains("notyet \"G3a\""),
"G3a landed: provenance is a real check now"
);
assert!(
script.contains("sign_slsa"),
"the online side signs SLSA provenance for the wheel"
);
assert!(
script.contains("default: require(keys: [\"demo-release\"], slsa_min: 2)"),
"both sides require it"
);
assert!(
script.contains("provenance:holger-provenance=pass"),
"the slot the step expects"
);
assert!(
script.contains("holger-provenance-3"),
"a tampered attestation is refused on the secure side by its code"
);
}
}