use super::payload::Exception;
use crate::privacy::{filter_value, PrivacyFilter};
pub fn scrub_exception(exc: &mut Exception, filter: &PrivacyFilter) {
exc.value = scrub_string(&exc.value, filter);
for frame in exc.stacktrace.frames_mut() {
scrub_opt(&mut frame.function, filter);
scrub_opt(&mut frame.filename, filter);
scrub_opt(&mut frame.module, filter);
}
}
fn scrub_opt(field: &mut Option<String>, filter: &PrivacyFilter) {
if let Some(value) = field {
*value = scrub_string(value, filter);
}
}
fn scrub_string(s: &str, filter: &PrivacyFilter) -> String {
let mut v = serde_json::Value::String(s.to_string());
filter_value(&mut v, filter);
match v {
serde_json::Value::String(out) => out,
_ => s.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::telemetry::crash::payload::{Frame, Mechanism, Stacktrace};
fn filter() -> PrivacyFilter {
PrivacyFilter::new(&[])
}
fn exception(message: &str, frames: Vec<Frame>) -> Exception {
Exception {
exception_type: "panic".into(),
value: message.to_string(),
mechanism: Some(Mechanism::panic()),
thread_id: None,
stacktrace: Stacktrace::Raw { frames },
}
}
#[test]
fn a_credential_in_the_panic_message_is_redacted() {
let fake_aws = format!("{}{}", "AKIA", "1234567890ABCDEF"); let mut exc = exception(&format!("boom: {fake_aws} leaked"), vec![]);
scrub_exception(&mut exc, &filter());
assert!(
exc.value.contains("[AWS_KEY:AKIA***]"),
"got: {}",
exc.value
);
assert!(!exc.value.contains(&fake_aws));
}
#[test]
fn a_bearer_token_in_the_panic_message_is_redacted() {
let mut exc = exception(
"auth failed for Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig",
vec![],
);
scrub_exception(&mut exc, &filter());
assert!(
!exc.value.contains("eyJhbGciOiJIUzI1NiJ9.payload.sig"),
"got: {}",
exc.value
);
}
#[test]
fn frame_filename_function_and_module_are_all_scrubbed() {
let mut frame = Frame::address_only(Some("0x1000".into()), Some("0x1000".into()));
frame.filename = Some("/tmp/ghp_abcdefghijklmnopqrstuvwxyz0123456789/main.rs".into());
frame.function = Some("boom_ghp_abcdefghijklmnopqrstuvwxyz0123456789".into());
frame.module = Some("ghp_abcdefghijklmnopqrstuvwxyz0123456789".into());
let mut exc = exception("boom", vec![frame]);
scrub_exception(&mut exc, &filter());
let scrubbed = &exc.stacktrace.frames()[0];
for (label, value) in [
("filename", scrubbed.filename.as_deref()),
("function", scrubbed.function.as_deref()),
("module", scrubbed.module.as_deref()),
] {
let value = value.unwrap_or_default();
assert!(
!value.contains("ghp_abcdefghijklmnopqrstuvwxyz0123456789"),
"{label} still carries the token: {value}"
);
}
}
#[test]
fn addresses_are_never_rewritten() {
let mut frame =
Frame::address_only(Some("0x7f3a9c041b2d".into()), Some("0x7f3a9c000000".into()));
frame.filename = Some("/home/alice/src/main.rs".into());
let mut exc = exception("boom", vec![frame]);
scrub_exception(&mut exc, &filter());
let scrubbed = &exc.stacktrace.frames()[0];
assert_eq!(scrubbed.instruction_addr.as_deref(), Some("0x7f3a9c041b2d"));
assert_eq!(scrubbed.image_addr.as_deref(), Some("0x7f3a9c000000"));
}
#[test]
fn an_ordinary_panic_survives_unchanged() {
let mut exc = exception("ordinary panic with no secrets", vec![]);
scrub_exception(&mut exc, &filter());
assert_eq!(exc.value, "ordinary panic with no secrets");
}
#[test]
fn scrubbing_never_empties_the_frame_list() {
let frames = vec![
Frame::address_only(Some("0x1".into()), None),
Frame::address_only(Some("0x2".into()), None),
];
let mut exc = exception("boom", frames);
scrub_exception(&mut exc, &filter());
assert_eq!(exc.stacktrace.frames().len(), 2);
}
}