n8n-nodes-redactor 4.0.0 → 4.2.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/README.md +15 -1
- package/dist/credentials/RedactorVaultApi.credentials.js +23 -0
- package/dist/credentials/redactorVault.dark.svg +1 -0
- package/dist/credentials/redactorVault.svg +1 -0
- package/dist/nodes/Redactor/Redactor.node.js +60 -47
- package/dist/nodes/Redactor/engine.js +366 -287
- package/dist/nodes/Redactor/injection.js +7 -1
- package/dist/nodes/Redactor/profiles.js +7 -6
- package/dist/nodes/Redactor/regexSafety.js +114 -0
- package/dist/nodes/Redactor/runtime.js +80 -0
- package/dist/nodes/Redactor/vault.js +145 -70
- package/package.json +7 -4
- package/dist/nodes/Redactor/Redactor.node.d.ts +0 -5
- package/dist/nodes/Redactor/audit.d.ts +0 -21
- package/dist/nodes/Redactor/classification.d.ts +0 -13
- package/dist/nodes/Redactor/context.d.ts +0 -7
- package/dist/nodes/Redactor/encryption.d.ts +0 -4
- package/dist/nodes/Redactor/engine.d.ts +0 -6
- package/dist/nodes/Redactor/injection.d.ts +0 -20
- package/dist/nodes/Redactor/names.d.ts +0 -8
- package/dist/nodes/Redactor/patterns.d.ts +0 -8
- package/dist/nodes/Redactor/presidio.d.ts +0 -36
- package/dist/nodes/Redactor/profiles.d.ts +0 -20
- package/dist/nodes/Redactor/pseudonymize.d.ts +0 -1
- package/dist/nodes/Redactor/ropa.d.ts +0 -51
- package/dist/nodes/Redactor/types.d.ts +0 -73
- package/dist/nodes/Redactor/vault.d.ts +0 -57
|
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.collectTokenLike = collectTokenLike;
|
|
36
37
|
exports.redactValue = redactValue;
|
|
37
38
|
exports.restoreValue = restoreValue;
|
|
38
39
|
exports.enhanceWithPresidio = enhanceWithPresidio;
|
|
39
40
|
exports.buildReport = buildReport;
|
|
40
41
|
const crypto = __importStar(require("crypto"));
|
|
41
42
|
const patterns_1 = require("./patterns");
|
|
43
|
+
const regexSafety_1 = require("./regexSafety");
|
|
42
44
|
const context_1 = require("./context");
|
|
43
45
|
const names_1 = require("./names");
|
|
44
46
|
const pseudonymize_1 = require("./pseudonymize");
|
|
@@ -95,55 +97,79 @@ function maskValue(value, label) {
|
|
|
95
97
|
function hashValue(value) {
|
|
96
98
|
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
97
99
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
100
|
+
function storeReversible(match, pattern, vault, sessionId, dedup, makeToken) {
|
|
101
|
+
if (dedup) {
|
|
102
|
+
const existing = vault.findByOriginal(sessionId, match);
|
|
103
|
+
if (existing && !existing.numeric)
|
|
104
|
+
return existing.token;
|
|
105
|
+
}
|
|
106
|
+
const token = makeToken();
|
|
107
|
+
vault.addEntry(sessionId, {
|
|
108
|
+
token,
|
|
109
|
+
original: match,
|
|
110
|
+
patternLabel: pattern.label,
|
|
111
|
+
category: ('category' in pattern ? pattern.category : 'identity'),
|
|
112
|
+
createdAt: new Date().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
return token;
|
|
115
|
+
}
|
|
116
|
+
const IRREVERSIBLE = {
|
|
117
|
+
mask: (match, label) => maskValue(match, label),
|
|
118
|
+
hash: (match, label) => `[${label}:${hashValue(match)}]`,
|
|
119
|
+
redact: () => '[REDACTED]',
|
|
120
|
+
blackout: (match) => '\u2588'.repeat(match.length),
|
|
121
|
+
remove: () => '',
|
|
122
|
+
};
|
|
123
|
+
function applyRedaction(match, pattern, mode, vault, sessionId, dedup, reserved) {
|
|
124
|
+
if (mode === 'token') {
|
|
125
|
+
return storeReversible(match, pattern, vault, sessionId, dedup, () => freshToken(pattern.label, vault, sessionId, reserved));
|
|
126
|
+
}
|
|
127
|
+
if (mode === 'pseudonymize') {
|
|
128
|
+
return storeReversible(match, pattern, vault, sessionId, dedup, () => uniquePseudonym(vault, sessionId, match, pattern.label));
|
|
129
|
+
}
|
|
130
|
+
const irreversible = IRREVERSIBLE[mode];
|
|
131
|
+
return irreversible ? irreversible(match, pattern.label) : match;
|
|
132
|
+
}
|
|
133
|
+
function freshToken(label, vault, sessionId, reserved) {
|
|
134
|
+
var _a, _b;
|
|
135
|
+
const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
|
|
136
|
+
let index = Object.keys(entries).length;
|
|
137
|
+
let token = generateToken(label, index);
|
|
138
|
+
while (entries[token] || (reserved === null || reserved === void 0 ? void 0 : reserved.has(token)))
|
|
139
|
+
token = generateToken(label, ++index);
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
|
|
143
|
+
function collectTokenLike(value, into = new Set()) {
|
|
144
|
+
const scan = (text) => {
|
|
145
|
+
for (const m of text.matchAll(TOKEN_LIKE_RE))
|
|
146
|
+
into.add(m[0]);
|
|
147
|
+
};
|
|
148
|
+
if (typeof value === 'string')
|
|
149
|
+
scan(value);
|
|
150
|
+
else if (Array.isArray(value))
|
|
151
|
+
value.forEach((v) => collectTokenLike(v, into));
|
|
152
|
+
else if (value !== null && typeof value === 'object') {
|
|
153
|
+
for (const [k, v] of Object.entries(value)) {
|
|
154
|
+
scan(k);
|
|
155
|
+
collectTokenLike(v, into);
|
|
143
156
|
}
|
|
144
|
-
default:
|
|
145
|
-
return match;
|
|
146
157
|
}
|
|
158
|
+
return into;
|
|
159
|
+
}
|
|
160
|
+
function uniquePseudonym(vault, sessionId, original, label) {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
|
|
163
|
+
for (let attempt = 0; attempt < 32; attempt++) {
|
|
164
|
+
const seed = attempt === 0 ? original : `${original}\u0000${attempt}`;
|
|
165
|
+
const candidate = (0, pseudonymize_1.generatePseudonym)(sessionId, seed, label);
|
|
166
|
+
const taken = entries[candidate];
|
|
167
|
+
if (candidate === original)
|
|
168
|
+
continue;
|
|
169
|
+
if (!taken || taken.original === original)
|
|
170
|
+
return candidate;
|
|
171
|
+
}
|
|
172
|
+
return generateToken(label, Object.keys(entries).length);
|
|
147
173
|
}
|
|
148
174
|
function shouldProcessField(fieldPath, fieldMode, fieldRules) {
|
|
149
175
|
if (fieldMode === 'all')
|
|
@@ -169,17 +195,7 @@ function fieldPathMatches(actual, pattern) {
|
|
|
169
195
|
}
|
|
170
196
|
}
|
|
171
197
|
function isSafeRegex(regexStr) {
|
|
172
|
-
|
|
173
|
-
return false;
|
|
174
|
-
if (regexStr.length > 500)
|
|
175
|
-
return false;
|
|
176
|
-
try {
|
|
177
|
-
new RegExp(regexStr);
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
198
|
+
return (0, regexSafety_1.checkRegex)(regexStr) === null;
|
|
183
199
|
}
|
|
184
200
|
const SEMANTIC_FIELD_MAP = [
|
|
185
201
|
{
|
|
@@ -386,279 +402,342 @@ function getSemanticLabel(fieldKey, siblingKeys) {
|
|
|
386
402
|
}
|
|
387
403
|
return null;
|
|
388
404
|
}
|
|
389
|
-
function
|
|
390
|
-
var _a
|
|
405
|
+
function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
406
|
+
var _a;
|
|
407
|
+
const semantic = getSemanticLabel(path, siblingKeys);
|
|
408
|
+
if (!semantic || !semantic.label || raw.trim().length === 0)
|
|
409
|
+
return null;
|
|
410
|
+
if (isAllowListed(raw, ctx.allowList))
|
|
411
|
+
return null;
|
|
412
|
+
const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
|
|
413
|
+
if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
|
|
414
|
+
return null;
|
|
415
|
+
const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
|
|
416
|
+
hits.push({
|
|
417
|
+
token: replacement, original: '***',
|
|
418
|
+
patternName: `semantic_${semantic.label}`,
|
|
419
|
+
patternLabel: semantic.label,
|
|
420
|
+
category: semantic.category,
|
|
421
|
+
field: display, itemIndex,
|
|
422
|
+
confidence: semConf,
|
|
423
|
+
});
|
|
424
|
+
return replacement;
|
|
425
|
+
}
|
|
426
|
+
function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
427
|
+
var _a;
|
|
428
|
+
const text = String(value);
|
|
429
|
+
const numCtx = { ...ctx, dedup: false };
|
|
430
|
+
let out = null;
|
|
431
|
+
if (!ctx.skipSemantic)
|
|
432
|
+
out = redactSemantic(text, numCtx, vault, sessionId, hits, itemIndex, path, display, siblingKeys);
|
|
433
|
+
if (out === null && ctx.scanNumbers)
|
|
434
|
+
out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
|
|
435
|
+
if (out === null || out === text)
|
|
436
|
+
return value;
|
|
437
|
+
const entry = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries[out];
|
|
438
|
+
if (entry && entry.original === text)
|
|
439
|
+
entry.numeric = true;
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
|
|
443
|
+
var _a;
|
|
444
|
+
const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
|
|
445
|
+
if (out === key)
|
|
446
|
+
return key;
|
|
447
|
+
const clashes = (k) => k === '' || k in taken || sourceKeys.has(k);
|
|
448
|
+
if (!clashes(out))
|
|
449
|
+
return out;
|
|
450
|
+
if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
|
|
451
|
+
const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...sourceKeys, ...Object.keys(taken)]));
|
|
452
|
+
vault.addEntry(sessionId, { token, original: key, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
|
|
453
|
+
return token;
|
|
454
|
+
}
|
|
455
|
+
let suffixed = out;
|
|
456
|
+
for (let n = 1; clashes(suffixed); n++)
|
|
457
|
+
suffixed = `${out}_${n}`;
|
|
458
|
+
return suffixed;
|
|
459
|
+
}
|
|
460
|
+
function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, display) {
|
|
461
|
+
var _a;
|
|
462
|
+
const entries = Object.entries(value);
|
|
463
|
+
const siblingKeys = entries.map(([k]) => k);
|
|
464
|
+
const sourceKeys = new Set(siblingKeys);
|
|
465
|
+
const origins = new Map();
|
|
466
|
+
const obj = {};
|
|
467
|
+
for (const [k, v] of entries) {
|
|
468
|
+
const fieldPath = path ? `${path}.${k}` : k;
|
|
469
|
+
const keyHits = [];
|
|
470
|
+
const outKey = shouldProcessField(fieldPath, ctx.fieldMode, ctx.fieldRules)
|
|
471
|
+
? redactKey(k, obj, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits)
|
|
472
|
+
: k;
|
|
473
|
+
origins.set(outKey, k);
|
|
474
|
+
const childDisplay = display ? `${display}.${outKey}` : outKey;
|
|
475
|
+
for (const h of keyHits)
|
|
476
|
+
hits.push({ ...h, field: childDisplay });
|
|
477
|
+
obj[outKey] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay);
|
|
478
|
+
}
|
|
479
|
+
(_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
|
|
480
|
+
return obj;
|
|
481
|
+
}
|
|
482
|
+
function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath = '', siblingKeys, displayPath = currentPath) {
|
|
483
|
+
const processed = shouldProcessField(currentPath, ctx.fieldMode, ctx.fieldRules);
|
|
391
484
|
if (typeof value === 'string') {
|
|
392
|
-
if (!
|
|
485
|
+
if (!processed)
|
|
393
486
|
return value;
|
|
394
|
-
}
|
|
395
487
|
if (!ctx.skipSemantic) {
|
|
396
|
-
const semantic =
|
|
397
|
-
if (semantic
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
|
|
402
|
-
if (semConf >= ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0)) {
|
|
403
|
-
const replacement = applyRedaction(value, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup);
|
|
404
|
-
hits.push({
|
|
405
|
-
token: replacement, original: '***',
|
|
406
|
-
patternName: `semantic_${semantic.label}`,
|
|
407
|
-
patternLabel: semantic.label,
|
|
408
|
-
category: semantic.category,
|
|
409
|
-
field: currentPath, itemIndex,
|
|
410
|
-
confidence: semConf,
|
|
411
|
-
});
|
|
412
|
-
return replacement;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
488
|
+
const semantic = redactSemantic(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
|
|
489
|
+
if (semantic !== null)
|
|
490
|
+
return semantic;
|
|
416
491
|
}
|
|
417
|
-
return redactText(value, ctx, vault, sessionId, hits, itemIndex,
|
|
492
|
+
return redactText(value, ctx, vault, sessionId, hits, itemIndex, displayPath);
|
|
418
493
|
}
|
|
419
|
-
if (typeof value === 'number'
|
|
420
|
-
if (!
|
|
494
|
+
if (typeof value === 'number') {
|
|
495
|
+
if (!processed)
|
|
421
496
|
return value;
|
|
422
|
-
|
|
423
|
-
const semantic = getSemanticLabel(currentPath, siblingKeys);
|
|
424
|
-
if (semantic && semantic.label) {
|
|
425
|
-
const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
|
|
426
|
-
if (semConf >= ((_b = ctx.confidenceThreshold) !== null && _b !== void 0 ? _b : 0)) {
|
|
427
|
-
const strValue = String(value);
|
|
428
|
-
const replacement = applyRedaction(strValue, { label: semantic.label, category: semantic.category }, ctx.mode, vault, sessionId, ctx.dedup);
|
|
429
|
-
hits.push({
|
|
430
|
-
token: replacement, original: '***',
|
|
431
|
-
patternName: `semantic_${semantic.label}`,
|
|
432
|
-
patternLabel: semantic.label,
|
|
433
|
-
category: semantic.category,
|
|
434
|
-
field: currentPath, itemIndex,
|
|
435
|
-
confidence: semConf,
|
|
436
|
-
});
|
|
437
|
-
return replacement;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
497
|
+
return redactNumber(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
|
|
440
498
|
}
|
|
441
499
|
if (Array.isArray(value)) {
|
|
442
|
-
return value.map((v, i) => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`));
|
|
500
|
+
return value.map((v, i) => redactValue(v, ctx, vault, sessionId, hits, itemIndex, `${currentPath}[${i}]`, undefined, `${displayPath}[${i}]`));
|
|
443
501
|
}
|
|
444
502
|
if (value !== null && typeof value === 'object') {
|
|
445
|
-
|
|
446
|
-
const siblingKeys = entries.map(([k]) => k);
|
|
447
|
-
const obj = {};
|
|
448
|
-
for (const [k, v] of entries) {
|
|
449
|
-
const fieldPath = currentPath ? `${currentPath}.${k}` : k;
|
|
450
|
-
obj[k] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys);
|
|
451
|
-
}
|
|
452
|
-
return obj;
|
|
503
|
+
return redactObject(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath);
|
|
453
504
|
}
|
|
454
505
|
return value;
|
|
455
506
|
}
|
|
456
|
-
|
|
507
|
+
const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
508
|
+
function segments(text, produced) {
|
|
457
509
|
var _a;
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
break;
|
|
470
|
-
case 'contains':
|
|
471
|
-
re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
472
|
-
break;
|
|
473
|
-
default:
|
|
474
|
-
re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
475
|
-
}
|
|
476
|
-
result = result.replace(re, (match) => {
|
|
477
|
-
if (TOKEN_RE.test(match))
|
|
478
|
-
return match;
|
|
479
|
-
const replacement = applyRedaction(match, { label: 'DENY_LIST', category: 'other' }, ctx.mode, vault, sessionId, ctx.dedup);
|
|
480
|
-
hits.push({
|
|
481
|
-
token: replacement, original: '***',
|
|
482
|
-
patternName: 'denyList', patternLabel: 'DENY_LIST',
|
|
483
|
-
category: 'other', field: fieldPath, itemIndex,
|
|
484
|
-
confidence: context_1.DENY_LIST_CONFIDENCE,
|
|
485
|
-
});
|
|
486
|
-
return replacement;
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
catch { }
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
const enabled = (0, patterns_1.getPatternsByNames)(ctx.enabledPatterns);
|
|
493
|
-
const activePatterns = [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)];
|
|
494
|
-
for (const pattern of activePatterns) {
|
|
495
|
-
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
496
|
-
result = result.replace(re, (match, ...args) => {
|
|
497
|
-
if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
|
|
498
|
-
return match;
|
|
499
|
-
const validatorPassed = pattern.validate ? pattern.validate(match) : true;
|
|
500
|
-
if (pattern.validate && !validatorPassed)
|
|
501
|
-
return match;
|
|
502
|
-
if (isAllowListed(match, ctx.allowList))
|
|
503
|
-
return match;
|
|
504
|
-
const matchOffset = typeof args[args.length - 2] === 'number' ? args[args.length - 2] : 0;
|
|
505
|
-
const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, text, matchOffset, matchOffset + match.length);
|
|
506
|
-
if (confidence < threshold)
|
|
507
|
-
return match;
|
|
508
|
-
const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup);
|
|
509
|
-
hits.push({
|
|
510
|
-
token: replacement, original: '***',
|
|
511
|
-
patternName: pattern.name, patternLabel: pattern.label,
|
|
512
|
-
category: pattern.category, field: fieldPath, itemIndex,
|
|
513
|
-
confidence,
|
|
514
|
-
});
|
|
515
|
-
return replacement;
|
|
516
|
-
});
|
|
510
|
+
if (produced.size === 0)
|
|
511
|
+
return [{ text, locked: false }];
|
|
512
|
+
const guard = new RegExp([...produced].sort((a, b) => b.length - a.length).map(escapeRe).join('|'), 'g');
|
|
513
|
+
const out = [];
|
|
514
|
+
let last = 0;
|
|
515
|
+
for (const m of text.matchAll(guard)) {
|
|
516
|
+
const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
517
|
+
if (at > last)
|
|
518
|
+
out.push({ text: text.slice(last, at), locked: false });
|
|
519
|
+
out.push({ text: m[0], locked: true });
|
|
520
|
+
last = at + m[0].length;
|
|
517
521
|
}
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
522
|
+
if (last < text.length)
|
|
523
|
+
out.push({ text: text.slice(last), locked: false });
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
function replaceOutside(text, produced, re, fn) {
|
|
527
|
+
return segments(text, produced)
|
|
528
|
+
.map((seg) => (seg.locked ? seg.text : seg.text.replace(new RegExp(re.source, re.flags), fn)))
|
|
529
|
+
.join('');
|
|
530
|
+
}
|
|
531
|
+
function occursOutside(text, produced, needle) {
|
|
532
|
+
return segments(text, produced).some((seg) => !seg.locked && seg.text.includes(needle));
|
|
533
|
+
}
|
|
534
|
+
function redactMatch(pass, match, pattern, detail) {
|
|
535
|
+
const { ctx, vault, sessionId } = pass;
|
|
536
|
+
const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
|
|
537
|
+
pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...detail });
|
|
538
|
+
if (replacement)
|
|
539
|
+
pass.produced.add(replacement);
|
|
540
|
+
return replacement;
|
|
541
|
+
}
|
|
542
|
+
function denyListRegex(entry) {
|
|
543
|
+
if (entry.type === 'regex')
|
|
544
|
+
return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
|
|
545
|
+
return new RegExp(escapeRe(entry.value), 'gi');
|
|
546
|
+
}
|
|
547
|
+
function denyListStage(pass) {
|
|
548
|
+
var _a;
|
|
549
|
+
for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
|
|
550
|
+
try {
|
|
551
|
+
const re = denyListRegex(entry);
|
|
552
|
+
if (!re)
|
|
530
553
|
continue;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
category: 'identity', field: fieldPath, itemIndex,
|
|
537
|
-
confidence: nameConfidence,
|
|
538
|
-
});
|
|
554
|
+
pass.result = replaceOutside(pass.result, pass.produced, re, (match) => TOKEN_RE.test(match)
|
|
555
|
+
? match
|
|
556
|
+
: redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
|
|
557
|
+
patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
|
|
558
|
+
}));
|
|
539
559
|
}
|
|
560
|
+
catch { }
|
|
540
561
|
}
|
|
541
|
-
|
|
542
|
-
|
|
562
|
+
}
|
|
563
|
+
function builtInMatch(pass, pattern, match, args) {
|
|
564
|
+
if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
|
|
565
|
+
return match;
|
|
566
|
+
const validatorPassed = pattern.validate ? pattern.validate(match) : true;
|
|
567
|
+
if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
|
|
568
|
+
return match;
|
|
569
|
+
const offsetArg = args[args.length - 2];
|
|
570
|
+
const offset = typeof offsetArg === 'number' ? offsetArg : 0;
|
|
571
|
+
const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
|
|
572
|
+
if (confidence < pass.threshold)
|
|
573
|
+
return match;
|
|
574
|
+
return redactMatch(pass, match, pattern, {
|
|
575
|
+
patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function builtInStage(pass) {
|
|
579
|
+
const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
|
|
580
|
+
for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
|
|
581
|
+
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
582
|
+
pass.result = replaceOutside(pass.result, pass.produced, re, (match, ...args) => builtInMatch(pass, pattern, match, args));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function nameStage(pass) {
|
|
586
|
+
if (pass.ctx.skipSemantic)
|
|
587
|
+
return;
|
|
588
|
+
for (const nm of (0, names_1.detectNamesInText)(pass.result)) {
|
|
589
|
+
if ((nm.name.includes('[') && nm.name.includes(']')) || isAllowListed(nm.name, pass.ctx.allowList))
|
|
590
|
+
continue;
|
|
591
|
+
const confidence = (0, context_1.hasContextWords)(pass.source, nm.start, nm.end, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
|
|
592
|
+
if (confidence < pass.threshold || !occursOutside(pass.result, pass.produced, nm.name))
|
|
543
593
|
continue;
|
|
544
|
-
|
|
594
|
+
const before = new Set(pass.produced);
|
|
595
|
+
const replacement = redactMatch(pass, nm.name, { label: 'PERSON_NAME', category: 'identity' }, {
|
|
596
|
+
patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
|
|
597
|
+
});
|
|
598
|
+
pass.result = replaceOutside(pass.result, before, new RegExp(escapeRe(nm.name), 'g'), () => replacement);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function customStage(pass) {
|
|
602
|
+
if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
|
|
603
|
+
return;
|
|
604
|
+
for (const cp of pass.ctx.customPatterns) {
|
|
605
|
+
if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
|
|
545
606
|
continue;
|
|
607
|
+
const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
|
|
546
608
|
try {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (isAllowListed(match, ctx.allowList))
|
|
552
|
-
return match;
|
|
553
|
-
if (context_1.CUSTOM_PATTERN_CONFIDENCE < threshold)
|
|
554
|
-
return match;
|
|
555
|
-
const pseudoPattern = {
|
|
556
|
-
label: cp.label.toUpperCase(),
|
|
557
|
-
category: cp.category || 'identity',
|
|
558
|
-
};
|
|
559
|
-
const replacement = applyRedaction(match, pseudoPattern, ctx.mode, vault, sessionId, ctx.dedup);
|
|
560
|
-
hits.push({
|
|
561
|
-
token: replacement, original: '***',
|
|
562
|
-
patternName: `custom_${cp.label}`, patternLabel: pseudoPattern.label,
|
|
563
|
-
category: pseudoPattern.category, field: fieldPath, itemIndex,
|
|
609
|
+
pass.result = replaceOutside(pass.result, pass.produced, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
|
|
610
|
+
? match
|
|
611
|
+
: redactMatch(pass, match, pattern, {
|
|
612
|
+
patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
|
|
564
613
|
confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
|
|
565
|
-
});
|
|
566
|
-
return replacement;
|
|
567
|
-
});
|
|
614
|
+
}));
|
|
568
615
|
}
|
|
569
|
-
catch {
|
|
616
|
+
catch { }
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
|
|
620
|
+
var _a;
|
|
621
|
+
const pass = {
|
|
622
|
+
source: text, result: text, produced: new Set(), threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
|
|
623
|
+
ctx, vault, sessionId, hits, itemIndex, fieldPath,
|
|
624
|
+
};
|
|
625
|
+
denyListStage(pass);
|
|
626
|
+
builtInStage(pass);
|
|
627
|
+
nameStage(pass);
|
|
628
|
+
customStage(pass);
|
|
629
|
+
return pass.result;
|
|
630
|
+
}
|
|
631
|
+
function makeRestorer(entries) {
|
|
632
|
+
const tokens = Object.keys(entries).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
|
|
633
|
+
if (tokens.length === 0)
|
|
634
|
+
return (text) => text;
|
|
635
|
+
const re = new RegExp(tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
636
|
+
return (text) => text.replace(re, (token) => entries[token].original);
|
|
637
|
+
}
|
|
638
|
+
function restoreWith(value, restore, entries) {
|
|
639
|
+
if (typeof value === 'string') {
|
|
640
|
+
const exact = entries[value];
|
|
641
|
+
if ((exact === null || exact === void 0 ? void 0 : exact.numeric) && Number.isFinite(Number(exact.original)))
|
|
642
|
+
return Number(exact.original);
|
|
643
|
+
return restore(value);
|
|
644
|
+
}
|
|
645
|
+
if (Array.isArray(value))
|
|
646
|
+
return value.map((v) => restoreWith(v, restore, entries));
|
|
647
|
+
if (value !== null && typeof value === 'object') {
|
|
648
|
+
const obj = {};
|
|
649
|
+
for (const [k, v] of Object.entries(value)) {
|
|
650
|
+
obj[restore(k)] = restoreWith(v, restore, entries);
|
|
570
651
|
}
|
|
652
|
+
return obj;
|
|
571
653
|
}
|
|
572
|
-
return
|
|
654
|
+
return value;
|
|
573
655
|
}
|
|
574
656
|
function restoreValue(value, vault, sessionId) {
|
|
575
657
|
const session = vault.getSession(sessionId);
|
|
576
658
|
if (!session)
|
|
577
659
|
return value;
|
|
660
|
+
return restoreWith(value, makeRestorer(session.entries), session.entries);
|
|
661
|
+
}
|
|
662
|
+
function toUtf16Spans(text, entities) {
|
|
663
|
+
const offsets = [0];
|
|
664
|
+
for (const ch of text)
|
|
665
|
+
offsets.push(offsets[offsets.length - 1] + ch.length);
|
|
666
|
+
return entities
|
|
667
|
+
.filter((e) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.end > e.start && e.end < offsets.length)
|
|
668
|
+
.map((e) => ({ start: offsets[e.start], end: offsets[e.end], score: e.score, entityType: e.entity_type }));
|
|
669
|
+
}
|
|
670
|
+
function resolveOverlaps(spans) {
|
|
671
|
+
const ranked = [...spans].sort((a, b) => b.score - a.score || (b.end - b.start) - (a.end - a.start) || a.start - b.start);
|
|
672
|
+
const kept = [];
|
|
673
|
+
for (const span of ranked) {
|
|
674
|
+
if (!kept.some((k) => span.start < k.end && k.start < span.end))
|
|
675
|
+
kept.push(span);
|
|
676
|
+
}
|
|
677
|
+
return kept.sort((a, b) => b.start - a.start);
|
|
678
|
+
}
|
|
679
|
+
async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
|
|
680
|
+
var _a;
|
|
681
|
+
if (value.trim().length === 0 || TOKEN_RE.test(value))
|
|
682
|
+
return value;
|
|
683
|
+
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
684
|
+
const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
|
|
685
|
+
const spans = resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold));
|
|
686
|
+
let text = value;
|
|
687
|
+
for (const span of spans) {
|
|
688
|
+
const matchedText = value.slice(span.start, span.end);
|
|
689
|
+
if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
|
|
690
|
+
continue;
|
|
691
|
+
if (isAllowListed(matchedText, ctx.allowList))
|
|
692
|
+
continue;
|
|
693
|
+
const label = (0, presidio_1.getRedactorLabel)(span.entityType);
|
|
694
|
+
const category = (0, presidio_1.getRedactorCategory)(span.entityType);
|
|
695
|
+
const replacement = applyRedaction(matchedText, { label, category }, ctx.mode, vault, sessionId, ctx.dedup, ctx.reservedTokens);
|
|
696
|
+
text = text.slice(0, span.start) + replacement + text.slice(span.end);
|
|
697
|
+
hits.push({
|
|
698
|
+
token: replacement, original: '***',
|
|
699
|
+
patternName: `presidio_${span.entityType}`, patternLabel: label,
|
|
700
|
+
category, field: path, itemIndex, confidence: span.score,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return text;
|
|
704
|
+
}
|
|
705
|
+
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
|
|
706
|
+
var _a, _b;
|
|
578
707
|
if (typeof value === 'string') {
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
708
|
+
if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
|
|
709
|
+
return value;
|
|
710
|
+
try {
|
|
711
|
+
return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
return value;
|
|
582
715
|
}
|
|
583
|
-
return result;
|
|
584
716
|
}
|
|
585
717
|
if (Array.isArray(value)) {
|
|
586
|
-
|
|
718
|
+
const out = [];
|
|
719
|
+
for (let i = 0; i < value.length; i++) {
|
|
720
|
+
out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`));
|
|
721
|
+
}
|
|
722
|
+
return out;
|
|
587
723
|
}
|
|
588
724
|
if (value !== null && typeof value === 'object') {
|
|
589
|
-
const
|
|
725
|
+
const origins = (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.get(value);
|
|
726
|
+
const out = {};
|
|
590
727
|
for (const [k, v] of Object.entries(value)) {
|
|
591
|
-
|
|
728
|
+
const original = (_b = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _b !== void 0 ? _b : k;
|
|
729
|
+
const child = path ? `${path}.${k}` : k;
|
|
730
|
+
const childPolicy = policyPath ? `${policyPath}.${original}` : original;
|
|
731
|
+
out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
|
|
592
732
|
}
|
|
593
|
-
return
|
|
733
|
+
return out;
|
|
594
734
|
}
|
|
595
735
|
return value;
|
|
596
736
|
}
|
|
597
737
|
async function enhanceWithPresidio(data, ctx, vault, sessionId, hits, itemIndex) {
|
|
598
|
-
var _a;
|
|
599
738
|
if (!ctx.presidioUrl)
|
|
600
739
|
return data;
|
|
601
|
-
|
|
602
|
-
for (const [key, value] of Object.entries(result)) {
|
|
603
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
604
|
-
result[key] = await enhanceWithPresidio(value, ctx, vault, sessionId, hits, itemIndex);
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
if (Array.isArray(value)) {
|
|
608
|
-
const arr = [];
|
|
609
|
-
for (const item of value) {
|
|
610
|
-
if (item !== null && typeof item === 'object') {
|
|
611
|
-
arr.push(await enhanceWithPresidio(item, ctx, vault, sessionId, hits, itemIndex));
|
|
612
|
-
}
|
|
613
|
-
else {
|
|
614
|
-
arr.push(item);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
result[key] = arr;
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
if (typeof value !== 'string')
|
|
621
|
-
continue;
|
|
622
|
-
if (value.trim().length === 0)
|
|
623
|
-
continue;
|
|
624
|
-
if (TOKEN_RE.test(value))
|
|
625
|
-
continue;
|
|
626
|
-
try {
|
|
627
|
-
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
628
|
-
if (entities.length === 0)
|
|
629
|
-
continue;
|
|
630
|
-
const sorted = entities.sort((a, b) => b.start - a.start);
|
|
631
|
-
let text = value;
|
|
632
|
-
for (const entity of sorted) {
|
|
633
|
-
const matchedText = text.slice(entity.start, entity.end);
|
|
634
|
-
if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
|
|
635
|
-
continue;
|
|
636
|
-
if (isAllowListed(matchedText, ctx.allowList))
|
|
637
|
-
continue;
|
|
638
|
-
if (entity.score < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
|
|
639
|
-
continue;
|
|
640
|
-
const label = (0, presidio_1.getRedactorLabel)(entity.entity_type);
|
|
641
|
-
const category = (0, presidio_1.getRedactorCategory)(entity.entity_type);
|
|
642
|
-
const replacement = applyRedaction(matchedText, { label, category }, ctx.mode, vault, sessionId, ctx.dedup);
|
|
643
|
-
text = text.slice(0, entity.start) + replacement + text.slice(entity.end);
|
|
644
|
-
hits.push({
|
|
645
|
-
token: replacement,
|
|
646
|
-
original: '***',
|
|
647
|
-
patternName: `presidio_${entity.entity_type}`,
|
|
648
|
-
patternLabel: label,
|
|
649
|
-
category,
|
|
650
|
-
field: key,
|
|
651
|
-
itemIndex,
|
|
652
|
-
confidence: entity.score,
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
result[key] = text;
|
|
656
|
-
}
|
|
657
|
-
catch {
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
return result;
|
|
740
|
+
return (await presidioWalk(data, ctx, vault, sessionId, hits, itemIndex, '', ''));
|
|
662
741
|
}
|
|
663
742
|
function buildReport(sessionId, hits) {
|
|
664
743
|
const hitsByCategory = {};
|