scrubber_rb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.cargo/config.toml +21 -0
- data/CHANGELOG.md +59 -0
- data/Cargo.lock +626 -0
- data/Cargo.toml +10 -0
- data/LICENSE +21 -0
- data/README.md +298 -0
- data/SECURITY.md +54 -0
- data/ext/scrubber_rb/Cargo.toml +41 -0
- data/ext/scrubber_rb/build.rs +6 -0
- data/ext/scrubber_rb/extconf.rb +6 -0
- data/ext/scrubber_rb/src/detectors/checksum.rs +222 -0
- data/ext/scrubber_rb/src/detectors/mod.rs +636 -0
- data/ext/scrubber_rb/src/detectors/upi.rs +138 -0
- data/ext/scrubber_rb/src/detectors/validate.rs +339 -0
- data/ext/scrubber_rb/src/engine.rs +716 -0
- data/ext/scrubber_rb/src/lib.rs +247 -0
- data/ext/scrubber_rb/src/nogvl.rs +84 -0
- data/ext/scrubber_rb/src/offsets.rs +118 -0
- data/ext/scrubber_rb/src/pattern.rs +319 -0
- data/ext/scrubber_rb/src/replace.rs +238 -0
- data/lib/scrubber/instance.rb +223 -0
- data/lib/scrubber/llm_guard.rb +79 -0
- data/lib/scrubber/log_formatter.rb +40 -0
- data/lib/scrubber/match.rb +27 -0
- data/lib/scrubber/middleware.rb +124 -0
- data/lib/scrubber/version.rb +5 -0
- data/lib/scrubber_rb.rb +109 -0
- data/sig/scrubber_rb.rbs +150 -0
- metadata +96 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//! Known UPI payment service provider (PSP) handles.
|
|
2
|
+
//!
|
|
3
|
+
//! A UPI VPA looks like `name@psp`. Matching `\w+@\w+` alone would redact half
|
|
4
|
+
//! of every log line, so we require the suffix to be a real PSP handle. This
|
|
5
|
+
//! list is deliberately a plain constant: adding a handle is a one-line PR.
|
|
6
|
+
//!
|
|
7
|
+
//! Source: NPCI's published list of live UPI members plus the handles used by
|
|
8
|
+
//! the major third-party apps (GPay/PhonePe/Paytm/Amazon Pay/CRED).
|
|
9
|
+
|
|
10
|
+
pub const PSP_HANDLES: &[&str] = &[
|
|
11
|
+
// Third-party apps
|
|
12
|
+
"ybl", // PhonePe (Yes Bank)
|
|
13
|
+
"ibl", // PhonePe (ICICI)
|
|
14
|
+
"axl", // PhonePe (Axis)
|
|
15
|
+
"okaxis", // Google Pay
|
|
16
|
+
"okhdfcbank", // Google Pay
|
|
17
|
+
"okicici", // Google Pay
|
|
18
|
+
"oksbi", // Google Pay
|
|
19
|
+
"paytm", // Paytm
|
|
20
|
+
"ptaxis", // Paytm
|
|
21
|
+
"ptyes", // Paytm
|
|
22
|
+
"ptsbi", // Paytm
|
|
23
|
+
"pthdfc", // Paytm
|
|
24
|
+
"apl", // Amazon Pay
|
|
25
|
+
"yapl", // Amazon Pay
|
|
26
|
+
"rapl", // Amazon Pay
|
|
27
|
+
"abfspay", // Aditya Birla
|
|
28
|
+
"freecharge", // Freecharge
|
|
29
|
+
"jupiteraxis", // Jupiter
|
|
30
|
+
"naviaxis", // Navi
|
|
31
|
+
"fam", // FamPay
|
|
32
|
+
"goaxb", // Google/Axis
|
|
33
|
+
"superyes", // Super.money
|
|
34
|
+
"slice", // Slice
|
|
35
|
+
"timecosmos", // CRED
|
|
36
|
+
"waaxis", // WhatsApp Pay
|
|
37
|
+
"wahdfcbank", // WhatsApp Pay
|
|
38
|
+
"waicici", // WhatsApp Pay
|
|
39
|
+
"wasbi", // WhatsApp Pay
|
|
40
|
+
"mbk", // MobiKwik
|
|
41
|
+
"ikwik", // MobiKwik
|
|
42
|
+
"yesg", // Groww
|
|
43
|
+
// Banks
|
|
44
|
+
"sbi",
|
|
45
|
+
"hdfcbank",
|
|
46
|
+
"icici",
|
|
47
|
+
"axisbank",
|
|
48
|
+
"kotak",
|
|
49
|
+
"kmb",
|
|
50
|
+
"kmbl",
|
|
51
|
+
"yesbank",
|
|
52
|
+
"yesbankltd",
|
|
53
|
+
"idfcbank",
|
|
54
|
+
"idfcfirst",
|
|
55
|
+
"indus",
|
|
56
|
+
"indianbank",
|
|
57
|
+
"iob",
|
|
58
|
+
"federal",
|
|
59
|
+
"fbl",
|
|
60
|
+
"pnb",
|
|
61
|
+
"uboi",
|
|
62
|
+
"unionbank",
|
|
63
|
+
"unionbankofindia",
|
|
64
|
+
"ucobank",
|
|
65
|
+
"barodampay",
|
|
66
|
+
"barodapay",
|
|
67
|
+
"cnrb",
|
|
68
|
+
"cbin",
|
|
69
|
+
"citi",
|
|
70
|
+
"citigold",
|
|
71
|
+
"dbs",
|
|
72
|
+
"dlb",
|
|
73
|
+
"equitas",
|
|
74
|
+
"hsbc",
|
|
75
|
+
"idbi",
|
|
76
|
+
"jkb",
|
|
77
|
+
"jsb",
|
|
78
|
+
"kbl",
|
|
79
|
+
"karb",
|
|
80
|
+
"lvb",
|
|
81
|
+
"mahb",
|
|
82
|
+
"psb",
|
|
83
|
+
"rbl",
|
|
84
|
+
"sc",
|
|
85
|
+
"scb",
|
|
86
|
+
"scbl",
|
|
87
|
+
"sib",
|
|
88
|
+
"srcb",
|
|
89
|
+
"tjsb",
|
|
90
|
+
"utbi",
|
|
91
|
+
"uco",
|
|
92
|
+
"aubank",
|
|
93
|
+
"bandhan",
|
|
94
|
+
"dcb",
|
|
95
|
+
"finobank",
|
|
96
|
+
"airtel",
|
|
97
|
+
"aubankltd",
|
|
98
|
+
"postbank",
|
|
99
|
+
"allbank",
|
|
100
|
+
"andb",
|
|
101
|
+
"upi",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/// True if `suffix` (the part after `@`, case-insensitive) is a known PSP.
|
|
105
|
+
pub fn is_psp(suffix: &str) -> bool {
|
|
106
|
+
let lowered = suffix.to_ascii_lowercase();
|
|
107
|
+
PSP_HANDLES.iter().any(|h| *h == lowered)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#[cfg(test)]
|
|
111
|
+
mod tests {
|
|
112
|
+
use super::*;
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn recognises_common_handles() {
|
|
116
|
+
assert!(is_psp("ybl"));
|
|
117
|
+
assert!(is_psp("okaxis"));
|
|
118
|
+
assert!(is_psp("PAYTM"));
|
|
119
|
+
assert!(is_psp("OkHdfcBank"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[test]
|
|
123
|
+
fn rejects_lookalikes() {
|
|
124
|
+
assert!(!is_psp("gmail"));
|
|
125
|
+
assert!(!is_psp("example"));
|
|
126
|
+
assert!(!is_psp("yblx"));
|
|
127
|
+
assert!(!is_psp(""));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[test]
|
|
131
|
+
fn handle_list_is_lowercase_and_unique() {
|
|
132
|
+
let mut seen = std::collections::HashSet::new();
|
|
133
|
+
for h in PSP_HANDLES {
|
|
134
|
+
assert_eq!(*h, &h.to_ascii_lowercase(), "{h} must be lowercase");
|
|
135
|
+
assert!(seen.insert(*h), "{h} is duplicated");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
//! Per-detector validators.
|
|
2
|
+
//!
|
|
3
|
+
//! A validator runs *after* a regex matches and gets the full surrounding text,
|
|
4
|
+
//! so it can do things the (deliberately) backtracking-free regex engine
|
|
5
|
+
//! cannot: run a checksum, check the character just past the match, or parse
|
|
6
|
+
//! the candidate with a real parser.
|
|
7
|
+
|
|
8
|
+
use std::net::Ipv6Addr;
|
|
9
|
+
use std::str::FromStr;
|
|
10
|
+
|
|
11
|
+
use super::checksum::{iban_mod97, luhn, verhoeff};
|
|
12
|
+
use super::upi;
|
|
13
|
+
|
|
14
|
+
/// What a validator sees: the matched slice plus its position in the haystack.
|
|
15
|
+
pub struct Candidate<'a> {
|
|
16
|
+
pub text: &'a str,
|
|
17
|
+
pub matched: &'a str,
|
|
18
|
+
pub start: usize,
|
|
19
|
+
pub end: usize,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl Candidate<'_> {
|
|
23
|
+
/// The byte immediately before the match, if any.
|
|
24
|
+
pub fn prev_byte(&self) -> Option<u8> {
|
|
25
|
+
self.text
|
|
26
|
+
.as_bytes()
|
|
27
|
+
.get(self.start.wrapping_sub(1))
|
|
28
|
+
.copied()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// The byte immediately after the match, if any.
|
|
32
|
+
pub fn next_byte(&self) -> Option<u8> {
|
|
33
|
+
self.text.as_bytes().get(self.end).copied()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn digits(&self) -> Vec<u8> {
|
|
37
|
+
self.matched
|
|
38
|
+
.bytes()
|
|
39
|
+
.filter(u8::is_ascii_digit)
|
|
40
|
+
.map(|b| b - b'0')
|
|
41
|
+
.collect()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub type Validator = fn(&Candidate) -> bool;
|
|
46
|
+
|
|
47
|
+
/// Credit card: 13-19 digits that pass Luhn.
|
|
48
|
+
pub fn credit_card(c: &Candidate) -> bool {
|
|
49
|
+
let digits = c.digits();
|
|
50
|
+
(13..=19).contains(&digits.len()) && luhn(&digits)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Aadhaar: exactly 12 digits, first digit 2-9, passing Verhoeff.
|
|
54
|
+
pub fn aadhaar(c: &Candidate) -> bool {
|
|
55
|
+
let digits = c.digits();
|
|
56
|
+
digits.len() == 12 && digits[0] >= 2 && verhoeff(&digits)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// IBAN: registered country code, then ISO 7064 mod-97-10.
|
|
60
|
+
pub fn iban(c: &Candidate) -> bool {
|
|
61
|
+
let compact: Vec<u8> = c
|
|
62
|
+
.matched
|
|
63
|
+
.bytes()
|
|
64
|
+
.filter(|b| b.is_ascii_alphanumeric())
|
|
65
|
+
.map(|b| b.to_ascii_uppercase())
|
|
66
|
+
.collect();
|
|
67
|
+
if compact.len() < 15 {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
let country = std::str::from_utf8(&compact[..2]).unwrap_or("");
|
|
71
|
+
if !IBAN_COUNTRIES.contains(&country) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
iban_mod97(&compact)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// US SSN, rejecting the ranges the SSA never issues.
|
|
78
|
+
pub fn ssn(c: &Candidate) -> bool {
|
|
79
|
+
let digits = c.digits();
|
|
80
|
+
if digits.len() != 9 {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
let area = u16::from(digits[0]) * 100 + u16::from(digits[1]) * 10 + u16::from(digits[2]);
|
|
84
|
+
let group = digits[3] * 10 + digits[4];
|
|
85
|
+
let serial = u16::from(digits[5]) * 1000
|
|
86
|
+
+ u16::from(digits[6]) * 100
|
|
87
|
+
+ u16::from(digits[7]) * 10
|
|
88
|
+
+ u16::from(digits[8]);
|
|
89
|
+
area != 0 && area != 666 && area < 900 && group != 0 && serial != 0
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// IPv4: four octets, each 0-255, not embedded in a longer dotted run.
|
|
93
|
+
pub fn ipv4(c: &Candidate) -> bool {
|
|
94
|
+
if matches!(c.prev_byte(), Some(b'.') | Some(b'-')) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if matches!(c.next_byte(), Some(b'.')) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
let mut octets = 0;
|
|
101
|
+
for part in c.matched.split('.') {
|
|
102
|
+
if part.is_empty() || part.len() > 3 {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
match part.parse::<u16>() {
|
|
106
|
+
Ok(v) if v <= 255 => octets += 1,
|
|
107
|
+
_ => return false,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
octets == 4
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// IPv6: hand the candidate to the standard-library parser and believe it.
|
|
114
|
+
pub fn ipv6(c: &Candidate) -> bool {
|
|
115
|
+
// The address has to be a standalone token. Without this, the `::C` in
|
|
116
|
+
// `PG::ConnectionBad` parses as a perfectly valid IPv6 address, and every
|
|
117
|
+
// Ruby or C++ namespace separator in your logs becomes an "address".
|
|
118
|
+
if matches!(c.prev_byte(), Some(b) if b.is_ascii_alphanumeric() || b == b':') {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if matches!(c.next_byte(), Some(b) if b.is_ascii_alphanumeric() || b == b':') {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
Ipv6Addr::from_str(c.matched).is_ok()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Reject a value that is already one of our own redaction tokens.
|
|
128
|
+
///
|
|
129
|
+
/// Scrubbing an already-scrubbed line is normal — nested middleware, a log
|
|
130
|
+
/// formatter running over a pre-redacted message — and it has to be a no-op.
|
|
131
|
+
/// Without this, `password=[PASSWORD_PAIR]` re-redacts to
|
|
132
|
+
/// `password=[PASSWORD_PAIR]]`, corrupting the line a little more on every pass.
|
|
133
|
+
///
|
|
134
|
+
/// The check is deliberately narrow: `[secret]` is a real value and still gets
|
|
135
|
+
/// redacted. Only `[UPPER_CASE]` and `[UPPER_CASE:0badc0de]` are treated as ours.
|
|
136
|
+
pub fn not_redaction_token(c: &Candidate) -> bool {
|
|
137
|
+
let Some(body) = c
|
|
138
|
+
.matched
|
|
139
|
+
.strip_prefix('[')
|
|
140
|
+
.map(|rest| rest.strip_suffix(']').unwrap_or(rest))
|
|
141
|
+
else {
|
|
142
|
+
return true;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let (label, digest) = match body.split_once(':') {
|
|
146
|
+
Some((label, digest)) => (label, Some(digest)),
|
|
147
|
+
None => (body, None),
|
|
148
|
+
};
|
|
149
|
+
let labelish = !label.is_empty()
|
|
150
|
+
&& label
|
|
151
|
+
.bytes()
|
|
152
|
+
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_');
|
|
153
|
+
let digestish = match digest {
|
|
154
|
+
None => true,
|
|
155
|
+
Some(d) => !d.is_empty() && d.bytes().all(|b| b.is_ascii_hexdigit()),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
!(labelish && digestish)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Indian PAN: the 4th character encodes the holder type.
|
|
162
|
+
pub fn pan(c: &Candidate) -> bool {
|
|
163
|
+
// A=AOP, B=BOI, C=Company, F=Firm, G=Government, H=HUF, J=Artificial
|
|
164
|
+
// juridical person, L=Local authority, P=Individual, T=Trust, E=LLP,
|
|
165
|
+
// K=Krish (trust variant).
|
|
166
|
+
const ENTITY_TYPES: &[u8] = b"ABCFGHJLPTEK";
|
|
167
|
+
c.matched
|
|
168
|
+
.as_bytes()
|
|
169
|
+
.get(3)
|
|
170
|
+
.is_some_and(|b| ENTITY_TYPES.contains(b))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// UPI VPA: the handle after `@` must be a known PSP, and the match must not be
|
|
174
|
+
/// the front half of an email address (`nik@upi.example.com`).
|
|
175
|
+
pub fn upi_vpa(c: &Candidate) -> bool {
|
|
176
|
+
if matches!(c.next_byte(), Some(b) if b == b'.' || b == b'-' || b == b'_' || b.is_ascii_alphanumeric())
|
|
177
|
+
{
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
match c.matched.rsplit_once('@') {
|
|
181
|
+
Some((local, handle)) => !local.is_empty() && upi::is_psp(handle),
|
|
182
|
+
None => false,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Email: reject the domain shapes the (deliberately loose) regex lets through.
|
|
187
|
+
pub fn email(c: &Candidate) -> bool {
|
|
188
|
+
let Some((local, domain)) = c.matched.rsplit_once('@') else {
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
if local.is_empty() || local.len() > 64 || local.starts_with('.') || local.ends_with('.') {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if domain.contains("..") || domain.starts_with('.') || domain.starts_with('-') {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// Don't fire on the tail of something already handled as a UPI VPA or on a
|
|
198
|
+
// match that starts mid-token.
|
|
199
|
+
!matches!(c.prev_byte(), Some(b) if b.is_ascii_alphanumeric() || b == b'@')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Generic guard: the match must not start in the middle of a word.
|
|
203
|
+
pub fn word_start(c: &Candidate) -> bool {
|
|
204
|
+
!matches!(c.prev_byte(), Some(b) if b.is_ascii_alphanumeric())
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Countries that have joined the IBAN registry. Checking this before mod-97
|
|
208
|
+
/// takes the false-positive rate on random uppercase tokens from ~1% to ~0.
|
|
209
|
+
const IBAN_COUNTRIES: &[&str] = &[
|
|
210
|
+
"AD", "AE", "AL", "AT", "AZ", "BA", "BE", "BG", "BH", "BI", "BR", "BY", "CH", "CR", "CY", "CZ",
|
|
211
|
+
"DE", "DJ", "DK", "DO", "EE", "EG", "ES", "FI", "FK", "FO", "FR", "GB", "GE", "GI", "GL", "GR",
|
|
212
|
+
"GT", "HN", "HR", "HU", "IE", "IL", "IQ", "IS", "IT", "JO", "KW", "KZ", "LB", "LC", "LI", "LT",
|
|
213
|
+
"LU", "LV", "LY", "MC", "MD", "ME", "MK", "MN", "MR", "MT", "MU", "NI", "NL", "NO", "OM", "PK",
|
|
214
|
+
"PL", "PS", "PT", "QA", "RO", "RS", "RU", "SA", "SC", "SD", "SE", "SI", "SK", "SM", "SO", "ST",
|
|
215
|
+
"SV", "TL", "TN", "TR", "UA", "VA", "VG", "XK", "YE",
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
#[cfg(test)]
|
|
219
|
+
mod tests {
|
|
220
|
+
use super::*;
|
|
221
|
+
|
|
222
|
+
fn cand<'a>(text: &'a str, matched: &'a str) -> Candidate<'a> {
|
|
223
|
+
let start = text.find(matched).expect("matched must occur in text");
|
|
224
|
+
Candidate {
|
|
225
|
+
text,
|
|
226
|
+
matched,
|
|
227
|
+
start,
|
|
228
|
+
end: start + matched.len(),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn credit_card_needs_luhn_and_length() {
|
|
234
|
+
assert!(credit_card(&cand(
|
|
235
|
+
"x 4111 1111 1111 1111 y",
|
|
236
|
+
"4111 1111 1111 1111"
|
|
237
|
+
)));
|
|
238
|
+
assert!(credit_card(&cand("x 378282246310005 y", "378282246310005")));
|
|
239
|
+
// Right shape, wrong checksum (S3).
|
|
240
|
+
assert!(!credit_card(&cand(
|
|
241
|
+
"x 1234567890123456 y",
|
|
242
|
+
"1234567890123456"
|
|
243
|
+
)));
|
|
244
|
+
// Passes Luhn but too short to be a PAN.
|
|
245
|
+
assert!(!credit_card(&cand("x 000000000000 y", "000000000000")));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[test]
|
|
249
|
+
fn ssn_rejects_unissued_ranges() {
|
|
250
|
+
assert!(ssn(&cand("a 123-45-6789 b", "123-45-6789")));
|
|
251
|
+
assert!(!ssn(&cand("a 000-45-6789 b", "000-45-6789")));
|
|
252
|
+
assert!(!ssn(&cand("a 666-45-6789 b", "666-45-6789")));
|
|
253
|
+
assert!(!ssn(&cand("a 900-45-6789 b", "900-45-6789")));
|
|
254
|
+
assert!(!ssn(&cand("a 123-00-6789 b", "123-00-6789")));
|
|
255
|
+
assert!(!ssn(&cand("a 123-45-0000 b", "123-45-0000")));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#[test]
|
|
259
|
+
fn ipv4_validates_octets_and_boundaries() {
|
|
260
|
+
assert!(ipv4(&cand("from 192.168.1.10 ok", "192.168.1.10")));
|
|
261
|
+
assert!(!ipv4(&cand("v 999.1.1.1 x", "999.1.1.1")));
|
|
262
|
+
assert!(!ipv4(&cand("v 1.2.3.4.5 x", "1.2.3.4")));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn ipv6_uses_the_real_parser() {
|
|
267
|
+
assert!(ipv6(&cand(
|
|
268
|
+
"src 2001:0db8:85a3:0000:0000:8a2e:0370:7334 dst",
|
|
269
|
+
"2001:0db8:85a3:0000:0000:8a2e:0370:7334"
|
|
270
|
+
)));
|
|
271
|
+
assert!(ipv6(&cand("host ::1 port", "::1")));
|
|
272
|
+
assert!(ipv6(&cand("host fe80::1 port", "fe80::1")));
|
|
273
|
+
// A timestamp is not an address.
|
|
274
|
+
assert!(!ipv6(&cand("at 12:34:56 done", "12:34:56")));
|
|
275
|
+
// Neither is a MAC.
|
|
276
|
+
assert!(!ipv6(&cand(
|
|
277
|
+
"mac 00:1a:2b:3c:4d:5e up",
|
|
278
|
+
"00:1a:2b:3c:4d:5e"
|
|
279
|
+
)));
|
|
280
|
+
// Nor is a Ruby constant path, even though `::C` parses as an address.
|
|
281
|
+
assert!(!ipv6(&cand("PG::ConnectionBad raised", "::C")));
|
|
282
|
+
assert!(!ipv6(&cand("Foo::Bar::Baz", "::Ba")));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[test]
|
|
286
|
+
fn redaction_tokens_are_not_re_redacted() {
|
|
287
|
+
for token in [
|
|
288
|
+
"[PASSWORD_PAIR",
|
|
289
|
+
"[PASSWORD_PAIR]",
|
|
290
|
+
"[EMAIL:9f86d081]",
|
|
291
|
+
"[API_KEY]",
|
|
292
|
+
] {
|
|
293
|
+
let text = format!("password={token}");
|
|
294
|
+
assert!(
|
|
295
|
+
!not_redaction_token(&cand(&text, token)),
|
|
296
|
+
"{token} should be recognised as ours"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[test]
|
|
302
|
+
fn real_bracketed_values_are_still_redacted() {
|
|
303
|
+
for value in ["[secret]", "[hunter2", "[MiXeD]", "[EMAIL:zzz]"] {
|
|
304
|
+
let text = format!("password={value}");
|
|
305
|
+
assert!(
|
|
306
|
+
not_redaction_token(&cand(&text, value)),
|
|
307
|
+
"{value} is a real value, not a token"
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#[test]
|
|
313
|
+
fn pan_checks_entity_character() {
|
|
314
|
+
assert!(pan(&cand("pan ABCPE1234F now", "ABCPE1234F")));
|
|
315
|
+
assert!(pan(&cand("pan ABCCE1234F now", "ABCCE1234F")));
|
|
316
|
+
// 'X' is not a valid holder type.
|
|
317
|
+
assert!(!pan(&cand("pan ABCXE1234F now", "ABCXE1234F")));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
#[test]
|
|
321
|
+
fn upi_requires_known_psp_and_clean_boundary() {
|
|
322
|
+
assert!(upi_vpa(&cand("pay nik@ybl now", "nik@ybl")));
|
|
323
|
+
assert!(!upi_vpa(&cand("mail nik@gmail.com now", "nik@gmail")));
|
|
324
|
+
// `nik@upi.example.com` is an email, not a VPA.
|
|
325
|
+
assert!(!upi_vpa(&cand("mail nik@upi.example.com x", "nik@upi")));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn iban_requires_registered_country() {
|
|
330
|
+
assert!(iban(&cand(
|
|
331
|
+
"acct GB82 WEST 1234 5698 7654 32 ok",
|
|
332
|
+
"GB82 WEST 1234 5698 7654 32"
|
|
333
|
+
)));
|
|
334
|
+
assert!(!iban(&cand(
|
|
335
|
+
"acct ZZ82WEST12345698765432 ok",
|
|
336
|
+
"ZZ82WEST12345698765432"
|
|
337
|
+
)));
|
|
338
|
+
}
|
|
339
|
+
}
|