sidecheck-core 0.1.0

Remote timing side-channel auditor for your own HTTP services
Documentation
//! Перевод статистики в понятный человеку вердикт.
//! Никаких сырых t-значений наружу — только то, что нужно для решения.

use crate::stats::BoxTestResult;

pub struct DetectionReport {
    pub target: String,
    pub field: String,
    pub samples_per_class: usize,
    pub result: BoxTestResult,
    pub jitter_seconds: f64,
    pub failures: usize,
    pub seed: u64,
    pub sidecheck_version: String,
}

/// Автоматически выбирает единицу измерения (ns/μs/ms/s), чтобы не выводить
/// "16264.9 μs" там, где человеку читается проще "16.3 ms".
fn format_duration(seconds: f64) -> String {
    let abs = seconds.abs();
    if abs >= 1.0 {
        format!("{seconds:.3} s")
    } else if abs >= 0.001 {
        format!("{:.2} ms", seconds * 1_000.0)
    } else if abs >= 0.000_001 {
        format!("{:.1} μs", seconds * 1_000_000.0)
    } else {
        format!("{:.0} ns", seconds * 1_000_000_000.0)
    }
}

impl DetectionReport {
    pub fn render(&self) -> String {
        let jitter = format_duration(self.jitter_seconds);
        let significant = self.result.is_significant();

        let mut out = String::new();
        out.push_str(&"".repeat(48));
        out.push_str("\nsidecheck timing report\n");
        out.push_str(&"".repeat(48));
        out.push_str(&format!("\n\ntarget          {}\n", self.target));
        out.push_str(&format!("field            {}\n", self.field));
        out.push_str(&format!("samples/class    {}\n", self.samples_per_class));
        out.push_str(&format!("network jitter   {jitter}\n"));
        if self.failures > 0 {
            out.push_str(&format!(
                "failed requests  {} (excluded from analysis)\n",
                self.failures
            ));
        }
        out.push('\n');

        if significant {
            out.push_str("⚠ timing leak detected\n");
            out.push_str(&format!(
                "  estimated leak         {}\n",
                format_duration(self.result.estimated_leak.abs())
            ));
            // "confidence" сама по себе неоднозначна — может читаться как
            // "вероятность, что сервер уязвим". Уточняем явно, что это
            // bootstrap-доверительный интервал разницы, а не вероятность
            // уязвимости и не p-value.
            out.push_str(&format!(
                "  bootstrap confidence   {:.1}% (of the measured difference being non-zero)\n",
                self.result.confidence * 100.0
            ));
            out.push_str("\n  this endpoint responds measurably differently depending on\n");
            out.push_str("  input correctness. an attacker can exploit this to recover\n");
            out.push_str("  secrets character-by-character instead of brute-forcing them.\n\n");
            out.push_str("  fix: use a constant-time comparison instead of == on secret\n");
            out.push_str("  bytes (e.g. the `subtle` crate in Rust, `crypto/subtle` in Go,\n");
            out.push_str("  `hmac.compare_digest` in Python).\n");
        } else {
            out.push_str("✓ no statistically significant timing difference detected\n");
            out.push_str(&format!(
                "  (bootstrap {:.0}% CI of the difference: [{}, {}], includes zero)\n",
                self.result.confidence * 100.0,
                format_duration(self.result.ci_low),
                format_duration(self.result.ci_high)
            ));
        }

        out.push_str(&"".repeat(48));
        out.push_str("\nsidecheck cannot prove the absence of a timing leak — only detect a\n");
        out.push_str("statistically significant one under the tested conditions. A clean\n");
        out.push_str("result here is not a safety guarantee.\n");
        out.push_str(&format!(
            "generated by sidecheck {} · seed {} (rerun with --seed {} to reproduce request order)\n",
            self.sidecheck_version, self.seed, self.seed
        ));

        out
    }
}