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> = Lazy::new(|| {
15 regex::Regex::new(r"[*?\[\]]").unwrap()
16});
17
18#[derive(Debug, Clone, PartialEq, Default)]
20pub enum FileOperationType {
21 #[default]
22 Read,
23 Write,
24 Create,
25}
26
27#[derive(Debug, Clone)]
29pub struct PathCheckResult {
30 pub allowed: bool,
31 pub decision_reason: Option<String>,
32}
33
34#[derive(Debug, Clone)]
36pub struct ResolvedPathCheckResult {
37 pub allowed: bool,
38 pub decision_reason: Option<String>,
39 pub resolved_path: String,
40}
41
42#[derive(Debug, Clone)]
44pub struct CmdletPathConfig {
45 pub operation_type: FileOperationType,
46 pub path_params: Vec<String>,
47 pub known_switches: Vec<String>,
48 pub known_value_params: Vec<String>,
49 pub leaf_only_path_params: Option<Vec<String>>,
50 pub positional_skip: Option<usize>,
51 pub optional_write: bool,
52}
53
54impl Default for CmdletPathConfig {
55 fn default() -> Self {
56 Self {
57 operation_type: FileOperationType::Read,
58 path_params: Vec::new(),
59 known_switches: Vec::new(),
60 known_value_params: Vec::new(),
61 leaf_only_path_params: None,
62 positional_skip: None,
63 optional_write: false,
64 }
65 }
66}
67
68static CMDLET_PATH_CONFIG: Lazy<HashMap<&'static str, CmdletPathConfig>> = Lazy::new(|| {
70 let mut map = HashMap::new();
71
72 map.insert("set-content", CmdletPathConfig {
74 operation_type: FileOperationType::Write,
75 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
76 known_switches: vec!["-passthru".to_string(), "-force".to_string(), "-whatif".to_string(), "-confirm".to_string(), "-nonewline".to_string()],
77 known_value_params: vec!["-value".to_string(), "-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-encoding".to_string()],
78 ..Default::default()
79 });
80
81 map.insert("add-content", CmdletPathConfig {
82 operation_type: FileOperationType::Write,
83 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
84 known_switches: vec!["-passthru".to_string(), "-force".to_string(), "-whatif".to_string(), "-confirm".to_string(), "-nonewline".to_string()],
85 known_value_params: vec!["-value".to_string(), "-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-encoding".to_string()],
86 ..Default::default()
87 });
88
89 map.insert("remove-item", CmdletPathConfig {
90 operation_type: FileOperationType::Write,
91 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
92 known_switches: vec!["-recurse".to_string(), "-force".to_string(), "-whatif".to_string(), "-confirm".to_string()],
93 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-stream".to_string()],
94 ..Default::default()
95 });
96
97 map.insert("clear-content", CmdletPathConfig {
98 operation_type: FileOperationType::Write,
99 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
100 known_switches: vec!["-force".to_string(), "-whatif".to_string(), "-confirm".to_string()],
101 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-stream".to_string()],
102 ..Default::default()
103 });
104
105 map.insert("out-file", CmdletPathConfig {
106 operation_type: FileOperationType::Write,
107 path_params: vec!["-filepath".to_string(), "-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
108 known_switches: vec!["-append".to_string(), "-force".to_string(), "-noclobber".to_string(), "-nonewline".to_string(), "-whatif".to_string(), "-confirm".to_string()],
109 known_value_params: vec!["-inputobject".to_string(), "-encoding".to_string(), "-width".to_string()],
110 ..Default::default()
111 });
112
113 map.insert("new-item", CmdletPathConfig {
114 operation_type: FileOperationType::Create,
115 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
116 leaf_only_path_params: Some(vec!["-name".to_string()]),
117 known_switches: vec!["-force".to_string(), "-whatif".to_string(), "-confirm".to_string()],
118 known_value_params: vec!["-itemtype".to_string(), "-value".to_string(), "-type".to_string()],
119 ..Default::default()
120 });
121
122 map.insert("copy-item", CmdletPathConfig {
123 operation_type: FileOperationType::Write,
124 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string(), "-destination".to_string()],
125 known_switches: vec!["-container".to_string(), "-force".to_string(), "-passthru".to_string(), "-recurse".to_string(), "-whatif".to_string(), "-confirm".to_string()],
126 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-fromsession".to_string(), "-tosession".to_string()],
127 ..Default::default()
128 });
129
130 map.insert("move-item", CmdletPathConfig {
131 operation_type: FileOperationType::Write,
132 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string(), "-destination".to_string()],
133 known_switches: vec!["-force".to_string(), "-passthru".to_string(), "-whatif".to_string(), "-confirm".to_string()],
134 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string()],
135 ..Default::default()
136 });
137
138 map.insert("rename-item", CmdletPathConfig {
139 operation_type: FileOperationType::Write,
140 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
141 known_switches: vec!["-force".to_string(), "-passthru".to_string(), "-whatif".to_string(), "-confirm".to_string()],
142 known_value_params: vec!["-newname".to_string(), "-credential".to_string(), "-filter".to_string(), "-include".to_string(), "-exclude".to_string()],
143 ..Default::default()
144 });
145
146 map.insert("get-content", CmdletPathConfig {
148 operation_type: FileOperationType::Read,
149 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
150 known_switches: vec!["-force".to_string(), "-wait".to_string(), "-raw".to_string(), "-asbytestream".to_string()],
151 known_value_params: vec!["-readcount".to_string(), "-totalcount".to_string(), "-tail".to_string(), "-first".to_string(), "-head".to_string(), "-last".to_string(), "-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-delimiter".to_string(), "-encoding".to_string(), "-stream".to_string()],
152 ..Default::default()
153 });
154
155 map.insert("get-childitem", CmdletPathConfig {
156 operation_type: FileOperationType::Read,
157 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
158 known_switches: vec!["-recurse".to_string(), "-force".to_string(), "-name".to_string(), "-directory".to_string(), "-file".to_string(), "-hidden".to_string(), "-readonly".to_string(), "-system".to_string()],
159 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-depth".to_string(), "-attributes".to_string()],
160 ..Default::default()
161 });
162
163 map.insert("get-item", CmdletPathConfig {
164 operation_type: FileOperationType::Read,
165 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
166 known_switches: vec!["-force".to_string()],
167 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-stream".to_string()],
168 ..Default::default()
169 });
170
171 map.insert("get-itemproperty", CmdletPathConfig {
172 operation_type: FileOperationType::Read,
173 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
174 known_switches: vec![],
175 known_value_params: vec!["-name".to_string(), "-filter".to_string(), "-include".to_string(), "-exclude".to_string()],
176 ..Default::default()
177 });
178
179 map.insert("test-path", CmdletPathConfig {
180 operation_type: FileOperationType::Read,
181 path_params: vec!["-path".to_string(), "-literalpath".to_string(), "-pspath".to_string(), "-lp".to_string()],
182 known_switches: vec!["-isvalid".to_string()],
183 known_value_params: vec!["-filter".to_string(), "-include".to_string(), "-exclude".to_string(), "-pathtype".to_string(), "-olderthan".to_string(), "-newerthan".to_string()],
184 ..Default::default()
185 });
186
187 map
188});
189
190pub fn get_cmdlet_path_config(cmdlet_name: &str) -> Option<&'static CmdletPathConfig> {
192 if let Some(config) = CMDLET_PATH_CONFIG.get(cmdlet_name) {
194 return Some(config);
195 }
196
197 use super::read_only_validation::resolve_to_canonical;
199 let canonical = resolve_to_canonical(cmdlet_name);
200 CMDLET_PATH_CONFIG.get(canonical.as_str())
201}
202
203pub fn is_dangerous_removal_path(path: &str) -> bool {
205 let lower = path.to_lowercase();
206
207 let dangerous_paths = [
209 "/",
210 "/bin",
211 "/etc",
212 "/usr",
213 "/usr/bin",
214 "/usr/sbin",
215 "/var",
216 "/tmp",
217 "/home",
218 "/root",
219 "c:\\",
220 "c:\\windows",
221 "c:\\program files",
222 "c:\\program files (x86)",
223 ];
224
225 for dp in dangerous_paths.iter() {
226 if lower == *dp || lower.starts_with(&format!("{}/", dp)) || lower.starts_with(dp) {
227 return true;
228 }
229 }
230
231 false
232}
233
234pub fn check_path_constraints(
236 command: &str,
237 _allowed_paths: &[String],
238) -> PathCheckResult {
239 use super::read_only_validation::resolve_to_canonical;
240
241 let parts: Vec<&str> = command.split_whitespace().collect();
242 if parts.is_empty() {
243 return PathCheckResult {
244 allowed: true,
245 decision_reason: None,
246 };
247 }
248
249 let cmdlet_name = resolve_to_canonical(parts[0]);
251
252 let config = match get_cmdlet_path_config(&cmdlet_name) {
254 Some(c) => c,
255 None => {
256 return PathCheckResult {
258 allowed: false,
259 decision_reason: Some("Cmdlet not in path validation config".to_string()),
260 };
261 }
262 };
263
264 if config.optional_write && config.operation_type == FileOperationType::Write {
266 let has_path = parts.iter().any(|arg| {
268 config.path_params.iter().any(|p| arg.to_lowercase().starts_with(p))
269 });
270
271 if !has_path {
272 return PathCheckResult {
274 allowed: true,
275 decision_reason: None,
276 };
277 }
278 }
279
280 if config.operation_type == FileOperationType::Write || config.operation_type == FileOperationType::Create {
282 for (i, arg) in parts.iter().enumerate() {
284 if arg.starts_with('-') {
286 continue;
287 }
288
289 let is_path_param = if i > 0 {
291 let prev = parts[i - 1].to_lowercase();
292 config.path_params.iter().any(|p| prev == *p)
293 } else {
294 false
295 };
296
297 if is_path_param || (!arg.starts_with('-') && i > 0) {
298 if is_dangerous_removal_path(arg) {
299 return PathCheckResult {
300 allowed: false,
301 decision_reason: Some(format!("Path '{}' is a dangerous system path", arg)),
302 };
303 }
304 }
305 }
306 }
307
308 PathCheckResult {
309 allowed: true,
310 decision_reason: None,
311 }
312}
313
314pub fn dangerous_removal_deny(path: &str) -> bool {
316 is_dangerous_removal_path(path)
317}
318
319pub fn is_dangerous_removal_raw_path(path: &str) -> bool {
321 is_dangerous_removal_path(path)
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_get_cmdlet_path_config() {
330 let config = get_cmdlet_path_config("set-content");
331 assert!(config.is_some());
332 assert_eq!(config.unwrap().operation_type, FileOperationType::Write);
333
334 let config = get_cmdlet_path_config("get-content");
335 assert!(config.is_some());
336 assert_eq!(config.unwrap().operation_type, FileOperationType::Read);
337 }
338
339 #[test]
340 fn test_is_dangerous_removal_path() {
341 assert!(is_dangerous_removal_path("/etc/passwd"));
342 assert!(is_dangerous_removal_path("/bin"));
343 assert!(is_dangerous_removal_path("/home/user/file.txt"));
345 }
346
347 #[test]
348 fn test_check_path_constraints() {
349 let result = check_path_constraints("Get-Content test.txt", &["/home/user".to_string()]);
350 assert!(result.allowed);
351
352 let result = check_path_constraints("Remove-Item /etc/passwd", &["/home/user".to_string()]);
353 assert!(!result.allowed);
354 }
355}