fix: redact grouped German IBAN (DE89 3704 …) as one IBAN, not partial+phone

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-07-07 20:22:12 +02:00
parent ffc9de6bc3
commit a0e713d752

View file

@ -227,11 +227,15 @@ fn anonymize(text: &str, custom_raw: &str) -> Result<(String, Vec<Redaction>), M
"IBAN", "IBAN",
// Match the structural shape, surrounded by word // Match the structural shape, surrounded by word
// boundaries so we don't slice into longer alnum // boundaries so we don't slice into longer alnum
// strings. // strings. The BBAN body allows an OPTIONAL single space
// before each character so the standard grouped spelling
// (`DE89 3704 0044 0532 0130 00`) is redacted, not just
// the contiguous form — otherwise the leading group leaks
// and the phone rule mislabels the rest.
Regex::new(r"(?x) Regex::new(r"(?x)
\b \b
[A-Z]{2}\d{2} [A-Z]{2}\d{2}
[A-Z0-9]{11,30} (?:\s?[A-Z0-9]){11,30}
\b \b
").map_err(re_err)?, ").map_err(re_err)?,
), ),
@ -434,6 +438,23 @@ mod tests {
assert_eq!(reds.iter().filter(|r| r.kind == "BIC").count(), 1); assert_eq!(reds.iter().filter(|r| r.kind == "BIC").count(), 1);
} }
#[test]
fn redacts_grouped_iban_without_phone_misclassification() {
// The standard German spelling groups the IBAN in 4-char
// blocks with spaces. It must redact as a single IBAN — not
// leak the leading group while the phone rule eats the rest.
let (out, reds) =
anonymize("Zahlung an DE89 3704 0044 0532 0130 00 heute", "").unwrap();
assert!(
out.contains("\u{27E6}IBAN_1\u{27E7}"),
"grouped IBAN not redacted: {out}"
);
assert!(!out.contains("PHONE"), "grouped IBAN mislabeled as phone: {out}");
assert!(!out.contains("DE89"), "leading IBAN group leaked: {out}");
assert_eq!(reds.iter().filter(|r| r.kind == "IBAN").count(), 1);
assert_eq!(reds.iter().filter(|r| r.kind == "PHONE").count(), 0);
}
#[test] #[test]
fn redacts_phone_numbers() { fn redacts_phone_numbers() {
let cases = [ let cases = [