tcrm-task 0.4.2

Task execution unit for TCRM project
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
use std::collections::HashMap;
use std::path::Path;

use crate::tasks::error::TaskError;
const MAX_COMMAND_LEN: usize = 4096;
const MAX_ARG_LEN: usize = 4096;
const MAX_WORKING_DIR_LEN: usize = 4096;
const MAX_ENV_KEY_LEN: usize = 1024;
const MAX_ENV_VALUE_LEN: usize = 4096;
/// Security validation utilities for task configuration
pub struct ConfigValidator;

impl ConfigValidator {
    /// Validates command name.
    ///
    /// # Arguments
    ///
    /// * `command` - The command string to validate.
    ///
    /// # Returns
    ///
    /// - `Ok(())` if the command is valid.
    /// - `Err(TaskError::InvalidConfiguration)` if the command is invalid.
    ///
    /// # Errors
    ///
    /// Returns a [`TaskError::InvalidConfiguration`] if:
    /// - Command is empty or contains only whitespace
    /// - Command has leading or trailing whitespace
    /// - Command length exceeds maximum allowed length
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    ///
    /// let valid_command = "echo";
    /// assert!(ConfigValidator::validate_command(valid_command).is_ok());
    ///
    /// let invalid_command = "";
    /// assert!(ConfigValidator::validate_command(invalid_command).is_err());
    /// ```
    pub fn validate_command(command: &str) -> Result<(), TaskError> {
        // Check for empty or whitespace-only commands
        if command.trim().is_empty() {
            return Err(TaskError::InvalidConfiguration(
                "Command cannot be empty".to_string(),
            ));
        }

        // Only check for obvious injection attempts, not shell features
        // if Self::contains_obvious_injection(command) {
        //     return Err(TaskError::InvalidConfiguration(
        //         "Command contains potentially dangerous injection patterns".to_string(),
        //     ));
        // }

        if command.trim() != command {
            return Err(TaskError::InvalidConfiguration(
                "Command cannot have leading or trailing whitespace".to_string(),
            ));
        }
        if command.len() > MAX_COMMAND_LEN {
            return Err(TaskError::InvalidConfiguration(
                "Command length exceeds maximum allowed length".to_string(),
            ));
        }

        Ok(())
    }

    /// Validates arguments.
    ///
    /// # Arguments
    ///
    /// * `args` - A slice of argument strings to validate.
    ///
    /// # Returns
    ///
    /// - `Ok(())` if all arguments are valid.
    /// - `Err(TaskError::InvalidConfiguration)` if any argument is invalid.
    ///
    /// # Errors
    ///
    /// Returns a [`TaskError::InvalidConfiguration`] if:
    /// - any argument contains null bytes
    /// - any argument is an empty string
    /// - any argument has leading or trailing whitespace
    /// - any argument exceeds maximum length
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    ///
    /// let valid_args = vec!["arg1".to_string(), "arg2".to_string()];
    /// assert!(ConfigValidator::validate_args(&valid_args).is_ok());
    ///
    /// let invalid_args = vec!["arg1".to_string(), "\0".to_string()];
    /// assert!(ConfigValidator::validate_args(&invalid_args).is_err());
    /// ```
    pub fn validate_args(args: &[String]) -> Result<(), TaskError> {
        for arg in args {
            // Only check for null bytes
            if arg.contains('\0') {
                return Err(TaskError::InvalidConfiguration(
                    "Argument contains null characters".to_string(),
                ));
            }
            if arg.is_empty() {
                return Err(TaskError::InvalidConfiguration(
                    "Arguments cannot be empty".to_string(),
                ));
            }
            if arg.trim() != arg {
                return Err(TaskError::InvalidConfiguration(format!(
                    "Argument '{arg}' cannot have leading/trailing whitespace"
                )));
            }
            if arg.len() > MAX_ARG_LEN {
                return Err(TaskError::InvalidConfiguration(format!(
                    "Argument '{arg}' exceeds maximum length"
                )));
            }
        }
        Ok(())
    }

