use crate::args::ScanArgs;
use anyhow::Result;
use keyhog_core::{DetectorSpec, RawMatch, Source, VerifiedFinding};
use keyhog_scanner::{CompiledScanner, ScannerConfig};
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::{Arc, Mutex, MutexGuard};
pub struct TestApi;
pub const API: TestApi = TestApi;
#[cfg(unix)]
#[derive(Debug)]
pub enum DaemonTerminalFixture {
CleanShutdown,
AcceptLoopPanic,
FatalAccept(std::io::Error),
}
static SCAN_RUNTIME_TEST_LOCK: Mutex<()> = Mutex::new(());
#[must_use = "hold ScanRuntimeGuard across CLI test-facade calls that touch process-global scan state"]
pub struct ScanRuntimeGuard {
_guard: MutexGuard<'static, ()>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Baseline {
pub version: u32,
pub created: String,
pub entries: Vec<BaselineEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct BaselineEntry {
pub detector_id: String,
pub credential_hash: String,
pub file_path: Option<String>,
pub line: Option<usize>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ScanRuntimeSnapshot {
pub scanned_chunks: usize,
pub total_chunks: usize,
pub findings_count: usize,
pub gpu_scanned_chunks: usize,
pub backend_recovery_events: usize,
pub backend_recovered_chunks: usize,
pub backend_recovered_bytes: u64,
pub source_errors: usize,
pub failed_sources: usize,
pub incremental_cache_errors: usize,
pub scanner_panicked: bool,
pub dogfood_enabled: bool,
pub example_suppressions: usize,
pub decode_truncations: usize,
}
#[cfg(unix)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StaticRecoveryMergeSnapshot {
pub before: keyhog_scanner::telemetry::StaticRecoveryStatus,
pub after: keyhog_scanner::telemetry::StaticRecoveryStatus,
}
pub struct TestFixtureSuppressions(crate::test_fixture_suppressions::TestFixtureSuppressions);
impl std::fmt::Debug for TestFixtureSuppressions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestFixtureSuppressions")
.field("exact_count", &self.0.exact_count())
.finish_non_exhaustive()
}
}
pub struct FindingSink(crate::subcommands::scan_system::testing::FindingSink);
pub struct ScanOrchestrator(crate::orchestrator::ScanOrchestrator);
#[derive(Debug)]
pub struct SkipDirPolicyView(crate::skip_dirs::SkipDirPolicy);
impl SkipDirPolicyView {
pub fn is_watch_component(&self, component: &str) -> bool {
self.0.is_watch_component(component)
}
pub fn is_git_discovery_component(&self, component: &str) -> bool {
self.0.is_git_discovery_component(component)
}
}
pub type DownloadFuture<'a> = Pin<Box<dyn Future<Output = Result<Vec<u8>>> + 'a>>;
pub type ReleaseResolutionFuture<'a> = Pin<Box<dyn Future<Output = Result<ResolvedRelease>> + 'a>>;
pub type ReleaseInstallFuture<'a> = Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRelease {
pub tag_name: String,
pub asset_name: String,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct VerificationTally {
pub live: usize,
pub inactive: usize,
pub skipped: usize,
pub unverifiable: usize,
pub incomplete: usize,
}
pub trait CliTestApi {
fn removed_verification_state(&self, result: &keyhog_core::VerificationResult) -> &'static str;
fn removed_verification_blocks_success(&self, result: &keyhog_core::VerificationResult)
-> bool;
fn parse_min_confidence(&self, s: &str) -> std::result::Result<f64, String>;
fn parse_verify_rate(&self, s: &str) -> std::result::Result<f64, String>;
fn parse_ml_threshold(&self, s: &str) -> std::result::Result<f64, String>;
fn parse_decode_depth(&self, s: &str) -> std::result::Result<usize, String>;
fn parse_min_secret_len(&self, s: &str) -> std::result::Result<usize, String>;
fn parse_positive_thread_count(&self, s: &str) -> std::result::Result<usize, String>;
fn parse_positive_usize(&self, s: &str) -> std::result::Result<usize, String>;
fn parse_positive_millis(&self, s: &str) -> std::result::Result<u64, String>;
fn parse_daemon_request_timeout_secs(&self, s: &str) -> std::result::Result<u64, String>;
fn parse_byte_size(&self, s: &str) -> std::result::Result<usize, String>;
fn parse_severity_filter(&self, s: &str) -> Option<crate::args::SeverityFilter>;
fn parse_output_format(&self, s: &str) -> Option<crate::args::OutputFormat>;
fn parse_dedup_scope(&self, s: &str) -> Option<crate::args::CliDedupScope>;
fn format_gpu_summary(&self) -> String;
fn write_banner(&self, colors: bool, detector_count: usize) -> std::io::Result<Vec<u8>>;
fn format_gpu_max_buffer(&self, max_buffer_mb: u64) -> String;
fn format_backend_probe_count_metric(&self, value: Option<usize>) -> String;
fn format_backend_probe_mb_metric(&self, value: Option<u64>) -> String;
fn find_config_file(&self, start: Option<&Path>) -> Option<PathBuf>;
fn apply_config_file_quiet(&self, args: &mut ScanArgs);
fn build_sources(
&self,
args: &ScanArgs,
allowlist_paths: Vec<String>,
merkle: Option<Arc<keyhog_core::MerkleIndex>>,
) -> Result<Vec<Box<dyn Source>>>;
fn set_buffered_stdin(&self, args: &mut ScanArgs, bytes: Vec<u8>);
fn merge_scan_ignore_paths(&self, args: &ScanArgs, allowlist_paths: Vec<String>)
-> Vec<String>;
fn validate_cli_path_arg(&self, path: &Path, name: &str) -> Result<()>;
fn resolve_scan_roots(&self, requested: &[PathBuf]) -> Result<Vec<PathBuf>>;
fn guard_multi_root_combinations(&self, args: &ScanArgs) -> Result<()>;
fn report_findings(
&self,
findings: &[VerifiedFinding],
args: &ScanArgs,
_guard: &ScanRuntimeGuard,
) -> Result<()>;
fn attach_inline_suppression_context_for_test(
&self,
chunk: &keyhog_core::Chunk,
matches: &mut [RawMatch],
);
fn attach_inline_suppression_context_for_chunks_for_test(
&self,
chunks: &[keyhog_core::Chunk],
per_chunk: &mut [Vec<RawMatch>],
);
fn filter_inline_suppressions(&self, matches: Vec<RawMatch>) -> Vec<RawMatch>;
fn format_bytes(&self, n: u64) -> String;
#[cfg(unix)]
fn ensure_private_socket_dir(&self, parent: &Path) -> Result<()>;
#[cfg(unix)]
fn remove_stale_socket_if_trusted(&self, socket_path: &Path) -> Result<()>;
#[cfg(unix)]
fn validate_socket_for_connect(&self, socket_path: &Path) -> Result<()>;
#[cfg(unix)]
fn current_uid(&self) -> libc::uid_t;
#[cfg(unix)]
fn connected_peer_uid(&self, stream: &tokio::net::UnixStream) -> Result<libc::uid_t>;
#[cfg(unix)]
fn verify_accepted_peer(&self, stream: &tokio::net::UnixStream) -> Result<()>;
fn render_credential(
&self,
credential: &keyhog_core::SensitiveString,
show_secrets: bool,
) -> std::borrow::Cow<'static, str>;
#[cfg(unix)]
fn is_transient_accept_error(&self, error: &std::io::Error) -> bool;
#[cfg(unix)]
fn finish_daemon_terminal_fixture(
&self,
socket_path: PathBuf,
fixture: DaemonTerminalFixture,
) -> Pin<Box<dyn Future<Output = Result<()>>>>;
fn cli_error_exit_code(&self, error: &anyhow::Error) -> u8;
fn baseline_version(&self) -> u32;
fn baseline_empty(&self) -> Baseline;
fn baseline_load(&self, path: &Path) -> Result<Baseline>;
fn baseline_save(&self, baseline: &Baseline, path: &Path) -> Result<()>;
fn baseline_from_findings(&self, findings: &[VerifiedFinding]) -> Baseline;
fn baseline_merge(&self, baseline: &mut Baseline, findings: &[VerifiedFinding]);
fn baseline_contains(&self, baseline: &Baseline, finding: &VerifiedFinding) -> bool;
fn baseline_filter_new(
&self,
baseline: &Baseline,
findings: &[VerifiedFinding],
) -> Vec<VerifiedFinding>;
fn baseline_retain_new(&self, baseline: &Baseline, findings: &mut Vec<VerifiedFinding>);
fn baseline_looks_like_findings_report(&self, content: &str) -> bool;
fn write_scan_receipt_for_test(
&self,
args: &ScanArgs,
findings: usize,
exit_code: u8,
status: keyhog_core::ScanCompletionStatus,
) -> Result<()>;
fn bundled_test_fixture_suppressions(&self) -> TestFixtureSuppressions;
fn empty_test_fixture_suppressions(&self) -> TestFixtureSuppressions;
fn test_fixture_suppressions_from_toml(
&self,
raw: &str,
) -> std::result::Result<TestFixtureSuppressions, String>;
fn test_fixture_suppresses(&self, suppressions: &TestFixtureSuppressions, cred: &str) -> bool;
fn test_fixture_exact_count(&self, suppressions: &TestFixtureSuppressions) -> usize;
fn asset_name(&self, os: &str, arch: &str) -> Option<String>;
fn select_release_asset_name(&self, tag_name: &str, asset_names: &[&str]) -> Result<String>;
fn parse_semver(&self, tag: &str) -> Option<(u64, u64, u64)>;
fn is_newer(&self, current: &str, latest: &str) -> bool;
fn release_channel_state(&self, current: &str, latest: &str) -> &'static str;
fn looks_like_native_executable(&self, bytes: &[u8]) -> bool;
fn looks_like_native_executable_for_os(&self, bytes: &[u8], os: &str) -> bool;
fn verify_release_signature(&self, data: &[u8], signature: &str) -> Result<()>;
fn verify_release_checksum(
&self,
data: &[u8],
asset_name: &str,
checksum_file: &[u8],
) -> Result<()>;
fn parse_gpu_literal_sidecar(
&self,
archive: &[u8],
expected_release_tag: &str,
) -> Result<Vec<(String, Vec<u8>)>>;
fn install_gpu_literal_files_in_dir(
&self,
cache_dir: &Path,
files: &[(&str, &[u8])],
commit: bool,
) -> Result<()>;
fn release_api_base(&self) -> &'static str;
fn resolve_release_at<'a>(
&self,
client: &'a reqwest::Client,
version: Option<&'a str>,
release_api_base: &'a str,
) -> ReleaseResolutionFuture<'a>;
fn install_verified_release_payload_at<'a>(
&self,
client: &'a reqwest::Client,
version: Option<&'a str>,
release_api_base: &'a str,
asset_name: &'a str,
target: &'a Path,
) -> ReleaseInstallFuture<'a>;
fn release_public_key(&self) -> &'static str;
fn release_repo(&self) -> &'static str;
fn scan_engine_self_test(&self) -> Result<bool>;
fn verify_via_doctor(&self, exe: &Path) -> bool;
fn http_client(&self) -> Result<reqwest::Client>;
fn download_verified_asset<'a>(
&self,
client: &'a reqwest::Client,
name: &'a str,
browser_download_url: String,
) -> DownloadFuture<'a>;
fn current_binary(&self) -> Result<PathBuf>;
fn replace_running_binary<F>(
&self,
exe: &Path,
bytes: &[u8],
verify: F,
) -> Result<Option<PathBuf>>
where
F: FnOnce(&Path) -> bool;
fn reap_stale_binaries(&self, exe: &Path);
fn backup_path(&self, exe: &Path) -> PathBuf;
fn verify_candidate_release(
&self,
exe: &Path,
expected_release_tag: &str,
current_version: &str,
allow_explicit_downgrade: bool,
) -> Result<()>;
fn install_with_rollback<F>(&self, exe: &Path, bytes: &[u8], verify: F) -> Result<()>
where
F: FnOnce(&Path) -> bool;
fn install_with_rollback_checked<F>(&self, exe: &Path, bytes: &[u8], verify: F) -> Result<()>
where
F: FnOnce(&Path) -> Result<()>;
fn rewrite_detector_braces(&self, s: &str) -> (String, usize);
fn fix_single_brace_in_verify_blocks(&self, toml_text: &str) -> (String, usize);
fn fix_verify_braces(&self, toml_text: &str) -> (String, usize);
fn rewrite_braces_in_string_literals(&self, line: &str) -> (String, usize);
fn canonical_for_hot_id(&self, id: &str) -> Option<&'static str>;
fn explain_not_found(
&self,
detectors: &[DetectorSpec],
requested: &str,
lowered: &str,
) -> anyhow::Error;
fn render_failing_region_presence_probe_json(&self) -> Result<String>;
fn doctor_canonicalize_for_shadow_check(&self, path: PathBuf) -> PathBuf;
fn doctor_should_run_gpu_self_tests(&self, gpu_available: bool, gpu_is_software: bool) -> bool;
fn canonical_scan_args(&self) -> &'static str;
fn hook_content(&self) -> &'static str;
fn watch_content_hash(&self, data: &[u8]) -> u64;
fn watch_duplicate_event_decisions(
&self,
first: &[u8],
second: &[u8],
elapsed: std::time::Duration,
) -> (bool, bool);
fn watch_findings_fingerprint(&self, matches: &[keyhog_core::RawMatch]) -> [u8; 32];
fn watch_duplicate_findings_decisions(
&self,
first: [u8; 32],
second: [u8; 32],
elapsed: std::time::Duration,
) -> (bool, bool);
fn watch_resolve_roots(&self, requested: &[PathBuf]) -> Result<Vec<PathBuf>>;
fn watch_roots_hint(&self, roots: &[PathBuf]) -> String;
fn max_resident_findings(&self) -> usize;
fn parse_macos_mount_table_for_test(
&self,
text: &str,
include_network: bool,
) -> Result<Vec<PathBuf>>;
fn windows_drive_filter_decisions_for_test(&self) -> Result<(bool, bool, bool, bool)>;
fn windows_drive_skip_prefix_decisions_for_test(&self) -> (bool, bool);
#[cfg(target_os = "linux")]
fn decoded_mount_target_if_included_for_test(
&self,
target: &str,
skip_path_prefixes: Vec<String>,
) -> Result<Option<String>>;
fn scan_system_discover_git_repos_for_test(&self, root: &Path) -> Result<Vec<PathBuf>>;
fn scan_system_chunk_fits_space_cap(
&self,
bytes_scanned: u64,
chunk_len: usize,
space_cap: u64,
) -> bool;
fn finding_sink_new(&self) -> FindingSink;
fn finding_sink_with_cap(&self, cap: usize) -> FindingSink;
fn finding_sink_record_skipped_chunk(&self, sink: &mut FindingSink);
fn finding_sink_skipped_chunks(&self, sink: &FindingSink) -> u64;
fn finding_sink_absorb(&self, sink: &mut FindingSink, matches: Vec<RawMatch>);
fn finding_sink_is_empty(&self, sink: &FindingSink) -> bool;
fn finding_sink_total(&self, sink: &FindingSink) -> u64;
fn finding_sink_retained_len(&self, sink: &FindingSink) -> usize;
fn finding_sink_cap(&self, sink: &FindingSink) -> usize;
fn finding_sink_capped_warned(&self, sink: &FindingSink) -> bool;
fn finding_sink_retained_hash(
&self,
sink: &FindingSink,
index: usize,
) -> Option<keyhog_core::CredentialHash>;
fn finding_sink_retained_json(&self, sink: &FindingSink) -> serde_json::Result<String>;
fn sanitise_thread_count(
&self,
requested: usize,
physical_cores: usize,
source: &'static str,
) -> usize;
fn max_threads_cap(&self) -> usize;
fn load_detectors_or_embedded(&self, path: &Path) -> Result<Vec<DetectorSpec>>;
fn load_detectors_from_dir_with_cache(
&self,
source_dir: &Path,
cache_path: &Path,
) -> Result<Vec<DetectorSpec>>;
fn build_scanner_config(&self, args: &ScanArgs) -> ScannerConfig;
fn resolve_scan_config(&self, args: &mut ScanArgs) -> Result<()>;
fn resolve_scan_config_aws_canary_accounts(&self, args: &mut ScanArgs) -> Result<Vec<String>>;
fn render_effective_config_for_scanner(&self, scanner: ScannerConfig) -> String;
fn autoroute_config_digest_for_args(&self, args: &mut ScanArgs) -> Result<u64>;
fn autoroute_config_digest_for_scanner(&self, scanner: ScannerConfig) -> u64;
fn profiling_config_digests_for_args(
&self,
args: &mut ScanArgs,
) -> Result<([u8; 32], [u8; 32])>;
fn ml_threshold_default(&self) -> f64;
fn explicit_backend_override(
&self,
raw: Option<&str>,
) -> Result<Option<keyhog_scanner::ScanBackend>>;
fn forced_backend_runtime_detector_ids(&self, backend: &str, body: &str)
-> Result<Vec<String>>;
fn disabled_gpu_dispatch_for_test(
&self,
body: &str,
recover_automatic_backend_faults: bool,
_guard: &ScanRuntimeGuard,
) -> Result<Vec<String>>;
fn automatic_backend_recovery_allowed_for_test(
&self,
explicit_backend: Option<keyhog_scanner::ScanBackend>,
calibration_mode: bool,
gpu_runtime_policy: keyhog_scanner::gpu::GpuRuntimePolicy,
) -> bool;
fn router_gpu_participates_for_test(
&self,
explicit_backend: Option<keyhog_scanner::ScanBackend>,
gpu_runtime_policy: keyhog_scanner::gpu::GpuRuntimePolicy,
) -> bool;
fn router_uses_gpu_probe_for_test(&self, gpu_participates: bool) -> bool;
fn current_target_digest_for_test(&self) -> [u8; 32];
fn autoroute_default_config_identity_for_test(&self) -> String;
fn allowlist_root_for_test(&self, path: &Path) -> PathBuf;
fn backend_requires_coalesced_batch_pipeline_for_test(
&self,
explicit: Option<keyhog_scanner::ScanBackend>,
) -> bool;
fn gpu_init_policy_for_args_for_test(&self, args: &ScanArgs) -> keyhog_scanner::GpuInitPolicy;
fn gpu_init_policy_for_resolved_autoroute_for_test(
&self,
args: &ScanArgs,
autoroute_cache_path: Option<&Path>,
autoroute_gpu: bool,
autoroute_calibration: bool,
) -> keyhog_scanner::GpuInitPolicy;
fn scanner_panic_notice_for_test(&self, panicked: bool) -> Option<String>;
fn scan_exit_code(&self, findings: &[VerifiedFinding]) -> u8;
fn resolve_scan_exit_for_test(
&self,
has_new_entries: bool,
incremental_cache_failed: bool,
source_coverage_incomplete: bool,
) -> u8;
fn scan_orchestrator_from_parts_for_test(
&self,
args: ScanArgs,
detectors: Vec<DetectorSpec>,
scanner: Arc<CompiledScanner>,
signatures: std::collections::HashSet<Arc<str>>,
test_fixture_suppressions: TestFixtureSuppressions,
) -> ScanOrchestrator;
fn scan_orchestrator_scanner<'a>(
&self,
orchestrator: &'a ScanOrchestrator,
) -> &'a CompiledScanner;
fn scan_orchestrator_args<'a>(&self, orchestrator: &'a ScanOrchestrator) -> &'a ScanArgs;
fn scan_orchestrator_detector_count(&self, orchestrator: &ScanOrchestrator) -> usize;
fn scan_orchestrator_retained_detector_specs(&self, orchestrator: &ScanOrchestrator) -> usize;
fn scan_orchestrator_scan_sources_for_test(
&self,
orchestrator: &ScanOrchestrator,
sources: Vec<Box<dyn Source>>,
show_progress: bool,
merkle: Option<Arc<keyhog_core::MerkleIndex>>,
_guard: &ScanRuntimeGuard,
) -> Result<Vec<RawMatch>>;
fn scan_runtime_guard_for_test(&self) -> ScanRuntimeGuard;
fn seed_scan_runtime_state_for_test(&self, _guard: &ScanRuntimeGuard);
fn reset_scan_runtime_state_for_test(&self, _guard: &ScanRuntimeGuard);
fn scan_runtime_snapshot(&self, _guard: &ScanRuntimeGuard) -> ScanRuntimeSnapshot;
fn backend_recovery_summaries_for_test(
&self,
_guard: &ScanRuntimeGuard,
) -> Vec<keyhog_core::ScanBackendRecoverySummary>;
fn scanned_chunks(&self, _guard: &ScanRuntimeGuard) -> usize;
fn scanner_panicked(&self, _guard: &ScanRuntimeGuard) -> bool;
fn verification_tally(&self, findings: &[VerifiedFinding]) -> VerificationTally;
fn render_verification_summary(
&self,
findings: &[VerifiedFinding],
color: bool,
) -> Option<String>;
fn render_severity_summary(&self, findings: &[VerifiedFinding], color: bool) -> Option<String>;
fn render_progress_bar(&self, frac: f64, width: usize, color: bool) -> String;
fn render_scanning_ticker(
&self,
scanned: usize,
total: usize,
findings: usize,
elapsed: f64,
frame: usize,
color: bool,
) -> String;
fn render_verification_ticker(
&self,
total: usize,
elapsed: f64,
frame: usize,
color: bool,
) -> String;
fn render_reporting_ticker(
&self,
total: usize,
elapsed: f64,
frame: usize,
color: bool,
) -> String;
fn fmt_secs(&self, secs: f64) -> String;
fn ticker_guard_spawns_and_joins(&self) -> bool;
fn redact_url_target(&self, raw: &str) -> String;
fn skip_dir_policy_from_toml(
&self,
toml: &str,
) -> std::result::Result<SkipDirPolicyView, String>;
fn skip_dir_policy_from_bundled(&self) -> std::result::Result<SkipDirPolicyView, String>;
fn skip_dir_policy_from_bundled_plus_user(
&self,
user_toml: &str,
) -> std::result::Result<SkipDirPolicyView, String>;
fn skip_dir_section_counts(
&self,
toml: &str,
) -> std::result::Result<(usize, usize, usize), String>;
#[cfg(unix)]
fn merge_daemon_static_recovery(
&self,
rejections: std::collections::BTreeMap<String, u64>,
status: keyhog_scanner::telemetry::StaticRecoveryStatus,
) -> Result<StaticRecoveryMergeSnapshot>;
}
impl CliTestApi for TestApi {
fn removed_verification_state(&self, result: &keyhog_core::VerificationResult) -> &'static str {
crate::subcommands::diff::removed_state_label_for_test(result)
}
fn removed_verification_blocks_success(
&self,
result: &keyhog_core::VerificationResult,
) -> bool {
crate::subcommands::diff::removed_result_blocks_success_for_test(result)
}
fn parse_min_confidence(&self, s: &str) -> std::result::Result<f64, String> {
crate::value_parsers::parse_min_confidence(s)
}
fn parse_verify_rate(&self, s: &str) -> std::result::Result<f64, String> {
crate::value_parsers::parse_verify_rate(s)
}
fn parse_ml_threshold(&self, s: &str) -> std::result::Result<f64, String> {
crate::value_parsers::parse_ml_threshold(s)
}
fn parse_decode_depth(&self, s: &str) -> std::result::Result<usize, String> {
crate::value_parsers::parse_decode_depth(s)
}
fn parse_min_secret_len(&self, s: &str) -> std::result::Result<usize, String> {
crate::value_parsers::parse_min_secret_len(s)
}
fn parse_positive_thread_count(&self, s: &str) -> std::result::Result<usize, String> {
crate::value_parsers::parse_positive_thread_count(s)
}
fn parse_positive_usize(&self, s: &str) -> std::result::Result<usize, String> {
crate::value_parsers::parse_positive_usize(s)
}
fn parse_positive_millis(&self, s: &str) -> std::result::Result<u64, String> {
crate::value_parsers::parse_positive_millis(s)
}
fn parse_daemon_request_timeout_secs(&self, s: &str) -> std::result::Result<u64, String> {
crate::value_parsers::parse_daemon_request_timeout_secs(s)
}
fn parse_byte_size(&self, s: &str) -> std::result::Result<usize, String> {
crate::value_parsers::parse_byte_size(s)
}
fn parse_severity_filter(&self, s: &str) -> Option<crate::args::SeverityFilter> {
crate::value_parsers::parse_severity_filter(s)
}
fn parse_output_format(&self, s: &str) -> Option<crate::args::OutputFormat> {
crate::value_parsers::parse_output_format(s)
}
fn parse_dedup_scope(&self, s: &str) -> Option<crate::args::CliDedupScope> {
crate::value_parsers::parse_dedup_scope(s)
}
fn format_gpu_summary(&self) -> String {
crate::benchmark::format_gpu_summary()
}
fn write_banner(&self, colors: bool, detector_count: usize) -> std::io::Result<Vec<u8>> {
let mut output = Vec::new();
crate::write_banner(&mut output, colors, detector_count)?;
Ok(output)
}
fn format_gpu_max_buffer(&self, max_buffer_mb: u64) -> String {
crate::subcommands::backend::testing::format_gpu_max_buffer(max_buffer_mb)
}
fn format_backend_probe_count_metric(&self, value: Option<usize>) -> String {
crate::subcommands::backend::testing::format_probe_count_metric(value)
}
fn format_backend_probe_mb_metric(&self, value: Option<u64>) -> String {
crate::subcommands::backend::testing::format_probe_mb_metric(value)
}
fn find_config_file(&self, start: Option<&Path>) -> Option<PathBuf> {
crate::config::find_config_file(start)
}
fn apply_config_file_quiet(&self, args: &mut ScanArgs) {
let _outcome = crate::config::apply_config_file_quiet(args);
}
fn build_sources(
&self,
args: &ScanArgs,
allowlist_paths: Vec<String>,
merkle: Option<Arc<keyhog_core::MerkleIndex>>,
) -> Result<Vec<Box<dyn Source>>> {
let mut resolved_args = args.clone();
let resolved = crate::orchestrator_config::resolve_scan_config(&mut resolved_args)?;
crate::sources::build_sources(&resolved_args, &resolved, allowlist_paths, merkle)
}
fn set_buffered_stdin(&self, args: &mut ScanArgs, bytes: Vec<u8>) {
args.buffered_stdin = Some(bytes.into());
}
fn merge_scan_ignore_paths(
&self,
args: &ScanArgs,
allowlist_paths: Vec<String>,
) -> Vec<String> {
let exclude_paths = args.exclude_paths.as_deref().unwrap_or(&[]); crate::sources::merge_scan_ignore_paths(exclude_paths, allowlist_paths)
}
fn validate_cli_path_arg(&self, path: &Path, name: &str) -> Result<()> {
crate::path_validation::validate_cli_path_arg(path, name)
}
fn resolve_scan_roots(&self, requested: &[PathBuf]) -> Result<Vec<PathBuf>> {
crate::sources::resolve_scan_roots(requested)
}
fn guard_multi_root_combinations(&self, args: &ScanArgs) -> Result<()> {
crate::subcommands::scan::guard_multi_root_combinations(args)
}
fn report_findings(
&self,
findings: &[VerifiedFinding],
args: &ScanArgs,
_guard: &ScanRuntimeGuard,
) -> Result<()> {
crate::reporting::report_findings(findings, args)
}
fn attach_inline_suppression_context_for_test(
&self,
chunk: &keyhog_core::Chunk,
matches: &mut [RawMatch],
) {
crate::inline_suppression::attach_inline_suppression_context_to_matches(chunk, matches)
}
fn attach_inline_suppression_context_for_chunks_for_test(
&self,
chunks: &[keyhog_core::Chunk],
per_chunk: &mut [Vec<RawMatch>],
) {
crate::inline_suppression::attach_inline_suppression_context(chunks, per_chunk)
}
fn filter_inline_suppressions(&self, matches: Vec<RawMatch>) -> Vec<RawMatch> {
crate::inline_suppression::filter_inline_suppressions(matches)
}
fn format_bytes(&self, n: u64) -> String {
crate::format::format_bytes(n)
}
#[cfg(unix)]
fn ensure_private_socket_dir(&self, parent: &Path) -> Result<()> {
crate::daemon::server::testing::ensure_private_socket_dir(parent)
}
#[cfg(unix)]
fn remove_stale_socket_if_trusted(&self, socket_path: &Path) -> Result<()> {
crate::daemon::server::testing::remove_stale_socket_if_trusted(socket_path)
}
#[cfg(unix)]
fn validate_socket_for_connect(&self, socket_path: &Path) -> Result<()> {
crate::daemon::client::testing::validate_socket_for_connect(socket_path)
}
#[cfg(unix)]
fn current_uid(&self) -> libc::uid_t {
crate::daemon::client::testing::current_uid()
}
#[cfg(unix)]
fn connected_peer_uid(&self, stream: &tokio::net::UnixStream) -> Result<libc::uid_t> {
crate::daemon::client::testing::connected_peer_uid(stream)
}
#[cfg(unix)]
fn verify_accepted_peer(&self, stream: &tokio::net::UnixStream) -> Result<()> {
crate::daemon::server::testing::verify_accepted_peer(stream)
}
fn render_credential(
&self,
credential: &keyhog_core::SensitiveString,
show_secrets: bool,
) -> std::borrow::Cow<'static, str> {
crate::orchestrator::render_credential(credential, show_secrets)
}
#[cfg(unix)]
fn is_transient_accept_error(&self, error: &std::io::Error) -> bool {
crate::daemon::server::is_transient_accept_error(error)
}
#[cfg(unix)]
fn finish_daemon_terminal_fixture(
&self,
socket_path: PathBuf,
fixture: DaemonTerminalFixture,
) -> Pin<Box<dyn Future<Output = Result<()>>>> {
Box::pin(
crate::daemon::server::testing::finish_daemon_service_for_test(socket_path, fixture),
)
}
fn cli_error_exit_code(&self, error: &anyhow::Error) -> u8 {
crate::cli_error_exit_code(error)
}
fn baseline_version(&self) -> u32 {
crate::baseline::testing::baseline_version()
}
fn baseline_empty(&self) -> Baseline {
expose_baseline(crate::baseline::Baseline::empty())
}
fn baseline_load(&self, path: &Path) -> Result<Baseline> {
crate::baseline::Baseline::load(path).map(expose_baseline)
}
fn baseline_save(&self, baseline: &Baseline, path: &Path) -> Result<()> {
baseline.to_internal().save(path)
}
fn baseline_from_findings(&self, findings: &[VerifiedFinding]) -> Baseline {
expose_baseline(crate::baseline::Baseline::from_findings(findings))
}
fn baseline_merge(&self, baseline: &mut Baseline, findings: &[VerifiedFinding]) {
let mut inner = baseline.to_internal();
inner.merge(findings);
*baseline = expose_baseline(inner);
}
fn baseline_contains(&self, baseline: &Baseline, finding: &VerifiedFinding) -> bool {
baseline.to_internal().contains(finding)
}
fn baseline_filter_new(
&self,
baseline: &Baseline,
findings: &[VerifiedFinding],
) -> Vec<VerifiedFinding> {
baseline.to_internal().filter_new(findings)
}
fn baseline_retain_new(&self, baseline: &Baseline, findings: &mut Vec<VerifiedFinding>) {
baseline.to_internal().retain_new(findings);
}
fn baseline_looks_like_findings_report(&self, content: &str) -> bool {
crate::baseline::testing::looks_like_findings_report(content)
}
fn write_scan_receipt_for_test(
&self,
args: &ScanArgs,
findings: usize,
exit_code: u8,
status: keyhog_core::ScanCompletionStatus,
) -> Result<()> {
crate::action_report::write_scan_receipt(args, findings, exit_code, status)
}
fn bundled_test_fixture_suppressions(&self) -> TestFixtureSuppressions {
TestFixtureSuppressions(
crate::test_fixture_suppressions::TestFixtureSuppressions::bundled(),
)
}
fn empty_test_fixture_suppressions(&self) -> TestFixtureSuppressions {
TestFixtureSuppressions(crate::test_fixture_suppressions::TestFixtureSuppressions::empty())
}
fn test_fixture_suppressions_from_toml(
&self,
raw: &str,
) -> std::result::Result<TestFixtureSuppressions, String> {
crate::test_fixture_suppressions::TestFixtureSuppressions::from_toml(raw)
.map(TestFixtureSuppressions)
}
fn test_fixture_suppresses(&self, suppressions: &TestFixtureSuppressions, cred: &str) -> bool {
suppressions.0.suppresses(cred)
}
fn test_fixture_exact_count(&self, suppressions: &TestFixtureSuppressions) -> usize {
suppressions.0.exact_count()
}
fn asset_name(&self, os: &str, arch: &str) -> Option<String> {
crate::installer::asset_name(os, arch)
}
fn select_release_asset_name(&self, tag_name: &str, asset_names: &[&str]) -> Result<String> {
let release = crate::installer::Release {
tag_name: tag_name.to_string(),
draft: false,
prerelease: false,
assets: asset_names
.iter()
.map(|name| crate::installer::Asset {
name: (*name).to_string(),
browser_download_url: format!("https://example.invalid/{name}"),
})
.collect(),
};
crate::installer::select_asset(&release).map(|asset| asset.name.clone())
}
fn parse_semver(&self, tag: &str) -> Option<(u64, u64, u64)> {
crate::installer::parse_semver(tag)
}
fn is_newer(&self, current: &str, latest: &str) -> bool {
crate::installer::is_newer(current, latest)
}
fn release_channel_state(&self, current: &str, latest: &str) -> &'static str {
match crate::installer::classify_channel(current, latest) {
crate::installer::ReleaseChannelState::UpdateAvailable => "update-available",
crate::installer::ReleaseChannelState::OnNewestAsset => "on-newest-asset",
crate::installer::ReleaseChannelState::ChannelBehind => "channel-behind",
}
}
fn looks_like_native_executable(&self, bytes: &[u8]) -> bool {
crate::installer::looks_like_native_executable(bytes)
}
fn looks_like_native_executable_for_os(&self, bytes: &[u8], os: &str) -> bool {
crate::installer::looks_like_native_executable_for_os(bytes, os)
}
fn verify_release_signature(&self, data: &[u8], signature: &str) -> Result<()> {
crate::installer::verify_release_signature(data, signature)
}
fn verify_release_checksum(
&self,
data: &[u8],
asset_name: &str,
checksum_file: &[u8],
) -> Result<()> {
crate::installer::verify_release_checksum(data, asset_name, checksum_file)
}
fn parse_gpu_literal_sidecar(
&self,
archive: &[u8],
expected_release_tag: &str,
) -> Result<Vec<(String, Vec<u8>)>> {
crate::installer::parse_gpu_literal_sidecar(archive, expected_release_tag).map(|files| {
files
.into_iter()
.map(|file| (file.name, file.bytes))
.collect()
})
}
fn install_gpu_literal_files_in_dir(
&self,
cache_dir: &Path,
files: &[(&str, &[u8])],
commit: bool,
) -> Result<()> {
let files = files
.iter()
.map(|(name, bytes)| crate::installer::GpuLiteralFile {
name: (*name).to_string(),
bytes: (*bytes).to_vec(),
})
.collect::<Vec<_>>();
let transaction = crate::installer::install_gpu_literal_files_in_dir(cache_dir, &files)?;
if commit {
transaction.commit();
}
Ok(())
}
fn release_api_base(&self) -> &'static str {
crate::installer::release_api_base()
}
fn resolve_release_at<'a>(
&self,
client: &'a reqwest::Client,
version: Option<&'a str>,
release_api_base: &'a str,
) -> ReleaseResolutionFuture<'a> {
Box::pin(async move {
let release =
crate::installer::resolve_release_at(client, version, release_api_base).await?;
let asset_name = crate::installer::select_asset(&release)?.name.clone();
Ok(ResolvedRelease {
tag_name: release.tag_name,
asset_name,
})
})
}
fn install_verified_release_payload_at<'a>(
&self,
client: &'a reqwest::Client,
version: Option<&'a str>,
release_api_base: &'a str,
asset_name: &'a str,
target: &'a Path,
) -> ReleaseInstallFuture<'a> {
Box::pin(async move {
let bytes = crate::installer::resolve_and_download_verified_payload_at(
client,
version,
release_api_base,
asset_name,
)
.await?;
let expected = bytes.clone();
crate::installer::install_with_rollback_checked(target, &bytes, move |candidate| {
let installed = std::fs::read(candidate).map_err(anyhow::Error::from)?;
anyhow::ensure!(
installed == expected,
"installed release payload changed bytes"
);
Ok(())
})
})
}
fn release_public_key(&self) -> &'static str {
crate::installer::RELEASE_PUBLIC_KEY
}
fn release_repo(&self) -> &'static str {
crate::installer::REPO
}
fn scan_engine_self_test(&self) -> Result<bool> {
crate::installer::scan_engine_self_test()
}
fn verify_via_doctor(&self, exe: &Path) -> bool {
crate::installer::verify_via_doctor_checked(exe).is_ok()
}
fn http_client(&self) -> Result<reqwest::Client> {
crate::installer::http_client()
}
fn download_verified_asset<'a>(
&self,
client: &'a reqwest::Client,
name: &'a str,
browser_download_url: String,
) -> DownloadFuture<'a> {
Box::pin(async move {
let asset = crate::installer::Asset {
name: name.to_string(),
browser_download_url: browser_download_url.clone(),
};
let release = crate::installer::Release {
tag_name: "v0.0.0-test".to_string(),
draft: false,
prerelease: false,
assets: vec![
asset.clone(),
crate::installer::Asset {
name: format!("{name}.minisig"),
browser_download_url: format!("{browser_download_url}.minisig"),
},
crate::installer::Asset {
name: format!("{name}.sha256"),
browser_download_url: format!("{browser_download_url}.sha256"),
},
],
};
crate::installer::download_verified_asset(client, &release, &asset).await
})
}
fn current_binary(&self) -> Result<PathBuf> {
crate::installer::current_binary()
}
fn replace_running_binary<F>(
&self,
exe: &Path,
bytes: &[u8],
verify: F,
) -> Result<Option<PathBuf>>
where
F: FnOnce(&Path) -> bool,
{
crate::installer::replace_running_binary(exe, bytes, verify)
}
fn reap_stale_binaries(&self, exe: &Path) {
crate::installer::reap_stale_binaries(exe)
}
fn backup_path(&self, exe: &Path) -> PathBuf {
crate::installer::backup_path(exe)
}
fn verify_candidate_release(
&self,
exe: &Path,
expected_release_tag: &str,
current_version: &str,
allow_explicit_downgrade: bool,
) -> Result<()> {
crate::installer::verify_candidate_release(
exe,
expected_release_tag,
current_version,
allow_explicit_downgrade,
)
}
fn install_with_rollback<F>(&self, exe: &Path, bytes: &[u8], verify: F) -> Result<()>
where
F: FnOnce(&Path) -> bool,
{
crate::installer::install_with_rollback(exe, bytes, verify)
}
fn install_with_rollback_checked<F>(&self, exe: &Path, bytes: &[u8], verify: F) -> Result<()>
where
F: FnOnce(&Path) -> Result<()>,
{
crate::installer::install_with_rollback_checked(exe, bytes, verify)
}
fn rewrite_detector_braces(&self, s: &str) -> (String, usize) {
crate::subcommands::detectors::testing::rewrite_braces(s)
}
fn fix_single_brace_in_verify_blocks(&self, toml_text: &str) -> (String, usize) {
crate::subcommands::detectors::testing::fix_single_brace_in_verify_blocks(toml_text)
}
fn fix_verify_braces(&self, toml_text: &str) -> (String, usize) {
crate::subcommands::detectors::testing::fix_verify_braces_for_test(toml_text)
}
fn rewrite_braces_in_string_literals(&self, line: &str) -> (String, usize) {
crate::subcommands::detectors::testing::rewrite_braces_in_string_literals(line)
}
fn canonical_for_hot_id(&self, id: &str) -> Option<&'static str> {
crate::subcommands::explain::testing::canonical_for_hot_id(id)
}
fn explain_not_found(
&self,
detectors: &[DetectorSpec],
requested: &str,
lowered: &str,
) -> anyhow::Error {
crate::subcommands::explain::testing::explain_not_found(detectors, requested, lowered)
}
fn render_failing_region_presence_probe_json(&self) -> Result<String> {
crate::subcommands::backend::testing::render_failing_region_presence_probe_json()
}
fn doctor_canonicalize_for_shadow_check(&self, path: PathBuf) -> PathBuf {
crate::subcommands::doctor::testing::canonicalize_for_shadow_check(path)
}
fn doctor_should_run_gpu_self_tests(&self, gpu_available: bool, gpu_is_software: bool) -> bool {
crate::subcommands::doctor::testing::should_run_gpu_self_tests(
gpu_available,
gpu_is_software,
)
}
fn canonical_scan_args(&self) -> &'static str {
crate::subcommands::hook::testing::CANONICAL_SCAN_ARGS
}
fn hook_content(&self) -> &'static str {
crate::subcommands::hook::testing::HOOK_CONTENT
}
fn watch_content_hash(&self, data: &[u8]) -> u64 {
crate::subcommands::watch::testing::content_hash(data)
}
fn watch_duplicate_event_decisions(
&self,
first: &[u8],
second: &[u8],
elapsed: std::time::Duration,
) -> (bool, bool) {
crate::subcommands::watch::testing::duplicate_event_decisions(first, second, elapsed)
}
fn watch_findings_fingerprint(&self, matches: &[keyhog_core::RawMatch]) -> [u8; 32] {
crate::subcommands::watch::testing::findings_fingerprint(matches)
}
fn watch_duplicate_findings_decisions(
&self,
first: [u8; 32],
second: [u8; 32],
elapsed: std::time::Duration,
) -> (bool, bool) {
crate::subcommands::watch::testing::duplicate_findings_decisions(first, second, elapsed)
}
fn watch_resolve_roots(&self, requested: &[PathBuf]) -> Result<Vec<PathBuf>> {
crate::subcommands::watch::testing::resolve_watch_roots(requested)
}
fn watch_roots_hint(&self, roots: &[PathBuf]) -> String {
crate::subcommands::watch::testing::roots_hint(roots)
}
fn max_resident_findings(&self) -> usize {
crate::subcommands::scan_system::testing::MAX_RESIDENT_FINDINGS
}
fn parse_macos_mount_table_for_test(
&self,
text: &str,
include_network: bool,
) -> Result<Vec<PathBuf>> {
crate::subcommands::scan_system::testing::parse_macos_mount_table_for_test(
text,
include_network,
)
.map_err(anyhow::Error::from)
}
fn windows_drive_filter_decisions_for_test(&self) -> Result<(bool, bool, bool, bool)> {
crate::subcommands::scan_system::testing::windows_drive_filter_decisions_for_test()
.map_err(anyhow::Error::from)
}
fn windows_drive_skip_prefix_decisions_for_test(&self) -> (bool, bool) {
crate::subcommands::scan_system::testing::windows_drive_skip_prefix_decisions_for_test()
}
#[cfg(target_os = "linux")]
fn decoded_mount_target_if_included_for_test(
&self,
target: &str,
skip_path_prefixes: Vec<String>,
) -> Result<Option<String>> {
crate::subcommands::scan_system::testing::decoded_mount_target_if_included_for_test(
target,
skip_path_prefixes,
)
}
fn scan_system_discover_git_repos_for_test(&self, root: &Path) -> Result<Vec<PathBuf>> {
crate::subcommands::scan_system::testing::git_repos_for_test(root)
}
fn scan_system_chunk_fits_space_cap(
&self,
bytes_scanned: u64,
chunk_len: usize,
space_cap: u64,
) -> bool {
crate::subcommands::scan_system::testing::chunk_fits_space_cap(
bytes_scanned,
chunk_len,
space_cap,
)
}
fn finding_sink_new(&self) -> FindingSink {
FindingSink(crate::subcommands::scan_system::testing::FindingSink::new())
}
fn finding_sink_with_cap(&self, cap: usize) -> FindingSink {
FindingSink(crate::subcommands::scan_system::testing::FindingSink::with_cap(cap))
}
fn finding_sink_record_skipped_chunk(&self, sink: &mut FindingSink) {
sink.0.record_skipped_chunk();
}
fn finding_sink_skipped_chunks(&self, sink: &FindingSink) -> u64 {
sink.0.skipped_chunks()
}
fn finding_sink_absorb(&self, sink: &mut FindingSink, matches: Vec<RawMatch>) {
sink.0.absorb(matches);
}
fn finding_sink_is_empty(&self, sink: &FindingSink) -> bool {
sink.0.is_empty()
}
fn finding_sink_total(&self, sink: &FindingSink) -> u64 {
sink.0.total()
}
fn finding_sink_retained_len(&self, sink: &FindingSink) -> usize {
sink.0.retained_len()
}
fn finding_sink_cap(&self, sink: &FindingSink) -> usize {
sink.0.cap()
}
fn finding_sink_capped_warned(&self, sink: &FindingSink) -> bool {
sink.0.capped_warned()
}
fn finding_sink_retained_hash(
&self,
sink: &FindingSink,
index: usize,
) -> Option<keyhog_core::CredentialHash> {
sink.0.retained_hash(index)
}
fn finding_sink_retained_json(&self, sink: &FindingSink) -> serde_json::Result<String> {
sink.0.retained_json()
}
fn sanitise_thread_count(
&self,
requested: usize,
physical_cores: usize,
source: &'static str,
) -> usize {
crate::orchestrator_config::testing::sanitise_thread_count(
requested,
physical_cores,
source,
)
}
fn max_threads_cap(&self) -> usize {
crate::orchestrator_config::MAX_THREADS_CAP
}
fn load_detectors_or_embedded(&self, path: &Path) -> Result<Vec<DetectorSpec>> {
crate::orchestrator_config::load_detectors_or_embedded(path)
}
fn load_detectors_from_dir_with_cache(
&self,
source_dir: &Path,
cache_path: &Path,
) -> Result<Vec<DetectorSpec>> {
crate::orchestrator_config::testing::load_detectors_from_dir_with_cache(
source_dir, cache_path,
)
}
fn build_scanner_config(&self, args: &ScanArgs) -> ScannerConfig {
crate::orchestrator_config::build_scanner_config(args)
}
fn resolve_scan_config(&self, args: &mut ScanArgs) -> Result<()> {
crate::orchestrator_config::resolve_scan_config(args).map(|_| ())
}
fn resolve_scan_config_aws_canary_accounts(&self, args: &mut ScanArgs) -> Result<Vec<String>> {
crate::orchestrator_config::resolve_scan_config(args)
.map(|resolved| resolved.aws_canary_accounts)
}
fn render_effective_config_for_scanner(&self, scanner: ScannerConfig) -> String {
let resolved = crate::orchestrator_config::resolved_scan_config_for_scanner(scanner);
crate::orchestrator_config::render_effective_config(&resolved)
}
fn autoroute_config_digest_for_args(&self, args: &mut ScanArgs) -> Result<u64> {
let resolved = crate::orchestrator_config::resolve_scan_config(args)?;
Ok(crate::orchestrator_config::autoroute_config_digest(
&resolved,
))
}
fn autoroute_config_digest_for_scanner(&self, scanner: ScannerConfig) -> u64 {
let resolved = crate::orchestrator_config::resolved_scan_config_for_scanner(scanner);
crate::orchestrator_config::autoroute_config_digest(&resolved)
}
fn profiling_config_digests_for_args(
&self,
args: &mut ScanArgs,
) -> Result<([u8; 32], [u8; 32])> {
let resolved = crate::orchestrator_config::resolve_scan_config(args)?;
Ok((
crate::orchestrator_config::profiling_resolved_config_digest(&resolved),
crate::orchestrator_config::profiling_policy_digest(&resolved),
))
}
fn ml_threshold_default(&self) -> f64 {
crate::orchestrator_config::ML_THRESHOLD_DEFAULT
}
fn explicit_backend_override(
&self,
raw: Option<&str>,
) -> Result<Option<keyhog_scanner::ScanBackend>> {
crate::orchestrator::explicit_backend_override(raw)
}
fn forced_backend_runtime_detector_ids(
&self,
backend: &str,
body: &str,
) -> Result<Vec<String>> {
let detectors = keyhog_core::load_embedded_detectors_or_fail()?;
let forced = crate::orchestrator::explicit_backend_override(Some(backend))?
.ok_or_else(|| anyhow::anyhow!("'{backend}' is auto, not an explicit backend"))?;
let runtime =
crate::orchestrator::compile_default_scan_runtime(detectors, Some(forced), |e| {
anyhow::anyhow!("{e}")
})?
.with_backend_override(Some(forced));
let chunk = keyhog_core::Chunk {
data: body.to_string().into(),
metadata: keyhog_core::ChunkMetadata {
source_type: "filesystem".into(),
path: Some("watched.env".into()),
..Default::default()
},
};
Ok(runtime
.scan_chunk(&chunk)?
.iter()
.map(|m| m.detector_id.as_ref().to_string())
.collect())
}
fn disabled_gpu_dispatch_for_test(
&self,
body: &str,
recover_automatic_backend_faults: bool,
_guard: &ScanRuntimeGuard,
) -> Result<Vec<String>> {
crate::reset_scan_runtime_state();
let detectors = keyhog_core::load_embedded_detectors_or_fail()?;
let scanner = keyhog_scanner::CompiledScanner::compile_with_gpu_policy(
detectors,
keyhog_scanner::GpuInitPolicy::ForceDisabled,
)?;
let chunks = vec![keyhog_core::Chunk {
data: body.to_string().into(),
metadata: keyhog_core::ChunkMetadata {
source_type: "filesystem".into(),
path: Some("gpu-recovery.env".into()),
..Default::default()
},
}];
let outcome = crate::orchestrator::scan_selected_batch(
&scanner,
&chunks,
keyhog_scanner::ScanBackend::GpuWgpu,
#[cfg(feature = "gpu")]
None,
None,
scanner.execution_route_for_backend(keyhog_scanner::ScanBackend::GpuWgpu),
recover_automatic_backend_faults.then_some(crate::orchestrator::BackendRecoveryPlan {
backend: keyhog_scanner::ScanBackend::SimdCpu,
execution_route: scanner
.execution_route_for_backend(keyhog_scanner::ScanBackend::SimdCpu),
}),
)?;
if recover_automatic_backend_faults && !outcome.recovered {
anyhow::bail!("disabled GPU unexpectedly completed without recovery");
}
Ok(outcome
.per_chunk
.into_iter()
.flatten()
.map(|finding| finding.detector_id.as_ref().to_string())
.collect())
}
fn automatic_backend_recovery_allowed_for_test(
&self,
explicit_backend: Option<keyhog_scanner::ScanBackend>,
calibration_mode: bool,
gpu_runtime_policy: keyhog_scanner::gpu::GpuRuntimePolicy,
) -> bool {
crate::orchestrator::automatic_backend_recovery_allowed(
explicit_backend,
calibration_mode,
gpu_runtime_policy,
)
}
fn router_gpu_participates_for_test(
&self,
explicit_backend: Option<keyhog_scanner::ScanBackend>,
gpu_runtime_policy: keyhog_scanner::gpu::GpuRuntimePolicy,
) -> bool {
crate::orchestrator::router_gpu_participates_for_test(explicit_backend, gpu_runtime_policy)
}
fn router_uses_gpu_probe_for_test(&self, gpu_participates: bool) -> bool {
crate::orchestrator::router_uses_gpu_probe_for_test(gpu_participates)
}
fn current_target_digest_for_test(&self) -> [u8; 32] {
crate::execution_pack_install::current_target_digest()
}
fn autoroute_default_config_identity_for_test(&self) -> String {
crate::orchestrator::autoroute_default_config_identity()
}
fn allowlist_root_for_test(&self, path: &Path) -> PathBuf {
crate::orchestrator::allowlist_root_for_test(path)
}
fn backend_requires_coalesced_batch_pipeline_for_test(
&self,
explicit: Option<keyhog_scanner::ScanBackend>,
) -> bool {
crate::orchestrator::backend_requires_coalesced_batch_pipeline_for_test(explicit)
}
fn gpu_init_policy_for_args_for_test(&self, args: &ScanArgs) -> keyhog_scanner::GpuInitPolicy {
crate::orchestrator::gpu_init_policy_for_args_for_test(args)
}
fn gpu_init_policy_for_resolved_autoroute_for_test(
&self,
args: &ScanArgs,
autoroute_cache_path: Option<&Path>,
autoroute_gpu: bool,
autoroute_calibration: bool,
) -> keyhog_scanner::GpuInitPolicy {
crate::orchestrator::gpu_init_policy_for_resolved_autoroute_for_test(
args,
autoroute_cache_path,
autoroute_gpu,
autoroute_calibration,
)
}
fn scanner_panic_notice_for_test(&self, panicked: bool) -> Option<String> {
crate::orchestrator::scanner_panic_notice_for_test(panicked)
}
fn scan_exit_code(&self, findings: &[VerifiedFinding]) -> u8 {
crate::orchestrator::scan_exit_code(findings)
}
fn resolve_scan_exit_for_test(
&self,
has_new_entries: bool,
incremental_cache_failed: bool,
source_coverage_incomplete: bool,
) -> u8 {
crate::orchestrator::resolve_scan_exit_for_test(
has_new_entries,
incremental_cache_failed,
source_coverage_incomplete,
)
}
fn scan_orchestrator_from_parts_for_test(
&self,
args: ScanArgs,
detectors: Vec<DetectorSpec>,
scanner: Arc<CompiledScanner>,
signatures: std::collections::HashSet<Arc<str>>,
test_fixture_suppressions: TestFixtureSuppressions,
) -> ScanOrchestrator {
ScanOrchestrator(crate::orchestrator::ScanOrchestrator::from_parts_for_test(
args,
detectors,
scanner,
signatures,
test_fixture_suppressions.0,
))
}
fn scan_orchestrator_scanner<'a>(
&self,
orchestrator: &'a ScanOrchestrator,
) -> &'a CompiledScanner {
orchestrator.0.scanner()
}
fn scan_orchestrator_args<'a>(&self, orchestrator: &'a ScanOrchestrator) -> &'a ScanArgs {
orchestrator.0.args()
}
fn scan_orchestrator_detector_count(&self, orchestrator: &ScanOrchestrator) -> usize {
orchestrator.0.detector_count
}
fn scan_orchestrator_retained_detector_specs(&self, orchestrator: &ScanOrchestrator) -> usize {
#[cfg(feature = "verify")]
{
orchestrator
.0
.verifier_detectors
.as_deref()
.map_or(0, <[DetectorSpec]>::len)
}
#[cfg(not(feature = "verify"))]
{
let _ = orchestrator; 0
}
}
fn scan_orchestrator_scan_sources_for_test(
&self,
orchestrator: &ScanOrchestrator,
sources: Vec<Box<dyn Source>>,
show_progress: bool,
merkle: Option<Arc<keyhog_core::MerkleIndex>>,
_guard: &ScanRuntimeGuard,
) -> Result<Vec<RawMatch>> {
orchestrator
.0
.scan_sources_for_test(sources, show_progress, merkle)
}
fn scan_runtime_guard_for_test(&self) -> ScanRuntimeGuard {
let guard = match SCAN_RUNTIME_TEST_LOCK.lock() {
Ok(guard) => guard,
Err(poisoned) => {
SCAN_RUNTIME_TEST_LOCK.clear_poison();
poisoned.into_inner()
}
};
ScanRuntimeGuard { _guard: guard }
}
fn seed_scan_runtime_state_for_test(&self, _guard: &ScanRuntimeGuard) {
use std::sync::atomic::Ordering::Relaxed;
crate::SCANNED_CHUNKS.store(11, Relaxed);
crate::TOTAL_CHUNKS.store(13, Relaxed);
crate::FINDINGS_COUNT.store(17, Relaxed);
crate::GPU_SCANNED_CHUNKS.store(19, Relaxed);
crate::BACKEND_RECOVERY_EVENTS.store(2, Relaxed);
crate::BACKEND_RECOVERED_CHUNKS.store(3, Relaxed);
crate::BACKEND_RECOVERED_BYTES.store(5, Relaxed);
let _source_error_receipt = crate::record_source_error();
let _failed_source_receipt = crate::record_failed_source();
let _incremental_cache_receipt = crate::record_incremental_cache_persist_failed();
let _scanner_panic_receipt = crate::record_scanner_panic();
keyhog_scanner::telemetry::enable_dogfood();
keyhog_scanner::telemetry::add_example_suppressions(23);
}
fn reset_scan_runtime_state_for_test(&self, _guard: &ScanRuntimeGuard) {
crate::reset_scan_runtime_state();
}
fn scan_runtime_snapshot(&self, _guard: &ScanRuntimeGuard) -> ScanRuntimeSnapshot {
use std::sync::atomic::Ordering::Relaxed;
ScanRuntimeSnapshot {
scanned_chunks: crate::SCANNED_CHUNKS.load(Relaxed),
total_chunks: crate::TOTAL_CHUNKS.load(Relaxed),
findings_count: crate::FINDINGS_COUNT.load(Relaxed),
gpu_scanned_chunks: crate::GPU_SCANNED_CHUNKS.load(Relaxed),
backend_recovery_events: crate::BACKEND_RECOVERY_EVENTS.load(Relaxed),
backend_recovered_chunks: crate::BACKEND_RECOVERED_CHUNKS.load(Relaxed),
backend_recovered_bytes: crate::BACKEND_RECOVERED_BYTES.load(Relaxed),
source_errors: crate::SOURCE_ERRORS.load(Relaxed),
failed_sources: crate::FAILED_SOURCES.load(Relaxed),
incremental_cache_errors: crate::INCREMENTAL_CACHE_ERRORS.load(Relaxed),
scanner_panicked: crate::SCANNER_PANICKED.load(Relaxed),
dogfood_enabled: keyhog_scanner::telemetry::is_dogfood_enabled(),
example_suppressions: keyhog_scanner::telemetry::example_suppression_count(),
decode_truncations: keyhog_scanner::telemetry::decode_truncation_count(),
}
}
fn backend_recovery_summaries_for_test(
&self,
_guard: &ScanRuntimeGuard,
) -> Vec<keyhog_core::ScanBackendRecoverySummary> {
crate::backend_recovery_summaries()
}
fn scanned_chunks(&self, _guard: &ScanRuntimeGuard) -> usize {
crate::SCANNED_CHUNKS.load(std::sync::atomic::Ordering::Relaxed)
}
fn scanner_panicked(&self, _guard: &ScanRuntimeGuard) -> bool {
crate::SCANNER_PANICKED.load(std::sync::atomic::Ordering::Relaxed)
}
fn verification_tally(&self, findings: &[VerifiedFinding]) -> VerificationTally {
let breakdown = crate::orchestrator::verification_breakdown(findings);
VerificationTally {
live: breakdown.live,
inactive: breakdown.inactive,
skipped: breakdown.skipped,
unverifiable: breakdown.unverifiable,
incomplete: breakdown.incomplete,
}
}
fn render_verification_summary(
&self,
findings: &[VerifiedFinding],
color: bool,
) -> Option<String> {
let breakdown = crate::orchestrator::verification_breakdown(findings);
crate::orchestrator::render_verification_line(&breakdown, findings.len(), color)
}
fn render_severity_summary(&self, findings: &[VerifiedFinding], color: bool) -> Option<String> {
crate::orchestrator::render_severity_line(findings, color)
}
fn render_progress_bar(&self, frac: f64, width: usize, color: bool) -> String {
crate::orchestrator::render_progress_bar(frac, width, color)
}
fn render_scanning_ticker(
&self,
scanned: usize,
total: usize,
findings: usize,
elapsed: f64,
frame: usize,
color: bool,
) -> String {
crate::orchestrator::render_ticker_line(scanned, total, findings, elapsed, frame, color)
}
fn render_verification_ticker(
&self,
total: usize,
elapsed: f64,
frame: usize,
color: bool,
) -> String {
crate::orchestrator::render_verification_ticker_line(total, elapsed, frame, color)
}
fn render_reporting_ticker(
&self,
total: usize,
elapsed: f64,
frame: usize,
color: bool,
) -> String {
crate::orchestrator::render_reporting_ticker_line(total, elapsed, frame, color)
}
fn fmt_secs(&self, secs: f64) -> String {
crate::orchestrator::fmt_secs(secs)
}
fn ticker_guard_spawns_and_joins(&self) -> bool {
use std::sync::atomic::Ordering;
use std::time::Duration;
let (tx, rx) = std::sync::mpsc::channel();
let guard = crate::orchestrator::TickerGuard::spawn("test", move |done, _started| {
while !done.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(1));
}
let _ = tx.send(()); });
guard.stop();
rx.recv_timeout(Duration::from_secs(1)).is_ok()
}
fn redact_url_target(&self, raw: &str) -> String {
crate::reporting::redact_url_target(raw)
}
fn skip_dir_policy_from_toml(
&self,
toml: &str,
) -> std::result::Result<SkipDirPolicyView, String> {
crate::skip_dirs::testing::policy_from_toml(toml).map(SkipDirPolicyView)
}
fn skip_dir_policy_from_bundled(&self) -> std::result::Result<SkipDirPolicyView, String> {
crate::skip_dirs::testing::policy_from_bundled().map(SkipDirPolicyView)
}
fn skip_dir_policy_from_bundled_plus_user(
&self,
user_toml: &str,
) -> std::result::Result<SkipDirPolicyView, String> {
crate::skip_dirs::testing::policy_from_bundled_plus_user(user_toml).map(SkipDirPolicyView)
}
fn skip_dir_section_counts(
&self,
toml: &str,
) -> std::result::Result<(usize, usize, usize), String> {
crate::skip_dirs::testing::section_counts(toml)
}
#[cfg(unix)]
fn merge_daemon_static_recovery(
&self,
rejections: std::collections::BTreeMap<String, u64>,
status: keyhog_scanner::telemetry::StaticRecoveryStatus,
) -> Result<StaticRecoveryMergeSnapshot> {
use crate::daemon::protocol::{RequiredOption, Response, SourceCoverageGaps};
let before = keyhog_scanner::telemetry::static_recovery_status();
crate::subcommands::scan::unwrap_scan_results(Response::ScanResults {
path: None,
matches: Vec::new(),
engine_example_suppressions: 0,
dogfood_events: Vec::new(),
static_recovery_rejections: rejections,
static_recovery_status: status,
dogfood_detail_events_dropped: 0,
source_coverage_gaps: SourceCoverageGaps::default(),
backend_recovery: RequiredOption::None,
profile: RequiredOption::None,
})?;
let after = keyhog_scanner::telemetry::static_recovery_status();
Ok(StaticRecoveryMergeSnapshot { before, after })
}
}
fn expose_baseline(inner: crate::baseline::Baseline) -> Baseline {
Baseline {
version: inner.version,
created: inner.created,
entries: inner
.entries
.into_iter()
.map(|entry| BaselineEntry {
detector_id: entry.detector_id,
credential_hash: entry.credential_hash,
file_path: entry.file_path,
line: entry.line,
})
.collect(),
}
}
impl Baseline {
fn to_internal(&self) -> crate::baseline::Baseline {
let mut baseline = crate::baseline::Baseline::empty();
baseline.version = self.version;
baseline.created = self.created.clone();
baseline.entries = self
.entries
.iter()
.map(|entry| crate::baseline::BaselineEntry {
detector_id: entry.detector_id.clone(),
credential_hash: entry.credential_hash.clone(),
file_path: entry.file_path.clone(),
line: entry.line,
legacy_status: None,
})
.collect();
baseline
}
}
pub struct StableHashProbe {
inner: crate::stable_hash::StableHasher,
}
impl StableHashProbe {
pub fn new(domain: &str) -> Self {
Self {
inner: crate::stable_hash::StableHasher::new(domain),
}
}
pub fn bool(mut self, name: &str, value: bool) -> Self {
self.inner.field_bool(name, value);
self
}
pub fn bytes(mut self, name: &str, value: &[u8]) -> Self {
self.inner.field_bytes(name, value);
self
}
pub fn f64_bits(mut self, name: &str, value: f64) -> Self {
self.inner.field_f64_bits(name, value);
self
}
pub fn opt_path(mut self, name: &str, value: Option<&std::path::Path>) -> Self {
self.inner.field_option_path(name, value);
self
}
pub fn opt_str(mut self, name: &str, value: Option<&str>) -> Self {
self.inner.field_option_str(name, value);
self
}
pub fn opt_u64(mut self, name: &str, value: Option<u64>) -> Self {
self.inner.field_option_u64(name, value);
self
}
pub fn opt_usize(mut self, name: &str, value: Option<usize>) -> Self {
self.inner.field_option_usize(name, value);
self
}
pub fn path(mut self, name: &str, value: &std::path::Path) -> Self {
self.inner.field_path(name, value);
self
}
pub fn str(mut self, name: &str, value: &str) -> Self {
self.inner.field_str(name, value);
self
}
pub fn u64(mut self, name: &str, value: u64) -> Self {
self.inner.field_u64(name, value);
self
}
pub fn usize(mut self, name: &str, value: usize) -> Self {
self.inner.field_usize(name, value);
self
}
pub fn finish(&self) -> u64 {
self.inner.finish_u64()
}
}