jsdet-core 0.1.0

Core WASM-sandboxed JavaScript detonation engine
Documentation
use crate::observation::Value;

/// The bridge interface between JavaScript and the host.
///
/// Consumers implement this trait to provide fake API surfaces.
/// Sear implements browser bridges (document, window, navigator, fetch).
/// Soleno implements Chrome extension bridges (chrome.tabs, chrome.cookies, etc.).
///
/// Every call through the bridge is observable — the sandbox records what
/// function was called, with what arguments, and what was returned.
///
/// # Design
///
/// The bridge is intentionally stringly-typed. JS APIs are a massive surface
/// and encoding every possible API as a Rust enum would be both fragile and
/// incomplete. Instead, the bridge receives the API name as a string and
/// the arguments as `Vec<Value>`. The consumer pattern-matches on the string.
///
/// # Thread safety
///
/// Bridge implementations must be `Send + Sync` because the sandbox may run
/// on a different thread from the caller. For single-threaded use, wrap
/// interior state in `RefCell` behind a `Mutex`.
pub trait Bridge: Send + Sync {
    /// Handle a function call from JavaScript.
    ///
    /// `api` is the fully qualified name: `"document.createElement"`,
    /// `"chrome.tabs.query"`, `"fetch"`, etc.
    ///
    /// Return `Ok(value)` to provide a return value to JS.
    /// Return `Err(message)` to throw a JS exception.
    ///
    /// # Errors
    ///
    /// Returns an error if the API throws a JS exception.
    fn call(&self, api: &str, args: &[Value]) -> Result<Value, String>;

    /// Handle a property read from JavaScript.
    ///
    /// `object` is the object name: `"navigator"`, `"document"`, `"chrome.runtime"`.
    /// `property` is the property name: `"userAgent"`, `"cookie"`, `"id"`.
    ///
    /// # Errors
    ///
    /// Returns an error if the property is undefined or reading it throws an exception.
    fn get_property(&self, object: &str, property: &str) -> Result<Value, String>;

    /// Handle a property write from JavaScript.
    ///
    /// # Errors
    ///
    /// Returns an error if writing the property throws an exception.
    fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String>;

    /// Return the list of global objects this bridge provides.
    ///
    /// These become `globalThis.{name}` in the JS environment.
    /// Example: `["document", "window", "navigator", "fetch", "XMLHttpRequest"]`
    /// Example: `["chrome"]`
    fn provided_globals(&self) -> Vec<String>;

    /// Return the JS bootstrap code that installs this bridge's API surface.
    ///
    /// This code runs BEFORE user scripts. It should define the global objects,
    /// prototype chains, and proxy traps that make the bridge look like a real API.
    ///
    /// The bootstrap has access to `__jsdet_call(api, args)` and
    /// `__jsdet_get(object, property)` host-imported functions for calling
    /// back into the bridge.
    fn bootstrap_js(&self) -> String;
}

/// A bridge that provides no APIs. Useful for testing the core sandbox
/// in isolation — scripts execute but all API calls throw.
pub struct EmptyBridge;

impl Bridge for EmptyBridge {
    fn call(&self, api: &str, _args: &[Value]) -> Result<Value, String> {
        Err(format!("{api} is not defined"))
    }

    fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
        Err(format!("{object}.{property} is not defined"))
    }

    fn set_property(&self, _object: &str, _property: &str, _value: &Value) -> Result<(), String> {
        Ok(()) // silently ignore writes
    }

    fn provided_globals(&self) -> Vec<String> {
        Vec::new()
    }

    fn bootstrap_js(&self) -> String {
        String::new()
    }
}

/// Compose multiple bridges into one.
///
/// Each bridge handles its own globals. When a call comes in, the composite
/// tries each bridge in order until one succeeds. This lets you combine
/// `jsdet-browser` + custom hooks without modifying either.
pub struct CompositeBridge {
    bridges: Vec<Box<dyn Bridge>>,
}

impl CompositeBridge {
    #[must_use]
    pub fn new(bridges: Vec<Box<dyn Bridge>>) -> Self {
        Self { bridges }
    }
}

impl Bridge for CompositeBridge {
    fn call(&self, api: &str, args: &[Value]) -> Result<Value, String> {
        for bridge in &self.bridges {
            match bridge.call(api, args) {
                Ok(value) => return Ok(value),
                Err(e) if e.ends_with("is not defined") => {}
                Err(e) => return Err(e),
            }
        }
        Err(format!("{api} is not defined"))
    }

    fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
        for bridge in &self.bridges {
            match bridge.get_property(object, property) {
                Ok(value) => return Ok(value),
                Err(e) if e.ends_with("is not defined") => {}
                Err(e) => return Err(e),
            }
        }
        Err(format!("{object}.{property} is not defined"))
    }

    fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String> {
        for bridge in &self.bridges {
            match bridge.set_property(object, property, value) {
                Ok(()) => return Ok(()),
                Err(e) if e.ends_with("is not defined") => {}
                Err(e) => return Err(e),
            }
        }
        Ok(())
    }

    fn provided_globals(&self) -> Vec<String> {
        self.bridges
            .iter()
            .flat_map(|b| b.provided_globals())
            .collect()
    }

    fn bootstrap_js(&self) -> String {
        self.bridges
            .iter()
            .map(|b| b.bootstrap_js())
            .collect::<Vec<_>>()
            .join("\n")
    }
}

/// Hook for intercepting and modifying bridge calls.
///
/// Security researchers use this to:
/// - Log all calls to a specific API
/// - Modify return values (e.g., fake canvas fingerprints)
/// - Block specific APIs
/// - Inject faults
pub trait Hook: Send + Sync {
    /// Called BEFORE the bridge handles the request.
    /// Return `Some(value)` to short-circuit — the bridge is not called.
    /// Return `None` to let the bridge handle it normally.
    fn before_call(&self, _api: &str, _args: &[Value]) -> Option<Result<Value, String>> {
        None
    }

    /// Called AFTER the bridge returns.
    /// Can modify the return value.
    ///
    /// # Errors
    ///
    /// Returns an error if the hook changes the modification to failure.
    fn after_call(
        &self,
        _api: &str,
        _args: &[Value],
        result: Result<Value, String>,
    ) -> Result<Value, String> {
        result
    }
}

/// A bridge wrapped with hooks for interception.
pub struct HookedBridge {
    inner: Box<dyn Bridge>,
    hooks: Vec<Box<dyn Hook>>,
}

impl HookedBridge {
    #[must_use]
    pub fn new(inner: Box<dyn Bridge>, hooks: Vec<Box<dyn Hook>>) -> Self {
        Self { inner, hooks }
    }
}

impl Bridge for HookedBridge {
    fn call(&self, api: &str, args: &[Value]) -> Result<Value, String> {
        // Run before hooks — first one to return Some wins.
        for hook in &self.hooks {
            if let Some(result) = hook.before_call(api, args) {
                return result;
            }
        }

        let mut result = self.inner.call(api, args);

        // Run after hooks in order.
        for hook in &self.hooks {
            result = hook.after_call(api, args, result);
        }

        result
    }

    fn get_property(&self, object: &str, property: &str) -> Result<Value, String> {
        self.inner.get_property(object, property)
    }

    fn set_property(&self, object: &str, property: &str, value: &Value) -> Result<(), String> {
        self.inner.set_property(object, property, value)
    }

    fn provided_globals(&self) -> Vec<String> {
        self.inner.provided_globals()
    }

    fn bootstrap_js(&self) -> String {
        self.inner.bootstrap_js()
    }
}