use super::*;
fn scope(cmd: &str) -> Option<String> {
classify_bash(cmd).map(|s| s.key())
}
#[test]
fn heredoc_read_script_over_ssh_is_grantable() {
let cmd = "ssh -o BatchMode=yes karajan@people.example.edu 'sh -s' <<'REMOTE'\ncd /home/web/app || exit 1\necho --TAIL--\ntail -n 120 storage/logs/app.log\nREMOTE\n";
assert_eq!(
scope(cmd).as_deref(),
Some("ssh:karajan@people.example.edu:ro")
);
}
#[test]
fn every_read_shape_to_one_host_shares_one_key() {
let want = Some("ssh:karajan@people.example.edu:ro");
for cmd in [
"ssh -o BatchMode=yes karajan@people.example.edu 'cd /home/web/app && echo --TAIL--; tail -n 120 log'",
"ssh karajan@people.example.edu \"cd /home/web/app && ls -la\"",
"ssh -p 2222 karajan@people.example.edu tail -n 50 /var/log/syslog",
"ssh karajan@people.example.edu sh -s <<'REMOTE'\nps aux | grep php\nREMOTE\n",
] {
assert_eq!(scope(cmd).as_deref(), want, "cmd: {cmd}");
}
}
#[test]
fn quoted_operators_are_not_shell_operators() {
let cmd = "ssh karajan@people.example.edu \"awk '/2026-09-21/ && /PayslipSend/ {n++} END{print n}' storage/logs/app.log\"";
assert_eq!(
scope(cmd).as_deref(),
Some("ssh:karajan@people.example.edu:ro")
);
}
#[test]
fn writes_are_never_grantable() {
for cmd in [
"ssh karajan@people.example.edu 'cat >/tmp/status.php'",
"ssh karajan@people.example.edu 'php artisan tinker --execute=\"...\"'",
"ssh karajan@people.example.edu 'rm -rf /tmp/x'",
"ssh karajan@people.example.edu sudo systemctl restart php-fpm",
"ssh karajan@people.example.edu \"awk '{print > \\\"/tmp/out\\\"}' f\"",
"tail -n 5 log > /tmp/copy",
] {
assert_eq!(scope(cmd), None, "must still ask: {cmd}");
}
}
#[test]
fn substitution_refuses_the_whole_command() {
for cmd in [
"ssh karajan@people.example.edu \"tail -n 5 $(cat /tmp/which)\"",
"ssh karajan@people.example.edu \"cat `cat /tmp/which`\"",
"cat /tmp/$(whoami)",
] {
assert_eq!(scope(cmd), None, "must still ask: {cmd}");
}
}
#[test]
fn a_grant_covers_exactly_one_destination() {
assert_eq!(
scope("ssh a@one.example 'ls' && ssh b@two.example 'ls'"),
None,
"two hosts in one command must not ride either host's grant"
);
assert_eq!(
scope("ssh a@one.example 'ssh b@two.example ls'"),
None,
"a second hop is a destination the human never named"
);
}
#[test]
fn unquoted_heredoc_tag_with_expansion_is_refused() {
let expanded =
"ssh karajan@people.example.edu sh -s <<REMOTE\ntail -n 5 $(whoami).log\nREMOTE\n";
assert_eq!(scope(expanded), None);
let literal = "ssh karajan@people.example.edu sh -s <<REMOTE\ntail -n 5 app.log\nREMOTE\n";
assert_eq!(
scope(literal).as_deref(),
Some("ssh:karajan@people.example.edu:ro")
);
}
#[test]
fn local_reads_share_one_local_key() {
for cmd in [
"ls -la src/",
"cat Cargo.toml",
"git log --oneline -5",
"cd /tmp && ls",
] {
assert_eq!(scope(cmd).as_deref(), Some("local:ro"), "cmd: {cmd}");
}
}
#[test]
fn local_preamble_keeps_the_remote_key() {
assert_eq!(
scope("echo checking; ssh karajan@people.example.edu 'ls -la'").as_deref(),
Some("ssh:karajan@people.example.edu:ro")
);
}
#[test]
fn a_bare_login_is_not_a_read() {
assert_eq!(scope("ssh karajan@people.example.edu"), None);
}
#[test]
fn unterminated_heredoc_is_refused() {
assert_eq!(scope("ssh h 'sh -s' <<'EOF'\nls\n"), None);
}
#[test]
fn host_case_is_normalised() {
assert_eq!(
scope("ssh Karajan@People.Example.EDU ls").as_deref(),
Some("ssh:karajan@people.example.edu:ro")
);
}
#[test]
fn quoted_metachars_stay_in_the_read_lane() {
for cmd in [
r#"gh pr view 1719 --json mergeable | jq -r '"m=\(.mergeable)"'"#,
r#"gh pr view 1719 --json state | jq -r '.state + "!"'"#,
r#"git log --oneline -5 | awk '{print $1}'"#,
r#"grep -rn 'fn foo(' src/"#,
] {
assert_eq!(
scope(cmd).as_deref(),
Some("local:ro"),
"should read: {cmd}"
);
}
}
#[test]
fn unquoted_metachars_still_refused() {
for cmd in ["cat a > b", "ls $(whoami)", r#"echo "$(rm -rf /)""#, "ls &"] {
assert_eq!(scope(cmd), None, "must not be grantable: {cmd}");
}
}
#[test]
fn stream_discard_and_join_redirects_keep_a_read_local() {
for cmd in [
"gh issue list --limit 10 2>/dev/null",
"gh issue view 1878 2>&1 | head -80",
"git log --oneline -5 2>/dev/null",
"ls missing >/dev/null 2>&1",
"cat a 1>/dev/null",
"grep -rn x src 2> /dev/null | head",
"cd /repo && git rev-parse HEAD 2>&1 | head -8",
] {
assert_eq!(scope(cmd).as_deref(), Some("local:ro"), "cmd: {cmd}");
}
}
#[test]
fn every_other_redirect_is_still_refused() {
for cmd in [
"cat a > b",
"cat a >> b",
"ls 2>err.log",
"ls 2>/dev/nullx",
"ls 2>/dev/null/x",
"ls '2'>/dev/null",
"ls >&-",
"ls 2>&1x",
"cat < secret",
"ls &>/dev/null",
"ls 2>'/dev/null'",
] {
assert_eq!(scope(cmd), None, "cmd: {cmd}");
}
}