371 lines
14 KiB
Rust
371 lines
14 KiB
Rust
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<i64>,
|
|
pub name: String,
|
|
pub description: Option<String>,
|
|
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<i64>,
|
|
pub profile_id: i64,
|
|
pub operation_type: String,
|
|
pub pattern_type: String,
|
|
pub pattern_value: String,
|
|
pub enabled: bool,
|
|
pub platform_support: Option<String>,
|
|
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<Self> {
|
|
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<SandboxRule>, sandbox_enabled: bool, enable_file_read: bool, enable_file_write: bool, enable_network: bool) -> Result<ProfileBuildResult> {
|
|
// 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<SandboxRule>) -> Result<Profile> {
|
|
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<SandboxRule>) -> Result<ProfileBuildResult> {
|
|
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<Option<Operation>> {
|
|
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<Option<(Operation, SerializedOperation)>> {
|
|
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<PathPattern> {
|
|
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<AddressPattern> {
|
|
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::<u16>()
|
|
.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::<Vec<String>>(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<SandboxProfile> {
|
|
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<SandboxProfile> {
|
|
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<Vec<SandboxRule>> {
|
|
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::<Result<Vec<_>, _>>()?;
|
|
|
|
Ok(rules)
|
|
}
|
|
|
|
/// Get or create the gaol Profile for execution
|
|
pub fn get_gaol_profile(conn: &Connection, profile_id: Option<i64>, project_path: PathBuf) -> Result<Profile> {
|
|
// 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)
|
|
}
|