mod allowlist;
mod dispatch;
mod postprocess;
mod reporting;
mod run;
use crate::args::ScanArgs;
use crate::orchestrator_config::{
auto_discover_detectors, configure_threads, load_detectors_no_cache, load_detectors_with_cache,
resolve_scan_config, ResolvedScanConfig,
};
use anyhow::{Context, Result};
use keyhog_core::{DetectorSpec, RawMatch, Source};
use keyhog_scanner::{CompiledScanner, GpuInitPolicy};
use std::path::PathBuf;
use std::sync::Arc;
pub use run::{EXIT_LIVE_CREDENTIALS, EXIT_SCANNER_PANIC};
pub(crate) use postprocess::offline_finding_metadata;
#[doc(hidden)]
pub use dispatch::{backend_requires_legacy_gpu_pipeline_for_test, explicit_backend_override};
#[doc(hidden)]
pub fn gpu_init_policy_for_args_for_test(args: &ScanArgs) -> GpuInitPolicy {
gpu_init_policy_for_args(args)
}
#[doc(hidden)]
pub fn allowlist_root_for_test(path: &std::path::Path) -> std::path::PathBuf {
allowlist::allowlist_root(path)
}
pub struct ScanOrchestrator {
pub(crate) args: ScanArgs,
pub(crate) detectors: Vec<DetectorSpec>,
pub(crate) scanner: Arc<CompiledScanner>,
pub(crate) signatures: std::collections::HashSet<Arc<str>>,
pub(crate) test_fixture_suppressions: crate::test_fixture_suppressions::TestFixtureSuppressions,
pub(crate) disabled_detectors: std::collections::HashSet<String>,
pub(crate) detector_min_confidence: std::collections::HashMap<String, f64>,
pub(crate) effective_config: ResolvedScanConfig,
}
impl ScanOrchestrator {
pub fn new(mut args: ScanArgs) -> Result<Self> {
if matches!(args.input.as_deref().and_then(|p| p.to_str()), Some("-"))
|| matches!(args.path.as_deref().and_then(|p| p.to_str()), Some("-"))
{
args.stdin = true;
args.input = None;
args.path = None;
}
if args.path.is_none() {
args.path = args.input.clone();
}
#[cfg(feature = "git")]
if args.git_staged && args.path.is_none() {
args.path = Some(PathBuf::from("."));
}
let mut effective_config = resolve_scan_config(&mut args);
let disabled_detectors = effective_config.disabled_detectors.clone();
let mut detector_min_confidence = effective_config.detector_min_confidence.clone();
if effective_config.require_lockdown && !args.lockdown {
anyhow::bail!(
".keyhog.toml sets [lockdown] require = true, but --lockdown was not passed. \
Re-run with --lockdown to enforce the configured hardening, or remove the \
requirement from .keyhog.toml."
);
}
keyhog_scanner::types::set_regex_dfa_limit(args.regex_dfa_limit.unwrap_or(0));
let hw = keyhog_scanner::hw_probe::probe_hardware();
configure_threads(args.threads, hw.physical_cores);
let detectors_path = auto_discover_detectors(&args.detectors)?;
let mut detectors = if args.lockdown {
load_detectors_no_cache(&detectors_path)
.context("loading detectors (lockdown: cache disabled)")?
} else {
load_detectors_with_cache(&detectors_path)?
};
for d in &detectors {
if let Some(mc) = d.min_confidence {
detector_min_confidence
.entry(d.id.clone())
.or_insert(mc.clamp(0.0, 1.0));
}
}
if !disabled_detectors.is_empty() {
let before = detectors.len();
detectors.retain(|d| !disabled_detectors.iter().any(|id| id == &d.id));
let dropped = before - detectors.len();
if dropped > 0 {
tracing::info!(
target: "keyhog::config",
dropped,
"disabled detectors via .keyhog.toml [detector.<id>] enabled = false"
);
} else {
eprintln!(
"⚠️ .keyhog.toml disables detector id(s) {disabled_detectors:?}, but none matched the loaded corpus. \
Detector ids come from `keyhog detectors` (e.g. hot-pattern ids are prefixed `hot-`)."
);
}
}
if let Some(mem_mb) = hw.total_memory_mb {
if mem_mb < 4096 {
effective_config.scanner.max_matches_per_chunk =
effective_config.scanner.max_matches_per_chunk.min(500);
effective_config.scanner.max_decode_bytes =
effective_config.scanner.max_decode_bytes.min(256 * 1024);
}
}
effective_config.min_confidence = effective_config.scanner.min_confidence;
effective_config.ml_enabled = effective_config.scanner.ml_enabled;
if args.precision {
let floor = effective_config.scanner.min_confidence;
for v in detector_min_confidence.values_mut() {
*v = v.max(floor);
}
}
let gpu_init_policy = gpu_init_policy_for_args(&args);
let scanner = Arc::new(
CompiledScanner::compile_with_gpu_policy(detectors.clone(), gpu_init_policy)
.with_context(|| {
format!("compiling scanner from {} detector specs", detectors.len())
})?
.with_config(effective_config.scanner.clone()),
);
scanner.warm();
let signatures: std::collections::HashSet<Arc<str>> = detectors
.iter()
.flat_map(|d| d.patterns.iter().map(|p| Arc::from(p.regex.as_str())))
.chain(
detectors
.iter()
.flat_map(|d| d.companions.iter().map(|c| Arc::from(c.regex.as_str()))),
)
.collect();
let test_fixture_suppressions = if args.no_suppress_test_fixtures {
crate::test_fixture_suppressions::TestFixtureSuppressions::empty()
} else {
crate::test_fixture_suppressions::TestFixtureSuppressions::bundled()
};
Ok(Self {
args,
detectors,
scanner,
signatures,
test_fixture_suppressions,
disabled_detectors,
detector_min_confidence,
effective_config,
})
}
pub fn scanner(&self) -> &CompiledScanner {
self.scanner.as_ref()
}
pub fn args(&self) -> &ScanArgs {
&self.args
}
pub(crate) fn incremental_cache_path(&self) -> Option<std::path::PathBuf> {
if !self.args.incremental {
return None;
}
if self.args.lockdown {
tracing::warn!("lockdown mode: --incremental disabled (cache writes refused)");
return None;
}
self.args
.incremental_cache
.clone()
.or_else(keyhog_core::merkle_index::default_cache_path)
}
pub(crate) fn build_merkle_index(&self) -> Option<Arc<keyhog_core::merkle_index::MerkleIndex>> {
let path = self.incremental_cache_path()?;
let spec_hash = keyhog_core::merkle_index::compute_spec_hash(&self.detectors);
let idx = keyhog_core::merkle_index::MerkleIndex::load_with_spec(&path, &spec_hash);
tracing::info!(indexed = idx.len(), "incremental scan: loaded merkle index");
Some(Arc::new(idx))
}
#[doc(hidden)]
pub fn scan_sources_for_test(
&self,
sources: Vec<Box<dyn Source>>,
show_progress: bool,
merkle: Option<Arc<keyhog_core::merkle_index::MerkleIndex>>,
) -> Vec<RawMatch> {
self.scan_sources(sources, show_progress, merkle)
}
#[doc(hidden)]
pub fn from_parts_for_test(
args: ScanArgs,
detectors: Vec<DetectorSpec>,
scanner: Arc<CompiledScanner>,
signatures: std::collections::HashSet<Arc<str>>,
test_fixture_suppressions: crate::test_fixture_suppressions::TestFixtureSuppressions,
) -> Self {
Self {
args,
detectors,
scanner,
signatures,
test_fixture_suppressions,
disabled_detectors: std::collections::HashSet::new(),
detector_min_confidence: std::collections::HashMap::new(),
effective_config: ResolvedScanConfig {
scanner: keyhog_scanner::ScannerConfig::default(),
min_confidence: keyhog_scanner::ScannerConfig::default().min_confidence,
ml_enabled: keyhog_scanner::ScannerConfig::default().ml_enabled,
detector_min_confidence: std::collections::HashMap::new(),
disabled_detectors: std::collections::HashSet::new(),
require_lockdown: false,
},
}
}
}
fn gpu_init_policy_for_args(args: &ScanArgs) -> GpuInitPolicy {
if let Some(policy) = backend_name_gpu_policy(args.backend.as_deref()) {
return policy;
}
if let Some(policy) = explicit_backend_override().map(backend_gpu_policy) {
return policy;
}
if filesystem_auto_scan_cannot_route_gpu(args)
&& std::env::var("KEYHOG_REQUIRE_GPU").as_deref() != Ok("1")
&& !env_explicitly_enables_gpu()
{
return GpuInitPolicy::ForceDisabled;
}
GpuInitPolicy::FromEnvironment
}
fn backend_name_gpu_policy(name: Option<&str>) -> Option<GpuInitPolicy> {
let name = name?.trim().to_ascii_lowercase();
match name.as_str() {
"gpu" | "gpu-zero-copy" | "literal-set" | "mega-scan" | "megascan" | "gpu-mega-scan"
| "regex-nfa" | "rule-pipeline" => Some(GpuInitPolicy::ForceEnabled),
"simd" | "simd-regex" | "hyperscan" | "cpu" | "cpu-fallback" | "scalar" => {
Some(GpuInitPolicy::ForceDisabled)
}
"auto" => None,
_ => None,
}
}
fn backend_gpu_policy(backend: keyhog_scanner::ScanBackend) -> GpuInitPolicy {
match backend {
keyhog_scanner::ScanBackend::Gpu | keyhog_scanner::ScanBackend::MegaScan => {
GpuInitPolicy::ForceEnabled
}
keyhog_scanner::ScanBackend::SimdCpu | keyhog_scanner::ScanBackend::CpuFallback => {
GpuInitPolicy::ForceDisabled
}
_ => GpuInitPolicy::FromEnvironment,
}
}
fn env_explicitly_enables_gpu() -> bool {
std::env::var("KEYHOG_NO_GPU")
.map(|v| matches!(v.as_str(), "" | "0" | "false" | "FALSE" | "off" | "OFF"))
.unwrap_or(false)
}
fn filesystem_auto_scan_cannot_route_gpu(args: &ScanArgs) -> bool {
if std::env::var_os("KEYHOG_LEGACY_PIPELINE").is_some() {
return false;
}
if args.path.is_none() {
return false;
}
if args.stdin {
return false;
}
#[cfg(feature = "binary")]
if args.binary {
return false;
}
#[cfg(feature = "git")]
if args.git_blobs.is_some() || args.git_diff.is_some() || args.git_history.is_some() {
return false;
}
#[cfg(feature = "github")]
if args.github_org.is_some() {
return false;
}
#[cfg(feature = "s3")]
if args.s3_bucket.is_some() {
return false;
}
#[cfg(feature = "docker")]
if args.docker_image.is_some() {
return false;
}
#[cfg(feature = "web")]
if args.url.is_some() {
return false;
}
if args
.source
.as_ref()
.is_some_and(|sources| !sources.is_empty())
{
return false;
}
true
}