use anyhow::{Context, Result}; use gaol::profile::{AddressPattern, Operation, OperationSupport, PathPattern, Profile}; use log::{debug, info, warn}; use rusqlite::{params, Connection}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use crate::sandbox::executor::{SerializedOperation, SerializedProfile}; /// Represents a sandbox profile from the database #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxProfile { pub id: Option, pub name: String, pub description: Option, pub is_active: bool, pub is_default: bool, pub created_at: String, pub updated_at: String, } /// Represents a sandbox rule from the database #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SandboxRule { pub id: Option, pub profile_id: i64, pub operation_type: String, pub pattern_type: String, pub pattern_value: String, pub enabled: bool, pub platform_support: Option, pub created_at: String, } /// Result of building a profile pub struct ProfileBuildResult { pub profile: Profile, pub serialized: SerializedProfile, } /// Builder for creating gaol profiles from database configuration pub struct ProfileBuilder { project_path: PathBuf, home_dir: PathBuf, } impl ProfileBuilder { /// Create a new profile builder pub fn new(project_path: PathBuf) -> Result { let home_dir = dirs::home_dir() .context("Could not determine home directory")?; Ok(Self { project_path, home_dir, }) } /// Build a gaol Profile from database rules filtered by agent permissions pub fn build_agent_profile(&self, rules: Vec, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result { // If sandbox is completely disabled, return an empty profile if !sandbox_enabled { return Ok(ProfileBuildResult { profile: Profile::new(vec![]).map_err(|_| anyhow::anyhow!("Failed to create empty profile"))?, serialized: SerializedProfile { operations: vec![] }, }); } let mut filtered_rules = Vec::new(); for rule in rules { if !rule.enabled { continue; } // Filter rules based on agent permissions let include_rule = match rule.operation_type.as_str() { "file_read_all" | "file_read_metadata" => enable_file_read, "network_outbound" => enable_network, "system_info_read" => true, // Always allow system info reading _ => true // Include unknown rule types by default }; if include_rule { filtered_rules.push(rule); } } // Always ensure project path access if file reading is enabled if enable_file_read { let has_project_access = filtered_rules.iter().any(|rule| { rule.operation_type == "file_read_all" && rule.pattern_type == "subpath" && rule.pattern_value.contains("{{PROJECT_PATH}}") }); if !has_project_access { // Add a default project access rule filtered_rules.push(SandboxRule { id: None, profile_id: 0, operation_type: "file_read_all".to_string(), pattern_type: "subpath".to_string(), pattern_value: "{{PROJECT_PATH}}".to_string(), enabled: true, platform_support: None, created_at: String::new(), }); } } self.build_profile_with_serialization(filtered_rules) } /// Build a gaol Profile from database rules pub fn build_profile(&self, rules: Vec) -> Result { let result = self.build_profile_with_serialization(rules)?; Ok(result.profile) } /// Build a gaol Profile from database rules and return serialized operations pub fn build_profile_with_serialization(&self, rules: Vec) -> Result { let mut operations = Vec::new(); let mut serialized_operations = Vec::new(); for rule in rules { if !rule.enabled { continue; } // Check platform support if !self.is_rule_supported_on_platform(&rule) { debug!("Skipping rule {} - not supported on current platform", rule.operation_type); continue; } match self.build_operation_with_serialization(&rule) { Ok(Some((op, serialized))) => { // Check if operation is supported on current platform if matches!(op.support(), gaol::profile::OperationSupportLevel::CanBeAllowed) { operations.push(op); serialized_operations.push(serialized); } else { warn!("Operation {:?} not supported at desired level on current platform", rule.operation_type); } }, Ok(None) => { debug!("Skipping unsupported operation type: {}", rule.operation_type); }, Err(e) => { warn!("Failed to build operation for rule {}: {}", rule.id.unwrap_or(0), e); } } } // Ensure project path access is included let has_project_access = serialized_operations.iter().any(|op| { matches!(op, SerializedOperation::FileReadAll { path, is_subpath: true } if path == &self.project_path) }); if !has_project_access { operations.push(Operation::FileReadAll(PathPattern::Subpath(self.project_path.clone()))); serialized_operations.push(SerializedOperation::FileReadAll { path: self.project_path.clone(), is_subpath: true, }); } // Create the profile let profile = Profile::new(operations) .map_err(|_| anyhow::anyhow!("Failed to create sandbox profile - some operations may not be supported on this platform"))?; Ok(ProfileBuildResult { profile, serialized: SerializedProfile { operations: serialized_operations, }, }) } /// Build a gaol Operation from a database rule fn build_operation(&self, rule: &SandboxRule) -> Result> { match self.build_operation_with_serialization(rule) { Ok(Some((op, _))) => Ok(Some(op)), Ok(None) => Ok(None), Err(e) => Err(e), } } /// Build a gaol Operation and its serialized form from a database rule fn build_operation_with_serialization(&self, rule: &SandboxRule) -> Result> { match rule.operation_type.as_str() { "file_read_all" => { let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?; Ok(Some(( Operation::FileReadAll(pattern), SerializedOperation::FileReadAll { path, is_subpath } ))) }, "file_read_metadata" => { let (pattern, path, is_subpath) = self.build_path_pattern_with_info(&rule.pattern_type, &rule.pattern_value)?; Ok(Some(( Operation::FileReadMetadata(pattern), SerializedOperation::FileReadMetadata { path, is_subpath } ))) }, "network_outbound" => { let (pattern, serialized) = self.build_address_pattern_with_serialization(&rule.pattern_type, &rule.pattern_value)?; Ok(Some((Operation::NetworkOutbound(pattern), serialized))) }, "system_info_read" => { Ok(Some(( Operation::SystemInfoRead, SerializedOperation::SystemInfoRead ))) }, _ => Ok(None) } } /// Build a PathPattern from pattern type and value fn build_path_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result { let (pattern, _, _) = self.build_path_pattern_with_info(pattern_type, pattern_value)?; Ok(pattern) } /// Build a PathPattern and return additional info for serialization fn build_path_pattern_with_info(&self, pattern_type: &str, pattern_value: &str) -> Result<(PathPattern, PathBuf, bool)> { // Replace template variables let expanded_value = pattern_value .replace("{{PROJECT_PATH}}", &self.project_path.to_string_lossy()) .replace("{{HOME}}", &self.home_dir.to_string_lossy()); let path = PathBuf::from(expanded_value); match pattern_type { "literal" => Ok((PathPattern::Literal(path.clone()), path, false)), "subpath" => Ok((PathPattern::Subpath(path.clone()), path, true)), _ => Err(anyhow::anyhow!("Unknown path pattern type: {}", pattern_type)) } } /// Build an AddressPattern from pattern type and value fn build_address_pattern(&self, pattern_type: &str, pattern_value: &str) -> Result { let (pattern, _) = self.build_address_pattern_with_serialization(pattern_type, pattern_value)?; Ok(pattern) } /// Build an AddressPattern and its serialized form fn build_address_pattern_with_serialization(&self, pattern_type: &str, pattern_value: &str) -> Result<(AddressPattern, SerializedOperation)> { match pattern_type { "all" => Ok(( AddressPattern::All, SerializedOperation::NetworkOutbound { pattern: "all".to_string() } )), "tcp" => { let port = pattern_value.parse::() .context("Invalid TCP port number")?; Ok(( AddressPattern::Tcp(port), SerializedOperation::NetworkTcp { port } )) }, "local_socket" => { let path = PathBuf::from(pattern_value); Ok(( AddressPattern::LocalSocket(path.clone()), SerializedOperation::NetworkLocalSocket { path } )) }, _ => Err(anyhow::anyhow!("Unknown address pattern type: {}", pattern_type)) } } /// Check if a rule is supported on the current platform fn is_rule_supported_on_platform(&self, rule: &SandboxRule) -> bool { if let Some(platforms_json) = &rule.platform_support { if let Ok(platforms) = serde_json::from_str::>(platforms_json) { let current_os = std::env::consts::OS; return platforms.contains(¤t_os.to_string()); } } // If no platform support specified, assume it's supported true } } /// Load a sandbox profile by ID pub fn load_profile(conn: &Connection, profile_id: i64) -> Result { conn.query_row( "SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE id = ?1", params![profile_id], |row| { Ok(SandboxProfile { id: Some(row.get(0)?), name: row.get(1)?, description: row.get(2)?, is_active: row.get(3)?, is_default: row.get(4)?, created_at: row.get(5)?, updated_at: row.get(6)?, }) } ) .context("Failed to load sandbox profile") } /// Load the default sandbox profile pub fn load_default_profile(conn: &Connection) -> Result { conn.query_row( "SELECT id, name, description, is_active, is_default, created_at, updated_at FROM sandbox_profiles WHERE is_default = 1", [], |row| { Ok(SandboxProfile { id: Some(row.get(0)?), name: row.get(1)?, description: row.get(2)?, is_active: row.get(3)?, is_default: row.get(4)?, created_at: row.get(5)?, updated_at: row.get(6)?, }) } ) .context("Failed to load default sandbox profile") } /// Load rules for a sandbox profile pub fn load_profile_rules(conn: &Connection, profile_id: i64) -> Result> { let mut stmt = conn.prepare( "SELECT id, profile_id, operation_type, pattern_type, pattern_value, enabled, platform_support, created_at FROM sandbox_rules WHERE profile_id = ?1 AND enabled = 1" )?; let rules = stmt.query_map(params![profile_id], |row| { Ok(SandboxRule { id: Some(row.get(0)?), profile_id: row.get(1)?, operation_type: row.get(2)?, pattern_type: row.get(3)?, pattern_value: row.get(4)?, enabled: row.get(5)?, platform_support: row.get(6)?, created_at: row.get(7)?, }) })? .collect::, _>>()?; Ok(rules) } /// Get or create the gaol Profile for execution pub fn get_gaol_profile(conn: &Connection, profile_id: Option, project_path: PathBuf) -> Result { // Load the profile let profile = if let Some(id) = profile_id { load_profile(conn, id)? } else { load_default_profile(conn)? }; info!("Using sandbox profile: {}", profile.name); // Load the rules let rules = load_profile_rules(conn, profile.id.unwrap())?; info!("Loaded {} sandbox rules", rules.len()); // Build the gaol profile let builder = ProfileBuilder::new(project_path)?; builder.build_profile(rules) }