@link-foundation/sensitive-data-sanitizer 0.19.3 → 0.20.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.
- package/CHANGELOG.md +6 -0
- package/README.md +8 -1
- package/bin/sensitive-data-sanitizer.js +49 -28
- package/docs/API.md +192 -1
- package/docs/COVERAGE.md +3 -3
- package/docs/RULES.md +217 -0
- package/package.json +1 -1
- package/src/allow-list.js +93 -0
- package/src/confidence.js +24 -9
- package/src/context.js +121 -25
- package/src/credentials.js +44 -0
- package/src/crypto-addresses.js +74 -0
- package/src/detection.js +21 -6
- package/src/document-person-cues.js +1 -1
- package/src/encrypt.js +95 -0
- package/src/entities.js +4 -3
- package/src/fake.js +29 -1
- package/src/files.js +57 -12
- package/src/gitleaks.js +82 -27
- package/src/identifier-detector.js +36 -0
- package/src/identity.js +14 -0
- package/src/index.d.ts +116 -1
- package/src/index.js +7 -0
- package/src/national-checks.js +209 -0
- package/src/national.js +310 -92
- package/src/pattern-detector.js +219 -0
- package/src/personal-context.js +3 -2
- package/src/personal-evidence.js +233 -0
- package/src/personal.js +42 -10
- package/src/projection.js +85 -26
- package/src/rule-catalog.js +424 -0
- package/src/sanitizer.js +48 -3
- package/src/sarif.js +127 -0
- package/src/token-checksum.js +49 -0
- package/src/transforms.js +108 -25
- package/src/vendor/betterleaks/LICENSE +21 -0
- package/src/vendor/betterleaks/provenance.json +11 -0
- package/src/vendor/betterleaks/rules-0.json +463 -0
- package/src/vendor/betterleaks/rules-1.json +454 -0
- package/src/vendor/betterleaks/rules-2.json +461 -0
- package/src/vendor/betterleaks/rules-3.json +455 -0
- package/src/vendor/betterleaks/rules-4.json +483 -0
- package/src/vendor/betterleaks/rules-5.json +35 -0
- package/src/vendor/entities/aws.json +1 -1
- package/src/vendor/entities/azure.json +6 -2
- package/src/vendor/entities/google.json +2 -1
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
76
|
-
for (const match of text.matchAll(pattern)) {
|
|
77
|
-
const
|
|
78
|
-
|
|
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
|
|
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] ?? '') ||
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { failure, escapePattern } from './detection.js';
|
|
2
|
+
|
|
3
|
+
// Declarative recognizers in the style of Presidio's PatternRecognizer
|
|
4
|
+
// (patterns with scores, deny lists, context words, validate_result) and
|
|
5
|
+
// Google custom infoTypes (regex/dictionary plus hotword likelihood rules).
|
|
6
|
+
// The result is an ordinary Detector, so it composes with every other port.
|
|
7
|
+
const typePattern = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
8
|
+
const rulePattern = /^[a-z0-9@/._-]{1,120}$/;
|
|
9
|
+
const maxPatterns = 64,
|
|
10
|
+
maxWords = 10000;
|
|
11
|
+
|
|
12
|
+
const score = (value) =>
|
|
13
|
+
typeof value === 'number' &&
|
|
14
|
+
Number.isFinite(value) &&
|
|
15
|
+
value >= 0 &&
|
|
16
|
+
value <= 1;
|
|
17
|
+
const optional = (value, test) => value === undefined || test(value);
|
|
18
|
+
const words = (list) =>
|
|
19
|
+
Array.isArray(list) &&
|
|
20
|
+
list.length <= maxWords &&
|
|
21
|
+
list.every((word) => typeof word === 'string' && word.trim().length > 0);
|
|
22
|
+
|
|
23
|
+
const validEntry = (raw) =>
|
|
24
|
+
Boolean(raw) &&
|
|
25
|
+
(raw.regex instanceof RegExp ||
|
|
26
|
+
(typeof raw.regex === 'string' && raw.regex.length > 0)) &&
|
|
27
|
+
optional(raw.confidence, score) &&
|
|
28
|
+
optional(raw.group, (group) => Number.isSafeInteger(group) && group >= 0);
|
|
29
|
+
|
|
30
|
+
function globalRegex(regex) {
|
|
31
|
+
try {
|
|
32
|
+
return regex instanceof RegExp
|
|
33
|
+
? new RegExp(
|
|
34
|
+
regex.source,
|
|
35
|
+
regex.flags.includes('g') ? regex.flags : `${regex.flags}g`
|
|
36
|
+
)
|
|
37
|
+
: new RegExp(regex, 'gu');
|
|
38
|
+
} catch {
|
|
39
|
+
throw failure('ERR_CONFIG');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compile(entry) {
|
|
44
|
+
const raw =
|
|
45
|
+
entry instanceof RegExp || typeof entry === 'string'
|
|
46
|
+
? { regex: entry }
|
|
47
|
+
: entry;
|
|
48
|
+
if (!validEntry(raw)) {
|
|
49
|
+
throw failure('ERR_CONFIG');
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
regex: globalRegex(raw.regex),
|
|
53
|
+
confidence: raw.confidence,
|
|
54
|
+
group: raw.group ?? 0,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function window(value) {
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return { before: 64, after: 16 };
|
|
61
|
+
}
|
|
62
|
+
const shape = typeof value === 'number' ? { before: value, after: 0 } : value;
|
|
63
|
+
if (
|
|
64
|
+
!shape ||
|
|
65
|
+
![shape.before ?? 0, shape.after ?? 0].every(
|
|
66
|
+
(n) => Number.isSafeInteger(n) && n >= 0 && n <= 4096
|
|
67
|
+
)
|
|
68
|
+
) {
|
|
69
|
+
throw failure('ERR_CONFIG');
|
|
70
|
+
}
|
|
71
|
+
return { before: shape.before ?? 0, after: shape.after ?? 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const specChecks = [
|
|
75
|
+
(spec) => typePattern.test(spec.type ?? ''),
|
|
76
|
+
(spec) =>
|
|
77
|
+
optional(spec.category, (c) => ['personal', 'credential'].includes(c)),
|
|
78
|
+
(spec) => optional(spec.rule, (rule) => rulePattern.test(rule)),
|
|
79
|
+
(spec) => optional(spec.confidence, score),
|
|
80
|
+
(spec) => optional(spec.denyListConfidence, score),
|
|
81
|
+
(spec) =>
|
|
82
|
+
optional(
|
|
83
|
+
spec.patterns,
|
|
84
|
+
(list) => Array.isArray(list) && list.length <= maxPatterns
|
|
85
|
+
),
|
|
86
|
+
(spec) => optional(spec.denyList, words),
|
|
87
|
+
(spec) => optional(spec.context, words),
|
|
88
|
+
(spec) =>
|
|
89
|
+
optional(
|
|
90
|
+
spec.contextBoost,
|
|
91
|
+
(boost) => typeof boost === 'number' && Math.abs(boost) <= 1
|
|
92
|
+
),
|
|
93
|
+
(spec) => optional(spec.validate, (fn) => typeof fn === 'function'),
|
|
94
|
+
(spec) => Boolean(spec.patterns?.length || spec.denyList?.length),
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
function compileSpec(spec) {
|
|
98
|
+
if (
|
|
99
|
+
!spec ||
|
|
100
|
+
typeof spec !== 'object' ||
|
|
101
|
+
!specChecks.every((check) => check(spec))
|
|
102
|
+
) {
|
|
103
|
+
throw failure('ERR_CONFIG');
|
|
104
|
+
}
|
|
105
|
+
const patterns = (spec.patterns ?? []).map(compile);
|
|
106
|
+
if (spec.denyList?.length) {
|
|
107
|
+
patterns.push({
|
|
108
|
+
regex: new RegExp(
|
|
109
|
+
`(?<![\\p{L}\\p{N}_])(?:${[...spec.denyList]
|
|
110
|
+
.sort((a, b) => b.length - a.length)
|
|
111
|
+
.map(escapePattern)
|
|
112
|
+
.join('|')})(?![\\p{L}\\p{N}_])`,
|
|
113
|
+
spec.caseSensitive ? 'gu' : 'giu'
|
|
114
|
+
),
|
|
115
|
+
confidence: spec.denyListConfidence ?? 1,
|
|
116
|
+
group: 0,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
const context = spec.context?.length
|
|
120
|
+
? new RegExp(
|
|
121
|
+
`(?<![\\p{L}\\p{N}])(?:${spec.context.map(escapePattern).join('|')})(?![\\p{L}\\p{N}])`,
|
|
122
|
+
'iu'
|
|
123
|
+
)
|
|
124
|
+
: undefined;
|
|
125
|
+
return Object.freeze({
|
|
126
|
+
type: spec.type,
|
|
127
|
+
category: spec.category ?? 'personal',
|
|
128
|
+
rule: spec.rule ?? 'custom-pattern',
|
|
129
|
+
confidence: spec.confidence ?? 0.6,
|
|
130
|
+
patterns,
|
|
131
|
+
context,
|
|
132
|
+
window: window(spec.contextWindow),
|
|
133
|
+
boost: spec.contextBoost ?? 0.35,
|
|
134
|
+
validate: spec.validate,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function scored(spec, base, text, start, end, value) {
|
|
139
|
+
let confidence = base;
|
|
140
|
+
if (
|
|
141
|
+
spec.context?.test(
|
|
142
|
+
`${text.slice(Math.max(0, start - spec.window.before), start)}\n${text.slice(end, end + spec.window.after)}`
|
|
143
|
+
)
|
|
144
|
+
) {
|
|
145
|
+
// Presidio raises a context-supported score by 0.35 with a 0.4 floor;
|
|
146
|
+
// a negative boost models Google's "decrease likelihood" hotwords.
|
|
147
|
+
confidence =
|
|
148
|
+
spec.boost >= 0
|
|
149
|
+
? Math.max(0.4, confidence + spec.boost)
|
|
150
|
+
: confidence + spec.boost;
|
|
151
|
+
}
|
|
152
|
+
if (spec.validate) {
|
|
153
|
+
const verdict = spec.validate(value);
|
|
154
|
+
if (verdict === false) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
if (typeof verdict === 'number' && score(verdict)) {
|
|
158
|
+
confidence = verdict;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return Math.min(1, Math.max(0, confidence));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function* matches(spec, text) {
|
|
165
|
+
for (const pattern of spec.patterns) {
|
|
166
|
+
pattern.regex.lastIndex = 0;
|
|
167
|
+
for (const match of text.matchAll(pattern.regex)) {
|
|
168
|
+
const value = match[pattern.group];
|
|
169
|
+
if (!value) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const start = match.index + (pattern.group ? match[0].indexOf(value) : 0);
|
|
173
|
+
yield {
|
|
174
|
+
start,
|
|
175
|
+
end: start + value.length,
|
|
176
|
+
value,
|
|
177
|
+
base: pattern.confidence ?? spec.confidence,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Builds a synchronous Detector from declarative specs. Each spec matches
|
|
185
|
+
* `patterns` (RegExp, string sources or `{regex, confidence, group}`) and/or
|
|
186
|
+
* a whole-word `denyList`, raises or lowers confidence when a `context` word
|
|
187
|
+
* occurs within `contextWindow`, and lets `validate(value)` reject a match
|
|
188
|
+
* (false) or set its confidence (0–1). Patterns are trusted configuration:
|
|
189
|
+
* avoid catastrophic backtracking in caller-supplied expressions.
|
|
190
|
+
*/
|
|
191
|
+
export function createPatternDetector(specs, { id = 'pattern-rules' } = {}) {
|
|
192
|
+
const list = Array.isArray(specs) ? specs : [specs];
|
|
193
|
+
if (!list.length || list.length > maxPatterns) {
|
|
194
|
+
throw failure('ERR_CONFIG');
|
|
195
|
+
}
|
|
196
|
+
const compiled = list.map(compileSpec);
|
|
197
|
+
return {
|
|
198
|
+
id,
|
|
199
|
+
detect(text) {
|
|
200
|
+
const findings = [];
|
|
201
|
+
for (const spec of compiled) {
|
|
202
|
+
for (const { start, end, value, base } of matches(spec, text)) {
|
|
203
|
+
const confidence = scored(spec, base, text, start, end, value);
|
|
204
|
+
if (confidence !== undefined) {
|
|
205
|
+
findings.push({
|
|
206
|
+
start,
|
|
207
|
+
end,
|
|
208
|
+
type: spec.type,
|
|
209
|
+
category: spec.category,
|
|
210
|
+
rule: spec.rule,
|
|
211
|
+
confidence,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return findings;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
package/src/personal-context.js
CHANGED
|
@@ -6,8 +6,9 @@ export const months =
|
|
|
6
6
|
'January February March April May June July August September October November December Jan Feb Mar Apr Jun Jul Aug Sep Sept Oct Nov Dec января февраля марта апреля мая июня июля августа сентября октября ноября декабря январь февраль март апрель май июнь июль август сентябрь октябрь ноябрь декабрь'
|
|
7
7
|
.split(' ')
|
|
8
8
|
.join('|');
|
|
9
|
+
const ordinal = '(?:st|nd|rd|th)?';
|
|
9
10
|
const dates = new RegExp(
|
|
10
|
-
`(?<![\\p{L}\\p{N}_])(?:\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[
|
|
11
|
+
`(?<![\\p{L}\\p{N}_])(?:\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[/.-]\\d{1,2}[/.-]\\d{4}|\\d{1,2}${ordinal}[ \\t]+(?:of[ \\t]+)?(?:${months})\\.?,?[ \\t]+\\d{4}|(?:${months})\\.?[ \\t]+\\d{1,2}${ordinal},?[ \\t]+\\d{4})(?![\\p{L}\\p{N}_])`,
|
|
11
12
|
'giu'
|
|
12
13
|
);
|
|
13
14
|
const phoneContext =
|
|
@@ -30,7 +31,7 @@ export function detectPersonalContext(text, emit) {
|
|
|
30
31
|
);
|
|
31
32
|
collectMatches(
|
|
32
33
|
text,
|
|
33
|
-
/(?<![\p{L}\p{N}_])(?:phone|fax|ĐT|điện thoại)(?:[ \t]*[::][ \t]*|[ \t]+)(\+?\(?\d[\d() .-]{5,38}\d)(?![\p{L}\p{N}_])/giu,
|
|
34
|
+
/(?<![\p{L}\p{N}_])(?:(?:phone|fax|ĐT|điện thoại)(?:[ \t]*[::][ \t]*|[ \t]+)|(?:telephone|tel|mobile|mob|cell|desk|ph)\.?[ \t]*[::][ \t]*)(\+?\(?\d[\d() .-]{5,38}\d)(?![\p{L}\p{N}_])/giu,
|
|
34
35
|
emit,
|
|
35
36
|
phone,
|
|
36
37
|
1,
|