use regex::Regex;
use std::sync::atomic::{AtomicU64, Ordering};
pub(crate) static REDACTION_COMPILE_FAILURES: AtomicU64 = AtomicU64::new(0);
const REDACTION_FAILED_PLACEHOLDER: &str =
"<git error suppressed: a credential-redaction pattern failed to compile; \
raw stderr withheld to avoid leaking secrets>";
pub(crate) fn sanitize_git_error_message(stderr: &str) -> String {
use std::sync::OnceLock;
static URL_CRED_RE: OnceLock<Result<Regex, String>> = OnceLock::new();
static AUTH_HEADER_RE: OnceLock<Result<Regex, String>> = OnceLock::new();
static TOKEN_RE: OnceLock<Result<Regex, String>> = OnceLock::new();
let url_cred =
URL_CRED_RE.get_or_init(|| compile_redaction_regex(r"([a-z][a-z0-9+\-.]*://)([^/\s]+)@"));
let auth_header = AUTH_HEADER_RE
.get_or_init(|| compile_redaction_regex(r"(?i)(authorization:\s*(?:basic|bearer)\s+)\S+"));
let token_pat = TOKEN_RE.get_or_init(|| {
compile_redaction_regex(r"(?:ghp_[A-Za-z0-9]{36}|gho_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}|glpat-[A-Za-z0-9_-]{20,64}|glrt-[A-Za-z0-9_-]{16,64}|gldt-[A-Za-z0-9_-]{16,64}|glcbt-[A-Za-z0-9_-]{16,64}|xoxb-[A-Za-z0-9-]{24,}|xoxp-[A-Za-z0-9-]{24,}|sk-proj-[A-Za-z0-9_-]{24,}|sk_live_[A-Za-z0-9]{24,}|sk_test_[A-Za-z0-9]{24,}|AKIA[0-9A-Z]{16})")
});
let url_cred = regex_result_as_option(url_cred);
let auth_header = regex_result_as_option(auth_header);
let token_pat = regex_result_as_option(token_pat);
redact_with(stderr, url_cred, auth_header, token_pat)
}
fn compile_redaction_regex(pattern: &str) -> Result<Regex, String> {
Regex::new(pattern).map_err(|error| error.to_string())
}
fn regex_result_as_option(result: &Result<Regex, String>) -> Option<&Regex> {
match result {
Ok(regex) => Some(regex),
Err(_error) => None,
}
}
pub(crate) fn redact_with(
stderr: &str,
url_cred: Option<&Regex>,
auth_header: Option<&Regex>,
token_pat: Option<&Regex>,
) -> String {
let (Some(url_cred), Some(auth_header), Some(token_pat)) = (url_cred, auth_header, token_pat)
else {
REDACTION_COMPILE_FAILURES.fetch_add(1, Ordering::Relaxed);
eprintln!(
"keyhog: SECURITY - a git-error credential-redaction regex failed to \
compile; refusing to surface raw git stderr"
);
return REDACTION_FAILED_PLACEHOLDER.to_string();
};
let mut result = url_cred.replace_all(stderr, "${1}<redacted>@").into_owned();
result = auth_header
.replace_all(&result, "${1}<redacted>")
.into_owned();
result = token_pat
.replace_all(&result, "<redacted-token>")
.into_owned();
result.trim().to_string()
}
#[cfg(test)]
pub(crate) const REDACTION_FAILED_PLACEHOLDER_FOR_TEST: &str = REDACTION_FAILED_PLACEHOLDER;
#[cfg(test)]
#[path = "../../tests/unit/hosted_git_sanitize.rs"]
mod tests;