// Claude Desktop — Guard-Rails System // Risiko-Klassifikation und Freigabe-Management use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Manager}; /// Risiko-Level einer Aktion #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum RiskLevel { Safe, // Read, Glob, Grep → auto-approve Moderate, // Write, Edit, Git commit → Hinweis in Statusbar Critical, // Prod-Deploy, DB-Schema, Git push → Modal + Bestätigung Blocked, // rm -rf, force push main → hart blockiert } /// Typ der Freigabe #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionType { Session, // Gilt nur für aktuelle Session Permanent, // Dauerhaft gespeichert } /// Eine Freigabe-Regel #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Permission { pub id: String, pub pattern: String, // z.B. "npm install *", "git commit -m *" pub tool: Option, // z.B. "Bash", "Edit", None = alle pub path_pattern: Option, // z.B. "/var/www/dolibarr/*" pub permission_type: PermissionType, pub action: PermissionAction, pub created_at: String, pub use_count: u32, pub last_used: Option, } /// Aktion einer Regel #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum PermissionAction { Allow, Deny, } /// Anfrage zur Freigabe #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionRequest { pub id: String, pub tool: String, pub command: String, pub args: Option, pub path: Option, pub risk_level: RiskLevel, pub suggested_pattern: Option, } /// Antwort auf Freigabe-Anfrage #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionResponse { pub request_id: String, pub allowed: bool, pub save_as: Option, pub pattern: Option, } /// Guard-Rails Manager pub struct GuardRails { permissions: Vec, session_permissions: Vec, blocked_patterns: Vec, } impl Default for GuardRails { fn default() -> Self { Self::new() } } impl GuardRails { pub fn new() -> Self { Self { permissions: vec![], session_permissions: vec![], blocked_patterns: vec![ // Immer blockiert "rm -rf /".to_string(), "rm -rf /*".to_string(), "rm -rf ~".to_string(), "git push --force origin main".to_string(), "git push --force origin master".to_string(), "git push -f origin main".to_string(), "DROP DATABASE".to_string(), "DROP TABLE".to_string(), "TRUNCATE TABLE".to_string(), "> /dev/sda".to_string(), "mkfs.".to_string(), "dd if=/dev/zero".to_string(), ":(){:|:&};:".to_string(), // Fork bomb ], } } /// Klassifiziert das Risiko einer Aktion pub fn classify_risk(&self, tool: &str, command: &str, path: Option<&str>) -> RiskLevel { // Erst prüfen ob blockiert if self.is_blocked(command) { return RiskLevel::Blocked; } // Tool-basierte Klassifikation match tool { // Sichere Tools "Read" | "Glob" | "Grep" | "WebFetch" | "WebSearch" => RiskLevel::Safe, // Moderate Tools "Write" | "Edit" | "NotebookEdit" => { // Pfad-basierte Eskalation if let Some(p) = path { if self.is_production_path(p) { RiskLevel::Critical } else { RiskLevel::Moderate } } else { RiskLevel::Moderate } } // Bash braucht Command-Analyse "Bash" => self.classify_bash_command(command, path), // Task/Agent - abhängig vom Typ "Task" => RiskLevel::Moderate, // Unbekannt = Moderate _ => RiskLevel::Moderate, } } /// Klassifiziert Bash-Befehle fn classify_bash_command(&self, command: &str, path: Option<&str>) -> RiskLevel { let cmd_lower = command.to_lowercase(); // Sichere Befehle if cmd_lower.starts_with("ls ") || cmd_lower.starts_with("pwd") || cmd_lower.starts_with("echo ") || cmd_lower.starts_with("cat ") || cmd_lower.starts_with("head ") || cmd_lower.starts_with("tail ") || cmd_lower.starts_with("wc ") || cmd_lower.starts_with("grep ") || cmd_lower.starts_with("find ") || cmd_lower.starts_with("which ") || cmd_lower.starts_with("whoami") || cmd_lower.starts_with("date") || cmd_lower.starts_with("uname") { return RiskLevel::Safe; } // Kritische Befehle if cmd_lower.contains("--force") || cmd_lower.contains(" -f ") || cmd_lower.starts_with("sudo ") || cmd_lower.starts_with("su ") || cmd_lower.contains("systemctl") || cmd_lower.contains("service ") || cmd_lower.contains("docker ") || cmd_lower.contains("kubectl") || cmd_lower.starts_with("rm ") || cmd_lower.starts_with("mv ") || cmd_lower.contains("chmod ") || cmd_lower.contains("chown ") { // Prod-Pfad macht es noch kritischer if let Some(p) = path { if self.is_production_path(p) { return RiskLevel::Critical; } } // rm ohne -r ist nur Moderate if cmd_lower.starts_with("rm ") && !cmd_lower.contains("-r") { return RiskLevel::Moderate; } return RiskLevel::Critical; } // Moderate Befehle if cmd_lower.starts_with("npm ") || cmd_lower.starts_with("cargo ") || cmd_lower.starts_with("git ") || cmd_lower.starts_with("mkdir ") || cmd_lower.starts_with("touch ") || cmd_lower.starts_with("cp ") { // git push ist kritisch if cmd_lower.contains("git push") { return RiskLevel::Critical; } return RiskLevel::Moderate; } // Default: Moderate RiskLevel::Moderate } /// Prüft ob ein Befehl blockiert ist fn is_blocked(&self, command: &str) -> bool { let cmd_lower = command.to_lowercase(); self.blocked_patterns .iter() .any(|p| cmd_lower.contains(&p.to_lowercase())) } /// Prüft ob ein Pfad in Produktion liegt fn is_production_path(&self, path: &str) -> bool { let prod_patterns = [ "/var/www/prod", "/var/www/production", "/opt/prod", "/home/prod", "/srv/prod", ]; prod_patterns.iter().any(|p| path.starts_with(p)) } /// Prüft ob eine Aktion erlaubt ist pub fn check_permission(&self, tool: &str, command: &str, path: Option<&str>) -> Option<&Permission> { // Erst Session-Permissions prüfen for perm in &self.session_permissions { if self.matches_permission(perm, tool, command, path) { return Some(perm); } } // Dann permanente Permissions for perm in &self.permissions { if self.matches_permission(perm, tool, command, path) { return Some(perm); } } None } /// Prüft ob eine Permission matcht fn matches_permission(&self, perm: &Permission, tool: &str, command: &str, path: Option<&str>) -> bool { // Tool prüfen if let Some(ref perm_tool) = perm.tool { if perm_tool != tool { return false; } } // Pfad prüfen if let (Some(ref perm_path), Some(actual_path)) = (&perm.path_pattern, path) { if !self.matches_pattern(perm_path, actual_path) { return false; } } // Command/Pattern prüfen self.matches_pattern(&perm.pattern, command) } /// Einfacher Pattern-Matcher mit * Wildcard fn matches_pattern(&self, pattern: &str, value: &str) -> bool { if pattern == "*" { return true; } if pattern.contains('*') { let parts: Vec<&str> = pattern.split('*').collect(); if parts.len() == 2 { let (prefix, suffix) = (parts[0], parts[1]); return value.starts_with(prefix) && value.ends_with(suffix); } } pattern == value } /// Fügt eine Permission hinzu pub fn add_permission(&mut self, permission: Permission) { match permission.permission_type { PermissionType::Session => self.session_permissions.push(permission), PermissionType::Permanent => self.permissions.push(permission), } } /// Entfernt eine Permission pub fn remove_permission(&mut self, id: &str) { self.permissions.retain(|p| p.id != id); self.session_permissions.retain(|p| p.id != id); } /// Session-Permissions löschen pub fn clear_session(&mut self) { self.session_permissions.clear(); } /// Alle Permissions abrufen pub fn get_all_permissions(&self) -> Vec<&Permission> { self.permissions.iter().chain(self.session_permissions.iter()).collect() } /// Schlägt ein Pattern vor pub fn suggest_pattern(&self, _tool: &str, command: &str) -> String { // Für npm install: npm install * if command.starts_with("npm install ") { return "npm install *".to_string(); } // Für git commit: git commit -m * if command.starts_with("git commit ") { return "git commit *".to_string(); } // Für cargo: cargo * if command.starts_with("cargo ") { let parts: Vec<&str> = command.split_whitespace().collect(); if parts.len() >= 2 { return format!("cargo {} *", parts[1]); } } // Default: exakter Befehl command.to_string() } } // ============ Tauri Commands ============ use std::sync::{Arc, Mutex}; /// Globaler Guard-Rails State pub type GuardState = Arc>; /// Prüft eine Aktion und gibt Risiko-Level zurück #[tauri::command] pub async fn check_action( app: AppHandle, tool: String, command: String, path: Option, ) -> Result { let state = app.state::(); let guard = state.lock().unwrap(); let risk = guard.classify_risk(&tool, &command, path.as_deref()); // Wenn blockiert, sofort ablehnen if risk == RiskLevel::Blocked { return Ok(serde_json::json!({ "allowed": false, "risk": "blocked", "reason": "Diese Aktion ist aus Sicherheitsgründen blockiert." })); } // Permission prüfen if let Some(perm) = guard.check_permission(&tool, &command, path.as_deref()) { return Ok(serde_json::json!({ "allowed": perm.action == PermissionAction::Allow, "risk": format!("{:?}", risk).to_lowercase(), "matched_rule": perm.id })); } // Kein Match - Frontend muss fragen let suggested = guard.suggest_pattern(&tool, &command); Ok(serde_json::json!({ "allowed": risk == RiskLevel::Safe, "risk": format!("{:?}", risk).to_lowercase(), "needs_confirmation": risk != RiskLevel::Safe, "suggested_pattern": suggested })) } /// Fügt eine Freigabe hinzu #[tauri::command] pub async fn add_permission( app: AppHandle, pattern: String, tool: Option, path_pattern: Option, permission_type: String, action: String, ) -> Result { let state = app.state::(); let mut guard = state.lock().unwrap(); let perm_type = match permission_type.as_str() { "session" => PermissionType::Session, "permanent" => PermissionType::Permanent, _ => return Err("Ungültiger Permission-Typ".to_string()), }; let perm_action = match action.as_str() { "allow" => PermissionAction::Allow, "deny" => PermissionAction::Deny, _ => return Err("Ungültige Aktion".to_string()), }; let id = uuid::Uuid::new_v4().to_string(); let permission = Permission { id: id.clone(), pattern, tool, path_pattern, permission_type: perm_type, action: perm_action, created_at: chrono::Local::now().to_rfc3339(), use_count: 0, last_used: None, }; guard.add_permission(permission.clone()); // Bei Permanent in SQLite speichern if perm_type == PermissionType::Permanent { let db_state = app.state::>>(); let db_lock = db_state.lock().unwrap(); db_lock.save_permission(&permission).map_err(|e| e.to_string())?; } println!("✅ Permission hinzugefügt: {}", id); Ok(id) } /// Entfernt eine Freigabe #[tauri::command] pub async fn remove_permission(app: AppHandle, id: String) -> Result<(), String> { let state = app.state::(); let mut guard = state.lock().unwrap(); guard.remove_permission(&id); // Aus SQLite löschen let db_state = app.state::>>(); let db_lock = db_state.lock().unwrap(); let _ = db_lock.delete_permission(&id); println!("🗑️ Permission entfernt: {}", id); Ok(()) } /// Holt alle Freigaben #[tauri::command] pub async fn get_permissions(app: AppHandle) -> Result, String> { let state = app.state::(); let guard = state.lock().unwrap(); Ok(guard.get_all_permissions().into_iter().cloned().collect()) } /// Holt blockierte Patterns #[tauri::command] pub async fn get_blocked_patterns(app: AppHandle) -> Result, String> { let state = app.state::(); let guard = state.lock().unwrap(); Ok(guard.blocked_patterns.clone()) }