klieo_pii_patterns/
lib.rs1#![deny(missing_docs)]
2#![deny(rust_2018_idioms)]
3#![deny(rustdoc::broken_intra_doc_links)]
4
5pub type PatternEntry = (&'static str, &'static str);
36
37pub const PATTERNS: &[PatternEntry] = &[
40 ("BIC", r"(?-i:\b[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?\b)"),
41 ("IBAN", r"\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b"),
42 (
43 "EMAIL",
44 r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b",
45 ),
46 (
47 "JWT",
48 r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b",
49 ),
50 ("IPV4", r"\b(?:\d{1,3}\.){3}\d{1,3}\b"),
51 (
52 "PEM",
53 r"-----BEGIN (?:[A-Z ]+)-----[\s\S]+?-----END (?:[A-Z ]+)-----",
54 ),
55 (
60 "TAXID_DE",
61 r"(?:steuer[-_ ]?id|steueridentifikationsnummer|tax[-_ ]?id)[:\s=]*\d{11}",
62 ),
63];
64
65pub mod labels {
69 pub const IBAN: &str = "IBAN";
71 pub const EMAIL: &str = "EMAIL";
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use regex::RegexBuilder;
79
80 #[test]
81 fn every_pattern_compiles_case_insensitively() {
82 for (label, pat) in PATTERNS {
83 RegexBuilder::new(pat)
84 .case_insensitive(true)
85 .build()
86 .unwrap_or_else(|e| panic!("{label} pattern failed to compile: {e}"));
87 }
88 }
89
90 #[test]
91 fn bic_precedes_iban_in_order() {
92 let positions: std::collections::HashMap<&str, usize> = PATTERNS
93 .iter()
94 .enumerate()
95 .map(|(i, (label, _))| (*label, i))
96 .collect();
97 assert!(positions["BIC"] < positions["IBAN"]);
98 }
99
100 #[test]
101 fn taxid_de_requires_prefix() {
102 let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "TAXID_DE").unwrap().1)
104 .case_insensitive(true)
105 .build()
106 .unwrap();
107 assert!(
108 !re.is_match("12345678901"),
109 "bare 11-digit string must not match"
110 );
111 assert!(re.is_match("steuer-id: 12345678901"));
112 assert!(re.is_match("Steueridentifikationsnummer: 12345678901"));
113 }
114
115 #[test]
116 fn iban_pattern_rejects_near_misses() {
117 let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "IBAN").unwrap().1)
118 .case_insensitive(true)
119 .build()
120 .unwrap();
121 assert!(
123 !re.is_match("DE12"),
124 "short string must not match IBAN pattern"
125 );
126 assert!(
128 !re.is_match("12345678901234"),
129 "bare numeric string must not match IBAN"
130 );
131 assert!(
133 re.is_match("DE89370400440532013000"),
134 "valid IBAN must match"
135 );
136 }
137
138 #[test]
139 fn email_pattern_rejects_near_misses() {
140 let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "EMAIL").unwrap().1)
141 .case_insensitive(true)
142 .build()
143 .unwrap();
144 assert!(
145 !re.is_match("not-an-email"),
146 "bare word must not match EMAIL"
147 );
148 assert!(
149 !re.is_match("missing-at-sign.com"),
150 "missing @ must not match EMAIL"
151 );
152 assert!(re.is_match("user@example.com"), "valid email must match");
154 }
155
156 #[test]
157 fn bic_pattern_rejects_near_misses() {
158 let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "BIC").unwrap().1)
159 .case_insensitive(true)
160 .build()
161 .unwrap();
162 assert!(!re.is_match("ABCD12"), "5-char string must not match BIC");
164 assert!(re.is_match("DEUTDEDB"), "valid 8-char BIC must match");
166 }
167
168 #[test]
169 fn bic_stays_case_sensitive_under_insensitive_builder() {
170 let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "BIC").unwrap().1)
175 .case_insensitive(true)
176 .build()
177 .unwrap();
178 for word in ["individuals", "potentially", "decision", "employee"] {
179 assert!(
180 !re.is_match(word),
181 "ordinary word `{word}` must not match BIC"
182 );
183 }
184 assert!(re.is_match("DEUTDEFF"), "uppercase BIC must still match");
185 assert!(
186 !re.is_match("deutdeff"),
187 "lowercase BIC-shaped token must not match"
188 );
189 }
190
191 #[test]
192 fn jwt_pattern_rejects_near_misses() {
193 let re = RegexBuilder::new(PATTERNS.iter().find(|(l, _)| *l == "JWT").unwrap().1)
194 .case_insensitive(true)
195 .build()
196 .unwrap();
197 assert!(
199 !re.is_match("header.payload.sig"),
200 "non-base64url JWT prefix must not match"
201 );
202 assert!(
204 !re.is_match("eyJhbGc.eyJzdWI"),
205 "two-segment token must not match JWT pattern"
206 );
207 assert!(
209 re.is_match("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.abc123"),
210 "valid three-segment JWT must match"
211 );
212 }
213}