/ ferris-proof-plugins / tests / network_isolation_property_test.rs
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  }