keyhog_scanner/decode/pipeline/
registry.rs1use crate::decode::base64::{Base64Decoder, Z85Decoder};
2use crate::decode::caesar::CaesarDecoder;
3use crate::decode::hex::HexDecoder;
4#[cfg(feature = "decode")]
5use crate::decode::javascript_static::JavaScriptStaticDecoder;
6use crate::decode::json::JsonDecoder;
7use crate::decode::reverse::ReverseDecoder;
8use crate::decode::url::{
9 HtmlNamedEntityDecoder, HtmlNumericEntityDecoder, MimeEncodedWordDecoder, OctalEscapeDecoder,
10 QuotedPrintableDecoder, UnicodeEscapeDecoder, UrlDecoder,
11};
12#[cfg(feature = "decode")]
13use crate::decode::DecodeAdmission;
14#[cfg(any(feature = "decode", test))]
15use crate::decode::DecodeAdmissionSketch;
16use crate::decode::Decoder;
17use parking_lot::RwLock;
18#[cfg(test)]
19use std::cell::RefCell;
20use std::sync::Arc;
21
22static DECODERS: std::sync::OnceLock<RwLock<DecoderRegistryState>> = std::sync::OnceLock::new();
27
28struct DecoderRegistryState {
29 decoders: Arc<Vec<RegisteredDecoder>>,
30 compatibility_failure: Option<DecoderRegistrationError>,
31}
32
33#[derive(Clone)]
34pub(crate) enum RegisteredDecoder {
35 Shared(Arc<dyn Decoder>),
36 Reverse,
37 Caesar,
38}
39
40impl RegisteredDecoder {
41 pub(crate) fn name(&self) -> &'static str {
42 match self {
43 Self::Shared(decoder) => decoder.name(),
44 Self::Reverse => "reverse",
45 Self::Caesar => "caesar",
46 }
47 }
48
49 fn version(&self) -> &'static str {
50 match self {
51 Self::Shared(decoder) => decoder.version(),
52 Self::Reverse | Self::Caesar => "detector-policy-1",
53 }
54 }
55
56 #[cfg(feature = "decode")]
57 fn admission(
58 &self,
59 chunk: &keyhog_core::Chunk,
60 policy: &super::super::policy::CompiledDecodeTransformPolicy,
61 ) -> DecodeAdmission {
62 match self {
63 Self::Shared(decoder) => decoder.admission(chunk),
64 Self::Reverse => ReverseDecoder
65 .admission_sketch_with_policy(chunk, policy)
66 .admission(),
67 Self::Caesar => CaesarDecoder
68 .admission_sketch_with_policy(chunk, policy)
69 .admission(),
70 }
71 }
72
73 #[cfg(any(feature = "decode", test))]
74 fn admission_sketch(
75 &self,
76 chunk: &keyhog_core::Chunk,
77 policy: &super::super::policy::CompiledDecodeTransformPolicy,
78 ) -> DecodeAdmissionSketch {
79 match self {
80 Self::Shared(decoder) => decoder.admission_sketch(chunk),
81 Self::Reverse => ReverseDecoder.admission_sketch_with_policy(chunk, policy),
82 Self::Caesar => CaesarDecoder.admission_sketch_with_policy(chunk, policy),
83 }
84 }
85
86 pub(super) fn decode_chunk(
87 &self,
88 chunk: &keyhog_core::Chunk,
89 policy: &super::super::policy::CompiledDecodeTransformPolicy,
90 ) -> Vec<keyhog_core::Chunk> {
91 match self {
92 Self::Shared(decoder) => decoder.decode_chunk(chunk),
93 Self::Reverse => ReverseDecoder.decode_chunk_with_policy(chunk, policy),
94 Self::Caesar => CaesarDecoder.decode_chunk_with_policy(chunk, policy),
95 }
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
100pub enum DecoderRegistrationError {
101 #[error("decoder name must be non-empty ASCII without whitespace")]
102 InvalidName,
103 #[error("decoder {name:?} version must be non-empty ASCII without whitespace")]
104 InvalidVersion { name: &'static str },
105 #[error("decoder name {0:?} is already registered")]
106 DuplicateName(&'static str),
107}
108
109#[derive(Clone)]
110pub(crate) struct CompiledDecoderPlan {
111 decoders: Arc<Vec<RegisteredDecoder>>,
112 identity: u64,
113}
114
115impl std::fmt::Debug for CompiledDecoderPlan {
116 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 formatter
118 .debug_struct("CompiledDecoderPlan")
119 .field("decoder_count", &self.decoders.len())
120 .field("identity", &self.identity)
121 .finish()
122 }
123}
124
125impl CompiledDecoderPlan {
126 pub(crate) fn snapshot() -> Result<Self, DecoderRegistrationError> {
127 let decoders = snapshot_decoders()?;
128 let mut names = std::collections::HashSet::with_capacity(decoders.len());
129 let mut hasher = blake3::Hasher::new();
130 hasher.update(b"keyhog-compiled-decoder-plan-v1\0");
131 for decoder in decoders.iter() {
132 let name = decoder.name();
133 let version = decoder.version();
134 validate_descriptor(name, version)?;
135 if !names.insert(name) {
136 return Err(DecoderRegistrationError::DuplicateName(name));
137 }
138 hash_descriptor(&mut hasher, name, version);
139 }
140 let mut bytes = [0u8; 8];
141 bytes.copy_from_slice(&hasher.finalize().as_bytes()[..8]);
142 Ok(Self {
143 decoders,
144 identity: u64::from_le_bytes(bytes),
145 })
146 }
147
148 pub(crate) fn identity(&self) -> u64 {
149 self.identity
150 }
151
152 #[cfg(feature = "decode")]
153 pub(crate) fn decoders(&self) -> &[RegisteredDecoder] {
154 &self.decoders
155 }
156}
157
158fn validate_descriptor(
159 name: &'static str,
160 version: &'static str,
161) -> Result<(), DecoderRegistrationError> {
162 if name.is_empty() || !name.is_ascii() || name.bytes().any(|byte| byte.is_ascii_whitespace()) {
163 return Err(DecoderRegistrationError::InvalidName);
164 }
165 if version.is_empty()
166 || !version.is_ascii()
167 || version.bytes().any(|byte| byte.is_ascii_whitespace())
168 {
169 return Err(DecoderRegistrationError::InvalidVersion { name });
170 }
171 Ok(())
172}
173
174fn hash_descriptor(hasher: &mut blake3::Hasher, name: &str, version: &str) {
175 for value in [name.as_bytes(), version.as_bytes()] {
176 hasher.update(&(value.len() as u64).to_le_bytes());
177 hasher.update(value);
178 }
179}
180
181#[cfg(test)]
182thread_local! {
183 static THREAD_DECODERS: RefCell<Vec<Arc<dyn Decoder>>> = RefCell::new(Vec::new());
184}
185
186pub(super) fn profile_enabled() -> bool {
191 crate::scan_profile::enabled()
192}
193
194const MAX_PROFILED_DECODERS: usize = 16;
201
202static DECODER_NS: [std::sync::atomic::AtomicU64; MAX_PROFILED_DECODERS] = {
203 const Z: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
204 [Z; MAX_PROFILED_DECODERS]
205};
206
207static DECODER_PRODUCED: [std::sync::atomic::AtomicU64; MAX_PROFILED_DECODERS] = {
211 const Z: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
212 [Z; MAX_PROFILED_DECODERS]
213};
214
215pub(super) fn record_decoder_run(
216 decoder_index: usize,
217 elapsed: std::time::Duration,
218 produced: usize,
219) {
220 if decoder_index >= MAX_PROFILED_DECODERS {
221 return;
222 }
223 use std::sync::atomic::Ordering::Relaxed;
224 DECODER_NS[decoder_index].fetch_add(elapsed.as_nanos() as u64, Relaxed);
225 DECODER_PRODUCED[decoder_index].fetch_add(produced as u64, Relaxed);
226}
227
228pub(crate) fn decoder_profile_dump() {
231 use std::sync::atomic::Ordering::Relaxed;
232 let decoders = active_decoders();
233 let names: Vec<&str> = decoders.iter().map(|d| d.name()).collect();
234 let mut rows: Vec<(String, f64)> = (0..names.len().min(MAX_PROFILED_DECODERS))
235 .map(|i| {
236 (
237 names[i].to_string(),
238 DECODER_NS[i].swap(0, Relaxed) as f64 / 1e6,
239 )
240 })
241 .collect();
242 rows.sort_by(|a, b| b.1.total_cmp(&a.1));
243 let total: f64 = rows.iter().map(|r| r.1).sum();
244 let mut prod: Vec<(String, u64)> = (0..names.len().min(MAX_PROFILED_DECODERS))
245 .map(|i| (names[i].to_string(), DECODER_PRODUCED[i].swap(0, Relaxed)))
246 .collect();
247 prod.sort_by(|a, b| b.1.cmp(&a.1));
248 let prod_total: u64 = prod.iter().map(|r| r.1).sum();
249 if total == 0.0 && prod_total == 0 {
250 return;
251 }
252 eprintln!("=== per-decoder decode_chunk time ===");
253 for (name, ms) in &rows {
254 let pct = if total > 0.0 { 100.0 * ms / total } else { 0.0 };
255 eprintln!(" {name:<18}: {ms:>8.1} ms ({pct:>5.1}%)");
256 }
257 eprintln!(" {:<18}: {total:>8.1} ms", "TOTAL");
258 eprintln!("=== per-decoder sub-chunks EMITTED (pre-dedup/screen) ===");
259 for (name, n) in &prod {
260 let pct = if prod_total > 0 {
261 100.0 * *n as f64 / prod_total as f64
262 } else {
263 0.0
264 };
265 eprintln!(" {name:<18}: {n:>8} ({pct:>5.1}%)");
266 }
267 eprintln!(" {:<18}: {prod_total:>8}", "TOTAL");
268}
269
270pub(crate) fn decoder_profile_reset() {
271 use std::sync::atomic::Ordering::Relaxed;
272 for slot in &DECODER_NS {
273 slot.store(0, Relaxed);
274 }
275 for slot in &DECODER_PRODUCED {
276 slot.store(0, Relaxed);
277 }
278}
279
280fn default_decoders() -> Vec<RegisteredDecoder> {
281 vec![
282 RegisteredDecoder::Shared(Arc::new(Base64Decoder)),
283 RegisteredDecoder::Shared(Arc::new(HexDecoder)),
284 RegisteredDecoder::Shared(Arc::new(UrlDecoder)),
285 RegisteredDecoder::Shared(Arc::new(QuotedPrintableDecoder)),
286 RegisteredDecoder::Shared(Arc::new(HtmlNamedEntityDecoder)),
287 RegisteredDecoder::Shared(Arc::new(HtmlNumericEntityDecoder)),
288 RegisteredDecoder::Shared(Arc::new(OctalEscapeDecoder)),
289 RegisteredDecoder::Shared(Arc::new(MimeEncodedWordDecoder)),
290 RegisteredDecoder::Shared(Arc::new(JsonDecoder)),
294 RegisteredDecoder::Shared(Arc::new(UnicodeEscapeDecoder)),
295 RegisteredDecoder::Shared(Arc::new(Z85Decoder)),
296 #[cfg(feature = "decode")]
299 RegisteredDecoder::Shared(Arc::new(JavaScriptStaticDecoder)),
300 RegisteredDecoder::Reverse,
301 RegisteredDecoder::Caesar,
302 ]
303}
304
305#[cfg(feature = "decode")]
311pub(crate) fn default_decoder_names() -> Vec<&'static str> {
312 default_decoders().iter().map(|d| d.name()).collect()
313}
314
315#[cfg(feature = "decode")]
322pub(crate) fn decoder_admission(
323 chunk: &keyhog_core::Chunk,
324 policy: &super::super::policy::CompiledDecodeTransformPolicy,
325 plan: &CompiledDecoderPlan,
326) -> DecodeAdmission {
327 super::extractor::clear_shared_candidates();
328 super::extractor::prime_shared_candidates(&chunk.data);
329
330 let mut aggregate = DecodeAdmission::Impossible;
331 for decoder in plan.decoders() {
332 match decoder.admission(chunk, policy) {
333 DecodeAdmission::Possible => {
334 aggregate = DecodeAdmission::Possible;
335 break;
336 }
337 DecodeAdmission::Unknown => aggregate = DecodeAdmission::Unknown,
338 DecodeAdmission::Impossible => {}
339 }
340 }
341
342 super::extractor::clear_shared_candidates();
343 aggregate
344}
345
346#[cfg(any(feature = "decode", test))]
347pub(crate) fn decoder_admission_sketch(
348 chunk: &keyhog_core::Chunk,
349 policy: &super::super::policy::CompiledDecodeTransformPolicy,
350 plan: &CompiledDecoderPlan,
351) -> DecodeAdmissionSketch {
352 decoder_admission_sketch_with_decoders(chunk, policy, plan.decoders())
353}
354
355#[cfg(any(feature = "decode", test))]
356pub(crate) fn active_decoder_admission_sketch(
357 chunk: &keyhog_core::Chunk,
358 policy: &super::super::policy::CompiledDecodeTransformPolicy,
359) -> DecodeAdmissionSketch {
360 let decoders = active_decoders();
361 decoder_admission_sketch_with_decoders(chunk, policy, &decoders)
362}
363
364#[cfg(any(feature = "decode", test))]
365fn decoder_admission_sketch_with_decoders(
366 chunk: &keyhog_core::Chunk,
367 policy: &super::super::policy::CompiledDecodeTransformPolicy,
368 decoders: &[RegisteredDecoder],
369) -> DecodeAdmissionSketch {
370 super::extractor::clear_shared_candidates();
371 super::extractor::prime_shared_candidates(&chunk.data);
372
373 let mut aggregate = DecodeAdmissionSketch::NONE;
374 for decoder in decoders {
375 aggregate.merge(decoder.admission_sketch(chunk, policy));
376 }
377
378 super::extractor::clear_shared_candidates();
379 aggregate
380}
381
382fn decoder_registry() -> &'static RwLock<DecoderRegistryState> {
383 DECODERS.get_or_init(|| {
384 RwLock::new(DecoderRegistryState {
385 decoders: Arc::new(default_decoders()),
386 compatibility_failure: None,
387 })
388 })
389}
390
391#[cfg(not(test))]
392pub(super) fn active_decoders() -> Arc<Vec<RegisteredDecoder>> {
393 Arc::clone(&decoder_registry().read().decoders)
397}
398
399#[cfg(test)]
400pub(super) fn active_decoders() -> Arc<Vec<RegisteredDecoder>> {
401 let base = Arc::clone(&decoder_registry().read().decoders);
402 THREAD_DECODERS.with(|thread_decoders| {
403 let thread = thread_decoders.borrow();
404 if thread.is_empty() {
405 base
408 } else {
409 let mut combined = (*base).clone();
410 combined.extend(thread.iter().cloned().map(RegisteredDecoder::Shared));
411 Arc::new(combined)
412 }
413 })
414}
415
416#[cfg(not(test))]
417fn snapshot_decoders() -> Result<Arc<Vec<RegisteredDecoder>>, DecoderRegistrationError> {
418 let registry = decoder_registry().read();
419 if let Some(error) = registry.compatibility_failure.clone() {
420 Err(error)
421 } else {
422 Ok(Arc::clone(®istry.decoders))
423 }
424}
425
426#[cfg(test)]
427fn snapshot_decoders() -> Result<Arc<Vec<RegisteredDecoder>>, DecoderRegistrationError> {
428 let base = {
429 let registry = decoder_registry().read();
430 if let Some(error) = registry.compatibility_failure.clone() {
431 return Err(error);
432 }
433 Arc::clone(®istry.decoders)
434 };
435 THREAD_DECODERS.with(|thread_decoders| {
436 let thread = thread_decoders.borrow();
437 if thread.is_empty() {
438 Ok(base)
439 } else {
440 let mut combined = (*base).clone();
441 combined.extend(thread.iter().cloned().map(RegisteredDecoder::Shared));
442 Ok(Arc::new(combined))
443 }
444 })
445}
446
447pub fn register_decoder(decoder: Box<dyn Decoder>) {
454 if let Err(error) = register_decoder_inner(decoder, true) {
455 tracing::error!(%error, "decoder registration failed; later scanner compilation will fail");
456 }
457}
458
459pub fn try_register_decoder(decoder: Box<dyn Decoder>) -> Result<(), DecoderRegistrationError> {
465 register_decoder_inner(decoder, false)
466}
467
468fn register_decoder_inner(
469 decoder: Box<dyn Decoder>,
470 record_failure: bool,
471) -> Result<(), DecoderRegistrationError> {
472 let decoder_name = decoder.name();
473 let mut guard = decoder_registry().write();
474 if let Some(error) = guard.compatibility_failure.clone() {
475 return Err(error);
476 }
477 let result = validate_descriptor(decoder_name, decoder.version()).and_then(|()| {
478 if guard
479 .decoders
480 .iter()
481 .any(|existing| existing.name() == decoder_name)
482 {
483 Err(DecoderRegistrationError::DuplicateName(decoder_name))
484 } else {
485 Ok(())
486 }
487 });
488 if let Err(error) = result {
489 if record_failure && guard.compatibility_failure.is_none() {
490 guard.compatibility_failure = Some(error.clone());
491 }
492 return Err(error);
493 }
494 let mut next = (*guard.decoders).clone();
499 next.push(RegisteredDecoder::Shared(Arc::from(decoder)));
500 guard.decoders = Arc::new(next);
501 Ok(())
502}
503
504#[cfg(test)]
505pub(crate) struct ScopedDecoderRegistration {
506 name: &'static str,
507 active: bool,
508}
509
510#[cfg(test)]
511impl Drop for ScopedDecoderRegistration {
512 fn drop(&mut self) {
513 if !self.active {
514 return;
515 }
516 THREAD_DECODERS.with(|thread_decoders| {
517 let mut decoders = thread_decoders.borrow_mut();
518 if let Some(index) = decoders
519 .iter()
520 .rposition(|decoder| decoder.name() == self.name)
521 {
522 decoders.remove(index);
523 }
524 });
525 }
526}
527
528#[cfg(test)]
529pub(crate) fn register_thread_decoder(decoder: Box<dyn Decoder>) -> ScopedDecoderRegistration {
530 let decoder_name = decoder.name();
531 let has_global_duplicate = decoder_registry()
532 .read()
533 .decoders
534 .iter()
535 .any(|existing| existing.name() == decoder_name);
536 if has_global_duplicate {
537 tracing::warn!(
538 decoder = decoder_name,
539 "register_thread_decoder called with a duplicate global decoder name; decoder ignored"
540 );
541 return ScopedDecoderRegistration {
542 name: decoder_name,
543 active: false,
544 };
545 }
546
547 let mut inserted = false;
548 let decoder = Arc::from(decoder);
549 THREAD_DECODERS.with(|thread_decoders| {
550 let mut decoders = thread_decoders.borrow_mut();
551 if decoders.iter().any(|existing| existing.name() == decoder_name) {
552 tracing::warn!(
553 decoder = decoder_name,
554 "register_thread_decoder called with a duplicate thread decoder name; decoder ignored"
555 );
556 return;
557 }
558 decoders.push(decoder);
559 inserted = true;
560 });
561 ScopedDecoderRegistration {
562 name: decoder_name,
563 active: inserted,
564 }
565}
566
567#[cfg(test)]
568#[path = "../../../tests/unit/decode_admission_soundness.rs"]
569mod admission_soundness_tests;