jsdet-core 0.1.0

Core WASM-sandboxed JavaScript detonation engine
Documentation
/// Streaming observation API for real-time reaction during execution.
///
/// Instead of waiting for all scripts to finish, the consumer receives
/// each observation as it happens. This enables:
///
/// - **Early termination** — stop execution when a confirmed finding is detected
/// - **Real-time MCTS reward** — Soleno feeds observations directly to search
/// - **Rate limiting** — drop observations when the buffer is too large
/// - **Live monitoring** — security researchers watch execution in real-time
///
use crate::observation::Observation;

/// Control flow decision returned by the streaming callback.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlFlow {
    /// Continue execution normally.
    Continue,
    /// Halt execution immediately. Remaining scripts are not evaluated.
    /// The `ExecutionResult` will have `timed_out = true` and contain
    /// all observations collected up to this point.
    Halt,
}

/// A callback that receives observations during execution.
///
/// Implement this trait to process observations in real-time.
/// The default implementation collects into a Vec (batch mode).
pub trait ObservationSink: Send {
    /// Called for each observation during execution.
    ///
    /// Return `ControlFlow::Continue` to keep executing.
    /// Return `ControlFlow::Halt` to stop immediately.
    fn on_observation(&mut self, observation: &Observation) -> ControlFlow;

    /// Called when execution completes (normally or via halt).
    /// Override to perform cleanup or final analysis.
    fn on_complete(&mut self) {}
}

/// Batch collector — the default sink. Collects all observations into a Vec.
pub struct BatchCollector {
    observations: Vec<Observation>,
    max_observations: usize,
}

impl BatchCollector {
    #[must_use]
    pub fn new(max_observations: usize) -> Self {
        Self {
            observations: Vec::new(),
            max_observations,
        }
    }

    #[must_use]
    pub fn into_observations(self) -> Vec<Observation> {
        self.observations
    }
}

impl ObservationSink for BatchCollector {
    fn on_observation(&mut self, observation: &Observation) -> ControlFlow {
        if self.observations.len() < self.max_observations {
            self.observations.push(observation.clone());
        }
        ControlFlow::Continue
    }
}

/// Early-stop sink — halts execution when a predicate matches.
pub struct EarlyStopSink<F: FnMut(&Observation) -> bool> {
    predicate: F,
    observations: Vec<Observation>,
}

impl<F: FnMut(&Observation) -> bool> EarlyStopSink<F> {
    pub fn new(predicate: F) -> Self {
        Self {
            predicate,
            observations: Vec::new(),
        }
    }

    pub fn into_observations(self) -> Vec<Observation> {
        self.observations
    }
}

impl<F: FnMut(&Observation) -> bool + Send> ObservationSink for EarlyStopSink<F> {
    fn on_observation(&mut self, observation: &Observation) -> ControlFlow {
        self.observations.push(observation.clone());
        if (self.predicate)(observation) {
            ControlFlow::Halt
        } else {
            ControlFlow::Continue
        }
    }
}

/// Counting sink — counts observations by category without storing them.
/// Useful for high-throughput scanning where individual observations
/// are less important than aggregate behavior.
#[derive(Debug, Default)]
pub struct CountingSink {
    pub api_calls: u64,
    pub dom_mutations: u64,
    pub network_requests: u64,
    pub dynamic_code: u64,
    pub cookie_access: u64,
    pub errors: u64,
    pub wasm_instantiations: u64,
    pub fingerprint_access: u64,
    pub context_messages: u64,
    pub resource_limits: u64,
    pub total: u64,
}

impl ObservationSink for CountingSink {
    fn on_observation(&mut self, observation: &Observation) -> ControlFlow {
        // CRITICAL FIX: Use saturating arithmetic to prevent overflow
        self.total = self.total.saturating_add(1);
        match observation {
            Observation::ApiCall { .. } => self.api_calls = self.api_calls.saturating_add(1),
            Observation::DomMutation { .. } => {
                self.dom_mutations = self.dom_mutations.saturating_add(1)
            }
            Observation::NetworkRequest { .. } => {
                self.network_requests = self.network_requests.saturating_add(1)
            }
            Observation::DynamicCodeExec { .. } => {
                self.dynamic_code = self.dynamic_code.saturating_add(1)
            }
            Observation::CookieAccess { .. } => {
                self.cookie_access = self.cookie_access.saturating_add(1)
            }
            Observation::Error { .. } => self.errors = self.errors.saturating_add(1),
            Observation::WasmInstantiation { .. } => {
                self.wasm_instantiations = self.wasm_instantiations.saturating_add(1)
            }
            Observation::FingerprintAccess { .. } => {
                self.fingerprint_access = self.fingerprint_access.saturating_add(1)
            }
            Observation::ContextMessage { .. } => {
                self.context_messages = self.context_messages.saturating_add(1)
            }
            Observation::ResourceLimit { .. } => {
                self.resource_limits = self.resource_limits.saturating_add(1)
            }
            _ => {}
        }
        ControlFlow::Continue
    }
}