#[derive(Debug, Clone)]
pub struct CommandArg(String);
impl CommandArg {
#[must_use]
pub const fn new(arg: String) -> Self {
Self(arg)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
fn is_sensitive_key(key: &str) -> bool {
const SENSITIVE_KEYS: [&str; 7] = [
"password",
"token",
"secret",
"api_key",
"apikey",
"auth",
"authorization",
];
SENSITIVE_KEYS
.iter()
.any(|candidate| key.eq_ignore_ascii_case(candidate))
}
#[must_use]
pub fn is_sensitive_arg(arg: &CommandArg) -> bool {
arg.as_str()
.split_once('=')
.is_some_and(|(key, _)| is_sensitive_key(key.trim()))
}
#[must_use]
pub fn redact_argument(arg: &CommandArg) -> CommandArg {
if is_sensitive_arg(arg) {
if let Some((key, _)) = arg.as_str().split_once('=') {
let trimmed = key.trim();
return CommandArg::new(format!("{trimmed}=***REDACTED***"));
}
return CommandArg::new(String::from("***REDACTED***"));
}
arg.clone()
}
#[must_use]
pub fn redact_sensitive_args(args: &[CommandArg]) -> Vec<CommandArg> {
args.iter().map(redact_argument).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_sensitive_arg_only_flags_known_keys() {
let token = CommandArg::new(String::from("token=abc"));
assert!(is_sensitive_arg(&token));
let positional = CommandArg::new(String::from("secrets.yml"));
assert!(!is_sensitive_arg(&positional));
let path_arg = CommandArg::new(String::from("path=/tmp/secrets.yml"));
assert!(!is_sensitive_arg(&path_arg));
let spaced = CommandArg::new(String::from(" PASSWORD = value "));
assert!(is_sensitive_arg(&spaced));
}
#[test]
fn redact_argument_preserves_non_sensitive_pairs() {
let redacted = redact_argument(&CommandArg::new(String::from("auth = token123")));
assert_eq!(redacted.as_str(), "auth=***REDACTED***");
let untouched = redact_argument(&CommandArg::new(String::from("path=/var/secrets")));
assert_eq!(untouched.as_str(), "path=/var/secrets");
let positional = redact_argument(&CommandArg::new(String::from("secret")));
assert_eq!(positional.as_str(), "secret");
}
}