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::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};

use execsurface_policy::{Verdict, VerdictReport};

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

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

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

fn learn(dir: &std::path::Path, baseline: &std::path::Path, command: &[&str]) {
    let mut cmd = Command::new(cli());
    cmd.current_dir(dir)
        .args(["learn", "--output"])
        .arg(baseline)
        .arg("--");
    cmd.args(command);
    let status = cmd.stdout(Stdio::null()).status().expect("learn");
    assert!(status.success());
}

fn check(
    dir: &std::path::Path,
    baseline: &std::path::Path,
    policy: Option<&std::path::Path>,
    command: &[&str],
) -> std::process::Output {
    let mut cmd = Command::new(cli());
    cmd.current_dir(dir)
        .args(["check", "--json", "--baseline"])
        .arg(baseline);
    if let Some(policy) = policy {
        cmd.arg("--policy").arg(policy);
    }
    cmd.arg("--").args(command);
    cmd.output().expect("check")
}

#[test]
fn no_drift_is_pass_with_exit_zero() {
    let dir = temp_dir("pass");
    let baseline = dir.join("lock.json");
    learn(&dir, &baseline, &["/bin/true"]);

    let output = check(&dir, &baseline, None, &["/bin/true"]);
    assert_eq!(output.status.code(), Some(0));
    let report: VerdictReport = serde_json::from_slice(&output.stdout).expect("verdict json");
    assert_eq!(report.verdict, Verdict::Pass);
    assert!(report.findings.is_empty());

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

#[test]
fn unmatched_drift_is_review_with_stable_exit_code() {
    let dir = temp_dir("review");
    let baseline = dir.join("lock.json");
    learn(&dir, &baseline, &["/bin/sh", "-c", "true"]);

    let output = check(
        &dir,
        &baseline,
        None,
        &["/bin/sh", "-c", "true; /bin/echo x >/dev/null"],
    );
    assert_eq!(output.status.code(), Some(10));
    let report: VerdictReport = serde_json::from_slice(&output.stdout).expect("verdict json");
    assert_eq!(report.verdict, Verdict::Review);
    assert!(!report.findings.is_empty());

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

#[test]
fn explicit_policy_can_block_added_exec() {
    let dir = temp_dir("block");
    let baseline = dir.join("lock.json");
    let policy = dir.join("policy.json");
    learn(&dir, &baseline, &["/bin/sh", "-c", "true"]);
    fs::write(
        &policy,
        br#"{
  "schema_version": 1,
  "default_action": "review",
  "rules": [
    {
      "id": "block-added-exec",
      "action": "block",
      "match": {
        "change": "added",
        "effect": "process_exec"
      }
    }
  ]
}"#,
    )
    .unwrap();

    let output = check(
        &dir,
        &baseline,
        Some(&policy),
        &["/bin/sh", "-c", "true; /bin/echo x >/dev/null"],
    );
    assert_eq!(output.status.code(), Some(20));
    let report: VerdictReport = serde_json::from_slice(&output.stdout).expect("verdict json");
    assert_eq!(report.verdict, Verdict::Block);
    assert!(report.findings.iter().any(|finding| finding
        .matched_rules
        .iter()
        .any(|id| id == "block-added-exec")));

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

#[test]
fn block_default_cannot_turn_no_drift_into_block() {
    let dir = temp_dir("no-drift-block-default");
    let baseline = dir.join("lock.json");
    let policy = dir.join("policy.json");
    learn(&dir, &baseline, &["/bin/true"]);
    fs::write(
        &policy,
        br#"{
  "schema_version": 1,
  "default_action": "block",
  "rules": []
}"#,
    )
    .unwrap();

    let output = check(&dir, &baseline, Some(&policy), &["/bin/true"]);
    assert_eq!(output.status.code(), Some(0));
    let report: VerdictReport = serde_json::from_slice(&output.stdout).expect("verdict json");
    assert_eq!(report.verdict, Verdict::Pass);

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

#[test]
fn invalid_policy_is_error_exit_two() {
    let dir = temp_dir("invalid");
    let baseline = dir.join("lock.json");
    let policy = dir.join("policy.json");
    learn(&dir, &baseline, &["/bin/true"]);
    fs::write(
        &policy,
        br#"{
  "schema_version": 1,
  "default_action": "review",
  "rules": [
    {"id":"dup","action":"allow","match":{"effect":"process_exec"}},
    {"id":"dup","action":"block","match":{"effect":"process_exec"}}
  ]
}"#,
    )
    .unwrap();

    let output = check(&dir, &baseline, Some(&policy), &["/bin/true"]);
    assert_eq!(output.status.code(), Some(2));
    assert!(String::from_utf8_lossy(&output.stderr).contains("duplicate policy rule id"));

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