camel_component_exec/
policy.rs1use crate::config::{ArgPolicy, DenyFlags, EnvPolicy};
4use crate::error::ExecError;
5use std::collections::HashMap;
6
7pub fn eval_args(args: &[String], policy: &ArgPolicy, deny: &DenyFlags) -> Result<(), ExecError> {
10 if args.is_empty() {
11 return Ok(());
12 }
13 for a in args {
14 for f in &deny.0 {
15 if a == f || a.starts_with(f) {
16 return Err(ExecError::ArgPolicyDenied {
17 arg: a.clone(),
18 reason: format!("matches deny_flags entry {f:?}"),
19 });
20 }
21 }
22 let ok = match policy {
23 ArgPolicy::Any => true,
24 ArgPolicy::Exact { values } => values.iter().any(|v| v == a),
25 ArgPolicy::Prefix { values } => values.iter().any(|v| a.starts_with(v)),
26 };
27 if !ok {
28 return Err(ExecError::ArgPolicyDenied {
29 arg: a.clone(),
30 reason: "not permitted by allow policy".into(),
31 });
32 }
33 }
34 Ok(())
35}
36
37pub fn build_env(
40 policy: &EnvPolicy,
41 deny_env: &[String],
42 host: &HashMap<String, String>,
43) -> HashMap<String, String> {
44 let mut out = HashMap::new();
45 for k in &policy.allow {
46 if let Some(v) = host.get(k) {
47 out.insert(k.clone(), v.clone());
48 }
49 }
50 for (k, v) in &policy.set {
51 out.insert(k.clone(), v.clone());
52 }
53 out.retain(|k, _| !deny_env.iter().any(|pat| glob_matches(pat, k)));
55 out
56}
57
58fn glob_matches(pat: &str, name: &str) -> bool {
61 glob::Pattern::new(pat)
62 .map(|p| p.matches(name))
63 .unwrap_or(false)
64}
65
66pub fn is_known_shell(exe: &str) -> bool {
68 let base = std::path::Path::new(exe)
69 .file_name()
70 .and_then(|s| s.to_str())
71 .unwrap_or(exe)
72 .to_ascii_lowercase();
73 const SHELLS: &[&str] = &[
74 "sh",
75 "bash",
76 "dash",
77 "zsh",
78 "ksh",
79 "fish",
80 "csh",
81 "tcsh",
82 "cmd.exe",
83 "powershell.exe",
84 "pwsh",
85 ];
86 SHELLS
87 .iter()
88 .any(|s| base == *s || base.starts_with(&format!("{s}.")))
89}
90
91#[cfg(test)]
92mod arg_tests {
93 use super::*;
94 use crate::config::{ArgPolicy, DenyFlags};
95
96 #[test]
97 fn any_accepts_all() {
98 assert!(
99 eval_args(
100 &["log", "--oneline"].map(String::from),
101 &ArgPolicy::Any,
102 &DenyFlags::default()
103 )
104 .is_ok()
105 );
106 }
107 #[test]
108 fn exact_rejects_unknown_element() {
109 let p = ArgPolicy::Exact {
110 values: ["check"].map(String::from).to_vec(),
111 };
112 assert!(eval_args(&["check"].map(String::from), &p, &DenyFlags::default()).is_ok());
113 assert!(
114 eval_args(
115 &["check", "--release"].map(String::from),
116 &p,
117 &DenyFlags::default()
118 )
119 .is_err()
120 );
121 }
122 #[test]
123 fn prefix_matches_per_element() {
124 let p = ArgPolicy::Prefix {
125 values: ["log", "diff", "--"].map(String::from).to_vec(),
126 };
127 assert!(
128 eval_args(
129 &["log", "--oneline"].map(String::from),
130 &p,
131 &DenyFlags::default()
132 )
133 .is_ok()
134 );
135 assert!(eval_args(&["push"].map(String::from), &p, &DenyFlags::default()).is_err());
136 }
137 #[test]
138 fn deny_flags_applied_first() {
139 let p = ArgPolicy::Any;
140 let d = DenyFlags(["--upload-pack"].map(String::from).to_vec());
141 assert!(eval_args(&["--upload-pack=x"].map(String::from), &p, &d).is_err());
142 }
143 #[test]
144 fn empty_args_always_allowed() {
145 let p = ArgPolicy::Exact { values: vec![] };
146 assert!(eval_args(&[], &p, &DenyFlags::default()).is_ok());
147 }
148}
149
150#[cfg(test)]
151mod env_shell_tests {
152 use super::*;
153 use crate::config::EnvPolicy;
154 use std::collections::HashMap;
155
156 fn env(allow: &[&str], set: &[(&str, &str)]) -> EnvPolicy {
157 EnvPolicy {
158 allow: allow.iter().map(|s| s.to_string()).collect(),
159 set: set
160 .iter()
161 .map(|(k, v)| (k.to_string(), v.to_string()))
162 .collect(),
163 }
164 }
165
166 #[test]
167 fn deny_env_globs_stripped_last() {
168 let mut host = HashMap::new();
172 host.insert("POLICY_TEST_GITHUB_TOKEN".to_string(), "leak".into());
173 host.insert("POLICY_TEST_HOME".to_string(), "/h".into());
174 let p = env(
175 &["POLICY_TEST_HOME", "POLICY_TEST_GITHUB_TOKEN"], &[("CI", "1"), ("POLICY_TEST_AWS_TOKEN", "secret")], );
178 let out = build_env(&p, &["*_TOKEN".to_string()], &host);
179 assert_eq!(out.get("CI"), Some(&"1".to_string()));
180 assert_eq!(out.get("POLICY_TEST_HOME"), Some(&"/h".to_string()));
181 assert!(
182 !out.contains_key("POLICY_TEST_GITHUB_TOKEN"),
183 "deny_env must strip allowed *_TOKEN"
184 );
185 assert!(
186 !out.contains_key("POLICY_TEST_AWS_TOKEN"),
187 "deny_env must strip explicitly-set *_TOKEN"
188 );
189 }
190
191 #[test]
192 fn known_shell_rejected_unless_allowed() {
193 assert!(is_known_shell("/bin/sh"));
194 assert!(is_known_shell("/usr/bin/bash"));
195 assert!(is_known_shell("cmd.exe"));
196 assert!(is_known_shell("/usr/bin/pwsh.exe")); assert!(!is_known_shell("/usr/bin/git"));
198 }
199}