@link-foundation/sensitive-data-sanitizer 0.19.3 → 0.21.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +22 -2
  3. package/bin/sensitive-data-sanitizer.js +50 -28
  4. package/docs/API.md +355 -16
  5. package/docs/COVERAGE.md +19 -9
  6. package/docs/RULES.md +218 -0
  7. package/examples/second-pass-placeholders.mjs +28 -0
  8. package/package.json +12 -3
  9. package/src/allow-list.js +93 -0
  10. package/src/confidence.js +24 -9
  11. package/src/context.js +287 -40
  12. package/src/credential-spans.js +180 -0
  13. package/src/credentials-entry.d.ts +23 -0
  14. package/src/credentials-entry.js +62 -0
  15. package/src/credentials.js +51 -0
  16. package/src/crypto-addresses.js +74 -0
  17. package/src/detection.js +87 -41
  18. package/src/document-person-cues.js +1 -1
  19. package/src/encoded.js +137 -51
  20. package/src/encrypt.js +95 -0
  21. package/src/engines.js +33 -10
  22. package/src/entities.js +4 -3
  23. package/src/exempt.js +72 -0
  24. package/src/fake-memory.js +32 -0
  25. package/src/fake.js +33 -29
  26. package/src/features.js +29 -0
  27. package/src/files.js +57 -12
  28. package/src/gitleaks.js +82 -27
  29. package/src/html-tags.js +8 -0
  30. package/src/identifier-detector.js +36 -0
  31. package/src/identity-types.js +63 -0
  32. package/src/identity.js +3 -47
  33. package/src/index.d.ts +157 -3
  34. package/src/index.js +13 -0
  35. package/src/known.js +33 -15
  36. package/src/national-checks.js +209 -0
  37. package/src/national.js +310 -92
  38. package/src/pattern-detector.js +219 -0
  39. package/src/personal-context.js +3 -2
  40. package/src/personal-evidence.js +233 -0
  41. package/src/personal-features.js +39 -0
  42. package/src/personal.js +66 -10
  43. package/src/placeholder-types.js +154 -4
  44. package/src/placeholders.js +127 -45
  45. package/src/projection.js +78 -26
  46. package/src/proper-names.js +48 -5
  47. package/src/prose-words.js +120 -0
  48. package/src/record-worker.js +1 -0
  49. package/src/rule-catalog.js +426 -0
  50. package/src/rules.js +8 -0
  51. package/src/sanitizer.js +133 -23
  52. package/src/sarif.js +127 -0
  53. package/src/stream-worker.js +1 -0
  54. package/src/strict-attributes.js +48 -4
  55. package/src/strict.js +1 -1
  56. package/src/structured.js +2 -0
  57. package/src/token-checksum.js +49 -0
  58. package/src/transforms.js +146 -33
  59. package/src/vendor/betterleaks/LICENSE +21 -0
  60. package/src/vendor/betterleaks/provenance.json +11 -0
  61. package/src/vendor/betterleaks/rules-0.json +463 -0
  62. package/src/vendor/betterleaks/rules-1.json +454 -0
  63. package/src/vendor/betterleaks/rules-2.json +461 -0
  64. package/src/vendor/betterleaks/rules-3.json +455 -0
  65. package/src/vendor/betterleaks/rules-4.json +483 -0
  66. package/src/vendor/betterleaks/rules-5.json +35 -0
  67. package/src/vendor/entities/aws.json +1 -1
  68. package/src/vendor/entities/azure.json +6 -2
  69. package/src/vendor/entities/google.json +2 -1
  70. package/src/vocabulary.js +27 -1
  71. package/src/worker-options.js +1 -0
@@ -127,3 +127,212 @@ export function southAfricanId(value) {
127
127
  shortLuhn(value)
128
128
  );
129
129
  }
