tcrm_task/tasks/
validator.rs

1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::tasks::error::TaskError;
5const MAX_COMMAND_LEN: usize = 4096;
6const MAX_ARG_LEN: usize = 4096;
7const MAX_WORKING_DIR_LEN: usize = 4096;
8const MAX_ENV_KEY_LEN: usize = 1024;
9const MAX_ENV_VALUE_LEN: usize = 4096;
10/// Security validation utilities for task configuration
11pub struct ConfigValidator;
12
13impl ConfigValidator {
14    /// Validates command name.
15    ///
16    /// # Arguments
17    ///
18    /// * `command` - The command string to validate.
19    ///
20    /// # Returns
21    ///
22    /// - `Ok(())` if the command is valid.
23    /// - `Err(TaskError::InvalidConfiguration)` if the command is invalid.
24    ///
25    /// # Errors
26    ///
27    /// Returns a [`TaskError::InvalidConfiguration`] if:
28    /// - Command is empty or contains only whitespace
29    /// - Command has leading or trailing whitespace
30    /// - Command length exceeds maximum allowed length
31    ///
32    /// # Examples
33    /// ```rust
34    /// use tcrm_task::tasks::validator::ConfigValidator;
35    ///
36    /// let valid_command = "echo";
37    /// assert!(ConfigValidator::validate_command(valid_command).is_ok());
38    ///
39    /// let invalid_command = "";
40    /// assert!(ConfigValidator::validate_command(invalid_command).is_err());
41    /// ```
42    pub fn validate_command(command: &str) -> Result<(), TaskError> {
43        // Check for empty or whitespace-only commands
44        if command.trim().is_empty() {
45            return Err(TaskError::InvalidConfiguration(
46                "Command cannot be empty".to_string(),
47            ));
48        }
49
50        // Only check for obvious injection attempts, not shell features
51        // if Self::contains_obvious_injection(command) {
52        //     return Err(TaskError::InvalidConfiguration(
53        //         "Command contains potentially dangerous injection patterns".to_string(),
54        //     ));
55        // }
56
57        if command.trim() != command {
58            return Err(TaskError::InvalidConfiguration(
59                "Command cannot have leading or trailing whitespace".to_string(),
60            ));
61        }
62        if command.len() > MAX_COMMAND_LEN {
63            return Err(TaskError::InvalidConfiguration(
64                "Command length exceeds maximum allowed length".to_string(),
65            ));
66        }
67
68        Ok(())
69    }
70
71    /// Validates arguments.
72    ///
73    /// # Arguments
74    ///
75    /// * `args` - A slice of argument strings to validate.
76    ///
77    /// # Returns
78    ///
79    /// - `Ok(())` if all arguments are valid.
80    /// - `Err(TaskError::InvalidConfiguration)` if any argument is invalid.
81    ///
82    /// # Errors
83    ///
84    /// Returns a [`TaskError::InvalidConfiguration`] if:
85    /// - any argument contains null bytes
86    /// - any argument is an empty string
87    /// - any argument has leading or trailing whitespace
88    /// - any argument exceeds maximum length
89    ///
90    /// # Examples
91    /// ```rust
92    /// use tcrm_task::tasks::validator::ConfigValidator;
93    ///
94    /// let valid_args = vec!["arg1".to_string(), "arg2".to_string()];
95    /// assert!(ConfigValidator::validate_args(&valid_args).is_ok());
96    ///
97    /// let invalid_args = vec!["arg1".to_string(), "\0".to_string()];
98    /// assert!(ConfigValidator::validate_args(&invalid_args).is_err());
99    /// ```
100    pub fn validate_args(args: &[String]) -> Result<(), TaskError> {
101        for arg in args {
102            // Only check for null bytes
103            if arg.contains('\0') {
104                return Err(TaskError::InvalidConfiguration(
105                    "Argument contains null characters".to_string(),
106                ));
107            }
108            if arg.is_empty() {
109                return Err(TaskError::InvalidConfiguration(
110                    "Arguments cannot be empty".to_string(),
111                ));
112            }
113            if arg.trim() != arg {
114                return Err(TaskError::InvalidConfiguration(format!(
115                    "Argument '{arg}' cannot have leading/trailing whitespace"
116                )));
117            }
118            if arg.len() > MAX_ARG_LEN {
119                return Err(TaskError::InvalidConfiguration(format!(
120                    "Argument '{arg}' exceeds maximum length"
121                )));
122            }
123        }
124        Ok(())
125    }
126
127    /// Validates the working directory path.
128    ///
129    /// # Arguments
130    ///
131    /// * `dir` - The directory path to validate.
132    ///
133    /// # Returns
134    ///
135    /// - `Ok(())` if the directory is valid.
136    /// - `Err(TaskError::InvalidConfiguration)` if the directory is invalid.
137    ///
138    /// # Errors
139    ///
140    /// Returns a [`TaskError::InvalidConfiguration`] if:
141    /// - Directory does not exist
142    /// - Path exists but is not a directory
143    /// - Directory has leading or trailing whitespace
144    /// - Directory path exceeds maximum length
145    ///
146    /// # Examples
147    /// ```rust
148    /// use tcrm_task::tasks::validator::ConfigValidator;
149    /// use std::env;
150    ///
151    /// // Test with current directory (should exist)
152    /// let current_dir = env::current_dir().unwrap();
153    /// let valid_dir = current_dir.to_str().unwrap();
154    /// assert!(ConfigValidator::validate_working_dir(valid_dir).is_ok());
155    ///
156    /// // Test with nonexistent directory
157    /// let invalid_dir = "nonexistent_dir_12345";
158    /// assert!(ConfigValidator::validate_working_dir(invalid_dir).is_err());
159    /// ```
160    pub fn validate_working_dir(dir: &str) -> Result<(), TaskError> {
161        let path = Path::new(dir);
162
163        // Check if path exists
164        if !path.exists() {
165            return Err(TaskError::IO(format!(
166                "Working directory does not exist: {dir}"
167            )));
168        }
169
170        // Check if it's actually a directory
171        if !path.is_dir() {
172            return Err(TaskError::InvalidConfiguration(format!(
173                "Working directory is not a directory: {dir}"
174            )));
175        }
176
177        if dir.trim() != dir {
178            return Err(TaskError::InvalidConfiguration(
179                "Working directory cannot have leading/trailing whitespace".to_string(),
180            ));
181        }
182        if dir.len() > MAX_WORKING_DIR_LEN {
183            return Err(TaskError::InvalidConfiguration(
184                "Working directory path exceeds maximum length".to_string(),
185            ));
186        }
187
188        Ok(())
189    }
190
191    /// Validates environment variables.
192    ///
193    /// # Arguments
194    ///
195    /// * `env` - A hashmap of environment variable key-value pairs to validate.
196    ///
197    /// # Returns
198    ///
199    /// - `Ok(())` if all environment variables are valid.
200    /// - `Err(TaskError::InvalidConfiguration)` if any environment variable is invalid.
201    ///
202    /// # Errors
203    ///
204    /// Returns a [`TaskError::InvalidConfiguration`] if:
205    /// - Any environment variable key contains spaces, '=', or null bytes
206    /// - Any environment variable value contains null bytes
207    /// - Any environment variable key/value exceeds maximum length
208    ///
209    /// # Examples
210    /// ```rust
211    /// use tcrm_task::tasks::validator::ConfigValidator;
212    /// use std::collections::HashMap;
213    ///
214    /// let mut valid_env = HashMap::new();
215    /// valid_env.insert("KEY".to_string(), "VALUE".to_string());
216    /// assert!(ConfigValidator::validate_env_vars(&valid_env).is_ok());
217    ///
218    /// let mut invalid_env = HashMap::new();
219    /// invalid_env.insert("KEY\0".to_string(), "VALUE".to_string());
220    /// assert!(ConfigValidator::validate_env_vars(&invalid_env).is_err());
221    /// ```
222    pub fn validate_env_vars(env: &HashMap<String, String>) -> Result<(), TaskError> {
223        for (key, value) in env {
224            // Validate key
225            if key.trim().is_empty() {
226                return Err(TaskError::InvalidConfiguration(
227                    "Environment variable key cannot be empty".to_string(),
228                ));
229            }
230            if key.contains('=') || key.contains('\0') || key.contains('\t') || key.contains('\n') {
231                return Err(TaskError::InvalidConfiguration(
232                    "Environment variable key contains invalid characters".to_string(),
233                ));
234            }
235
236            if key.contains(' ') {
237                return Err(TaskError::InvalidConfiguration(format!(
238                    "Environment variable key '{key}' cannot contain spaces"
239                )));
240            }
241
242            if key.len() > MAX_ENV_KEY_LEN {
243                return Err(TaskError::InvalidConfiguration(format!(
244                    "Environment variable key '{key}' exceeds maximum length"
245                )));
246            }
247
248            // Validate value
249            if value.contains('\0') {
250                return Err(TaskError::InvalidConfiguration(
251                    "Environment variable value contains null characters".to_string(),
252                ));
253            }
254
255            if value.trim() != value {
256                return Err(TaskError::InvalidConfiguration(format!(
257                    "Environment variable '{key}' value cannot have leading/trailing whitespace"
258                )));
259            }
260            if value.len() > MAX_ENV_VALUE_LEN {
261                return Err(TaskError::InvalidConfiguration(format!(
262                    "Environment variable '{key}' value exceeds maximum length"
263                )));
264            }
265        }
266        Ok(())
267    }
268
269    /// Validates ready indicator string
270    ///
271    /// # Arguments
272    ///
273    /// * `indicator` - The ready indicator string to validate
274    ///
275    /// # Returns
276    ///
277    /// - `Ok(())` if the indicator is valid
278    /// - `Err(TaskError::InvalidConfiguration)` if the indicator is invalid
279    ///
280    /// # Errors
281    ///
282    /// Returns [`TaskError::InvalidConfiguration`] if:
283    /// - Indicator is empty
284    ///
285    /// # Examples
286    ///
287    /// ```rust
288    /// use tcrm_task::tasks::validator::ConfigValidator;
289    ///
290    /// let valid_indicator = "ready";
291    /// assert!(ConfigValidator::validate_ready_indicator(valid_indicator).is_ok());
292    ///
293    /// let invalid_indicator = "";
294    /// assert!(ConfigValidator::validate_ready_indicator(invalid_indicator).is_err());
295    /// ```
296    pub fn validate_ready_indicator(indicator: &str) -> Result<(), TaskError> {
297        if indicator.is_empty() {
298            return Err(TaskError::InvalidConfiguration(
299                "ready_indicator cannot be empty string".to_string(),
300            ));
301        }
302        Ok(())
303    }
304
305    /// Validates timeout value.
306    ///
307    /// Ensures that the timeout value is greater than 0. A timeout of 0 would mean
308    /// immediate timeout, which is not useful for task execution.
309    ///
310    /// # Arguments
311    ///
312    /// * `timeout` - The timeout value in milliseconds to validate
313    ///
314    /// # Returns
315    ///
316    /// * `Ok(())` - If timeout is valid (greater than 0)
317    /// * `Err(TaskError)` - If timeout is 0
318    ///
319    /// # Example
320    ///
321    /// ```rust
322    /// use tcrm_task::tasks::validator::ConfigValidator;
323    ///
324    /// // Valid timeout
325    /// assert!(ConfigValidator::validate_timeout(&5000).is_ok());
326    ///
327    /// // Invalid timeout (0)
328    /// assert!(ConfigValidator::validate_timeout(&0).is_err());
329    /// ```
330    pub fn validate_timeout(timeout: &u64) -> Result<(), TaskError> {
331        if *timeout == 0 {
332            return Err(TaskError::InvalidConfiguration(
333                "Timeout must be greater than 0".to_string(),
334            ));
335        }
336        Ok(())
337    }
338
339    /// Checks for obvious injection attempts while allowing normal shell features.
340    ///
341    /// This method identifies clearly malicious patterns without blocking
342    /// legitimate shell functionality. It focuses on patterns that are rarely used
343    /// in normal command execution.
344    ///
345    /// # Arguments
346    ///
347    /// * `input` - The command string to check for injection patterns.
348    ///
349    /// # Returns
350    ///
351    /// `true` if obvious injection patterns are detected, `false` otherwise.
352    pub fn contains_obvious_injection(input: &str) -> bool {
353        // Only block patterns that are clearly malicious, not functional shell features
354        let obvious_injection_patterns = [
355            "\0",         // Null bytes
356            "\x00",       // Null bytes (hex)
357            "\r\n",       // CRLF injection
358            "eval(",      // Direct eval calls
359            "exec(",      // Direct exec calls
360            "os.system(", // Direct Python code execution
361        ];
362
363        obvious_injection_patterns
364            .iter()
365            .any(|pattern| input.contains(pattern))
366    }
367
368    /// Validates command with strict security rules for untrusted input sources.
369    ///
370    /// This is an alternative to `validate_command` that blocks all shell features
371    ///
372    /// # Arguments
373    ///
374    /// * `command` - The command string to validate strictly.
375    ///
376    /// # Returns
377    ///
378    /// - `Ok(())` if the command passes strict validation.
379    /// - `Err(TaskError::InvalidConfiguration)` if the command contains potentially dangerous patterns.
380    ///
381    /// # Errors
382    ///
383    /// Returns a [`TaskError::InvalidConfiguration`] if:
384    /// - Command is empty or contains only whitespace
385    /// - Command contains shell metacharacters or redirection operators
386    ///
387    /// # Examples
388    /// ```rust
389    /// use tcrm_task::tasks::validator::ConfigValidator;
390    ///
391    /// // Simple command should pass
392    /// let simple_command = "echo";
393    /// assert!(ConfigValidator::validate_command_strict(simple_command).is_ok());
394    ///
395    /// // Command with shell features should fail
396    /// let shell_command = "echo hello; rm -rf /";
397    /// assert!(ConfigValidator::validate_command_strict(shell_command).is_err());
398    /// ```
399    #[allow(dead_code)]
400    pub fn validate_command_strict(command: &str) -> Result<(), TaskError> {
401        // Check for empty or whitespace-only commands
402        if command.trim().is_empty() {
403            return Err(TaskError::InvalidConfiguration(
404                "Command cannot be empty".to_string(),
405            ));
406        }
407
408        // Strict validation - blocks shell features
409        let dangerous_patterns = [
410            ";", "&", "|", "`", "#", "$", "(", ")", "{", "}", "[", "]", "<", ">", "&&", "||", ">>",
411            "<<", "\n", "\r",
412        ];
413
414        if dangerous_patterns
415            .iter()
416            .any(|pattern| command.contains(pattern))
417        {
418            return Err(TaskError::InvalidConfiguration(
419                "Command contains shell metacharacters (use validate_command for developer tools)"
420                    .to_string(),
421            ));
422        }
423
424        Ok(())
425    }
426}