1use crate::error::{NetwatchError, Result};
7use crate::security::{record_security_event, SecurityEvent};
8use std::path::Path;
9
10const MAX_INTERFACE_NAME_LEN: usize = 16;
12
13const MAX_PATH_LEN: usize = 4096;
15
16const MAX_REFRESH_INTERVAL: u64 = 60_000; const MIN_REFRESH_INTERVAL: u64 = 100; pub fn validate_interface_name(name: &str) -> Result<()> {
39 if name.is_empty() {
41 record_security_event(SecurityEvent::InvalidInput {
42 input_type: "interface_name".to_string(),
43 attempted_value: name.to_string(),
44 source: "validation".to_string(),
45 });
46 return Err(NetwatchError::Parse(
47 "Interface name cannot be empty".to_string(),
48 ));
49 }
50
51 if name.len() > MAX_INTERFACE_NAME_LEN {
52 record_security_event(SecurityEvent::InvalidInput {
53 input_type: "interface_name".to_string(),
54 attempted_value: name.to_string(),
55 source: "validation".to_string(),
56 });
57 return Err(NetwatchError::Parse(format!(
58 "Interface name too long (max {MAX_INTERFACE_NAME_LEN} characters)"
59 )));
60 }
61
62 if name.contains("..") || name.contains('/') || name.contains('\\') {
64 record_security_event(SecurityEvent::InvalidInput {
65 input_type: "interface_name".to_string(),
66 attempted_value: name.to_string(),
67 source: "validation".to_string(),
68 });
69 return Err(NetwatchError::Parse(
70 "Invalid characters in interface name".to_string(),
71 ));
72 }
73
74 if name.contains('\0') || name.chars().any(|c| c.is_control()) {
76 record_security_event(SecurityEvent::InvalidInput {
77 input_type: "interface_name".to_string(),
78 attempted_value: name.to_string(),
79 source: "validation".to_string(),
80 });
81 return Err(NetwatchError::Parse(
82 "Control characters not allowed in interface name".to_string(),
83 ));
84 }
85
86 if !name
88 .chars()
89 .all(|c| c.is_alphanumeric() || c == '-' || c == '_' || c == '.')
90 {
91 record_security_event(SecurityEvent::InvalidInput {
92 input_type: "interface_name".to_string(),
93 attempted_value: name.to_string(),
94 source: "validation".to_string(),
95 });
96 return Err(NetwatchError::Parse(
97 "Invalid characters in interface name".to_string(),
98 ));
99 }
100
101 let name_lower = name.to_lowercase();
103 if name_lower.contains("proc") || name_lower.contains("sys") || name_lower.contains("dev") {
104 record_security_event(SecurityEvent::InvalidInput {
105 input_type: "interface_name".to_string(),
106 attempted_value: name.to_string(),
107 source: "validation".to_string(),
108 });
109 return Err(NetwatchError::Parse(
110 "Suspicious interface name pattern".to_string(),
111 ));
112 }
113
114 Ok(())
115}
116
117pub fn validate_file_path(path: &str, expected_extension: Option<&str>) -> Result<()> {
133 if path.is_empty() {
134 return Err(NetwatchError::Config(
135 "File path cannot be empty".to_string(),
136 ));
137 }
138
139 if path.len() > MAX_PATH_LEN {
140 return Err(NetwatchError::Config(format!(
141 "File path too long (max {MAX_PATH_LEN} characters)"
142 )));
143 }
144
145 if path.contains('\0') || path.chars().any(|c| c.is_control()) {
147 return Err(NetwatchError::Config(
148 "Control characters not allowed in file path".to_string(),
149 ));
150 }
151
152 let path_obj = Path::new(path);
153
154 if path.contains("..") {
156 record_security_event(SecurityEvent::InvalidInput {
157 input_type: "file_path".to_string(),
158 attempted_value: path.to_string(),
159 source: "validation".to_string(),
160 });
161 return Err(NetwatchError::Config("Path traversal detected".to_string()));
162 }
163
164 let sensitive_dirs = [
166 "/etc",
167 "/boot",
168 "/proc",
169 "/sys",
170 "/dev",
171 "/root",
172 "/usr/bin",
173 "/usr/sbin",
174 "/bin",
175 "/sbin",
176 ];
177
178 for sensitive_dir in &sensitive_dirs {
179 if path.starts_with(sensitive_dir) {
180 return Err(NetwatchError::Config(
181 "Access to sensitive directory denied".to_string(),
182 ));
183 }
184 }
185
186 if let Some(expected_ext) = expected_extension {
188 if let Some(extension) = path_obj.extension() {
189 if extension.to_string_lossy().to_lowercase() != expected_ext.to_lowercase() {
190 return Err(NetwatchError::Config(format!(
191 "Invalid file extension, expected: {expected_ext}"
192 )));
193 }
194 } else {
195 return Err(NetwatchError::Config(format!(
196 "Missing file extension, expected: {expected_ext}"
197 )));
198 }
199 }
200
201 Ok(())
202}
203
204pub fn validate_refresh_interval(interval_ms: u64) -> Result<()> {
211 if interval_ms < MIN_REFRESH_INTERVAL {
212 return Err(NetwatchError::Config(format!(
213 "Refresh interval too small (minimum {MIN_REFRESH_INTERVAL} ms)"
214 )));
215 }
216
217 if interval_ms > MAX_REFRESH_INTERVAL {
218 return Err(NetwatchError::Config(format!(
219 "Refresh interval too large (maximum {MAX_REFRESH_INTERVAL} ms)"
220 )));
221 }
222
223 Ok(())
224}
225
226pub fn validate_bandwidth(bandwidth_kbps: u64) -> Result<()> {
233 const MAX_BANDWIDTH: u64 = 1_000_000_000;
235
236 if bandwidth_kbps > MAX_BANDWIDTH {
237 return Err(NetwatchError::Config(format!(
238 "Bandwidth value too large (maximum {MAX_BANDWIDTH} kbps)"
239 )));
240 }
241
242 Ok(())
243}
244
245pub fn validate_config_string(value: &str, field_name: &str) -> Result<()> {
252 if value.len() > 1024 {
253 return Err(NetwatchError::Config(format!(
254 "Configuration value too long for field: {field_name}"
255 )));
256 }
257
258 if value.contains('\0')
260 || value
261 .chars()
262 .any(|c| c.is_control() && c != '\n' && c != '\t')
263 {
264 return Err(NetwatchError::Config(format!(
265 "Invalid characters in configuration field: {field_name}"
266 )));
267 }
268
269 let dangerous_patterns = ["$(", "`", "${", "&&", "||", ";", "|", ">", "<", "&"];
271
272 for pattern in &dangerous_patterns {
273 if value.contains(pattern) {
274 return Err(NetwatchError::Config(format!(
275 "Suspicious pattern detected in field: {field_name}"
276 )));
277 }
278 }
279
280 Ok(())
281}
282
283pub fn sanitize_user_input(input: &str, max_length: usize) -> String {
290 input
291 .chars()
292 .filter(|c| !c.is_control() || *c == '\n' || *c == '\t')
293 .take(max_length)
294 .collect::<String>()
295 .replace('$', "\\$")
296 .replace('`', "\\`")
297 .replace('"', "\\\"")
298 .replace('\'', "\\'")
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_interface_name_validation() {
307 assert!(validate_interface_name("eth0").is_ok());
309 assert!(validate_interface_name("wlan0").is_ok());
310 assert!(validate_interface_name("en0").is_ok());
311 assert!(validate_interface_name("lo").is_ok());
312 assert!(validate_interface_name("br-docker0").is_ok());
313
314 assert!(validate_interface_name("").is_err());
316 assert!(validate_interface_name("../../../etc/passwd").is_err());
317 assert!(validate_interface_name(
318 "interface_with_very_long_name_that_exceeds_the_maximum_allowed_length"
319 )
320 .is_err());
321 assert!(validate_interface_name("interface with spaces").is_err());
322 assert!(validate_interface_name("interface\x00null").is_err());
323 assert!(validate_interface_name("interface\nwith\nnewlines").is_err());
324 assert!(validate_interface_name("/proc/net/dev").is_err());
325 assert!(validate_interface_name("proc").is_err());
326 assert!(validate_interface_name("sys").is_err());
327 }
328
329 #[test]
330 fn test_file_path_validation() {
331 assert!(validate_file_path("/tmp/netwatch.log", Some("log")).is_ok());
333 assert!(validate_file_path("/home/user/config.toml", Some("toml")).is_ok());
334 assert!(validate_file_path("./local.log", Some("log")).is_ok());
335
336 assert!(validate_file_path("", None).is_err());
338 assert!(validate_file_path("../../../etc/passwd", None).is_err());
339 assert!(validate_file_path("/etc/shadow", None).is_err());
340 assert!(validate_file_path("/proc/version", None).is_err());
341 assert!(validate_file_path("file\x00with\x00nulls", None).is_err());
342 assert!(validate_file_path("/tmp/file.txt", Some("log")).is_err()); }
344
345 #[test]
346 fn test_refresh_interval_validation() {
347 assert!(validate_refresh_interval(500).is_ok());
349 assert!(validate_refresh_interval(1000).is_ok());
350 assert!(validate_refresh_interval(30000).is_ok());
351
352 assert!(validate_refresh_interval(50).is_err()); assert!(validate_refresh_interval(120000).is_err()); }
356
357 #[test]
358 fn test_bandwidth_validation() {
359 assert!(validate_bandwidth(1000).is_ok());
361 assert!(validate_bandwidth(1000000).is_ok());
362 assert!(validate_bandwidth(100000000).is_ok());
363
364 assert!(validate_bandwidth(u64::MAX).is_err()); assert!(validate_bandwidth(2_000_000_000).is_err()); }
368
369 #[test]
370 fn test_config_string_validation() {
371 assert!(validate_config_string("normal_value", "test_field").is_ok());
373 assert!(validate_config_string("value-with-hyphens", "test_field").is_ok());
374 assert!(validate_config_string("value_with_underscores", "test_field").is_ok());
375
376 assert!(validate_config_string("value$(echo hack)", "test_field").is_err());
378 assert!(validate_config_string("value`whoami`", "test_field").is_err());
379 assert!(validate_config_string("value && rm -rf /", "test_field").is_err());
380 assert!(validate_config_string("value\x00null", "test_field").is_err());
381 assert!(validate_config_string(&"x".repeat(2000), "test_field").is_err());
382 }
384
385 #[test]
386 fn test_sanitize_user_input() {
387 assert_eq!(sanitize_user_input("normal input", 100), "normal input");
388 assert_eq!(sanitize_user_input("input$(echo)", 100), "input\\$(echo)");
389 assert_eq!(
390 sanitize_user_input("input`whoami`", 100),
391 "input\\`whoami\\`"
392 );
393 assert_eq!(
394 sanitize_user_input("input\"quoted\"", 100),
395 "input\\\"quoted\\\""
396 );
397 assert_eq!(
398 sanitize_user_input("very long input that exceeds limit", 10),
399 "very long "
400 );
401 assert_eq!(sanitize_user_input("input\x00null", 100), "inputnull");
402 }
403}