    /// Validates the working directory path.
    ///
    /// # Arguments
    ///
    /// * `dir` - The directory path to validate.
    ///
    /// # Returns
    ///
    /// - `Ok(())` if the directory is valid.
    /// - `Err(TaskError::InvalidConfiguration)` if the directory is invalid.
    ///
    /// # Errors
    ///
    /// Returns a [`TaskError::InvalidConfiguration`] if:
    /// - Directory does not exist
    /// - Path exists but is not a directory
    /// - Directory has leading or trailing whitespace
    /// - Directory path exceeds maximum length
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    /// use std::env;
    ///
    /// // Test with current directory (should exist)
    /// let current_dir = env::current_dir().unwrap();
    /// let valid_dir = current_dir.to_str().unwrap();
    /// assert!(ConfigValidator::validate_working_dir(valid_dir).is_ok());
    ///
    /// // Test with nonexistent directory
    /// let invalid_dir = "nonexistent_dir_12345";
    /// assert!(ConfigValidator::validate_working_dir(invalid_dir).is_err());
    /// ```
    pub fn validate_working_dir(dir: &str) -> Result<(), TaskError> {
        let path = Path::new(dir);

        // Check if path exists
        if !path.exists() {
            return Err(TaskError::IO(format!(
                "Working directory does not exist: {dir}"
            )));
        }

        // Check if it's actually a directory
        if !path.is_dir() {
            return Err(TaskError::InvalidConfiguration(format!(
                "Working directory is not a directory: {dir}"
            )));
        }

        if dir.trim() != dir {
            return Err(TaskError::InvalidConfiguration(
                "Working directory cannot have leading/trailing whitespace".to_string(),
            ));
        }
        if dir.len() > MAX_WORKING_DIR_LEN {
            return Err(TaskError::InvalidConfiguration(
                "Working directory path exceeds maximum length".to_string(),
            ));
        }

        Ok(())
    }

    /// Validates environment variables.
    ///
    /// # Arguments
    ///
    /// * `env` - A hashmap of environment variable key-value pairs to validate.
    ///
    /// # Returns
    ///
    /// - `Ok(())` if all environment variables are valid.
    /// - `Err(TaskError::InvalidConfiguration)` if any environment variable is invalid.
    ///
    /// # Errors
    ///
    /// Returns a [`TaskError::InvalidConfiguration`] if:
    /// - Any environment variable key contains spaces, '=', or null bytes
    /// - Any environment variable value contains null bytes
    /// - Any environment variable key/value exceeds maximum length
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    /// use std::collections::HashMap;
    ///
    /// let mut valid_env = HashMap::new();
    /// valid_env.insert("KEY".to_string(), "VALUE".to_string());
    /// assert!(ConfigValidator::validate_env_vars(&valid_env).is_ok());
    ///
    /// let mut invalid_env = HashMap::new();
    /// invalid_env.insert("KEY\0".to_string(), "VALUE".to_string());
    /// assert!(ConfigValidator::validate_env_vars(&invalid_env).is_err());
    /// ```
    pub fn validate_env_vars(env: &HashMap<String, String>) -> Result<(), TaskError> {
        for (key, value) in env {
            // Validate key
            if key.trim().is_empty() {
                return Err(TaskError::InvalidConfiguration(
                    "Environment variable key cannot be empty".to_string(),
                ));
            }
            if key.contains('=') || key.contains('\0') || key.contains('\t') || key.contains('\n') {
                return Err(TaskError::InvalidConfiguration(
                    "Environment variable key contains invalid characters".to_string(),
                ));
            }

            if key.contains(' ') {
                return Err(TaskError::InvalidConfiguration(format!(
                    "Environment variable key '{key}' cannot contain spaces"
                )));
            }

            if key.len() > MAX_ENV_KEY_LEN {
                return Err(TaskError::InvalidConfiguration(format!(
                    "Environment variable key '{key}' exceeds maximum length"
                )));
            }

            // Validate value
            if value.contains('\0') {
                return Err(TaskError::InvalidConfiguration(
                    "Environment variable value contains null characters".to_string(),
                ));
            }

            if value.trim() != value {
                return Err(TaskError::InvalidConfiguration(format!(
                    "Environment variable '{key}' value cannot have leading/trailing whitespace"
                )));
            }
            if value.len() > MAX_ENV_VALUE_LEN {
                return Err(TaskError::InvalidConfiguration(format!(
                    "Environment variable '{key}' value exceeds maximum length"
                )));
            }
        }
        Ok(())
    }

