@link-foundation/sensitive-data-sanitizer 0.0.0-stage → 0.11.31
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 +530 -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
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
function resolveVariable(context, node) {
|
|
2
|
+
for (
|
|
3
|
+
let scope = context.sourceCode.getScope(node);
|
|
4
|
+
scope;
|
|
5
|
+
scope = scope.upper
|
|
6
|
+
) {
|
|
7
|
+
if (scope.set.has(node.name)) {
|
|
8
|
+
return scope.set.get(node.name);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Optional static publication guard; runtime verification remains required. */
|
|
14
|
+
function name(node) {
|
|
15
|
+
return node?.name ?? node?.property?.name ?? node?.property?.value;
|
|
16
|
+
}
|
|
17
|
+
function literal(node) {
|
|
18
|
+
return node?.type === 'Literal' ? node.value : undefined;
|
|
19
|
+
}
|
|
20
|
+
function publicationCommand(text) {
|
|
21
|
+
return (
|
|
22
|
+
typeof text === 'string' &&
|
|
23
|
+
/\bgh\s+(?:pr|issue|gist|release)\s+(?:create|edit|comment|review)\b/.test(
|
|
24
|
+
text
|
|
25
|
+
) &&
|
|
26
|
+
/--(?:body|title|notes|comment)(?:-file)?\b|\s-[bt]\b/.test(text)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const rule = {
|
|
30
|
+
meta: {
|
|
31
|
+
type: 'problem',
|
|
32
|
+
docs: {
|
|
33
|
+
description:
|
|
34
|
+
'Require sanitization before generated data crosses an outbound boundary.',
|
|
35
|
+
},
|
|
36
|
+
schema: [
|
|
37
|
+
{
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
sinks: { type: 'array', items: { type: 'string' } },
|
|
41
|
+
sanitizers: { type: 'array', items: { type: 'string' } },
|
|
42
|
+
},
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
messages: {
|
|
47
|
+
unsafe: 'Sanitize generated outbound data before publication.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
create(context) {
|
|
51
|
+
const settings = context.options[0] ?? {};
|
|
52
|
+
const sinks = new Set(
|
|
53
|
+
settings.sinks ?? [
|
|
54
|
+
'publish',
|
|
55
|
+
'send',
|
|
56
|
+
'captureException',
|
|
57
|
+
'captureMessage',
|
|
58
|
+
]
|
|
59
|
+
);
|
|
60
|
+
const sanitizers = new Set(
|
|
61
|
+
settings.sanitizers ?? [
|
|
62
|
+
'sanitize',
|
|
63
|
+
'sanitizePayload',
|
|
64
|
+
'sanitizeForPublication',
|
|
65
|
+
]
|
|
66
|
+
);
|
|
67
|
+
const safeVariables = new Set(),
|
|
68
|
+
arrays = new Map();
|
|
69
|
+
function safeCall(node) {
|
|
70
|
+
return (
|
|
71
|
+
sanitizers.has(name(node.callee)) ||
|
|
72
|
+
(name(node.callee) === 'stringify' && node.arguments.every(safe))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function safe(node) {
|
|
76
|
+
if (!node) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (node.type === 'Literal') {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (node.type === 'Identifier') {
|
|
83
|
+
const variable = resolveVariable(context, node);
|
|
84
|
+
return safeVariables.has(variable);
|
|
85
|
+
}
|
|
86
|
+
if (['AwaitExpression', 'ChainExpression'].includes(node.type)) {
|
|
87
|
+
return safe(node.argument ?? node.expression);
|
|
88
|
+
}
|
|
89
|
+
if (node.type === 'MemberExpression') {
|
|
90
|
+
return safe(node.object);
|
|
91
|
+
}
|
|
92
|
+
if (node.type === 'CallExpression') {
|
|
93
|
+
return safeCall(node);
|
|
94
|
+
}
|
|
95
|
+
if (node.type === 'TemplateLiteral') {
|
|
96
|
+
return node.expressions.every(safe);
|
|
97
|
+
}
|
|
98
|
+
if (node.type === 'BinaryExpression') {
|
|
99
|
+
return safe(node.left) && safe(node.right);
|
|
100
|
+
}
|
|
101
|
+
if (node.type === 'ArrayExpression') {
|
|
102
|
+
return node.elements.every(safe);
|
|
103
|
+
}
|
|
104
|
+
if (node.type === 'ObjectExpression') {
|
|
105
|
+
return node.properties.every((p) => safe(p.value ?? p.argument));
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function checkGh(node) {
|
|
110
|
+
if (literal(node.arguments[0]) !== 'gh') {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const expression = node.arguments[1];
|
|
114
|
+
const argv =
|
|
115
|
+
expression?.type === 'ArrayExpression'
|
|
116
|
+
? expression
|
|
117
|
+
: arrays.get(
|
|
118
|
+
expression?.type === 'Identifier'
|
|
119
|
+
? resolveVariable(context, expression)
|
|
120
|
+
: undefined
|
|
121
|
+
);
|
|
122
|
+
if (
|
|
123
|
+
!argv ||
|
|
124
|
+
!['pr', 'issue', 'gist', 'release'].includes(
|
|
125
|
+
literal(argv.elements[0])
|
|
126
|
+
) ||
|
|
127
|
+
!['create', 'edit', 'comment', 'review'].includes(
|
|
128
|
+
literal(argv.elements[1])
|
|
129
|
+
)
|
|
130
|
+
) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
for (let i = 2; i < argv.elements.length - 1; i++) {
|
|
134
|
+
if (
|
|
135
|
+
/^(?:--body(?:-file)?|--title|--notes(?:-file)?|--comment|-b|-t)$/.test(
|
|
136
|
+
literal(argv.elements[i])
|
|
137
|
+
) &&
|
|
138
|
+
!safe(argv.elements[i + 1])
|
|
139
|
+
) {
|
|
140
|
+
context.report({ node, messageId: 'unsafe' });
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
'VariableDeclarator:exit'(node) {
|
|
147
|
+
if (node.id.type !== 'Identifier') {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const variable = context.sourceCode.getDeclaredVariables(node)[0];
|
|
151
|
+
if (node.parent.kind === 'const' && safe(node.init)) {
|
|
152
|
+
safeVariables.add(variable);
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
node.parent.kind === 'const' &&
|
|
156
|
+
node.init?.type === 'ArrayExpression'
|
|
157
|
+
) {
|
|
158
|
+
arrays.set(variable, node.init);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
'CallExpression:exit'(node) {
|
|
162
|
+
checkGh(node);
|
|
163
|
+
if (
|
|
164
|
+
sinks.has(name(node.callee)) &&
|
|
165
|
+
node.arguments.some((arg) => !safe(arg))
|
|
166
|
+
) {
|
|
167
|
+
context.report({ node, messageId: 'unsafe' });
|
|
168
|
+
}
|
|
169
|
+
const command = node.arguments[0];
|
|
170
|
+
if (
|
|
171
|
+
command?.type === 'TemplateLiteral' &&
|
|
172
|
+
publicationCommand(
|
|
173
|
+
command.quasis.map((q) => q.value.raw).join('${x}')
|
|
174
|
+
) &&
|
|
175
|
+
!command.expressions.every(safe)
|
|
176
|
+
) {
|
|
177
|
+
context.report({ node, messageId: 'unsafe' });
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
TaggedTemplateExpression(node) {
|
|
181
|
+
if (
|
|
182
|
+
publicationCommand(
|
|
183
|
+
node.quasi.quasis.map((q) => q.value.raw).join('${x}')
|
|
184
|
+
) &&
|
|
185
|
+
!node.quasi.expressions.every(safe)
|
|
186
|
+
) {
|
|
187
|
+
context.report({ node, messageId: 'unsafe' });
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
export default { rules: { 'require-sanitized-output': rule } };
|
package/src/external.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fromGitleaks, fromTrufflehog, fromPresidio } from './adapters.js';
|
|
6
|
+
import { readTextFile } from './files.js';
|
|
7
|
+
import { failure } from './detection.js';
|
|
8
|
+
|
|
9
|
+
function run(
|
|
10
|
+
command,
|
|
11
|
+
args,
|
|
12
|
+
input,
|
|
13
|
+
{ timeoutMs = 30000, maxOutputBytes = 10 * 1024 * 1024 } = {}
|
|
14
|
+
) {
|
|
15
|
+
if (
|
|
16
|
+
typeof command !== 'string' ||
|
|
17
|
+
!command ||
|
|
18
|
+
!Array.isArray(args) ||
|
|
19
|
+
args.some((a) => typeof a !== 'string') ||
|
|
20
|
+
!Number.isSafeInteger(timeoutMs) ||
|
|
21
|
+
timeoutMs <= 0 ||
|
|
22
|
+
!Number.isSafeInteger(maxOutputBytes) ||
|
|
23
|
+
maxOutputBytes <= 0
|
|
24
|
+
) {
|
|
25
|
+
throw failure('ERR_CONFIG');
|
|
26
|
+
}
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const child = spawn(command, args, {
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
shell: false,
|
|
31
|
+
windowsHide: true,
|
|
32
|
+
});
|
|
33
|
+
const chunks = [];
|
|
34
|
+
let size = 0,
|
|
35
|
+
failed = false;
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
failed = true;
|
|
38
|
+
child.kill('SIGKILL');
|
|
39
|
+
}, timeoutMs);
|
|
40
|
+
child.stdout.on('data', (chunk) => {
|
|
41
|
+
size += chunk.length;
|
|
42
|
+
if (size > maxOutputBytes) {
|
|
43
|
+
failed = true;
|
|
44
|
+
child.kill('SIGKILL');
|
|
45
|
+
} else {
|
|
46
|
+
chunks.push(chunk);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
child.stderr.on('data', () => {}); // Scanner diagnostics can contain credentials.
|
|
50
|
+
child.stdin.on('error', () => {
|
|
51
|
+
failed = true;
|
|
52
|
+
});
|
|
53
|
+
child.on('error', () => {
|
|
54
|
+
failed = true;
|
|
55
|
+
});
|
|
56
|
+
child.on('close', (code) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
if (failed || code !== 0) {
|
|
59
|
+
reject(failure('ERR_ENGINE'));
|
|
60
|
+
} else {
|
|
61
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
child.stdin.end(input);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createGitleaksDetector({
|
|
69
|
+
command = 'gitleaks',
|
|
70
|
+
timeoutMs,
|
|
71
|
+
maxOutputBytes,
|
|
72
|
+
} = {}) {
|
|
73
|
+
return {
|
|
74
|
+
id: 'gitleaks',
|
|
75
|
+
async detect(text) {
|
|
76
|
+
const directory = await mkdtemp(join(tmpdir(), 'sanitizer-gitleaks-'));
|
|
77
|
+
const report = join(directory, 'report.json');
|
|
78
|
+
try {
|
|
79
|
+
await run(
|
|
80
|
+
command,
|
|
81
|
+
[
|
|
82
|
+
'stdin',
|
|
83
|
+
'--no-banner',
|
|
84
|
+
'--log-level',
|
|
85
|
+
'error',
|
|
86
|
+
'--ignore-gitleaks-allow',
|
|
87
|
+
'--exit-code',
|
|
88
|
+
'0',
|
|
89
|
+
'--report-format',
|
|
90
|
+
'json',
|
|
91
|
+
'--report-path',
|
|
92
|
+
report,
|
|
93
|
+
],
|
|
94
|
+
text,
|
|
95
|
+
{ timeoutMs, maxOutputBytes }
|
|
96
|
+
);
|
|
97
|
+
const json = await readTextFile(
|
|
98
|
+
report,
|
|
99
|
+
maxOutputBytes ?? 10 * 1024 * 1024
|
|
100
|
+
);
|
|
101
|
+
return fromGitleaks(text, JSON.parse(json));
|
|
102
|
+
} catch {
|
|
103
|
+
throw failure('ERR_ENGINE');
|
|
104
|
+
} finally {
|
|
105
|
+
await rm(directory, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createTrufflehogDetector({
|
|
112
|
+
command = 'trufflehog',
|
|
113
|
+
timeoutMs,
|
|
114
|
+
maxOutputBytes,
|
|
115
|
+
} = {}) {
|
|
116
|
+
return {
|
|
117
|
+
id: 'trufflehog',
|
|
118
|
+
async detect(text) {
|
|
119
|
+
const directory = await mkdtemp(join(tmpdir(), 'sanitizer-trufflehog-'));
|
|
120
|
+
const input = join(directory, 'input.txt');
|
|
121
|
+
try {
|
|
122
|
+
await writeFile(input, text, { mode: 0o600 });
|
|
123
|
+
const output = await run(
|
|
124
|
+
command,
|
|
125
|
+
['filesystem', input, '--json', '--no-verification', '--no-update'],
|
|
126
|
+
undefined,
|
|
127
|
+
{ timeoutMs, maxOutputBytes }
|
|
128
|
+
);
|
|
129
|
+
const report = output
|
|
130
|
+
.split('\n')
|
|
131
|
+
.filter((line) => line.trim())
|
|
132
|
+
.map((line) => JSON.parse(line));
|
|
133
|
+
return fromTrufflehog(text, report);
|
|
134
|
+
} catch {
|
|
135
|
+
throw failure('ERR_ENGINE');
|
|
136
|
+
} finally {
|
|
137
|
+
await rm(directory, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A local Presidio bridge accepts text on stdin and emits its AnalyzerResult
|
|
144
|
+
// list as JSON. Model installation and language choice belong to the caller.
|
|
145
|
+
export function createPresidioDetector({
|
|
146
|
+
command = 'python3',
|
|
147
|
+
args,
|
|
148
|
+
timeoutMs,
|
|
149
|
+
maxOutputBytes,
|
|
150
|
+
} = {}) {
|
|
151
|
+
if (!Array.isArray(args)) {
|
|
152
|
+
throw failure('ERR_CONFIG');
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
id: 'presidio',
|
|
156
|
+
async detect(text) {
|
|
157
|
+
try {
|
|
158
|
+
return fromPresidio(
|
|
159
|
+
text,
|
|
160
|
+
JSON.parse(
|
|
161
|
+
await run(command, args, text, { timeoutMs, maxOutputBytes })
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
} catch {
|
|
165
|
+
throw failure('ERR_ENGINE');
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
Faker,
|
|
4
|
+
en,
|
|
5
|
+
es,
|
|
6
|
+
fr,
|
|
7
|
+
de,
|
|
8
|
+
pt_BR,
|
|
9
|
+
ru,
|
|
10
|
+
ja,
|
|
11
|
+
zh_CN,
|
|
12
|
+
zh_TW,
|
|
13
|
+
ko,
|
|
14
|
+
ar,
|
|
15
|
+
he,
|
|
16
|
+
tr,
|
|
17
|
+
id_ID,
|
|
18
|
+
vi,
|
|
19
|
+
th,
|
|
20
|
+
} from '@faker-js/faker';
|
|
21
|
+
import { firstNames } from './names.js';
|
|
22
|
+
|
|
23
|
+
const hi = {
|
|
24
|
+
person: {
|
|
25
|
+
first_name: {
|
|
26
|
+
female: ['प्रिया', 'पूजा', 'नेहा', 'मीरा'],
|
|
27
|
+
male: ['अर्जुन', 'अमित', 'विजय', 'राहुल'],
|
|
28
|
+
},
|
|
29
|
+
last_name: { generic: ['शर्मा', 'गुप्ता', 'वर्मा', 'सिंह'] },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const locales = {
|
|
33
|
+
en,
|
|
34
|
+
es,
|
|
35
|
+
fr,
|
|
36
|
+
de,
|
|
37
|
+
pt: pt_BR,
|
|
38
|
+
ru,
|
|
39
|
+
ruLatn: ru,
|
|
40
|
+
ja,
|
|
41
|
+
zh: zh_CN,
|
|
42
|
+
'zh-Hant': zh_TW,
|
|
43
|
+
ko,
|
|
44
|
+
ar,
|
|
45
|
+
he,
|
|
46
|
+
hi,
|
|
47
|
+
tr,
|
|
48
|
+
id: id_ID,
|
|
49
|
+
vi,
|
|
50
|
+
th,
|
|
51
|
+
};
|
|
52
|
+
const transliteration = Object.fromEntries(
|
|
53
|
+
Array.from('абвгдеёжзийклмнопрстуфхцчшщъыьэюя').map((letter, i) => [
|
|
54
|
+
letter,
|
|
55
|
+
[
|
|
56
|
+
'a',
|
|
57
|
+
'b',
|
|
58
|
+
'v',
|
|
59
|
+
'g',
|
|
60
|
+
'd',
|
|
61
|
+
'e',
|
|
62
|
+
'e',
|
|
63
|
+
'zh',
|
|
64
|
+
'z',
|
|
65
|
+
'i',
|
|
66
|
+
'y',
|
|
67
|
+
'k',
|
|
68
|
+
'l',
|
|
69
|
+
'm',
|
|
70
|
+
'n',
|
|
71
|
+
'o',
|
|
72
|
+
'p',
|
|
73
|
+
'r',
|
|
74
|
+
's',
|
|
75
|
+
't',
|
|
76
|
+
'u',
|
|
77
|
+
'f',
|
|
78
|
+
'kh',
|
|
79
|
+
'ts',
|
|
80
|
+
'ch',
|
|
81
|
+
'sh',
|
|
82
|
+
'shch',
|
|
83
|
+
'',
|
|
84
|
+
'y',
|
|
85
|
+
'',
|
|
86
|
+
'e',
|
|
87
|
+
'yu',
|
|
88
|
+
'ya',
|
|
89
|
+
][i],
|
|
90
|
+
])
|
|
91
|
+
);
|
|
92
|
+
export function latinName(value) {
|
|
93
|
+
return value
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.normalize('NFD')
|
|
96
|
+
.replace(/\p{M}/gu, '')
|
|
97
|
+
.replace(/[а-яё]/g, (c) => transliteration[c]);
|
|
98
|
+
}
|
|
99
|
+
const given = new Map();
|
|
100
|
+
for (const [locale, list] of Object.entries(firstNames)) {
|
|
101
|
+
for (const name of list.split(' ')) {
|
|
102
|
+
given.set(latinName(name), locale);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const surnames = new Map(),
|
|
106
|
+
female = new Set();
|
|
107
|
+
for (const [locale, data] of Object.entries(locales)) {
|
|
108
|
+
for (const name of data.person?.first_name?.female ?? []) {
|
|
109
|
+
female.add(latinName(name));
|
|
110
|
+
}
|
|
111
|
+
for (const names of Object.values(data.person?.last_name ?? {})) {
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
if (!surnames.has(latinName(name))) {
|
|
114
|
+
surnames.set(latinName(name), locale);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const femaleNames =
|
|
120
|
+
/^(?:marina|anna|mariya|maria|elena|olga|natalya|irina|svetlana|tatyana|ekaterina|anastasia|yulia|mary|jane|alice|emma|sarah|jessica|patricia|jennifer|linda|elizabeth|barbara|susan|olivia)$/;
|
|
121
|
+
function scriptLocale(value) {
|
|
122
|
+
for (const [script, locale] of [
|
|
123
|
+
['Cyrillic', 'ru'],
|
|
124
|
+
['Arabic', 'ar'],
|
|
125
|
+
['Hebrew', 'he'],
|
|
126
|
+
['Devanagari', 'hi'],
|
|
127
|
+
['Thai', 'th'],
|
|
128
|
+
['Hangul', 'ko'],
|
|
129
|
+
['Hiragana', 'ja'],
|
|
130
|
+
['Katakana', 'ja'],
|
|
131
|
+
['Han', 'zh'],
|
|
132
|
+
]) {
|
|
133
|
+
if (new RegExp(`\\p{Script=${script}}`, 'u').test(value)) {
|
|
134
|
+
return locale;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
export function fakeName(value, key) {
|
|
140
|
+
return value.replace(/[\p{L}\p{M}]+/gu, (word) => {
|
|
141
|
+
for (const locale of ['ja', 'zh', 'ko']) {
|
|
142
|
+
const family = firstNames[locale]
|
|
143
|
+
.split(' ')
|
|
144
|
+
.find((name) => word.startsWith(name) && word.length > name.length);
|
|
145
|
+
if (family) {
|
|
146
|
+
return (
|
|
147
|
+
fakeWord(family, key, 'lastName', locale) +
|
|
148
|
+
fakeWord(word.slice(family.length), key, 'firstName', locale)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return fakeWord(word, key);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function wordProfile(word, forcedRole, forcedLocale) {
|
|
156
|
+
const canonical = latinName(word);
|
|
157
|
+
const patronymic = /(?:vich|vna|ichna|ogly|kyzy)$/.test(canonical);
|
|
158
|
+
const slavic = /(?:ova|eva|ov|ev|in|ina|enko|sky|ski|skaya)$/.test(canonical);
|
|
159
|
+
const locale =
|
|
160
|
+
forcedLocale ??
|
|
161
|
+
scriptLocale(word) ??
|
|
162
|
+
given.get(canonical) ??
|
|
163
|
+
(slavic || patronymic ? 'ru' : (surnames.get(canonical) ?? 'en'));
|
|
164
|
+
const sex =
|
|
165
|
+
femaleNames.test(canonical) ||
|
|
166
|
+
female.has(canonical) ||
|
|
167
|
+
/(?:ova|eva|ina|skaya|vna|ichna|kyzy)$/.test(canonical)
|
|
168
|
+
? 'female'
|
|
169
|
+
: 'male';
|
|
170
|
+
const role =
|
|
171
|
+
forcedRole ??
|
|
172
|
+
(patronymic
|
|
173
|
+
? 'middleName'
|
|
174
|
+
: given.has(canonical) &&
|
|
175
|
+
!['jaLatn', 'zhLatn', 'vi', 'ja', 'zh', 'ko'].includes(
|
|
176
|
+
given.get(canonical)
|
|
177
|
+
)
|
|
178
|
+
? 'firstName'
|
|
179
|
+
: 'lastName');
|
|
180
|
+
return { canonical, locale, sex, role };
|
|
181
|
+
}
|
|
182
|
+
function fakeWord(word, key, forcedRole, forcedLocale) {
|
|
183
|
+
const { canonical, locale, sex, role } = wordProfile(
|
|
184
|
+
word,
|
|
185
|
+
forcedRole,
|
|
186
|
+
forcedLocale
|
|
187
|
+
);
|
|
188
|
+
const faker = new Faker({ locale: [locales[locale] ?? en, en] });
|
|
189
|
+
faker.seed(
|
|
190
|
+
createHmac('sha256', key)
|
|
191
|
+
.update(`name\0${canonical}\0${role}`)
|
|
192
|
+
.digest()
|
|
193
|
+
.readUInt32BE(0)
|
|
194
|
+
);
|
|
195
|
+
let replacement;
|
|
196
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
197
|
+
replacement = faker.person[role](sex).replace(/[ -].*$/, '');
|
|
198
|
+
if (
|
|
199
|
+
latinName(replacement) !== canonical &&
|
|
200
|
+
(!/^\p{Script=Han}+$/u.test(word) ||
|
|
201
|
+
/^\p{Script=Han}+$/u.test(replacement))
|
|
202
|
+
) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!scriptLocale(word) && /\p{Script=Cyrillic}/u.test(replacement)) {
|
|
207
|
+
replacement = latinName(replacement);
|
|
208
|
+
}
|
|
209
|
+
if (latinName(replacement) === canonical) {
|
|
210
|
+
replacement = faker.person[role](sex).replace(/[ -].*$/, '');
|
|
211
|
+
if (!scriptLocale(word)) {
|
|
212
|
+
replacement = latinName(replacement);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (word === word.toUpperCase()) {
|
|
216
|
+
return replacement.toUpperCase();
|
|
217
|
+
}
|
|
218
|
+
if (word === word.toLowerCase()) {
|
|
219
|
+
return replacement.toLowerCase();
|
|
220
|
+
}
|
|
221
|
+
return replacement[0].toUpperCase() + replacement.slice(1).toLowerCase();
|
|
222
|
+
}
|