use serde_json::Value;
pub const REDACTED: &str = "[redacted]";
pub const CREDENTIAL_WORDS: &[&str] = &[
"TOKEN",
"SECRET",
"PASSWORD",
"PASSWD",
"CREDENTIAL",
"APIKEY",
"API_KEY",
"PRIVATE_KEY",
];
pub const CREDENTIAL_PREFIXES: &[&str] = &[
"ghp_",
"gho_",
"ghs_",
"ghu_",
"ghr_",
"github_pat_",
"AKIA",
];
const MIN_SECRET_LEN: usize = 8;
const MIN_PREFIXED_TAIL: usize = 8;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Redactor {
secrets: Vec<String>,
}
impl Redactor {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_env() -> Self {
let mut redactor = Self::new();
for (name, value) in std::env::vars() {
let upper = name.to_ascii_uppercase();
if CREDENTIAL_WORDS.iter().any(|word| upper.contains(word)) {
redactor.add_secret(value);
}
}
redactor
}
#[must_use]
pub fn with_secret(mut self, secret: impl Into<String>) -> Self {
self.add_secret(secret.into());
self
}
fn add_secret(&mut self, secret: String) {
if secret.len() >= MIN_SECRET_LEN && !self.secrets.contains(&secret) {
self.secrets.push(secret);
}
}
#[must_use]
pub fn redact(&self, text: &str) -> String {
let mut clean = text.to_owned();
for secret in &self.secrets {
clean = clean.replace(secret, REDACTED);
}
clean
.split_inclusive(|c: char| c.is_whitespace())
.map(|word| {
let trimmed = word.trim_end();
let spacing = &word[trimmed.len()..];
let bare = trimmed.trim_end_matches(['"', '\'', ',', ';', ')']);
let punctuation = &trimmed[bare.len()..];
if CREDENTIAL_PREFIXES.iter().any(|prefix| {
bare.starts_with(prefix) && bare.len() >= prefix.len() + MIN_PREFIXED_TAIL
}) {
format!("{REDACTED}{punctuation}{spacing}")
} else {
word.to_owned()
}
})
.collect()
}
#[must_use]
pub fn redact_value(&self, value: Value) -> Value {
match value {
Value::String(text) => Value::String(self.redact(&text)),
Value::Array(items) => Value::Array(
items
.into_iter()
.map(|item| self.redact_value(item))
.collect(),
),
Value::Object(fields) => Value::Object(
fields
.into_iter()
.map(|(key, value)| (key, self.redact_value(value)))
.collect(),
),
other => other,
}
}
}