    /// Validates ready indicator string
    ///
    /// # Arguments
    ///
    /// * `indicator` - The ready indicator string to validate
    ///
    /// # Returns
    ///
    /// - `Ok(())` if the indicator is valid
    /// - `Err(TaskError::InvalidConfiguration)` if the indicator is invalid
    ///
    /// # Errors
    ///
    /// Returns [`TaskError::InvalidConfiguration`] if:
    /// - Indicator is empty
    ///
    /// # Examples
    ///
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    ///
    /// let valid_indicator = "ready";
    /// assert!(ConfigValidator::validate_ready_indicator(valid_indicator).is_ok());
    ///
    /// let invalid_indicator = "";
    /// assert!(ConfigValidator::validate_ready_indicator(invalid_indicator).is_err());
    /// ```
    pub fn validate_ready_indicator(indicator: &str) -> Result<(), TaskError> {
        if indicator.is_empty() {
            return Err(TaskError::InvalidConfiguration(
                "ready_indicator cannot be empty string".to_string(),
            ));
        }
        Ok(())
    }

    /// Validates timeout value.
    ///
    /// Ensures that the timeout value is greater than 0. A timeout of 0 would mean
    /// immediate timeout, which is not useful for task execution.
    ///
    /// # Arguments
    ///
    /// * `timeout` - The timeout value in milliseconds to validate
    ///
    /// # Returns
    ///
    /// * `Ok(())` - If timeout is valid (greater than 0)
    /// * `Err(TaskError)` - If timeout is 0
    ///
    /// # Example
    ///
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    ///
    /// // Valid timeout
    /// assert!(ConfigValidator::validate_timeout(&5000).is_ok());
    ///
    /// // Invalid timeout (0)
    /// assert!(ConfigValidator::validate_timeout(&0).is_err());
    /// ```
    pub fn validate_timeout(timeout: &u64) -> Result<(), TaskError> {
        if *timeout == 0 {
            return Err(TaskError::InvalidConfiguration(
                "Timeout must be greater than 0".to_string(),
            ));
        }
        Ok(())
    }

    /// Checks for obvious injection attempts while allowing normal shell features.
    ///
    /// This method identifies clearly malicious patterns without blocking
    /// legitimate shell functionality. It focuses on patterns that are rarely used
    /// in normal command execution.
    ///
    /// # Arguments
    ///
    /// * `input` - The command string to check for injection patterns.
    ///
    /// # Returns
    ///
    /// `true` if obvious injection patterns are detected, `false` otherwise.
    pub fn contains_obvious_injection(input: &str) -> bool {
        // Only block patterns that are clearly malicious, not functional shell features
        let obvious_injection_patterns = [
            "\0",         // Null bytes
            "\x00",       // Null bytes (hex)
            "\r\n",       // CRLF injection
            "eval(",      // Direct eval calls
            "exec(",      // Direct exec calls
            "os.system(", // Direct Python code execution
        ];

        obvious_injection_patterns
            .iter()
            .any(|pattern| input.contains(pattern))
    }

    /// Validates command with strict security rules for untrusted input sources.
    ///
    /// This is an alternative to `validate_command` that blocks all shell features
    ///
    /// # Arguments
    ///
    /// * `command` - The command string to validate strictly.
    ///
    /// # Returns
    ///
    /// - `Ok(())` if the command passes strict validation.
    /// - `Err(TaskError::InvalidConfiguration)` if the command contains potentially dangerous patterns.
    ///
    /// # Errors
    ///
    /// Returns a [`TaskError::InvalidConfiguration`] if:
    /// - Command is empty or contains only whitespace
    /// - Command contains shell metacharacters or redirection operators
    ///
    /// # Examples
    /// ```rust
    /// use tcrm_task::tasks::validator::ConfigValidator;
    ///
    /// // Simple command should pass
    /// let simple_command = "echo";
    /// assert!(ConfigValidator::validate_command_strict(simple_command).is_ok());
    ///
    /// // Command with shell features should fail
    /// let shell_command = "echo hello; rm -rf /";
    /// assert!(ConfigValidator::validate_command_strict(shell_command).is_err());
    /// ```
    #[allow(dead_code)]
    pub fn validate_command_strict(command: &str) -> Result<(), TaskError> {
        // Check for empty or whitespace-only commands
        if command.trim().is_empty() {
            return Err(TaskError::InvalidConfiguration(
                "Command cannot be empty".to_string(),
            ));
        }

        // Strict validation - blocks shell features
        let dangerous_patterns = [
            ";", "&", "|", "`", "#", "$", "(", ")", "{", "}", "[", "]", "<", ">", "&&", "||", ">>",
            "<<", "\n", "\r",
        ];

        if dangerous_patterns
            .iter()
            .any(|pattern| command.contains(pattern))
        {
            return Err(TaskError::InvalidConfiguration(
                "Command contains shell metacharacters (use validate_command for developer tools)"
                    .to_string(),
            ));
        }

        Ok(())
    }
}