use std::path::Path;
fn create_capability_landlock_config(
_capability: &str,
allowed_paths: &[String],
workdir: &Path,
) -> LandlockConfig {
let mut rules = Vec::new();
rules.push(LandlockRule {
path: workdir.to_string_lossy().to_string(),
access: LandlockAccess::Read | LandlockAccess::Write,
});
for path in allowed_paths {
rules.push(LandlockRule {
path: path.clone(),
access: LandlockAccess::Read | LandlockAccess::Write,
});
}
LandlockConfig {
enabled: true,
rules,
}
}
fn create_capability_seccomp_config(capability: &str) -> SeccompConfig {
let allowed_syscalls = match capability {
"fs.read.v1" => vec![
libc::SYS_read as i32,
libc::SYS_open as i32,
libc::SYS_openat as i32,
libc::SYS_fstat as i32,
libc::SYS_lseek as i32,
libc::SYS_close as i32,
libc::SYS_mmap as i32,
libc::SYS_munmap as i32,
libc::SYS_brk as i32,
libc::SYS_exit_group as i32,
],
"http.fetch.v1" => vec![
libc::SYS_read as i32,
libc::SYS_write as i32,
libc::SYS_socket as i32,
libc::SYS_connect as i32,
libc::SYS_sendto as i32,
libc::SYS_recvfrom as i32,
libc::SYS_poll as i32,
libc::SYS_epoll_create as i32,
libc::SYS_epoll_ctl as i32,
libc::SYS_epoll_wait as i32,
libc::SYS_close as i32,
libc::SYS_mmap as i32,
libc::SYS_munmap as i32,
libc::SYS_brk as i32,
libc::SYS_exit_group as i32,
],
_ => vec![
libc::SYS_read as i32,
libc::SYS_write as i32,
libc::SYS_exit_group as i32,
],
};
SeccompConfig {
enabled: true,
allowed_syscalls,
action: SeccompAction::Allow,
}
}
#[derive(Debug, Clone)]
pub struct LandlockConfig {
pub enabled: bool,
pub rules: Vec<LandlockRule>,
}
#[derive(Debug, Clone)]
pub struct LandlockRule {
pub path: String,
pub access: LandlockAccess,
}
bitflags::bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct LandlockAccess: u32 {
const Read = 1 << 0;
const Write = 1 << 1;
const Execute = 1 << 2;
}
}
#[derive(Debug, Clone)]
pub struct SeccompConfig {
pub enabled: bool,
pub allowed_syscalls: Vec<i32>,
pub action: SeccompAction,
}
#[derive(Debug, Clone)]
pub enum SeccompAction {
Allow,
Kill,
Trap,
}
use anyhow::Result;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};
use tempfile::{tempdir, TempDir};
use tracing::debug;
use uuid::Uuid;
pub struct SecurityIntegrationTestEnvironment {
pub workdir: TempDir,
pub allowed_dir: TempDir,
pub forbidden_dir: TempDir,
pub capability_bundle: PathBuf,
pub test_files: HashMap<String, PathBuf>,
pub test_intents: HashMap<String, Value>,
}
impl SecurityIntegrationTestEnvironment {
pub fn new() -> Result<Self> {
let workdir = tempdir()?;
let allowed_dir = tempdir()?;
let forbidden_dir = tempdir()?;
let capability_bundle = workdir.path().join("capability_bundle.json");
Self::create_test_capability_bundle(&capability_bundle, &allowed_dir)?;
let mut test_files = HashMap::new();
let safe_file = allowed_dir.path().join("safe.txt");
File::create(&safe_file)?.write_all(b"safe content")?;
test_files.insert("safe".to_string(), safe_file);
let large_safe_file = allowed_dir.path().join("large_safe.txt");
File::create(&large_safe_file)?.write_all(&vec![b'X'; 1024])?;
test_files.insert("large_safe".to_string(), large_safe_file);
let forbidden_file = forbidden_dir.path().join("forbidden.txt");
File::create(&forbidden_file)?.write_all(b"forbidden content")?;
test_files.insert("forbidden".to_string(), forbidden_file);
let binary_file = allowed_dir.path().join("binary.bin");
File::create(&binary_file)?.write_all(&[0, 1, 2, 255, 254, 253])?;
test_files.insert("binary".to_string(), binary_file);
let test_intents = Self::create_test_intents(&test_files, &allowed_dir)?;
Ok(Self {
workdir,
allowed_dir,
forbidden_dir,
capability_bundle,
test_files,
test_intents,
})
}
fn create_test_capability_bundle(path: &Path, allowed_dir: &TempDir) -> Result<()> {
let policy = json!({
"version": "1.0",
"policies": {
"fs.read.v1": {
"allowed_paths": [allowed_dir.path().to_string_lossy()],
"max_file_size": 1048576,
"allowed_extensions": [".txt", ".json", ".log"],
"rate_limit": {
"requests_per_minute": 60,
"burst_size": 10
}
},
"http.fetch.v1": {
"allowed_domains": [
"httpbin.org",
"api.github.com",
"jsonplaceholder.typicode.com"
],
"allowed_methods": ["GET", "POST", "PUT", "DELETE"],
"max_response_size": 1048576,
"timeout_seconds": 30,
"rate_limit": {
"requests_per_minute": 30,
"burst_size": 5
}
},
"sqlite.query.v1": {
"allowed_operations": ["SELECT", "INSERT", "UPDATE"],
"max_query_complexity": 100,
"max_result_rows": 1000,
"rate_limit": {
"requests_per_minute": 120,
"burst_size": 20
}
}
},
"security": {
"landlock_enabled": true,
"seccomp_enabled": true,
"namespace_isolation": true,
"resource_limits": {
"max_memory_mb": 256,
"max_cpu_seconds": 30,
"max_file_descriptors": 100
}
}
});
let mut file = File::create(path)?;
file.write_all(serde_json::to_string_pretty(&policy)?.as_bytes())?;
Ok(())
}
fn create_test_intents(
test_files: &HashMap<String, PathBuf>,
allowed_dir: &TempDir,
) -> Result<HashMap<String, Value>> {
let mut intents = HashMap::new();
intents.insert(
"safe_fs_read".to_string(),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "fs.read.v1",
"params": {
"path": test_files["safe"].to_string_lossy(),
"max_size": 1024
}
}),
);
intents.insert(
"dangerous_fs_read".to_string(),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "fs.read.v1",
"params": {
"path": format!("{}/../../../etc/passwd", allowed_dir.path().display()),
"max_size": 1024
}
}),
);
intents.insert(
"safe_http_fetch".to_string(),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "http.fetch.v1",
"params": {
"url": "https://httpbin.org/get",
"method": "GET",
"timeout_seconds": 30
}
}),
);
intents.insert(
"dangerous_http_fetch".to_string(),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "http.fetch.v1",
"params": {
"url": "http://127.0.0.1:8080/admin",
"method": "GET",
"timeout_seconds": 30
}
}),
);
intents.insert(
"metadata_http_fetch".to_string(),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "http.fetch.v1",
"params": {
"url": "http://169.254.169.254/latest/meta-data/",
"method": "GET",
"timeout_seconds": 10
}
}),
);
intents.insert(
"oversized_fs_read".to_string(),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "fs.read.v1",
"params": {
"path": test_files["safe"].to_string_lossy(),
"max_size": 10_000_000 }
}),
);
Ok(intents)
}
}
#[derive(Debug, Clone)]
pub struct SecurityTestResult {
pub test_name: String,
pub intent_id: String,
pub expected_outcome: SecurityTestOutcome,
pub actual_outcome: SecurityTestOutcome,
pub execution_time: Duration,
pub error_message: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SecurityTestOutcome {
Allowed,
Blocked,
Error,
Timeout,
}
#[cfg(test)]
mod tests {
use super::test_helpers::*;
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_security_integration_environment_setup() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
assert!(env.workdir.path().exists());
assert!(env.allowed_dir.path().exists());
assert!(env.forbidden_dir.path().exists());
assert!(env.capability_bundle.exists());
assert!(env.test_files["safe"].exists());
assert!(env.test_files["forbidden"].exists());
assert!(env.test_files["binary"].exists());
assert!(env.test_intents.contains_key("safe_fs_read"));
assert!(env.test_intents.contains_key("dangerous_fs_read"));
assert!(env.test_intents.contains_key("safe_http_fetch"));
assert!(env.test_intents.contains_key("dangerous_http_fetch"));
}
#[test]
fn test_policy_enforcement_integration() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let safe_intent = &env.test_intents["safe_fs_read"];
let result = validate_intent_against_policy(safe_intent, &env.capability_bundle);
assert!(result.is_ok(), "Safe intent should pass policy validation");
let dangerous_intent = &env.test_intents["dangerous_fs_read"];
let result = validate_intent_against_policy(dangerous_intent, &env.capability_bundle);
assert!(
result.is_err(),
"Dangerous intent should fail policy validation"
);
let safe_http = &env.test_intents["safe_http_fetch"];
let result = validate_intent_against_policy(safe_http, &env.capability_bundle);
assert!(
result.is_ok(),
"Safe HTTP intent should pass policy validation"
);
let dangerous_http = &env.test_intents["dangerous_http_fetch"];
let result = validate_intent_against_policy(dangerous_http, &env.capability_bundle);
assert!(
result.is_err(),
"Dangerous HTTP intent should fail policy validation"
);
}
#[test]
fn test_landlock_isolation_integration() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let allowed_paths = vec![env.allowed_dir.path().to_string_lossy().to_string()];
let landlock_config =
create_capability_landlock_config("fs.read.v1", &allowed_paths, env.workdir.path());
assert!(landlock_config.enabled);
assert!(!landlock_config.rules.is_empty());
let rules = &landlock_config.rules;
let has_workdir = rules
.iter()
.any(|r| r.path.starts_with(env.workdir.path().to_str().unwrap()));
let has_allowed = rules
.iter()
.any(|r| r.path == env.allowed_dir.path().to_str().unwrap());
assert!(has_workdir, "Landlock should include workdir");
assert!(has_allowed, "Landlock should include allowed directory");
}
#[test]
fn test_seccomp_filtering_integration() {
let fs_read_config = create_capability_seccomp_config("fs.read.v1");
let http_fetch_config = create_capability_seccomp_config("http.fetch.v1");
assert!(fs_read_config.enabled);
assert!(http_fetch_config.enabled);
assert!(!fs_read_config.allowed_syscalls.is_empty());
assert!(!http_fetch_config.allowed_syscalls.is_empty());
let fs_syscalls = &fs_read_config.allowed_syscalls;
let http_syscalls = &http_fetch_config.allowed_syscalls;
assert!(!fs_syscalls.contains(&(libc::SYS_socket as i32)));
assert!(!fs_syscalls.contains(&(libc::SYS_connect as i32)));
assert!(http_syscalls.contains(&(libc::SYS_socket as i32)));
assert!(http_syscalls.contains(&(libc::SYS_connect as i32)));
let dangerous_syscalls = [
libc::SYS_ptrace as i32,
libc::SYS_mount as i32,
libc::SYS_umount2 as i32, libc::SYS_reboot as i32,
];
for dangerous in dangerous_syscalls {
assert!(!fs_syscalls.contains(&dangerous));
assert!(!http_syscalls.contains(&dangerous));
}
}
#[test]
fn test_complete_security_pipeline() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let safe_intent = &env.test_intents["safe_fs_read"];
let result = execute_secure_intent(safe_intent, &env);
match result {
Ok(outcome) => {
assert_eq!(outcome, SecurityTestOutcome::Allowed);
}
Err(_) => {
debug!("Safe intent execution failed in test environment (expected)");
}
}
let dangerous_intent = &env.test_intents["dangerous_fs_read"];
let result = execute_secure_intent(dangerous_intent, &env);
assert!(result.is_err(), "Dangerous intent should be blocked");
}
#[test]
fn test_concurrent_security_enforcement() {
let env = Arc::new(SecurityIntegrationTestEnvironment::new().unwrap());
let handles: Vec<_> = (0..5)
.map(|i| {
let env = Arc::clone(&env);
thread::spawn(move || {
let intent = &env.test_intents["safe_fs_read"];
let result = validate_intent_against_policy(intent, &env.capability_bundle);
(i, result.is_ok())
})
})
.collect();
for handle in handles {
let (i, success) = handle.join().unwrap();
assert!(success, "Concurrent validation {} should succeed", i);
}
let mixed_handles: Vec<_> = (0..10)
.map(|i| {
let env = Arc::clone(&env);
thread::spawn(move || {
let intent_key = if i % 2 == 0 {
"safe_fs_read"
} else {
"dangerous_fs_read"
};
let intent = &env.test_intents[intent_key];
let result = validate_intent_against_policy(intent, &env.capability_bundle);
(i, intent_key, result.is_ok())
})
})
.collect();
for handle in mixed_handles {
let (i, intent_key, success) = handle.join().unwrap();
if intent_key == "safe_fs_read" {
assert!(success, "Safe concurrent validation {} should succeed", i);
} else {
assert!(
!success,
"Dangerous concurrent validation {} should fail",
i
);
}
}
}
#[test]
fn test_resource_limit_enforcement() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let oversized_intent = &env.test_intents["oversized_fs_read"];
let result = validate_intent_against_policy(oversized_intent, &env.capability_bundle);
assert!(result.is_err(), "Oversized request should be blocked");
let timeout_intent = json!({
"id": Uuid::new_v4().to_string(),
"capability": "http.fetch.v1",
"params": {
"url": "https://httpbin.org/delay/60", "method": "GET",
"timeout_seconds": 5 }
});
let result = validate_intent_against_policy(&timeout_intent, &env.capability_bundle);
match result {
Ok(_) => debug!("Timeout intent passed policy validation"),
Err(_) => debug!("Timeout intent blocked at policy level"),
}
}
#[test]
fn test_attack_vector_prevention() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let attack_intents = vec![
json!({
"id": Uuid::new_v4().to_string(),
"capability": "fs.read.v1",
"params": {
"path": "../../../../etc/passwd",
"max_size": 1024
}
}),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "http.fetch.v1",
"params": {
"url": "http://169.254.169.254/latest/meta-data/",
"method": "GET"
}
}),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "fs.read.v1",
"params": {
"path": env.test_files["safe"].to_string_lossy(),
"max_size": 1_000_000_000 }
}),
];
for (i, attack_intent) in attack_intents.iter().enumerate() {
let result = validate_intent_against_policy(attack_intent, &env.capability_bundle);
assert!(
result.is_err(),
"Attack vector {} should be blocked: {:?}",
i,
attack_intent["params"]
);
}
}
#[test]
fn test_performance_under_security_constraints() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let start = Instant::now();
for _ in 0..100 {
let intent = &env.test_intents["safe_fs_read"];
let _ = validate_intent_against_policy(intent, &env.capability_bundle);
}
let validation_duration = start.elapsed();
assert!(
validation_duration < Duration::from_millis(1000),
"100 validations should complete in <1s, took {:?}",
validation_duration
);
let start = Instant::now();
for _ in 0..50 {
let allowed_paths = vec![env.allowed_dir.path().to_string_lossy().to_string()];
let _ =
create_capability_landlock_config("fs.read.v1", &allowed_paths, env.workdir.path());
}
let landlock_duration = start.elapsed();
assert!(
landlock_duration < Duration::from_millis(500),
"50 landlock configs should complete in <500ms, took {:?}",
landlock_duration
);
let start = Instant::now();
for _ in 0..50 {
let _ = create_capability_seccomp_config("fs.read.v1");
}
let seccomp_duration = start.elapsed();
assert!(
seccomp_duration < Duration::from_millis(500),
"50 seccomp configs should complete in <500ms, took {:?}",
seccomp_duration
);
}
#[test]
fn test_error_handling_in_security_pipeline() {
let env = SecurityIntegrationTestEnvironment::new().unwrap();
let malformed_intents = vec![
json!({"invalid": "intent"}), json!({
"id": "not-a-uuid",
"capability": "fs.read.v1",
"params": {"path": "/forbidden/path"}
}),
json!({
"id": Uuid::new_v4().to_string(),
"capability": "invalid.capability.v1",
"params": {"path": "/tmp/test"}
}),
];
for malformed in malformed_intents {
let result = validate_intent_against_policy(&malformed, &env.capability_bundle);
assert!(
result.is_err(),
"Malformed intent should be rejected: {:?}",
malformed
);
}
let invalid_policy_path = env.workdir.path().join("invalid_policy.json");
File::create(&invalid_policy_path)
.unwrap()
.write_all(b"invalid json content")
.unwrap();
let intent = &env.test_intents["safe_fs_read"];
let result = validate_intent_against_policy(intent, &invalid_policy_path);
assert!(
result.is_err(),
"Invalid capability bundle should cause validation failure"
);
}
}
#[cfg(test)]
mod test_helpers {
use super::*;
pub fn validate_intent_against_policy(
intent: &Value,
capability_bundle_path: &Path,
) -> Result<()> {
let policy_content = fs::read_to_string(capability_bundle_path)?;
let policy: Value = serde_json::from_str(&policy_content)?;
let capability = intent["capability"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing capability in intent"))?;
let params = intent["params"]
.as_object()
.ok_or_else(|| anyhow::anyhow!("Missing params in intent"))?;
let capability_policy = policy["policies"][capability]
.as_object()
.ok_or_else(|| anyhow::anyhow!("No policy found for capability: {}", capability))?;
match capability {
"fs.read.v1" => validate_fs_read_policy(params, capability_policy),
"http.fetch.v1" => validate_http_fetch_policy(params, capability_policy),
"sqlite.query.v1" => validate_sqlite_query_policy(params, capability_policy),
_ => Err(anyhow::anyhow!("Unknown capability: {}", capability)),
}
}
fn validate_fs_read_policy(
params: &serde_json::Map<String, Value>,
policy: &serde_json::Map<String, Value>,
) -> Result<()> {
let path = params
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing path parameter"))?;
let max_size = params
.get("max_size")
.and_then(|v| v.as_u64())
.unwrap_or(1024);
if path.contains("..") {
return Err(anyhow::anyhow!("Path traversal detected"));
}
let allowed_paths = policy["allowed_paths"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Missing allowed_paths in policy"))?;
let policy_max_size = policy["max_file_size"].as_u64().unwrap_or(1048576);
if max_size > policy_max_size {
return Err(anyhow::anyhow!("File size exceeds policy limit"));
}
let path_allowed = allowed_paths.iter().any(|allowed_path| {
if let Some(allowed_str) = allowed_path.as_str() {
path.starts_with(allowed_str)
} else {
false
}
});
if !path_allowed {
return Err(anyhow::anyhow!("Path not in allowed list"));
}
Ok(())
}
fn validate_http_fetch_policy(
params: &serde_json::Map<String, Value>,
policy: &serde_json::Map<String, Value>,
) -> Result<()> {
let url_str = params["url"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing URL parameter"))?;
let method = params
.get("method")
.and_then(|v| v.as_str())
.unwrap_or("GET");
let url = url::Url::parse(url_str).map_err(|_| anyhow::anyhow!("Invalid URL format"))?;
let allowed_domains = policy["allowed_domains"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Missing allowed_domains in policy"))?;
let host = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("URL must have host"))?;
let domain_allowed = allowed_domains.iter().any(|domain| {
if let Some(domain_str) = domain.as_str() {
host == domain_str || host.ends_with(&format!(".{}", domain_str))
} else {
false
}
});
if !domain_allowed {
return Err(anyhow::anyhow!("Domain not in allowed list: {}", host));
}
let allowed_methods = policy["allowed_methods"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Missing allowed_methods in policy"))?;
let method_allowed = allowed_methods.iter().any(|m| {
if let Some(method_str) = m.as_str() {
method_str == method
} else {
false
}
});
if !method_allowed {
return Err(anyhow::anyhow!("Method not allowed: {}", method));
}
if host.contains("127.0.0.1")
|| host.contains("localhost")
|| host.contains("169.254.169.254")
|| host.contains("metadata.google.internal")
{
return Err(anyhow::anyhow!("Dangerous URL detected: {}", url_str));
}
Ok(())
}
fn validate_sqlite_query_policy(
params: &serde_json::Map<String, Value>,
policy: &serde_json::Map<String, Value>,
) -> Result<()> {
let query = params["query"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing query parameter"))?;
let allowed_operations = policy["allowed_operations"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("Missing allowed_operations in policy"))?;
let query_upper = query.to_uppercase();
let operation_allowed = allowed_operations.iter().any(|op| {
if let Some(op_str) = op.as_str() {
query_upper.starts_with(op_str)
} else {
false
}
});
if !operation_allowed {
return Err(anyhow::anyhow!("SQL operation not allowed"));
}
let dangerous_keywords = vec!["DROP", "DELETE", "UPDATE", "ALTER", "CREATE", "PRAGMA"];
for keyword in dangerous_keywords {
if query_upper.contains(keyword)
&& !allowed_operations
.iter()
.any(|op| op.as_str() == Some(keyword))
{
return Err(anyhow::anyhow!(
"Dangerous SQL operation detected: {}",
keyword
));
}
}
Ok(())
}
pub fn execute_secure_intent(
intent: &Value,
env: &SecurityIntegrationTestEnvironment,
) -> Result<SecurityTestOutcome> {
let start = Instant::now();
validate_intent_against_policy(intent, &env.capability_bundle)?;
let capability = intent["capability"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing capability"))?;
let _landlock_config = create_capability_landlock_config(
capability,
&[env.allowed_dir.path().to_string_lossy().to_string()],
env.workdir.path(),
);
let _seccomp_config = create_capability_seccomp_config(capability);
match capability {
"fs.read.v1" => {
}
"http.fetch.v1" => {
}
_ => return Err(anyhow::anyhow!("Unknown capability")),
}
let _execution_time = start.elapsed();
Ok(SecurityTestOutcome::Allowed)
}
}