use once_cell::sync::Lazy;
use std::collections::HashMap;
const MAX_DIRS_TO_LIST: usize = 5;
static GLOB_PATTERN_REGEX: Lazy<regex::Regex> =
Lazy::new(|| regex::Regex::new(r"[*?\[\]]").unwrap());
#[derive(Debug, Clone, PartialEq, Default)]
pub enum FileOperationType {
#[default]
Read,
Write,
Create,
}
#[derive(Debug, Clone)]
pub struct PathCheckResult {
pub allowed: bool,
pub decision_reason: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ResolvedPathCheckResult {
pub allowed: bool,
pub decision_reason: Option<String>,
pub resolved_path: String,
}
#[derive(Debug, Clone)]
pub struct CmdletPathConfig {
pub operation_type: FileOperationType,
pub path_params: Vec<String>,
pub known_switches: Vec<String>,
pub known_value_params: Vec<String>,
pub leaf_only_path_params: Option<Vec<String>>,
pub positional_skip: Option<usize>,
pub optional_write: bool,
}
impl Default for CmdletPathConfig {
fn default() -> Self {
Self {
operation_type: FileOperationType::Read,
path_params: Vec::new(),
known_switches: Vec::new(),
known_value_params: Vec::new(),
leaf_only_path_params: None,
positional_skip: None,
optional_write: false,
}
}
}
static CMDLET_PATH_CONFIG: Lazy<HashMap<&'static str, CmdletPathConfig>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert(
"set-content",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-passthru".to_string(),
"-force".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
"-nonewline".to_string(),
],
known_value_params: vec![
"-value".to_string(),
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-encoding".to_string(),
],
..Default::default()
},
);
map.insert(
"add-content",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-passthru".to_string(),
"-force".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
"-nonewline".to_string(),
],
known_value_params: vec![
"-value".to_string(),
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-encoding".to_string(),
],
..Default::default()
},
);
map.insert(
"remove-item",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-recurse".to_string(),
"-force".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-stream".to_string(),
],
..Default::default()
},
);
map.insert(
"clear-content",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-force".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-stream".to_string(),
],
..Default::default()
},
);
map.insert(
"out-file",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-filepath".to_string(),
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-append".to_string(),
"-force".to_string(),
"-noclobber".to_string(),
"-nonewline".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-inputobject".to_string(),
"-encoding".to_string(),
"-width".to_string(),
],
..Default::default()
},
);
map.insert(
"new-item",
CmdletPathConfig {
operation_type: FileOperationType::Create,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
leaf_only_path_params: Some(vec!["-name".to_string()]),
known_switches: vec![
"-force".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-itemtype".to_string(),
"-value".to_string(),
"-type".to_string(),
],
..Default::default()
},
);
map.insert(
"copy-item",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
"-destination".to_string(),
],
known_switches: vec![
"-container".to_string(),
"-force".to_string(),
"-passthru".to_string(),
"-recurse".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-fromsession".to_string(),
"-tosession".to_string(),
],
..Default::default()
},
);
map.insert(
"move-item",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
"-destination".to_string(),
],
known_switches: vec![
"-force".to_string(),
"-passthru".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
],
..Default::default()
},
);
map.insert(
"rename-item",
CmdletPathConfig {
operation_type: FileOperationType::Write,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-force".to_string(),
"-passthru".to_string(),
"-whatif".to_string(),
"-confirm".to_string(),
],
known_value_params: vec![
"-newname".to_string(),
"-credential".to_string(),
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
],
..Default::default()
},
);
map.insert(
"get-content",
CmdletPathConfig {
operation_type: FileOperationType::Read,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![
"-force".to_string(),
"-wait".to_string(),
"-raw".to_string(),
"-asbytestream".to_string(),
],
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(),
],
..Default::default()
},
);
map.insert(
"get-childitem",
CmdletPathConfig {
operation_type: FileOperationType::Read,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
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(),
],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-depth".to_string(),
"-attributes".to_string(),
],
..Default::default()
},
);
map.insert(
"get-item",
CmdletPathConfig {
operation_type: FileOperationType::Read,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec!["-force".to_string()],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-stream".to_string(),
],
..Default::default()
},
);
map.insert(
"get-itemproperty",
CmdletPathConfig {
operation_type: FileOperationType::Read,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec![],
known_value_params: vec![
"-name".to_string(),
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
],
..Default::default()
},
);
map.insert(
"test-path",
CmdletPathConfig {
operation_type: FileOperationType::Read,
path_params: vec![
"-path".to_string(),
"-literalpath".to_string(),
"-pspath".to_string(),
"-lp".to_string(),
],
known_switches: vec!["-isvalid".to_string()],
known_value_params: vec![
"-filter".to_string(),
"-include".to_string(),
"-exclude".to_string(),
"-pathtype".to_string(),
"-olderthan".to_string(),
"-newerthan".to_string(),
],
..Default::default()
},
);
map
});
pub fn get_cmdlet_path_config(cmdlet_name: &str) -> Option<&'static CmdletPathConfig> {
if let Some(config) = CMDLET_PATH_CONFIG.get(cmdlet_name) {
return Some(config);
}
use super::read_only_validation::resolve_to_canonical;
let canonical = resolve_to_canonical(cmdlet_name);
CMDLET_PATH_CONFIG.get(canonical.as_str())
}
pub fn is_dangerous_removal_path(path: &str) -> bool {
let lower = path.to_lowercase();
let dangerous_paths = [
"/",
"/bin",
"/etc",
"/usr",
"/usr/bin",
"/usr/sbin",
"/var",
"/tmp",
"/home",
"/root",
"c:\\",
"c:\\windows",
"c:\\program files",
"c:\\program files (x86)",
];
for dp in dangerous_paths.iter() {
if lower == *dp || lower.starts_with(&format!("{}/", dp)) || lower.starts_with(dp) {
return true;
}
}
false
}
pub fn check_path_constraints(command: &str, _allowed_paths: &[String]) -> PathCheckResult {
use super::read_only_validation::resolve_to_canonical;
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.is_empty() {
return PathCheckResult {
allowed: true,
decision_reason: None,
};
}
let cmdlet_name = resolve_to_canonical(parts[0]);
let config = match get_cmdlet_path_config(&cmdlet_name) {
Some(c) => c,
None => {
return PathCheckResult {
allowed: false,
decision_reason: Some("Cmdlet not in path validation config".to_string()),
};
}
};
if config.optional_write && config.operation_type == FileOperationType::Write {
let has_path = parts.iter().any(|arg| {
config
.path_params
.iter()
.any(|p| arg.to_lowercase().starts_with(p))
});
if !has_path {
return PathCheckResult {
allowed: true,
decision_reason: None,
};
}
}
if config.operation_type == FileOperationType::Write
|| config.operation_type == FileOperationType::Create
{
for (i, arg) in parts.iter().enumerate() {
if arg.starts_with('-') {
continue;
}
let is_path_param = if i > 0 {
let prev = parts[i - 1].to_lowercase();
config.path_params.iter().any(|p| prev == *p)
} else {
false
};
if is_path_param || (!arg.starts_with('-') && i > 0) {
if is_dangerous_removal_path(arg) {
return PathCheckResult {
allowed: false,
decision_reason: Some(format!("Path '{}' is a dangerous system path", arg)),
};
}
}
}
}
PathCheckResult {
allowed: true,
decision_reason: None,
}
}
pub fn dangerous_removal_deny(path: &str) -> bool {
is_dangerous_removal_path(path)
}
pub fn is_dangerous_removal_raw_path(path: &str) -> bool {
is_dangerous_removal_path(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_cmdlet_path_config() {
let config = get_cmdlet_path_config("set-content");
assert!(config.is_some());
assert_eq!(config.unwrap().operation_type, FileOperationType::Write);
let config = get_cmdlet_path_config("get-content");
assert!(config.is_some());
assert_eq!(config.unwrap().operation_type, FileOperationType::Read);
}
#[test]
fn test_is_dangerous_removal_path() {
assert!(is_dangerous_removal_path("/etc/passwd"));
assert!(is_dangerous_removal_path("/bin"));
assert!(is_dangerous_removal_path("/home/user/file.txt"));
}
#[test]
fn test_check_path_constraints() {
let result = check_path_constraints("Get-Content test.txt", &["/home/user".to_string()]);
assert!(result.allowed);
let result = check_path_constraints("Remove-Item /etc/passwd", &["/home/user".to_string()]);
assert!(!result.allowed);
}
}