#![cfg(unix)]
use std::path::{Path, PathBuf};
use std::process::Output;
fn alint_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_alint"))
}
fn run(dir: &Path, args: &[&str]) -> Output {
std::process::Command::new(alint_bin())
.args(args)
.current_dir(dir)
.output()
.expect("spawn alint")
}
fn spawn_rule_fields(kind: &str, sentinel: &Path) -> Vec<String> {
let mut lines = vec![format!("kind: {kind}")];
match kind {
"command" => lines.push("paths: \"**/*\"".to_string()),
"generated_file_fresh" => lines.push("file: out.txt".to_string()),
"command_idempotent" => {}
other => panic!(
"unhandled spawning kind {other:?}; add its minimal fields to spawn_rule_fields \
so the spawn-gate canary matrix covers it"
),
}
lines.push(format!(
"command: [\"sh\", \"-c\", \"touch {}\"]",
sentinel.display()
));
lines.push("level: error".to_string());
lines
}
fn indent(lines: &[String], n: usize) -> String {
let pad = " ".repeat(n);
lines
.iter()
.map(|l| format!("{pad}{l}"))
.collect::<Vec<_>>()
.join("\n")
}
fn assert_refused_no_rce(vector: &str, kind: &str, dir: &Path, sentinel: &Path) {
let out = run(dir, &["check", "."]);
let code = out.status.code();
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(
code,
Some(2),
"[{vector}/{kind}] expected exit 2 (config refused), got {code:?}\nstderr: {stderr}"
);
assert!(
stderr.contains("arbitrary code"),
"[{vector}/{kind}] rejection should explain the RCE risk; stderr: {stderr}"
);
assert!(
stderr.contains(kind),
"[{vector}/{kind}] rejection should name the offending kind; stderr: {stderr}"
);
assert!(
!sentinel.exists(),
"[{vector}/{kind}] RCE: the smuggled `touch` ran — the gate did not block execution"
);
}
fn scaffold() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::Builder::new()
.prefix("alint-spawngate-")
.tempdir()
.expect("tempdir");
std::fs::write(dir.path().join("data.txt"), "hi\n").unwrap();
let sentinel = dir.path().join("SENTINEL");
(dir, sentinel)
}
fn spawning_kinds() -> &'static [&'static str] {
alint_dsl::SPAWNING_RULE_KINDS
}
#[test]
fn positive_control_trusted_top_level_command_executes() {
let (dir, sentinel) = scaffold();
let body = format!(
"version: 1\nrules:\n - id: canary\n{}\n",
indent(&spawn_rule_fields("command", &sentinel), 4)
);
std::fs::write(dir.path().join(".alint.yml"), body).unwrap();
let out = run(dir.path(), &["check", "."]);
assert!(
sentinel.exists(),
"positive control failed: a trusted top-level command rule did not run \
(canary mechanism is broken, making the rejection tests vacuous). \
exit={:?} stderr={}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn spawning_kind_via_extends_rules_is_refused() {
for &kind in spawning_kinds() {
let (dir, sentinel) = scaffold();
let base = format!(
"version: 1\nrules:\n - id: smuggled\n{}\n",
indent(&spawn_rule_fields(kind, &sentinel), 4)
);
std::fs::write(dir.path().join("base.yml"), base).unwrap();
std::fs::write(
dir.path().join(".alint.yml"),
"version: 1\nextends: [./base.yml]\nrules: []\n",
)
.unwrap();
assert_refused_no_rce("extends-rules", kind, dir.path(), &sentinel);
}
}
#[test]
fn spawning_kind_via_extends_template_is_refused() {
for &kind in spawning_kinds() {
let (dir, sentinel) = scaffold();
let base = format!(
"version: 1\ntemplates:\n - id: t\n{}\nrules:\n - id: smuggled\n extends_template: t\n",
indent(&spawn_rule_fields(kind, &sentinel), 4)
);
std::fs::write(dir.path().join("base.yml"), base).unwrap();
std::fs::write(
dir.path().join(".alint.yml"),
"version: 1\nextends: [./base.yml]\nrules: []\n",
)
.unwrap();
assert_refused_no_rce("extends-template", kind, dir.path(), &sentinel);
}
}
#[test]
fn spawning_kind_via_extends_require_block_is_refused() {
for &kind in spawning_kinds() {
let (dir, sentinel) = scaffold();
let fields = indent(&spawn_rule_fields(kind, &sentinel), 8);
let fields = fields.replacen(" kind:", " - kind:", 1);
let base = format!(
"version: 1\nrules:\n - id: outer\n kind: for_each_dir\n select: \"**/\"\n require:\n{fields}\n level: error\n",
);
std::fs::write(dir.path().join("base.yml"), base).unwrap();
std::fs::write(
dir.path().join(".alint.yml"),
"version: 1\nextends: [./base.yml]\nrules: []\n",
)
.unwrap();
assert_refused_no_rce("extends-require", kind, dir.path(), &sentinel);
}
}
#[test]
fn spawning_kind_via_nested_config_is_refused() {
for &kind in spawning_kinds() {
let (dir, sentinel) = scaffold();
std::fs::write(
dir.path().join(".alint.yml"),
"version: 1\nnested_configs: true\nrules: []\n",
)
.unwrap();
let pkg = dir.path().join("packages/foo");
std::fs::create_dir_all(&pkg).unwrap();
let nested = format!(
"version: 1\nrules:\n - id: smuggled\n{}\n",
indent(&spawn_rule_fields(kind, &sentinel), 4)
);
std::fs::write(pkg.join(".alint.yml"), nested).unwrap();
assert_refused_no_rce("nested-config", kind, dir.path(), &sentinel);
}
}