mod scan;
pub use scan::ScanArgs;
use clap::{Parser, ValueEnum};
use keyhog_core::DedupScope;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "keyhog",
about = "KeyHog: The developer-first secret scanner.\nFind leaked credentials in your code before hackers do. Fast, accurate, and verifying.",
after_help = "EXIT CODES:\n 0 Success (no secrets found)\n 1 Secrets found (unverified or verification skipped)\n 2 User error (e.g., config error, unreadable path)\n 3 System error (local environment failure or detector-corpus audit failure)\n 4 Health/self-test failure (doctor unhealthy / repair could not restore a working binary / backend --self-test failed)\n 10 Live credentials found (requires --verify)\n 11 Scanner thread panicked mid-scan (state is unreliable)",
disable_version_flag = true
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(short = 'V', long)]
pub version: bool,
}
#[derive(clap::Subcommand)]
pub enum Command {
#[command(verbatim_doc_comment)]
Scan(Box<ScanArgs>),
#[command(verbatim_doc_comment)]
Hook {
#[command(subcommand)]
command: crate::subcommands::hook::HookCommand,
},
#[command(verbatim_doc_comment)]
Detectors(DetectorArgs),
#[command(verbatim_doc_comment)]
Explain(ExplainArgs),
#[command(verbatim_doc_comment)]
Diff(DiffArgs),
#[command(verbatim_doc_comment)]
Calibrate(CalibrateArgs),
#[command(verbatim_doc_comment)]
Watch(WatchArgs),
#[command(verbatim_doc_comment)]
Completion(CompletionArgs),
#[command(verbatim_doc_comment)]
Backend(BackendArgs),
#[command(verbatim_doc_comment)]
Doctor(DoctorArgs),
#[command(verbatim_doc_comment)]
Update(UpdateArgs),
#[command(verbatim_doc_comment)]
Repair(RepairArgs),
#[command(verbatim_doc_comment)]
Uninstall(UninstallArgs),
#[command(verbatim_doc_comment)]
ScanSystem(ScanSystemArgs),
#[command(verbatim_doc_comment)]
Daemon(DaemonArgs),
#[cfg(feature = "tui")]
#[command(verbatim_doc_comment)]
Tui(TuiArgs),
}
#[cfg(feature = "tui")]
#[derive(Parser)]
pub struct TuiArgs {
#[arg(value_name = "PATH", default_value = ".")]
pub path: PathBuf,
#[arg(long, value_name = "N", default_value_t = 0)]
pub max_files: usize,
#[arg(long, value_name = "N", default_value_t = 200)]
pub feed_depth: usize,
#[arg(long, value_name = "MS", default_value_t = 0)]
pub throttle_ms: u64,
}
#[derive(Parser)]
pub struct DaemonArgs {
#[command(subcommand)]
pub action: DaemonAction,
}
#[derive(clap::Subcommand)]
pub enum DaemonAction {
Start {
#[arg(long, value_name = "PATH")]
socket: Option<PathBuf>,
#[arg(long, default_value = "detectors")]
detectors: PathBuf,
},
Stop {
#[arg(long, value_name = "PATH")]
socket: Option<PathBuf>,
},
Status {
#[arg(long, value_name = "PATH")]
socket: Option<PathBuf>,
},
}
#[derive(Parser)]
pub struct ScanSystemArgs {
#[arg(long, default_value = "50G", value_parser = parse_space_bytes)]
pub space: u64,
#[arg(long, default_value_t = false)]
pub include_network: bool,
#[arg(long, default_value_t = false)]
pub no_git_history: bool,
#[arg(long, default_value_t = false)]
pub respect_gitignore: bool,
#[arg(long)]
pub output: Option<PathBuf>,
#[arg(long, default_value = "detectors")]
pub detectors: PathBuf,
#[arg(long, value_name = "N", value_parser = crate::value_parsers::parse_positive_thread_count)]
pub threads: Option<usize>,
#[arg(long, default_value_t = false)]
pub lockdown: bool,
}
#[doc(hidden)]
pub fn parse_space_bytes(s: &str) -> Result<u64, String> {
crate::value_parsers::parse_byte_size(s).map(|bytes| bytes as u64)
}
#[derive(Parser)]
pub struct CompletionArgs {
#[arg(value_enum)]
pub shell: clap_complete::Shell,
}
#[derive(Parser)]
pub struct BackendArgs {
#[arg(long)]
pub probe_bytes: Option<u64>,
#[arg(long, default_value_t = 1509)]
pub patterns: usize,
#[arg(long)]
pub self_test: bool,
#[arg(long, requires = "self_test")]
pub json: bool,
}
#[derive(Parser)]
pub struct DoctorArgs {}
#[derive(Parser)]
pub struct UpdateArgs {
#[arg(long)]
pub check: bool,
#[arg(long)]
pub version: Option<String>,
#[arg(long)]
pub variant: Option<String>,
}
#[derive(Parser)]
pub struct RepairArgs {
#[arg(long)]
pub force: bool,
#[arg(long)]
pub version: Option<String>,
#[arg(long)]
pub variant: Option<String>,
}
#[derive(Parser)]
pub struct UninstallArgs {
#[arg(long)]
pub yes: bool,
}
#[derive(Parser)]
pub struct WatchArgs {
#[arg(value_name = "PATH", default_value = ".")]
pub path: PathBuf,
#[arg(short, long, default_value = "detectors")]
pub detectors: PathBuf,
#[arg(long)]
pub quiet: bool,
}
#[derive(Parser)]
pub struct CalibrateArgs {
#[arg(long, value_name = "DETECTOR_ID")]
pub tp: Vec<String>,
#[arg(long, value_name = "DETECTOR_ID")]
pub fp: Vec<String>,
#[arg(long, conflicts_with_all = ["tp", "fp"])]
pub show: bool,
#[arg(long, value_name = "PATH")]
pub cache: Option<PathBuf>,
}
#[derive(Parser)]
pub struct DiffArgs {
pub before: PathBuf,
pub after: PathBuf,
#[arg(long)]
pub hide_unchanged: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Parser)]
pub struct ExplainArgs {
pub detector_id: String,
#[arg(short, long, default_value = "detectors")]
pub detectors: PathBuf,
}
#[derive(Parser)]
pub struct DetectorArgs {
#[arg(value_name = "VERB", value_parser = crate::value_parsers::parse_detectors_verb)]
pub verb: Option<String>,
#[arg(short, long, default_value = "detectors")]
pub detectors: PathBuf,
#[arg(short, long)]
pub search: Option<String>,
#[arg(short, long, default_value_t = false)]
pub verbose: bool,
#[arg(long, conflicts_with = "fix")]
pub audit: bool,
#[arg(long, conflicts_with = "audit")]
pub fix: bool,
#[arg(long, requires = "fix")]
pub dry_run: bool,
#[arg(long, conflicts_with_all = ["audit", "fix"])]
pub json: bool,
}
#[derive(Clone, ValueEnum)]
pub enum SeverityFilter {
Info,
Low,
Medium,
High,
Critical,
}
impl SeverityFilter {
pub fn to_severity(&self) -> keyhog_core::Severity {
match self {
Self::Info => keyhog_core::Severity::Info,
Self::Low => keyhog_core::Severity::Low,
Self::Medium => keyhog_core::Severity::Medium,
Self::High => keyhog_core::Severity::High,
Self::Critical => keyhog_core::Severity::Critical,
}
}
}
#[derive(Clone, ValueEnum)]
pub enum OutputFormat {
Text,
Json,
Jsonl,
Sarif,
Csv,
Html,
Junit,
}
#[derive(Clone, ValueEnum, PartialEq)]
pub enum CliDedupScope {
Credential,
File,
None,
}
impl CliDedupScope {
pub fn to_core(&self) -> DedupScope {
match self {
Self::Credential => DedupScope::Credential,
Self::File => DedupScope::File,
Self::None => DedupScope::None,
}
}
}
pub fn command() -> clap::Command {
use clap::CommandFactory;
let count = keyhog_core::embedded_detector_count();
let long_help = format!(
"Filter detectors by substring match (case-insensitive) against id, \
name, service, and keywords. Useful for finding detectors in the \
{count}-strong corpus (e.g. `keyhog detectors --search aws`)."
);
Cli::command().mut_subcommand("detectors", move |sub| {
sub.mut_arg("search", move |arg| arg.long_help(long_help.clone()))
})
}
pub fn parse() -> Cli {
use clap::FromArgMatches;
let matches = command().get_matches();
match Cli::from_arg_matches(&matches) {
Ok(cli) => cli,
Err(err) => err.exit(),
}
}