keyhog_scanner/
telemetry.rs1use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20use std::borrow::Cow;
21use std::collections::HashSet;
22use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
23use std::sync::{Mutex, OnceLock};
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(tag = "kind", rename_all = "snake_case")]
30pub enum DogfoodEvent {
31 ExampleSuppressed {
40 detector: String,
41 path: Option<String>,
42 credential_redacted: String,
43 reason: Cow<'static, str>,
44 },
45}
46
47#[derive(Default)]
48struct Telemetry {
49 dogfood_enabled: AtomicBool,
50 example_suppressions: AtomicUsize,
51 events: Mutex<Vec<DogfoodEvent>>,
52 seen_example_suppressions: Mutex<HashSet<String>>,
53}
54
55static FILES_SCANNED: AtomicUsize = AtomicUsize::new(0);
57static BYTES_SCANNED: AtomicUsize = AtomicUsize::new(0);
58static SKIPPED_FILES: AtomicUsize = AtomicUsize::new(0);
59static TOTAL_MATCHES: AtomicUsize = AtomicUsize::new(0);
60static GPU_DISPATCHES: AtomicUsize = AtomicUsize::new(0);
61
62static DOGFOOD_ENABLED: AtomicBool = AtomicBool::new(false);
64
65#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
66pub struct TelemetrySnapshot {
67 pub files_scanned: usize,
68 pub bytes_scanned: usize,
69 pub skipped_files: usize,
70 pub total_matches: usize,
71 pub gpu_dispatches: usize,
72 pub example_suppressions: usize,
73}
74
75fn cell() -> &'static Telemetry {
76 static CELL: OnceLock<Telemetry> = OnceLock::new();
77 CELL.get_or_init(Telemetry::default)
78}
79
80pub fn enable_dogfood() {
82 DOGFOOD_ENABLED.store(true, Ordering::Relaxed);
83 cell().dogfood_enabled.store(true, Ordering::Relaxed);
84}
85
86pub fn is_dogfood_enabled() -> bool {
87 DOGFOOD_ENABLED.load(Ordering::Relaxed)
88}
89
90pub fn record_example_suppression(
93 detector: &str,
94 path: Option<&str>,
95 credential: &str,
96 reason: &'static str,
97) {
98 let t = cell();
99 let credential_hash = {
100 let mut hasher = Sha256::new();
101 hasher.update(credential.as_bytes());
102 let digest = hasher.finalize();
103 let mut bytes = [0u8; 32];
104 bytes.copy_from_slice(&digest);
105 keyhog_core::hex_encode(&bytes)
106 };
107 let key = format!(
108 "{}\0{}\0{}\0{}",
109 detector,
110 path.unwrap_or(""),
111 credential_hash,
112 reason
113 );
114 if let Ok(mut seen) = t.seen_example_suppressions.lock() {
115 if !seen.insert(key) {
116 return;
117 }
118 }
119
120 t.example_suppressions.fetch_add(1, Ordering::Relaxed);
121
122 if !is_dogfood_enabled() {
124 return;
125 }
126
127 let redacted = keyhog_core::redact(credential).into_owned();
131 if let Ok(mut events) = t.events.lock() {
132 events.push(DogfoodEvent::ExampleSuppressed {
133 detector: detector.to_string(),
134 path: path.map(str::to_string),
135 credential_redacted: redacted,
136 reason: Cow::Borrowed(reason),
137 });
138 }
139}
140
141pub fn example_suppression_count() -> usize {
143 cell().example_suppressions.load(Ordering::Relaxed)
144}
145
146pub fn reset_example_suppression_count() {
151 cell().example_suppressions.store(0, Ordering::Relaxed);
152}
153
154pub fn add_example_suppressions(n: usize) {
159 cell().example_suppressions.fetch_add(n, Ordering::Relaxed);
160}
161
162pub fn append_events<I: IntoIterator<Item = DogfoodEvent>>(events: I) {
167 let t = cell();
168 if let Ok(mut buf) = t.events.lock() {
169 buf.extend(events);
170 }
171}
172
173pub fn drain_events() -> Vec<DogfoodEvent> {
176 let t = cell();
177 if let Ok(mut events) = t.events.lock() {
178 std::mem::take(&mut *events)
179 } else {
180 Vec::new()
181 }
182}
183
184pub fn record_file_scanned(bytes: usize) {
186 FILES_SCANNED.fetch_add(1, Ordering::Relaxed);
187 BYTES_SCANNED.fetch_add(bytes, Ordering::Relaxed);
188}
189
190pub fn record_file_skipped() {
191 SKIPPED_FILES.fetch_add(1, Ordering::Relaxed);
192}
193
194pub fn record_match_found() {
195 TOTAL_MATCHES.fetch_add(1, Ordering::Relaxed);
196}
197
198pub fn record_gpu_dispatch() {
199 GPU_DISPATCHES.fetch_add(1, Ordering::Relaxed);
200}
201
202pub fn get_telemetry_snapshot() -> TelemetrySnapshot {
204 TelemetrySnapshot {
205 files_scanned: FILES_SCANNED.load(Ordering::Relaxed),
206 bytes_scanned: BYTES_SCANNED.load(Ordering::Relaxed),
207 skipped_files: SKIPPED_FILES.load(Ordering::Relaxed),
208 total_matches: TOTAL_MATCHES.load(Ordering::Relaxed),
209 gpu_dispatches: GPU_DISPATCHES.load(Ordering::Relaxed),
210 example_suppressions: example_suppression_count(),
211 }
212}
213
214#[doc(hidden)]
216pub fn reset() {
217 let t = cell();
218 DOGFOOD_ENABLED.store(false, Ordering::Relaxed);
219 t.dogfood_enabled.store(false, Ordering::Relaxed);
220 t.example_suppressions.store(0, Ordering::Relaxed);
221 FILES_SCANNED.store(0, Ordering::Relaxed);
222 BYTES_SCANNED.store(0, Ordering::Relaxed);
223 SKIPPED_FILES.store(0, Ordering::Relaxed);
224 TOTAL_MATCHES.store(0, Ordering::Relaxed);
225 GPU_DISPATCHES.store(0, Ordering::Relaxed);
226 if let Ok(mut events) = t.events.lock() {
227 events.clear();
228 }
229 if let Ok(mut seen) = t.seen_example_suppressions.lock() {
230 seen.clear();
231 }
232}