use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SecurityLevel {
Trusted,
#[default]
Standard,
Strict,
Paranoid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionRule {
pub pattern: String,
pub allow: bool,
pub description: Option<String>,
}
impl PermissionRule {
pub fn allow(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
allow: true,
description: None,
}
}
pub fn deny(pattern: impl Into<String>) -> Self {
Self {
pattern: pattern.into(),
allow: false,
description: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn matches(&self, value: &str) -> bool {
let pattern_parts: Vec<&str> = self.pattern.split('*').collect();
if pattern_parts.len() == 1 {
return value == self.pattern;
}
if !value.starts_with(pattern_parts[0]) {
return false;
}
if !value.ends_with(pattern_parts.last().unwrap()) {
return false;
}
let mut search_start = pattern_parts[0].len();
for part in pattern_parts.iter().skip(1).take(pattern_parts.len() - 2) {
if let Some(pos) = value[search_start..].find(part) {
search_start += pos + part.len();
} else {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionPolicy {
pub level: SecurityLevel,
pub allow_rules: Vec<PermissionRule>,
pub deny_rules: Vec<PermissionRule>,
pub trusted_categories: HashSet<String>,
pub blocked_categories: HashSet<String>,
pub trusted_paths: HashSet<String>,
pub blocked_paths: HashSet<String>,
pub trusted_urls: HashSet<String>,
pub blocked_urls: HashSet<String>,
pub trusted_commands: HashSet<String>,
pub blocked_commands: HashSet<String>,
pub enable_cache: bool,
pub cache_expire_seconds: u64,
pub audit_enabled: bool,
pub max_audit_entries: usize,
}
impl Default for PermissionPolicy {
fn default() -> Self {
Self {
level: SecurityLevel::Standard,
allow_rules: Vec::new(),
deny_rules: Vec::new(),
trusted_categories: HashSet::new(),
blocked_categories: HashSet::new(),
trusted_paths: HashSet::new(),
blocked_paths: Self::default_blocked_paths(),
trusted_urls: HashSet::new(),
blocked_urls: HashSet::new(),
trusted_commands: HashSet::new(),
blocked_commands: Self::default_blocked_commands(),
enable_cache: true,
cache_expire_seconds: 3600, audit_enabled: true,
max_audit_entries: 10000,
}
}
}
impl PermissionPolicy {
pub fn trusted() -> Self {
Self {
level: SecurityLevel::Trusted,
..Self::default()
}
}
pub fn strict() -> Self {
Self {
level: SecurityLevel::Strict,
..Self::default()
}
}
pub fn paranoid() -> Self {
Self {
level: SecurityLevel::Paranoid,
audit_enabled: true,
..Self::strict()
}
}
fn default_blocked_paths() -> HashSet<String> {
let mut paths = HashSet::new();
paths.insert(".env".to_string());
paths.insert(".env.local".to_string());
paths.insert(".env.*.local".to_string());
paths.insert("**/credentials.json".to_string());
paths.insert("**/secrets.json".to_string());
paths.insert("**/api_keys.json".to_string());
paths.insert("~/.ssh/id_rsa".to_string());
paths.insert("~/.ssh/id_ed25519".to_string());
paths.insert("/etc/shadow".to_string());
paths.insert("/etc/passwd".to_string());
paths
}
fn default_blocked_commands() -> HashSet<String> {
let mut commands = HashSet::new();
commands.insert("rm -rf /".to_string());
commands.insert("rm -rf ~".to_string());
commands.insert("mkfs".to_string());
commands.insert("dd if=/dev/zero".to_string());
commands.insert(":(){ :|:& };:".to_string()); commands.insert("chmod 777".to_string());
commands
}
pub fn add_trusted_path(mut self, path: impl Into<String>) -> Self {
self.trusted_paths.insert(path.into());
self
}
pub fn add_blocked_path(mut self, path: impl Into<String>) -> Self {
self.blocked_paths.insert(path.into());
self
}
pub fn add_trusted_url(mut self, url: impl Into<String>) -> Self {
self.trusted_urls.insert(url.into());
self
}
pub fn add_blocked_url(mut self, url: impl Into<String>) -> Self {
self.blocked_urls.insert(url.into());
self
}
pub fn add_trusted_command(mut self, command: impl Into<String>) -> Self {
self.trusted_commands.insert(command.into());
self
}
pub fn add_blocked_command(mut self, command: impl Into<String>) -> Self {
self.blocked_commands.insert(command.into());
self
}
pub fn is_path_trusted(&self, path: &str) -> bool {
self.trusted_paths
.iter()
.any(|p| path.starts_with(p) || PermissionRule::allow(p.clone()).matches(path))
}
pub fn is_path_blocked(&self, path: &str) -> bool {
self.blocked_paths
.iter()
.any(|p| path.starts_with(p) || PermissionRule::deny(p.clone()).matches(path))
}
pub fn should_auto_approve(&self, action_category: &str) -> bool {
match self.level {
SecurityLevel::Trusted => true,
SecurityLevel::Standard => self.trusted_categories.contains(action_category),
SecurityLevel::Strict | SecurityLevel::Paranoid => false,
}
}
pub fn is_category_blocked(&self, category: &str) -> bool {
self.blocked_categories.contains(category)
}
pub fn load_from_file(path: &std::path::Path) -> anyhow::Result<Self> {
let content = std::fs::read_to_string(path)?;
let policy: Self = toml::from_str(&content)?;
Ok(policy)
}
pub fn save_to_file(&self, path: &std::path::Path) -> anyhow::Result<()> {
let content = toml::to_string_pretty(self)?;
std::fs::write(path, content)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_rule_matching() {
let rule = PermissionRule::allow("/home/user/*.txt");
assert!(rule.matches("/home/user/test.txt"));
assert!(rule.matches("/home/user/another.txt"));
assert!(!rule.matches("/home/other/test.txt"));
}
#[test]
fn test_default_blocked_paths() {
let policy = PermissionPolicy::default();
assert!(policy.is_path_blocked(".env"));
assert!(policy.is_path_blocked("/etc/shadow"));
}
#[test]
fn test_security_level_auto_approve() {
let trusted_policy = PermissionPolicy::trusted();
assert!(trusted_policy.should_auto_approve("file_read"));
let strict_policy = PermissionPolicy::strict();
assert!(!strict_policy.should_auto_approve("file_read"));
}
}