use std::io::{self, BufRead, BufWriter, Read, Write};
use std::process::ExitCode;
use leakguard::{Kind, Mask, Redactor};
const HELP: &str = "\
leakguard -- redact secrets & PII from text
USAGE:
leakguard [OPTIONS] [FILES...]
Reads from the given files (or stdin if none) and writes redacted text to
stdout, line by line.
OPTIONS:
--mask <MODE> label (default) | fixed | char | partial | hash
--fixed <STR> replacement string for --mask fixed (default: [REDACTED])
--char <C> fill char for char/partial masks (default: *)
--keep <N> characters to keep for --mask partial (default: 4)
--only <LIST> comma-separated kinds to detect (e.g. email,ipv4,jwt)
--check don't print; exit 1 if any sensitive data is found
-h, --help print this help
-V, --version print version
KINDS:
email, credit_card, ipv4, ipv6, jwt, us_ssn, mac, aws_access_key,
url_credentials, phone, github_token, slack_token, stripe_key,
google_api_key, openai_key, private_key, iban
";
fn parse_kind(s: &str) -> Option<Kind> {
Some(match s.trim().to_ascii_lowercase().as_str() {
"email" => Kind::Email,
"credit_card" | "creditcard" | "cc" => Kind::CreditCard,
"ipv4" | "ip" => Kind::IpV4,
"ipv6" => Kind::IpV6,
"jwt" => Kind::Jwt,
"us_ssn" | "ssn" => Kind::UsSsn,
"mac" => Kind::MacAddress,
"aws_access_key" | "aws" => Kind::AwsAccessKey,
"url_credentials" | "url" => Kind::UrlCredentials,
"phone" => Kind::PhoneNumber,
"github_token" | "github" | "gh" => Kind::GitHubToken,
"slack_token" | "slack" => Kind::SlackToken,
"stripe_key" | "stripe" => Kind::StripeKey,
"google_api_key" | "google" | "gcp" => Kind::GoogleApiKey,
"openai_key" | "openai" => Kind::OpenAiKey,
"private_key" | "pem" => Kind::PrivateKey,
"iban" => Kind::Iban,
_ => return None,
})
}
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(e) => {
eprintln!("leakguard: {e}");
ExitCode::from(2)
}
}
}
fn run() -> io::Result<ExitCode> {
let mut args = std::env::args().skip(1).peekable();
let mut mask_mode = String::from("label");
let mut fixed = String::from("[REDACTED]");
let mut fill = '*';
let mut keep = 4usize;
let mut only: Vec<Kind> = Vec::new();
let mut check = false;
let mut files: Vec<String> = Vec::new();
while let Some(arg) = args.next() {
match arg.as_str() {
"-h" | "--help" => {
print!("{HELP}");
return Ok(ExitCode::SUCCESS);
}
"-V" | "--version" => {
println!("leakguard {}", env!("CARGO_PKG_VERSION"));
return Ok(ExitCode::SUCCESS);
}
"--mask" => mask_mode = next_val(&mut args, "--mask")?,
"--fixed" => fixed = next_val(&mut args, "--fixed")?,
"--char" => {
fill = next_val(&mut args, "--char")?.chars().next().unwrap_or('*');
}
"--keep" => {
keep = next_val(&mut args, "--keep")?.parse().map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "--keep needs a number")
})?;
}
"--only" => {
for part in next_val(&mut args, "--only")?.split(',') {
match parse_kind(part) {
Some(k) => only.push(k),
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unknown kind: {part}"),
))
}
}
}
}
"--check" => check = true,
other if other.starts_with('-') && other != "-" => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unknown option: {other} (try --help)"),
));
}
other => files.push(other.to_string()),
}
}
let mask = match mask_mode.as_str() {
"label" => Mask::Label,
"fixed" => Mask::Fixed(Box::leak(fixed.into_boxed_str())),
"char" => Mask::Char(fill),
"partial" => Mask::Partial {
keep_last: keep,
ch: fill,
},
"hash" => Mask::Hash,
other => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unknown mask: {other}"),
))
}
};
let redactor = if only.is_empty() {
Redactor::new()
} else {
Redactor::only(&only)
}
.mask(mask);
let stdout = io::stdout();
let mut out = BufWriter::new(stdout.lock());
let mut found_any = false;
let mut process = |reader: Box<dyn BufRead>| -> io::Result<()> {
for line in reader.lines() {
let line = line?;
if check {
if redactor.is_dirty(&line) {
found_any = true;
}
} else {
writeln!(out, "{}", redactor.clean(&line))?;
}
}
Ok(())
};
if files.is_empty() {
let stdin = io::stdin();
process(Box::new(stdin.lock()))?;
} else {
for f in &files {
if f == "-" {
let stdin = io::stdin();
let mut buf = String::new();
stdin.lock().read_to_string(&mut buf)?;
process(Box::new(io::Cursor::new(buf)))?;
} else {
let file = std::fs::File::open(f)?;
process(Box::new(io::BufReader::new(file)))?;
}
}
}
out.flush()?;
if check && found_any {
return Ok(ExitCode::from(1));
}
Ok(ExitCode::SUCCESS)
}
fn next_val<I: Iterator<Item = String>>(
args: &mut std::iter::Peekable<I>,
flag: &str,
) -> io::Result<String> {
args.next()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, format!("{flag} needs a value")))
}