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 Runtime error (e.g., config error, unreadable path)\n 10 Live credentials found (requires --verify)",
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),
}
#[derive(Parser)]
pub struct ScanArgs {
#[arg(short, long, default_value = "detectors")]
pub detectors: PathBuf,
#[arg(value_name = "PATH", conflicts_with = "path")]
pub input: Option<PathBuf>,
#[arg(short, long)]
pub path: Option<PathBuf>,
#[cfg(feature = "binary")]
#[arg(long)]
pub binary: bool,
#[arg(long)]
pub stdin: bool,
#[cfg(feature = "git")]
#[arg(long)]
pub git_blobs: Option<PathBuf>,
#[cfg(feature = "git")]
#[arg(long, value_name = "BASE_REF")]
pub git_diff: Option<String>,
#[cfg(feature = "git")]
#[arg(long, value_name = "PATH")]
pub git_history: Option<PathBuf>,
#[cfg(feature = "git")]
#[arg(long)]
pub git_staged: bool,
#[cfg(feature = "git")]
#[arg(long, requires = "git_diff")]
pub git_diff_path: Option<PathBuf>,
#[cfg(feature = "github")]
#[arg(long, requires = "github_token", value_name = "ORG")]
pub github_org: Option<String>,
#[cfg(feature = "github")]
#[arg(long, requires = "github_org", value_name = "PAT")]
pub github_token: Option<String>,
#[cfg(feature = "s3")]
#[arg(long, value_name = "BUCKET")]
pub s3_bucket: Option<String>,
#[cfg(feature = "s3")]
#[arg(long, requires = "s3_bucket", value_name = "PREFIX")]
pub s3_prefix: Option<String>,
#[cfg(feature = "s3")]
#[arg(long, requires = "s3_bucket", value_name = "URL")]
pub s3_endpoint: Option<String>,
#[cfg(feature = "docker")]
#[arg(long, value_name = "IMAGE")]
pub docker_image: Option<String>,
#[cfg(feature = "web")]
#[arg(long, value_name = "URL", num_args = 1..)]
pub url: Option<Vec<String>>,
#[cfg(feature = "git")]
#[arg(long, default_value = "1000")]
pub max_commits: usize,
#[cfg(feature = "verify")]
#[arg(long)]
pub verify: bool,
#[arg(long)]
pub show_secrets: bool,
#[arg(long, default_value = "text", value_enum)]
pub format: OutputFormat,
#[arg(long)]
pub progress: bool,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long, default_value = "5")]
pub timeout: u64,
#[arg(long, default_value = "5")]
pub rate: usize,
#[arg(short, long, value_enum)]
pub severity: Option<SeverityFilter>,
#[arg(long, value_name = "SIZE", value_parser = crate::utils::parse_byte_size)]
pub max_file_size: Option<usize>,
#[arg(long, value_name = "NAME")]
pub source: Option<Vec<String>>,
#[cfg_attr(feature = "full", arg(long, conflicts_with_all = ["deep", "no_decode", "no_entropy"]))]
#[cfg_attr(not(feature = "full"), arg(long, conflicts_with_all = ["deep", "no_decode"]))]
pub fast: bool,
#[cfg_attr(feature = "full", arg(long, conflicts_with_all = ["fast", "no_decode", "no_entropy"]))]
#[cfg_attr(not(feature = "full"), arg(long, conflicts_with_all = ["fast", "no_decode"]))]
pub deep: bool,
#[arg(long)]
pub no_decode: bool,
#[cfg(feature = "full")]
#[arg(long)]
pub no_entropy: bool,
#[cfg(feature = "full")]
#[arg(long, default_value = "0.5", value_name = "THRESHOLD")]
pub ml_threshold: f64,
#[arg(long, value_name = "FLOAT", value_parser = crate::utils::parse_min_confidence)]
pub min_confidence: Option<f64>,
#[arg(long, value_name = "N")]
pub threads: Option<usize>,
#[arg(long, default_value = "credential", value_enum)]
pub dedup: CliDedupScope,
#[arg(long, value_name = "PATH")]
pub config: Option<PathBuf>,
#[arg(long, value_name = "PATH", conflicts_with_all = ["create_baseline", "update_baseline"])]
pub baseline: Option<PathBuf>,
#[arg(long, value_name = "PATH", conflicts_with_all = ["baseline", "update_baseline"])]
pub create_baseline: Option<PathBuf>,
#[arg(long, value_name = "PATH", conflicts_with_all = ["baseline", "create_baseline"])]
pub update_baseline: Option<PathBuf>,
#[arg(long, value_name = "DEPTH", value_parser = crate::utils::parse_decode_depth)]
pub decode_depth: Option<usize>,
#[arg(long, value_name = "SIZE", value_parser = crate::utils::parse_byte_size)]
pub decode_size_limit: Option<usize>,
#[cfg(feature = "full")]
#[arg(long)]
pub entropy_source_files: bool,
#[arg(long)]
pub no_default_excludes: bool,
#[arg(long, value_name = "PATH", num_args = 1..)]
pub exclude_paths: Option<Vec<String>>,
#[cfg(feature = "full")]
#[arg(long, value_name = "BITS")]
pub entropy_threshold: Option<f64>,
#[arg(long)]
pub no_unicode_norm: bool,
#[arg(long)]
pub no_ml: bool,
#[arg(long)]
pub benchmark: bool,
#[arg(long, value_name = "WEIGHT")]
pub ml_weight: Option<f64>,
#[arg(skip)]
pub known_prefixes: Vec<String>,
#[arg(skip)]
pub secret_keywords: Vec<String>,
#[arg(skip)]
pub test_keywords: Vec<String>,
#[arg(skip)]
pub placeholder_keywords: Vec<String>,
}
#[derive(Parser)]
pub struct DetectorArgs {
#[arg(short, long, default_value = "detectors")]
pub detectors: PathBuf,
}
#[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,
}
#[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,
}
}
}