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,
}
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())
));
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
}
}