adaptive_pipeline_bootstrap/cli/
validator.rs1use crate::config::AppConfig;
43use std::path::{Path, PathBuf};
44use thiserror::Error;
45
46const MAX_ARG_COUNT: usize = 100;
48
49const MAX_ARG_LENGTH: usize = 1000;
51
52const MAX_PATH_LENGTH: usize = 4096;
54
55const DANGEROUS_PATTERNS: &[&str] = &[
57 "..", "~", "$", "`", ";", "&", "|", ">", "<", "\n", "\r", "\0", ];
70
71const PROTECTED_DIRS: &[&str] = &[
73 "/etc",
74 "/bin",
75 "/sbin",
76 "/usr/bin",
77 "/usr/sbin",
78 "/boot",
79 "/sys",
80 "/proc",
81 "/dev",
82];
83
84#[derive(Debug, Error)]
86pub enum ParseError {
87 #[error("Too many arguments (max {MAX_ARG_COUNT})")]
89 TooManyArguments,
90
91 #[error("Argument too long (max {MAX_ARG_LENGTH} characters): {0}")]
93 ArgumentTooLong(String),
94
95 #[error("Dangerous pattern detected in argument: {pattern} in {arg}")]
97 DangerousPattern { pattern: String, arg: String },
98
99 #[error("Path exceeds maximum length (max {MAX_PATH_LENGTH})")]
101 PathTooLong,
102
103 #[error("Access to protected system directory denied: {0}")]
105 ProtectedDirectory(String),
106
107 #[error("Path does not exist: {0}")]
109 PathNotFound(String),
110
111 #[error("Invalid path: {0}")]
113 InvalidPath(String),
114
115 #[error("Missing required argument: {0}")]
117 MissingArgument(String),
118
119 #[error("Invalid argument value for {arg}: {reason}")]
121 InvalidValue { arg: String, reason: String },
122}
123
124pub struct SecureArgParser;
128
129impl SecureArgParser {
130 pub fn parse(args: &[String]) -> Result<AppConfig, ParseError> {
144 if args.len() > MAX_ARG_COUNT {
146 return Err(ParseError::TooManyArguments);
147 }
148
149 Ok(AppConfig::builder().app_name("adaptive-pipeline").build())
160 }
161
162 pub fn validate_argument(arg: &str) -> Result<(), ParseError> {
169 if arg.len() > MAX_ARG_LENGTH {
171 return Err(ParseError::ArgumentTooLong(
172 arg.chars().take(50).collect::<String>() + "...",
173 ));
174 }
175
176 for pattern in DANGEROUS_PATTERNS {
178 if arg.contains(pattern) {
179 return Err(ParseError::DangerousPattern {
180 pattern: pattern.to_string(),
181 arg: arg.to_string(),
182 });
183 }
184 }
185
186 Ok(())
187 }
188
189 pub fn validate_path(path: &str) -> Result<PathBuf, ParseError> {
206 Self::validate_argument(path).map_err(|e| match e {
208 ParseError::ArgumentTooLong(_) => ParseError::InvalidPath(format!("Path too long: {}", path)),
209 ParseError::DangerousPattern { pattern, .. } => {
210 ParseError::InvalidPath(format!("Path contains dangerous pattern '{}': {}", pattern, path))
211 }
212 other => other,
213 })?;
214
215 let path_obj = Path::new(path);
217
218 let canonical = path_obj.canonicalize().map_err(|e| {
220 if !path_obj.exists() {
221 ParseError::PathNotFound(path.to_string())
222 } else {
223 ParseError::InvalidPath(format!("{}: {}", path, e))
224 }
225 })?;
226
227 if canonical.to_string_lossy().len() > MAX_PATH_LENGTH {
229 return Err(ParseError::PathTooLong);
230 }
231
232 for protected in PROTECTED_DIRS {
234 if canonical.starts_with(protected) {
235 return Err(ParseError::ProtectedDirectory(canonical.display().to_string()));
236 }
237 }
238
239 Ok(canonical)
240 }
241
242 pub fn validate_optional_path(path: Option<&str>) -> Result<Option<PathBuf>, ParseError> {
244 match path {
245 Some(p) => Self::validate_path(p).map(Some),
246 None => Ok(None),
247 }
248 }
249
250 pub fn validate_number<T>(arg_name: &str, value: &str, min: Option<T>, max: Option<T>) -> Result<T, ParseError>
252 where
253 T: std::str::FromStr + PartialOrd + std::fmt::Display,
254 {
255 Self::validate_argument(value)?;
257
258 let num = value.parse::<T>().map_err(|_| ParseError::InvalidValue {
260 arg: arg_name.to_string(),
261 reason: format!("Not a valid number: {}", value),
262 })?;
263
264 if let Some(min_val) = min {
266 if num < min_val {
267 return Err(ParseError::InvalidValue {
268 arg: arg_name.to_string(),
269 reason: format!("Value {} is less than minimum {}", value, min_val),
270 });
271 }
272 }
273
274 if let Some(max_val) = max {
275 if num > max_val {
276 return Err(ParseError::InvalidValue {
277 arg: arg_name.to_string(),
278 reason: format!("Value {} is greater than maximum {}", value, max_val),
279 });
280 }
281 }
282
283 Ok(num)
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 mod argument_validation {
292 use super::*;
293
294 #[test]
295 fn accepts_safe_arguments() {
296 assert!(SecureArgParser::validate_argument("safe-arg").is_ok());
297 assert!(SecureArgParser::validate_argument("file.txt").is_ok());
298 assert!(SecureArgParser::validate_argument("path/to/file").is_ok());
299 }
300
301 #[test]
302 fn rejects_too_long_arguments() {
303 let long_arg = "a".repeat(MAX_ARG_LENGTH + 1);
304 assert!(matches!(
305 SecureArgParser::validate_argument(&long_arg),
306 Err(ParseError::ArgumentTooLong(_))
307 ));
308 }
309
310 #[test]
311 fn detects_dangerous_patterns() {
312 let dangerous = vec![
313 "../etc/passwd",
314 "~/.ssh/id_rsa",
315 "$(whoami)",
316 "`ls`",
317 "file;rm -rf /",
318 "file&background",
319 "file|pipe",
320 "file>output",
321 "file<input",
322 "file\nwith\nnewlines",
323 ];
324
325 for arg in dangerous {
326 assert!(
327 matches!(
328 SecureArgParser::validate_argument(arg),
329 Err(ParseError::DangerousPattern { .. })
330 ),
331 "Failed to detect dangerous pattern in: {}",
332 arg
333 );
334 }
335 }
336 }
337
338 mod number_validation {
339 use super::*;
340
341 #[test]
342 fn validates_valid_numbers() {
343 let result = SecureArgParser::validate_number::<u32>("threads", "8", Some(1), Some(16));
344 assert_eq!(result.unwrap(), 8);
345 }
346
347 #[test]
348 fn rejects_invalid_numbers() {
349 let result = SecureArgParser::validate_number::<u32>("threads", "abc", None, None);
350 assert!(matches!(result, Err(ParseError::InvalidValue { .. })));
351 }
352
353 #[test]
354 fn enforces_range_constraints() {
355 let result = SecureArgParser::validate_number::<u32>("threads", "100", Some(1), Some(16));
356 assert!(matches!(result, Err(ParseError::InvalidValue { .. })));
357
358 let result = SecureArgParser::validate_number::<u32>("threads", "0", Some(1), Some(16));
359 assert!(matches!(result, Err(ParseError::InvalidValue { .. })));
360 }
361 }
362
363 mod parsing {
364 use super::*;
365
366 #[test]
367 fn parses_basic_arguments() {
368 let args = vec!["program".to_string()];
369 let result = SecureArgParser::parse(&args);
370 assert!(result.is_ok());
371 }
372
373 #[test]
374 fn rejects_too_many_arguments() {
375 let args = vec!["arg".to_string(); MAX_ARG_COUNT + 1];
376 let result = SecureArgParser::parse(&args);
377 assert!(matches!(result, Err(ParseError::TooManyArguments)));
378 }
379 }
380}