network_isolation_property_test.rs
1 use ferris_proof_plugins::sandbox::{NetworkPolicy, ResourceLimits, SandboxedExecutor}; 2 use proptest::prelude::*; 3 use std::collections::HashMap; 4 use std::path::PathBuf; 5 use std::time::Duration; 6 use tokio::runtime::Runtime; 7 8 /// **Feature: ferris-proof, Property 11: Network isolation** 9 /// **Validates: Requirements 12.1** 10 /// 11 /// For any verification execution without explicit external service consent, 12 /// the system should not initiate any network connections to external services. 13 #[cfg(test)] 14 mod network_isolation_tests { 15 use super::*; 16 17 proptest! { 18 #[test] 19 /// Property test for network isolation enforcement 20 /// Tests that commands attempting network access are properly blocked 21 /// when network policy is set to Denied 22 fn network_isolation_blocks_external_connections( 23 // Generate various network-related commands that should be blocked 24 network_command in prop::sample::select(vec![ 25 ("curl", vec!["https://example.com".to_string()]), 26 ("wget", vec!["http://example.com/file.txt".to_string()]), 27 ("ping", vec!["8.8.8.8".to_string()]), 28 ("nslookup", vec!["example.com".to_string()]), 29 ("nc", vec!["-z".to_string(), "example.com".to_string(), "80".to_string()]), 30 ]), 31 // Generate different working directories 32 working_dir in prop::option::of( 33 prop::sample::select(vec![ 34 PathBuf::from("/tmp"), 35 PathBuf::from("."), 36 PathBuf::from("/var/tmp"), 37 ]) 38 ), 39 // Generate various environment variables 40 env_vars in prop::collection::hash_map( 41 "[A-Z_]{1,10}", 42 "[a-zA-Z0-9_/.-]{0,50}", 43 0..5 44 ) 45 ) { 46 let rt = Runtime::new().unwrap(); 47 48 rt.block_on(async { 49 // Create sandboxed executor with network access denied 50 let executor = SandboxedExecutor::new() 51 .with_network_policy(NetworkPolicy::Denied) 52 .with_limits(ResourceLimits { 53 max_memory: 100 * 1024 * 1024, // 100MB 54 max_cpu_time: 10, // 10 seconds 55 max_file_descriptors: 64, 56 max_processes: 2, 57 max_file_size: 1024 * 1024, // 1MB 58 }) 59 .with_timeout(Duration::from_secs(5)); 60 61 let (command, args) = network_command; 62 let args_str: Vec<&str> = args.iter().map(|s| s.as_ref()).collect(); 63 64 // Execute the network command 65 let result = executor.execute( 66 command, 67 &args_str, 68 env_vars, 69 working_dir.as_ref() 70 ).await; 71 72 match result { 73 Ok(output) => { 74 // If command succeeded, it should not have made external connections 75 let network_blocked = 76 output.exit_code != 0 || // Command failed 77 output.stderr.contains("Connection refused") || 78 output.stderr.contains("Network is unreachable") || 79 output.stderr.contains("Name or service not known") || 80 output.stderr.contains("Temporary failure in name resolution") || 81 output.stderr.contains("No route to host") || 82 output.stdout.is_empty(); // No successful output 83 84 prop_assert!( 85 network_blocked, 86 "Network command '{}' should have been blocked or failed, but got exit_code: {}, stdout: '{}', stderr: '{}'", 87 command, 88 output.exit_code, 89 output.stdout.trim(), 90 output.stderr.trim() 91 ); 92 } 93 Err(e) => { 94 // Command was blocked at validation level - this is expected and good 95 let error_msg = e.to_string(); 96 let expected_blocking = 97 error_msg.contains("not allowed in sandbox") || 98 error_msg.contains("Network access denied") || 99 error_msg.contains("Command") && error_msg.contains("not allowed"); 100 101 if !expected_blocking { 102 // If it's not a security-related error, it might be a system issue 103 // (e.g., command not found), which is acceptable 104 prop_assume!( 105 error_msg.contains("not found") || 106 error_msg.contains("No such file") || 107 error_msg.contains("Failed to spawn process") 108 ); 109 } 110 } 111 } 112 113 Ok(()) 114 })?; 115 } 116 117 #[test] 118 /// Property test for unrestricted network access with consent 119 /// Tests that network access works when explicitly granted 120 fn unrestricted_network_with_consent_allows_access( 121 user_consent in any::<bool>(), 122 command in prop::sample::select(vec!["echo", "true", "false"]), // Safe commands 123 ) { 124 let rt = Runtime::new().unwrap(); 125 126 rt.block_on(async { 127 let executor = SandboxedExecutor::new() 128 .with_network_policy(NetworkPolicy::Unrestricted { user_consent }) 129 .with_limits(ResourceLimits::default()) 130 .with_timeout(Duration::from_secs(2)); 131 132 let result = executor.execute( 133 command, 134 &[], 135 HashMap::new(), 136 None 137 ).await; 138 139 if user_consent { 140 // With consent, safe commands should execute successfully 141 prop_assert!( 142 result.is_ok(), 143 "Command '{}' should succeed with user consent, but got error: {:?}", 144 command, 145 result.err() 146 ); 147 } else { 148 // Without consent, should be blocked 149 match result { 150 Ok(_) => { 151 // This might happen for very safe commands that don't trigger network checks 152 // This is acceptable as long as no actual network access occurs 153 } 154 Err(e) => { 155 let error_msg = e.to_string(); 156 prop_assert!( 157 error_msg.contains("requires") && error_msg.contains("consent"), 158 "Expected consent error without user consent, got: {}", 159 error_msg 160 ); 161 } 162 } 163 } 164 165 Ok(()) 166 })?; 167 } 168 } 169 170 #[tokio::test] 171 /// Integration test for network isolation with real network commands 172 /// This test uses actual network commands to verify isolation works 173 async fn test_network_isolation_integration() { 174 let executor = SandboxedExecutor::new() 175 .with_network_policy(NetworkPolicy::Denied) 176 .with_limits(ResourceLimits { 177 max_memory: 50 * 1024 * 1024, // 50MB 178 max_cpu_time: 5, // 5 seconds 179 max_file_descriptors: 32, 180 max_processes: 1, 181 max_file_size: 1024, // 1KB 182 }) 183 .with_timeout(Duration::from_secs(3)); 184 185 // Test that curl is blocked 186 let result = executor 187 .execute( 188 "curl", 189 &["--connect-timeout", "1", "https://httpbin.org/get"], 190 HashMap::new(), 191 None, 192 ) 193 .await; 194 195 match result { 196 Ok(output) => { 197 // Command executed but should have failed due to network restrictions 198 assert_ne!( 199 output.exit_code, 0, 200 "curl should fail when network is denied" 201 ); 202 203 // Check for network-related error messages 204 let has_network_error = output.stderr.contains("Could not resolve host") 205 || output.stderr.contains("Connection refused") 206 || output.stderr.contains("Network is unreachable") 207 || output 208 .stderr 209 .contains("Temporary failure in name resolution"); 210 211 assert!( 212 has_network_error || output.stdout.is_empty(), 213 "Expected network error or empty output, got stdout: '{}', stderr: '{}'", 214 output.stdout, 215 output.stderr 216 ); 217 } 218 Err(e) => { 219 // Command was blocked at validation level - this is also acceptable 220 let error_msg = e.to_string(); 221 assert!( 222 error_msg.contains("not allowed") 223 || error_msg.contains("Network access denied") 224 || error_msg.contains("not found"), // curl might not be installed 225 "Unexpected error: {}", 226 error_msg 227 ); 228 } 229 } 230 } 231 232 #[tokio::test] 233 /// Test that environment variables properly restrict network access 234 async fn test_environment_variable_network_restrictions() { 235 let executor = SandboxedExecutor::new() 236 .with_network_policy(NetworkPolicy::Denied) 237 .with_limits(ResourceLimits::default()) 238 .with_timeout(Duration::from_secs(2)); 239 240 // Test with a command that checks environment variables 241 let result = executor.execute("env", &[], HashMap::new(), None).await; 242 243 if let Ok(output) = result { 244 let env_output = output.stdout; 245 246 // Check that network-restricting environment variables are set 247 assert!( 248 env_output.contains("NO_PROXY=*") || env_output.contains("no_proxy=*"), 249 "Network-restricting environment variables should be set" 250 ); 251 252 // Check that potentially dangerous proxy variables are not set 253 assert!( 254 !env_output.contains("HTTP_PROXY=") 255 || !env_output.contains("HTTPS_PROXY=") 256 || env_output.contains("NO_PROXY=*"), 257 "Proxy variables should be cleared or NO_PROXY should be set" 258 ); 259 } 260 } 261 }