- 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>
470 lines
14 KiB
Rust
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())
|
|
}
|