euredact 0.3.5 → 0.3.6
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/dist/cjs/rules/suppressors.js +80 -11
- package/dist/esm/rules/suppressors.js +80 -11
- package/package.json +5 -1
|
@@ -12,15 +12,29 @@ function getContext(text, start, end) {
|
|
|
12
12
|
}
|
|
13
13
|
// Unicode-safe word boundary: negative lookahead for any letter (including accented)
|
|
14
14
|
const _UWB = "(?![a-zA-Z\\u00C0-\\u024F\\u0400-\\u04FF])";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// Symbols are held apart from the alphabetic codes because a word boundary
|
|
16
|
+
// after "€" is meaningless — the symbol is not a word character. Python used a
|
|
17
|
+
// bare "\b" here and so could never match "1163 €," at all; this port used the
|
|
18
|
+
// lookahead below and did not have that defect. Kept split so the difference
|
|
19
|
+
// cannot come back.
|
|
20
|
+
const CURRENCY_SYMBOL = "€|\\$|£|¥|₺|zł|Kč|лв|kn|Ft|₽";
|
|
21
|
+
const CURRENCY_WORD = "EUR|USD|GBP|CHF|ISK|SEK|NOK|DKK|PLN|CZK|HUF|RON|BGN|HRK|" +
|
|
22
|
+
"euro|euros|dollar|dollars|pond|kronor|kroner|kronur|kr|" +
|
|
23
|
+
"złoty|korun|forint|lei|leva";
|
|
24
|
+
const CURRENCY_AFTER = new RegExp(`^\\s*(?:(?:${CURRENCY_SYMBOL})|(?:${CURRENCY_WORD})${_UWB})`, "i");
|
|
25
|
+
const CURRENCY_COMMA_AFTER = new RegExp(`^[.,]\\d{1,2}\\s*(?:(?:${CURRENCY_SYMBOL})|(?:${CURRENCY_WORD})${_UWB})`, "i");
|
|
26
|
+
const CURRENCY_BEFORE = new RegExp(`(?:(?:${CURRENCY_SYMBOL})|\\b(?:${CURRENCY_WORD}))\\s*$`, "i");
|
|
27
|
+
const AMOUNT_LABEL_BEFORE = /\b(?:Montant|Beløb|Summa|Summe|Bedrag|Amount|Total|TTC|inkl|Upphæð|Importe|Importo|Valore|Wartość|Kwota|Částka|Összeg|Sum|Beloop|Prix|Preis|Price|Loyer|Miete|Huur|Rent|Betrag)\s*(?:\w+\s*)?:?\s*$/i;
|
|
19
28
|
// Use Unicode-aware word boundary via \p{L} negative lookahead instead of \b
|
|
20
29
|
// because JS \b is ASCII-only and fails on Unicode letters (e.g. "München" → \bm\b matches M before ü)
|
|
21
30
|
const UNIT_AFTER = /^\s*(?:kg|km|cm|mm|m[²³]|ml|mg|GB|MB|KB|TB|%|jaar|maanden|weken|dagen|uur|minuten|seconden|stuks|pcs|pieces|ans|mois|semaines|jours|heures|Jahre|Monate|Wochen|Tage|Stunden)(?![a-zA-Z\u00C0-\u024F\u0400-\u04FF])/i;
|
|
22
31
|
// Same missing-boundary flaw: "ref" matched the tail of "kortref".
|
|
23
|
-
const REFERENCE_BEFORE = /\b(?:dossier|ref\.?|referentie|reference|référence|factuurnummer|invoice\s*(?:nr|number|no)?|bestelnummer|order\s*(?:nr|number|no)?|kenmerk|ordernummer|Aktenzeichen|numéro\s*de\s*(?:dossier|facture|commande)|bestellnummer|Rechnungsnummer|artikelnr|article\s*no|contract\s*(?:nr|number|no)?|pagina|page|Seite|blz\.?|Facture\s*n[°o]?|Faktura\s*n[°or]\.?|Lasku\s*n[°or]o?\.?|Rechnung\s*(?:Nr|n[°o])?|faktura\s*(?:nr|n[°o])?|bestilling\s*(?:nr|n[°o])?|bestelling\s*n[°or]\.?|Reikningur\s*nr|Factuur\s*n[ro]?\.?|Nota\s*n[ro]?\.?)\s*[
|
|
32
|
+
const REFERENCE_BEFORE = /\b(?:dossier|ref\.?|referentie|reference|référence|factuurnummer|invoice\s*(?:nr|number|no)?|bestelnummer|order\s*(?:nr|number|no)?|kenmerk|ordernummer|Aktenzeichen|numéro\s*de\s*(?:dossier|facture|commande)|bestellnummer|Rechnungsnummer|artikelnr|article\s*no|contract\s*(?:nr|number|no)?|pagina|page|Seite|blz\.?|Facture\s*n[°o]?|Faktura\s*n[°or]\.?|Lasku\s*n[°or]o?\.?|Rechnung\s*(?:Nr|n[°o])?|faktura\s*(?:nr|n[°o])?|bestilling\s*(?:nr|n[°o])?|bestelling\s*n[°or]\.?|Reikningur\s*nr|ticket|incident\s*(?:report|nr|no)?|case\s*(?:nr|no|id)?|zaaknummer|meldingsnummer|Vorgangsnummer|Vorgang|Störungsmeldung|saksnummer|ärendenummer|sagsnummer|asianumero|Factuur\s*n[ro]?\.?|Nota\s*n[ro]?\.?)\s*[:.#]?\s*$/i;
|
|
33
|
+
// "#" before a number marks a reference in every language in scope — a ticket,
|
|
34
|
+
// an order, a line item — and never a postcode or a national identifier.
|
|
35
|
+
const HASH_BEFORE = /#\s*$/;
|
|
36
|
+
// A short uppercase tag hyphenated to the number: "IR-43433", "INC-2024".
|
|
37
|
+
const REF_PREFIX_BEFORE = /(?:^|[\s([])[A-Z]{2,5}-$/;
|
|
24
38
|
const LEGAL_BEFORE = /(?:Art(?:ikel|icle|\.)|§|Artikel|Section|Sectie|Afdeling|paragraaf|Absatz|alinéa|punt|point|Punkt|lid)\s*$/i;
|
|
25
39
|
const MATH_BEFORE = /[=+\-×÷*/]\s*$/;
|
|
26
40
|
const MATH_AFTER = /^\s*[=+\-×÷*/]/;
|
|
@@ -52,7 +66,11 @@ const MONTH_NAMES = "january|february|march|april|may|june|july|august|september
|
|
|
52
66
|
"|gennaio|febbraio|marzo|aprile|maggio|giugno|luglio|agosto|settembre|ottobre|dicembre" +
|
|
53
67
|
"|enero|febrero|abril|mayo|junio|julio|septiembre|octubre|noviembre|diciembre" +
|
|
54
68
|
"|janeiro|fevereiro|março|marco|maio|junho|julho|setembro|outubro|novembro|dezembro" +
|
|
55
|
-
"
|
|
69
|
+
// "desember" (nb/nn/is) was the one December spelling missing from a list
|
|
70
|
+
// that already carried ten others — "1. desember 2025" was a Norwegian
|
|
71
|
+
// postcode because of it.
|
|
72
|
+
"|marts|augusti|desember" +
|
|
73
|
+
"|janúar|febrúar|apríl|maí|júní|júlí|ágúst|nóvember" +
|
|
56
74
|
"|tammikuu|helmikuu|maaliskuu|huhtikuu|toukokuu|kesäkuu|heinäkuu|elokuu|syyskuu|lokakuu|marraskuu|joulukuu" +
|
|
57
75
|
"|jaanuar|veebruar|aprill|juuni|juuli|oktoober|detsember" +
|
|
58
76
|
"|janvāris|februāris|aprīlis|maijs|jūnijs|jūlijs|augusts|septembris|oktobris|novembris|decembris" +
|
|
@@ -77,7 +95,11 @@ const DATE_AFTER = /^[./-]\d{1,2}[./-]\d{1,2}\b/;
|
|
|
77
95
|
* ISO 4217 codes made only of the consonants a Spanish plate accepts, so a money
|
|
78
96
|
* amount reads as a registration. This is why "2297 DKK" was a plate.
|
|
79
97
|
*/
|
|
80
|
-
|
|
98
|
+
// Crypto tickers are the same shape and the same mistake — every surviving
|
|
99
|
+
// plate false positive was "4499 BTC" or a sibling. A ticker is a unit, exactly
|
|
100
|
+
// as an ISO 4217 code is.
|
|
101
|
+
const CURRENCY_CODE = "(?:DKK|SEK|NOK|ISK|CZK|PLN|HUF|RON|BGN|HRK|CHF|GBP|TRY|RSD|MKD|BYN|KZT|CNY|JPY|KRW|ZAR|BRL|MXN|CLP|COP|PLZ|SKK|TRL" +
|
|
102
|
+
"|BTC|ETH|LTC|XRP|BCH|XLM|XMR|DOT|SOL|ADA|TRX|DOGE|USDT|USDC)";
|
|
81
103
|
const CURRENCY_CODE_TAIL = new RegExp(`\\s${CURRENCY_CODE}$`);
|
|
82
104
|
const CURRENCY_CODE_AFTER = new RegExp(`^\\s*${CURRENCY_CODE}\\b`);
|
|
83
105
|
/** Log-timestamp tails, which the ":"-anchored secret rule picks up. */
|
|
@@ -91,6 +113,32 @@ const NOT_A_SECRET = new RegExp("^(?:af|ap|ca|cn|eu|il|me|sa|us|gov)-(?:north|so
|
|
|
91
113
|
const EXACT_UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
92
114
|
const BIC_SHAPE = /^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$/;
|
|
93
115
|
const BIC_CUE_BEFORE = /(?:BIC|SWIFT|BIC\/SWIFT)\s*[:=]?\s*\(?\s*$/i;
|
|
116
|
+
/** An email address, exactly. Same argument as the UUID and the BIC. */
|
|
117
|
+
const EXACT_EMAIL = /^[\w.!#$%&'*+/=?^`{|}~-]+@[\w-]+(?:\.[\w-]+)+$/;
|
|
118
|
+
/**
|
|
119
|
+
* A URL, or the tail of one after the scheme has been split off. The secret
|
|
120
|
+
* rules anchor on ":" and "=", so "https://api.sendgrid.com/v3/mail/send" hands
|
|
121
|
+
* them "//api.sendgrid.com/v3/mail/send" and a slash-separated path scores as
|
|
122
|
+
* high-entropy. An endpoint is published documentation, not a credential.
|
|
123
|
+
*/
|
|
124
|
+
const URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^\s/]+(?:\/\S*)?$|^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,}(?::\d{1,5})?(?:\/\S*)?$/i;
|
|
125
|
+
/**
|
|
126
|
+
* A URL that *carries* a credential is the opposite case, and it is the common
|
|
127
|
+
* one: "mongodb://admin:DFKDKi1eb51OOhuHPYz@rds-main.eu-west-1.rds.amazonaws.com"
|
|
128
|
+
* is a connection string with a live password in it. Suppressing those as "just
|
|
129
|
+
* a URL" lost 347 real secrets, so the endpoint test must not fire when userinfo
|
|
130
|
+
* or a credential-bearing query parameter is present.
|
|
131
|
+
*/
|
|
132
|
+
const URL_WITH_CREDENTIALS = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^/@\s]*:[^/@\s]*@|[?&](?:api_?key|access_?token|auth|token|secret|password|pwd|sig|signature|credential)=/i;
|
|
133
|
+
/** An LDAP distinguished name: "cn=github-actions,dc=corp,dc=eu". */
|
|
134
|
+
const LDAP_DN = /^(?:cn|ou|dc|uid|o|l|st|c)=[^,=]+(?:,\s*(?:cn|ou|dc|uid|o|l|st|c)=[^,=]+)+$/i;
|
|
135
|
+
/**
|
|
136
|
+
* Lower-case words joined by hyphens, optionally with a short id suffix:
|
|
137
|
+
* "service-account", "data-lake-33e061". The single-word test below already
|
|
138
|
+
* rejects "sozialversicherungsnummer" on the reasoning that a real secret would
|
|
139
|
+
* be mixed case; a hyphenated compound is the same token with a separator.
|
|
140
|
+
*/
|
|
141
|
+
const HYPHENATED_WORDS = /^[a-z]{2,}(?:-[a-z]{2,})+(?:-[0-9a-f]{4,8})?$/;
|
|
94
142
|
/** A dotted quad with every octet in range. */
|
|
95
143
|
const DOTTED_QUAD = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
96
144
|
/** Two plausible years joined by a hyphen — a school year, a contract term. */
|
|
@@ -120,7 +168,7 @@ function suppressSecretOverStructured(text, match) {
|
|
|
120
168
|
if (match.patternDef.entityType !== types_js_1.EntityType.SECRET)
|
|
121
169
|
return false;
|
|
122
170
|
const token = coreToken(match.text);
|
|
123
|
-
if (EXACT_UUID.test(token))
|
|
171
|
+
if (EXACT_UUID.test(token) || EXACT_EMAIL.test(token))
|
|
124
172
|
return true;
|
|
125
173
|
if (BIC_SHAPE.test(token)) {
|
|
126
174
|
return BIC_CUE_BEFORE.test(text.slice(Math.max(0, match.start - 24), match.start));
|
|
@@ -145,6 +193,12 @@ function suppressSecretNotASecret(_text, match) {
|
|
|
145
193
|
return true;
|
|
146
194
|
if (NOT_A_SECRET.test(bare))
|
|
147
195
|
return true;
|
|
196
|
+
if (URL_LIKE.test(bare) && !URL_WITH_CREDENTIALS.test(bare))
|
|
197
|
+
return true;
|
|
198
|
+
if (LDAP_DN.test(bare))
|
|
199
|
+
return true;
|
|
200
|
+
if (HYPHENATED_WORDS.test(bare))
|
|
201
|
+
return true;
|
|
148
202
|
const alpha = /^[A-Za-zÀ-ÿ]+$/.test(bare);
|
|
149
203
|
const oneWord = bare === bare.toLowerCase() ||
|
|
150
204
|
(bare.slice(0, 1) === bare.slice(0, 1).toUpperCase() &&
|
|
@@ -205,12 +259,27 @@ function suppressUnits(text, match) {
|
|
|
205
259
|
const [, after] = getContext(text, match.start, match.end);
|
|
206
260
|
return UNIT_AFTER.test(after);
|
|
207
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Suppress numbers preceded by reference/invoice/dossier keywords.
|
|
264
|
+
*
|
|
265
|
+
* POSTAL_CODE belongs here for the same reason every other numeric type does,
|
|
266
|
+
* and its absence was an oversight: a five-digit ticket number sits in exactly
|
|
267
|
+
* the shape a German or French postcode occupies, so "Ticket #94730" and
|
|
268
|
+
* "Incident report IR-43433" were masked as addresses.
|
|
269
|
+
*
|
|
270
|
+
* "#" and "XX-" are checked adjacently rather than through the 150-character
|
|
271
|
+
* keyword window. That window is what made the postal rule claim years in
|
|
272
|
+
* dates; widening its reach to fix a different symptom would repeat the error.
|
|
273
|
+
*/
|
|
208
274
|
function suppressReference(text, match) {
|
|
209
|
-
const applicable = new Set([types_js_1.EntityType.PHONE, types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.SSN, types_js_1.EntityType.TAX_ID, types_js_1.EntityType.IBAN, types_js_1.EntityType.CHAMBER_OF_COMMERCE]);
|
|
275
|
+
const applicable = new Set([types_js_1.EntityType.PHONE, types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.SSN, types_js_1.EntityType.TAX_ID, types_js_1.EntityType.IBAN, types_js_1.EntityType.CHAMBER_OF_COMMERCE, types_js_1.EntityType.POSTAL_CODE]);
|
|
210
276
|
if (!applicable.has(match.patternDef.entityType))
|
|
211
277
|
return false;
|
|
212
278
|
const [before] = getContext(text, match.start, match.end);
|
|
213
|
-
|
|
279
|
+
if (REFERENCE_BEFORE.test(before))
|
|
280
|
+
return true;
|
|
281
|
+
const adjacent = text.slice(Math.max(0, match.start - 8), match.start);
|
|
282
|
+
return HASH_BEFORE.test(adjacent) || REF_PREFIX_BEFORE.test(adjacent);
|
|
214
283
|
}
|
|
215
284
|
function suppressLegal(text, match) {
|
|
216
285
|
const applicable = new Set([types_js_1.EntityType.PHONE, types_js_1.EntityType.NATIONAL_ID, types_js_1.EntityType.POSTAL_CODE]);
|
|
@@ -664,7 +733,7 @@ const TYPE_SUPPRESSORS = {
|
|
|
664
733
|
[types_js_1.EntityType.NATIONAL_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressLegal, suppressMath, suppressNatidAsPassport, suppressSeNatidAsOrg],
|
|
665
734
|
[types_js_1.EntityType.SSN]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
|
|
666
735
|
[types_js_1.EntityType.TAX_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressTaxidAsIpAddress],
|
|
667
|
-
[types_js_1.EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier],
|
|
736
|
+
[types_js_1.EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier, suppressReference],
|
|
668
737
|
[types_js_1.EntityType.BIC]: [suppressBicWithoutEvidence],
|
|
669
738
|
[types_js_1.EntityType.BANK_ACCOUNT]: [suppressReference],
|
|
670
739
|
[types_js_1.EntityType.LICENSE_PLATE]: [suppressPlateInCompound, suppressDePlateUnknownDistrict, suppressPlateAsCurrencyAmount],
|
|
@@ -9,15 +9,29 @@ function getContext(text, start, end) {
|
|
|
9
9
|
}
|
|
10
10
|
// Unicode-safe word boundary: negative lookahead for any letter (including accented)
|
|
11
11
|
const _UWB = "(?![a-zA-Z\\u00C0-\\u024F\\u0400-\\u04FF])";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
// Symbols are held apart from the alphabetic codes because a word boundary
|
|
13
|
+
// after "€" is meaningless — the symbol is not a word character. Python used a
|
|
14
|
+
// bare "\b" here and so could never match "1163 €," at all; this port used the
|
|
15
|
+
// lookahead below and did not have that defect. Kept split so the difference
|
|
16
|
+
// cannot come back.
|
|
17
|
+
const CURRENCY_SYMBOL = "€|\\$|£|¥|₺|zł|Kč|лв|kn|Ft|₽";
|
|
18
|
+
const CURRENCY_WORD = "EUR|USD|GBP|CHF|ISK|SEK|NOK|DKK|PLN|CZK|HUF|RON|BGN|HRK|" +
|
|
19
|
+
"euro|euros|dollar|dollars|pond|kronor|kroner|kronur|kr|" +
|
|
20
|
+
"złoty|korun|forint|lei|leva";
|
|
21
|
+
const CURRENCY_AFTER = new RegExp(`^\\s*(?:(?:${CURRENCY_SYMBOL})|(?:${CURRENCY_WORD})${_UWB})`, "i");
|
|
22
|
+
const CURRENCY_COMMA_AFTER = new RegExp(`^[.,]\\d{1,2}\\s*(?:(?:${CURRENCY_SYMBOL})|(?:${CURRENCY_WORD})${_UWB})`, "i");
|
|
23
|
+
const CURRENCY_BEFORE = new RegExp(`(?:(?:${CURRENCY_SYMBOL})|\\b(?:${CURRENCY_WORD}))\\s*$`, "i");
|
|
24
|
+
const AMOUNT_LABEL_BEFORE = /\b(?:Montant|Beløb|Summa|Summe|Bedrag|Amount|Total|TTC|inkl|Upphæð|Importe|Importo|Valore|Wartość|Kwota|Částka|Összeg|Sum|Beloop|Prix|Preis|Price|Loyer|Miete|Huur|Rent|Betrag)\s*(?:\w+\s*)?:?\s*$/i;
|
|
16
25
|
// Use Unicode-aware word boundary via \p{L} negative lookahead instead of \b
|
|
17
26
|
// because JS \b is ASCII-only and fails on Unicode letters (e.g. "München" → \bm\b matches M before ü)
|
|
18
27
|
const UNIT_AFTER = /^\s*(?:kg|km|cm|mm|m[²³]|ml|mg|GB|MB|KB|TB|%|jaar|maanden|weken|dagen|uur|minuten|seconden|stuks|pcs|pieces|ans|mois|semaines|jours|heures|Jahre|Monate|Wochen|Tage|Stunden)(?![a-zA-Z\u00C0-\u024F\u0400-\u04FF])/i;
|
|
19
28
|
// Same missing-boundary flaw: "ref" matched the tail of "kortref".
|
|
20
|
-
const REFERENCE_BEFORE = /\b(?:dossier|ref\.?|referentie|reference|référence|factuurnummer|invoice\s*(?:nr|number|no)?|bestelnummer|order\s*(?:nr|number|no)?|kenmerk|ordernummer|Aktenzeichen|numéro\s*de\s*(?:dossier|facture|commande)|bestellnummer|Rechnungsnummer|artikelnr|article\s*no|contract\s*(?:nr|number|no)?|pagina|page|Seite|blz\.?|Facture\s*n[°o]?|Faktura\s*n[°or]\.?|Lasku\s*n[°or]o?\.?|Rechnung\s*(?:Nr|n[°o])?|faktura\s*(?:nr|n[°o])?|bestilling\s*(?:nr|n[°o])?|bestelling\s*n[°or]\.?|Reikningur\s*nr|Factuur\s*n[ro]?\.?|Nota\s*n[ro]?\.?)\s*[
|
|
29
|
+
const REFERENCE_BEFORE = /\b(?:dossier|ref\.?|referentie|reference|référence|factuurnummer|invoice\s*(?:nr|number|no)?|bestelnummer|order\s*(?:nr|number|no)?|kenmerk|ordernummer|Aktenzeichen|numéro\s*de\s*(?:dossier|facture|commande)|bestellnummer|Rechnungsnummer|artikelnr|article\s*no|contract\s*(?:nr|number|no)?|pagina|page|Seite|blz\.?|Facture\s*n[°o]?|Faktura\s*n[°or]\.?|Lasku\s*n[°or]o?\.?|Rechnung\s*(?:Nr|n[°o])?|faktura\s*(?:nr|n[°o])?|bestilling\s*(?:nr|n[°o])?|bestelling\s*n[°or]\.?|Reikningur\s*nr|ticket|incident\s*(?:report|nr|no)?|case\s*(?:nr|no|id)?|zaaknummer|meldingsnummer|Vorgangsnummer|Vorgang|Störungsmeldung|saksnummer|ärendenummer|sagsnummer|asianumero|Factuur\s*n[ro]?\.?|Nota\s*n[ro]?\.?)\s*[:.#]?\s*$/i;
|
|
30
|
+
// "#" before a number marks a reference in every language in scope — a ticket,
|
|
31
|
+
// an order, a line item — and never a postcode or a national identifier.
|
|
32
|
+
const HASH_BEFORE = /#\s*$/;
|
|
33
|
+
// A short uppercase tag hyphenated to the number: "IR-43433", "INC-2024".
|
|
34
|
+
const REF_PREFIX_BEFORE = /(?:^|[\s([])[A-Z]{2,5}-$/;
|
|
21
35
|
const LEGAL_BEFORE = /(?:Art(?:ikel|icle|\.)|§|Artikel|Section|Sectie|Afdeling|paragraaf|Absatz|alinéa|punt|point|Punkt|lid)\s*$/i;
|
|
22
36
|
const MATH_BEFORE = /[=+\-×÷*/]\s*$/;
|
|
23
37
|
const MATH_AFTER = /^\s*[=+\-×÷*/]/;
|
|
@@ -49,7 +63,11 @@ const MONTH_NAMES = "january|february|march|april|may|june|july|august|september
|
|
|
49
63
|
"|gennaio|febbraio|marzo|aprile|maggio|giugno|luglio|agosto|settembre|ottobre|dicembre" +
|
|
50
64
|
"|enero|febrero|abril|mayo|junio|julio|septiembre|octubre|noviembre|diciembre" +
|
|
51
65
|
"|janeiro|fevereiro|março|marco|maio|junho|julho|setembro|outubro|novembro|dezembro" +
|
|
52
|
-
"
|
|
66
|
+
// "desember" (nb/nn/is) was the one December spelling missing from a list
|
|
67
|
+
// that already carried ten others — "1. desember 2025" was a Norwegian
|
|
68
|
+
// postcode because of it.
|
|
69
|
+
"|marts|augusti|desember" +
|
|
70
|
+
"|janúar|febrúar|apríl|maí|júní|júlí|ágúst|nóvember" +
|
|
53
71
|
"|tammikuu|helmikuu|maaliskuu|huhtikuu|toukokuu|kesäkuu|heinäkuu|elokuu|syyskuu|lokakuu|marraskuu|joulukuu" +
|
|
54
72
|
"|jaanuar|veebruar|aprill|juuni|juuli|oktoober|detsember" +
|
|
55
73
|
"|janvāris|februāris|aprīlis|maijs|jūnijs|jūlijs|augusts|septembris|oktobris|novembris|decembris" +
|
|
@@ -74,7 +92,11 @@ const DATE_AFTER = /^[./-]\d{1,2}[./-]\d{1,2}\b/;
|
|
|
74
92
|
* ISO 4217 codes made only of the consonants a Spanish plate accepts, so a money
|
|
75
93
|
* amount reads as a registration. This is why "2297 DKK" was a plate.
|
|
76
94
|
*/
|
|
77
|
-
|
|
95
|
+
// Crypto tickers are the same shape and the same mistake — every surviving
|
|
96
|
+
// plate false positive was "4499 BTC" or a sibling. A ticker is a unit, exactly
|
|
97
|
+
// as an ISO 4217 code is.
|
|
98
|
+
const CURRENCY_CODE = "(?:DKK|SEK|NOK|ISK|CZK|PLN|HUF|RON|BGN|HRK|CHF|GBP|TRY|RSD|MKD|BYN|KZT|CNY|JPY|KRW|ZAR|BRL|MXN|CLP|COP|PLZ|SKK|TRL" +
|
|
99
|
+
"|BTC|ETH|LTC|XRP|BCH|XLM|XMR|DOT|SOL|ADA|TRX|DOGE|USDT|USDC)";
|
|
78
100
|
const CURRENCY_CODE_TAIL = new RegExp(`\\s${CURRENCY_CODE}$`);
|
|
79
101
|
const CURRENCY_CODE_AFTER = new RegExp(`^\\s*${CURRENCY_CODE}\\b`);
|
|
80
102
|
/** Log-timestamp tails, which the ":"-anchored secret rule picks up. */
|
|
@@ -88,6 +110,32 @@ const NOT_A_SECRET = new RegExp("^(?:af|ap|ca|cn|eu|il|me|sa|us|gov)-(?:north|so
|
|
|
88
110
|
const EXACT_UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
89
111
|
const BIC_SHAPE = /^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$/;
|
|
90
112
|
const BIC_CUE_BEFORE = /(?:BIC|SWIFT|BIC\/SWIFT)\s*[:=]?\s*\(?\s*$/i;
|
|
113
|
+
/** An email address, exactly. Same argument as the UUID and the BIC. */
|
|
114
|
+
const EXACT_EMAIL = /^[\w.!#$%&'*+/=?^`{|}~-]+@[\w-]+(?:\.[\w-]+)+$/;
|
|
115
|
+
/**
|
|
116
|
+
* A URL, or the tail of one after the scheme has been split off. The secret
|
|
117
|
+
* rules anchor on ":" and "=", so "https://api.sendgrid.com/v3/mail/send" hands
|
|
118
|
+
* them "//api.sendgrid.com/v3/mail/send" and a slash-separated path scores as
|
|
119
|
+
* high-entropy. An endpoint is published documentation, not a credential.
|
|
120
|
+
*/
|
|
121
|
+
const URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^\s/]+(?:\/\S*)?$|^[a-z0-9-]+(?:\.[a-z0-9-]+)*\.[a-z]{2,}(?::\d{1,5})?(?:\/\S*)?$/i;
|
|
122
|
+
/**
|
|
123
|
+
* A URL that *carries* a credential is the opposite case, and it is the common
|
|
124
|
+
* one: "mongodb://admin:DFKDKi1eb51OOhuHPYz@rds-main.eu-west-1.rds.amazonaws.com"
|
|
125
|
+
* is a connection string with a live password in it. Suppressing those as "just
|
|
126
|
+
* a URL" lost 347 real secrets, so the endpoint test must not fire when userinfo
|
|
127
|
+
* or a credential-bearing query parameter is present.
|
|
128
|
+
*/
|
|
129
|
+
const URL_WITH_CREDENTIALS = /^(?:[a-z][a-z0-9+.-]*:)?\/\/[^/@\s]*:[^/@\s]*@|[?&](?:api_?key|access_?token|auth|token|secret|password|pwd|sig|signature|credential)=/i;
|
|
130
|
+
/** An LDAP distinguished name: "cn=github-actions,dc=corp,dc=eu". */
|
|
131
|
+
const LDAP_DN = /^(?:cn|ou|dc|uid|o|l|st|c)=[^,=]+(?:,\s*(?:cn|ou|dc|uid|o|l|st|c)=[^,=]+)+$/i;
|
|
132
|
+
/**
|
|
133
|
+
* Lower-case words joined by hyphens, optionally with a short id suffix:
|
|
134
|
+
* "service-account", "data-lake-33e061". The single-word test below already
|
|
135
|
+
* rejects "sozialversicherungsnummer" on the reasoning that a real secret would
|
|
136
|
+
* be mixed case; a hyphenated compound is the same token with a separator.
|
|
137
|
+
*/
|
|
138
|
+
const HYPHENATED_WORDS = /^[a-z]{2,}(?:-[a-z]{2,})+(?:-[0-9a-f]{4,8})?$/;
|
|
91
139
|
/** A dotted quad with every octet in range. */
|
|
92
140
|
const DOTTED_QUAD = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
93
141
|
/** Two plausible years joined by a hyphen — a school year, a contract term. */
|
|
@@ -117,7 +165,7 @@ function suppressSecretOverStructured(text, match) {
|
|
|
117
165
|
if (match.patternDef.entityType !== EntityType.SECRET)
|
|
118
166
|
return false;
|
|
119
167
|
const token = coreToken(match.text);
|
|
120
|
-
if (EXACT_UUID.test(token))
|
|
168
|
+
if (EXACT_UUID.test(token) || EXACT_EMAIL.test(token))
|
|
121
169
|
return true;
|
|
122
170
|
if (BIC_SHAPE.test(token)) {
|
|
123
171
|
return BIC_CUE_BEFORE.test(text.slice(Math.max(0, match.start - 24), match.start));
|
|
@@ -142,6 +190,12 @@ function suppressSecretNotASecret(_text, match) {
|
|
|
142
190
|
return true;
|
|
143
191
|
if (NOT_A_SECRET.test(bare))
|
|
144
192
|
return true;
|
|
193
|
+
if (URL_LIKE.test(bare) && !URL_WITH_CREDENTIALS.test(bare))
|
|
194
|
+
return true;
|
|
195
|
+
if (LDAP_DN.test(bare))
|
|
196
|
+
return true;
|
|
197
|
+
if (HYPHENATED_WORDS.test(bare))
|
|
198
|
+
return true;
|
|
145
199
|
const alpha = /^[A-Za-zÀ-ÿ]+$/.test(bare);
|
|
146
200
|
const oneWord = bare === bare.toLowerCase() ||
|
|
147
201
|
(bare.slice(0, 1) === bare.slice(0, 1).toUpperCase() &&
|
|
@@ -202,12 +256,27 @@ function suppressUnits(text, match) {
|
|
|
202
256
|
const [, after] = getContext(text, match.start, match.end);
|
|
203
257
|
return UNIT_AFTER.test(after);
|
|
204
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Suppress numbers preceded by reference/invoice/dossier keywords.
|
|
261
|
+
*
|
|
262
|
+
* POSTAL_CODE belongs here for the same reason every other numeric type does,
|
|
263
|
+
* and its absence was an oversight: a five-digit ticket number sits in exactly
|
|
264
|
+
* the shape a German or French postcode occupies, so "Ticket #94730" and
|
|
265
|
+
* "Incident report IR-43433" were masked as addresses.
|
|
266
|
+
*
|
|
267
|
+
* "#" and "XX-" are checked adjacently rather than through the 150-character
|
|
268
|
+
* keyword window. That window is what made the postal rule claim years in
|
|
269
|
+
* dates; widening its reach to fix a different symptom would repeat the error.
|
|
270
|
+
*/
|
|
205
271
|
function suppressReference(text, match) {
|
|
206
|
-
const applicable = new Set([EntityType.PHONE, EntityType.NATIONAL_ID, EntityType.SSN, EntityType.TAX_ID, EntityType.IBAN, EntityType.CHAMBER_OF_COMMERCE]);
|
|
272
|
+
const applicable = new Set([EntityType.PHONE, EntityType.NATIONAL_ID, EntityType.SSN, EntityType.TAX_ID, EntityType.IBAN, EntityType.CHAMBER_OF_COMMERCE, EntityType.POSTAL_CODE]);
|
|
207
273
|
if (!applicable.has(match.patternDef.entityType))
|
|
208
274
|
return false;
|
|
209
275
|
const [before] = getContext(text, match.start, match.end);
|
|
210
|
-
|
|
276
|
+
if (REFERENCE_BEFORE.test(before))
|
|
277
|
+
return true;
|
|
278
|
+
const adjacent = text.slice(Math.max(0, match.start - 8), match.start);
|
|
279
|
+
return HASH_BEFORE.test(adjacent) || REF_PREFIX_BEFORE.test(adjacent);
|
|
211
280
|
}
|
|
212
281
|
function suppressLegal(text, match) {
|
|
213
282
|
const applicable = new Set([EntityType.PHONE, EntityType.NATIONAL_ID, EntityType.POSTAL_CODE]);
|
|
@@ -661,7 +730,7 @@ const TYPE_SUPPRESSORS = {
|
|
|
661
730
|
[EntityType.NATIONAL_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressLegal, suppressMath, suppressNatidAsPassport, suppressSeNatidAsOrg],
|
|
662
731
|
[EntityType.SSN]: [suppressCurrency, suppressUnits, suppressReference, suppressMath],
|
|
663
732
|
[EntityType.TAX_ID]: [suppressCurrency, suppressUnits, suppressReference, suppressMath, suppressTaxidAsIpAddress],
|
|
664
|
-
[EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier],
|
|
733
|
+
[EntityType.POSTAL_CODE]: [suppressCurrency, suppressUnits, suppressMath, suppressLegal, suppressYearAsPostal, suppressPostalInsideIban, suppressPostalAsHouseNumber, suppressPostalInLongerIdentifier, suppressReference],
|
|
665
734
|
[EntityType.BIC]: [suppressBicWithoutEvidence],
|
|
666
735
|
[EntityType.BANK_ACCOUNT]: [suppressReference],
|
|
667
736
|
[EntityType.LICENSE_PLATE]: [suppressPlateInCompound, suppressDePlateUnknownDistrict, suppressPlateAsCurrencyAmount],
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euredact",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "European PII redaction SDK -- fast, zero-dependency, rule-based detection and redaction of personal data across 31 European countries",
|
|
5
|
+
"homepage": "https://euredact.dev",
|
|
5
6
|
"license": "Apache-2.0",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"main": "./dist/cjs/index.js",
|
|
@@ -66,5 +67,8 @@
|
|
|
66
67
|
"devDependencies": {
|
|
67
68
|
"typescript": "^5.4.0",
|
|
68
69
|
"tsx": "^4.7.0"
|
|
70
|
+
},
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/euredact/euredact/issues"
|
|
69
73
|
}
|
|
70
74
|
}
|