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

nordisk/holger

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