Files
multiverse/xcom-ultra/xcu-v8-sandbox/src/lib.rs
T

153 lines
6.2 KiB
Rust

#![deny(warnings)]
#![allow(dead_code)]
//! [TSM.ID].[11031972] -- Platform X Ecosystem
//! xcu-v8-sandbox -- JavaScript Execution Sandbox with Permission System
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
pub enum SandboxError { PermissionDenied(String), MemoryExceeded(String), TimeoutError(String), ScriptError(String) }
impl std::fmt::Display for SandboxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { Self::PermissionDenied(e) => write!(f, "Permission: {e}"), Self::MemoryExceeded(e) => write!(f, "Memory: {e}"), Self::TimeoutError(e) => write!(f, "Timeout: {e}"), Self::ScriptError(e) => write!(f, "Script: {e}") }
}
}
impl std::error::Error for SandboxError {}
#[derive(Debug, Clone)]
pub struct SandboxPolicy {
pub max_memory_mb: u64,
pub max_execution_ms: u64,
pub allowed_apis: Vec<String>,
pub denied_apis: Vec<String>,
pub allow_network: bool,
pub allow_fs: bool,
pub allow_eval: bool,
}
impl Default for SandboxPolicy {
fn default() -> Self {
Self { max_memory_mb: 128, max_execution_ms: 5000,
allowed_apis: vec!["console".into(), "Math".into(), "JSON".into(), "Date".into(), "Array".into(), "Object".into(), "String".into()],
denied_apis: vec!["eval".into(), "Function".into(), "require".into(), "import".into(), "process".into(), "fs".into()],
allow_network: false, allow_fs: false, allow_eval: false }
}
}
#[derive(Debug, Clone)]
pub enum JsValue { Undefined, Null, Bool(bool), Number(f64), Str(String), Array(Vec<JsValue>), Object(HashMap<String, JsValue>) }
#[derive(Debug, Clone)]
pub struct ScriptResult { pub value: JsValue, pub memory_used_bytes: u64, pub execution_ms: u64, pub api_calls: Vec<String> }
pub struct JsSandbox {
policy: SandboxPolicy,
globals: HashMap<String, JsValue>,
audit_log: Arc<Mutex<Vec<String>>>,
}
impl JsSandbox {
pub fn new(policy: SandboxPolicy) -> Self {
Self { policy, globals: HashMap::new(), audit_log: Arc::new(Mutex::new(Vec::new())) }
}
pub fn set_global(&mut self, name: &str, value: JsValue) { self.globals.insert(name.into(), value); }
/// Check if API call is permitted
pub fn check_api(&self, api_name: &str) -> Result<(), SandboxError> {
if self.policy.denied_apis.iter().any(|d| api_name.starts_with(d)) {
return Err(SandboxError::PermissionDenied(format!("API '{api_name}' is denied")));
}
if !api_name.contains('.') && !self.policy.allowed_apis.iter().any(|a| api_name.starts_with(a)) {
return Err(SandboxError::PermissionDenied(format!("API '{api_name}' not in allow list")));
}
Ok(())
}
/// Evaluate simple expression (interpreter for basic math/string ops)
pub fn eval_expr(&mut self, expr: &str) -> Result<ScriptResult, SandboxError> {
if !self.policy.allow_eval && expr.contains("eval(") {
return Err(SandboxError::PermissionDenied("eval() is disabled".into()));
}
let trimmed = expr.trim();
let mut api_calls = Vec::new();
// Check for denied APIs in expression
for denied in &self.policy.denied_apis {
if trimmed.contains(denied.as_str()) {
return Err(SandboxError::PermissionDenied(format!("'{denied}' found in expression")));
}
}
// Simple expression evaluator
let value = if let Ok(n) = trimmed.parse::<f64>() {
JsValue::Number(n)
} else if trimmed.starts_with('"') && trimmed.ends_with('"') {
JsValue::Str(trimmed[1..trimmed.len()-1].to_string())
} else if trimmed == "true" { JsValue::Bool(true) }
else if trimmed == "false" { JsValue::Bool(false) }
else if trimmed == "null" { JsValue::Null }
else if trimmed.starts_with("Math.") {
api_calls.push("Math".into());
match trimmed {
"Math.PI" => JsValue::Number(std::f64::consts::PI),
"Math.E" => JsValue::Number(std::f64::consts::E),
"Math.SQRT2" => JsValue::Number(std::f64::consts::SQRT_2),
_ => JsValue::Undefined,
}
} else if let Some(var) = self.globals.get(trimmed) { var.clone() }
else { JsValue::Undefined };
// Log
if let Ok(mut log) = self.audit_log.lock() { log.push(format!("eval: {trimmed}")); }
Ok(ScriptResult { value, memory_used_bytes: trimmed.len() as u64, execution_ms: 0, api_calls })
}
/// Check memory budget
pub fn check_memory(&self, used_bytes: u64) -> Result<(), SandboxError> {
let limit = self.policy.max_memory_mb * 1024 * 1024;
if used_bytes > limit { Err(SandboxError::MemoryExceeded(format!("{}B > {}B", used_bytes, limit))) }
else { Ok(()) }
}
pub fn audit_log(&self) -> Vec<String> { self.audit_log.lock().map(|l| l.clone()).unwrap_or_default() }
pub fn policy(&self) -> &SandboxPolicy { &self.policy }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_check() {
let sb = JsSandbox::new(SandboxPolicy::default());
assert!(sb.check_api("Math").is_ok());
assert!(sb.check_api("console").is_ok());
assert!(sb.check_api("eval").is_err());
assert!(sb.check_api("require").is_err());
}
#[test]
fn test_eval_number() {
let mut sb = JsSandbox::new(SandboxPolicy::default());
let r = sb.eval_expr("42").unwrap();
if let JsValue::Number(n) = r.value { assert_eq!(n, 42.0); }
}
#[test]
fn test_eval_denied() {
let mut sb = JsSandbox::new(SandboxPolicy::default());
assert!(sb.eval_expr("require('fs')").is_err());
}
#[test]
fn test_eval_math() {
let mut sb = JsSandbox::new(SandboxPolicy::default());
let r = sb.eval_expr("Math.PI").unwrap();
if let JsValue::Number(n) = r.value { assert!((n - std::f64::consts::PI).abs() < 0.001); }
}
#[test]
fn test_globals() {
let mut sb = JsSandbox::new(SandboxPolicy::default());
sb.set_global("x", JsValue::Number(99.0));
let r = sb.eval_expr("x").unwrap();
if let JsValue::Number(n) = r.value { assert_eq!(n, 99.0); }
}
}