mod stable_hash;
use std::io::Write;
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::{LazyLock, Mutex};
pub(crate) static SCANNED_CHUNKS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static SCANNED_BYTES: AtomicU64 = AtomicU64::new(0);
pub(crate) static TOTAL_CHUNKS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static FINDINGS_COUNT: AtomicUsize = AtomicUsize::new(0);
pub(crate) static GPU_SCANNED_CHUNKS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static BACKEND_RECOVERY_EVENTS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static BACKEND_RECOVERED_CHUNKS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static BACKEND_RECOVERED_BYTES: AtomicU64 = AtomicU64::new(0);
const MAX_BACKEND_RECOVERY_SUMMARY_ROWS: usize = 256;
const BACKEND_RECOVERY_OVERFLOW_REASON: &str =
"additional distinct backend faults; inspect stderr and autoroute runtime health";
pub(crate) static BACKEND_RECOVERY_SUMMARIES: LazyLock<
Mutex<Vec<keyhog_core::ScanBackendRecoverySummary>>,
> = LazyLock::new(|| Mutex::new(Vec::new()));
pub(crate) static SOURCE_ERRORS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static FAILED_SOURCES: AtomicUsize = AtomicUsize::new(0);
pub(crate) static INCREMENTAL_CACHE_ERRORS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static AUTOROUTE_PERSIST_ERRORS: AtomicUsize = AtomicUsize::new(0);
pub(crate) static BATCHES_NOT_ROUTED: AtomicUsize = AtomicUsize::new(0);
pub(crate) static SCANNER_PANICKED: AtomicBool = AtomicBool::new(false);
static OPERATOR_PROFILE_ACTIVE: AtomicBool = AtomicBool::new(false);
pub fn operator_profile_active() -> bool {
OPERATOR_PROFILE_ACTIVE.load(Ordering::Relaxed)
}
pub(crate) fn set_operator_profile_active(active: bool) {
OPERATOR_PROFILE_ACTIVE.store(active, Ordering::Relaxed);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScanFailureEvent {
SourceError,
FailedSource,
IncrementalCachePersistFailed,
AutoroutePersistFailed,
BatchNotRouted,
ScannerPanicked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use = "scan failure events must be recorded through the typed recorder so exit/status semantics remain honest"]
pub(crate) struct RecordedScanFailureEvent {
event: ScanFailureEvent,
previous: usize,
}
pub(crate) fn record_scan_failure(event: ScanFailureEvent) -> RecordedScanFailureEvent {
let previous = match event {
ScanFailureEvent::SourceError => SOURCE_ERRORS.fetch_add(1, Ordering::Relaxed),
ScanFailureEvent::FailedSource => FAILED_SOURCES.fetch_add(1, Ordering::Relaxed),
ScanFailureEvent::IncrementalCachePersistFailed => {
INCREMENTAL_CACHE_ERRORS.fetch_add(1, Ordering::Relaxed)
}
ScanFailureEvent::AutoroutePersistFailed => {
AUTOROUTE_PERSIST_ERRORS.fetch_add(1, Ordering::Relaxed)
}
ScanFailureEvent::BatchNotRouted => BATCHES_NOT_ROUTED.fetch_add(1, Ordering::Relaxed),
ScanFailureEvent::ScannerPanicked => {
let was_panicked = SCANNER_PANICKED.swap(true, Ordering::Relaxed);
usize::from(was_panicked)
}
};
RecordedScanFailureEvent { event, previous }
}
pub(crate) fn record_source_error() -> RecordedScanFailureEvent {
record_scan_failure(ScanFailureEvent::SourceError)
}
pub(crate) fn record_failed_source() -> RecordedScanFailureEvent {
record_scan_failure(ScanFailureEvent::FailedSource)
}
pub(crate) fn record_incremental_cache_persist_failed() -> RecordedScanFailureEvent {
record_scan_failure(ScanFailureEvent::IncrementalCachePersistFailed)
}
pub(crate) fn record_autoroute_persist_failed() -> RecordedScanFailureEvent {
record_scan_failure(ScanFailureEvent::AutoroutePersistFailed)
}
pub(crate) fn record_batch_not_routed() -> RecordedScanFailureEvent {
record_scan_failure(ScanFailureEvent::BatchNotRouted)
}
pub(crate) fn record_scanner_panic() -> RecordedScanFailureEvent {
record_scan_failure(ScanFailureEvent::ScannerPanicked)
}
pub fn interrupt_counts() -> (usize, usize, usize) {
(
SCANNED_CHUNKS.load(Ordering::Relaxed),
TOTAL_CHUNKS.load(Ordering::Relaxed),
FINDINGS_COUNT.load(Ordering::Relaxed),
)
}
pub(crate) fn reset_scan_runtime_state() {
SCANNED_CHUNKS.store(0, Ordering::Relaxed);
SCANNED_BYTES.store(0, Ordering::Relaxed);
TOTAL_CHUNKS.store(0, Ordering::Relaxed);
FINDINGS_COUNT.store(0, Ordering::Relaxed);
GPU_SCANNED_CHUNKS.store(0, Ordering::Relaxed);
BACKEND_RECOVERY_EVENTS.store(0, Ordering::Relaxed);
BACKEND_RECOVERED_CHUNKS.store(0, Ordering::Relaxed);
BACKEND_RECOVERED_BYTES.store(0, Ordering::Relaxed);
match BACKEND_RECOVERY_SUMMARIES.lock() {
Ok(mut summaries) => summaries.clear(),
Err(poisoned) => {
BACKEND_RECOVERY_SUMMARIES.clear_poison();
poisoned.into_inner().clear();
}
}
SOURCE_ERRORS.store(0, Ordering::Relaxed);
FAILED_SOURCES.store(0, Ordering::Relaxed);
INCREMENTAL_CACHE_ERRORS.store(0, Ordering::Relaxed);
AUTOROUTE_PERSIST_ERRORS.store(0, Ordering::Relaxed);
BATCHES_NOT_ROUTED.store(0, Ordering::Relaxed);
SCANNER_PANICKED.store(false, Ordering::Relaxed);
keyhog_scanner::telemetry::reset_for_scan();
}
pub(crate) fn record_backend_recovery_summary(summary: keyhog_core::ScanBackendRecoverySummary) {
let mut summaries = match BACKEND_RECOVERY_SUMMARIES.lock() {
Ok(summaries) => summaries,
Err(poisoned) => {
BACKEND_RECOVERY_SUMMARIES.clear_poison();
poisoned.into_inner()
}
};
if let Some(existing) = summaries.iter_mut().find(|existing| {
existing.failed_backend == summary.failed_backend
&& existing.recovery_backend == summary.recovery_backend
&& existing.reason == summary.reason
&& existing.repair_command == summary.repair_command
}) {
existing.events = existing.events.saturating_add(summary.events);
existing.recovered_ranges = existing
.recovered_ranges
.saturating_add(summary.recovered_ranges);
existing.recovered_chunks = existing
.recovered_chunks
.saturating_add(summary.recovered_chunks);
existing.recovered_bytes = existing
.recovered_bytes
.saturating_add(summary.recovered_bytes);
return;
}
if summaries.len() + 1 < MAX_BACKEND_RECOVERY_SUMMARY_ROWS {
summaries.push(summary);
return;
}
if let Some(overflow) = summaries
.iter_mut()
.find(|existing| existing.reason == BACKEND_RECOVERY_OVERFLOW_REASON)
{
overflow.events = overflow.events.saturating_add(summary.events);
overflow.recovered_ranges = overflow
.recovered_ranges
.saturating_add(summary.recovered_ranges);
overflow.recovered_chunks = overflow
.recovered_chunks
.saturating_add(summary.recovered_chunks);
overflow.recovered_bytes = overflow
.recovered_bytes
.saturating_add(summary.recovered_bytes);
} else {
summaries.push(keyhog_core::ScanBackendRecoverySummary {
events: summary.events,
failed_backend: "multiple".to_string(),
recovery_backend: "multiple".to_string(),
recovered_ranges: summary.recovered_ranges,
recovered_chunks: summary.recovered_chunks,
recovered_bytes: summary.recovered_bytes,
reason: BACKEND_RECOVERY_OVERFLOW_REASON.to_string(),
repair_command: summary.repair_command,
});
}
}
pub(crate) fn backend_recovery_summaries() -> Vec<keyhog_core::ScanBackendRecoverySummary> {
match BACKEND_RECOVERY_SUMMARIES.lock() {
Ok(summaries) => summaries.clone(),
Err(poisoned) => {
BACKEND_RECOVERY_SUMMARIES.clear_poison();
poisoned.into_inner().clone()
}
}
}
pub(crate) fn write_banner<W: Write>(
w: &mut W,
colors: bool,
detector_count: usize,
) -> std::io::Result<()> {
let palette = style::terminal_palette(colors, false);
if colors {
writeln!(w, " {}K E Y H O G{}", palette.bold, palette.reset)?;
writeln!(w, " {}───────────{}", palette.dim, palette.reset)?;
writeln!(
w,
" {}v{} · secret scanner · {} detectors{}",
palette.green,
env!("CARGO_PKG_VERSION"),
detector_count,
palette.reset
)?;
writeln!(w, " {}by santh{}", palette.dim, palette.reset)?;
} else {
writeln!(w, " K E Y H O G")?;
writeln!(w, " ───────────")?;
writeln!(
w,
" v{} · secret scanner · {} detectors",
env!("CARGO_PKG_VERSION"),
detector_count
)?;
writeln!(w, " by santh")?;
}
writeln!(w)?;
Ok(())
}
fn exit_now(code: u8) -> ! {
use std::io::Write;
if let Err(error) = std::io::stdout().flush() {
eprintln!("keyhog: stdout flush failed before immediate exit: {error}");
}
let _ = std::io::stderr().flush();
#[cfg(unix)]
unsafe {
libc::_exit(i32::from(code))
}
#[cfg(not(unix))]
std::process::exit(i32::from(code))
}
pub async fn cli_main() -> ExitCode {
let startup_span = keyhog_profile::span(keyhog_profile::Stage::Preprocess);
let mut is_version = false;
let mut full_version = false;
let mut maintenance_subcommand_seen = false;
for arg in std::env::args_os().skip(1) {
if let Some(value) = arg.to_str() {
maintenance_subcommand_seen |= value == "update" || value == "repair";
is_version |= value == "-V" || (value == "--version" && !maintenance_subcommand_seen);
full_version |= value == "--full";
}
}
if is_version {
print_version_info(full_version);
return ExitCode::SUCCESS;
}
#[cfg(not(unix))]
tokio::spawn(async move {
if let Ok(()) = tokio::signal::ctrl_c().await {
let (scanned, total, findings) = interrupt_counts();
eprintln!("\nScan interrupted. {scanned}/{total} files scanned. {findings} findings.");
if operator_profile_active() {
eprintln!(
"profile outcome status=failed coverage=cancelled errors=1 exit=130 interruption=ctrl-c"
);
}
std::process::exit(i32::from(exit_codes::EXIT_INTERRUPTED));
}
});
let log_ansi = {
use std::io::IsTerminal;
std::io::stderr().is_terminal() && !crate::style::no_color_requested()
};
let default_log_directive = match "keyhog=warn".parse() {
Ok(directive) => directive,
Err(error) => {
tracing::warn!(
%error,
"failed to parse built-in logging directive; enabling info-level logs"
);
tracing_subscriber::filter::Directive::from(tracing::Level::INFO)
}
};
{
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(log_ansi)
.with_target(false);
let fmt_layer =
tracing_subscriber::Layer::with_filter(fmt_layer, log_dedup::WarnRepeatLimit);
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(default_log_directive),
)
.with(fmt_layer)
.init();
}
let _warn_dedup_summary = log_dedup::WarnDedupSummaryGuard;
drop(startup_span);
let cli = args::parse();
if cli.build_version {
print_version_info(cli.full);
return ExitCode::SUCCESS;
}
let command_outcome = match cli.command {
Some(args::Command::Scan(args)) => {
let profile_requested = args.profile;
set_operator_profile_active(profile_requested);
let outcome = subcommands::scan::run(*args).await;
if profile_requested {
set_operator_profile_active(false);
}
outcome
}
Some(args::Command::Config(args)) => subcommands::config::run(*args),
Some(args::Command::CompileExecutionPacks(args)) => {
subcommands::compile_execution_packs::run(args).map(|()| ExitCode::SUCCESS)
}
Some(args::Command::ActionReport(args)) => match args.command {
args::ActionReportCommand::Verify(args) => action_report::verify(args),
},
Some(args::Command::Hook { command }) => subcommands::hook::run(command),
Some(args::Command::Detectors(args)) => subcommands::detectors::run(args),
Some(args::Command::Explain(args)) => {
subcommands::explain::run(args).map(|()| ExitCode::SUCCESS)
}
Some(args::Command::Diff(args)) => subcommands::diff::run(args).await,
Some(args::Command::Calibrate(args)) => {
subcommands::calibrate::run(args).map(|()| ExitCode::SUCCESS)
}
Some(args::Command::CalibrateAutoroute(args)) => {
subcommands::calibrate_autoroute::run(args)
}
Some(args::Command::Watch(args)) => {
subcommands::watch::run(args).map(|()| ExitCode::SUCCESS)
}
Some(args::Command::Completion(args)) => {
subcommands::completion::run(args);
return ExitCode::SUCCESS;
}
Some(args::Command::Backend(args)) => subcommands::backend::run(args),
Some(args::Command::Doctor(args)) => subcommands::doctor::run(args),
Some(args::Command::BloomDiagnostic(args)) => bloom_diagnostic::run(args),
Some(args::Command::Update(args)) => subcommands::update::run(args).await,
Some(args::Command::Repair(args)) => subcommands::repair::run(args).await,
Some(args::Command::Uninstall(args)) => subcommands::uninstall::run(args),
Some(args::Command::ScanSystem(args)) => subcommands::scan_system::run(args),
#[cfg(unix)]
Some(args::Command::Daemon(args)) => subcommands::daemon::run(args).await,
#[cfg(not(unix))]
Some(args::Command::Daemon(_args)) => Err(anyhow::anyhow!(
"`keyhog daemon` is a unix-only command (it serves scans over a \
Unix-domain socket). On Windows, run scans in-process: \
`keyhog scan <path>`. No Windows daemon transport ships."
)),
#[cfg(unix)]
Some(args::Command::Guard(args)) => subcommands::guard::run(args).await,
#[cfg(not(unix))]
Some(args::Command::Guard(_args)) => Err(anyhow::anyhow!(
"`keyhog guard` requires the Unix daemon transport. On Windows, \
run `keyhog scan <path>` in process; no guard daemon ships."
)),
None => {
let mut cmd = args::command();
let _ = cmd.print_help(); return ExitCode::SUCCESS;
}
};
match command_outcome {
Ok(outcome) => {
if SCANNER_PANICKED.load(Ordering::Relaxed) {
exit_now(exit_codes::EXIT_SCANNER_PANIC);
} else {
outcome
}
}
Err(error) => {
eprintln!("error: {error:#}");
let code = cli_error_exit_code(&error);
exit_now(code);
}
}
}
fn cli_error_exit_code(error: &anyhow::Error) -> u8 {
if SCANNER_PANICKED.load(Ordering::SeqCst) {
exit_codes::EXIT_SCANNER_PANIC
} else if error
.chain()
.any(|cause| cause.is::<orchestrator::GpuUnavailableError>())
{
exit_codes::EXIT_REQUIRE_GPU_UNMET
} else if error.chain().any(|cause| {
matches!(
cause.downcast_ref::<keyhog_scanner::ScanError>(),
Some(keyhog_scanner::ScanError::Gpu(_))
)
}) {
exit_codes::EXIT_REQUIRE_GPU_UNMET
} else if error.chain().any(|cause| {
matches!(
cause.downcast_ref::<keyhog_scanner::ScanError>(),
Some(keyhog_scanner::ScanError::Simd(_))
)
}) {
exit_codes::EXIT_SYSTEM_ERROR
} else if is_daemon_service_failure(error) {
exit_codes::EXIT_SYSTEM_ERROR
} else if error.chain().any(is_user_io_error) {
exit_codes::EXIT_USER_ERROR
} else if error.chain().any(|cause| cause.is::<std::io::Error>()) {
exit_codes::EXIT_SYSTEM_ERROR
} else {
exit_codes::EXIT_USER_ERROR
}
}
#[cfg(unix)]
fn is_daemon_service_failure(error: &anyhow::Error) -> bool {
error
.chain()
.any(|cause| cause.is::<daemon::server::DaemonServiceFailure>())
}
#[cfg(not(unix))]
fn is_daemon_service_failure(_error: &anyhow::Error) -> bool {
false
}
fn is_user_io_error(error: &(dyn std::error::Error + 'static)) -> bool {
let Some(io) = error.downcast_ref::<std::io::Error>() else {
return false;
};
matches!(
io.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::InvalidInput
| std::io::ErrorKind::InvalidData
| std::io::ErrorKind::AlreadyExists
)
}
fn print_version_info(full: bool) {
println!("KeyHog v{}", env!("CARGO_PKG_VERSION"));
println!("Commit: {}", keyhog_core::git_hash());
println!(
"Detector Set: {} ({})",
keyhog_core::embedded_detector_count(),
keyhog_core::detector_digest()
);
println!(
"Build Target: {}-{}",
std::env::consts::ARCH,
std::env::consts::OS
);
println!(
"ML Model Version: {}",
keyhog_scanner::ml_scorer::model_version()
);
println!(
"ML Model Card: {}",
keyhog_scanner::ml_scorer::model_card_summary()
);
if !full {
return;
}
let hw = keyhog_scanner::hw_probe::probe_hardware();
if hw.gpu_available {
println!(
"GPU Acceleration: {}{}",
hw.gpu_name.as_deref().unwrap_or("available"), hw.gpu_vram_mb
.map(|mb| {
if mb >= 1024 {
format!(" (max buffer {} GB)", mb / 1024)
} else {
format!(" (max buffer {mb} MB)")
}
})
.unwrap_or_default() );
} else {
println!("GPU Acceleration: not detected");
}
if hw.hyperscan_available {
println!("SIMD Regex: vectorscan/hyperscan (active)");
} else if hw.has_avx512 || hw.has_avx2 || hw.has_neon {
let simd = if hw.has_avx512 {
"AVX-512"
} else if hw.has_avx2 {
"AVX2"
} else {
"NEON"
};
println!("SIMD Regex: {simd} (no Hyperscan)");
} else {
println!("SIMD Regex: not available");
}
if hw.io_uring_available {
println!("io_uring: available");
}
}
pub(crate) mod action_report;
pub mod args;
pub(crate) mod atomic_file;
pub(crate) mod autoroute_cache_path;
pub(crate) mod baseline;
pub(crate) mod benchmark;
pub(crate) mod bloom_diagnostic;
pub(crate) mod config;
mod execution_pack_install;
pub mod exit_codes;
pub(crate) mod format;
pub(crate) mod installer;
pub(crate) mod log_dedup;
pub(crate) mod matcher_cache_path;
pub(crate) mod runtime_preflight;
#[cfg(test)]
mod cli_reference;
#[cfg(unix)]
pub mod daemon;
pub(crate) mod inline_suppression;
pub(crate) mod orchestrator;
pub(crate) mod orchestrator_config;
pub(crate) mod path_validation;
pub(crate) mod reporting;
pub(crate) mod skip_dirs;
pub(crate) mod sources;
mod style;
pub(crate) mod subcommands;
pub(crate) mod test_fixture_suppressions;
#[cfg(test)]
extern crate self as keyhog;
#[cfg(test)]
#[path = "../tests/unit/docs_help_coherence.rs"]
mod docs_help_coherence;
pub mod testing;
#[doc(hidden)]
pub mod profiling_test_seams {
pub fn load_rule_suppressor(
scan_path: Option<&std::path::Path>,
) -> anyhow::Result<keyhog_core::RuleSuppressor> {
crate::orchestrator::load_rule_suppressor(scan_path)
}
pub fn filter_rule_suppressed(
suppressor: &keyhog_core::RuleSuppressor,
matches: Vec<keyhog_core::RawMatch>,
) -> Vec<keyhog_core::RawMatch> {
crate::subcommands::watch::filter_rule_suppressed(suppressor, matches)
}
pub fn load_detector_corpus(
path: &std::path::Path,
) -> anyhow::Result<Vec<keyhog_core::DetectorSpec>> {
crate::subcommands::detectors::load_detector_corpus(path)
}
pub fn doctor_host_probe() -> &'static keyhog_scanner::hw_probe::HardwareCaps {
crate::subcommands::doctor::collect_host_probe()
}
}
pub(crate) mod value_parsers;