130
+ const repeated = (digits) => /^(\d)\1+$/.test(digits);
131
+ // US ABA routing transit number: Federal Reserve prefix and 3-7-1 weights.
132
+ export function abaRouting(value) {
133
+ const d = digitsOf(value),
134
+ prefix = Number(d.slice(0, 2));
135
+ return (
136
+ d.length === 9 &&
137
+ !repeated(d) &&
138
+ (prefix <= 12 ||
139
+ (prefix >= 21 && prefix <= 32) ||
140
+ (prefix >= 61 && prefix <= 72) ||
141
+ prefix === 80) &&
142
+ sum(d, [3, 7, 1, 3, 7, 1, 3, 7, 1]) % 10 === 0
143
+ );
144
+ }
145
+ const dniLetters = 'TRWAGMYFPDXBNJZSQVHLCKE';
146
+ // Spanish NIE: X/Y/Z stands for 0/1/2 before the DNI mod-23 letter.
147
+ export function spanishNie(value) {
148
+ const v = value.toUpperCase();
149
+ return (
150
+ /^[XYZ]\d{7}[A-Z]$/.test(v) &&
151
+ dniLetters[Number(`${'XYZ'.indexOf(v[0])}${v.slice(1, 8)}`) % 23] === v[8]
152
+ );
153
+ }
154
+ // Australian Business Number: subtract one from the first digit, mod 89.
155
+ export function abn(value) {
156
+ const d = digitsOf(value).split('').map(Number);
157
+ if (d.length !== 11 || repeated(d.join(''))) {
158
+ return false;
159
+ }
160
+ d[0] -= 1;
161
+ return sum(d, [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19]) % 89 === 0;
162
+ }
163
+ // Australian Company Number: weighted complement mod 10.
164
+ export function acn(value) {
165
+ const d = digitsOf(value);
166
+ return (
167
+ d.length === 9 &&
168
+ !repeated(d) &&
169
+ (10 - (sum(d, [8, 7, 6, 5, 4, 3, 2, 1]) % 10)) % 10 === Number(d[8])
170
+ );
171
+ }
172
+ // Australian Medicare card: first digit 2-6, ninth digit is the check digit.
173
+ export function medicare(value) {
174
+ const d = digitsOf(value);
175
+ return (
176
+ /^[2-6]\d{9,10}$/.test(d) &&
177
+ sum(d, [1, 3, 7, 9, 1, 3, 7, 9]) % 10 === Number(d[8])
178
+ );
179
+ }
180
+ const verhoeffD = [
181
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
182
+ [1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
183
+ [2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
184
+ [3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
185
+ [4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
186
+ [5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
187
+ [6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
188
+ [7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
189
+ [8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
190
+ [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],
191
+ ];
192
+ const verhoeffP = [
193
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
194
+ [1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
195
+ [5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
196
+ [8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
197
+ [9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
198
+ [4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
199
+ [2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
200
+ [7, 0, 4, 6, 9, 1, 3, 2, 5, 8],
201
+ ];
202
+ export function verhoeff(value) {
203
+ const d = digitsOf(value);
204
+ let check = 0;
205
+ for (let i = 0; i < d.length; i++) {
206
+ check = verhoeffD[check][verhoeffP[i % 8][Number(d[d.length - 1 - i])]];
207
+ }
208
+ return d.length > 0 && !repeated(d) && check === 0;
209
+ }
210
+ // Indian Aadhaar: 12 digits, never starting with 0 or 1, Verhoeff check.
211
+ export const aadhaar = (value) =>
212
+ /^[2-9]\d{11}$/.test(digitsOf(value)) && verhoeff(value);
213
+ const base36 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
214
+ // Indian GSTIN: state code, PAN, entity number, "Z", mod-36 check character.
215
+ export function gstin(value) {
216
+ const v = value.toUpperCase();
217
+ if (!/^\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(v)) {
218
+ return false;
219
+ }
220
+ let total = 0;
221
+ for (let i = 0; i < 14; i++) {
222
+ const product = base36.indexOf(v[i]) * (i % 2 === 0 ? 1 : 2);
223
+ total += Math.floor(product / 36) + (product % 36);
224
+ }
225
+ return base36[(36 - (total % 36)) % 36] === v[14];
226
+ }
227
+ // Finnish henkilötunnus: DDMMYY, century sign, individual number, mod 31.
228
+ export function finnishIdentityCode(value) {
229
+ const v = value.toUpperCase(),
230
+ match = /^(\d{6})([-+A-FU-Y])(\d{3})([0-9A-Y])$/.exec(v);
231
+ if (!match) {
232
+ return false;
233
+ }
234
+ const [, date, , individual, check] = match;
235
+ const swapped = `${date.slice(4, 6)}${date.slice(2, 4)}${date.slice(0, 2)}`;
236
+ return (
237
+ datePart(swapped) &&
238
+ '0123456789ABCDEFHJKLMNPRSTUVWXY'[Number(date + individual) % 31] === check
239
+ );
240
+ }
241
+ // Thai national identification number: 13 digits, weighted mod 11.
242
+ export function thaiId(value) {
243
+ const d = digitsOf(value);
244
+ return (
245
+ /^[1-8]\d{12}$/.test(d) &&
246
+ (11 - (sum(d, [13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) % 11)) % 10 ===
247
+ Number(d[12])
248
+ );
249
+ }
250
+ // US DEA registration: registrant letter, name letter or 9, six digits, check.
251
+ export function dea(value) {
252
+ const v = value.toUpperCase();
253
+ if (!/^[ABCDEFGHJKLMPRSTUX][A-Z9]\d{7}$/.test(v)) {
254
+ return false;
255
+ }
256
+ const d = v.slice(2);
257
+ return (
258
+ (Number(d[0]) +
259
+ Number(d[2]) +
260
+ Number(d[4]) +
261
+ 2 * (Number(d[1]) + Number(d[3]) + Number(d[5]))) %
262
+ 10 ===
263
+ Number(d[6])
264
+ );
265
+ }
266
+ const regions = new Intl.DisplayNames(['en'], {
267
+ type: 'region',
268
+ fallback: 'none',
269
+ });
270
+ // ISO 3166 alpha-2 region known to the runtime's Unicode CLDR data.
271
+ export const regionCode = (code) =>
272
+ /^[A-Z]{2}$/.test(code) && code !== 'ZZ' && regions.of(code) !== undefined;
273
+ // SWIFT/BIC: bank, ISO country, location, optional branch.
274
+ export const swiftCode = (value) =>
275
+ /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$/.test(value) &&
276
+ regionCode(value.slice(4, 6));
277
+ // ISO 3779 / FMVSS 115 vehicle identification number: position 9 check digit.
278
+ export function vin(value) {
279
+ const v = value.toUpperCase();
280
+ if (!/^[A-HJ-NPR-Z\d]{17}$/.test(v) || !/\d/.test(v) || !/[A-Z]/.test(v)) {
281
+ return false;
282
+ }
283
+ const weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
284
+ let total = 0;
285
+ for (let i = 0; i < 17; i++) {
286
+ const char = v[i];
287
+ // Transliteration: A-H 1-8, J-N 1-5, P 7, R 9, S-Z 2-9 (I, O, Q unused).
288
+ const code = /\d/.test(char)
289
+ ? Number(char)
290
+ : Number('12345678 12345 7 923456789'[char.charCodeAt(0) - 65]);
291
+ total += code * weights[i];
292
+ }
293
+ const check = total % 11;
294
+ return (check === 10 ? 'X' : String(check)) === v[8];
295
+ }
296
+ // German Steuerliche Identifikationsnummer: ISO 7064 MOD 11,10; no leading 0.
297
+ // ISO 7064 MOD 11,10 over all digits but the last, which is the check digit.
298
+ function mod11x10(d) {
299
+ let product = 10;
300
+ for (const digit of d.slice(0, -1)) {
301
+ const total = (Number(digit) + product) % 10 || 10;
302
+ product = (total * 2) % 11;
303
+ }
304
+ return (11 - product) % 10 === Number(d.at(-1));
305
+ }
306
+ export function germanTaxId(value) {
307
+ const d = digitsOf(value);
308
+ return /^[1-9]\d{10}$/.test(d) && mod11x10(d);
309
+ }
310
+ // German USt-IdNr: DE plus eight digits and a MOD 11,10 check digit.
311
+ export function germanVat(value) {
312
+ const v = value.replace(/[\s.-]/g, '').toUpperCase();
313
+ return /^DE[1-9]\d{8}$/.test(v) && mod11x10(v.slice(2));
314
+ }
315
+ // UPS 1Z tracking number: letters map to (code - 63) mod 10, even positions
316
+ // are doubled, and the last character is the check digit.
317
+ export function upsTracking(value) {
318
+ const v = value.replace(/\s/g, '').toUpperCase();
319
+ if (!/^1Z[0-9A-Z]{15}\d$/.test(v)) {
320
+ return false;
321
+ }
322
+ let total = 0;
323
+ for (let i = 2; i < 17; i++) {
324
+ const code = /\d/.test(v[i]) ? Number(v[i]) : (v.charCodeAt(i) - 63) % 10;
325
+ total += i % 2 === 1 ? code * 2 : code;
326
+ }
327
+ return (10 - (total % 10)) % 10 === Number(v[17]);
328
+ }
329
+ // UAE Emirates ID: 784, birth year, serial and a Luhn check digit.
330
+ export const emiratesId = (value) =>
331
+ /^784(?:19|20)\d{10}$/.test(digitsOf(value)) && shortLuhn(value);
332
+ // Saudi national ID (1…) and Iqama (2…) numbers use Luhn over 10 digits.
333
+ export const saudiId = (value) =>
334
+ /^[12]\d{9}$/.test(digitsOf(value)) && shortLuhn(value);
335
+ // Israeli Teudat Zehut: up to 9 digits, left-padded, Luhn-weighted 1,2,1,2…
336
+ export const israeliId = (value) =>
337
+ /^\d{5,9}$/.test(digitsOf(value)) &&
338
+ shortLuhn(digitsOf(value).padStart(9, '0'));
package/src/national.js CHANGED
@@ -1,112 +1,330 @@
1
1
  import { numericNoise, epochNumber } from './confidence.js';
2
- import {
3
- digitsOf,
4
- datePart,
5
- nhs,
6
- snils,
7
- inn,
8
- pesel,
9
- rrn,
10
- tfn,
11
- nric,
12
- fiscalCode,
13
- personnummer,
14
- southAfricanId,
15
- shortLuhn,
16
- } from './national-checks.js';
2
+ import * as checks from './national-checks.js';
17
3
 
18
- const rules = [
4
+ const never = () => false;
5
+ const always = () => true;
6
+ // Each rule is data: `context` near the value, a `checksum` and a distinctive
7
+ // `plausible` shape raise confidence; `valid` (when present) must always hold.
8
+ // Callers may reuse or extend this list through `nationalRules`.
9
+ export const nationalRules = Object.freeze(
19
10
  [
20
- 'UK_NHS',
21
- /\d{3}[ -]?\d{3}[ -]?\d{4}/g,
22
- /\b(?:NHS|patient|healthcare|provider)\b/i,
23
- nhs,
24
- ],
25
- [
26
- 'KR_RRN',
27
- /\d{6}[ -]?[1-8]\d{6}/g,
28
- /\bRRN\b|주민등록/iu,
29
- rrn,
30
- (v) => datePart(digitsOf(v)),
31
- ],
32
- [
33
- 'IT_FISCAL_CODE',
34
- /[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]/g,
35
- /\bCF\b|codice fiscale/iu,
36
- fiscalCode,
37
- () => true,
38
- ],
39
- ['SG_NRIC_FIN', /[STFGM]\d{7}[A-Z]/g, /\b(?:NRIC|FIN)\b/iu, nric, () => true],
40
- ['PL_PESEL', /\d{11}/g, /\bPESEL\b/iu, pesel],
41
- [
42
- 'SE_PERSONNUMMER',
43
- /(?:\d{2})?\d{6}[-+]\d{4}/g,
44
- /personnummer/iu,
45
- personnummer,
46
- ],
47
- [
48
- 'ZA_ID_NUMBER',
49
- /\d{10}[0-2][89]\d/g,
50
- /\b(?:SA ID|RSA ID)\b|south african id/iu,
51
- southAfricanId,
52
- ],
53
- ['AU_TFN', /\d{3}[ -]?\d{3}[ -]?\d{3}/g, /\bTFN\b|tax file number/iu, tfn],
54
- [
55
- 'CA_SIN',
56
- /\d{3}[ -]?\d{3}[ -]?\d{3}/g,
57
- /\bSIN\b|social insurance/iu,
58
- shortLuhn,
59
- ],
60
- [
61
- 'RU_SNILS',
62
- /\d{3}[- ]\d{3}[- ]\d{3}[ -]\d{2}|\d{11}/g,
63
- /\bSNILS\b|снилс|пенсионное|страховое свидетельство|страховой номер/iu,
64
- snils,
65
- ],
66
- ['RU_INN', /\d{12}|\d{10}/g, /\bINN\b|инн/iu, inn],
67
- ];
11
+ {
12
+ type: 'UK_NHS',
13
+ pattern: /\d{3}[ -]?\d{3}[ -]?\d{4}/g,
14
+ context: /\b(?:NHS|patient|healthcare|provider)\b/i,
15
+ checksum: checks.nhs,
16
+ },
17
+ {
18
+ type: 'KR_RRN',
19
+ pattern: /\d{6}[ -]?[1-8]\d{6}/g,
20
+ context: /\bRRN\b|주민등록/iu,
21
+ checksum: checks.rrn,
22
+ plausible: (v) => checks.datePart(checks.digitsOf(v)),
23
+ },
24
+ {
25
+ type: 'IT_FISCAL_CODE',
26
+ pattern:
27
+ /[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]/g,
28
+ context: /\bCF\b|codice fiscale/iu,
29
+ checksum: checks.fiscalCode,
30
+ plausible: always,
31
+ },
32
+ {
33
+ type: 'SG_NRIC_FIN',
34
+ pattern: /[STFGM]\d{7}[A-Z]/g,
35
+ context: /\b(?:NRIC|FIN)\b/iu,
36
+ checksum: checks.nric,
37
+ plausible: always,
38
+ },
39
+ {
40
+ type: 'PL_PESEL',
41
+ pattern: /\d{11}/g,
42
+ context: /\bPESEL\b/iu,
43
+ checksum: checks.pesel,
44
+ },
45
+ {
46
+ type: 'SE_PERSONNUMMER',
47
+ pattern: /(?:\d{2})?\d{6}[-+]\d{4}/g,
48
+ context: /personnummer/iu,
49
+ checksum: checks.personnummer,
50
+ },
51
+ {
52
+ type: 'ZA_ID_NUMBER',
53
+ pattern: /\d{10}[0-2][89]\d/g,
54
+ context: /\b(?:SA ID|RSA ID)\b|south african id/iu,
55
+ checksum: checks.southAfricanId,
56
+ },
57
+ {
58
+ type: 'AU_TFN',
59
+ pattern: /\d{3}[ -]?\d{3}[ -]?\d{3}/g,
60
+ context: /\bTFN\b|tax file number/iu,
61
+ checksum: checks.tfn,
62
+ },
63
+ {
64
+ type: 'CA_SIN',
65
+ pattern: /\d{3}[ -]?\d{3}[ -]?\d{3}/g,
66
+ context: /\b(?:SIN|NAS)\b|social insurance|assurance sociale/iu,
67
+ checksum: checks.shortLuhn,
68
+ },
69
+ {
70
+ type: 'RU_SNILS',
71
+ pattern: /\d{3}[- ]\d{3}[- ]\d{3}[ -]\d{2}|\d{11}/g,
72
+ context:
73
+ /\bSNILS\b|снилс|пенсионное|страховое свидетельство|страховой номер/iu,
74
+ checksum: checks.snils,
75
+ },
76
+ {
77
+ type: 'RU_INN',
78
+ pattern: /\d{12}|\d{10}/g,
79
+ context: /\bINN\b|инн/iu,
80
+ checksum: checks.inn,
81
+ },
82
+ {
83
+ type: 'US_ITIN',
84
+ pattern: /9\d{2}([- ]?)(?:5\d|6[0-5]|7\d|8[0-8]|9[0-2]|9[4-9])\1\d{4}/g,
85
+ context: /\bITIN\b|taxpayer identification/iu,
86
+ checksum: never,
87
+ plausible: (v) => /^\d{3}-\d{2}-\d{4}$/.test(v),
88
+ },
89
+ {
90
+ type: 'ABA_ROUTING_NUMBER',
91
+ pattern: /\d{4}-?\d{4}-?\d/g,
92
+ context: /\b(?:routing|ABA|RTN|transit)\b/iu,
93
+ checksum: checks.abaRouting,
94
+ valid: checks.abaRouting,
95
+ },
96
+ {
97
+ type: 'US_BANK_NUMBER',
98
+ pattern: /\d{8,17}/g,
99
+ context:
100
+ /\bbank\s+account\b|\b(?:account|acct)\.?\s*(?:number|num|no\.?|#)|\b(?:checking|savings)\s+account\b/iu,
101
+ checksum: never,
102
+ },
103
+ {
104
+ type: 'US_DRIVER_LICENSE',
105
+ pattern: /[A-Z]{1,2}\d{4,14}|\d{7,14}/g,
106
+ context: /\bdriver'?s?[ -]licen[cs]e\b|\bDLN?\b|\bD\.L\.(?=\s|$)/iu,
107
+ checksum: never,
108
+ },
109
+ {
110
+ type: 'UK_DRIVING_LICENCE',
111
+ pattern: /[A-Z9]{5}\d[0156]\d{4}[A-Z9]{2}\d[A-Z]{2}/g,
112
+ context: /\bdriv(?:ing|er'?s?)[ -]licen[cs]e\b|\bDVLA\b/iu,
113
+ checksum: never,
114
+ plausible: always,
115
+ },
116
+ {
117
+ type: 'UK_NATIONAL_INSURANCE_NUMBER',
118
+ pattern: /[A-Z]{2} ?\d{2} ?\d{2} ?\d{2} ?[A-D]/g,
119
+ context: /\bNINO\b|national insurance|\bNI (?:number|no\b)/iu,
120
+ checksum: never,
121
+ },
122
+ {
123
+ type: 'ES_NIE',
124
+ pattern: /[XYZ]-?\d{7}-?[A-Z]/g,
125
+ context: /\bNIE\b|extranjero/iu,
126
+ checksum: (v) => checks.spanishNie(v.replace(/-/g, '')),
127
+ plausible: always,
128
+ },
129
+ {
130
+ type: 'IT_VAT_CODE',
131
+ pattern: /\d{11}/g,
132
+ context: /partita\s+iva|\bP\.?\s?IVA\b|\bVAT\b/iu,
133
+ checksum: checks.shortLuhn,
134
+ },
135
+ {
136
+ type: 'AU_ABN',
137
+ pattern: /\d{2} ?\d{3} ?\d{3} ?\d{3}/g,
138
+ context: /\bABN\b|australian business number/iu,
139
+ checksum: checks.abn,
140
+ plausible: (v) => /^\d{2} \d{3} \d{3} \d{3}$/.test(v),
141
+ },
142
+ {
143
+ type: 'AU_ACN',
144
+ pattern: /\d{3} ?\d{3} ?\d{3}/g,
145
+ context: /\bACN\b|australian company number/iu,
146
+ checksum: checks.acn,
147
+ },
148
+ {
149
+ type: 'AU_MEDICARE',
150
+ pattern: /[2-6]\d{3} ?\d{5} ?\d(?:[ -/]?\d)?/g,
151
+ context: /\bmedicare\b/iu,
152
+ checksum: checks.medicare,
153
+ },
154
+ {
155
+ type: 'IN_AADHAAR',
156
+ pattern: /[2-9]\d{3}([ -]?)\d{4}\1\d{4}/g,
157
+ context: /aadhaa?r|आधार|\bUIDAI\b/iu,
158
+ checksum: checks.aadhaar,
159
+ },
160
+ {
161
+ type: 'IN_GSTIN',
162
+ pattern: /\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]/g,
163
+ context: /\bGSTIN?\b|goods and services tax/iu,
164
+ checksum: checks.gstin,
165
+ plausible: always,
166
+ },
167
+ {
168
+ type: 'IN_VOTER',
169
+ pattern: /[A-Z]{3}\d{7}/g,
170
+ context: /\bvoter\b|\bEPIC\b|election commission/iu,
171
+ checksum: never,
172
+ },
173
+ {
174
+ type: 'IN_PASSPORT',
175
+ pattern: /[A-PR-WY][1-9]\d ?\d{4}[1-9]/g,
176
+ context: /\bpassport\b/iu,
177
+ checksum: never,
178
+ },
179
+ {
180
+ type: 'FI_PERSONAL_IDENTITY_CODE',
181
+ pattern: /\d{6}[-+A-FU-Y]\d{3}[0-9A-FHJ-NPR-Y]/g,
182
+ context:
183
+ /henkilötunnus|\bHETU\b|personal identity code|personbeteckning/iu,
184
+ checksum: checks.finnishIdentityCode,
185
+ plausible: always,
186
+ },
187
+ {
188
+ type: 'TH_TNIN',
189
+ pattern: /[1-8]\d{12}|[1-8]-\d{4}-\d{5}-\d{2}-\d/g,
190
+ context: /\bthai\b|\bTNIN\b|เลขประจำตัวประชาชน|บัตรประชาชน/iu,
191
+ checksum: checks.thaiId,
192
+ },
193
+ {
194
+ type: 'MEDICAL_LICENSE',
195
+ pattern: /[ABCDEFGHJKLMPRSTUX][A-Z9]\d{7}/g,
196
+ context: /\bDEA\b|medical licen[cs]e|prescriber/iu,
197
+ checksum: checks.dea,
198
+ },
199
+ {
200
+ type: 'IMEI',
201
+ pattern: /\d{15}|\d{2}-\d{6}-\d{6}-\d/g,
202
+ context: /\bIMEI\b/iu,
203
+ checksum: checks.shortLuhn,
204
+ },
205
+ {
206
+ type: 'SWIFT_CODE',
207
+ pattern: /[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?/g,
208
+ context: /\b(?:SWIFT|BIC)\b/iu,
209
+ checksum: never,
210
+ valid: checks.swiftCode,
211
+ },
212
+ {
213
+ type: 'VIN',
214
+ pattern: /[A-HJ-NPR-Z\d]{17}/g,
215
+ context: /\bVIN\b|vehicle identification|chassis/iu,
216
+ checksum: checks.vin,
217
+ valid: (v) => /\d/.test(v) && /[A-Z]/.test(v),
218
+ },
219
+ {
220
+ type: 'UK_POSTCODE',
221
+ pattern: /[A-Z]{1,2}\d[A-Z\d]? ?\d[ABD-HJLNP-UW-Z]{2}/g,
222
+ context: /\bpost ?code\b|\bpostal code\b/iu,
223
+ checksum: never,
224
+ },
225
+ {
226
+ type: 'SG_UEN',
227
+ pattern: /\d{8}[A-Z]|(?:19|20)\d{7}[A-Z]|[TSR]\d{2}[A-Z]{2}\d{4}[A-Z]/g,
228
+ context: /\bUEN\b|unique entity number/iu,
229
+ checksum: never,
230
+ },
231
+ {
232
+ type: 'DE_TAX_ID',
233
+ pattern: /[1-9]\d(?: ?\d{3}){3}|[1-9]\d{10}/g,
234
+ context: /steuer[ -]?id|steueridentifikationsnummer|\bIdNr\b|\bTIN\b/iu,
235
+ checksum: checks.germanTaxId,
236
+ },
237
+ {
238
+ // EU VAT identification numbers by member-state prefix (VIES formats).
239
+ type: 'EU_VAT_NUMBER',
240
+ pattern:
241
+ /(?:AT ?U\d{8}|BE ?[01]\d{9}|DE ?[1-9]\d{8}|DK ?\d{8}|EL ?\d{9}|ES ?[0-9A-Z]\d{7}[0-9A-Z]|FI ?\d{8}|FR ?[0-9A-HJ-NP-Z]{2}\d{9}|IE ?\d[0-9A-Z+*]\d{5}[A-Z]{1,2}|IT ?\d{11}|LU ?\d{8}|NL ?\d{9}B\d{2}|PL ?\d{10}|PT ?\d{9}|SE ?\d{12})/g,
242
+ context:
243
+ /\bVAT\b|\bUSt\b|USt-?Id|Umsatzsteuer|\bMwSt\b|\bTVA\b|\bBTW\b|\bIVA\b|\bALV\b|\bmoms\b|\bNIP\b|\bNIF\b/iu,
244
+ checksum: checks.germanVat,
245
+ plausible: (v) => /^(?:ATU|NL\d{9}B)/.test(v),
246
+ },
247
+ {
248
+ type: 'AE_EMIRATES_ID',
249
+ pattern: /784-?(?:19|20)\d{2}-?\d{7}-?\d/g,
250
+ context: /emirates\s+id|\bEID\b|الهوية/iu,
251
+ checksum: checks.emiratesId,
252
+ plausible: (v) => /^784-\d{4}-\d{7}-\d$/.test(v),
253
+ },
254
+ {
255
+ type: 'SA_NATIONAL_ID',
256
+ pattern: /[12]\d{9}/g,
257
+ context: /\biqama\b|saudi|national\s+id|رقم الهوية|الإقامة/iu,
258
+ checksum: checks.saudiId,
259
+ },
260
+ {
261
+ type: 'IL_ID_NUMBER',
262
+ pattern: /\d{9}/g,
263
+ context: /teudat\s+zehut|israeli\s+id|תעודת זהות|\bת\.ז\b/iu,
264
+ checksum: checks.israeliId,
265
+ },
266
+ {
267
+ type: 'STEAM_ID',
268
+ pattern: /7656119\d{10}/g,
269
+ context: /\bsteam/iu,
270
+ checksum: never,
271
+ },
272
+ {
273
+ type: 'TRACKING_NUMBER',
274
+ pattern: /1Z[0-9A-Z]{15}\d|9[2-5]\d{2}(?: ?\d{4}){4}(?: ?\d{2})?/g,
275
+ context: /tracking|\bUPS\b|\bUSPS\b|shipment|parcel/iu,
276
+ checksum: checks.upsTracking,
277
+ plausible: (v) => v.startsWith('1Z'),
278
+ },
279
+ ].map((rule) => Object.freeze(rule))
280
+ );
68
281
  function distinctiveShape(type, value) {
69
282
  return (
70
283
  (type === 'RU_SNILS' && /^\d{3}([- ])\d{3}\1\d{3} \d{2}$/.test(value)) ||
71
284
  (type === 'RU_INN' && value.length === 12)
72
285
  );
73
286
  }
74
- export function detectNational(text, emit) {
75
- for (const [type, pattern, context, checksum, plausible] of rules) {
76
- for (const match of text.matchAll(pattern)) {
77
- const start = match.index,
78
- end = start + match[0].length;
79
- if (!nationalBoundary(text, start, end)) {
80
- continue;
81
- }
82
- const hasContext = context.test(
83
- text.slice(Math.max(0, start - 64), end + 32)
84
- );
85
- if (
86
- !hasContext &&
87
- (numericNoise(text, start, end) || epochNumber(match[0]))
88
- ) {
89
- continue;
90
- }
91
- const checked = checksum(match[0]);
92
- if (checked || hasContext || plausible?.(match[0])) {
287
+ export function detectNational(text, emit, rules = nationalRules) {
288
+ for (const rule of rules) {
289
+ for (const match of text.matchAll(rule.pattern)) {
290
+ const confidence = nationalMatch(text, rule, match);
291
+ if (confidence !== undefined) {
93
292
  emit({
94
- start,
95
- end,
96
- type,
293
+ start: match.index,
294
+ end: match.index + match[0].length,
295
+ type: rule.type,
97
296
  category: 'personal',
98
297
  rule: 'national-format',
99
- confidence: nationalConfidence(
100
- hasContext,
101
- checked,
102
- plausible?.(match[0]) ||
103
- (checked && distinctiveShape(type, match[0]))
104
- ),
298
+ confidence,
105
299
  });
106
300
  }
107
301
  }
108
302
  }
109
303
  }
304
+ // Confidence for one candidate, or undefined when it is not an identifier.
305
+ function nationalMatch(text, rule, match) {
306
+ const value = match[0],
307
+ start = match.index,
308
+ end = start + value.length;
309
+ if (!nationalBoundary(text, start, end) || rule.valid?.(value) === false) {
310
+ return undefined;
311
+ }
312
+ const hasContext = rule.context.test(
313
+ text.slice(Math.max(0, start - 64), end + 32)
314
+ );
315
+ if (!hasContext && (numericNoise(text, start) || epochNumber(value))) {
316
+ return undefined;
317
+ }
318
+ const checked = rule.checksum(value),
319
+ plausible = rule.plausible?.(value);
320
+ return checked || hasContext || plausible
321
+ ? nationalConfidence(
322
+ hasContext,
323
+ checked,
324
+ plausible || (checked && distinctiveShape(rule.type, value))
325
+ )
326
+ : undefined;
327
+ }
110
328
  function nationalBoundary(text, start, end) {
111
329
  return !(
112
330
  /[\p{L}\p{N}_.+-]/u.test(text[start - 1] ?? '') ||