use keyhog_core::{Severity, VerifiedFinding};
use std::io::Write;
use crate::style::terminal_palette;
use crate::style::{
SEV_AMBER as C_AMBER, SEV_BRAND as C_BRAND, SEV_CRITICAL as C_CRITICAL, SEV_HIGH as C_HIGH,
SEV_LOW as C_LOW, SEV_MEDIUM as C_MEDIUM, SEV_MUTED as C_MUTED, SEV_RESET as C_RESET,
SEV_SAFE as C_SAFE,
};
mod progress;
pub(crate) use progress::{
fmt_secs, progress_ticker, render_progress_bar, render_reporting_ticker_line,
render_ticker_line, render_verification_ticker_line, reporting_ticker, TickerGuard,
};
#[cfg(feature = "verify")]
pub(crate) use progress::verification_ticker;
#[cfg(test)]
pub(crate) use progress::{BAR_WIDTH, FRAMES};
pub(crate) fn stream_finding_preview<W: Write>(w: &mut W, f: &VerifiedFinding) {
let path = f.location.file_path.as_deref().unwrap_or("<stdin>"); let line = f
.location
.line
.map(|n| n.to_string())
.unwrap_or_else(|| "?".into()); if let Err(error) = writeln!(
w,
"[stream] {sev:<8} {service}/{detector} {path}:{line} {redacted}",
sev = f.severity.as_str().to_uppercase(),
service = f.service,
detector = f.detector_id,
path = path,
line = line,
redacted = f.credential_redacted,
) {
tracing::debug!(%error, "stream finding preview write error");
}
}
pub(crate) fn stream_report_previews(findings: &[VerifiedFinding]) {
if findings.is_empty() {
return;
}
let mut w = std::io::LineWriter::new(std::io::stderr());
for f in findings {
stream_finding_preview(&mut w, f);
}
let _ = w.flush(); }
pub(crate) fn scanner_panic_notice(panicked: bool) -> Option<String> {
panicked.then(|| {
"SCAN INCOMPLETE: the scanner thread panicked mid-scan. The findings below \
are PARTIAL: chunks in flight when it crashed were NOT scanned, so a \
\"0 secrets\" / low count is NOT a clean result. The process exits with a \
distinct scanner-panic code. Re-run; if it persists, file a bug with the \
input that triggered it."
.to_string()
})
}
#[derive(Default, Debug, PartialEq, Eq)]
pub(crate) struct VerificationBreakdown {
pub live: usize,
pub inactive: usize,
pub skipped: usize,
pub unverifiable: usize,
pub incomplete: usize,
}
pub(crate) fn verification_breakdown(findings: &[VerifiedFinding]) -> VerificationBreakdown {
use keyhog_core::VerificationResult as V;
let mut b = VerificationBreakdown::default();
for f in findings {
match &f.verification {
V::Live => b.live += 1,
V::Revoked | V::Dead => b.inactive += 1,
V::Skipped => b.skipped += 1,
V::Unverifiable => b.unverifiable += 1,
V::RateLimited | V::Error(_) => b.incomplete += 1,
}
}
b
}
fn count_token(count: usize, label: &str, color_code: &str, color: bool) -> String {
crate::style::paint(format!("{count} {label}"), color_code, color)
}
pub(super) fn secret_noun(count: usize) -> &'static str {
if count == 1 {
"secret"
} else {
"secrets"
}
}
pub(super) fn finding_noun(count: usize) -> &'static str {
if count == 1 {
"finding"
} else {
"findings"
}
}
pub(super) fn dot_join(parts: &[String], color: bool) -> String {
let sep = if color {
format!("{C_MUTED} · {C_RESET}")
} else {
" · ".to_string()
};
parts.join(&sep)
}
fn severity_color(severity: Severity) -> &'static str {
match severity {
Severity::Critical => C_CRITICAL,
Severity::High => C_HIGH,
Severity::Medium => C_MEDIUM,
Severity::Low => C_LOW,
Severity::ClientSafe => C_SAFE,
Severity::Info => C_MUTED,
}
}
pub(crate) fn render_severity_line(findings: &[VerifiedFinding], color: bool) -> Option<String> {
if findings.is_empty() {
return None;
}
let mut critical = 0usize;
let mut high = 0usize;
let mut medium = 0usize;
let mut low = 0usize;
let mut client_safe = 0usize;
let mut info = 0usize;
for finding in findings {
match finding.severity {
Severity::Critical => critical += 1,
Severity::High => high += 1,
Severity::Medium => medium += 1,
Severity::Low => low += 1,
Severity::ClientSafe => client_safe += 1,
Severity::Info => info += 1,
}
}
let counts = [
(Severity::Critical, critical),
(Severity::High, high),
(Severity::Medium, medium),
(Severity::Low, low),
(Severity::ClientSafe, client_safe),
(Severity::Info, info),
];
let parts: Vec<String> = counts
.into_iter()
.filter(|(_, count)| *count > 0)
.map(|(severity, count)| {
count_token(count, severity.as_str(), severity_color(severity), color)
})
.collect();
let (muted, reset) = if color { (C_MUTED, C_RESET) } else { ("", "") };
Some(format!(
"{muted}↳ severity: {reset}{}",
dot_join(&parts, color)
))
}
pub(crate) fn render_verification_line(
b: &VerificationBreakdown,
total: usize,
color: bool,
) -> Option<String> {
if total == 0 {
return None;
}
let (muted, brand, amber, reset) = if color {
(C_MUTED, C_BRAND, C_AMBER, C_RESET)
} else {
("", "", "", "")
};
if b.skipped == total {
return Some(format!(
"{muted}↳ verification: {amber}not checked{reset}{muted}: liveness check did not run; pass {brand}--verify{reset}{muted} \
to confirm which are active{reset}"
));
}
let mut parts: Vec<String> = Vec::new();
if b.live > 0 {
parts.push(count_token(b.live, "live", C_CRITICAL, color));
}
if b.inactive > 0 {
parts.push(count_token(b.inactive, "revoked/dead", C_SAFE, color));
}
if b.skipped > 0 {
parts.push(count_token(b.skipped, "not checked", C_AMBER, color));
}
if b.unverifiable > 0 {
parts.push(count_token(b.unverifiable, "no verifier", C_AMBER, color));
}
if b.incomplete > 0 {
parts.push(count_token(b.incomplete, "inconclusive", C_AMBER, color));
}
Some(format!(
"{muted}↳ verification: {reset}{}",
dot_join(&parts, color)
))
}
pub(crate) fn report_completion_summary(
findings: &[VerifiedFinding],
elapsed: f64,
ansi: bool,
backend_override: Option<keyhog_scanner::ScanBackend>,
) {
let count = findings.len();
let palette = terminal_palette(ansi, false);
let completion =
if crate::BACKEND_RECOVERY_EVENTS.load(std::sync::atomic::Ordering::Relaxed) > 0 {
"Scan complete after recovery."
} else {
"Scan complete."
};
if let Some(notice) =
scanner_panic_notice(crate::SCANNER_PANICKED.load(std::sync::atomic::Ordering::Relaxed))
{
eprintln!("{}FAIL{} {notice}", palette.red, palette.reset);
}
if count == 0 {
eprintln!(
"\n{completion} Found {}0{} secrets in {}{:.2}s{}.",
palette.green, palette.reset, palette.yellow, elapsed, palette.reset
);
} else {
let noun = secret_noun(count);
eprintln!(
"\n{completion} Found {}{}{} {} in {}{:.2}s{}.",
palette.red, count, palette.reset, noun, palette.yellow, elapsed, palette.reset
);
if let Some(line) = render_severity_line(findings, ansi) {
eprintln!("{line}");
}
if let Some(line) = render_verification_line(&verification_breakdown(findings), count, ansi)
{
eprintln!("{line}");
}
}
report_skip_summary(ansi);
report_backend_summary(ansi, backend_override);
}
pub(crate) fn report_backend_summary(
ansi: bool,
backend_override: Option<keyhog_scanner::ScanBackend>,
) {
use std::sync::atomic::Ordering;
let total = crate::SCANNED_CHUNKS.load(Ordering::Relaxed);
if total == 0 {
return;
}
let gpu = crate::GPU_SCANNED_CHUNKS.load(Ordering::Relaxed).min(total);
let non_gpu = total - gpu;
let recovery_events = crate::BACKEND_RECOVERY_EVENTS.load(Ordering::Relaxed);
let recovered_chunks = crate::BACKEND_RECOVERED_CHUNKS.load(Ordering::Relaxed);
let recovered_bytes = crate::BACKEND_RECOVERED_BYTES.load(Ordering::Relaxed);
let hw = keyhog_scanner::hw_probe::probe_hardware();
let line = if let Some(backend) = backend_override {
format!("backend: {} (forced via --backend)", backend.label())
} else if recovery_events > 0 {
format!(
"backend: an automatic route faulted and completed through exact recovery; recovered {recovered_chunks} chunk(s), {recovered_bytes} byte(s) across {recovery_events} event(s); scan coverage is complete; repair: keyhog calibrate-autoroute"
)
} else if gpu > 0 && non_gpu > 0 {
format!(
"backend: calibrated GPU route ({gpu} chunk(s)) + calibrated non-GPU route ({non_gpu} chunk(s)); inspect `keyhog backend --autoroute` for exact per-bucket routes"
)
} else if gpu > 0 {
"backend: calibrated GPU driver peer (inspect `keyhog backend --autoroute` for the exact route)".to_string()
} else if hw.gpu_available && !hw.gpu_is_software {
let name = hw.gpu_name.as_deref().unwrap_or("a GPU").trim().to_string(); format!(
"backend: calibrated non-GPU route; {name} was eligible but was not the \
fastest measured-correct route for the exact workload bucket(s) scanned. \
Inspect the persisted decision with `keyhog backend --autoroute`; explicit \
`--backend gpu-cuda` or `--backend gpu-wgpu` is diagnostic only."
)
} else {
"backend: calibrated non-GPU route (no hardware GPU available on this host)".to_string()
};
let palette = terminal_palette(ansi, false);
eprintln!("{}INFO{} {line}", palette.cyan, palette.reset);
}
pub(crate) fn report_autoroute_cache_summary(ansi: bool, backend_forced: bool) {
if backend_forced {
return;
}
let stats = crate::orchestrator::dispatch::autoroute_cache_stats();
let Some(summary) = crate::orchestrator::dispatch::render_cache_summary(&stats) else {
return;
};
let palette = terminal_palette(ansi, false);
let label = if stats.misses > 0 {
format!("{}WARN{}", palette.yellow, palette.reset)
} else {
format!("{}INFO{}", palette.cyan, palette.reset)
};
eprintln!("{label} {summary}");
for bucket in crate::orchestrator::dispatch::render_missing_buckets(&stats) {
tracing::info!(
target: "keyhog::routing",
"uncalibrated autoroute bucket, {bucket}"
);
}
}
pub(crate) fn report_skip_summary(ansi: bool) {
use crate::reporting::{CoverageCounts, CoverageGapKind, CoverageSeverity};
let counts = CoverageCounts::current();
for kind in CoverageGapKind::ALL {
let n = kind.count(&counts);
if n == 0 {
continue;
}
let palette = terminal_palette(ansi, false);
let (label, color) = match kind.severity() {
CoverageSeverity::Fail => ("FAIL", palette.red),
CoverageSeverity::Warn => ("WARN", palette.yellow),
};
let msg = kind.human_reason(n);
eprintln!("{color}{label} {msg}{}", palette.reset);
}
}
pub(crate) fn dump_dogfood_trace() {
if !keyhog_scanner::telemetry::is_dogfood_enabled() {
return;
}
let events = keyhog_scanner::telemetry::drain_events();
let suppressed = keyhog_scanner::telemetry::example_suppression_count();
let static_recovery_rejections = keyhog_scanner::telemetry::static_recovery_rejection_counts();
let detail_events_dropped = keyhog_scanner::telemetry::dogfood_detail_events_dropped();
let payload = serde_json::json!({
"dogfood": {
"example_suppressions_total": suppressed,
"static_recovery_rejections": static_recovery_rejections,
"detail_events_dropped": detail_events_dropped,
"events": events,
}
});
eprintln!("{payload}");
}
#[cfg(test)]
mod tests;