style: apply cargo fmt across entire Rust codebase
- Remove Rust formatting check from CI workflow since formatting is now applied - Standardize import ordering and organization throughout codebase - Fix indentation, spacing, and line breaks for consistency - Clean up trailing whitespace and formatting inconsistencies - Apply rustfmt to all Rust source files including checkpoint, sandbox, commands, and test modules This establishes a consistent code style baseline for the project.
This commit is contained in:
@@ -14,36 +14,37 @@ pub fn execute_claude_task(
|
||||
timeout_secs: u64,
|
||||
) -> Result<ClaudeOutput> {
|
||||
let mut cmd = Command::new("claude");
|
||||
|
||||
|
||||
// Add task
|
||||
cmd.arg("-p").arg(task);
|
||||
|
||||
|
||||
// Add system prompt if provided
|
||||
if let Some(prompt) = system_prompt {
|
||||
cmd.arg("--system-prompt").arg(prompt);
|
||||
}
|
||||
|
||||
|
||||
// Add model if provided
|
||||
if let Some(m) = model {
|
||||
cmd.arg("--model").arg(m);
|
||||
}
|
||||
|
||||
|
||||
// Always add these flags for testing
|
||||
cmd.arg("--output-format").arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
cmd.arg("--output-format")
|
||||
.arg("stream-json")
|
||||
.arg("--verbose")
|
||||
.arg("--dangerously-skip-permissions")
|
||||
.current_dir(project_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
|
||||
// Add sandbox profile ID if provided
|
||||
if let Some(profile_id) = sandbox_profile_id {
|
||||
cmd.env("CLAUDIA_SANDBOX_PROFILE_ID", profile_id.to_string());
|
||||
}
|
||||
|
||||
|
||||
// Execute with timeout (use gtimeout on macOS, timeout on Linux)
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
|
||||
let timeout_cmd = if cfg!(target_os = "macos") {
|
||||
// On macOS, try gtimeout (from GNU coreutils) first, fallback to direct execution
|
||||
if std::process::Command::new("which")
|
||||
@@ -60,15 +61,15 @@ pub fn execute_claude_task(
|
||||
} else {
|
||||
"timeout"
|
||||
};
|
||||
|
||||
|
||||
let output = if timeout_cmd.is_empty() {
|
||||
// Run without timeout wrapper
|
||||
cmd.output()
|
||||
.context("Failed to execute Claude command")?
|
||||
cmd.output().context("Failed to execute Claude command")?
|
||||
} else {
|
||||
// Run with timeout wrapper
|
||||
let mut timeout_cmd = Command::new(timeout_cmd);
|
||||
timeout_cmd.arg(timeout_secs.to_string())
|
||||
timeout_cmd
|
||||
.arg(timeout_secs.to_string())
|
||||
.arg("claude")
|
||||
.args(cmd.get_args())
|
||||
.current_dir(project_path)
|
||||
@@ -78,9 +79,9 @@ pub fn execute_claude_task(
|
||||
.output()
|
||||
.context("Failed to execute Claude command with timeout")?
|
||||
};
|
||||
|
||||
|
||||
let duration = start.elapsed();
|
||||
|
||||
|
||||
Ok(ClaudeOutput {
|
||||
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
@@ -103,7 +104,7 @@ impl ClaudeOutput {
|
||||
pub fn contains_operation(&self, operation: &str) -> bool {
|
||||
self.stdout.contains(operation) || self.stderr.contains(operation)
|
||||
}
|
||||
|
||||
|
||||
/// Check if operation was blocked (look for permission denied, sandbox violation, etc)
|
||||
pub fn operation_was_blocked(&self, operation: &str) -> bool {
|
||||
let blocked_patterns = [
|
||||
@@ -114,16 +115,16 @@ impl ClaudeOutput {
|
||||
"access denied",
|
||||
"sandbox violation",
|
||||
];
|
||||
|
||||
|
||||
let output = format!("{}\n{}", self.stdout, self.stderr).to_lowercase();
|
||||
let op_lower = operation.to_lowercase();
|
||||
|
||||
|
||||
// Check if operation was mentioned along with a block pattern
|
||||
blocked_patterns.iter().any(|pattern| {
|
||||
output.contains(&op_lower) && output.contains(pattern)
|
||||
})
|
||||
blocked_patterns
|
||||
.iter()
|
||||
.any(|pattern| output.contains(&op_lower) && output.contains(pattern))
|
||||
}
|
||||
|
||||
|
||||
/// Check if file read was successful
|
||||
pub fn file_read_succeeded(&self, filename: &str) -> bool {
|
||||
// Look for patterns indicating successful file read
|
||||
@@ -133,10 +134,12 @@ impl ClaudeOutput {
|
||||
&format!("Contents of {}", filename),
|
||||
"test content", // Our test files contain this
|
||||
];
|
||||
|
||||
patterns.iter().any(|pattern| self.contains_operation(pattern))
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
|
||||
|
||||
/// Check if network connection was attempted
|
||||
pub fn network_attempted(&self, host: &str) -> bool {
|
||||
let patterns = [
|
||||
@@ -145,8 +148,10 @@ impl ClaudeOutput {
|
||||
&format!("connect to {}", host),
|
||||
host,
|
||||
];
|
||||
|
||||
patterns.iter().any(|pattern| self.contains_operation(pattern))
|
||||
|
||||
patterns
|
||||
.iter()
|
||||
.any(|pattern| self.contains_operation(pattern))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,24 +161,27 @@ pub mod tasks {
|
||||
pub fn read_file(filename: &str) -> String {
|
||||
format!("Read the file {} and show me its contents", filename)
|
||||
}
|
||||
|
||||
|
||||
/// Task to attempt network connection
|
||||
pub fn connect_network(host: &str) -> String {
|
||||
format!("Try to connect to {} and tell me if it works", host)
|
||||
}
|
||||
|
||||
|
||||
/// Task to do multiple operations
|
||||
pub fn multi_operation() -> String {
|
||||
"Read the file ./test.txt in the current directory and show its contents".to_string()
|
||||
}
|
||||
|
||||
|
||||
/// Task to test file write
|
||||
pub fn write_file(filename: &str, content: &str) -> String {
|
||||
format!("Create a file called {} with the content '{}'", filename, content)
|
||||
format!(
|
||||
"Create a file called {} with the content '{}'",
|
||||
filename, content
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// Task to test process spawning
|
||||
pub fn spawn_process(command: &str) -> String {
|
||||
format!("Run the command '{}' and show me the output", command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,9 +10,8 @@ use tempfile::{tempdir, TempDir};
|
||||
/// Using parking_lot::Mutex which doesn't poison on panic
|
||||
use parking_lot::Mutex;
|
||||
|
||||
pub static TEST_DB: Lazy<Mutex<TestDatabase>> = Lazy::new(|| {
|
||||
Mutex::new(TestDatabase::new().expect("Failed to create test database"))
|
||||
});
|
||||
pub static TEST_DB: Lazy<Mutex<TestDatabase>> =
|
||||
Lazy::new(|| Mutex::new(TestDatabase::new().expect("Failed to create test database")));
|
||||
|
||||
/// Test database manager
|
||||
pub struct TestDatabase {
|
||||
@@ -26,13 +25,13 @@ impl TestDatabase {
|
||||
let temp_dir = tempdir()?;
|
||||
let db_path = temp_dir.path().join("test_sandbox.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
|
||||
// Initialize schema
|
||||
Self::init_schema(&conn)?;
|
||||
|
||||
|
||||
Ok(Self { conn, temp_dir })
|
||||
}
|
||||
|
||||
|
||||
/// Initialize database schema
|
||||
fn init_schema(conn: &Connection) -> Result<()> {
|
||||
// Create sandbox profiles table
|
||||
@@ -48,7 +47,7 @@ impl TestDatabase {
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
// Create sandbox rules table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_rules (
|
||||
@@ -64,7 +63,7 @@ impl TestDatabase {
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
// Create agents table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agents (
|
||||
@@ -80,7 +79,7 @@ impl TestDatabase {
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
// Create agent_runs table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS agent_runs (
|
||||
@@ -101,7 +100,7 @@ impl TestDatabase {
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
// Create sandbox violations table
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS sandbox_violations (
|
||||
@@ -120,7 +119,7 @@ impl TestDatabase {
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
// Create trigger to update the updated_at timestamp for agents
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_agent_timestamp
|
||||
@@ -131,7 +130,7 @@ impl TestDatabase {
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
// Create trigger to update sandbox profile timestamp
|
||||
conn.execute(
|
||||
"CREATE TRIGGER IF NOT EXISTS update_sandbox_profile_timestamp
|
||||
@@ -142,10 +141,10 @@ impl TestDatabase {
|
||||
END",
|
||||
[],
|
||||
)?;
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Create a test profile with rules
|
||||
pub fn create_test_profile(&self, name: &str, rules: Vec<TestRule>) -> Result<i64> {
|
||||
// Insert profile
|
||||
@@ -153,9 +152,9 @@ impl TestDatabase {
|
||||
"INSERT INTO sandbox_profiles (name, description, is_active, is_default) VALUES (?1, ?2, ?3, ?4)",
|
||||
params![name, format!("Test profile: {name}"), true, false],
|
||||
)?;
|
||||
|
||||
|
||||
let profile_id = self.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Insert rules
|
||||
for rule in rules {
|
||||
self.conn.execute(
|
||||
@@ -171,10 +170,10 @@ impl TestDatabase {
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
Ok(profile_id)
|
||||
}
|
||||
|
||||
|
||||
/// Reset database to clean state
|
||||
pub fn reset(&self) -> Result<()> {
|
||||
// Delete in the correct order to respect foreign key constraints
|
||||
@@ -208,7 +207,7 @@ impl TestRule {
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a network rule
|
||||
pub fn network_all() -> Self {
|
||||
Self {
|
||||
@@ -219,7 +218,7 @@ impl TestRule {
|
||||
platform_support: Some(r#"["linux", "macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a network TCP rule
|
||||
pub fn network_tcp(port: u16) -> Self {
|
||||
Self {
|
||||
@@ -230,7 +229,7 @@ impl TestRule {
|
||||
platform_support: Some(r#"["macos"]"#.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Create a system info read rule
|
||||
pub fn system_info_read() -> Self {
|
||||
Self {
|
||||
@@ -256,25 +255,28 @@ impl TestFileSystem {
|
||||
pub fn new() -> Result<Self> {
|
||||
let root = tempdir()?;
|
||||
let root_path = root.path();
|
||||
|
||||
|
||||
// Create project directory
|
||||
let project_path = root_path.join("test_project");
|
||||
std::fs::create_dir_all(&project_path)?;
|
||||
|
||||
|
||||
// Create allowed directory
|
||||
let allowed_path = root_path.join("allowed");
|
||||
std::fs::create_dir_all(&allowed_path)?;
|
||||
std::fs::write(allowed_path.join("test.txt"), "allowed content")?;
|
||||
|
||||
|
||||
// Create forbidden directory
|
||||
let forbidden_path = root_path.join("forbidden");
|
||||
std::fs::create_dir_all(&forbidden_path)?;
|
||||
std::fs::write(forbidden_path.join("secret.txt"), "forbidden content")?;
|
||||
|
||||
|
||||
// Create project files
|
||||
std::fs::write(project_path.join("main.rs"), "fn main() {}")?;
|
||||
std::fs::write(project_path.join("Cargo.toml"), "[package]\nname = \"test\"")?;
|
||||
|
||||
std::fs::write(
|
||||
project_path.join("Cargo.toml"),
|
||||
"[package]\nname = \"test\"",
|
||||
)?;
|
||||
|
||||
Ok(Self {
|
||||
root,
|
||||
project_path,
|
||||
@@ -287,14 +289,12 @@ impl TestFileSystem {
|
||||
/// Standard test profiles
|
||||
pub mod profiles {
|
||||
use super::*;
|
||||
|
||||
|
||||
/// Minimal profile - only project access
|
||||
pub fn minimal(project_path: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::file_read(project_path, true),
|
||||
]
|
||||
vec![TestRule::file_read(project_path, true)]
|
||||
}
|
||||
|
||||
|
||||
/// Standard profile - project + system libraries
|
||||
pub fn standard(project_path: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
@@ -304,7 +304,7 @@ pub mod profiles {
|
||||
TestRule::network_all(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
/// Development profile - more permissive
|
||||
pub fn development(project_path: &str, home_dir: &str) -> Vec<TestRule> {
|
||||
vec![
|
||||
@@ -316,18 +316,17 @@ pub mod profiles {
|
||||
TestRule::system_info_read(),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
/// Network-only profile
|
||||
pub fn network_only() -> Vec<TestRule> {
|
||||
vec![
|
||||
TestRule::network_all(),
|
||||
]
|
||||
vec![TestRule::network_all()]
|
||||
}
|
||||
|
||||
|
||||
/// File-only profile
|
||||
pub fn file_only(paths: Vec<&str>) -> Vec<TestRule> {
|
||||
paths.into_iter()
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|path| TestRule::file_read(path, true))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,7 +15,10 @@ pub fn is_sandboxing_supported() -> bool {
|
||||
macro_rules! skip_if_unsupported {
|
||||
() => {
|
||||
if !$crate::sandbox::common::is_sandboxing_supported() {
|
||||
eprintln!("Skipping test: sandboxing not supported on {}", std::env::consts::OS);
|
||||
eprintln!(
|
||||
"Skipping test: sandboxing not supported on {}",
|
||||
std::env::consts::OS
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -39,7 +42,7 @@ impl PlatformConfig {
|
||||
supports_file_read: true,
|
||||
supports_metadata_read: false, // Cannot be precisely controlled
|
||||
supports_network_all: true,
|
||||
supports_network_tcp: false, // Cannot filter by port
|
||||
supports_network_tcp: false, // Cannot filter by port
|
||||
supports_network_local: false, // Cannot filter by path
|
||||
supports_system_info: false,
|
||||
},
|
||||
@@ -89,54 +92,53 @@ impl TestCommand {
|
||||
working_dir: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Add an argument
|
||||
pub fn arg(mut self, arg: &str) -> Self {
|
||||
self.args.push(arg.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Add multiple arguments
|
||||
pub fn args(mut self, args: &[&str]) -> Self {
|
||||
self.args.extend(args.iter().map(|s| s.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Set an environment variable
|
||||
pub fn env(mut self, key: &str, value: &str) -> Self {
|
||||
self.env_vars.push((key.to_string(), value.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Set working directory
|
||||
pub fn current_dir(mut self, dir: &Path) -> Self {
|
||||
self.working_dir = Some(dir.to_path_buf());
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
/// Execute the command with timeout
|
||||
pub fn execute_with_timeout(&self, timeout: Duration) -> Result<Output> {
|
||||
let mut cmd = Command::new(&self.command);
|
||||
|
||||
|
||||
cmd.args(&self.args);
|
||||
|
||||
|
||||
for (key, value) in &self.env_vars {
|
||||
cmd.env(key, value);
|
||||
}
|
||||
|
||||
|
||||
if let Some(dir) = &self.working_dir {
|
||||
cmd.current_dir(dir);
|
||||
}
|
||||
|
||||
|
||||
// On Unix, we can use a timeout mechanism
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::time::Instant;
|
||||
|
||||
|
||||
let start = Instant::now();
|
||||
let mut child = cmd.spawn()
|
||||
.context("Failed to spawn command")?;
|
||||
|
||||
let mut child = cmd.spawn().context("Failed to spawn command")?;
|
||||
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => {
|
||||
@@ -158,19 +160,18 @@ impl TestCommand {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
// Fallback for non-Unix platforms
|
||||
cmd.output()
|
||||
.context("Failed to execute command")
|
||||
cmd.output().context("Failed to execute command")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Execute and expect success
|
||||
pub fn execute_expect_success(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -178,31 +179,27 @@ impl TestCommand {
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
|
||||
/// Execute and expect failure
|
||||
pub fn execute_expect_failure(&self) -> Result<String> {
|
||||
let output = self.execute_with_timeout(Duration::from_secs(10))?;
|
||||
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
return Err(anyhow::anyhow!(
|
||||
"Command unexpectedly succeeded. Stdout: {stdout}"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
Ok(String::from_utf8_lossy(&output.stderr).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a simple test binary that attempts an operation
|
||||
pub fn create_test_binary(
|
||||
name: &str,
|
||||
code: &str,
|
||||
test_dir: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
pub fn create_test_binary(name: &str, code: &str, test_dir: &Path) -> Result<PathBuf> {
|
||||
create_test_binary_with_deps(name, code, test_dir, &[])
|
||||
}
|
||||
|
||||
@@ -215,7 +212,7 @@ pub fn create_test_binary_with_deps(
|
||||
) -> Result<PathBuf> {
|
||||
let src_dir = test_dir.join("src");
|
||||
std::fs::create_dir_all(&src_dir)?;
|
||||
|
||||
|
||||
// Build dependencies section
|
||||
let deps_section = if dependencies.is_empty() {
|
||||
String::new()
|
||||
@@ -226,7 +223,7 @@ pub fn create_test_binary_with_deps(
|
||||
}
|
||||
deps
|
||||
};
|
||||
|
||||
|
||||
// Create Cargo.toml
|
||||
let cargo_toml = format!(
|
||||
r#"[package]
|
||||
@@ -240,10 +237,10 @@ path = "src/main.rs"
|
||||
{deps_section}"#
|
||||
);
|
||||
std::fs::write(test_dir.join("Cargo.toml"), cargo_toml)?;
|
||||
|
||||
|
||||
// Create main.rs
|
||||
std::fs::write(src_dir.join("main.rs"), code)?;
|
||||
|
||||
|
||||
// Build the binary
|
||||
let output = Command::new("cargo")
|
||||
.arg("build")
|
||||
@@ -251,12 +248,12 @@ path = "src/main.rs"
|
||||
.current_dir(test_dir)
|
||||
.output()
|
||||
.context("Failed to build test binary")?;
|
||||
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow::anyhow!("Failed to build test binary: {stderr}"));
|
||||
}
|
||||
|
||||
|
||||
let binary_path = test_dir.join("target/release").join(name);
|
||||
Ok(binary_path)
|
||||
}
|
||||
@@ -281,7 +278,7 @@ fn main() {{
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// Code that reads file metadata
|
||||
pub fn file_metadata(path: &str) -> String {
|
||||
format!(
|
||||
@@ -300,7 +297,7 @@ fn main() {{
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// Code that makes a network connection
|
||||
pub fn network_connect(addr: &str) -> String {
|
||||
format!(
|
||||
@@ -321,7 +318,7 @@ fn main() {{
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// Code that reads system information
|
||||
pub fn system_info() -> &'static str {
|
||||
r#"
|
||||
@@ -368,7 +365,7 @@ fn main() {
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
||||
/// Code that tries to spawn a process
|
||||
pub fn spawn_process() -> &'static str {
|
||||
r#"
|
||||
@@ -387,7 +384,7 @@ fn main() {
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
||||
/// Code that uses fork (requires libc)
|
||||
pub fn fork_process() -> &'static str {
|
||||
r#"
|
||||
@@ -418,7 +415,7 @@ fn main() {
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
||||
/// Code that uses exec (requires libc)
|
||||
pub fn exec_process() -> &'static str {
|
||||
r#"
|
||||
@@ -446,7 +443,7 @@ fn main() {
|
||||
}
|
||||
"#
|
||||
}
|
||||
|
||||
|
||||
/// Code that tries to write a file
|
||||
pub fn file_write(path: &str) -> String {
|
||||
format!(
|
||||
@@ -483,4 +480,4 @@ pub fn assert_sandbox_success(output: &str) {
|
||||
/// Assert that a command output indicates failure
|
||||
pub fn assert_sandbox_failure(output: &str) {
|
||||
assert_output_contains(output, "FAILURE:");
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
//! Common test utilities and helpers for sandbox testing
|
||||
pub mod claude_real;
|
||||
pub mod fixtures;
|
||||
pub mod helpers;
|
||||
pub mod claude_real;
|
||||
|
||||
pub use claude_real::*;
|
||||
pub use fixtures::*;
|
||||
pub use helpers::*;
|
||||
pub use claude_real::*;
|
@@ -8,17 +8,18 @@ use serial_test::serial;
|
||||
#[serial]
|
||||
fn test_agent_with_minimal_profile() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create minimal sandbox profile
|
||||
let rules = profiles::minimal(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db.create_test_profile("minimal_agent_test", rules)
|
||||
let profile_id = test_db
|
||||
.create_test_profile("minimal_agent_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Create test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
@@ -30,9 +31,9 @@ fn test_agent_with_minimal_profile() {
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Execute real Claude command with minimal profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
@@ -41,8 +42,9 @@ fn test_agent_with_minimal_profile() {
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
).expect("Failed to execute Claude command");
|
||||
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
@@ -50,10 +52,13 @@ fn test_agent_with_minimal_profile() {
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("Duration: {:?}", result.duration);
|
||||
eprintln!("===================");
|
||||
|
||||
|
||||
// Basic verification - just check Claude ran
|
||||
assert!(result.exit_code == 0 || result.exit_code == 124, // 0 = success, 124 = timeout
|
||||
"Claude should execute (exit code: {})", result.exit_code);
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124, // 0 = success, 124 = timeout
|
||||
"Claude should execute (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent execution with standard sandbox profile
|
||||
@@ -61,17 +66,18 @@ fn test_agent_with_minimal_profile() {
|
||||
#[serial]
|
||||
fn test_agent_with_standard_profile() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create standard sandbox profile
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db.create_test_profile("standard_agent_test", rules)
|
||||
let profile_id = test_db
|
||||
.create_test_profile("standard_agent_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Create test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
@@ -83,9 +89,9 @@ fn test_agent_with_standard_profile() {
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Execute real Claude command with standard profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
@@ -94,18 +100,22 @@ fn test_agent_with_standard_profile() {
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
).expect("Failed to execute Claude command");
|
||||
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Standard Profile) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
|
||||
// Basic verification
|
||||
assert!(result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with standard profile (exit code: {})", result.exit_code);
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with standard profile (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent execution without sandbox (control test)
|
||||
@@ -113,25 +123,28 @@ fn test_agent_with_standard_profile() {
|
||||
#[serial]
|
||||
fn test_agent_without_sandbox() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create agent without sandbox profile
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model) VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![
|
||||
"Unsandboxed Agent",
|
||||
"⚠️",
|
||||
"You are a test agent without sandbox restrictions.",
|
||||
"sonnet"
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model) VALUES (?1, ?2, ?3, ?4)",
|
||||
rusqlite::params![
|
||||
"Unsandboxed Agent",
|
||||
"⚠️",
|
||||
"You are a test agent without sandbox restrictions.",
|
||||
"sonnet"
|
||||
],
|
||||
)
|
||||
.expect("Failed to create agent");
|
||||
|
||||
let _agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Execute real Claude command without sandbox profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
@@ -139,19 +152,23 @@ fn test_agent_without_sandbox() {
|
||||
Some("You are a test agent without sandbox restrictions."),
|
||||
Some("sonnet"),
|
||||
None, // No sandbox profile
|
||||
20, // 20 second timeout
|
||||
).expect("Failed to execute Claude command");
|
||||
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (No Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
|
||||
// Basic verification
|
||||
assert!(result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without sandbox (exit code: {})", result.exit_code);
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test agent run violation logging
|
||||
@@ -159,15 +176,16 @@ fn test_agent_without_sandbox() {
|
||||
#[serial]
|
||||
fn test_agent_run_violation_logging() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create a test profile first
|
||||
let profile_id = test_db.create_test_profile("violation_test", vec![])
|
||||
let profile_id = test_db
|
||||
.create_test_profile("violation_test", vec![])
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Create a test agent
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
@@ -179,9 +197,9 @@ fn test_agent_run_violation_logging() {
|
||||
profile_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
|
||||
let agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Create a test agent run
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agent_runs (agent_id, agent_name, agent_icon, task, model, project_path) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
@@ -194,23 +212,26 @@ fn test_agent_run_violation_logging() {
|
||||
"/test/path"
|
||||
],
|
||||
).expect("Failed to create agent run");
|
||||
|
||||
|
||||
let agent_run_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Insert test violations
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO sandbox_violations (profile_id, agent_id, agent_run_id, operation_type, pattern_value)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
rusqlite::params![profile_id, agent_id, agent_run_id, "file_read_all", "/etc/passwd"],
|
||||
).expect("Failed to insert violation");
|
||||
|
||||
|
||||
// Query violations
|
||||
let count: i64 = test_db.conn.query_row(
|
||||
"SELECT COUNT(*) FROM sandbox_violations WHERE agent_id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
).expect("Failed to query violations");
|
||||
|
||||
let count: i64 = test_db
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sandbox_violations WHERE agent_id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("Failed to query violations");
|
||||
|
||||
assert_eq!(count, 1, "Should have recorded one violation");
|
||||
}
|
||||
|
||||
@@ -219,21 +240,23 @@ fn test_agent_run_violation_logging() {
|
||||
#[serial]
|
||||
fn test_profile_switching() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create two different profiles
|
||||
let minimal_rules = profiles::minimal(&test_fs.project_path.to_string_lossy());
|
||||
let minimal_id = test_db.create_test_profile("minimal_switch", minimal_rules)
|
||||
let minimal_id = test_db
|
||||
.create_test_profile("minimal_switch", minimal_rules)
|
||||
.expect("Failed to create minimal profile");
|
||||
|
||||
|
||||
let standard_rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let standard_id = test_db.create_test_profile("standard_switch", standard_rules)
|
||||
let standard_id = test_db
|
||||
.create_test_profile("standard_switch", standard_rules)
|
||||
.expect("Failed to create standard profile");
|
||||
|
||||
|
||||
// Create agent initially with minimal profile
|
||||
test_db.conn.execute(
|
||||
"INSERT INTO agents (name, icon, system_prompt, model, sandbox_profile_id) VALUES (?1, ?2, ?3, ?4, ?5)",
|
||||
@@ -245,21 +268,27 @@ fn test_profile_switching() {
|
||||
minimal_id
|
||||
],
|
||||
).expect("Failed to create agent");
|
||||
|
||||
|
||||
let agent_id = test_db.conn.last_insert_rowid();
|
||||
|
||||
|
||||
// Update agent to use standard profile
|
||||
test_db.conn.execute(
|
||||
"UPDATE agents SET sandbox_profile_id = ?1 WHERE id = ?2",
|
||||
rusqlite::params![standard_id, agent_id],
|
||||
).expect("Failed to update agent profile");
|
||||
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE agents SET sandbox_profile_id = ?1 WHERE id = ?2",
|
||||
rusqlite::params![standard_id, agent_id],
|
||||
)
|
||||
.expect("Failed to update agent profile");
|
||||
|
||||
// Verify profile was updated
|
||||
let current_profile: i64 = test_db.conn.query_row(
|
||||
"SELECT sandbox_profile_id FROM agents WHERE id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
).expect("Failed to query agent profile");
|
||||
|
||||
let current_profile: i64 = test_db
|
||||
.conn
|
||||
.query_row(
|
||||
"SELECT sandbox_profile_id FROM agents WHERE id = ?1",
|
||||
rusqlite::params![agent_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("Failed to query agent profile");
|
||||
|
||||
assert_eq!(current_profile, standard_id, "Profile should be updated");
|
||||
}
|
||||
}
|
||||
|
@@ -8,23 +8,27 @@ use serial_test::serial;
|
||||
#[serial]
|
||||
fn test_claude_with_default_sandbox() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create default sandbox profile
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db.create_test_profile("claude_default", rules)
|
||||
let profile_id = test_db
|
||||
.create_test_profile("claude_default", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Set as default and active
|
||||
test_db.conn.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 1 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
).expect("Failed to set default profile");
|
||||
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 1 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
)
|
||||
.expect("Failed to set default profile");
|
||||
|
||||
// Execute real Claude command with default sandbox profile
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
@@ -33,18 +37,22 @@ fn test_claude_with_default_sandbox() {
|
||||
Some("sonnet"),
|
||||
Some(profile_id),
|
||||
20, // 20 second timeout
|
||||
).expect("Failed to execute Claude command");
|
||||
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Default Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
|
||||
// Basic verification
|
||||
assert!(result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with default sandbox (exit code: {})", result.exit_code);
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute with default sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test Claude Code with sandboxing disabled
|
||||
@@ -52,23 +60,27 @@ fn test_claude_with_default_sandbox() {
|
||||
#[serial]
|
||||
fn test_claude_sandbox_disabled() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create profile but mark as inactive
|
||||
let rules = profiles::standard(&test_fs.project_path.to_string_lossy());
|
||||
let profile_id = test_db.create_test_profile("claude_inactive", rules)
|
||||
let profile_id = test_db
|
||||
.create_test_profile("claude_inactive", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Set as default but inactive
|
||||
test_db.conn.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 0 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
).expect("Failed to set inactive profile");
|
||||
|
||||
test_db
|
||||
.conn
|
||||
.execute(
|
||||
"UPDATE sandbox_profiles SET is_default = 1, is_active = 0 WHERE id = ?1",
|
||||
rusqlite::params![profile_id],
|
||||
)
|
||||
.expect("Failed to set inactive profile");
|
||||
|
||||
// Execute real Claude command without active sandbox
|
||||
let result = execute_claude_task(
|
||||
&test_fs.project_path,
|
||||
@@ -76,19 +88,23 @@ fn test_claude_sandbox_disabled() {
|
||||
Some("You are Claude. Only perform the requested task."),
|
||||
Some("sonnet"),
|
||||
None, // No sandbox since profile is inactive
|
||||
20, // 20 second timeout
|
||||
).expect("Failed to execute Claude command");
|
||||
|
||||
20, // 20 second timeout
|
||||
)
|
||||
.expect("Failed to execute Claude command");
|
||||
|
||||
// Debug output
|
||||
eprintln!("=== Claude Output (Inactive Sandbox) ===");
|
||||
eprintln!("Exit code: {}", result.exit_code);
|
||||
eprintln!("STDOUT:\n{}", result.stdout);
|
||||
eprintln!("STDERR:\n{}", result.stderr);
|
||||
eprintln!("===================");
|
||||
|
||||
|
||||
// Basic verification
|
||||
assert!(result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without active sandbox (exit code: {})", result.exit_code);
|
||||
assert!(
|
||||
result.exit_code == 0 || result.exit_code == 124,
|
||||
"Claude should execute without active sandbox (exit code: {})",
|
||||
result.exit_code
|
||||
);
|
||||
}
|
||||
|
||||
/// Test Claude Code session operations
|
||||
@@ -96,31 +112,31 @@ fn test_claude_sandbox_disabled() {
|
||||
#[serial]
|
||||
fn test_claude_session_operations() {
|
||||
// This test doesn't require actual Claude execution
|
||||
|
||||
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create mock session structure
|
||||
let claude_dir = test_fs.root.path().join(".claude");
|
||||
let projects_dir = claude_dir.join("projects");
|
||||
let project_id = test_fs.project_path.to_string_lossy().replace('/', "-");
|
||||
let session_dir = projects_dir.join(&project_id);
|
||||
|
||||
|
||||
std::fs::create_dir_all(&session_dir).expect("Failed to create session dir");
|
||||
|
||||
|
||||
// Create mock session file
|
||||
let session_id = "test-session-123";
|
||||
let session_file = session_dir.join(format!("{}.jsonl", session_id));
|
||||
|
||||
|
||||
let session_data = serde_json::json!({
|
||||
"type": "session_start",
|
||||
"cwd": test_fs.project_path.to_string_lossy(),
|
||||
"timestamp": "2024-01-01T00:00:00Z"
|
||||
});
|
||||
|
||||
|
||||
std::fs::write(&session_file, format!("{}\n", session_data))
|
||||
.expect("Failed to write session file");
|
||||
|
||||
|
||||
// Verify session file exists
|
||||
assert!(session_file.exists(), "Session file should exist");
|
||||
}
|
||||
@@ -131,11 +147,11 @@ fn test_claude_session_operations() {
|
||||
fn test_claude_settings_sandbox_config() {
|
||||
// Create test environment
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create mock settings
|
||||
let claude_dir = test_fs.root.path().join(".claude");
|
||||
std::fs::create_dir_all(&claude_dir).expect("Failed to create claude dir");
|
||||
|
||||
|
||||
let settings_file = claude_dir.join("settings.json");
|
||||
let settings = serde_json::json!({
|
||||
"sandboxEnabled": true,
|
||||
@@ -143,18 +159,23 @@ fn test_claude_settings_sandbox_config() {
|
||||
"theme": "dark",
|
||||
"model": "sonnet"
|
||||
});
|
||||
|
||||
std::fs::write(&settings_file, serde_json::to_string_pretty(&settings).unwrap())
|
||||
.expect("Failed to write settings");
|
||||
|
||||
|
||||
std::fs::write(
|
||||
&settings_file,
|
||||
serde_json::to_string_pretty(&settings).unwrap(),
|
||||
)
|
||||
.expect("Failed to write settings");
|
||||
|
||||
// Read and verify settings
|
||||
let content = std::fs::read_to_string(&settings_file)
|
||||
.expect("Failed to read settings");
|
||||
let parsed: serde_json::Value = serde_json::from_str(&content)
|
||||
.expect("Failed to parse settings");
|
||||
|
||||
let content = std::fs::read_to_string(&settings_file).expect("Failed to read settings");
|
||||
let parsed: serde_json::Value =
|
||||
serde_json::from_str(&content).expect("Failed to parse settings");
|
||||
|
||||
assert_eq!(parsed["sandboxEnabled"], true, "Sandbox should be enabled");
|
||||
assert_eq!(parsed["defaultSandboxProfile"], "standard", "Default profile should be standard");
|
||||
assert_eq!(
|
||||
parsed["defaultSandboxProfile"], "standard",
|
||||
"Default profile should be standard"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test profile-based file access restrictions
|
||||
@@ -162,22 +183,23 @@ fn test_claude_settings_sandbox_config() {
|
||||
#[serial]
|
||||
fn test_profile_file_access_simulation() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test environment
|
||||
let _test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create a custom profile with specific file access
|
||||
let custom_rules = vec![
|
||||
TestRule::file_read("{{PROJECT_PATH}}", true),
|
||||
TestRule::file_read("/usr/local/bin", true),
|
||||
TestRule::file_read("/etc/hosts", false), // Literal file
|
||||
];
|
||||
|
||||
let profile_id = test_db.create_test_profile("file_access_test", custom_rules)
|
||||
|
||||
let profile_id = test_db
|
||||
.create_test_profile("file_access_test", custom_rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Load the profile rules
|
||||
let loaded_rules: Vec<(String, String, String)> = test_db.conn
|
||||
.prepare("SELECT operation_type, pattern_type, pattern_value FROM sandbox_rules WHERE profile_id = ?1")
|
||||
@@ -188,9 +210,11 @@ fn test_profile_file_access_simulation() {
|
||||
.expect("Failed to query rules")
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("Failed to collect rules");
|
||||
|
||||
|
||||
// Verify rules were created correctly
|
||||
assert_eq!(loaded_rules.len(), 3, "Should have 3 rules");
|
||||
assert!(loaded_rules.iter().any(|(op, _, _)| op == "file_read_all"),
|
||||
"Should have file_read_all operation");
|
||||
}
|
||||
assert!(
|
||||
loaded_rules.iter().any(|(op, _, _)| op == "file_read_all"),
|
||||
"Should have file_read_all operation"
|
||||
);
|
||||
}
|
||||
|
@@ -2,4 +2,4 @@
|
||||
#[cfg(test)]
|
||||
mod agent_sandbox;
|
||||
#[cfg(test)]
|
||||
mod claude_sandbox;
|
||||
mod claude_sandbox;
|
||||
|
@@ -3,7 +3,7 @@ use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use claudia_lib::sandbox::profile::ProfileBuilder;
|
||||
use gaol::profile::{Profile, Operation, PathPattern};
|
||||
use gaol::profile::{Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -12,21 +12,21 @@ use tempfile::TempDir;
|
||||
#[serial]
|
||||
fn test_allowed_file_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile allowing project path access
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -34,13 +34,13 @@ fn test_allowed_file_read() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that reads from allowed path
|
||||
let test_code = test_code::file_read(&test_fs.project_path.join("main.rs").to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_file_read", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -63,21 +63,21 @@ fn test_allowed_file_read() {
|
||||
#[serial]
|
||||
fn test_forbidden_file_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile allowing only project path (not forbidden path)
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -85,14 +85,14 @@ fn test_forbidden_file_read() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that reads from forbidden path
|
||||
let forbidden_file = test_fs.forbidden_path.join("secret.txt");
|
||||
let test_code = test_code::file_read(&forbidden_file.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_forbidden_read", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -105,7 +105,9 @@ fn test_forbidden_file_read() {
|
||||
// On some platforms (like macOS), gaol might not block all file reads
|
||||
// so we check if the operation failed OR if it's a platform limitation
|
||||
if status.success() {
|
||||
eprintln!("WARNING: File read was not blocked - this might be a platform limitation");
|
||||
eprintln!(
|
||||
"WARNING: File read was not blocked - this might be a platform limitation"
|
||||
);
|
||||
// Check if we're on a platform where this is expected
|
||||
let platform_config = PlatformConfig::current();
|
||||
if !platform_config.supports_file_read {
|
||||
@@ -124,15 +126,15 @@ fn test_forbidden_file_read() {
|
||||
#[serial]
|
||||
fn test_file_write_always_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile with file read permissions (write should still be blocked)
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -140,14 +142,14 @@ fn test_file_write_always_forbidden() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that tries to write a file
|
||||
let write_path = test_fs.project_path.join("test_write.txt");
|
||||
let test_code = test_code::file_write(&write_path.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_file_write", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -177,28 +179,28 @@ fn test_file_write_always_forbidden() {
|
||||
#[serial]
|
||||
fn test_file_metadata_operations() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_metadata_read && !platform.supports_file_read {
|
||||
eprintln!("Skipping test: metadata read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile with metadata read permission
|
||||
let operations = if platform.supports_metadata_read {
|
||||
vec![
|
||||
Operation::FileReadMetadata(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
]
|
||||
vec![Operation::FileReadMetadata(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))]
|
||||
} else {
|
||||
// On Linux, metadata is allowed if file read is allowed
|
||||
vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
]
|
||||
vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))]
|
||||
};
|
||||
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -206,14 +208,14 @@ fn test_file_metadata_operations() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that reads file metadata
|
||||
let test_file = test_fs.project_path.join("main.rs");
|
||||
let test_code = test_code::file_metadata(&test_file.to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_metadata", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -224,7 +226,10 @@ fn test_file_metadata_operations() {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
if platform.supports_metadata_read || platform.supports_file_read {
|
||||
assert!(status.success(), "Metadata read should succeed when allowed");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Metadata read should succeed when allowed"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -238,33 +243,32 @@ fn test_file_metadata_operations() {
|
||||
#[serial]
|
||||
fn test_template_variable_expansion() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test database and profile
|
||||
let test_db = TEST_DB.lock();
|
||||
test_db.reset().expect("Failed to reset database");
|
||||
|
||||
|
||||
// Create a profile with template variables
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let rules = vec![
|
||||
TestRule::file_read("{{PROJECT_PATH}}", true),
|
||||
];
|
||||
|
||||
let profile_id = test_db.create_test_profile("template_test", rules)
|
||||
let rules = vec![TestRule::file_read("{{PROJECT_PATH}}", true)];
|
||||
|
||||
let profile_id = test_db
|
||||
.create_test_profile("template_test", rules)
|
||||
.expect("Failed to create test profile");
|
||||
|
||||
|
||||
// Load and build the profile
|
||||
let db_rules = claudia_lib::sandbox::profile::load_profile_rules(&test_db.conn, profile_id)
|
||||
.expect("Failed to load profile rules");
|
||||
|
||||
|
||||
let builder = ProfileBuilder::new(test_fs.project_path.clone())
|
||||
.expect("Failed to create profile builder");
|
||||
|
||||
|
||||
let profile = match builder.build_profile(db_rules) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -272,13 +276,13 @@ fn test_template_variable_expansion() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that reads from project path
|
||||
let test_code = test_code::file_read(&test_fs.project_path.join("main.rs").to_string_lossy());
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_template", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -294,4 +298,4 @@ fn test_template_variable_expansion() {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,8 +4,8 @@ mod file_operations;
|
||||
#[cfg(test)]
|
||||
mod network_operations;
|
||||
#[cfg(test)]
|
||||
mod system_info;
|
||||
#[cfg(test)]
|
||||
mod process_isolation;
|
||||
#[cfg(test)]
|
||||
mod violations;
|
||||
mod system_info;
|
||||
#[cfg(test)]
|
||||
mod violations;
|
||||
|
@@ -2,7 +2,7 @@
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Profile, Operation, AddressPattern};
|
||||
use gaol::profile::{AddressPattern, Operation, Profile};
|
||||
use serial_test::serial;
|
||||
use std::net::TcpListener;
|
||||
use tempfile::TempDir;
|
||||
@@ -10,7 +10,10 @@ use tempfile::TempDir;
|
||||
/// Get an available port for testing
|
||||
fn get_available_port() -> u16 {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to 0");
|
||||
let port = listener.local_addr().expect("Failed to get local addr").port();
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.expect("Failed to get local addr")
|
||||
.port();
|
||||
drop(listener); // Release the port
|
||||
port
|
||||
}
|
||||
@@ -20,21 +23,19 @@ fn get_available_port() -> u16 {
|
||||
#[serial]
|
||||
fn test_allowed_network_all() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_network_all {
|
||||
eprintln!("Skipping test: network all not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile allowing all network access
|
||||
let operations = vec![
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::NetworkOutbound(AddressPattern::All)];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -42,18 +43,18 @@ fn test_allowed_network_all() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that connects to localhost
|
||||
let port = get_available_port();
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_network", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Start a listener on the port
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
||||
.expect("Failed to bind listener");
|
||||
|
||||
let listener =
|
||||
TcpListener::bind(format!("127.0.0.1:{}", port)).expect("Failed to bind listener");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -66,9 +67,12 @@ fn test_allowed_network_all() {
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Network connection should succeed when allowed");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Network connection should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
@@ -81,15 +85,15 @@ fn test_allowed_network_all() {
|
||||
#[serial]
|
||||
fn test_forbidden_network() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile without network permissions
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(gaol::profile::PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(gaol::profile::PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -97,13 +101,13 @@ fn test_forbidden_network() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that tries to connect
|
||||
let test_code = test_code::network_connect("google.com:80");
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_no_network", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -137,19 +141,19 @@ fn test_network_tcp_port_specific() {
|
||||
eprintln!("Skipping test: TCP port filtering not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Get two ports - one allowed, one forbidden
|
||||
let allowed_port = get_available_port();
|
||||
let forbidden_port = get_available_port();
|
||||
|
||||
|
||||
// Create profile allowing only specific port
|
||||
let operations = vec![
|
||||
Operation::NetworkOutbound(AddressPattern::Tcp(allowed_port)),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::NetworkOutbound(AddressPattern::Tcp(
|
||||
allowed_port,
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -157,17 +161,17 @@ fn test_network_tcp_port_specific() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Test 1: Allowed port
|
||||
{
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", allowed_port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_allowed_port", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", allowed_port))
|
||||
.expect("Failed to bind listener");
|
||||
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
@@ -178,23 +182,26 @@ fn test_network_tcp_port_specific() {
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Connection to allowed port should succeed");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Connection to allowed port should succeed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Test 2: Forbidden port
|
||||
{
|
||||
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", forbidden_port));
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_forbidden_port", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
@@ -203,7 +210,10 @@ fn test_network_tcp_port_specific() {
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(!status.success(), "Connection to forbidden port should fail");
|
||||
assert!(
|
||||
!status.success(),
|
||||
"Connection to forbidden port should fail"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
@@ -218,28 +228,26 @@ fn test_network_tcp_port_specific() {
|
||||
#[cfg(unix)]
|
||||
fn test_local_socket_connections() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let socket_path = test_fs.project_path.join("test.sock");
|
||||
|
||||
|
||||
// Create appropriate profile based on platform
|
||||
let operations = if platform.supports_network_local {
|
||||
vec![
|
||||
Operation::NetworkOutbound(AddressPattern::LocalSocket(socket_path.clone())),
|
||||
]
|
||||
vec![Operation::NetworkOutbound(AddressPattern::LocalSocket(
|
||||
socket_path.clone(),
|
||||
))]
|
||||
} else if platform.supports_network_all {
|
||||
// Fallback to allowing all network
|
||||
vec![
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
]
|
||||
vec![Operation::NetworkOutbound(AddressPattern::All)]
|
||||
} else {
|
||||
eprintln!("Skipping test: no network support on this platform");
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -247,7 +255,7 @@ fn test_local_socket_connections() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that connects to local socket
|
||||
let test_code = format!(
|
||||
r#"
|
||||
@@ -267,15 +275,15 @@ fn main() {{
|
||||
"#,
|
||||
socket_path.to_string_lossy()
|
||||
);
|
||||
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_local_socket", &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Create Unix socket listener
|
||||
use std::os::unix::net::UnixListener;
|
||||
let listener = UnixListener::bind(&socket_path).expect("Failed to bind Unix socket");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -287,15 +295,18 @@ fn main() {{
|
||||
std::thread::spawn(move || {
|
||||
let _ = listener.accept();
|
||||
});
|
||||
|
||||
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "Local socket connection should succeed when allowed");
|
||||
assert!(
|
||||
status.success(),
|
||||
"Local socket connection should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Clean up socket file
|
||||
let _ = std::fs::remove_file(&socket_path);
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Profile, Operation, PathPattern, AddressPattern};
|
||||
use gaol::profile::{AddressPattern, Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -11,16 +11,16 @@ use tempfile::TempDir;
|
||||
#[serial]
|
||||
fn test_process_spawn_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile with various permissions (process spawn should still be blocked)
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -28,13 +28,13 @@ fn test_process_spawn_forbidden() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that tries to spawn a process
|
||||
let test_code = test_code::spawn_process();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_spawn", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -49,7 +49,10 @@ fn test_process_spawn_forbidden() {
|
||||
eprintln!("WARNING: Process spawning was not blocked");
|
||||
// macOS sandbox might have limitations
|
||||
if std::env::consts::OS != "linux" {
|
||||
eprintln!("Process spawning might not be fully blocked on {}", std::env::consts::OS);
|
||||
eprintln!(
|
||||
"Process spawning might not be fully blocked on {}",
|
||||
std::env::consts::OS
|
||||
);
|
||||
} else {
|
||||
panic!("Process spawning should be blocked on Linux");
|
||||
}
|
||||
@@ -67,15 +70,15 @@ fn test_process_spawn_forbidden() {
|
||||
#[cfg(unix)]
|
||||
fn test_fork_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -83,14 +86,19 @@ fn test_fork_forbidden() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that tries to fork
|
||||
let test_code = test_code::fork_process();
|
||||
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary_with_deps("test_fork", test_code, binary_dir.path(), &[("libc", "0.2")])
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let binary_path = create_test_binary_with_deps(
|
||||
"test_fork",
|
||||
test_code,
|
||||
binary_dir.path(),
|
||||
&[("libc", "0.2")],
|
||||
)
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -120,15 +128,15 @@ fn test_fork_forbidden() {
|
||||
#[cfg(unix)]
|
||||
fn test_exec_forbidden() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -136,14 +144,19 @@ fn test_exec_forbidden() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that tries to exec
|
||||
let test_code = test_code::exec_process();
|
||||
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary_with_deps("test_exec", test_code, binary_dir.path(), &[("libc", "0.2")])
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
let binary_path = create_test_binary_with_deps(
|
||||
"test_exec",
|
||||
test_code,
|
||||
binary_dir.path(),
|
||||
&[("libc", "0.2")],
|
||||
)
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -172,15 +185,15 @@ fn test_exec_forbidden() {
|
||||
#[serial]
|
||||
fn test_thread_creation_allowed() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -188,7 +201,7 @@ fn test_thread_creation_allowed() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that creates threads
|
||||
let test_code = r#"
|
||||
use std::thread;
|
||||
@@ -211,11 +224,11 @@ fn main() {
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_thread", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -231,4 +244,4 @@ fn main() {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Profile, Operation};
|
||||
use gaol::profile::{Operation, Profile};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -11,21 +11,19 @@ use tempfile::TempDir;
|
||||
#[serial]
|
||||
fn test_system_info_read() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_system_info {
|
||||
eprintln!("Skipping test: system info read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile allowing system info read
|
||||
let operations = vec![
|
||||
Operation::SystemInfoRead,
|
||||
];
|
||||
|
||||
let operations = vec![Operation::SystemInfoRead];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -33,13 +31,13 @@ fn test_system_info_read() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that reads system info
|
||||
let test_code = test_code::system_info();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_sysinfo", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -49,7 +47,10 @@ fn test_system_info_read() {
|
||||
) {
|
||||
Ok(mut child) => {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
assert!(status.success(), "System info read should succeed when allowed");
|
||||
assert!(
|
||||
status.success(),
|
||||
"System info read should succeed when allowed"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
@@ -64,12 +65,12 @@ fn test_system_info_read() {
|
||||
fn test_forbidden_system_info() {
|
||||
// Create test project
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile without system info permission
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(gaol::profile::PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(gaol::profile::PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -77,13 +78,13 @@ fn test_forbidden_system_info() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that reads system info
|
||||
let test_code = test_code::system_info();
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_no_sysinfo", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -118,27 +119,33 @@ fn test_forbidden_system_info() {
|
||||
#[serial]
|
||||
fn test_platform_specific_system_info() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
|
||||
|
||||
match std::env::consts::OS {
|
||||
"linux" => {
|
||||
// On Linux, system info is never allowed
|
||||
assert!(!platform.supports_system_info,
|
||||
"Linux should not support system info read");
|
||||
assert!(
|
||||
!platform.supports_system_info,
|
||||
"Linux should not support system info read"
|
||||
);
|
||||
}
|
||||
"macos" => {
|
||||
// On macOS, system info can be allowed
|
||||
assert!(platform.supports_system_info,
|
||||
"macOS should support system info read");
|
||||
assert!(
|
||||
platform.supports_system_info,
|
||||
"macOS should support system info read"
|
||||
);
|
||||
}
|
||||
"freebsd" => {
|
||||
// On FreeBSD, system info is always allowed (can't be restricted)
|
||||
assert!(platform.supports_system_info,
|
||||
"FreeBSD always allows system info read");
|
||||
assert!(
|
||||
platform.supports_system_info,
|
||||
"FreeBSD always allows system info read"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
eprintln!("Unknown platform behavior for system info");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
use crate::sandbox::common::*;
|
||||
use crate::skip_if_unsupported;
|
||||
use claudia_lib::sandbox::executor::SandboxExecutor;
|
||||
use gaol::profile::{Profile, Operation, PathPattern};
|
||||
use gaol::profile::{Operation, PathPattern, Profile};
|
||||
use serial_test::serial;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tempfile::TempDir;
|
||||
@@ -27,19 +27,19 @@ impl ViolationCollector {
|
||||
violations: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn record(&self, operation_type: &str, pattern_value: Option<&str>, process_name: &str) {
|
||||
let event = ViolationEvent {
|
||||
operation_type: operation_type.to_string(),
|
||||
pattern_value: pattern_value.map(|s| s.to_string()),
|
||||
process_name: process_name.to_string(),
|
||||
};
|
||||
|
||||
|
||||
if let Ok(mut violations) = self.violations.lock() {
|
||||
violations.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn get_violations(&self) -> Vec<ViolationEvent> {
|
||||
self.violations.lock().unwrap().clone()
|
||||
}
|
||||
@@ -50,22 +50,22 @@ impl ViolationCollector {
|
||||
#[serial]
|
||||
fn test_violation_detection() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
let collector = ViolationCollector::new();
|
||||
|
||||
|
||||
// Create profile allowing only project path
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -73,19 +73,31 @@ fn test_violation_detection() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Test various forbidden operations
|
||||
let test_cases = vec![
|
||||
("file_read", test_code::file_read(&test_fs.forbidden_path.join("secret.txt").to_string_lossy()), "file_read_forbidden"),
|
||||
("file_write", test_code::file_write(&test_fs.project_path.join("new.txt").to_string_lossy()), "file_write_forbidden"),
|
||||
("process_spawn", test_code::spawn_process().to_string(), "process_spawn_forbidden"),
|
||||
(
|
||||
"file_read",
|
||||
test_code::file_read(&test_fs.forbidden_path.join("secret.txt").to_string_lossy()),
|
||||
"file_read_forbidden",
|
||||
),
|
||||
(
|
||||
"file_write",
|
||||
test_code::file_write(&test_fs.project_path.join("new.txt").to_string_lossy()),
|
||||
"file_write_forbidden",
|
||||
),
|
||||
(
|
||||
"process_spawn",
|
||||
test_code::spawn_process().to_string(),
|
||||
"process_spawn_forbidden",
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
for (op_type, test_code, binary_name) in test_cases {
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary(binary_name, &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
@@ -104,7 +116,7 @@ fn test_violation_detection() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Verify violations were detected
|
||||
let violations = collector.get_violations();
|
||||
// On some platforms (like macOS), sandbox might not block all operations
|
||||
@@ -122,25 +134,25 @@ fn test_violation_detection() {
|
||||
#[serial]
|
||||
fn test_violation_patterns() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
let platform = PlatformConfig::current();
|
||||
if !platform.supports_file_read {
|
||||
eprintln!("Skipping test: file read not supported on this platform");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create profile with specific allowed paths
|
||||
let allowed_dir = test_fs.root.path().join("allowed_specific");
|
||||
std::fs::create_dir_all(&allowed_dir).expect("Failed to create allowed dir");
|
||||
|
||||
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
Operation::FileReadAll(PathPattern::Literal(allowed_dir.join("file.txt"))),
|
||||
];
|
||||
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -148,21 +160,25 @@ fn test_violation_patterns() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Test accessing different forbidden paths
|
||||
let forbidden_db_path = test_fs.forbidden_path.join("data.db").to_string_lossy().to_string();
|
||||
let forbidden_db_path = test_fs
|
||||
.forbidden_path
|
||||
.join("data.db")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let forbidden_paths = vec![
|
||||
("/etc/passwd", "system_file"),
|
||||
("/tmp/test.txt", "temp_file"),
|
||||
(forbidden_db_path.as_str(), "forbidden_db"),
|
||||
];
|
||||
|
||||
|
||||
for (path, test_name) in forbidden_paths {
|
||||
let test_code = test_code::file_read(path);
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary(test_name, &test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
&binary_path.to_string_lossy(),
|
||||
@@ -173,7 +189,10 @@ fn test_violation_patterns() {
|
||||
let status = child.wait().expect("Failed to wait for child");
|
||||
// Some platforms might not block all file access
|
||||
if status.success() {
|
||||
eprintln!("WARNING: Access to {} was allowed (possible platform limitation)", path);
|
||||
eprintln!(
|
||||
"WARNING: Access to {} was allowed (possible platform limitation)",
|
||||
path
|
||||
);
|
||||
if std::env::consts::OS == "linux" && path.starts_with("/etc") {
|
||||
panic!("Access to {} should be denied on Linux", path);
|
||||
}
|
||||
@@ -191,15 +210,15 @@ fn test_violation_patterns() {
|
||||
#[serial]
|
||||
fn test_multiple_violations_sequence() {
|
||||
skip_if_unsupported!();
|
||||
|
||||
|
||||
// Create test file system
|
||||
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
||||
|
||||
|
||||
// Create minimal profile
|
||||
let operations = vec![
|
||||
Operation::FileReadAll(PathPattern::Subpath(test_fs.project_path.clone())),
|
||||
];
|
||||
|
||||
let operations = vec![Operation::FileReadAll(PathPattern::Subpath(
|
||||
test_fs.project_path.clone(),
|
||||
))];
|
||||
|
||||
let profile = match Profile::new(operations) {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
@@ -207,7 +226,7 @@ fn test_multiple_violations_sequence() {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Create test binary that attempts multiple forbidden operations
|
||||
let test_code = r#"
|
||||
use std::fs;
|
||||
@@ -249,11 +268,11 @@ fn main() {{
|
||||
}}
|
||||
}}
|
||||
"#;
|
||||
|
||||
|
||||
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let binary_path = create_test_binary("test_multi_violations", test_code, binary_dir.path())
|
||||
.expect("Failed to create test binary");
|
||||
|
||||
|
||||
// Execute in sandbox
|
||||
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
||||
match executor.execute_sandboxed_spawn(
|
||||
@@ -275,4 +294,4 @@ fn main() {{
|
||||
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
//! Comprehensive test suite for sandbox functionality
|
||||
//!
|
||||
//!
|
||||
//! This test suite validates the sandboxing capabilities across different platforms,
|
||||
//! ensuring that security policies are correctly enforced.
|
||||
|
||||
@@ -14,4 +14,4 @@ pub mod unit;
|
||||
pub mod integration;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod e2e;
|
||||
pub mod e2e;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
//! Unit tests for SandboxExecutor
|
||||
use claudia_lib::sandbox::executor::{SandboxExecutor, should_activate_sandbox};
|
||||
use gaol::profile::{Profile, Operation, PathPattern, AddressPattern};
|
||||
use claudia_lib::sandbox::executor::{should_activate_sandbox, SandboxExecutor};
|
||||
use gaol::profile::{AddressPattern, Operation, PathPattern, Profile};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -10,7 +10,7 @@ fn create_test_profile(project_path: PathBuf) -> Profile {
|
||||
Operation::FileReadAll(PathPattern::Subpath(project_path)),
|
||||
Operation::NetworkOutbound(AddressPattern::All),
|
||||
];
|
||||
|
||||
|
||||
Profile::new(operations).expect("Failed to create test profile")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ fn create_test_profile(project_path: PathBuf) -> Profile {
|
||||
fn test_executor_creation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
|
||||
|
||||
let _executor = SandboxExecutor::new(profile, project_path);
|
||||
// Executor should be created successfully
|
||||
}
|
||||
@@ -27,16 +27,25 @@ fn test_executor_creation() {
|
||||
fn test_should_activate_sandbox_env_var() {
|
||||
// Test when env var is not set
|
||||
env::remove_var("GAOL_SANDBOX_ACTIVE");
|
||||
assert!(!should_activate_sandbox(), "Should not activate when env var is not set");
|
||||
|
||||
assert!(
|
||||
!should_activate_sandbox(),
|
||||
"Should not activate when env var is not set"
|
||||
);
|
||||
|
||||
// Test when env var is set to "1"
|
||||
env::set_var("GAOL_SANDBOX_ACTIVE", "1");
|
||||
assert!(should_activate_sandbox(), "Should activate when env var is '1'");
|
||||
|
||||
assert!(
|
||||
should_activate_sandbox(),
|
||||
"Should activate when env var is '1'"
|
||||
);
|
||||
|
||||
// Test when env var is set to other value
|
||||
env::set_var("GAOL_SANDBOX_ACTIVE", "0");
|
||||
assert!(!should_activate_sandbox(), "Should not activate when env var is not '1'");
|
||||
|
||||
assert!(
|
||||
!should_activate_sandbox(),
|
||||
"Should not activate when env var is not '1'"
|
||||
);
|
||||
|
||||
// Clean up
|
||||
env::remove_var("GAOL_SANDBOX_ACTIVE");
|
||||
}
|
||||
@@ -46,9 +55,9 @@ fn test_prepare_sandboxed_command() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["hello"], &project_path);
|
||||
|
||||
|
||||
// The command should have sandbox environment variables set
|
||||
// Note: We can't easily test Command internals, but we can verify it doesn't panic
|
||||
}
|
||||
@@ -57,10 +66,10 @@ fn test_prepare_sandboxed_command() {
|
||||
fn test_executor_with_empty_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = Profile::new(vec![]).expect("Failed to create empty profile");
|
||||
|
||||
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
let _cmd = executor.prepare_sandboxed_command("echo", &["test"], &project_path);
|
||||
|
||||
|
||||
// Should handle empty profile gracefully
|
||||
}
|
||||
|
||||
@@ -76,15 +85,16 @@ fn test_executor_with_complex_profile() {
|
||||
Operation::NetworkOutbound(AddressPattern::Tcp(443)),
|
||||
Operation::SystemInfoRead,
|
||||
];
|
||||
|
||||
|
||||
// Only create profile with supported operations
|
||||
let filtered_ops: Vec<_> = operations.into_iter()
|
||||
let filtered_ops: Vec<_> = operations
|
||||
.into_iter()
|
||||
.filter(|op| {
|
||||
use gaol::profile::{OperationSupport, OperationSupportLevel};
|
||||
matches!(op.support(), OperationSupportLevel::CanBeAllowed)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
||||
if !filtered_ops.is_empty() {
|
||||
let profile = Profile::new(filtered_ops).expect("Failed to create complex profile");
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
@@ -97,12 +107,12 @@ fn test_command_environment_setup() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
|
||||
// Test with various arguments
|
||||
let _cmd1 = executor.prepare_sandboxed_command("ls", &[], &project_path);
|
||||
let _cmd2 = executor.prepare_sandboxed_command("cat", &["file.txt"], &project_path);
|
||||
let _cmd3 = executor.prepare_sandboxed_command("grep", &["-r", "pattern", "."], &project_path);
|
||||
|
||||
|
||||
// Commands should be prepared without panic
|
||||
}
|
||||
|
||||
@@ -110,18 +120,18 @@ fn test_command_environment_setup() {
|
||||
#[cfg(unix)]
|
||||
fn test_spawn_sandboxed_process() {
|
||||
use crate::sandbox::common::is_sandboxing_supported;
|
||||
|
||||
|
||||
if !is_sandboxing_supported() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let project_path = env::current_dir().unwrap_or_else(|_| PathBuf::from("/tmp"));
|
||||
let profile = create_test_profile(project_path.clone());
|
||||
let executor = SandboxExecutor::new(profile, project_path.clone());
|
||||
|
||||
|
||||
// Try to spawn a simple command
|
||||
let result = executor.execute_sandboxed_spawn("echo", &["sandbox test"], &project_path);
|
||||
|
||||
|
||||
// On supported platforms, this should either succeed or fail gracefully
|
||||
match result {
|
||||
Ok(mut child) => {
|
||||
@@ -133,4 +143,4 @@ fn test_spawn_sandboxed_process() {
|
||||
println!("Sandbox spawn failed (expected in some environments): {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
//! Unit tests for sandbox components
|
||||
#[cfg(test)]
|
||||
mod profile_builder;
|
||||
mod executor;
|
||||
#[cfg(test)]
|
||||
mod platform;
|
||||
#[cfg(test)]
|
||||
mod executor;
|
||||
mod profile_builder;
|
||||
|
@@ -1,13 +1,13 @@
|
||||
//! Unit tests for platform capabilities
|
||||
use claudia_lib::sandbox::platform::{get_platform_capabilities, is_sandboxing_available};
|
||||
use std::env;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_sandboxing_availability() {
|
||||
let is_available = is_sandboxing_available();
|
||||
let expected = matches!(env::consts::OS, "linux" | "macos" | "freebsd");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
is_available, expected,
|
||||
"Sandboxing availability should match platform support"
|
||||
@@ -17,44 +17,59 @@ fn test_sandboxing_availability() {
|
||||
#[test]
|
||||
fn test_platform_capabilities_structure() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
|
||||
// Verify basic structure
|
||||
assert_eq!(caps.os, env::consts::OS, "OS should match current platform");
|
||||
assert!(!caps.operations.is_empty() || !caps.sandboxing_supported,
|
||||
"Should have operations if sandboxing is supported");
|
||||
assert!(!caps.notes.is_empty(), "Should have platform-specific notes");
|
||||
assert!(
|
||||
!caps.operations.is_empty() || !caps.sandboxing_supported,
|
||||
"Should have operations if sandboxing is supported"
|
||||
);
|
||||
assert!(
|
||||
!caps.notes.is_empty(),
|
||||
"Should have platform-specific notes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "linux")]
|
||||
fn test_linux_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
|
||||
assert_eq!(caps.os, "linux");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
|
||||
// Verify Linux-specific capabilities
|
||||
let file_read = caps.operations.iter()
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "can_be_allowed");
|
||||
|
||||
let metadata_read = caps.operations.iter()
|
||||
|
||||
let metadata_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_metadata")
|
||||
.expect("file_read_metadata should be present");
|
||||
assert_eq!(metadata_read.support_level, "cannot_be_precisely");
|
||||
|
||||
let network_all = caps.operations.iter()
|
||||
|
||||
let network_all = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_all")
|
||||
.expect("network_outbound_all should be present");
|
||||
assert_eq!(network_all.support_level, "can_be_allowed");
|
||||
|
||||
let network_tcp = caps.operations.iter()
|
||||
|
||||
let network_tcp = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_tcp")
|
||||
.expect("network_outbound_tcp should be present");
|
||||
assert_eq!(network_tcp.support_level, "cannot_be_precisely");
|
||||
|
||||
let system_info = caps.operations.iter()
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "never");
|
||||
@@ -64,27 +79,35 @@ fn test_linux_capabilities() {
|
||||
#[cfg(target_os = "macos")]
|
||||
fn test_macos_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
|
||||
assert_eq!(caps.os, "macos");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
|
||||
// Verify macOS-specific capabilities
|
||||
let file_read = caps.operations.iter()
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "can_be_allowed");
|
||||
|
||||
let metadata_read = caps.operations.iter()
|
||||
|
||||
let metadata_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_metadata")
|
||||
.expect("file_read_metadata should be present");
|
||||
assert_eq!(metadata_read.support_level, "can_be_allowed");
|
||||
|
||||
let network_tcp = caps.operations.iter()
|
||||
|
||||
let network_tcp = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "network_outbound_tcp")
|
||||
.expect("network_outbound_tcp should be present");
|
||||
assert_eq!(network_tcp.support_level, "can_be_allowed");
|
||||
|
||||
let system_info = caps.operations.iter()
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "can_be_allowed");
|
||||
@@ -94,17 +117,21 @@ fn test_macos_capabilities() {
|
||||
#[cfg(target_os = "freebsd")]
|
||||
fn test_freebsd_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
|
||||
assert_eq!(caps.os, "freebsd");
|
||||
assert!(caps.sandboxing_supported);
|
||||
|
||||
|
||||
// Verify FreeBSD-specific capabilities
|
||||
let file_read = caps.operations.iter()
|
||||
let file_read = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "file_read_all")
|
||||
.expect("file_read_all should be present");
|
||||
assert_eq!(file_read.support_level, "never");
|
||||
|
||||
let system_info = caps.operations.iter()
|
||||
|
||||
let system_info = caps
|
||||
.operations
|
||||
.iter()
|
||||
.find(|op| op.operation == "system_info_read")
|
||||
.expect("system_info_read should be present");
|
||||
assert_eq!(system_info.support_level, "always");
|
||||
@@ -114,7 +141,7 @@ fn test_freebsd_capabilities() {
|
||||
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd")))]
|
||||
fn test_unsupported_platform_capabilities() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
|
||||
assert!(!caps.sandboxing_supported);
|
||||
assert_eq!(caps.operations.len(), 0);
|
||||
assert!(caps.notes.iter().any(|note| note.contains("not supported")));
|
||||
@@ -123,12 +150,18 @@ fn test_unsupported_platform_capabilities() {
|
||||
#[test]
|
||||
fn test_all_operations_have_descriptions() {
|
||||
let caps = get_platform_capabilities();
|
||||
|
||||
|
||||
for op in &caps.operations {
|
||||
assert!(!op.description.is_empty(),
|
||||
"Operation {} should have a description", op.operation);
|
||||
assert!(!op.support_level.is_empty(),
|
||||
"Operation {} should have a support level", op.operation);
|
||||
assert!(
|
||||
!op.description.is_empty(),
|
||||
"Operation {} should have a description",
|
||||
op.operation
|
||||
);
|
||||
assert!(
|
||||
!op.support_level.is_empty(),
|
||||
"Operation {} should have a support level",
|
||||
op.operation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +169,7 @@ fn test_all_operations_have_descriptions() {
|
||||
fn test_support_level_values() {
|
||||
let caps = get_platform_capabilities();
|
||||
let valid_levels = ["never", "can_be_allowed", "cannot_be_precisely", "always"];
|
||||
|
||||
|
||||
for op in &caps.operations {
|
||||
assert!(
|
||||
valid_levels.contains(&op.support_level.as_str()),
|
||||
@@ -145,4 +178,4 @@ fn test_support_level_values() {
|
||||
op.support_level
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,8 +18,7 @@ fn make_rule(
|
||||
pattern_value: pattern_value.to_string(),
|
||||
enabled: true,
|
||||
platform_support: platforms.map(|p| {
|
||||
serde_json::to_string(&p.iter().map(|s| s.to_string()).collect::<Vec<_>>())
|
||||
.unwrap()
|
||||
serde_json::to_string(&p.iter().map(|s| s.to_string()).collect::<Vec<_>>()).unwrap()
|
||||
}),
|
||||
created_at: String::new(),
|
||||
}
|
||||
@@ -29,34 +28,53 @@ fn make_rule(
|
||||
fn test_profile_builder_creation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone());
|
||||
|
||||
assert!(builder.is_ok(), "ProfileBuilder should be created successfully");
|
||||
|
||||
assert!(
|
||||
builder.is_ok(),
|
||||
"ProfileBuilder should be created successfully"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_rules_creates_empty_profile() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
|
||||
let profile = builder.build_profile(vec![]);
|
||||
assert!(profile.is_ok(), "Empty rules should create valid empty profile");
|
||||
assert!(
|
||||
profile.is_ok(),
|
||||
"Empty rules should create valid empty profile"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_read_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
|
||||
let rules = vec![
|
||||
make_rule("file_read_all", "literal", "/usr/lib/test.so", Some(&["linux", "macos"])),
|
||||
make_rule("file_read_all", "subpath", "/usr/lib", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"literal",
|
||||
"/usr/lib/test.so",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
|
||||
// Profile creation might fail on unsupported platforms, but parsing should work
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(_profile.is_ok(), "File read rules should be parsed on supported platforms");
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"File read rules should be parsed on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,17 +82,25 @@ fn test_file_read_rule_parsing() {
|
||||
fn test_network_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
|
||||
let rules = vec![
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("network_outbound", "tcp", "8080", Some(&["macos"])),
|
||||
make_rule("network_outbound", "local_socket", "/tmp/socket", Some(&["macos"])),
|
||||
make_rule(
|
||||
"network_outbound",
|
||||
"local_socket",
|
||||
"/tmp/socket",
|
||||
Some(&["macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(_profile.is_ok(), "Network rules should be parsed on supported platforms");
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"Network rules should be parsed on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +108,16 @@ fn test_network_rule_parsing() {
|
||||
fn test_system_info_rule_parsing() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule("system_info_read", "all", "", Some(&["macos"])),
|
||||
];
|
||||
|
||||
|
||||
let rules = vec![make_rule("system_info_read", "all", "", Some(&["macos"]))];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
|
||||
if std::env::consts::OS == "macos" {
|
||||
assert!(_profile.is_ok(), "System info rule should be parsed on macOS");
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"System info rule should be parsed on macOS"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,12 +125,22 @@ fn test_system_info_rule_parsing() {
|
||||
fn test_template_variable_replacement() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
|
||||
let rules = vec![
|
||||
make_rule("file_read_all", "subpath", "{{PROJECT_PATH}}/src", Some(&["linux", "macos"])),
|
||||
make_rule("file_read_all", "subpath", "{{HOME}}/.config", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}/src",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{HOME}}/.config",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// We can't easily verify the exact paths without inspecting the Profile internals,
|
||||
// but this test ensures template replacement doesn't panic
|
||||
@@ -113,10 +150,15 @@ fn test_template_variable_replacement() {
|
||||
fn test_disabled_rules_are_ignored() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let mut rule = make_rule("file_read_all", "subpath", "/usr/lib", Some(&["linux", "macos"]));
|
||||
|
||||
let mut rule = make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
);
|
||||
rule.enabled = false;
|
||||
|
||||
|
||||
let profile = builder.build_profile(vec![rule]);
|
||||
assert!(profile.is_ok(), "Disabled rules should be ignored");
|
||||
}
|
||||
@@ -125,21 +167,30 @@ fn test_disabled_rules_are_ignored() {
|
||||
fn test_platform_filtering() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
|
||||
let current_os = std::env::consts::OS;
|
||||
let other_os = if current_os == "linux" { "macos" } else { "linux" };
|
||||
|
||||
let other_os = if current_os == "linux" {
|
||||
"macos"
|
||||
} else {
|
||||
"linux"
|
||||
};
|
||||
|
||||
let rules = vec![
|
||||
// Rule for current platform
|
||||
make_rule("file_read_all", "subpath", "/test1", Some(&[current_os])),
|
||||
// Rule for other platform
|
||||
make_rule("file_read_all", "subpath", "/test2", Some(&[other_os])),
|
||||
// Rule for both platforms
|
||||
make_rule("file_read_all", "subpath", "/test3", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/test3",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
// Rule with no platform specification (should be included)
|
||||
make_rule("file_read_all", "subpath", "/test4", None),
|
||||
];
|
||||
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Rules for other platforms should be filtered out
|
||||
}
|
||||
@@ -148,11 +199,14 @@ fn test_platform_filtering() {
|
||||
fn test_invalid_operation_type() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule("invalid_operation", "subpath", "/test", Some(&["linux", "macos"])),
|
||||
];
|
||||
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"invalid_operation",
|
||||
"subpath",
|
||||
"/test",
|
||||
Some(&["linux", "macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
assert!(_profile.is_ok(), "Invalid operations should be skipped");
|
||||
}
|
||||
@@ -161,11 +215,14 @@ fn test_invalid_operation_type() {
|
||||
fn test_invalid_pattern_type() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule("file_read_all", "invalid_pattern", "/test", Some(&["linux", "macos"])),
|
||||
];
|
||||
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"file_read_all",
|
||||
"invalid_pattern",
|
||||
"/test",
|
||||
Some(&["linux", "macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Should either skip the rule or fail gracefully
|
||||
}
|
||||
@@ -174,11 +231,14 @@ fn test_invalid_pattern_type() {
|
||||
fn test_invalid_tcp_port() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
let rules = vec![
|
||||
make_rule("network_outbound", "tcp", "not_a_number", Some(&["macos"])),
|
||||
];
|
||||
|
||||
|
||||
let rules = vec![make_rule(
|
||||
"network_outbound",
|
||||
"tcp",
|
||||
"not_a_number",
|
||||
Some(&["macos"]),
|
||||
)];
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Should handle invalid port gracefully
|
||||
}
|
||||
@@ -188,13 +248,12 @@ fn test_invalid_tcp_port() {
|
||||
#[test_case("network_outbound", "all", "" ; "network all operation")]
|
||||
#[test_case("system_info_read", "all", "" ; "system info operation")]
|
||||
fn test_operation_support_level(operation_type: &str, pattern_type: &str, pattern_value: &str) {
|
||||
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
|
||||
let rule = make_rule(operation_type, pattern_type, pattern_value, None);
|
||||
let rules = vec![rule];
|
||||
|
||||
|
||||
match builder.build_profile(rules) {
|
||||
Ok(_) => {
|
||||
// Profile created successfully - operation is supported
|
||||
@@ -211,27 +270,43 @@ fn test_operation_support_level(operation_type: &str, pattern_type: &str, patter
|
||||
fn test_complex_profile_with_multiple_rules() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path.clone()).unwrap();
|
||||
|
||||
|
||||
let rules = vec![
|
||||
// File operations
|
||||
make_rule("file_read_all", "subpath", "{{PROJECT_PATH}}", Some(&["linux", "macos"])),
|
||||
make_rule("file_read_all", "subpath", "/usr/lib", Some(&["linux", "macos"])),
|
||||
make_rule("file_read_all", "literal", "/etc/hosts", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"{{PROJECT_PATH}}",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/usr/lib",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"literal",
|
||||
"/etc/hosts",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule("file_read_metadata", "subpath", "/", Some(&["macos"])),
|
||||
|
||||
// Network operations
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("network_outbound", "tcp", "443", Some(&["macos"])),
|
||||
make_rule("network_outbound", "tcp", "80", Some(&["macos"])),
|
||||
|
||||
// System info
|
||||
make_rule("system_info_read", "all", "", Some(&["macos"])),
|
||||
];
|
||||
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
|
||||
|
||||
if std::env::consts::OS == "linux" || std::env::consts::OS == "macos" {
|
||||
assert!(_profile.is_ok(), "Complex profile should be created on supported platforms");
|
||||
assert!(
|
||||
_profile.is_ok(),
|
||||
"Complex profile should be created on supported platforms"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,14 +314,24 @@ fn test_complex_profile_with_multiple_rules() {
|
||||
fn test_rule_order_preservation() {
|
||||
let project_path = PathBuf::from("/test/project");
|
||||
let builder = ProfileBuilder::new(project_path).unwrap();
|
||||
|
||||
|
||||
// Create rules with specific order
|
||||
let rules = vec![
|
||||
make_rule("file_read_all", "subpath", "/first", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/first",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
make_rule("network_outbound", "all", "", Some(&["linux", "macos"])),
|
||||
make_rule("file_read_all", "subpath", "/second", Some(&["linux", "macos"])),
|
||||
make_rule(
|
||||
"file_read_all",
|
||||
"subpath",
|
||||
"/second",
|
||||
Some(&["linux", "macos"]),
|
||||
),
|
||||
];
|
||||
|
||||
|
||||
let _profile = builder.build_profile(rules);
|
||||
// Order should be preserved in the resulting profile
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user