execsurface 1.0.0

Runtime execution-surface drift detection for Linux CI and developer workflows
#![cfg(all(target_os = "linux", target_arch = "x86_64"))]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

static NEXT_ID: AtomicU64 = AtomicU64::new(1);

const TRUSTED_POLICY: &str =
    "{\n  \"schema_version\": 2,\n  \"default_action\": \"review\",\n  \"rules\": []\n}\n";
const TRUSTED_POLICY_SHA256: &str =
    "sha256:36a0ca08546dbfef6466f66a8636890b50f90d0d7fc4ee6c4e64aa1efea4c80d";
const SUBSTITUTE_POLICY: &str =
    "{\n  \"schema_version\": 2,\n  \"default_action\": \"allow\",\n  \"rules\": []\n}\n";

fn cli() -> &'static str {
    env!("CARGO_BIN_EXE_execsurface")
}

fn temp_dir(name: &str) -> PathBuf {
    let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
    let path =
        std::env::temp_dir().join(format!("execsurface-r4-{name}-{}-{id}", std::process::id()));
    fs::create_dir_all(&path).expect("create r4 temp dir");
    path
}

fn learn(dir: &Path, baseline: &Path, label: &str, command: &[&str]) {
    let mut cmd = Command::new(cli());
    cmd.current_dir(dir)
        .args(["learn", "--output"])
        .arg(baseline)
        .args(["--label", label, "--"]);
    cmd.args(command);
    let output = cmd.output().expect("learn baseline");
    assert!(
        output.status.success(),
        "learn failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
}

fn baseline_digest(path: &Path) -> String {
    let value: serde_json::Value =
        serde_json::from_slice(&fs::read(path).expect("read baseline")).expect("baseline json");
    value["baseline_digest"]
        .as_str()
        .expect("baseline digest")
        .to_owned()
}

#[test]
fn r4_self_consistent_baseline_substitution_is_rejected_before_target() {
    let dir = temp_dir("baseline-substitution");
    let trusted = dir.join("trusted.lock.json");
    let substitute = dir.join("substitute.lock.json");
    let marker = dir.join("TARGET_RAN");

    learn(&dir, &trusted, "trusted", &["/bin/true"]);
    learn(&dir, &substitute, "substitute", &["/bin/true"]);
    let expected = baseline_digest(&trusted);
    assert_ne!(
        expected,
        baseline_digest(&substitute),
        "fixture requires two self-consistent but differently identified baselines"
    );

    let target = format!("touch {}", marker.display());
    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&substitute)
        .args([
            "--expect-baseline-digest",
            &expected,
            "--",
            "/bin/sh",
            "-c",
            &target,
        ])
        .output()
        .expect("check substituted baseline");

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "baseline custody mismatch must be rejected before target execution"
    );
    assert!(
        String::from_utf8_lossy(&output.stderr).contains("baseline custody mismatch"),
        "failure must identify the custody invariant rather than pass for an unrelated parse error: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let _ = fs::remove_dir_all(dir);
}

#[test]
fn r4_parseable_policy_substitution_is_rejected_before_target() {
    let dir = temp_dir("policy-substitution");
    let baseline = dir.join("baseline.lock.json");
    let policy = dir.join("execsurface-policy.json");
    let marker = dir.join("TARGET_RAN");

    learn(
        &dir,
        &baseline,
        "policy-custody",
        &["/bin/sh", "-c", "true"],
    );
    fs::write(&policy, SUBSTITUTE_POLICY).expect("write substitute policy");

    let target = format!("touch {}", marker.display());
    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&baseline)
        .args(["--policy"])
        .arg(&policy)
        .args([
            "--expect-policy-sha256",
            TRUSTED_POLICY_SHA256,
            "--",
            "/bin/sh",
            "-c",
            &target,
        ])
        .output()
        .expect("check substituted policy");

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "policy custody mismatch must be rejected before target execution"
    );
    assert!(
        String::from_utf8_lossy(&output.stderr).contains("policy custody mismatch"),
        "failure must identify policy custody rather than an unrelated error: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let _ = fs::remove_dir_all(dir);
}

