1use once_cell::sync::Lazy;
8use std::collections::HashMap;
9
10const MAX_DIRS_TO_LIST: usize = 5;
12
13static GLOB_PATTERN_REGEX: Lazy<regex::Regex> =
15 Lazy::new(|| regex::Regex::new(r"[*?\[\]]").unwrap());
16
17#[derive(Debug, Clone, PartialEq, Default)]
19pub enum FileOperationType {
20 #[default]
21 Read,
22 Write,
23 Create,
24}
25
26#[derive(Debug, Clone)]
28pub struct PathCheckResult {
29 pub allowed: bool,
30 pub decision_reason: Option<String>,
31}
32
33#[derive(Debug, Clone)]
35pub struct ResolvedPathCheckResult {
36 pub allowed: bool,
37 pub decision_reason: Option<String>,
38 pub resolved_path: String,
39}
40
41#[derive(Debug, Clone)]
43pub struct CmdletPathConfig {
44 pub operation_type: FileOperationType,
45 pub path_params: Vec<String>,
46 pub known_switches: Vec<String>,
47 pub known_value_params: Vec<String>,
48 pub leaf_only_path_params: Option<Vec<String>>,
49 pub positional_skip: Option<usize>,
50 pub optional_write: bool,
51}
52
53impl Default for CmdletPathConfig {
54 fn default() -> Self {
55 Self {
56 operation_type: FileOperationType::Read,
57 path_params: Vec::new(),
58 known_switches: Vec::new(),
59 known_value_params: Vec::new(),
60 leaf_only_path_params: None,
61 positional_skip: None,
62 optional_write: false,
63 }
64 }
65}
66
67static CMDLET_PATH_CONFIG: Lazy<HashMap<&'static str, CmdletPathConfig>> = Lazy::new(|| {
69 let mut map = HashMap::new();
70
71 map.insert(
73 "set-content",
74 CmdletPathConfig {
75 operation_type: FileOperationType::Write,
76 path_params: vec![
77 "-path".to_string(),
78 "-literalpath".to_string(),
79 "-pspath".to_string(),
80 "-lp".to_string(),
81 ],
82 known_switches: vec![
83 "-passthru".to_string(),
84 "-force".to_string(),
85 "-whatif".to_string(),
86 "-confirm".to_string(),
87 "-nonewline".to_string(),
88 ],
89 known_value_params: vec![
90 "-value".to_string(),
91 "-filter".to_string(),
92 "-include".to_string(),
93 "-exclude".to_string(),
94 "-encoding".to_string(),
95 ],
96 ..Default::default()
97 },
98 );
99
100 map.insert(
101 "add-content",
102 CmdletPathConfig {
103 operation_type: FileOperationType::Write,
104 path_params: vec![
105 "-path".to_string(),
106 "-literalpath".to_string(),
107 "-pspath".to_string(),
108 "-lp".to_string(),
109 ],
110 known_switches: vec![
111 "-passthru".to_string(),
112 "-force".to_string(),
113 "-whatif".to_string(),
114 "-confirm".to_string(),
115 "-nonewline".to_string(),
116 ],
117 known_value_params: vec![
118 "-value".to_string(),
119 "-filter".to_string(),
120 "-include".to_string(),
121 "-exclude".to_string(),
122 "-encoding".to_string(),
123 ],
124 ..Default::default()
125 },
126 );
127
128 map.insert(
129 "remove-item",
130 CmdletPathConfig {
131 operation_type: FileOperationType::Write,
132 path_params: vec![
133 "-path".to_string(),
134 "-literalpath".to_string(),
135 "-pspath".to_string(),
136 "-lp".to_string(),
137 ],
138 known_switches: vec![
139 "-recurse".to_string(),
140 "-force".to_string(),
141 "-whatif".to_string(),
142 "-confirm".to_string(),
143 ],
144 known_value_params: vec![
145 "-filter".to_string(),
146 "-include".to_string(),
147 "-exclude".to_string(),
148 "-stream".to_string(),
149 ],
150 ..Default::default()
151 },
152 );
153
154 map.insert(
155 "clear-content",
156 CmdletPathConfig {
157 operation_type: FileOperationType::Write,
158 path_params: vec![
159 "-path".to_string(),
160 "-literalpath".to_string(),
161 "-pspath".to_string(),
162 "-lp".to_string(),
163 ],
164 known_switches: vec![
165 "-force".to_string(),
166 "-whatif".to_string(),
167 "-confirm".to_string(),
168 ],
169 known_value_params: vec![
170 "-filter".to_string(),
171 "-include".to_string(),
172 "-exclude".to_string(),
173 "-stream".to_string(),
174 ],
175 ..Default::default()
176 },
177 );
178
179 map.insert(
180 "out-file",
181 CmdletPathConfig {
182 operation_type: FileOperationType::Write,
183 path_params: vec![
184 "-filepath".to_string(),
185 "-path".to_string(),
186 "-literalpath".to_string(),
187 "-pspath".to_string(),
188 "-lp".to_string(),
189 ],
190 known_switches: vec![
191 "-append".to_string(),
192 "-force".to_string(),
193 "-noclobber".to_string(),
194 "-nonewline".to_string(),
195 "-whatif".to_string(),
196 "-confirm".to_string(),
197 ],
198 known_value_params: vec![
199 "-inputobject".to_string(),
200 "-encoding".to_string(),
201 "-width".to_string(),
202 ],
203 ..Default::default()
204 },
205 );
206
207 map.insert(
208 "new-item",
209 CmdletPathConfig {
210 operation_type: FileOperationType::Create,
211 path_params: vec![
212 "-path".to_string(),
213 "-literalpath".to_string(),
214 "-pspath".to_string(),
215 "-lp".to_string(),
216 ],
217 leaf_only_path_params: Some(vec!["-name".to_string()]),
218 known_switches: vec![
219 "-force".to_string(),
220 "-whatif".to_string(),
221 "-confirm".to_string(),
222 ],
223 known_value_params: vec![
224 "-itemtype".to_string(),
225 "-value".to_string(),
226 "-type".to_string(),
227 ],
228 ..Default::default()
229 },
230 );
231
232 map.insert(
233 "copy-item",
234 CmdletPathConfig {
235 operation_type: FileOperationType::Write,
236 path_params: vec![
237 "-path".to_string(),
238 "-literalpath".to_string(),
239 "-pspath".to_string(),
240 "-lp".to_string(),
241 "-destination".to_string(),
242 ],
243 known_switches: vec![
244 "-container".to_string(),
245 "-force".to_string(),
246 "-passthru".to_string(),
247 "-recurse".to_string(),
248 "-whatif".to_string(),
249 "-confirm".to_string(),
250 ],
251 known_value_params: vec![
252 "-filter".to_string(),
253 "-include".to_string(),
254 "-exclude".to_string(),
255 "-fromsession".to_string(),
256 "-tosession".to_string(),
257 ],
258 ..Default::default()
259 },
260 );
261
262 map.insert(
263 "move-item",
264 CmdletPathConfig {
265 operation_type: FileOperationType::Write,
266 path_params: vec![
267 "-path".to_string(),
268 "-literalpath".to_string(),
269 "-pspath".to_string(),
270 "-lp".to_string(),
271 "-destination".to_string(),
272 ],
273 known_switches: vec![
274 "-force".to_string(),
275 "-passthru".to_string(),
276 "-whatif".to_string(),
277 "-confirm".to_string(),
278 ],
279 known_value_params: vec![
280 "-filter".to_string(),
281 "-include".to_string(),
282 "-exclude".to_string(),
283 ],
284 ..Default::default()
285 },
286 );
287
288 map.insert(
289 "rename-item",
290 CmdletPathConfig {
291 operation_type: FileOperationType::Write,
292 path_params: vec![
293 "-path".to_string(),
294 "-literalpath".to_string(),
295 "-pspath".to_string(),
296 "-lp".to_string(),
297 ],
298 known_switches: vec![
299 "-force".to_string(),
300 "-passthru".to_string(),
301 "-whatif".to_string(),
302 "-confirm".to_string(),
303 ],
304 known_value_params: vec![
305 "-newname".to_string(),
306 "-credential".to_string(),
307 "-filter".to_string(),
308 "-include".to_string(),
309 "-exclude".to_string(),
310 ],
311 ..Default::default()
312 },
313 );
314
315 map.insert(
317 "get-content",
318 CmdletPathConfig {
319 operation_type: FileOperationType::Read,
320 path_params: vec![
321 "-path".to_string(),
322 "-literalpath".to_string(),
323 "-pspath".to_string(),
324 "-lp".to_string(),
325 ],
326 known_switches: vec![
327 "-force".to_string(),
328 "-wait".to_string(),
329 "-raw".to_string(),
330 "-asbytestream".to_string(),
331 ],
332 known_value_params: vec![
333 "-readcount".to_string(),
334 "-totalcount".to_string(),
335 "-tail".to_string(),
336 "-first".to_string(),
337 "-head".to_string(),
338 "-last".to_string(),
339 "-filter".to_string(),
340 "-include".to_string(),
341 "-exclude".to_string(),
342 "-delimiter".to_string(),
343 "-encoding".to_string(),
344 "-stream".to_string(),
345 ],
346 ..Default::default()
347 },
348 );
349
350 map.insert(
351 "get-childitem",
352 CmdletPathConfig {
353 operation_type: FileOperationType::Read,
354 path_params: vec![
355 "-path".to_string(),
356 "-literalpath".to_string(),
357 "-pspath".to_string(),
358 "-lp".to_string(),
359 ],
360 known_switches: vec![
361 "-recurse".to_string(),
362 "-force".to_string(),
363 "-name".to_string(),
364 "-directory".to_string(),
365 "-file".to_string(),
366 "-hidden".to_string(),
367 "-readonly".to_string(),
368 "-system".to_string(),
369 ],
370 known_value_params: vec![
371 "-filter".to_string(),
372 "-include".to_string(),
373 "-exclude".to_string(),
374 "-depth".to_string(),
375 "-attributes".to_string(),
376 ],
377 ..Default::default()
378 },
379 );
380
381 map.insert(
382 "get-item",
383 CmdletPathConfig {
384 operation_type: FileOperationType::Read,
385 path_params: vec![
386 "-path".to_string(),
387 "-literalpath".to_string(),
388 "-pspath".to_string(),
389 "-lp".to_string(),
390 ],
391 known_switches: vec!["-force".to_string()],
392 known_value_params: vec![
393 "-filter".to_string(),
394 "-include".to_string(),
395 "-exclude".to_string(),
396 "-stream".to_string(),
397 ],
398 ..Default::default()
399 },
400 );
401
402 map.insert(
403 "get-itemproperty",
404 CmdletPathConfig {
405 operation_type: FileOperationType::Read,
406 path_params: vec![
407 "-path".to_string(),
408 "-literalpath".to_string(),
409 "-pspath".to_string(),
410 "-lp".to_string(),
411 ],
412 known_switches: vec![],
413 known_value_params: vec![
414 "-name".to_string(),
415 "-filter".to_string(),
416 "-include".to_string(),
417 "-exclude".to_string(),
418 ],
419 ..Default::default()
420 },
421 );
422
423 map.insert(
424 "test-path",
425 CmdletPathConfig {
426 operation_type: FileOperationType::Read,
427 path_params: vec![
428 "-path".to_string(),
429 "-literalpath".to_string(),
430 "-pspath".to_string(),
431 "-lp".to_string(),
432 ],
433 known_switches: vec!["-isvalid".to_string()],
434 known_value_params: vec![
435 "-filter".to_string(),
436 "-include".to_string(),
437 "-exclude".to_string(),
438 "-pathtype".to_string(),
439 "-olderthan".to_string(),
440 "-newerthan".to_string(),
441 ],
442 ..Default::default()
443 },
444 );
445
446 map
447});
448
449pub fn get_cmdlet_path_config(cmdlet_name: &str) -> Option<&'static CmdletPathConfig> {
451 if let Some(config) = CMDLET_PATH_CONFIG.get(cmdlet_name) {
453 return Some(config);
454 }
455
456 use super::read_only_validation::resolve_to_canonical;
458 let canonical = resolve_to_canonical(cmdlet_name);
459 CMDLET_PATH_CONFIG.get(canonical.as_str())
460}
461
462pub fn is_dangerous_removal_path(path: &str) -> bool {
464 let lower = path.to_lowercase();
465
466 let dangerous_paths = [
468 "/",
469 "/bin",
470 "/etc",
471 "/usr",
472 "/usr/bin",
473 "/usr/sbin",
474 "/var",
475 "/tmp",
476 "/home",
477 "/root",
478 "c:\\",
479 "c:\\windows",
480 "c:\\program files",
481 "c:\\program files (x86)",
482 ];
483
484 for dp in dangerous_paths.iter() {
485 if lower == *dp || lower.starts_with(&format!("{}/", dp)) || lower.starts_with(dp) {
486 return true;
487 }
488 }
489
490 false
491}
492
493pub fn check_path_constraints(command: &str, _allowed_paths: &[String]) -> PathCheckResult {
495 use super::read_only_validation::resolve_to_canonical;
496
497 let parts: Vec<&str> = command.split_whitespace().collect();
498 if parts.is_empty() {
499 return PathCheckResult {
500 allowed: true,
501 decision_reason: None,
502 };
503 }
504
505 let cmdlet_name = resolve_to_canonical(parts[0]);
507
508 let config = match get_cmdlet_path_config(&cmdlet_name) {
510 Some(c) => c,
511 None => {
512 return PathCheckResult {
514 allowed: false,
515 decision_reason: Some("Cmdlet not in path validation config".to_string()),
516 };
517 }
518 };
519
520 if config.optional_write && config.operation_type == FileOperationType::Write {
522 let has_path = parts.iter().any(|arg| {
524 config
525 .path_params
526 .iter()
527 .any(|p| arg.to_lowercase().starts_with(p))
528 });
529
530 if !has_path {
531 return PathCheckResult {
533 allowed: true,
534 decision_reason: None,
535 };
536 }
537 }
538
539 if config.operation_type == FileOperationType::Write
541 || config.operation_type == FileOperationType::Create
542 {
543 for (i, arg) in parts.iter().enumerate() {
545 if arg.starts_with('-') {
547 continue;
548 }
549
550 let is_path_param = if i > 0 {
552 let prev = parts[i - 1].to_lowercase();
553 config.path_params.iter().any(|p| prev == *p)
554 } else {
555 false
556 };
557
558 if is_path_param || (!arg.starts_with('-') && i > 0) {
559 if is_dangerous_removal_path(arg) {
560 return PathCheckResult {
561 allowed: false,
562 decision_reason: Some(format!("Path '{}' is a dangerous system path", arg)),
563 };
564 }
565 }
566 }
567 }
568
569 PathCheckResult {
570 allowed: true,
571 decision_reason: None,
572 }
573}
574
575pub fn dangerous_removal_deny(path: &str) -> bool {
577 is_dangerous_removal_path(path)
578}
579
580pub fn is_dangerous_removal_raw_path(path: &str) -> bool {
582 is_dangerous_removal_path(path)
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn test_get_cmdlet_path_config() {
591 let config = get_cmdlet_path_config("set-content");
592 assert!(config.is_some());
593 assert_eq!(config.unwrap().operation_type, FileOperationType::Write);
594
595 let config = get_cmdlet_path_config("get-content");
596 assert!(config.is_some());
597 assert_eq!(config.unwrap().operation_type, FileOperationType::Read);
598 }
599
600 #[test]
601 fn test_is_dangerous_removal_path() {
602 assert!(is_dangerous_removal_path("/etc/passwd"));
603 assert!(is_dangerous_removal_path("/bin"));
604 assert!(is_dangerous_removal_path("/home/user/file.txt"));
606 }
607
608 #[test]
609 fn test_check_path_constraints() {
610 let result = check_path_constraints("Get-Content test.txt", &["/home/user".to_string()]);
611 assert!(result.allowed);
612
613 let result = check_path_constraints("Remove-Item /etc/passwd", &["/home/user".to_string()]);
614 assert!(!result.allowed);
615 }
616}