claude-desktop/src-tauri/src/guard.rs
Eddy f5ca5bca7c Phase 3: SQLite-Persistierung, Guard-Rails Integration + Claude Bridge
- db.rs: Vollständige SQLite-Schicht (Permissions, Audit, Memory, Patterns, Settings)
- guard.rs: Risiko-Klassifikation + Freigabe-Management in lib.rs integriert
- scripts/claude-bridge.js: Node.js Bridge für Claude CLI (stream-json, NDJSON)
- audit.rs + memory.rs: An SQLite angebunden statt In-Memory
- Frontend: MemoryPanel + AuditLog laden echte Daten via Tauri-Commands
- shell.nix: Rust-Toolchain aus nixpkgs statt rustup
- Build: cargo check + npm run build erfolgreich

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:28:35 +02:00

470 lines
14 KiB
Rust

// 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<String>, // z.B. "Bash", "Edit", None = alle
pub path_pattern: Option<String>, // z.B. "/var/www/dolibarr/*"
pub permission_type: PermissionType,
pub action: PermissionAction,
pub created_at: String,
pub use_count: u32,
pub last_used: Option<String>,
}
/// 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<serde_json::Value>,
pub path: Option<String>,
pub risk_level: RiskLevel,
pub suggested_pattern: Option<String>,
}
/// Antwort auf Freigabe-Anfrage
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionResponse {
pub request_id: String,
pub allowed: bool,
pub save_as: Option<PermissionType>,
pub pattern: Option<String>,
}
/// Guard-Rails Manager
pub struct GuardRails {
permissions: Vec<Permission>,
session_permissions: Vec<Permission>,
blocked_patterns: Vec<String>,
}
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<Mutex<GuardRails>>;
/// 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<String>,
) -> Result<serde_json::Value, String> {
let state = app.state::<GuardState>();
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<String>,
path_pattern: Option<String>,
permission_type: String,
action: String,
) -> Result<String, String> {
let state = app.state::<GuardState>();
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::<Arc<Mutex<crate::db::Database>>>();
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::<GuardState>();
let mut guard = state.lock().unwrap();
guard.remove_permission(&id);
// Aus SQLite löschen
let db_state = app.state::<Arc<Mutex<crate::db::Database>>>();
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<Vec<Permission>, String> {
let state = app.state::<GuardState>();
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<Vec<String>, String> {
let state = app.state::<GuardState>();
let guard = state.lock().unwrap();
Ok(guard.blocked_patterns.clone())
}