#![forbid(unsafe_code)]
use std::borrow::Cow;
use std::sync::LazyLock;
use regex::Regex;
pub static SENSITIVE_HEADERS: phf::Set<&'static str> = phf::phf_set! {
"authorization",
"proxy-authorization",
"cookie",
"set-cookie",
"x-api-key",
"x-auth-token",
"x-amz-security-token",
"x-goog-api-key",
"x-anthropic-api-key",
"x-openai-api-key",
};
const REDACTED: &str = "[redacted]";
#[must_use]
pub fn string(input: &str) -> Cow<'_, str> {
if input.is_empty() {
return Cow::Borrowed(input);
}
if !fast_has_sensitive_anchor(input) {
return Cow::Borrowed(input);
}
let mut out = input.to_string();
apply_rules(&mut out);
if out == input {
Cow::Borrowed(input)
} else {
Cow::Owned(out)
}
}
pub fn string_into(input: &str, out: &mut String) {
out.clear();
if input.is_empty() {
return;
}
if !fast_has_sensitive_anchor(input) {
out.push_str(input);
return;
}
out.push_str(input);
apply_rules(out);
}
#[must_use]
pub fn is_sensitive_header(name: &str) -> bool {
let mut buf = [0u8; 128];
let bytes = name.as_bytes();
if bytes.len() > buf.len() {
return false;
}
for (i, &b) in bytes.iter().enumerate() {
buf[i] = b.to_ascii_lowercase();
}
let lower = std::str::from_utf8(&buf[..bytes.len()]).unwrap_or("");
SENSITIVE_HEADERS.contains(lower)
}
#[must_use]
pub fn redact_header_value(value: &str) -> String {
if value.is_empty() {
String::new()
} else {
REDACTED.to_string()
}
}
fn fast_has_sensitive_anchor(input: &str) -> bool {
input.contains('@') || input.contains('=') || input.contains('?')
|| input.contains('-') || input.contains('_')
|| input.contains('.') || input.contains("-----BEGIN ")
|| has_auth_scheme(input)
|| has_long_run(input)
}
fn has_long_run(input: &str) -> bool {
let mut run = 0usize;
for b in input.bytes() {
if b.is_ascii_alphanumeric()
|| b == b'+'
|| b == b'/'
|| b == b'='
|| b == b'_'
|| b == b'-'
{
run += 1;
if run >= 40 {
return true;
}
} else {
run = 0;
}
}
false
}
fn has_auth_scheme(input: &str) -> bool {
let lower_bytes = input.as_bytes();
for window in lower_bytes.windows(7) {
let w = window;
if eq_ignore_ascii_case(w, b"bearer ")
|| eq_ignore_ascii_case(&w[..6], b"basic ")
|| eq_ignore_ascii_case(&w[..6], b"token ")
{
return true;
}
}
false
}
fn eq_ignore_ascii_case(a: &[u8], b: &[u8]) -> bool {
if a.len() < b.len() {
return false;
}
a[..b.len()].iter().zip(b.iter()).all(|(x, y)| x.eq_ignore_ascii_case(y))
}
fn apply_rules(out: &mut String) {
replace_all(out, &RE_URL_USERINFO, |caps| format!("{}://{REDACTED}@", &caps[1]));
replace_all(out, &RE_AUTH_SCHEME, |caps| format!("{} {REDACTED}", &caps[1]));
replace_all(out, &RE_QUERY_SENSITIVE, |caps| format!("{}={REDACTED}", &caps[1]));
replace_all(out, &RE_PEM_BLOCK, |_caps| {
"-----BEGIN PRIVATE KEY-----\n[redacted]\n-----END PRIVATE KEY-----".to_string()
});
replace_all(out, &RE_NAMED_PREFIX, |caps| {
let matched = &caps[0];
if matched.len() >= 20 {
REDACTED.to_string()
} else {
matched.to_string()
}
});
replace_all(out, &RE_JWT, |caps| {
let matched = &caps[0];
if matched.len() >= 100 {
REDACTED.to_string()
} else {
matched.to_string()
}
});
replace_all(out, &RE_LONG_OPAQUE, |caps| format!("{}{REDACTED}{}", &caps[1], &caps[3]));
}
fn replace_all<F>(buf: &mut String, re: &Regex, mut f: F)
where
F: FnMut(®ex::Captures<'_>) -> String,
{
if !re.is_match(buf) {
return;
}
let mut out = String::with_capacity(buf.len());
let mut last = 0;
for caps in re.captures_iter(buf) {
let whole = caps.get(0).expect("captures always include group 0");
out.push_str(&buf[last..whole.start()]);
out.push_str(&f(&caps));
last = whole.end();
}
out.push_str(&buf[last..]);
*buf = out;
}
static RE_URL_USERINFO: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"([a-zA-Z][a-zA-Z0-9+.-]*)://[^:\s/?#]+:[^@\s]+@").expect("valid regex")
});
static RE_AUTH_SCHEME: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)\b(Bearer|Basic|Token)\s+[A-Za-z0-9_\-.+/=]+").expect("valid regex")
});
static RE_QUERY_SENSITIVE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?i)([?&]?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|signature|sig|auth|x[_-]?api[_-]?key))=[^&\s#]+"
)
.expect("valid regex")
});
static RE_NAMED_PREFIX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
\b(
sk-ant-[A-Za-z0-9_\-]+
| sk-[A-Za-z0-9_\-]+
| (?:ghp|gho|ghs|ghu)_[A-Za-z0-9]+
| glpat-[A-Za-z0-9_\-]+
| AIza[A-Za-z0-9_\-]+
| (?:AKIA|ASIA)[A-Z0-9]+
| xox[baprs]-[A-Za-z0-9\-]+
| SG\.[A-Za-z0-9_\-]{22,}\.[A-Za-z0-9_\-]{43,}
)
",
)
.expect("valid regex")
});
static RE_JWT: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+").expect("valid regex")
});
static RE_LONG_OPAQUE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(^|\s)([A-Za-z0-9+/=_\-]{40,})(\s|$)").expect("valid regex")
});
static RE_PEM_BLOCK: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?s)-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----")
.expect("valid regex")
});