#[test]
fn r4_malformed_expected_digest_fails_closed_before_target() {
    let dir = temp_dir("malformed-pin");
    let baseline = dir.join("baseline.lock.json");
    let marker = dir.join("TARGET_RAN");

    learn(&dir, &baseline, "malformed-pin", &["/bin/true"]);

    let target = format!("touch {}", marker.display());
    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&baseline)
        .args([
            "--expect-baseline-digest",
            "sha256:not-a-real-digest",
            "--",
            "/bin/sh",
            "-c",
            &target,
        ])
        .output()
        .expect("check malformed pin");

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "malformed pin must fail before target execution"
    );
    assert!(
        String::from_utf8_lossy(&output.stderr).contains("invalid expected baseline digest"),
        "malformed pin must fail for the frozen R4 reason: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let _ = fs::remove_dir_all(dir);
}

#[test]
fn r4_correct_baseline_and_policy_pins_preserve_pass() {
    let dir = temp_dir("correct-pins");
    let baseline = dir.join("baseline.lock.json");
    let policy = dir.join("execsurface-policy.json");

    learn(&dir, &baseline, "correct-pins", &["/bin/true"]);
    fs::write(&policy, TRUSTED_POLICY).expect("write trusted policy");
    let expected_baseline = baseline_digest(&baseline);

    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&baseline)
        .args(["--policy"])
        .arg(&policy)
        .args([
            "--expect-baseline-digest",
            &expected_baseline,
            "--expect-policy-sha256",
            TRUSTED_POLICY_SHA256,
            "--",
            "/bin/true",
        ])
        .output()
        .expect("check correct pins");

    assert!(
        output.status.success(),
        "correct external custody pins must preserve ordinary PASS: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let _ = fs::remove_dir_all(dir);
}

#[test]
fn r4_legacy_unpinned_check_remains_compatible_without_new_custody_claim() {
    let dir = temp_dir("legacy-unpinned");
    let baseline = dir.join("baseline.lock.json");

    learn(&dir, &baseline, "legacy-unpinned", &["/bin/true"]);

    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&baseline)
        .args(["--", "/bin/true"])
        .output()
        .expect("legacy unpinned check");

    assert!(
        output.status.success(),
        "R4 must not silently break the existing unpinned pre-v1 CLI path"
    );

    let _ = fs::remove_dir_all(dir);
}

#[test]
fn r4_duplicate_baseline_trust_pin_is_rejected_before_target() {
    let dir = temp_dir("duplicate-baseline-pin");
    let baseline = dir.join("baseline.lock.json");
    let marker = dir.join("TARGET_RAN");

    learn(
        &dir,
        &baseline,
        "duplicate-baseline-pin",
        &["/bin/sh", "-c", "true"],
    );
    let expected = baseline_digest(&baseline);
    let wrong = format!("sha256:{}", "0".repeat(64));
    let target = format!("touch {}", marker.display());

    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&baseline)
        .args(["--expect-baseline-digest", &wrong])
        .args(["--expect-baseline-digest", &expected])
        .args(["--", "/bin/sh", "-c", &target])
        .output()
        .expect("check duplicate baseline trust pin");

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "ambiguous duplicate baseline trust assertions must fail before target execution"
    );
    assert!(
        String::from_utf8_lossy(&output.stderr).contains("duplicate --expect-baseline-digest"),
        "duplicate trust assertion must be rejected explicitly: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let _ = fs::remove_dir_all(dir);
}

#[test]
fn r4_duplicate_policy_trust_pin_is_rejected_before_target() {
    let dir = temp_dir("duplicate-policy-pin");
    let baseline = dir.join("baseline.lock.json");
    let policy = dir.join("execsurface-policy.json");
    let marker = dir.join("TARGET_RAN");

    learn(
        &dir,
        &baseline,
        "duplicate-policy-pin",
        &["/bin/sh", "-c", "true"],
    );
    fs::write(&policy, TRUSTED_POLICY).expect("write trusted policy");
    let expected_baseline = baseline_digest(&baseline);
    let wrong = format!("sha256:{}", "0".repeat(64));
    let target = format!("touch {}", marker.display());

    let output = Command::new(cli())
        .current_dir(&dir)
        .args(["check", "--baseline"])
        .arg(&baseline)
        .args(["--policy"])
        .arg(&policy)
        .args(["--expect-baseline-digest", &expected_baseline])
        .args(["--expect-policy-sha256", &wrong])
        .args(["--expect-policy-sha256", TRUSTED_POLICY_SHA256])
        .args(["--", "/bin/sh", "-c", &target])
        .output()
        .expect("check duplicate policy trust pin");

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "ambiguous duplicate policy trust assertions must fail before target execution"
    );
    assert!(
        String::from_utf8_lossy(&output.stderr).contains("duplicate --expect-policy-sha256"),
        "duplicate trust assertion must be rejected explicitly: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let _ = fs::remove_dir_all(dir);
}