use crate::commands::pm_guard_secret_read::evaluate_secret_file_read;
fn verdict(command: &str) -> Option<String> {
let input = serde_json::json!({ "command": command });
evaluate_secret_file_read("Bash", Some(&input))
}
fn assert_denies(marker: &str, commands: &[&str]) {
let leaked: Vec<&str> = commands
.iter()
.copied()
.filter(|c| !verdict(c).is_some_and(|r| r.contains(marker)))
.collect();
assert!(leaked.is_empty(), "must deny under {marker}: {leaked:#?}");
}
fn assert_allows(commands: &[&str]) {
let denied: Vec<(&str, String)> = commands
.iter()
.copied()
.filter_map(|c| verdict(c).map(|r| (c, r)))
.collect();
assert!(denied.is_empty(), "must allow: {denied:#?}");
}
#[test]
fn refuses_a_process_dump_inside_a_command_substitution() {
assert_denies(
"#8756",
&[
"echo \"$(pm2 jlist)\"",
"echo $(pm2 jlist)",
"echo `pm2 jlist`",
"echo \"`pm2 describe 0`\"",
"x=$(launchctl print gui/501/x); echo \"$x\"",
"echo \"$(echo $(pm2 env 0))\"",
"echo \"$(launchctl print gui/501/com.example.daemon)\"",
"echo \"${X:-$(pm2 jlist)}\"",
"cat <<< \"$(pm2 jlist)\"",
],
);
}
#[test]
fn refuses_a_dump_through_a_wrapper_or_process_substitution() {
assert_denies(
"#8756",
&[
"bash -lc 'echo \"$(pm2 jlist)\"'",
"sh -c \"echo \\$(pm2 prettylist)\"",
"diff <(pm2 jlist) saved.json",
"cat <<EOF\n$(pm2 jlist)\nEOF",
"ssh host <<'EOF'\npm2 jlist\nEOF",
],
);
}
#[test]
fn refuses_a_dump_run_through_eval() {
assert_denies(
"#8756",
&[
"eval \"pm2 jlist\"",
"eval pm2 jlist",
"eval 'pm2 prettylist'",
"eval -- \"pm2 env 0\"",
"eval \"launchctl print gui/501/x\"",
"command eval \"pm2 describe 0\"",
"echo \"$(eval \"pm2 jlist\")\"",
"x=$(eval 'launchctl print gui/501/x'); echo \"$x\"",
"eval \"sh -c 'pm2 jlist'\"",
],
);
assert_denies("#7648", &["eval \"docker exec web env\""]);
}
#[test]
fn allows_eval_of_a_routine_command() {
assert_allows(&[
"eval \"$(ssh-agent -s)\"",
"eval \"$(direnv export bash)\"",
"eval \"$(tm completions zsh)\"",
"eval \"pm2 ls\"",
]);
}
#[test]
fn refuses_a_dump_through_every_dash_c_spelling() {
assert_denies(
"#8756",
&[
"bash -c \"pm2 jlist\"",
"sh -c 'pm2 jlist'",
"zsh -c \"launchctl print gui/501/x\"",
"bash -lc \"pm2 env 0\"",
"bash -ce \"pm2 jlist\"",
"bash -c -e 'pm2 jlist'",
"sh -c -- 'pm2 jlist'",
"bash -cl 'launchctl print gui/501/x'",
"bash -o pipefail -c 'pm2 prettylist'",
],
);
assert_allows(&["bash -ce \"pm2 ls\"", "bash script.sh -c 'pm2 jlist'"]);
}
#[test]
fn refuses_a_dump_inside_a_subshell_group() {
assert_denies(
"#8756",
&[
"(pm2 jlist)",
"echo \"$( (pm2 jlist) )\"",
"(cd /tmp && pm2 env 0)",
],
);
assert_denies("#7648", &["(docker exec web env)"]);
assert_allows(&["(cd /tmp && pm2 ls)", "echo \"(pm2 jlist)\""]);
}
#[test]
fn refuses_a_pod_env_dump_inside_a_command_substitution() {
assert_denies(
"#7648",
&[
"echo \"$(docker exec web env)\"",
"echo `kubectl exec web -- env`",
],
);
}
#[test]
fn refuses_a_secret_read_inside_a_command_substitution() {
assert_denies(
"refused",
&[
"echo \"$(cat .env)\"",
"echo \"$(echo $(sed -n 1p terraform.tfvars))\"",
"bash -lc 'echo \"$(cat .env)\"'",
"echo \"$(gcloud auth print-access-token)\"",
],
);
}
#[test]
fn refuses_an_unclosed_substitution_that_names_a_dump() {
assert_denies(
"#8756",
&[
"echo \"$(pm2 jlist\"",
"echo $(pm2 jlist",
"echo `pm2 jlist",
],
);
assert_allows(&["echo $(date"]);
}
#[test]
fn bounds_nesting_and_still_refuses_a_dump() {
let nest = |inner: &str| {
let depth = 40;
format!("{}{inner}{}", "echo $(".repeat(depth), ")".repeat(depth))
};
assert_denies("#8756", &[&nest("pm2 jlist")]);
assert_allows(&[&nest("date")]);
}
#[test]
fn allows_routine_commands_that_carry_a_substitution() {
assert_allows(&[
"git commit -m \"refuse pm2 jlist and launchctl print\"",
"echo \"$(date)\"",
"echo \"$(git rev-parse HEAD)\"",
"gh pr comment 1 --body \"$(cat notes.md)\"",
"echo '$(pm2 jlist)'",
"echo \"$(pm2 pid api)\"",
r#"git commit -m "refuse \$(pm2 jlist) and \`pm2 env 0\`""#,
r"echo \$(pm2 jlist)",
"gh pr create --title t --body \"$(cat <<'EOF'\nRefuse pm2 jlist and launchctl print \
inside a substitution.\nEOF\n)\"",
]);
}
#[test]
fn a_heredoc_body_runs_only_when_its_delimiter_is_unquoted() {
assert_allows(&[
"git commit -F - <<'EOF'\nfix: refuse `$(pm2 jlist)` in a substitution\nEOF",
"git commit -F - <<'EOF'\nfix: refuse pm2 jlist and launchctl print\nEOF",
"git commit -F - <<EOF\nfix: guard the $(git rev-parse --short HEAD) dump rules\nEOF",
"git commit -F - <<EOF\nfix: refuse \\$(pm2 jlist)\nEOF",
]);
assert_denies("#8756", &["git commit -F - <<EOF\nfix: $(pm2 jlist)\nEOF"]);
}
#[test]
fn keeps_the_credential_capture_forms_allowed() {
assert_allows(&[
"TOKEN=$(gcloud auth print-access-token)",
"curl -sS -H \"Authorization: Bearer $(gcloud auth print-access-token)\" \
https://example.test/v1",
]);
}