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::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};

use sha2::{Digest, Sha256};

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

const ALLOW_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-r7-{name}-{}-{id}", std::process::id()));
    fs::create_dir_all(&path).expect("create temp dir");
    path
}

fn learn(dir: &Path, baseline: &Path) {
    let output = Command::new(cli())
        .current_dir(dir)
        .args(["learn", "--output"])
        .arg(baseline)
        .args(["--", "/bin/sh", "-c", "true"])
        .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()
}

fn policy_digest(bytes: &[u8]) -> String {
    format!("sha256:{:x}", Sha256::digest(bytes))
}

fn check(
    dir: &Path,
    baseline: &Path,
    policy: Option<&Path>,
    json_output: Option<&Path>,
    markdown_output: Option<&Path>,
    target: &str,
) -> std::process::Output {
    let expected_baseline = baseline_digest(baseline);
    let mut command = Command::new(cli());
    command
        .current_dir(dir)
        .args(["check", "--baseline"])
        .arg(baseline)
        .args(["--expect-baseline-digest", &expected_baseline]);

    if let Some(policy) = policy {
        let bytes = fs::read(policy).expect("read policy for custody digest");
        let digest = policy_digest(&bytes);
        command
            .args(["--policy"])
            .arg(policy)
            .args(["--expect-policy-sha256", &digest]);
    }

    if let Some(path) = json_output {
        command.args(["--json-output"]).arg(path);
    }
    if let Some(path) = markdown_output {
        command.args(["--markdown-output"]).arg(path);
    }

    command
        .args(["--", "/bin/sh", "-c", target])
        .output()
        .expect("run check")
}

#[test]
fn r7_json_output_cannot_replace_custody_verified_baseline() {
    let dir = temp_dir("baseline-direct");
    let baseline = dir.join("baseline.lock.json");
    let marker = dir.join("TARGET_RAN");
    learn(&dir, &baseline);
    let before = fs::read(&baseline).expect("baseline bytes before");
    let target = format!("touch {}", marker.display());

    let output = check(&dir, &baseline, None, Some(&baseline), None, &target);

    assert_eq!(
        output.status.code(),
        Some(2),
        "trusted-input/output collision must be ERROR"
    );
    assert!(
        !marker.exists(),
        "knowable baseline/output collision must fail before target execution"
    );
    assert_eq!(
        fs::read(&baseline).expect("baseline bytes after"),
        before,
        "custody-verified baseline must not be overwritten by verdict output"
    );

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

#[test]
fn r7_markdown_output_symlink_alias_cannot_replace_verified_policy() {
    let dir = temp_dir("policy-symlink");
    let baseline = dir.join("baseline.lock.json");
    let policy = dir.join("policy.json");
    let alias = dir.join("summary.md");
    let marker = dir.join("TARGET_RAN");
    learn(&dir, &baseline);
    fs::write(&policy, ALLOW_POLICY).expect("write policy");
    symlink(&policy, &alias).expect("symlink output to policy");
    let before = fs::read(&policy).expect("policy bytes before");
    let target = format!("touch {}", marker.display());

    let output = check(&dir, &baseline, Some(&policy), None, Some(&alias), &target);

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "symlink alias to policy must be rejected before target execution"
    );
    assert_eq!(
        fs::read(&policy).expect("policy bytes after"),
        before,
        "verified policy must survive symlink-output alias"
    );

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

#[test]
fn r7_json_output_hardlink_alias_cannot_replace_verified_baseline() {
    let dir = temp_dir("baseline-hardlink");
    let baseline = dir.join("baseline.lock.json");
    let alias = dir.join("report.json");
    let marker = dir.join("TARGET_RAN");
    learn(&dir, &baseline);
    fs::hard_link(&baseline, &alias).expect("hardlink output to baseline");
    let before = fs::read(&baseline).expect("baseline bytes before");
    let target = format!("touch {}", marker.display());

    let output = check(&dir, &baseline, None, Some(&alias), None, &target);

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "hardlink alias to baseline must be rejected before target execution"
    );
    assert_eq!(
        fs::read(&baseline).expect("baseline bytes after"),
        before,
        "verified baseline inode must survive hardlink-output alias"
    );

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

#[test]
fn r7_json_and_markdown_outputs_cannot_alias_each_other() {
    let dir = temp_dir("output-output");
    let baseline = dir.join("baseline.lock.json");
    let output_path = dir.join("evidence.out");
    let marker = dir.join("TARGET_RAN");
    learn(&dir, &baseline);
    let target = format!("touch {}", marker.display());

    let output = check(
        &dir,
        &baseline,
        None,
        Some(&output_path),
        Some(&output_path),
        &target,
    );

    assert_eq!(output.status.code(), Some(2));
    assert!(
        !marker.exists(),
        "knowable JSON/Markdown alias must fail before target execution"
    );
    assert!(
        !output_path.exists(),
        "preflight-rejected output alias must not materialize either format"
    );

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

#[test]
fn r7_verdict_output_cannot_overwrite_workload_written_object() {
    let dir = temp_dir("workload-output");
    let baseline = dir.join("baseline.lock.json");
    let policy = dir.join("policy.json");
    let workload = dir.join("workload-owned.bin");
    learn(&dir, &baseline);
    fs::write(&policy, ALLOW_POLICY).expect("write policy");

    let target = format!("printf WORKLOAD-STATE > {}", workload.display());
    let output = check(
        &dir,
        &baseline,
        Some(&policy),
        Some(&workload),
        None,
        &target,
    );

    assert_eq!(
        output.status.code(),
        Some(2),
        "post-observation workload/output collision must fail report materialization"
    );
    assert_eq!(
        fs::read(&workload).expect("read workload-owned bytes"),
        b"WORKLOAD-STATE",
        "ExecSurface must not overwrite workload-owned state with its report"
    );

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

#[test]
fn r7_target_cannot_rebind_output_path_to_verified_baseline_after_preflight() {
    let dir = temp_dir("post-target-rebind");
    let baseline = dir.join("baseline.lock.json");
    let output_path = dir.join("report.json");
    learn(&dir, &baseline);
    let before = fs::read(&baseline).expect("baseline bytes before");

    let target = format!("ln -s {} {}", baseline.display(), output_path.display());
    let output = check(&dir, &baseline, None, Some(&output_path), None, &target);

    assert_eq!(
        output.status.code(),
        Some(2),
        "post-target output identity rebinding must fail before report write"
    );
    assert!(
        output_path.is_symlink(),
        "target should have executed and created the adversarial alias"
    );
    assert_eq!(
        fs::read(&baseline).expect("baseline bytes after"),
        before,
        "post-target symlink rebinding must not redirect report bytes into the baseline"
    );

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

#[test]
fn r7_target_cannot_move_verified_baseline_onto_output_path() {
    let dir = temp_dir("trusted-object-move");
    let baseline = dir.join("baseline.lock.json");
    let output_path = dir.join("report.json");
    learn(&dir, &baseline);
    let before = fs::read(&baseline).expect("baseline bytes before");

    let target = format!("mv {} {}", baseline.display(), output_path.display());
    let output = check(&dir, &baseline, None, Some(&output_path), None, &target);

    assert_eq!(
        output.status.code(),
        Some(2),
        "report output must not overwrite the preflight-verified baseline object after target rename"
    );
    assert!(
        !baseline.exists(),
        "target should have executed and moved the original baseline pathname"
    );
    assert_eq!(
        fs::read(&output_path).expect("moved baseline object"),
        before,
        "the original verified baseline object must remain intact at its new pathname"
    );

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