1use crate::stats::BoxTestResult;
5
6pub struct DetectionReport {
7 pub target: String,
8 pub field: String,
9 pub samples_per_class: usize,
10 pub result: BoxTestResult,
11 pub jitter_seconds: f64,
12 pub failures: usize,
13 pub seed: u64,
14 pub sidecheck_version: String,
15}
16
17fn format_duration(seconds: f64) -> String {
20 let abs = seconds.abs();
21 if abs >= 1.0 {
22 format!("{seconds:.3} s")
23 } else if abs >= 0.001 {
24 format!("{:.2} ms", seconds * 1_000.0)
25 } else if abs >= 0.000_001 {
26 format!("{:.1} μs", seconds * 1_000_000.0)
27 } else {
28 format!("{:.0} ns", seconds * 1_000_000_000.0)
29 }
30}
31
32impl DetectionReport {
33 pub fn render(&self) -> String {
34 let jitter = format_duration(self.jitter_seconds);
35 let significant = self.result.is_significant();
36
37 let mut out = String::new();
38 out.push_str(&"─".repeat(48));
39 out.push_str("\nsidecheck timing report\n");
40 out.push_str(&"─".repeat(48));
41 out.push_str(&format!("\n\ntarget {}\n", self.target));
42 out.push_str(&format!("field {}\n", self.field));
43 out.push_str(&format!("samples/class {}\n", self.samples_per_class));
44 out.push_str(&format!("network jitter {jitter}\n"));
45 if self.failures > 0 {
46 out.push_str(&format!(
47 "failed requests {} (excluded from analysis)\n",
48 self.failures
49 ));
50 }
51 out.push('\n');
52
53 if significant {
54 out.push_str("⚠ timing leak detected\n");
55 out.push_str(&format!(
56 " estimated leak {}\n",
57 format_duration(self.result.estimated_leak.abs())
58 ));
59 out.push_str(&format!(
64 " bootstrap confidence {:.1}% (of the measured difference being non-zero)\n",
65 self.result.confidence * 100.0
66 ));
67 out.push_str("\n this endpoint responds measurably differently depending on\n");
68 out.push_str(" input correctness. an attacker can exploit this to recover\n");
69 out.push_str(" secrets character-by-character instead of brute-forcing them.\n\n");
70 out.push_str(" fix: use a constant-time comparison instead of == on secret\n");
71 out.push_str(" bytes (e.g. the `subtle` crate in Rust, `crypto/subtle` in Go,\n");
72 out.push_str(" `hmac.compare_digest` in Python).\n");
73 } else {
74 out.push_str("✓ no statistically significant timing difference detected\n");
75 out.push_str(&format!(
76 " (bootstrap {:.0}% CI of the difference: [{}, {}], includes zero)\n",
77 self.result.confidence * 100.0,
78 format_duration(self.result.ci_low),
79 format_duration(self.result.ci_high)
80 ));
81 }
82
83 out.push_str(&"─".repeat(48));
84 out.push_str("\nsidecheck cannot prove the absence of a timing leak — only detect a\n");
85 out.push_str("statistically significant one under the tested conditions. A clean\n");
86 out.push_str("result here is not a safety guarantee.\n");
87 out.push_str(&format!(
88 "generated by sidecheck {} · seed {} (rerun with --seed {} to reproduce request order)\n",
89 self.sidecheck_version, self.seed, self.seed
90 ));
91
92 out
93 }
94}