forked from mirrors/kingfisher
264 lines
8 KiB
Rust
264 lines
8 KiB
Rust
use std::io::IsTerminal;
|
|
|
|
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
|
|
use once_cell::sync::Lazy;
|
|
use strum::Display;
|
|
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
|
|
use tracing::Level;
|
|
|
|
use crate::cli::commands::{
|
|
access_map::AccessMapArgs, revoke::RevokeArgs, rules::RulesArgs, scan::ScanCommandArgs,
|
|
validate::ValidateArgs, view::ViewArgs,
|
|
};
|
|
|
|
#[deny(missing_docs)]
|
|
#[derive(Parser, Debug)]
|
|
#[command(version = env!("CARGO_PKG_VERSION"))]
|
|
/// Kingfisher - Detect and validate secrets across files and full Git history
|
|
pub struct CommandLineArgs {
|
|
/// The command to execute
|
|
#[command(subcommand)]
|
|
pub command: Command,
|
|
|
|
/// Global arguments that apply to all subcommands
|
|
#[command(flatten)]
|
|
pub global_args: GlobalArgs,
|
|
}
|
|
impl CommandLineArgs {
|
|
/// Parse command-line arguments.
|
|
///
|
|
/// Automatically respects `NO_COLOR` and maps `--quiet` into disabling progress bars.
|
|
pub fn parse_args() -> Self {
|
|
// Use standard `Parser::parse` for simplicity
|
|
let mut args = CommandLineArgs::parse();
|
|
|
|
// Apply NO_COLOR environment variable
|
|
if std::env::var("NO_COLOR").is_ok() {
|
|
args.global_args.color = Mode::Never;
|
|
}
|
|
|
|
// If quiet is enabled, disable progress
|
|
if args.global_args.quiet {
|
|
args.global_args.progress = Mode::Never;
|
|
}
|
|
|
|
// Handle deprecated --ignore-certs flag as alias for --tls-mode=off
|
|
if args.global_args.ignore_certs {
|
|
args.global_args.tls_mode = TlsMode::Off;
|
|
}
|
|
|
|
if let Some(suffix) = args.global_args.user_agent_suffix.as_mut() {
|
|
let trimmed = suffix.trim();
|
|
if trimmed.is_empty() {
|
|
args.global_args.user_agent_suffix = None;
|
|
} else if trimmed.len() != suffix.len() {
|
|
*suffix = trimmed.to_string();
|
|
}
|
|
}
|
|
|
|
args
|
|
}
|
|
}
|
|
|
|
/// Top-level subcommands
|
|
#[derive(Subcommand, Debug)]
|
|
pub enum Command {
|
|
/// Scan content for secrets and sensitive information
|
|
Scan(ScanCommandArgs),
|
|
|
|
/// Manage rules
|
|
#[command(alias = "rule")]
|
|
Rules(RulesArgs),
|
|
|
|
/// Directly validate a known secret against a rule's validator (bypasses pattern matching)
|
|
Validate(ValidateArgs),
|
|
|
|
/// Directly revoke a known secret against a rule's revocation config
|
|
Revoke(RevokeArgs),
|
|
|
|
/// Map a cloud credential to its identity, permissions, and blast radius
|
|
#[command(name = "access-map", alias = "access_map")]
|
|
AccessMap(AccessMapArgs),
|
|
|
|
/// View Kingfisher JSON/JSONL reports in a local web UI
|
|
View(ViewArgs),
|
|
|
|
/// Update the Kingfisher binary
|
|
#[command(name = "update", alias = "self-update")]
|
|
SelfUpdate,
|
|
}
|
|
|
|
pub static RAM_GB: Lazy<Option<f64>> = Lazy::new(|| {
|
|
if sysinfo::IS_SUPPORTED_SYSTEM {
|
|
let s = System::new_with_specifics(
|
|
RefreshKind::new().with_memory(MemoryRefreshKind::new().with_ram()),
|
|
);
|
|
Some(s.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
/// Top-level global CLI arguments
|
|
#[derive(Args, Debug, Clone)]
|
|
#[command(next_help_heading = "Global Options")]
|
|
pub struct GlobalArgs {
|
|
/// Enable verbose output (up to 3 times for more detail)
|
|
#[arg(global = true, long = "verbose", short = 'v', action = ArgAction::Count)]
|
|
pub verbose: u8,
|
|
|
|
/// Suppress non-error messages and disable progress bars
|
|
#[arg(global = true, long, short)]
|
|
pub quiet: bool,
|
|
|
|
/// TLS certificate validation mode for secret validation requests.
|
|
///
|
|
/// - strict: Full WebPKI validation (default)
|
|
/// - lax: Accept self-signed/unknown CA, but enforce hostname + expiry
|
|
/// - off: Disable all certificate validation
|
|
#[arg(global = true, long, value_enum, default_value = "strict")]
|
|
pub tls_mode: TlsMode,
|
|
|
|
/// Disable TLS certificate validation (deprecated: use --tls-mode=off)
|
|
#[arg(global = true, long, hide = true)]
|
|
pub ignore_certs: bool,
|
|
|
|
/// Update the Kingfisher binary to the latest release
|
|
#[arg(global = true, long = "self-update", alias = "update", default_value_t = false)]
|
|
pub self_update: bool,
|
|
|
|
/// Disable automatic update checks
|
|
#[arg(global = true, long = "no-update-check", default_value_t = false)]
|
|
pub no_update_check: bool,
|
|
|
|
/// Append a custom suffix to the default Kingfisher user-agent string
|
|
#[arg(global = true, long = "user-agent-suffix", value_name = "SUFFIX")]
|
|
pub user_agent_suffix: Option<String>,
|
|
|
|
// Internal fields (not CLI arguments)
|
|
#[clap(skip)]
|
|
pub color: Mode,
|
|
|
|
#[clap(skip)]
|
|
pub progress: Mode,
|
|
}
|
|
|
|
impl Default for GlobalArgs {
|
|
fn default() -> Self {
|
|
Self {
|
|
verbose: 0,
|
|
quiet: false,
|
|
tls_mode: TlsMode::Strict,
|
|
ignore_certs: false,
|
|
self_update: false,
|
|
no_update_check: false,
|
|
user_agent_suffix: None,
|
|
color: Mode::Auto,
|
|
progress: Mode::Auto,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl GlobalArgs {
|
|
pub fn use_color<T: IsTerminal>(&self, out: T) -> bool {
|
|
match self.color {
|
|
Mode::Never => false,
|
|
Mode::Always => true,
|
|
Mode::Auto => out.is_terminal(),
|
|
}
|
|
}
|
|
|
|
pub fn use_progress(&self) -> bool {
|
|
match self.progress {
|
|
Mode::Never => false,
|
|
Mode::Always => true,
|
|
Mode::Auto => std::io::stderr().is_terminal(),
|
|
}
|
|
}
|
|
|
|
pub fn log_level(&self) -> Level {
|
|
if self.quiet {
|
|
Level::INFO
|
|
} else {
|
|
match self.verbose {
|
|
0 => Level::INFO, // Default level if no `-v` is provided
|
|
1 => Level::DEBUG, // `-v`
|
|
2 => Level::TRACE, // `-vv`
|
|
_ => Level::TRACE, // `-vvv` or more
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Mode for enabling or disabling features based on terminal capabilities
|
|
/// Generic mode with `auto/never/always`.
|
|
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
|
|
#[strum(serialize_all = "kebab-case")]
|
|
pub enum Mode {
|
|
#[default]
|
|
Auto,
|
|
Never,
|
|
Always,
|
|
}
|
|
|
|
/// TLS certificate validation mode for secret validation requests.
|
|
///
|
|
/// Controls how TLS certificates are validated when connecting to endpoints
|
|
/// during credential validation (e.g., database connections, API calls).
|
|
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
|
|
#[strum(serialize_all = "kebab-case")]
|
|
pub enum TlsMode {
|
|
/// Full WebPKI certificate validation: trusted CA chain, hostname match, not expired.
|
|
/// This is the default and most secure mode.
|
|
#[default]
|
|
Strict,
|
|
|
|
/// Accept self-signed or unknown CA certificates, but still enforce:
|
|
/// - Hostname must match certificate's CN/SAN
|
|
/// - Certificate must not be expired
|
|
/// - TLS 1.2 or higher required
|
|
///
|
|
/// Useful for database connections (PostgreSQL, MySQL, MongoDB) that often use
|
|
/// self-signed certificates or private CAs (e.g., Amazon RDS).
|
|
Lax,
|
|
|
|
/// Disable all TLS certificate validation. Use with extreme caution.
|
|
/// Equivalent to the legacy `--ignore-certs` flag.
|
|
Off,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn tls_mode_default_is_strict() {
|
|
assert_eq!(TlsMode::default(), TlsMode::Strict);
|
|
}
|
|
|
|
#[test]
|
|
fn tls_mode_display_formats_correctly() {
|
|
assert_eq!(TlsMode::Strict.to_string(), "strict");
|
|
assert_eq!(TlsMode::Lax.to_string(), "lax");
|
|
assert_eq!(TlsMode::Off.to_string(), "off");
|
|
}
|
|
|
|
#[test]
|
|
fn global_args_default_has_strict_tls() {
|
|
let args = GlobalArgs::default();
|
|
assert_eq!(args.tls_mode, TlsMode::Strict);
|
|
assert!(!args.ignore_certs);
|
|
}
|
|
|
|
#[test]
|
|
fn tls_mode_ordering_is_correct() {
|
|
// Strict < Lax < Off (more secure modes sort before less secure)
|
|
assert!(TlsMode::Strict < TlsMode::Lax);
|
|
assert!(TlsMode::Lax < TlsMode::Off);
|
|
}
|
|
|
|
#[test]
|
|
fn mode_default_is_auto() {
|
|
assert_eq!(Mode::default(), Mode::Auto);
|
|
}
|
|
}
|