pub(crate) fn mask_secrets(value: &str) -> String {
let mut masked = mask_proxy_secrets(value);
for key in [
"token",
"password",
"authorization",
"api_key",
"apikey",
"secret",
"credential",
] {
masked = mask_key_value(&masked, key);
}
let mut lines = Vec::new();
let mut sensitive_variable = false;
for line in masked.lines() {
let trimmed = line.trim_start();
let upper = trimmed.to_ascii_uppercase();
if trimmed.starts_with("name")
&& ["TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "API_KEY"]
.iter()
.any(|marker| upper.contains(marker))
{
sensitive_variable = true;
lines.push(line.to_string());
} else if sensitive_variable && trimmed.starts_with("value") {
if let Some(separator) = line.find('=') {
lines.push(format!("{}= \"***\"", &line[..separator]));
} else {
lines.push(line.to_string());
}
sensitive_variable = false;
} else {
lines.push(line.to_string());
}
}
lines.join("\n")
}
fn mask_key_value(value: &str, key: &str) -> String {
let lower = value.to_ascii_lowercase();
let Some(start) = lower.find(key) else {
return value.to_string();
};
let suffix = &value[start + key.len()..];
let Some(separator) = suffix.find(['=', ':']) else {
return value.to_string();
};
let value_start = start + key.len() + separator + 1;
let end = value[value_start..]
.find([',', ' ', '\n', '\r'])
.map_or(value.len(), |offset| value_start + offset);
format!("{}***{}", &value[..value_start], &value[end..])
}
fn mask_proxy_secrets(value: &str) -> String {
let Some(scheme_end) = value.find("://") else {
return value.to_string();
};
let auth_start = scheme_end + 3;
let Some(at_offset) = value[auth_start..].find('@') else {
return value.to_string();
};
let at = auth_start + at_offset;
let Some(host_end_offset) = value[auth_start..].find(['/', '?', '#', ' ', '"', '\'']) else {
return format!("{}***@{}", &value[..auth_start], &value[at + 1..]);
};
let host_end = auth_start + host_end_offset;
if at > host_end {
return value.to_string();
}
format!("{}***@{}", &value[..auth_start], &value[at + 1..])
}
#[cfg(test)]
mod tests {
use crate::redaction::{mask_proxy_secrets, mask_secrets};
#[test]
fn masks_proxy_credentials() {
assert_eq!(
mask_proxy_secrets("http://user:pass@127.0.0.1:7890"),
"http://***@127.0.0.1:7890"
);
assert_eq!(
mask_proxy_secrets("proxy = \"http://user:pass@example.com:8080\""),
"proxy = \"http://***@example.com:8080\""
);
}
#[test]
fn masks_common_secret_keys() {
assert_eq!(mask_secrets("token=secret next"), "token=*** next");
assert_eq!(
mask_secrets("Authorization: Bearer-secret"),
"Authorization:*** Bearer-secret"
);
assert_eq!(
mask_secrets("https://user:pass@example.com/path"),
"https://***@example.com/path"
);
}
}