use base64::Engine;
use std::io::{self, BufRead, Write};
fn b64_decode(field: &str) -> String {
if field.is_empty() {
return String::new();
}
let bytes = base64::engine::general_purpose::STANDARD
.decode(field.as_bytes())
.expect("valid base64 field");
String::from_utf8(bytes).expect("valid utf-8 field")
}
fn parse_list(decoded: &str) -> Vec<String> {
if decoded.is_empty() {
return Vec::new();
}
decoded.split('\n').map(|s| s.to_string()).collect()
}
fn resolve_detector(id: &str) -> Option<&'static keyhog_core::DetectorSpec> {
let finding_id = id.split(':').next().unwrap_or(id);
keyhog_core::detector_spec_by_id(finding_id).or_else(|| {
keyhog_core::embedded_detector_specs().iter().find(|spec| {
spec.entropy_fallback
.as_ref()
.is_some_and(|fallback| fallback.id == finding_id)
})
})
}
fn main() {
let stdin = io::stdin();
let stdout = io::stdout();
let mut out = io::BufWriter::new(stdout.lock());
for line in stdin.lock().lines() {
let line = line.expect("read line");
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split(' ').collect();
assert_eq!(fields.len(), 8, "expected 8 base64 fields per line");
let text = b64_decode(fields[0]);
let context = b64_decode(fields[1]);
let known_prefixes = parse_list(&b64_decode(fields[2]));
let secret_keywords = parse_list(&b64_decode(fields[3]));
let test_keywords = parse_list(&b64_decode(fields[4]));
let placeholder_keywords = parse_list(&b64_decode(fields[5]));
let detector_id = b64_decode(fields[6]);
let detector = resolve_detector(&detector_id)
.unwrap_or_else(|| panic!("unknown detector or entropy owner {detector_id:?}"));
let channel = match b64_decode(fields[7]).as_str() {
"pattern" => keyhog_scanner::ml_scorer::MlCandidateChannel::Pattern,
"entropy" => keyhog_scanner::ml_scorer::MlCandidateChannel::Entropy,
other => panic!("unknown ML candidate channel {other:?}"),
};
let features = keyhog_scanner::ml_scorer::compute_features_for_detector_with_config(
&text,
&context,
&known_prefixes,
&secret_keywords,
&test_keywords,
&placeholder_keywords,
detector,
channel,
);
let rendered: Vec<String> = features.iter().map(|v| format!("{v:.9}")).collect();
writeln!(out, "{}", rendered.join(" ")).expect("write line");
}
out.flush().expect("flush");
}