@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.12.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 +578 -0
- package/LICENSE +24 -0
- package/README.md +184 -2
- package/bin/legacy-example-package-name.js +95 -0
- package/bin/sensitive-data-sanitizer.js +430 -0
- package/bin/stream.js +56 -0
- package/docs/API.md +336 -0
- package/docs/COVERAGE.md +136 -0
- package/docs/HISTORY-REMEDIATION.md +41 -0
- package/examples/combined-scanners.mjs +23 -0
- package/examples/presidio-bridge.py +32 -0
- package/examples/realistic-fakes.mjs +18 -0
- package/examples/sanitize-session.mjs +27 -0
- package/examples/shared-tool-logs.mjs +21 -0
- package/examples/stream-jsonl.mjs +17 -0
- package/package.json +124 -4
- package/src/adapters.js +204 -0
- package/src/arithmetic.js +11 -0
- package/src/confidence.js +65 -0
- package/src/context.js +424 -0
- package/src/credentials.js +54 -0
- package/src/detection.js +293 -0
- package/src/encoded.js +233 -0
- package/src/engines.js +348 -0
- package/src/entities.js +166 -0
- package/src/escape-spans.js +20 -0
- package/src/eslint-plugin.d.ts +10 -0
- package/src/eslint-plugin.js +193 -0
- package/src/external.js +169 -0
- package/src/fake-names.js +222 -0
- package/src/fake.js +524 -0
- package/src/files.js +131 -0
- package/src/gitleaks.js +194 -0
- package/src/history-rewrite.js +339 -0
- package/src/history.js +103 -0
- package/src/identity.js +274 -0
- package/src/index.d.ts +321 -0
- package/src/index.js +40 -0
- package/src/jsonl-stream.js +198 -0
- package/src/keywords.js +45 -0
- package/src/known.js +164 -0
- package/src/legacy.d.ts +3 -0
- package/src/legacy.js +2 -0
- package/src/locales.js +217 -0
- package/src/log-bytes.js +85 -0
- package/src/names.js +65 -0
- package/src/national-checks.js +129 -0
- package/src/national.js +128 -0
- package/src/outbound.js +28 -0
- package/src/personal-context.js +85 -0
- package/src/personal.js +248 -0
- package/src/projection.js +126 -0
- package/src/propagation.js +163 -0
- package/src/public-verifier.js +77 -0
- package/src/public.js +153 -0
- package/src/record-worker.js +20 -0
- package/src/rules.js +210 -0
- package/src/sanitizer.js +794 -0
- package/src/specimens.js +34 -0
- package/src/stream-identity.js +129 -0
- package/src/stream-worker.js +18 -0
- package/src/stream.js +449 -0
- package/src/structured.js +278 -0
- package/src/transforms.js +192 -0
- package/src/transliteration.js +75 -0
- package/src/utf8.js +60 -0
- package/src/vendor/entities/aws.json +42 -0
- package/src/vendor/entities/azure.json +182 -0
- package/src/vendor/entities/google.json +219 -0
- package/src/vendor/gitleaks/LICENSE +21 -0
- package/src/vendor/gitleaks/provenance.json +8 -0
- package/src/vendor/gitleaks/rules-0.json +268 -0
- package/src/vendor/gitleaks/rules-1.json +1794 -0
- package/src/vendor/gitleaks/rules-2.json +292 -0
- package/src/vendor/gitleaks/rules-3.json +263 -0
- package/src/vendor/gitleaks/rules-4.json +277 -0
- package/src/vendor/gitleaks/rules-5.json +154 -0
- package/src/worker-options.js +27 -0
package/src/log-bytes.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { TextDecoder } from 'node:util';
|
|
2
|
+
import { sequenceWidth, validSequence } from './utf8.js';
|
|
3
|
+
|
|
4
|
+
// Surrogate escape: invalid bytes use unpaired U+DC80..U+DCFF in the internal
|
|
5
|
+
// text view. Valid UTF-8 (including non-BMP characters) keeps its Unicode form.
|
|
6
|
+
// encodeLogText restores those bytes after sanitization; no lossy replacement.
|
|
7
|
+
export function decodeLogBytes(input) {
|
|
8
|
+
const bytes = Buffer.from(input);
|
|
9
|
+
try {
|
|
10
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(
|
|
11
|
+
bytes
|
|
12
|
+
);
|
|
13
|
+
} catch {
|
|
14
|
+
const parts = [];
|
|
15
|
+
let start = 0;
|
|
16
|
+
for (let i = 0; i < bytes.length; ) {
|
|
17
|
+
const width = sequenceWidth(bytes[i]);
|
|
18
|
+
if (validSequence(bytes, i, width)) {
|
|
19
|
+
i += width;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
parts.push(
|
|
23
|
+
bytes.toString('utf8', start, i),
|
|
24
|
+
String.fromCharCode(0xdc00 + bytes[i])
|
|
25
|
+
);
|
|
26
|
+
start = ++i;
|
|
27
|
+
}
|
|
28
|
+
parts.push(bytes.toString('utf8', start));
|
|
29
|
+
return parts.join('');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function encodeLogText(text) {
|
|
34
|
+
const parts = [];
|
|
35
|
+
let start = 0;
|
|
36
|
+
for (let i = 0; i < text.length; i++) {
|
|
37
|
+
const code = text.charCodeAt(i);
|
|
38
|
+
if (
|
|
39
|
+
code >= 0xd800 &&
|
|
40
|
+
code <= 0xdbff &&
|
|
41
|
+
text.charCodeAt(i + 1) >= 0xdc00 &&
|
|
42
|
+
text.charCodeAt(i + 1) <= 0xdfff
|
|
43
|
+
) {
|
|
44
|
+
i++;
|
|
45
|
+
} else if (code >= 0xdc80 && code <= 0xdcff) {
|
|
46
|
+
parts.push(
|
|
47
|
+
Buffer.from(text.slice(start, i)),
|
|
48
|
+
Buffer.from([code - 0xdc00])
|
|
49
|
+
);
|
|
50
|
+
start = i + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!parts.length) {
|
|
54
|
+
return Buffer.from(text);
|
|
55
|
+
}
|
|
56
|
+
parts.push(Buffer.from(text.slice(start)));
|
|
57
|
+
return Buffer.concat(parts);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class LogDecoder {
|
|
61
|
+
pending = Buffer.alloc(0);
|
|
62
|
+
decode(input = Buffer.alloc(0), { stream = false } = {}) {
|
|
63
|
+
const bytes = this.pending.length
|
|
64
|
+
? Buffer.concat([this.pending, input])
|
|
65
|
+
: Buffer.from(input);
|
|
66
|
+
let end = bytes.length;
|
|
67
|
+
if (stream && end) {
|
|
68
|
+
let start = end - 1;
|
|
69
|
+
while (
|
|
70
|
+
start > 0 &&
|
|
71
|
+
end - start < 4 &&
|
|
72
|
+
bytes[start] >= 0x80 &&
|
|
73
|
+
bytes[start] <= 0xbf
|
|
74
|
+
) {
|
|
75
|
+
start--;
|
|
76
|
+
}
|
|
77
|
+
const width = sequenceWidth(bytes[start]);
|
|
78
|
+
if (width > end - start) {
|
|
79
|
+
end = start;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.pending = Buffer.from(bytes.subarray(end));
|
|
83
|
+
return decodeLogBytes(bytes.subarray(0, end));
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/names.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A small offline gazetteer plus script-specific name grammar. This is a
|
|
2
|
+
// transparent baseline, not a statistical NER model. Applications can union
|
|
3
|
+
// their own model with it through the required detector interface.
|
|
4
|
+
export const firstNames = {
|
|
5
|
+
en: 'John James Robert Michael William David Joseph Thomas Charles Mary Patricia Jennifer Linda Elizabeth Barbara Susan Jessica Sarah Alice Jane Peter Paul Mark Daniel George Emma Olivia Noah Liam Jack Henry Benjamin Anthony Andrew Steven Edward Richard Helen Margaret Nancy Carol Anna Martin Albert Bill Elon',
|
|
6
|
+
es: 'José Juan Carlos Luis Jorge Pedro Miguel Antonio Francisco Jesús Manuel María Carmen Ana Laura Isabel Elena Marta Lucía Sofía Javier Alejandro Diego Raúl Pablo Teresa Rosa',
|
|
7
|
+
fr: 'Jean Pierre Michel André Philippe Jacques François Louis Nicolas Antoine Julien Gabriel Étienne Marie Jeanne Françoise Catherine Isabelle Monique Sylvie Nathalie Sophie Camille Émilie Chloé Zoë',
|
|
8
|
+
de: 'Hans Klaus Wolfgang Jürgen Günter Dieter Karl Heinrich Wilhelm Friedrich Johann Alexander Lukas Leon Julia Hannah Emilia Lena Mia Sabine Ursula Ingrid Petra Monika Helga',
|
|
9
|
+
pt: 'João José Antônio Francisco Paulo Pedro Lucas Gabriel Rafael Bruno Carlos Maria Ana Beatriz Camila Larissa Mariana Fernanda Juliana Aline Bruna',
|
|
10
|
+
ruLatn:
|
|
11
|
+
'Ivan Petr Pyotr Sergey Sergei Aleksandr Alexander Alexey Aleksey Dmitry Dmitri Mikhail Nikolai Andrey Vladimir Pavel Viktor Oleg Boris Evgeny Vasily Yuri Marina Anna Mariya Elena Olga Natalya Irina Svetlana Tatyana Ekaterina Anastasia Yulia',
|
|
12
|
+
jaLatn: 'Yamada Sato Suzuki Takahashi Tanaka Ito Watanabe Nakamura Kobayashi',
|
|
13
|
+
zhLatn:
|
|
14
|
+
'Zhang Wang Li Zhao Liu Chen Yang Huang Zhou Wu Xu Sun Hu Zhu Gao Lin',
|
|
15
|
+
ru: 'Иван Пётр Петр Сергей Александр Алексей Дмитрий Михаил Николай Андрей Владимир Павел Виктор Олег Борис Евгений Василий Юрий Марина Анна Мария Елена Ольга Наталья Ирина Светлана Татьяна Екатерина Анастасия Юлия Ивану Ивана Иваном Сергея Сергею Александра Александру Анне Анну Елены',
|
|
16
|
+
ja: '山田 佐藤 鈴木 高橋 田中 伊藤 渡辺 渡邊 中村 小林 加藤 吉田 松本 山本 井上 木村 林 清水 斎藤 斉藤',
|
|
17
|
+
zh: '张 王 李 赵 刘 陈 杨 黄 周 吴 徐 孙 胡 朱 高 林 何 郭 马 罗 張 趙 劉 陳 楊 黃 吳 孫 馬 羅',
|
|
18
|
+
ko: '김 이 박 최 정 강 조 윤 장 임 한 오 서 신 권 황 안 송 전 홍 유 고 문 양 손 배 백',
|
|
19
|
+
ar: 'محمد أحمد احمد علي حسن حسين عمر خالد يوسف إبراهيم ابراهيم عبدالله عبد الرحمن محمود مصطفى سامي سعيد فاطمة مريم عائشة زينب نور ليلى سارة',
|
|
20
|
+
he: 'דוד משה יוסף אברהם יצחק יעקב דניאל חיים שלמה אהרון ישראל שרה רחל לאה מרים חנה רבקה נועה תמר יעל',
|
|
21
|
+
hi: 'आरव विवान आदित्य अर्जुन कृष्ण राहुल रोहित अमित विजय सुरेश रमेश राजेश संजय अनिल सुनील प्रिया पूजा नेहा अंजली आशा मीरा आरती लक्ष्मी सीता',
|
|
22
|
+
tr: 'Mehmet Mustafa Ahmet Ali Hüseyin Hasan İbrahim Murat Yusuf Emre Ömer Ayşe Fatma Emine Hatice Zeynep Elif Merve Esra Selin',
|
|
23
|
+
id: 'Budi Ahmad Muhammad Agus Bambang Dedi Eko Hadi Joko Rudi Siti Sri Dewi Putri Ayu Wati Lestari Ratna Indah',
|
|
24
|
+
vi: 'Nguyễn Trần Lê Phạm Hoàng Huỳnh Phan Vũ Võ Đặng Bùi Đỗ Hồ Ngô Dương Lý',
|
|
25
|
+
th: 'สมชาย สมศักดิ์ สุชาติ วิชัย ประเสริฐ สมพร สุรชัย อนุชา กิตติ วัฒนา สมหญิง สุภา จันทร์เพ็ญ มาลี สุดา อรทัย',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const nameExamples = [
|
|
29
|
+
['en', 'John Smith'],
|
|
30
|
+
['es', 'Juan García'],
|
|
31
|
+
['fr', 'Jean Dupont'],
|
|
32
|
+
['de', 'Hans Müller'],
|
|
33
|
+
['pt', 'João Silva'],
|
|
34
|
+
['ru', 'Иван Петров'],
|
|
35
|
+
['ja', '山田太郎'],
|
|
36
|
+
['zh', '张三'],
|
|
37
|
+
['zh-Hant', '張三'],
|
|
38
|
+
['ko', '김민수'],
|
|
39
|
+
['ar', 'محمد علي'],
|
|
40
|
+
['he', 'דוד כהן'],
|
|
41
|
+
['hi', 'आरव शर्मा'],
|
|
42
|
+
['tr', 'Mehmet Yılmaz'],
|
|
43
|
+
['id', 'Budi Santoso'],
|
|
44
|
+
['vi', 'Nguyễn Văn An'],
|
|
45
|
+
['th', 'สมชาย ใจดี'],
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const givenNames = new Set(
|
|
49
|
+
Object.values(firstNames).flatMap((list) => list.toLowerCase().split(' '))
|
|
50
|
+
);
|
|
51
|
+
// A familiar label is evidence only when its value also has a name shape.
|
|
52
|
+
// Single unfamiliar Latin words are commonly tool names or code identifiers.
|
|
53
|
+
export function plausiblePerson(value) {
|
|
54
|
+
if (!/^[\p{L}\p{M}][\p{L}\p{M}’' -]{1,127}$/u.test(value)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const words = value.split(/[ \t]+/);
|
|
58
|
+
if (words.some((word) => givenNames.has(word.toLowerCase()))) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (words.length >= 2 && words.length <= 4) {
|
|
62
|
+
return words.every((word) => /^\p{Lu}[\p{L}\p{M}’'-]+$/u.test(word));
|
|
63
|
+
}
|
|
64
|
+
return /^[\p{L}\p{M}]{2,32}$/u.test(value) && !/[\p{Lu}\p{Ll}]/u.test(value);
|
|
65
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Format arithmetic; these checks are confidence signals, not verification of
|
|
2
|
+
// issuance. Short-number Luhn is separate from the public credit-card helper.
|
|
3
|
+
export const digitsOf = (value) => value.replace(/\D/g, '');
|
|
4
|
+
const sum = (digits, weights) =>
|
|
5
|
+
weights.reduce((total, weight, i) => total + Number(digits[i]) * weight, 0);
|
|
6
|
+
export function shortLuhn(value) {
|
|
7
|
+
const digits = digitsOf(value);
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (
|
|
10
|
+
let i = digits.length - 1, double = false;
|
|
11
|
+
i >= 0;
|
|
12
|
+
i--, double = !double
|
|
13
|
+
) {
|
|
14
|
+
let digit = Number(digits[i]);
|
|
15
|
+
if (double) {
|
|
16
|
+
digit = digit * 2 > 9 ? digit * 2 - 9 : digit * 2;
|
|
17
|
+
}
|
|
18
|
+
total += digit;
|
|
19
|
+
}
|
|
20
|
+
return digits.length > 0 && !/^(\d)\1+$/.test(digits) && total % 10 === 0;
|
|
21
|
+
}
|
|
22
|
+
export function datePart(digits, monthOffset = 0, dayOffset = 0) {
|
|
23
|
+
const month = Number(digits.slice(2, 4)) - monthOffset;
|
|
24
|
+
const day = Number(digits.slice(4, 6)) - dayOffset;
|
|
25
|
+
const year = 2000 + Number(digits.slice(0, 2));
|
|
26
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
27
|
+
return (
|
|
28
|
+
date.getUTCFullYear() === year &&
|
|
29
|
+
date.getUTCMonth() === month - 1 &&
|
|
30
|
+
date.getUTCDate() === day
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
export function nhs(value) {
|
|
34
|
+
const d = digitsOf(value),
|
|
35
|
+
check = (11 - (sum(d, [10, 9, 8, 7, 6, 5, 4, 3, 2]) % 11)) % 11;
|
|
36
|
+
return (
|
|
37
|
+
d.length === 10 &&
|
|
38
|
+
!/^(\d)\1+$/.test(d) &&
|
|
39
|
+
check < 10 &&
|
|
40
|
+
check === Number(d[9])
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
export function snils(value) {
|
|
44
|
+
const d = digitsOf(value),
|
|
45
|
+
check = sum(d, [9, 8, 7, 6, 5, 4, 3, 2, 1]) % 101;
|
|
46
|
+
return (
|
|
47
|
+
d.length === 11 &&
|
|
48
|
+
!/^(\d)\1+$/.test(d) &&
|
|
49
|
+
(check === 100 ? 0 : check) === Number(d.slice(9))
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
export function inn(value) {
|
|
53
|
+
const d = digitsOf(value),
|
|
54
|
+
check = (weights) => (sum(d, weights) % 11) % 10;
|
|
55
|
+
if (/^(\d)\1+$/.test(d)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return d.length === 10
|
|
59
|
+
? check([2, 4, 10, 3, 5, 9, 4, 6, 8]) === Number(d[9])
|
|
60
|
+
: d.length === 12 &&
|
|
61
|
+
check([7, 2, 4, 10, 3, 5, 9, 4, 6, 8]) === Number(d[10]) &&
|
|
62
|
+
check([3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]) === Number(d[11]);
|
|
63
|
+
}
|
|
64
|
+
export function pesel(value) {
|
|
65
|
+
const d = digitsOf(value),
|
|
66
|
+
month = Number(d.slice(2, 4));
|
|
67
|
+
return (
|
|
68
|
+
d.length === 11 &&
|
|
69
|
+
datePart(d, Math.floor(month / 20) * 20) &&
|
|
70
|
+
(10 - (sum(d, [1, 3, 7, 9, 1, 3, 7, 9, 1, 3]) % 10)) % 10 === Number(d[10])
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
export function rrn(value) {
|
|
74
|
+
const d = digitsOf(value);
|
|
75
|
+
return (
|
|
76
|
+
d.length === 13 &&
|
|
77
|
+
datePart(d) &&
|
|
78
|
+
(11 - (sum(d, [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5]) % 11)) % 10 ===
|
|
79
|
+
Number(d[12])
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
export function tfn(value) {
|
|
83
|
+
const d = digitsOf(value);
|
|
84
|
+
return (
|
|
85
|
+
d.length === 9 &&
|
|
86
|
+
!/^(\d)\1+$/.test(d) &&
|
|
87
|
+
sum(d, [1, 4, 3, 7, 5, 8, 6, 9, 10]) % 11 === 0
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
export function nric(value) {
|
|
91
|
+
const v = value.toUpperCase(),
|
|
92
|
+
prefix = v[0];
|
|
93
|
+
const offset = { S: 0, T: 4, F: 0, G: 4, M: 3 }[prefix];
|
|
94
|
+
const alphabet = /[ST]/.test(prefix)
|
|
95
|
+
? 'JZIHGFEDCBA'
|
|
96
|
+
: prefix === 'M'
|
|
97
|
+
? 'KLJNPQRTUWX'
|
|
98
|
+
: 'XWUTRQPNMLK';
|
|
99
|
+
const index = (sum(v.slice(1, 8), [2, 7, 6, 5, 4, 3, 2]) + offset) % 11;
|
|
100
|
+
return (
|
|
101
|
+
/^[STFGM]\d{7}[A-Z]$/.test(v) &&
|
|
102
|
+
alphabet[prefix === 'M' ? 10 - index : index] === v[8]
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
export function fiscalCode(value) {
|
|
106
|
+
const odd = [
|
|
107
|
+
1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16,
|
|
108
|
+
10, 22, 25, 24, 23,
|
|
109
|
+
];
|
|
110
|
+
let total = 0;
|
|
111
|
+
for (let i = 0; i < 15; i++) {
|
|
112
|
+
const char = value[i],
|
|
113
|
+
position = /\d/.test(char) ? Number(char) : char.charCodeAt(0) - 65;
|
|
114
|
+
total += i % 2 === 0 ? odd[position] : position;
|
|
115
|
+
}
|
|
116
|
+
return String.fromCharCode(65 + (total % 26)) === value[15];
|
|
117
|
+
}
|
|
118
|
+
export function personnummer(value) {
|
|
119
|
+
const d = digitsOf(value).slice(-10),
|
|
120
|
+
day = Number(d.slice(4, 6));
|
|
121
|
+
return d.length === 10 && datePart(d, 0, day > 60 ? 60 : 0) && shortLuhn(d);
|
|
122
|
+
}
|
|
123
|
+
export function southAfricanId(value) {
|
|
124
|
+
return (
|
|
125
|
+
datePart(value) &&
|
|
126
|
+
/^[0-2][89]$/.test(value.slice(10, 12)) &&
|
|
127
|
+
shortLuhn(value)
|
|
128
|
+
);
|
|
129
|
+
}
|
package/src/national.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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';
|
|
17
|
+
|
|
18
|
+
const rules = [
|
|
19
|
+
[
|
|
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
|
+
];
|
|
68
|
+
function distinctiveShape(type, value) {
|
|
69
|
+
return (
|
|
70
|
+
(type === 'RU_SNILS' && /^\d{3}([- ])\d{3}\1\d{3} \d{2}$/.test(value)) ||
|
|
71
|
+
(type === 'RU_INN' && value.length === 12)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
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])) {
|
|
93
|
+
emit({
|
|
94
|
+
start,
|
|
95
|
+
end,
|
|
96
|
+
type,
|
|
97
|
+
category: 'personal',
|
|
98
|
+
rule: 'national-format',
|
|
99
|
+
confidence: nationalConfidence(
|
|
100
|
+
hasContext,
|
|
101
|
+
checked,
|
|
102
|
+
plausible?.(match[0]) ||
|
|
103
|
+
(checked && distinctiveShape(type, match[0]))
|
|
104
|
+
),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function nationalBoundary(text, start, end) {
|
|
111
|
+
return !(
|
|
112
|
+
/[\p{L}\p{N}_.+-]/u.test(text[start - 1] ?? '') ||
|
|
113
|
+
/[\p{L}\p{N}_]/u.test(text[end] ?? '') ||
|
|
114
|
+
/\d[ ()-]{0,3}$/.test(text.slice(Math.max(0, start - 4), start)) ||
|
|
115
|
+
/^[ ()-]{0,3}\d/.test(text.slice(end, end + 4))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
function nationalConfidence(context, checked, plausible) {
|
|
119
|
+
return context
|
|
120
|
+
? checked
|
|
121
|
+
? 0.99
|
|
122
|
+
: 0.9
|
|
123
|
+
: plausible
|
|
124
|
+
? checked
|
|
125
|
+
? 0.99
|
|
126
|
+
: 0.4
|
|
127
|
+
: 0.35;
|
|
128
|
+
}
|
package/src/outbound.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createSanitizer } from './engines.js';
|
|
2
|
+
import { failure } from './detection.js';
|
|
3
|
+
/** Sanitizes one serialized payload so contextual keys survive to detection. */
|
|
4
|
+
export async function sanitizePayload(value, options = {}) {
|
|
5
|
+
const engine = options.sanitizer ?? createSanitizer(options.sanitizerOptions);
|
|
6
|
+
try {
|
|
7
|
+
const serialized = JSON.stringify(value);
|
|
8
|
+
const result = await engine.sanitize(serialized);
|
|
9
|
+
return JSON.parse(result.text);
|
|
10
|
+
} catch {
|
|
11
|
+
throw failure('ERR_PUBLICATION');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function createSentryBeforeSend(options = {}) {
|
|
15
|
+
return async (event) => {
|
|
16
|
+
try {
|
|
17
|
+
return await sanitizePayload(event, options);
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createOutboundSanitizer(publish, options = {}) {
|
|
24
|
+
if (typeof publish !== 'function') {
|
|
25
|
+
throw failure('ERR_CONFIG');
|
|
26
|
+
}
|
|
27
|
+
return async (payload) => publish(await sanitizePayload(payload, options));
|
|
28
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { collectMatches } from './detection.js';
|
|
2
|
+
|
|
3
|
+
const birth =
|
|
4
|
+
/(?<![\p{L}\p{N}_])(?:born|birth[ _-]?date|date[ _-]of[ _-]birth|DOB|дата рождения|д\.р\.|родился|родилась|Geburtsdatum|fecha de nacimiento|date de naissance)(?![\p{L}\p{N}_])/giu;
|
|
5
|
+
const months =
|
|
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
|
+
.split(' ')
|
|
8
|
+
.join('|');
|
|
9
|
+
const dates = new RegExp(
|
|
10
|
+
`(?<![\\p{L}\\p{N}_])(?:\\d{4}-\\d{1,2}-\\d{1,2}|\\d{1,2}[/.]\\d{1,2}[/.]\\d{4}|\\d{1,2}[ \\t]+(?:${months})[ \\t]+\\d{4}|(?:${months})[ \\t]+\\d{1,2},?[ \\t]+\\d{4})(?![\\p{L}\\p{N}_])`,
|
|
11
|
+
'giu'
|
|
12
|
+
);
|
|
13
|
+
const phoneContext =
|
|
14
|
+
/(?<![\p{L}\p{N}_])(?:WhatsApp|Zalo|Viber|Telegram|Signal|WeChat|LINE|KakaoTalk|звоните|пишите|звонить|номер телефона|телефон|phone|fax|call|telp|telepon|HP|hubungi|โทร|เบอร์|gọi|SĐT|ĐT|điện thoại)(?![\p{L}\p{N}_])/iu;
|
|
15
|
+
const nationalPhone =
|
|
16
|
+
/(?<![\p{L}\p{N}_+])(?:\(?08\d{2}[ )-]{0,3}\d{3,4}[ -]{0,2}\d{3,4}|8[ ()-]{0,3}9\d{2}[ )-]{0,3}\d{3}[ -]{0,2}\d{2}[ -]{0,2}\d{2}|\(?0[ ()-]{0,2}[35789]\d[ )-]{0,3}\d{3}[ -]{0,2}\d{4}|\(?0[ ()-]{0,2}\d[ )-]{0,3}\d{3}[ -]{0,2}\d{4}|\(?0[ ()-]{0,2}8\d{2}[ )-]{0,3}\d{4}[ -]{0,2}\d{4})(?![\p{L}\p{N}_])/gu;
|
|
17
|
+
export function detectPersonalContext(text, emit) {
|
|
18
|
+
const phone = {
|
|
19
|
+
type: 'PHONE',
|
|
20
|
+
category: 'personal',
|
|
21
|
+
rule: 'phone-context',
|
|
22
|
+
confidence: 0.9,
|
|
23
|
+
};
|
|
24
|
+
collectMatches(
|
|
25
|
+
text,
|
|
26
|
+
/(?<![\p{L}\p{N}_])tel:[ \t]*(\+?\d[\d(). -]{0,38}\d|\d)(?![\p{L}\p{N}_])/giu,
|
|
27
|
+
emit,
|
|
28
|
+
{ ...phone, rule: 'tel-uri' },
|
|
29
|
+
1
|
|
30
|
+
);
|
|
31
|
+
collectMatches(
|
|
32
|
+
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
|
+
emit,
|
|
35
|
+
phone,
|
|
36
|
+
1,
|
|
37
|
+
(value) => {
|
|
38
|
+
const digits = value.replace(/\D/g, '').length;
|
|
39
|
+
return digits >= 7 && digits <= 15;
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
for (const label of text.matchAll(birth)) {
|
|
43
|
+
const start = label.index + label[0].length;
|
|
44
|
+
const window = text.slice(start, start + 80).split(/[\r\n|;]/)[0];
|
|
45
|
+
const match = new RegExp(dates.source, dates.flags).exec(window);
|
|
46
|
+
if (match) {
|
|
47
|
+
emit({
|
|
48
|
+
start: start + match.index,
|
|
49
|
+
end: start + match.index + match[0].length,
|
|
50
|
+
type: 'DATE_OF_BIRTH',
|
|
51
|
+
category: 'personal',
|
|
52
|
+
rule: 'birth-context',
|
|
53
|
+
confidence: 0.9,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
collectMatches(
|
|
58
|
+
text,
|
|
59
|
+
nationalPhone,
|
|
60
|
+
emit,
|
|
61
|
+
{
|
|
62
|
+
type: 'PHONE',
|
|
63
|
+
category: 'personal',
|
|
64
|
+
rule: 'phone-context',
|
|
65
|
+
confidence: 0.9,
|
|
66
|
+
},
|
|
67
|
+
0,
|
|
68
|
+
(value, match) => {
|
|
69
|
+
const prefix = text
|
|
70
|
+
.slice(Math.max(0, match.index - 80), match.index)
|
|
71
|
+
.split(/[\r\n|;]/)
|
|
72
|
+
.at(-1);
|
|
73
|
+
return (
|
|
74
|
+
(phoneContext.test(prefix) ||
|
|
75
|
+
/^\(?08/.test(value) ||
|
|
76
|
+
(/^0[689]/.test(value.replace(/\D/g, '')) &&
|
|
77
|
+
value.replace(/\D/g, '').length === 10)) &&
|
|
78
|
+
!/\d[ ()-]*$/.test(prefix) &&
|
|
79
|
+
!/^[ ()-]*\d/.test(
|
|
80
|
+
text.slice(match.index + value.length, match.index + value.length + 4)
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
}
|