Phase 2b of the DLP and mail flow rules spec: every identifier in the §2.3 catalog, each implemented from its issuer's published rules and tested against published examples. US (SSN, ITIN, EIN, ABA routing, driver's licenses, MBI, NPI, DEA), UK (NI number, NHS number, UTR), Canada (SIN), Australia (TFN, Medicare), the EU (Germany's tax ID and ID card, France's NIR, Spain's DNI/NIE, Italy's codice fiscale, the Dutch BSN, Belgium's national number, Poland's PESEL, Sweden's personnummer, Denmark's CPR, Finland's HETU, Ireland's PPS, Portugal's NIF, Austria's SVNR), Norway, Switzerland, India (Aadhaar, PAN), China, Japan, Singapore, South Korea, Brazil (CPF, CNPJ), Mexico (CURP) and South Africa. 49 detectors in all, plus seven templates named for what they find. An identifier that is only digits and whose check about one random number in ten passes counts alone only in its written form (536-22-1234, 943 476 5919) and as bare digits only beside a word; ABA routing numbers and NPIs always need one. Spec §2.3 records this. A test runs every detector over an ordinary business email (order, invoice and tracking numbers, dates, amounts, an address) and requires nothing to fire but the contact detectors. 47 unit tests.
67 lines
1.8 KiB
Rust
67 lines
1.8 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
//! Canadian identifiers (§2.3): the Social Insurance Number.
|
|
|
|
use super::{Detector, Findings, Region, Strength, checks, word_near};
|
|
use regex::Regex;
|
|
use std::sync::LazyLock;
|
|
|
|
pub static DETECTORS: &[Detector] = &[Detector::new(
|
|
"ca-sin",
|
|
"Canadian Social Insurance Number",
|
|
Region::Canada,
|
|
Strength::Checked,
|
|
sin,
|
|
)];
|
|
|
|
/// `NNN NNN NNN` or `NNN-NNN-NNN` stands alone; nine bare digits need a word.
|
|
static SIN: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r"\b(\d{3})([ -]?)(\d{3})([ -]?)(\d{3})\b").expect("detector pattern")
|
|
});
|
|
|
|
const SIN_WORDS: &[&str] = &[
|
|
"sin",
|
|
"social insurance",
|
|
"nas",
|
|
"numéro d'assurance sociale",
|
|
"assurance sociale",
|
|
];
|
|
|
|
fn sin(text: &str, findings: &mut Findings) {
|
|
for c in SIN.captures_iter(text) {
|
|
let whole = c.get(0).unwrap();
|
|
let n = format!("{}{}{}", &c[1], &c[3], &c[5]);
|
|
let written = !c[2].is_empty() && c[2] == c[4];
|
|
// 0 and 8 are never issued as a first digit
|
|
if !n.starts_with(['0', '8'])
|
|
&& checks::luhn(&n)
|
|
&& (written || word_near(text, whole.start(), whole.end(), SIN_WORDS))
|
|
{
|
|
findings.insert(n);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::mailflow::detectors::by_id;
|
|
|
|
fn count(text: &str) -> usize {
|
|
by_id("ca-sin").unwrap().count(text)
|
|
}
|
|
|
|
#[test]
|
|
fn social_insurance_numbers() {
|
|
assert_eq!(count("130 692 544 and 193-456-787"), 2);
|
|
assert_eq!(count("130 692 545"), 0);
|
|
// The government's printed example starts with 0, never issued
|
|
assert_eq!(count("046 454 286"), 0);
|
|
assert_eq!(count("order 130692544"), 0);
|
|
assert_eq!(count("SIN: 130692544"), 1);
|
|
}
|
|
}
|