use crate::config::{ArgPolicy, DenyFlags, EnvPolicy};
use crate::error::ExecError;
use std::collections::HashMap;
pub fn eval_args(args: &[String], policy: &ArgPolicy, deny: &DenyFlags) -> Result<(), ExecError> {
if args.is_empty() {
return Ok(());
}
for a in args {
for f in &deny.0 {
if a == f || a.starts_with(f) {
return Err(ExecError::ArgPolicyDenied {
arg: a.clone(),
reason: format!("matches deny_flags entry {f:?}"),
});
}
}
let ok = match policy {
ArgPolicy::Any => true,
ArgPolicy::Exact { values } => values.iter().any(|v| v == a),
ArgPolicy::Prefix { values } => values.iter().any(|v| a.starts_with(v)),
};
if !ok {
return Err(ExecError::ArgPolicyDenied {
arg: a.clone(),
reason: "not permitted by allow policy".into(),
});
}
}
Ok(())
}
pub fn build_env(
policy: &EnvPolicy,
deny_env: &[String],
host: &HashMap<String, String>,
) -> HashMap<String, String> {
let mut out = HashMap::new();
for k in &policy.allow {
if let Some(v) = host.get(k) {
out.insert(k.clone(), v.clone());
}
}
for (k, v) in &policy.set {
out.insert(k.clone(), v.clone());
}
out.retain(|k, _| !deny_env.iter().any(|pat| glob_matches(pat, k)));
out
}
fn glob_matches(pat: &str, name: &str) -> bool {
glob::Pattern::new(pat)
.map(|p| p.matches(name))
.unwrap_or(false)
}
pub fn is_known_shell(exe: &str) -> bool {
let base = std::path::Path::new(exe)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or(exe)
.to_ascii_lowercase();
const SHELLS: &[&str] = &[
"sh",
"bash",
"dash",
"zsh",
"ksh",
"fish",
"csh",
"tcsh",
"cmd.exe",
"powershell.exe",
"pwsh",
];
SHELLS
.iter()
.any(|s| base == *s || base.starts_with(&format!("{s}.")))
}
#[cfg(test)]
mod arg_tests {
use super::*;
use crate::config::{ArgPolicy, DenyFlags};
#[test]
fn any_accepts_all() {
assert!(
eval_args(
&["log", "--oneline"].map(String::from),
&ArgPolicy::Any,
&DenyFlags::default()
)
.is_ok()
);
}
#[test]
fn exact_rejects_unknown_element() {
let p = ArgPolicy::Exact {
values: ["check"].map(String::from).to_vec(),
};
assert!(eval_args(&["check"].map(String::from), &p, &DenyFlags::default()).is_ok());
assert!(
eval_args(
&["check", "--release"].map(String::from),
&p,
&DenyFlags::default()
)
.is_err()
);
}
#[test]
fn prefix_matches_per_element() {
let p = ArgPolicy::Prefix {
values: ["log", "diff", "--"].map(String::from).to_vec(),
};
assert!(
eval_args(
&["log", "--oneline"].map(String::from),
&p,
&DenyFlags::default()
)
.is_ok()
);
assert!(eval_args(&["push"].map(String::from), &p, &DenyFlags::default()).is_err());
}
#[test]
fn deny_flags_applied_first() {
let p = ArgPolicy::Any;
let d = DenyFlags(["--upload-pack"].map(String::from).to_vec());
assert!(eval_args(&["--upload-pack=x"].map(String::from), &p, &d).is_err());
}
#[test]
fn empty_args_always_allowed() {
let p = ArgPolicy::Exact { values: vec![] };
assert!(eval_args(&[], &p, &DenyFlags::default()).is_ok());
}
}
#[cfg(test)]
mod env_shell_tests {
use super::*;
use crate::config::EnvPolicy;
use std::collections::HashMap;
fn env(allow: &[&str], set: &[(&str, &str)]) -> EnvPolicy {
EnvPolicy {
allow: allow.iter().map(|s| s.to_string()).collect(),
set: set
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
}
}
#[test]
fn deny_env_globs_stripped_last() {
let mut host = HashMap::new();
host.insert("POLICY_TEST_GITHUB_TOKEN".to_string(), "leak".into());
host.insert("POLICY_TEST_HOME".to_string(), "/h".into());
let p = env(
&["POLICY_TEST_HOME", "POLICY_TEST_GITHUB_TOKEN"], &[("CI", "1"), ("POLICY_TEST_AWS_TOKEN", "secret")], );
let out = build_env(&p, &["*_TOKEN".to_string()], &host);
assert_eq!(out.get("CI"), Some(&"1".to_string()));
assert_eq!(out.get("POLICY_TEST_HOME"), Some(&"/h".to_string()));
assert!(
!out.contains_key("POLICY_TEST_GITHUB_TOKEN"),
"deny_env must strip allowed *_TOKEN"
);
assert!(
!out.contains_key("POLICY_TEST_AWS_TOKEN"),
"deny_env must strip explicitly-set *_TOKEN"
);
}
#[test]
fn known_shell_rejected_unless_allowed() {
assert!(is_known_shell("/bin/sh"));
assert!(is_known_shell("/usr/bin/bash"));
assert!(is_known_shell("cmd.exe"));
assert!(is_known_shell("/usr/bin/pwsh.exe")); assert!(!is_known_shell("/usr/bin/git"));
}
}