@link-foundation/sensitive-data-sanitizer 0.19.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +22 -2
- package/bin/sensitive-data-sanitizer.js +50 -28
- package/docs/API.md +355 -16
- package/docs/COVERAGE.md +19 -9
- package/docs/RULES.md +218 -0
- package/examples/second-pass-placeholders.mjs +28 -0
- package/package.json +12 -3
- package/src/allow-list.js +93 -0
- package/src/confidence.js +24 -9
- package/src/context.js +287 -40
- package/src/credential-spans.js +180 -0
- package/src/credentials-entry.d.ts +23 -0
- package/src/credentials-entry.js +62 -0
- package/src/credentials.js +51 -0
- package/src/crypto-addresses.js +74 -0
- package/src/detection.js +87 -41
- package/src/document-person-cues.js +1 -1
- package/src/encoded.js +137 -51
- package/src/encrypt.js +95 -0
- package/src/engines.js +33 -10
- package/src/entities.js +4 -3
- package/src/exempt.js +72 -0
- package/src/fake-memory.js +32 -0
- package/src/fake.js +33 -29
- package/src/features.js +29 -0
- package/src/files.js +57 -12
- package/src/gitleaks.js +82 -27
- package/src/html-tags.js +8 -0
- package/src/identifier-detector.js +36 -0
- package/src/identity-types.js +63 -0
- package/src/identity.js +3 -47
- package/src/index.d.ts +157 -3
- package/src/index.js +13 -0
- package/src/known.js +33 -15
- 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-features.js +39 -0
- package/src/personal.js +66 -10
- package/src/placeholder-types.js +154 -4
- package/src/placeholders.js +127 -45
- package/src/projection.js +78 -26
- package/src/proper-names.js +48 -5
- package/src/prose-words.js +120 -0
- package/src/record-worker.js +1 -0
- package/src/rule-catalog.js +426 -0
- package/src/rules.js +8 -0
- package/src/sanitizer.js +133 -23
- package/src/sarif.js +127 -0
- package/src/stream-worker.js +1 -0
- package/src/strict-attributes.js +48 -4
- package/src/strict.js +1 -1
- package/src/structured.js +2 -0
- package/src/token-checksum.js +49 -0
- package/src/transforms.js +146 -33
- 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/vocabulary.js +27 -1
- package/src/worker-options.js +1 -0
package/src/encrypt.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHmac,
|
|
5
|
+
hkdfSync,
|
|
6
|
+
} from 'node:crypto';
|
|
7
|
+
import { failure } from './detection.js';
|
|
8
|
+
|
|
9
|
+
// Deterministic authenticated encryption (synthetic IV), comparable to
|
|
10
|
+
// Google's CryptoDeterministicConfig (AES-SIV) and Presidio's encrypt
|
|
11
|
+
// operator. Equal values of one type produce equal tokens, so the output
|
|
12
|
+
// still supports joins and counts, and decrypt() recovers the value. The
|
|
13
|
+
// derived IV is an HMAC of the type and value, so it never repeats for
|
|
14
|
+
// different plaintexts under one key.
|
|
15
|
+
const ivBytes = 12,
|
|
16
|
+
tagBytes = 16;
|
|
17
|
+
const token = /\[ENCRYPTED:([A-Z][A-Z0-9_]{0,63}):([A-Za-z0-9_-]{38,10000})\]/g;
|
|
18
|
+
const keys = new Map();
|
|
19
|
+
|
|
20
|
+
function derived(key) {
|
|
21
|
+
if (typeof key !== 'string' || Buffer.byteLength(key) < 16) {
|
|
22
|
+
throw failure('ERR_CONFIG');
|
|
23
|
+
}
|
|
24
|
+
let entry = keys.get(key);
|
|
25
|
+
if (!entry) {
|
|
26
|
+
const material = Buffer.from(
|
|
27
|
+
hkdfSync(
|
|
28
|
+
'sha256',
|
|
29
|
+
key,
|
|
30
|
+
'sensitive-data-sanitizer',
|
|
31
|
+
'deterministic-encryption-v1',
|
|
32
|
+
64
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
entry = { cipher: material.subarray(0, 32), mac: material.subarray(32) };
|
|
36
|
+
if (keys.size >= 16) {
|
|
37
|
+
keys.clear();
|
|
38
|
+
}
|
|
39
|
+
keys.set(key, entry);
|
|
40
|
+
}
|
|
41
|
+
return entry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function encryptValue(value, type, key) {
|
|
45
|
+
const { cipher, mac } = derived(key);
|
|
46
|
+
const iv = createHmac('sha256', mac)
|
|
47
|
+
.update(`${type}\0${value}`)
|
|
48
|
+
.digest()
|
|
49
|
+
.subarray(0, ivBytes);
|
|
50
|
+
const engine = createCipheriv('aes-256-gcm', cipher, iv);
|
|
51
|
+
engine.setAAD(Buffer.from(type));
|
|
52
|
+
const body = Buffer.concat([engine.update(value, 'utf8'), engine.final()]);
|
|
53
|
+
const payload = Buffer.concat([iv, body, engine.getAuthTag()]);
|
|
54
|
+
return `[ENCRYPTED:${type}:${payload.toString('base64url')}]`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function decryptToken(type, encoded, key) {
|
|
58
|
+
const { cipher } = derived(key);
|
|
59
|
+
const payload = Buffer.from(encoded, 'base64url');
|
|
60
|
+
if (payload.length < ivBytes + tagBytes) {
|
|
61
|
+
throw failure('ERR_MAP');
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const engine = createDecipheriv(
|
|
65
|
+
'aes-256-gcm',
|
|
66
|
+
cipher,
|
|
67
|
+
payload.subarray(0, ivBytes)
|
|
68
|
+
);
|
|
69
|
+
engine.setAAD(Buffer.from(type));
|
|
70
|
+
engine.setAuthTag(payload.subarray(-tagBytes));
|
|
71
|
+
return Buffer.concat([
|
|
72
|
+
engine.update(payload.subarray(ivBytes, -tagBytes)),
|
|
73
|
+
engine.final(),
|
|
74
|
+
]).toString('utf8');
|
|
75
|
+
} catch {
|
|
76
|
+
throw failure('ERR_MAP');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Restores `[ENCRYPTED:TYPE:…]` tokens produced by `{mode: 'encrypt', key}`.
|
|
82
|
+
* A token that fails authentication under `key` throws ERR_MAP; nothing is
|
|
83
|
+
* partially restored. With `json: true` restored values are JSON-escaped for
|
|
84
|
+
* tokens inside JSON string literals.
|
|
85
|
+
*/
|
|
86
|
+
export function decrypt(text, key, { json = false } = {}) {
|
|
87
|
+
if (typeof text !== 'string') {
|
|
88
|
+
throw failure('ERR_INPUT');
|
|
89
|
+
}
|
|
90
|
+
derived(key);
|
|
91
|
+
return text.replace(token, (_, type, encoded) => {
|
|
92
|
+
const value = decryptToken(type, encoded, key);
|
|
93
|
+
return json ? JSON.stringify(value).slice(1, -1) : value;
|
|
94
|
+
});
|
|
95
|
+
}
|
package/src/engines.js
CHANGED
|
@@ -18,6 +18,7 @@ import { projectText } from './projection.js';
|
|
|
18
18
|
import { decodedRuns } from './encoded.js';
|
|
19
19
|
import { hasCustomTransforms } from './transforms.js';
|
|
20
20
|
import { builtInEnabled } from './policy.js';
|
|
21
|
+
import { exemptFindingsAsync } from './exempt.js';
|
|
21
22
|
import {
|
|
22
23
|
initializePlaceholders,
|
|
23
24
|
placeholderKnown,
|
|
@@ -220,10 +221,7 @@ async function analyzeStructured(
|
|
|
220
221
|
);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
options = applyProfile(options);
|
|
225
|
-
validateOptions(options);
|
|
226
|
-
options = initializePlaceholders(options);
|
|
224
|
+
function configuredEngines(options) {
|
|
227
225
|
for (const detector of options.detectors ?? []) {
|
|
228
226
|
if (!detector || typeof detector.detect !== 'function') {
|
|
229
227
|
throw failure('ERR_CONFIG');
|
|
@@ -234,6 +232,14 @@ export function createSanitizer(options = {}) {
|
|
|
234
232
|
if (!options.knownOnly) {
|
|
235
233
|
engines.push(...(options.detectors ?? []));
|
|
236
234
|
}
|
|
235
|
+
return engines;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function createSanitizer(options = {}) {
|
|
239
|
+
options = applyProfile(options);
|
|
240
|
+
validateOptions(options);
|
|
241
|
+
options = initializePlaceholders(options);
|
|
242
|
+
const engines = configuredEngines(options);
|
|
237
243
|
let prepared;
|
|
238
244
|
async function analyze(
|
|
239
245
|
text,
|
|
@@ -266,6 +272,7 @@ export function createSanitizer(options = {}) {
|
|
|
266
272
|
...options,
|
|
267
273
|
structured: undefined,
|
|
268
274
|
detectors: [],
|
|
275
|
+
exempt: undefined,
|
|
269
276
|
findings,
|
|
270
277
|
})
|
|
271
278
|
: reviewFindings(text, [...nativeFindings, ...findings], options);
|
|
@@ -274,15 +281,31 @@ export function createSanitizer(options = {}) {
|
|
|
274
281
|
: detected;
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
// The caller's `exempt` hook screens the findings of the text it sees,
|
|
285
|
+
// including the verification passes over the published output.
|
|
286
|
+
async function screened(
|
|
287
|
+
text,
|
|
288
|
+
initial,
|
|
289
|
+
plain,
|
|
290
|
+
scopedOptions = options,
|
|
291
|
+
nativeFindings
|
|
292
|
+
) {
|
|
293
|
+
return exemptFindingsAsync(
|
|
294
|
+
text,
|
|
295
|
+
await analyze(text, initial, plain, scopedOptions, nativeFindings),
|
|
296
|
+
scopedOptions.exempt
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
277
300
|
const sanitizer = {
|
|
278
|
-
inspect: guarded(
|
|
301
|
+
inspect: guarded(screened),
|
|
279
302
|
sanitize: guarded(sanitizeText, () => discardPlaceholderState(options)),
|
|
280
303
|
sanitizeJsonl: (text) =>
|
|
281
304
|
createSanitizer({ ...options, structured: 'jsonl' }).sanitize(text),
|
|
282
305
|
};
|
|
283
306
|
configurations.set(sanitizer, options);
|
|
284
307
|
streamPreparers.set(sanitizer, async (text, nativeFindings) => {
|
|
285
|
-
const findings = await
|
|
308
|
+
const findings = await screened(
|
|
286
309
|
text,
|
|
287
310
|
nativeFindings === undefined ? (options.findings ?? []) : [],
|
|
288
311
|
false,
|
|
@@ -305,7 +328,7 @@ export function createSanitizer(options = {}) {
|
|
|
305
328
|
const findings =
|
|
306
329
|
depth === 0 && prepared?.text === text
|
|
307
330
|
? prepared.findings
|
|
308
|
-
: await
|
|
331
|
+
: await screened(text, options.findings ?? [], plain, options);
|
|
309
332
|
if (depth === 0) {
|
|
310
333
|
prepared = undefined;
|
|
311
334
|
}
|
|
@@ -329,7 +352,7 @@ export function createSanitizer(options = {}) {
|
|
|
329
352
|
recursiveOptions
|
|
330
353
|
)
|
|
331
354
|
);
|
|
332
|
-
if (await customResidual(result.text, options, plain,
|
|
355
|
+
if (await customResidual(result.text, options, plain, screened)) {
|
|
333
356
|
throw failure('ERR_RESIDUAL');
|
|
334
357
|
}
|
|
335
358
|
const sanitized = fullRedaction
|
|
@@ -351,7 +374,7 @@ export function createSanitizer(options = {}) {
|
|
|
351
374
|
).text;
|
|
352
375
|
// Verification at the publication boundary runs every enabled engine.
|
|
353
376
|
if (
|
|
354
|
-
(await
|
|
377
|
+
(await screened(sanitized, [], plain, options)).some((f) =>
|
|
355
378
|
actionable(f, options)
|
|
356
379
|
)
|
|
357
380
|
) {
|
|
@@ -360,7 +383,7 @@ export function createSanitizer(options = {}) {
|
|
|
360
383
|
);
|
|
361
384
|
}
|
|
362
385
|
verifyStructuredResult(result.text, options, plain);
|
|
363
|
-
if (await placeholderResidual(result.text, options, plain,
|
|
386
|
+
if (await placeholderResidual(result.text, options, plain, screened)) {
|
|
364
387
|
throw failure('ERR_RESIDUAL');
|
|
365
388
|
}
|
|
366
389
|
return { ...result, findings: auditFaked(findings, options, result) };
|
package/src/entities.js
CHANGED
|
@@ -3,6 +3,7 @@ import azure from './vendor/entities/azure.json' with { type: 'json' };
|
|
|
3
3
|
import aws from './vendor/entities/aws.json' with { type: 'json' };
|
|
4
4
|
import { escapePattern, collectMatches } from './detection.js';
|
|
5
5
|
import { numericNoise, epochNumber } from './confidence.js';
|
|
6
|
+
import { cryptoAddress } from './crypto-addresses.js';
|
|
6
7
|
import { valueRange, personalValueAllowed } from './context.js';
|
|
7
8
|
export const entityCatalogs = { google, azure, aws };
|
|
8
9
|
const canonical = (name) =>
|
|
@@ -161,13 +162,13 @@ const formatRules = [
|
|
|
161
162
|
],
|
|
162
163
|
[
|
|
163
164
|
'SPAIN_DNI_NUMBER',
|
|
164
|
-
/\b\d{8}[A-Z]\b/g,
|
|
165
|
+
/\b\d{8}-?[A-Z]\b/g,
|
|
165
166
|
(v) => 'TRWAGMYFPDXBNJZSQVHLCKE'[Number(v.slice(0, 8)) % 23] === v.at(-1),
|
|
166
167
|
],
|
|
167
168
|
[
|
|
168
169
|
'CRYPTO_WALLET',
|
|
169
|
-
/\b(?:0x[a-fA-F0-9]{40}|[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[ac-hj-np-z02-9]{25,62})\b/g,
|
|
170
|
-
|
|
170
|
+
/\b(?:0x[a-fA-F0-9]{40}|[13][a-km-zA-HJ-NP-Z1-9]{25,34}|bc1[ac-hj-np-z02-9]{25,62}|BC1[AC-HJ-NP-Z02-9]{25,62})\b/g,
|
|
171
|
+
cryptoAddress,
|
|
171
172
|
],
|
|
172
173
|
['US_HEALTHCARE_NPI', /\b[12]\d{9}\b/g, npi],
|
|
173
174
|
];
|
package/src/exempt.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { failure } from './detection.js';
|
|
2
|
+
|
|
3
|
+
// `exempt(finding, text) => boolean` keeps findings the caller's policy
|
|
4
|
+
// knows are not sensitive (a configuration flag, a counter, a value another
|
|
5
|
+
// tool already masked). It runs after detection and before transformation.
|
|
6
|
+
// The hook decides per span: verification of the published text consults it
|
|
7
|
+
// again for each finding in the output, so the same value in another place
|
|
8
|
+
// is still caught unless the hook keeps that span too.
|
|
9
|
+
// Values the caller declared (`knownSecrets`, `knownPersonal`,
|
|
10
|
+
// `confirmedPersonal`) and structured `JSON_VALUE` denials are never offered.
|
|
11
|
+
const declared = new Set([
|
|
12
|
+
'known-secret',
|
|
13
|
+
'known-personal',
|
|
14
|
+
'confirmed-personal',
|
|
15
|
+
]);
|
|
16
|
+
const offered = (finding) =>
|
|
17
|
+
!declared.has(finding.rule) && finding.type !== 'JSON_VALUE';
|
|
18
|
+
|
|
19
|
+
function view(text, finding) {
|
|
20
|
+
return Object.freeze({
|
|
21
|
+
...finding,
|
|
22
|
+
value: text.slice(finding.start, finding.end),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateExempt(exempt) {
|
|
27
|
+
if (exempt !== undefined && typeof exempt !== 'function') {
|
|
28
|
+
throw failure('ERR_CONFIG');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function decision(kept) {
|
|
33
|
+
if (typeof kept !== 'boolean') {
|
|
34
|
+
// `undefined` from a forgotten return must not silently keep a secret.
|
|
35
|
+
throw failure('ERR_CONFIG');
|
|
36
|
+
}
|
|
37
|
+
return kept;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function exemptFindings(text, findings, exempt) {
|
|
41
|
+
if (!exempt) {
|
|
42
|
+
return findings;
|
|
43
|
+
}
|
|
44
|
+
return findings.filter((finding) => {
|
|
45
|
+
if (!offered(finding)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const kept = exempt(view(text, finding), text);
|
|
49
|
+
if (typeof kept?.then === 'function') {
|
|
50
|
+
// An async hook needs the async engine (`createSanitizer`).
|
|
51
|
+
kept.then(undefined, () => {});
|
|
52
|
+
throw failure('ERR_CONFIG');
|
|
53
|
+
}
|
|
54
|
+
return !decision(kept);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function exemptFindingsAsync(text, findings, exempt) {
|
|
59
|
+
if (!exempt) {
|
|
60
|
+
return findings;
|
|
61
|
+
}
|
|
62
|
+
const retained = [];
|
|
63
|
+
for (const finding of findings) {
|
|
64
|
+
if (
|
|
65
|
+
!offered(finding) ||
|
|
66
|
+
!decision(await exempt(view(text, finding), text))
|
|
67
|
+
) {
|
|
68
|
+
retained.push(finding);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return retained;
|
|
72
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { failure } from './detection.js';
|
|
2
|
+
|
|
3
|
+
// Values that `mode: 'fake'` generated, per transformation object, so that a
|
|
4
|
+
// later pass recognizes them as fictional. Kept apart from fake.js so that the
|
|
5
|
+
// credentials entry can read the (empty) memory without loading faker.
|
|
6
|
+
const generated = new WeakMap();
|
|
7
|
+
export function generatedValues(options) {
|
|
8
|
+
const transforms = [
|
|
9
|
+
options.transformation,
|
|
10
|
+
...Object.values(options.transformations ?? {}),
|
|
11
|
+
];
|
|
12
|
+
return transforms.flatMap((t) =>
|
|
13
|
+
t?.mode === 'fake'
|
|
14
|
+
? [...(generated.get(t) ?? [])].map(([value, credential]) => ({
|
|
15
|
+
value,
|
|
16
|
+
credential,
|
|
17
|
+
}))
|
|
18
|
+
: []
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
export function remember(value, transform, credential) {
|
|
22
|
+
let values = generated.get(transform);
|
|
23
|
+
if (!values) {
|
|
24
|
+
values = new Map();
|
|
25
|
+
generated.set(transform, values);
|
|
26
|
+
}
|
|
27
|
+
if (values.size >= 100000 && !values.has(value)) {
|
|
28
|
+
throw failure('ERR_LIMIT');
|
|
29
|
+
}
|
|
30
|
+
values.set(value, credential);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
package/src/fake.js
CHANGED
|
@@ -4,35 +4,11 @@ import { parsePhoneNumberFromString } from 'libphonenumber-js/max';
|
|
|
4
4
|
import { mrzCheck } from './identity.js';
|
|
5
5
|
import { fakeName } from './fake-names.js';
|
|
6
6
|
import * as checks from './national-checks.js';
|
|
7
|
-
import { REDACTED
|
|
7
|
+
import { REDACTED } from './detection.js';
|
|
8
|
+
import { remember } from './fake-memory.js';
|
|
9
|
+
|
|
10
|
+
export { generatedValues } from './fake-memory.js';
|
|
8
11
|
|
|
9
|
-
const generated = new WeakMap();
|
|
10
|
-
export function generatedValues(options) {
|
|
11
|
-
const transforms = [
|
|
12
|
-
options.transformation,
|
|
13
|
-
...Object.values(options.transformations ?? {}),
|
|
14
|
-
];
|
|
15
|
-
return transforms.flatMap((t) =>
|
|
16
|
-
t?.mode === 'fake'
|
|
17
|
-
? [...(generated.get(t) ?? [])].map(([value, credential]) => ({
|
|
18
|
-
value,
|
|
19
|
-
credential,
|
|
20
|
-
}))
|
|
21
|
-
: []
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
function remember(value, transform, credential) {
|
|
25
|
-
let values = generated.get(transform);
|
|
26
|
-
if (!values) {
|
|
27
|
-
values = new Map();
|
|
28
|
-
generated.set(transform, values);
|
|
29
|
-
}
|
|
30
|
-
if (values.size >= 100000 && !values.has(value)) {
|
|
31
|
-
throw failure('ERR_LIMIT');
|
|
32
|
-
}
|
|
33
|
-
values.set(value, credential);
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
12
|
function digest(value, key, purpose = 'value') {
|
|
37
13
|
return createHmac('sha256', key).update(`${purpose}\0${value}`).digest();
|
|
38
14
|
}
|
|
@@ -116,6 +92,18 @@ const validators = {
|
|
|
116
92
|
SPAIN_DNI_NUMBER: (v) =>
|
|
117
93
|
'TRWAGMYFPDXBNJZSQVHLCKE'[Number(v.slice(0, 8)) % 23] === v.at(-1),
|
|
118
94
|
BRAZIL_CPF_NUMBER: cpf,
|
|
95
|
+
ABA_ROUTING_NUMBER: checks.abaRouting,
|
|
96
|
+
ES_NIE: checks.spanishNie,
|
|
97
|
+
IT_VAT_CODE: checks.shortLuhn,
|
|
98
|
+
AU_ACN: checks.acn,
|
|
99
|
+
IN_AADHAAR: checks.aadhaar,
|
|
100
|
+
TH_TNIN: checks.thaiId,
|
|
101
|
+
MEDICAL_LICENSE: checks.dea,
|
|
102
|
+
IMEI: checks.shortLuhn,
|
|
103
|
+
DE_TAX_ID: checks.germanTaxId,
|
|
104
|
+
AE_EMIRATES_ID: checks.emiratesId,
|
|
105
|
+
SA_NATIONAL_ID: checks.saudiId,
|
|
106
|
+
IL_ID_NUMBER: checks.israeliId,
|
|
119
107
|
};
|
|
120
108
|
function cpf(d) {
|
|
121
109
|
for (const n of [9, 10]) {
|
|
@@ -226,9 +214,24 @@ function idShape(compact, candidate, type) {
|
|
|
226
214
|
candidate =
|
|
227
215
|
candidate.slice(0, 10) + compact.slice(10, 12) + candidate.slice(12);
|
|
228
216
|
}
|
|
229
|
-
if (
|
|
217
|
+
if (
|
|
218
|
+
[
|
|
219
|
+
'SG_NRIC_FIN',
|
|
220
|
+
'ES_NIE',
|
|
221
|
+
'IN_AADHAAR',
|
|
222
|
+
'TH_TNIN',
|
|
223
|
+
'DE_TAX_ID',
|
|
224
|
+
'SA_NATIONAL_ID',
|
|
225
|
+
].includes(type)
|
|
226
|
+
) {
|
|
230
227
|
candidate = compact[0] + candidate.slice(1);
|
|
231
228
|
}
|
|
229
|
+
if (['ABA_ROUTING_NUMBER', 'MEDICAL_LICENSE'].includes(type)) {
|
|
230
|
+
candidate = compact.slice(0, 2) + candidate.slice(2);
|
|
231
|
+
}
|
|
232
|
+
if (type === 'AE_EMIRATES_ID') {
|
|
233
|
+
candidate = compact.slice(0, 7) + candidate.slice(7);
|
|
234
|
+
}
|
|
232
235
|
if (type === 'CREDIT_CARD') {
|
|
233
236
|
candidate = compact.slice(0, 6) + candidate.slice(6);
|
|
234
237
|
}
|
|
@@ -255,6 +258,7 @@ function idChecks(compact, candidate, type) {
|
|
|
255
258
|
'SG_NRIC_FIN',
|
|
256
259
|
'IT_FISCAL_CODE',
|
|
257
260
|
'SPAIN_DNI_NUMBER',
|
|
261
|
+
'ES_NIE',
|
|
258
262
|
].includes(type);
|
|
259
263
|
const count =
|
|
260
264
|
(type === 'RU_INN' && compact.length === 12) ||
|
package/src/features.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Personal-data features that the main entry registers through
|
|
2
|
+
// `personal-features.js`. The `./credentials` entry never imports that module,
|
|
3
|
+
// so it does not load the personal detectors, faker or libphonenumber
|
|
4
|
+
// (issue 137). This module must stay free of imports.
|
|
5
|
+
const registered = {};
|
|
6
|
+
|
|
7
|
+
export function registerFeatures(entries) {
|
|
8
|
+
Object.assign(registered, entries);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Runs the registered implementation, or returns `fallback` without one. */
|
|
12
|
+
export function withFeature(name, fallback, ...args) {
|
|
13
|
+
const value = registered[name];
|
|
14
|
+
return value ? value(...args) : fallback;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The registered implementation; fails closed when it is unavailable. */
|
|
18
|
+
export function feature(name) {
|
|
19
|
+
const value = registered[name];
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw Object.assign(
|
|
22
|
+
new Error(
|
|
23
|
+
'This option needs the personal-data modules of the main entry.'
|
|
24
|
+
),
|
|
25
|
+
{ code: 'ERR_CONFIG' }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
package/src/files.js
CHANGED
|
@@ -4,31 +4,76 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { TextDecoder } from 'node:util';
|
|
6
6
|
import { failure } from './detection.js';
|
|
7
|
-
import { decodeLogBytes } from './log-bytes.js';
|
|
7
|
+
import { decodeLogBytes, encodeLogText } from './log-bytes.js';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// UTF-16 text (Windows logs, PowerShell transcripts) is recognised only by its
|
|
10
|
+
// byte order mark; BOM-less UTF-16 remains binary input.
|
|
11
|
+
export function textEncoding(bytes) {
|
|
12
|
+
if (bytes.length >= 2 && bytes.length % 2 === 0) {
|
|
13
|
+
if (bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
14
|
+
return 'utf-16le';
|
|
15
|
+
}
|
|
16
|
+
if (bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
17
|
+
return 'utf-16be';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return 'utf-8';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decodeWith(encoding, bytes) {
|
|
24
|
+
try {
|
|
25
|
+
return new TextDecoder(encoding, { fatal: true, ignoreBOM: true }).decode(
|
|
26
|
+
bytes
|
|
27
|
+
);
|
|
28
|
+
} catch {
|
|
29
|
+
throw failure('ERR_ENCODING');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Returns the decoded text and the encoding needed to write it back.
|
|
34
|
+
export function decodeTextInput(
|
|
10
35
|
bytes,
|
|
11
36
|
{ lossless = false, allowBinary = false } = {}
|
|
12
37
|
) {
|
|
13
38
|
if (typeof allowBinary !== 'boolean') {
|
|
14
39
|
throw failure('ERR_CONFIG');
|
|
15
40
|
}
|
|
41
|
+
const encoding = textEncoding(bytes);
|
|
42
|
+
if (encoding !== 'utf-8') {
|
|
43
|
+
return { text: decodeWith(encoding, bytes), encoding };
|
|
44
|
+
}
|
|
16
45
|
if (!allowBinary && bytes.includes(0)) {
|
|
17
46
|
throw failure('ERR_BINARY');
|
|
18
47
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
return {
|
|
49
|
+
text: lossless ? decodeLogBytes(bytes) : decodeWith('utf-8', bytes),
|
|
50
|
+
encoding,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function decodeText(bytes, options) {
|
|
55
|
+
return decodeTextInput(bytes, options).text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Encode output in the input encoding; UTF-16 keeps its byte order mark
|
|
59
|
+
// because the decoded text retains it.
|
|
60
|
+
export function encodeTextOutput(text, encoding = 'utf-8') {
|
|
61
|
+
if (encoding === 'utf-8') {
|
|
62
|
+
return encodeLogText(text);
|
|
28
63
|
}
|
|
64
|
+
const bytes = Buffer.from(text, 'utf16le');
|
|
65
|
+
return encoding === 'utf-16be' ? bytes.swap16() : bytes;
|
|
29
66
|
}
|
|
30
67
|
|
|
31
68
|
export async function readTextFile(path, maxBytes = 10 * 1024 * 1024, options) {
|
|
69
|
+
return (await readTextInput(path, maxBytes, options)).text;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function readTextInput(
|
|
73
|
+
path,
|
|
74
|
+
maxBytes = 10 * 1024 * 1024,
|
|
75
|
+
options
|
|
76
|
+
) {
|
|
32
77
|
let handle;
|
|
33
78
|
try {
|
|
34
79
|
const info = await lstat(path);
|
|
@@ -52,7 +97,7 @@ export async function readTextFile(path, maxBytes = 10 * 1024 * 1024, options) {
|
|
|
52
97
|
if (read > maxBytes || read > opened.size) {
|
|
53
98
|
throw failure('ERR_LIMIT');
|
|
54
99
|
}
|
|
55
|
-
return
|
|
100
|
+
return decodeTextInput(bytes.subarray(0, read), options);
|
|
56
101
|
} catch (error) {
|
|
57
102
|
if (error.code?.startsWith('ERR_')) {
|
|
58
103
|
throw error;
|