n8n-nodes-redactor 4.1.0 → 4.3.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/dist/nodes/Redactor/Redactor.node.js +20 -36
- package/dist/nodes/Redactor/engine.js +287 -152
- package/dist/nodes/Redactor/injection.js +6 -0
- package/dist/nodes/Redactor/regexCharset.js +107 -0
- package/dist/nodes/Redactor/regexSafety.js +44 -7
- package/dist/nodes/Redactor/runtime.js +45 -26
- package/dist/nodes/Redactor/vault.js +30 -0
- package/package.json +1 -1
|
@@ -1036,6 +1036,11 @@ class Redactor {
|
|
|
1036
1036
|
presidioUrl: enablePresidio ? presidioUrl : undefined,
|
|
1037
1037
|
presidioLanguage: enablePresidio ? presidioLanguage : undefined,
|
|
1038
1038
|
presidioRequest: enablePresidio ? presidioRequester : undefined,
|
|
1039
|
+
scanNumbers: true,
|
|
1040
|
+
reservedTokens: (0, engine_1.collectTokenLike)(items.map((it) => it.json)),
|
|
1041
|
+
keyOrigins: new WeakMap(),
|
|
1042
|
+
inputText: (0, engine_1.collectInputText)(items.map((it) => it.json)),
|
|
1043
|
+
lockedSpans: new Map(),
|
|
1039
1044
|
};
|
|
1040
1045
|
const allHits = [];
|
|
1041
1046
|
const returnData = [];
|
|
@@ -1118,48 +1123,27 @@ class Redactor {
|
|
|
1118
1123
|
if (!purgeSessionId) {
|
|
1119
1124
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
|
|
1120
1125
|
}
|
|
1121
|
-
const
|
|
1122
|
-
if (session) {
|
|
1123
|
-
const entryCount = Object.keys(session.entries).length;
|
|
1124
|
-
vault.deleteSession(purgeSessionId);
|
|
1125
|
-
return [[{
|
|
1126
|
-
json: {
|
|
1127
|
-
purged: true,
|
|
1128
|
-
sessionId: purgeSessionId,
|
|
1129
|
-
entriesDeleted: entryCount,
|
|
1130
|
-
message: `Session purged successfully.`,
|
|
1131
|
-
},
|
|
1132
|
-
pairedItem: allPaired,
|
|
1133
|
-
}]];
|
|
1134
|
-
}
|
|
1135
|
-
else {
|
|
1136
|
-
return [[{
|
|
1137
|
-
json: {
|
|
1138
|
-
purged: false,
|
|
1139
|
-
sessionId: purgeSessionId,
|
|
1140
|
-
message: `No session found with this ID.`,
|
|
1141
|
-
},
|
|
1142
|
-
pairedItem: allPaired,
|
|
1143
|
-
}]];
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
else {
|
|
1147
|
-
const sessions = vault.listSessions();
|
|
1148
|
-
let totalEntries = 0;
|
|
1149
|
-
for (const s of sessions) {
|
|
1150
|
-
totalEntries += s.entryCount;
|
|
1151
|
-
vault.deleteSession(s.sessionId);
|
|
1152
|
-
}
|
|
1126
|
+
const purged = vault.purgeSession(purgeSessionId);
|
|
1153
1127
|
return [[{
|
|
1154
1128
|
json: {
|
|
1155
|
-
purged:
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
message:
|
|
1129
|
+
purged: purged.found,
|
|
1130
|
+
sessionId: purgeSessionId,
|
|
1131
|
+
...(purged.found ? { entriesDeleted: purged.entries } : {}),
|
|
1132
|
+
message: purged.found ? 'Session purged successfully.' : 'No session found with this ID.',
|
|
1159
1133
|
},
|
|
1160
1134
|
pairedItem: allPaired,
|
|
1161
1135
|
}]];
|
|
1162
1136
|
}
|
|
1137
|
+
const all = vault.purgeAll();
|
|
1138
|
+
return [[{
|
|
1139
|
+
json: {
|
|
1140
|
+
purged: true,
|
|
1141
|
+
sessionsPurged: all.sessions,
|
|
1142
|
+
totalEntriesDeleted: all.entries,
|
|
1143
|
+
message: `All ${all.sessions} session(s) purged successfully.`,
|
|
1144
|
+
},
|
|
1145
|
+
pairedItem: allPaired,
|
|
1146
|
+
}]];
|
|
1163
1147
|
}
|
|
1164
1148
|
return [[]];
|
|
1165
1149
|
}
|
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.collectTokenLike = collectTokenLike;
|
|
37
|
+
exports.collectInputText = collectInputText;
|
|
36
38
|
exports.redactValue = redactValue;
|
|
37
39
|
exports.restoreValue = restoreValue;
|
|
38
40
|
exports.enhanceWithPresidio = enhanceWithPresidio;
|
|
@@ -96,10 +98,10 @@ function maskValue(value, label) {
|
|
|
96
98
|
function hashValue(value) {
|
|
97
99
|
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
98
100
|
}
|
|
99
|
-
function storeReversible(match, pattern, vault, sessionId,
|
|
100
|
-
if (dedup) {
|
|
101
|
+
function storeReversible(match, pattern, ctx, vault, sessionId, makeToken) {
|
|
102
|
+
if (ctx.dedup) {
|
|
101
103
|
const existing = vault.findByOriginal(sessionId, match);
|
|
102
|
-
if (existing)
|
|
104
|
+
if (existing && !existing.numeric)
|
|
103
105
|
return existing.token;
|
|
104
106
|
}
|
|
105
107
|
const token = makeToken();
|
|
@@ -119,30 +121,66 @@ const IRREVERSIBLE = {
|
|
|
119
121
|
blackout: (match) => '\u2588'.repeat(match.length),
|
|
120
122
|
remove: () => '',
|
|
121
123
|
};
|
|
122
|
-
function applyRedaction(match, pattern,
|
|
123
|
-
if (mode === 'token') {
|
|
124
|
-
return storeReversible(match, pattern, vault, sessionId,
|
|
125
|
-
const session = vault.getSession(sessionId);
|
|
126
|
-
return generateToken(pattern.label, session ? Object.keys(session.entries).length : 0);
|
|
127
|
-
});
|
|
124
|
+
function applyRedaction(match, pattern, ctx, vault, sessionId) {
|
|
125
|
+
if (ctx.mode === 'token') {
|
|
126
|
+
return storeReversible(match, pattern, ctx, vault, sessionId, () => freshToken(pattern.label, vault, sessionId, ctx.reservedTokens));
|
|
128
127
|
}
|
|
129
|
-
if (mode === 'pseudonymize') {
|
|
130
|
-
return storeReversible(match, pattern, vault, sessionId,
|
|
128
|
+
if (ctx.mode === 'pseudonymize') {
|
|
129
|
+
return storeReversible(match, pattern, ctx, vault, sessionId, () => uniquePseudonym(vault, sessionId, match, pattern.label, ctx));
|
|
131
130
|
}
|
|
132
|
-
const irreversible = IRREVERSIBLE[mode];
|
|
131
|
+
const irreversible = IRREVERSIBLE[ctx.mode];
|
|
133
132
|
return irreversible ? irreversible(match, pattern.label) : match;
|
|
134
133
|
}
|
|
135
|
-
function
|
|
134
|
+
function freshToken(label, vault, sessionId, reserved) {
|
|
136
135
|
var _a, _b;
|
|
137
136
|
const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
|
|
137
|
+
let index = Object.keys(entries).length;
|
|
138
|
+
let token = generateToken(label, index);
|
|
139
|
+
while (entries[token] || (reserved === null || reserved === void 0 ? void 0 : reserved.has(token)))
|
|
140
|
+
token = generateToken(label, ++index);
|
|
141
|
+
return token;
|
|
142
|
+
}
|
|
143
|
+
const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
|
|
144
|
+
function walkText(value, visit) {
|
|
145
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
146
|
+
visit(String(value));
|
|
147
|
+
else if (Array.isArray(value))
|
|
148
|
+
value.forEach((v) => walkText(v, visit));
|
|
149
|
+
else if (value !== null && typeof value === 'object') {
|
|
150
|
+
for (const [k, v] of Object.entries(value)) {
|
|
151
|
+
visit(k);
|
|
152
|
+
walkText(v, visit);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function collectTokenLike(value, into = new Set()) {
|
|
157
|
+
walkText(value, (text) => {
|
|
158
|
+
for (const m of text.matchAll(TOKEN_LIKE_RE))
|
|
159
|
+
into.add(m[0]);
|
|
160
|
+
});
|
|
161
|
+
return into;
|
|
162
|
+
}
|
|
163
|
+
function collectInputText(value) {
|
|
164
|
+
const parts = [];
|
|
165
|
+
walkText(value, (text) => parts.push(text));
|
|
166
|
+
return parts.join('\u0000');
|
|
167
|
+
}
|
|
168
|
+
function reusable(taken, original, ctx) {
|
|
169
|
+
return !!taken && taken.original === original && !!taken.numeric === !!ctx.numeric;
|
|
170
|
+
}
|
|
171
|
+
function uniquePseudonym(vault, sessionId, original, label, ctx) {
|
|
172
|
+
var _a, _b, _c;
|
|
173
|
+
const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
|
|
138
174
|
for (let attempt = 0; attempt < 32; attempt++) {
|
|
139
175
|
const seed = attempt === 0 ? original : `${original}\u0000${attempt}`;
|
|
140
176
|
const candidate = (0, pseudonymize_1.generatePseudonym)(sessionId, seed, label);
|
|
177
|
+
if (candidate === original || ((_c = ctx.inputText) === null || _c === void 0 ? void 0 : _c.includes(candidate)))
|
|
178
|
+
continue;
|
|
141
179
|
const taken = entries[candidate];
|
|
142
|
-
if (!taken || taken
|
|
180
|
+
if (!taken || reusable(taken, original, ctx))
|
|
143
181
|
return candidate;
|
|
144
182
|
}
|
|
145
|
-
return
|
|
183
|
+
return freshToken(label, vault, sessionId, ctx.reservedTokens);
|
|
146
184
|
}
|
|
147
185
|
function shouldProcessField(fieldPath, fieldMode, fieldRules) {
|
|
148
186
|
if (fieldMode === 'all')
|
|
@@ -376,7 +414,7 @@ function getSemanticLabel(fieldKey, siblingKeys) {
|
|
|
376
414
|
return null;
|
|
377
415
|
}
|
|
378
416
|
function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
379
|
-
var _a;
|
|
417
|
+
var _a, _b;
|
|
380
418
|
const semantic = getSemanticLabel(path, siblingKeys);
|
|
381
419
|
if (!semantic || !semantic.label || raw.trim().length === 0)
|
|
382
420
|
return null;
|
|
@@ -385,7 +423,9 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
385
423
|
const semConf = semantic.label === 'IDENTIFIER' ? context_1.AMBIGUOUS_FIELD_CONFIDENCE : context_1.SEMANTIC_CONFIDENCE;
|
|
386
424
|
if (semConf < ((_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0))
|
|
387
425
|
return null;
|
|
388
|
-
const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx
|
|
426
|
+
const replacement = applyRedaction(raw, { label: semantic.label, category: semantic.category }, ctx, vault, sessionId);
|
|
427
|
+
if (replacement)
|
|
428
|
+
(_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.set(lockKey(itemIndex, display), [[0, replacement.length]]);
|
|
389
429
|
hits.push({
|
|
390
430
|
token: replacement, original: '***',
|
|
391
431
|
patternName: `semantic_${semantic.label}`,
|
|
@@ -396,42 +436,75 @@ function redactSemantic(raw, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
396
436
|
});
|
|
397
437
|
return replacement;
|
|
398
438
|
}
|
|
439
|
+
function numericMapping(out, text, ctx, vault, sessionId) {
|
|
440
|
+
var _a, _b;
|
|
441
|
+
const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
|
|
442
|
+
const existing = entries[out];
|
|
443
|
+
if ((existing === null || existing === void 0 ? void 0 : existing.original) === text) {
|
|
444
|
+
existing.numeric = true;
|
|
445
|
+
return out;
|
|
446
|
+
}
|
|
447
|
+
const token = existing ? freshToken('NUMBER', vault, sessionId, ctx.reservedTokens) : out;
|
|
448
|
+
vault.addEntry(sessionId, {
|
|
449
|
+
token, original: text, patternLabel: 'NUMBER', category: 'other', createdAt: new Date().toISOString(), numeric: true,
|
|
450
|
+
});
|
|
451
|
+
return token;
|
|
452
|
+
}
|
|
399
453
|
function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
454
|
+
var _a;
|
|
400
455
|
const text = String(value);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
456
|
+
const numCtx = { ...ctx, dedup: false, numeric: true };
|
|
457
|
+
let out = null;
|
|
458
|
+
if (!ctx.skipSemantic)
|
|
459
|
+
out = redactSemantic(text, numCtx, vault, sessionId, hits, itemIndex, path, display, siblingKeys);
|
|
460
|
+
if (out === null && ctx.scanNumbers)
|
|
461
|
+
out = redactText(text, numCtx, vault, sessionId, hits, itemIndex, display);
|
|
462
|
+
if (out === null || out === text)
|
|
407
463
|
return value;
|
|
408
|
-
|
|
409
|
-
|
|
464
|
+
if (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize')
|
|
465
|
+
return out;
|
|
466
|
+
const mapped = numericMapping(out, text, ctx, vault, sessionId);
|
|
467
|
+
(_a = ctx.lockedSpans) === null || _a === void 0 ? void 0 : _a.set(lockKey(itemIndex, display), [[0, mapped.length]]);
|
|
468
|
+
return mapped;
|
|
410
469
|
}
|
|
411
|
-
function redactKey(key, taken, ctx, vault, sessionId, itemIndex, keyHits) {
|
|
412
|
-
|
|
470
|
+
function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
|
|
471
|
+
var _a;
|
|
472
|
+
const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
|
|
413
473
|
if (out === key)
|
|
414
474
|
return key;
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
out
|
|
418
|
-
|
|
475
|
+
const clashes = (k) => k === '' || k in taken || sourceKeys.has(k);
|
|
476
|
+
if (!clashes(out))
|
|
477
|
+
return out;
|
|
478
|
+
if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
|
|
479
|
+
const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...sourceKeys, ...Object.keys(taken)]));
|
|
480
|
+
vault.addEntry(sessionId, { token, original: key, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
|
|
481
|
+
return token;
|
|
482
|
+
}
|
|
483
|
+
let suffixed = out;
|
|
484
|
+
for (let n = 1; clashes(suffixed); n++)
|
|
485
|
+
suffixed = `${out}_${n}`;
|
|
486
|
+
return suffixed;
|
|
419
487
|
}
|
|
420
488
|
function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, display) {
|
|
489
|
+
var _a;
|
|
421
490
|
const entries = Object.entries(value);
|
|
422
491
|
const siblingKeys = entries.map(([k]) => k);
|
|
492
|
+
const sourceKeys = new Set(siblingKeys);
|
|
493
|
+
const origins = new Map();
|
|
423
494
|
const obj = {};
|
|
424
495
|
for (const [k, v] of entries) {
|
|
425
496
|
const fieldPath = path ? `${path}.${k}` : k;
|
|
426
497
|
const keyHits = [];
|
|
427
498
|
const outKey = shouldProcessField(fieldPath, ctx.fieldMode, ctx.fieldRules)
|
|
428
|
-
? redactKey(k, obj, ctx, vault, sessionId, itemIndex, keyHits)
|
|
499
|
+
? redactKey(k, obj, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits)
|
|
429
500
|
: k;
|
|
501
|
+
origins.set(outKey, k);
|
|
430
502
|
const childDisplay = display ? `${display}.${outKey}` : outKey;
|
|
431
503
|
for (const h of keyHits)
|
|
432
504
|
hits.push({ ...h, field: childDisplay });
|
|
433
505
|
obj[outKey] = redactValue(v, ctx, vault, sessionId, hits, itemIndex, fieldPath, siblingKeys, childDisplay);
|
|
434
506
|
}
|
|
507
|
+
(_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.set(obj, origins);
|
|
435
508
|
return obj;
|
|
436
509
|
}
|
|
437
510
|
function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath = '', siblingKeys, displayPath = currentPath) {
|
|
@@ -459,123 +532,170 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
|
|
|
459
532
|
}
|
|
460
533
|
return value;
|
|
461
534
|
}
|
|
462
|
-
|
|
535
|
+
const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
536
|
+
const USER_REGEX_WINDOW = 2048;
|
|
537
|
+
function windows(seg, size) {
|
|
538
|
+
const out = [];
|
|
539
|
+
let at = 0;
|
|
540
|
+
while (seg.text.length - at > size) {
|
|
541
|
+
const cut = seg.text.lastIndexOf(' ', at + size);
|
|
542
|
+
const end = cut > at + size / 2 ? cut + 1 : at + size;
|
|
543
|
+
out.push({ text: seg.text.slice(at, end), locked: false, src: seg.src + at });
|
|
544
|
+
at = end;
|
|
545
|
+
}
|
|
546
|
+
out.push({ text: seg.text.slice(at), locked: false, src: seg.src + at });
|
|
547
|
+
return out;
|
|
548
|
+
}
|
|
549
|
+
function replaceInFree(seg, re, fn, into) {
|
|
463
550
|
var _a;
|
|
464
|
-
let
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
case 'contains':
|
|
477
|
-
re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
478
|
-
break;
|
|
479
|
-
default:
|
|
480
|
-
re = new RegExp(entry.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
|
|
481
|
-
}
|
|
482
|
-
result = result.replace(re, (match) => {
|
|
483
|
-
if (TOKEN_RE.test(match))
|
|
484
|
-
return match;
|
|
485
|
-
const replacement = applyRedaction(match, { label: 'DENY_LIST', category: 'other' }, ctx.mode, vault, sessionId, ctx.dedup);
|
|
486
|
-
hits.push({
|
|
487
|
-
token: replacement, original: '***',
|
|
488
|
-
patternName: 'denyList', patternLabel: 'DENY_LIST',
|
|
489
|
-
category: 'other', field: fieldPath, itemIndex,
|
|
490
|
-
confidence: context_1.DENY_LIST_CONFIDENCE,
|
|
491
|
-
});
|
|
492
|
-
return replacement;
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
catch { }
|
|
496
|
-
}
|
|
551
|
+
let last = 0;
|
|
552
|
+
for (const m of seg.text.matchAll(re)) {
|
|
553
|
+
const at = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
554
|
+
if (m[0] === '')
|
|
555
|
+
continue;
|
|
556
|
+
const replacement = fn(m[0], seg.src + at);
|
|
557
|
+
if (replacement === null)
|
|
558
|
+
continue;
|
|
559
|
+
if (at > last)
|
|
560
|
+
into.push({ text: seg.text.slice(last, at), locked: false, src: seg.src + last });
|
|
561
|
+
into.push({ text: replacement, locked: true, src: -1 });
|
|
562
|
+
last = at + m[0].length;
|
|
497
563
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, text, matchOffset, matchOffset + match.length);
|
|
512
|
-
if (confidence < threshold)
|
|
513
|
-
return match;
|
|
514
|
-
const replacement = applyRedaction(match, pattern, ctx.mode, vault, sessionId, ctx.dedup);
|
|
515
|
-
hits.push({
|
|
516
|
-
token: replacement, original: '***',
|
|
517
|
-
patternName: pattern.name, patternLabel: pattern.label,
|
|
518
|
-
category: pattern.category, field: fieldPath, itemIndex,
|
|
519
|
-
confidence,
|
|
520
|
-
});
|
|
521
|
-
return replacement;
|
|
522
|
-
});
|
|
564
|
+
if (last < seg.text.length)
|
|
565
|
+
into.push({ text: seg.text.slice(last), locked: false, src: seg.src + last });
|
|
566
|
+
}
|
|
567
|
+
function replaceFree(pass, re, fn, window) {
|
|
568
|
+
const flags = re.flags.includes('g') ? re.flags : `${re.flags}g`;
|
|
569
|
+
const global = new RegExp(re.source, flags);
|
|
570
|
+
const next = [];
|
|
571
|
+
for (const seg of pass.segs) {
|
|
572
|
+
if (seg.locked)
|
|
573
|
+
next.push(seg);
|
|
574
|
+
else
|
|
575
|
+
for (const piece of window ? windows(seg, window) : [seg])
|
|
576
|
+
replaceInFree(piece, global, fn, next);
|
|
523
577
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
578
|
+
pass.segs = next;
|
|
579
|
+
}
|
|
580
|
+
function redactMatch(pass, match, pattern, detail) {
|
|
581
|
+
const replacement = applyRedaction(match, pattern, pass.ctx, pass.vault, pass.sessionId);
|
|
582
|
+
pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...detail });
|
|
583
|
+
return replacement;
|
|
584
|
+
}
|
|
585
|
+
function denyListRegex(entry) {
|
|
586
|
+
if (entry.type === 'regex')
|
|
587
|
+
return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
|
|
588
|
+
return new RegExp(escapeRe(entry.value), 'gi');
|
|
589
|
+
}
|
|
590
|
+
function denyListStage(pass) {
|
|
591
|
+
var _a;
|
|
592
|
+
for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
|
|
593
|
+
try {
|
|
594
|
+
const re = denyListRegex(entry);
|
|
595
|
+
if (!re)
|
|
536
596
|
continue;
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
confidence: nameConfidence,
|
|
544
|
-
});
|
|
597
|
+
const window = entry.type === 'regex' ? USER_REGEX_WINDOW : undefined;
|
|
598
|
+
replaceFree(pass, re, (match) => TOKEN_RE.test(match)
|
|
599
|
+
? null
|
|
600
|
+
: redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
|
|
601
|
+
patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
|
|
602
|
+
}), window);
|
|
545
603
|
}
|
|
604
|
+
catch { }
|
|
546
605
|
}
|
|
547
|
-
|
|
548
|
-
|
|
606
|
+
}
|
|
607
|
+
function builtInMatch(pass, pattern, match, offset) {
|
|
608
|
+
if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
|
|
609
|
+
return null;
|
|
610
|
+
const validatorPassed = pattern.validate ? pattern.validate(match) : true;
|
|
611
|
+
if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
|
|
612
|
+
return null;
|
|
613
|
+
const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
|
|
614
|
+
if (confidence < pass.threshold)
|
|
615
|
+
return null;
|
|
616
|
+
return redactMatch(pass, match, pattern, {
|
|
617
|
+
patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function builtInStage(pass) {
|
|
621
|
+
const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
|
|
622
|
+
for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
|
|
623
|
+
replaceFree(pass, pattern.regex, (match, offset) => builtInMatch(pass, pattern, match, offset));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function freeNames(pass) {
|
|
627
|
+
const names = new Map();
|
|
628
|
+
for (const seg of pass.segs) {
|
|
629
|
+
if (seg.locked)
|
|
630
|
+
continue;
|
|
631
|
+
for (const nm of (0, names_1.detectNamesInText)(seg.text))
|
|
632
|
+
if (!names.has(nm.name))
|
|
633
|
+
names.set(nm.name, seg.src + nm.start);
|
|
634
|
+
}
|
|
635
|
+
return names;
|
|
636
|
+
}
|
|
637
|
+
function nameStage(pass) {
|
|
638
|
+
if (pass.ctx.skipSemantic)
|
|
639
|
+
return;
|
|
640
|
+
for (const [name, start] of freeNames(pass)) {
|
|
641
|
+
if ((name.includes('[') && name.includes(']')) || isAllowListed(name, pass.ctx.allowList))
|
|
549
642
|
continue;
|
|
550
|
-
|
|
643
|
+
const confidence = (0, context_1.hasContextWords)(pass.source, start, start + name.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
|
|
644
|
+
if (confidence < pass.threshold)
|
|
551
645
|
continue;
|
|
646
|
+
let replacement;
|
|
647
|
+
replaceFree(pass, new RegExp(escapeRe(name), 'g'), (match) => {
|
|
648
|
+
replacement !== null && replacement !== void 0 ? replacement : (replacement = redactMatch(pass, match, { label: 'PERSON_NAME', category: 'identity' }, {
|
|
649
|
+
patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence,
|
|
650
|
+
}));
|
|
651
|
+
return replacement;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
function customStage(pass) {
|
|
656
|
+
if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
|
|
657
|
+
return;
|
|
658
|
+
for (const cp of pass.ctx.customPatterns) {
|
|
659
|
+
if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
|
|
660
|
+
continue;
|
|
661
|
+
const pattern = { label: cp.label.toUpperCase(), category: cp.category || 'identity' };
|
|
552
662
|
try {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (isAllowListed(match, ctx.allowList))
|
|
558
|
-
return match;
|
|
559
|
-
if (context_1.CUSTOM_PATTERN_CONFIDENCE < threshold)
|
|
560
|
-
return match;
|
|
561
|
-
const pseudoPattern = {
|
|
562
|
-
label: cp.label.toUpperCase(),
|
|
563
|
-
category: cp.category || 'identity',
|
|
564
|
-
};
|
|
565
|
-
const replacement = applyRedaction(match, pseudoPattern, ctx.mode, vault, sessionId, ctx.dedup);
|
|
566
|
-
hits.push({
|
|
567
|
-
token: replacement, original: '***',
|
|
568
|
-
patternName: `custom_${cp.label}`, patternLabel: pseudoPattern.label,
|
|
569
|
-
category: pseudoPattern.category, field: fieldPath, itemIndex,
|
|
663
|
+
replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => TOKEN_RE.test(match) || isAllowListed(match, pass.ctx.allowList)
|
|
664
|
+
? null
|
|
665
|
+
: redactMatch(pass, match, pattern, {
|
|
666
|
+
patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
|
|
570
667
|
confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
|
|
571
|
-
});
|
|
572
|
-
return replacement;
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
catch {
|
|
668
|
+
}), USER_REGEX_WINDOW);
|
|
576
669
|
}
|
|
670
|
+
catch { }
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function recordLocked(ctx, itemIndex, fieldPath, segs) {
|
|
674
|
+
if (!ctx.lockedSpans || fieldPath === '')
|
|
675
|
+
return;
|
|
676
|
+
const spans = [];
|
|
677
|
+
let at = 0;
|
|
678
|
+
for (const seg of segs) {
|
|
679
|
+
if (seg.locked)
|
|
680
|
+
spans.push([at, at + seg.text.length]);
|
|
681
|
+
at += seg.text.length;
|
|
577
682
|
}
|
|
578
|
-
|
|
683
|
+
if (spans.length > 0)
|
|
684
|
+
ctx.lockedSpans.set(lockKey(itemIndex, fieldPath), spans);
|
|
685
|
+
}
|
|
686
|
+
const lockKey = (itemIndex, fieldPath) => `${itemIndex}\u0000${fieldPath}`;
|
|
687
|
+
function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
|
|
688
|
+
var _a;
|
|
689
|
+
const pass = {
|
|
690
|
+
source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
|
|
691
|
+
ctx, vault, sessionId, hits, itemIndex, fieldPath,
|
|
692
|
+
};
|
|
693
|
+
denyListStage(pass);
|
|
694
|
+
builtInStage(pass);
|
|
695
|
+
nameStage(pass);
|
|
696
|
+
customStage(pass);
|
|
697
|
+
recordLocked(ctx, itemIndex, fieldPath, pass.segs);
|
|
698
|
+
return pass.segs.map((s) => s.text).join('');
|
|
579
699
|
}
|
|
580
700
|
function makeRestorer(entries) {
|
|
581
701
|
const tokens = Object.keys(entries).filter((t) => t.length > 0).sort((a, b) => b.length - a.length);
|
|
@@ -584,15 +704,19 @@ function makeRestorer(entries) {
|
|
|
584
704
|
const re = new RegExp(tokens.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'g');
|
|
585
705
|
return (text) => text.replace(re, (token) => entries[token].original);
|
|
586
706
|
}
|
|
587
|
-
function restoreWith(value, restore) {
|
|
588
|
-
if (typeof value === 'string')
|
|
707
|
+
function restoreWith(value, restore, entries) {
|
|
708
|
+
if (typeof value === 'string') {
|
|
709
|
+
const exact = entries[value];
|
|
710
|
+
if ((exact === null || exact === void 0 ? void 0 : exact.numeric) && Number.isFinite(Number(exact.original)))
|
|
711
|
+
return Number(exact.original);
|
|
589
712
|
return restore(value);
|
|
713
|
+
}
|
|
590
714
|
if (Array.isArray(value))
|
|
591
|
-
return value.map((v) => restoreWith(v, restore));
|
|
715
|
+
return value.map((v) => restoreWith(v, restore, entries));
|
|
592
716
|
if (value !== null && typeof value === 'object') {
|
|
593
717
|
const obj = {};
|
|
594
718
|
for (const [k, v] of Object.entries(value)) {
|
|
595
|
-
obj[restore(k)] = restoreWith(v, restore);
|
|
719
|
+
obj[restore(k)] = restoreWith(v, restore, entries);
|
|
596
720
|
}
|
|
597
721
|
return obj;
|
|
598
722
|
}
|
|
@@ -602,7 +726,7 @@ function restoreValue(value, vault, sessionId) {
|
|
|
602
726
|
const session = vault.getSession(sessionId);
|
|
603
727
|
if (!session)
|
|
604
728
|
return value;
|
|
605
|
-
return restoreWith(value, makeRestorer(session.entries));
|
|
729
|
+
return restoreWith(value, makeRestorer(session.entries), session.entries);
|
|
606
730
|
}
|
|
607
731
|
function toUtf16Spans(text, entities) {
|
|
608
732
|
const offsets = [0];
|
|
@@ -621,13 +745,18 @@ function resolveOverlaps(spans) {
|
|
|
621
745
|
}
|
|
622
746
|
return kept.sort((a, b) => b.start - a.start);
|
|
623
747
|
}
|
|
748
|
+
function presidioSpans(value, entities, ctx, key) {
|
|
749
|
+
var _a, _b, _c;
|
|
750
|
+
const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
|
|
751
|
+
const locked = (_c = (_b = ctx.lockedSpans) === null || _b === void 0 ? void 0 : _b.get(key)) !== null && _c !== void 0 ? _c : [];
|
|
752
|
+
const free = (s) => !locked.some(([start, end]) => s.start < end && start < s.end);
|
|
753
|
+
return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
|
|
754
|
+
}
|
|
624
755
|
async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path) {
|
|
625
|
-
var _a;
|
|
626
756
|
if (value.trim().length === 0 || TOKEN_RE.test(value))
|
|
627
757
|
return value;
|
|
628
758
|
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
629
|
-
const
|
|
630
|
-
const spans = resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold));
|
|
759
|
+
const spans = presidioSpans(value, entities, ctx, lockKey(itemIndex, path));
|
|
631
760
|
let text = value;
|
|
632
761
|
for (const span of spans) {
|
|
633
762
|
const matchedText = value.slice(span.start, span.end);
|
|
@@ -637,7 +766,7 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
|
|
|
637
766
|
continue;
|
|
638
767
|
const label = (0, presidio_1.getRedactorLabel)(span.entityType);
|
|
639
768
|
const category = (0, presidio_1.getRedactorCategory)(span.entityType);
|
|
640
|
-
const replacement = applyRedaction(matchedText, { label, category }, ctx
|
|
769
|
+
const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
|
|
641
770
|
text = text.slice(0, span.start) + replacement + text.slice(span.end);
|
|
642
771
|
hits.push({
|
|
643
772
|
token: replacement, original: '***',
|
|
@@ -647,9 +776,10 @@ async function presidioRedactString(value, ctx, vault, sessionId, hits, itemInde
|
|
|
647
776
|
}
|
|
648
777
|
return text;
|
|
649
778
|
}
|
|
650
|
-
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path) {
|
|
779
|
+
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
|
|
780
|
+
var _a, _b;
|
|
651
781
|
if (typeof value === 'string') {
|
|
652
|
-
if (!shouldProcessField(
|
|
782
|
+
if (!shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
|
|
653
783
|
return value;
|
|
654
784
|
try {
|
|
655
785
|
return await presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path);
|
|
@@ -660,14 +790,19 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path)
|
|
|
660
790
|
}
|
|
661
791
|
if (Array.isArray(value)) {
|
|
662
792
|
const out = [];
|
|
663
|
-
for (let i = 0; i < value.length; i++)
|
|
664
|
-
out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`));
|
|
793
|
+
for (let i = 0; i < value.length; i++) {
|
|
794
|
+
out.push(await presidioWalk(value[i], ctx, vault, sessionId, hits, itemIndex, `${path}[${i}]`, `${policyPath}[${i}]`));
|
|
795
|
+
}
|
|
665
796
|
return out;
|
|
666
797
|
}
|
|
667
798
|
if (value !== null && typeof value === 'object') {
|
|
799
|
+
const origins = (_a = ctx.keyOrigins) === null || _a === void 0 ? void 0 : _a.get(value);
|
|
668
800
|
const out = {};
|
|
669
801
|
for (const [k, v] of Object.entries(value)) {
|
|
670
|
-
|
|
802
|
+
const original = (_b = origins === null || origins === void 0 ? void 0 : origins.get(k)) !== null && _b !== void 0 ? _b : k;
|
|
803
|
+
const child = path ? `${path}.${k}` : k;
|
|
804
|
+
const childPolicy = policyPath ? `${policyPath}.${original}` : original;
|
|
805
|
+
out[k] = await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, child, childPolicy);
|
|
671
806
|
}
|
|
672
807
|
return out;
|
|
673
808
|
}
|
|
@@ -676,7 +811,7 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path)
|
|
|
676
811
|
async function enhanceWithPresidio(data, ctx, vault, sessionId, hits, itemIndex) {
|
|
677
812
|
if (!ctx.presidioUrl)
|
|
678
813
|
return data;
|
|
679
|
-
return (await presidioWalk(data, ctx, vault, sessionId, hits, itemIndex, ''));
|
|
814
|
+
return (await presidioWalk(data, ctx, vault, sessionId, hits, itemIndex, '', ''));
|
|
680
815
|
}
|
|
681
816
|
function buildReport(sessionId, hits) {
|
|
682
817
|
const hitsByCategory = {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INJECTION_DESCRIPTIONS = void 0;
|
|
3
4
|
exports.detectPromptInjection = detectPromptInjection;
|
|
4
5
|
exports.scanForInjection = scanForInjection;
|
|
5
6
|
const INJECTION_PATTERNS = [
|
|
@@ -200,6 +201,11 @@ const INJECTION_PATTERNS = [
|
|
|
200
201
|
description: 'Fake end-of-prompt marker',
|
|
201
202
|
},
|
|
202
203
|
];
|
|
204
|
+
exports.INJECTION_DESCRIPTIONS = [
|
|
205
|
+
...INJECTION_PATTERNS.map((p) => p.description),
|
|
206
|
+
'Invisible Unicode characters detected',
|
|
207
|
+
'Base64-encoded injection payload detected',
|
|
208
|
+
];
|
|
203
209
|
const ZERO_WIDTH_RE = /(?:\u200B|\u200C|\u200D|\uFEFF|\u00AD|\u2060|\u2061|\u2062|\u2063|\u2064|\u2066|\u2067|\u2068|\u2069|\u206A|\u206B|\u206C|\u206D|\u206E|\u206F)/g;
|
|
204
210
|
const BASE64_SUSPICIOUS_RE = /[A-Za-z0-9+/]{40,}={0,2}/g;
|
|
205
211
|
const INJECTION_SEEDS = [
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emptySet = void 0;
|
|
4
|
+
exports.union = union;
|
|
5
|
+
exports.overlaps = overlaps;
|
|
6
|
+
exports.atomCharset = atomCharset;
|
|
7
|
+
const emptySet = () => ({ ascii: new Array(128).fill(false), other: false });
|
|
8
|
+
exports.emptySet = emptySet;
|
|
9
|
+
const fullSet = () => ({ ascii: new Array(128).fill(true), other: true });
|
|
10
|
+
function addCode(set, code) {
|
|
11
|
+
if (code >= 128) {
|
|
12
|
+
set.other = true;
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
set.ascii[code] = true;
|
|
16
|
+
const ch = String.fromCharCode(code);
|
|
17
|
+
for (const folded of [ch.toLowerCase(), ch.toUpperCase()])
|
|
18
|
+
set.ascii[folded.charCodeAt(0)] = true;
|
|
19
|
+
}
|
|
20
|
+
function addRange(set, from, to) {
|
|
21
|
+
for (let c = from; c <= Math.min(to, 127); c++)
|
|
22
|
+
addCode(set, c);
|
|
23
|
+
if (to >= 128)
|
|
24
|
+
set.other = true;
|
|
25
|
+
}
|
|
26
|
+
function union(a, b) {
|
|
27
|
+
return { ascii: a.ascii.map((v, i) => v || b.ascii[i]), other: a.other || b.other };
|
|
28
|
+
}
|
|
29
|
+
function overlaps(a, b) {
|
|
30
|
+
return (a.other && b.other) || a.ascii.some((v, i) => v && b.ascii[i]);
|
|
31
|
+
}
|
|
32
|
+
function complement(set) {
|
|
33
|
+
return { ascii: set.ascii.map((v) => !v), other: true };
|
|
34
|
+
}
|
|
35
|
+
const CLASS_ESCAPES = {
|
|
36
|
+
d: (s) => addRange(s, 48, 57),
|
|
37
|
+
w: (s) => {
|
|
38
|
+
addRange(s, 48, 57);
|
|
39
|
+
addRange(s, 65, 90);
|
|
40
|
+
addCode(s, 95);
|
|
41
|
+
},
|
|
42
|
+
s: (s) => {
|
|
43
|
+
for (const c of [9, 10, 11, 12, 13, 32])
|
|
44
|
+
addCode(s, c);
|
|
45
|
+
s.other = true;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
function escapeSet(letter) {
|
|
49
|
+
const lower = letter.toLowerCase();
|
|
50
|
+
const fill = CLASS_ESCAPES[lower];
|
|
51
|
+
if (fill) {
|
|
52
|
+
const set = (0, exports.emptySet)();
|
|
53
|
+
fill(set);
|
|
54
|
+
return letter === lower ? set : complement(set);
|
|
55
|
+
}
|
|
56
|
+
if (/[a-zA-Z0-9]/.test(letter))
|
|
57
|
+
return fullSet();
|
|
58
|
+
const set = (0, exports.emptySet)();
|
|
59
|
+
addCode(set, letter.charCodeAt(0));
|
|
60
|
+
return set;
|
|
61
|
+
}
|
|
62
|
+
function classMember(body, j) {
|
|
63
|
+
var _a, _b;
|
|
64
|
+
if (body[j] === '\\') {
|
|
65
|
+
const set = escapeSet((_a = body[j + 1]) !== null && _a !== void 0 ? _a : '\\');
|
|
66
|
+
const single = /[a-zA-Z0-9]/.test((_b = body[j + 1]) !== null && _b !== void 0 ? _b : '') ? null : body.charCodeAt(j + 1);
|
|
67
|
+
return { set, next: j + 2, code: single };
|
|
68
|
+
}
|
|
69
|
+
const set = (0, exports.emptySet)();
|
|
70
|
+
addCode(set, body.charCodeAt(j));
|
|
71
|
+
return { set, next: j + 1, code: body.charCodeAt(j) };
|
|
72
|
+
}
|
|
73
|
+
function classSet(body) {
|
|
74
|
+
const negated = body.startsWith('^');
|
|
75
|
+
let set = (0, exports.emptySet)();
|
|
76
|
+
let j = negated ? 1 : 0;
|
|
77
|
+
while (j < body.length) {
|
|
78
|
+
const first = classMember(body, j);
|
|
79
|
+
j = first.next;
|
|
80
|
+
if (body[j] === '-' && j + 1 < body.length && first.code !== null) {
|
|
81
|
+
const last = classMember(body, j + 1);
|
|
82
|
+
if (last.code !== null)
|
|
83
|
+
addRange(set, first.code, last.code);
|
|
84
|
+
else
|
|
85
|
+
set = union(union(set, first.set), last.set);
|
|
86
|
+
j = last.next;
|
|
87
|
+
}
|
|
88
|
+
else
|
|
89
|
+
set = union(set, first.set);
|
|
90
|
+
}
|
|
91
|
+
return negated ? complement(set) : set;
|
|
92
|
+
}
|
|
93
|
+
function atomCharset(src, start, end) {
|
|
94
|
+
var _a, _b;
|
|
95
|
+
const c = src[start];
|
|
96
|
+
if (c === '^' || c === '$')
|
|
97
|
+
return null;
|
|
98
|
+
if (c === '\\')
|
|
99
|
+
return /[bB]/.test((_a = src[start + 1]) !== null && _a !== void 0 ? _a : '') ? null : escapeSet((_b = src[start + 1]) !== null && _b !== void 0 ? _b : '\\');
|
|
100
|
+
if (c === '[')
|
|
101
|
+
return classSet(src.slice(start + 1, end - 1));
|
|
102
|
+
if (c === '.')
|
|
103
|
+
return fullSet();
|
|
104
|
+
const set = (0, exports.emptySet)();
|
|
105
|
+
addCode(set, src.charCodeAt(start));
|
|
106
|
+
return set;
|
|
107
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkRegex = checkRegex;
|
|
4
|
+
const regexCharset_1 = require("./regexCharset");
|
|
4
5
|
const MAX_REGEX_LENGTH = 500;
|
|
6
|
+
const newFrame = (zeroWidth = false) => ({
|
|
7
|
+
hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
|
|
8
|
+
});
|
|
5
9
|
function readBraces(src, i) {
|
|
6
10
|
const m = /^\{(\d+)(,(\d*))?\}/.exec(src.slice(i));
|
|
7
11
|
if (!m)
|
|
@@ -55,11 +59,12 @@ function readToken(src, i, stack) {
|
|
|
55
59
|
if (c === '[')
|
|
56
60
|
return { atomEnd: skipClass(src, i), closed: null };
|
|
57
61
|
if (c === '(') {
|
|
58
|
-
stack.push(
|
|
62
|
+
stack.push(newFrame(/^\(\?(?:=|!|<=|<!)/.test(src.slice(i))));
|
|
59
63
|
return { atomEnd: i, closed: null, next: i + groupPrefixLength(src, i) };
|
|
60
64
|
}
|
|
61
65
|
if (c === '|') {
|
|
62
66
|
stack[stack.length - 1].hasAlternation = true;
|
|
67
|
+
stack[stack.length - 1].run = null;
|
|
63
68
|
return { atomEnd: i, closed: null, next: i + 1 };
|
|
64
69
|
}
|
|
65
70
|
const closed = c === ')' && stack.length > 1 ? stack.pop() : null;
|
|
@@ -69,16 +74,48 @@ const NESTED = 'a repeated group contains a quantifier or alternation (catastrop
|
|
|
69
74
|
function isRisky(frame) {
|
|
70
75
|
return !!frame && (frame.hasVariableQuantifier || frame.hasAlternation);
|
|
71
76
|
}
|
|
77
|
+
const NO_GROUP = newFrame();
|
|
78
|
+
const repeats = (q) => q !== null && q.max > 1;
|
|
79
|
+
const isVariable = (q) => q !== null && q.min !== q.max;
|
|
72
80
|
function applyQuantifier(q, closed, top) {
|
|
73
|
-
|
|
74
|
-
if (repeats && isRisky(closed))
|
|
81
|
+
if (repeats(q) && isRisky(closed))
|
|
75
82
|
return NESTED;
|
|
76
|
-
const
|
|
77
|
-
top.hasVariableQuantifier
|
|
83
|
+
const inner = closed !== null && closed !== void 0 ? closed : NO_GROUP;
|
|
84
|
+
top.hasVariableQuantifier || (top.hasVariableQuantifier = isVariable(q) || inner.hasVariableQuantifier);
|
|
85
|
+
top.hasAlternation || (top.hasAlternation = inner.hasAlternation);
|
|
78
86
|
return null;
|
|
79
87
|
}
|
|
88
|
+
const SEQUENTIAL = 'two repetitions in a row can match the same characters (catastrophic backtracking)';
|
|
89
|
+
function applySequence(chars, variable, required, top) {
|
|
90
|
+
if (chars === null)
|
|
91
|
+
return null;
|
|
92
|
+
top.chars = (0, regexCharset_1.union)(top.chars, chars);
|
|
93
|
+
const shared = top.run !== null && (0, regexCharset_1.overlaps)(top.run, chars);
|
|
94
|
+
if (variable) {
|
|
95
|
+
if (shared)
|
|
96
|
+
return SEQUENTIAL;
|
|
97
|
+
top.run = top.run ? (0, regexCharset_1.union)(top.run, chars) : chars;
|
|
98
|
+
}
|
|
99
|
+
else if (required && !shared)
|
|
100
|
+
top.run = null;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
function stepChars(src, i, step) {
|
|
104
|
+
const closed = step.closed;
|
|
105
|
+
if (!closed)
|
|
106
|
+
return (0, regexCharset_1.atomCharset)(src, i, step.atomEnd);
|
|
107
|
+
return closed.zeroWidth ? null : closed.chars;
|
|
108
|
+
}
|
|
109
|
+
function stepProblem(src, i, step, q, top) {
|
|
110
|
+
var _a, _b;
|
|
111
|
+
const problem = applyQuantifier(q, step.closed, top);
|
|
112
|
+
if (problem || (q === null || q === void 0 ? void 0 : q.max) === 0)
|
|
113
|
+
return problem;
|
|
114
|
+
const variable = isVariable(q) || ((_a = step.closed) === null || _a === void 0 ? void 0 : _a.hasVariableQuantifier) === true;
|
|
115
|
+
return applySequence(stepChars(src, i, step), variable, ((_b = q === null || q === void 0 ? void 0 : q.min) !== null && _b !== void 0 ? _b : 1) >= 1, top);
|
|
116
|
+
}
|
|
80
117
|
function structuralProblem(src) {
|
|
81
|
-
const stack = [
|
|
118
|
+
const stack = [newFrame()];
|
|
82
119
|
let i = 0;
|
|
83
120
|
while (i < src.length) {
|
|
84
121
|
const step = readToken(src, i, stack);
|
|
@@ -89,7 +126,7 @@ function structuralProblem(src) {
|
|
|
89
126
|
continue;
|
|
90
127
|
}
|
|
91
128
|
const q = readQuantifier(src, step.atomEnd);
|
|
92
|
-
const problem =
|
|
129
|
+
const problem = stepProblem(src, i, step, q, stack[stack.length - 1]);
|
|
93
130
|
if (problem)
|
|
94
131
|
return problem;
|
|
95
132
|
i = step.atomEnd + (q ? q.length : 0);
|
|
@@ -5,25 +5,48 @@ exports.workflowNamespace = workflowNamespace;
|
|
|
5
5
|
exports.assertUserRegexes = assertUserRegexes;
|
|
6
6
|
exports.sanitizeInjection = sanitizeInjection;
|
|
7
7
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
-
const patterns_1 = require("./patterns");
|
|
9
|
-
const vault_1 = require("./vault");
|
|
10
|
-
const engine_1 = require("./engine");
|
|
11
8
|
const regexSafety_1 = require("./regexSafety");
|
|
9
|
+
const injection_1 = require("./injection");
|
|
12
10
|
const VAULT_CREDENTIAL = 'redactorVaultApi';
|
|
13
11
|
async function resolvePassphrase(fns) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
var _a;
|
|
13
|
+
if ((_a = fns.getNode().credentials) === null || _a === void 0 ? void 0 : _a[VAULT_CREDENTIAL]) {
|
|
14
|
+
let cred;
|
|
15
|
+
try {
|
|
16
|
+
cred = (await fns.getCredentials(VAULT_CREDENTIAL));
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The Redactor Vault credential could not be read, so the vault stays locked.', {
|
|
20
|
+
description: `Nothing was stored. Re-enter the passphrase in the credential. Cause: ${error.message}`,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (typeof cred.passphrase !== 'string' || cred.passphrase === '') {
|
|
24
|
+
throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The Redactor Vault credential has an empty passphrase.', {
|
|
25
|
+
description: 'Nothing was stored. Set a passphrase in the credential, or remove the credential from the node.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return cred.passphrase;
|
|
20
29
|
}
|
|
30
|
+
return legacyPassphrase(fns);
|
|
31
|
+
}
|
|
32
|
+
function legacyPassphrase(fns) {
|
|
33
|
+
let raw;
|
|
21
34
|
try {
|
|
22
|
-
|
|
35
|
+
raw = fns.getNodeParameter('vaultPassphrase', 0, '');
|
|
23
36
|
}
|
|
24
|
-
catch {
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The legacy vault passphrase could not be evaluated, so the vault stays locked.', {
|
|
39
|
+
description: `Nothing was stored. Fix the expression, or move the passphrase into a Redactor Vault credential. Cause: ${error.message}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (raw === undefined || raw === null || raw === '')
|
|
25
43
|
return '';
|
|
44
|
+
if (typeof raw !== 'string') {
|
|
45
|
+
throw new n8n_workflow_1.NodeOperationError(fns.getNode(), 'The legacy vault passphrase is not text, so the vault stays locked.', {
|
|
46
|
+
description: 'Nothing was stored. Set the passphrase to a text value, or use a Redactor Vault credential.',
|
|
47
|
+
});
|
|
26
48
|
}
|
|
49
|
+
return raw;
|
|
27
50
|
}
|
|
28
51
|
function workflowNamespace(fns) {
|
|
29
52
|
try {
|
|
@@ -38,7 +61,7 @@ function workflowNamespace(fns) {
|
|
|
38
61
|
function assertUserRegexes(fns, customPatterns, lists) {
|
|
39
62
|
const refuse = (what, problem) => {
|
|
40
63
|
throw new n8n_workflow_1.NodeOperationError(fns.getNode(), `${what} regex ${problem}`, {
|
|
41
|
-
description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+.',
|
|
64
|
+
description: 'Rewrite the pattern without nested or alternated repetition, for example [ab]+ instead of (a|b)+, and without two repetitions in a row that can match the same characters, for example \\d+ instead of \\d+\\d+. Put a fixed separator such as - or @ between repeated parts.',
|
|
42
65
|
});
|
|
43
66
|
};
|
|
44
67
|
for (const cp of customPatterns) {
|
|
@@ -57,19 +80,15 @@ function assertUserRegexes(fns, customPatterns, lists) {
|
|
|
57
80
|
}
|
|
58
81
|
}
|
|
59
82
|
function sanitizeInjection(result) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
const scrub = (text) => (0, engine_1.redactValue)(text, ctx, vault, sessionId, [], 0);
|
|
72
|
-
const matchedPatterns = result.matchedPatterns.map((m) => ({ ...m, pattern: scrub(m.pattern), matchedText: scrub(m.matchedText) }));
|
|
73
|
-
vault.deleteSession(sessionId);
|
|
83
|
+
const matchedPatterns = result.matchedPatterns.map((m) => {
|
|
84
|
+
var _a;
|
|
85
|
+
return ({
|
|
86
|
+
category: m.category,
|
|
87
|
+
pattern: (_a = injection_1.INJECTION_DESCRIPTIONS.find((d) => m.pattern === d || m.pattern.endsWith(` ${d}`))) !== null && _a !== void 0 ? _a : 'Prompt injection detector',
|
|
88
|
+
matchedText: '(withheld)',
|
|
89
|
+
severity: m.severity,
|
|
90
|
+
score: m.score,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
74
93
|
return { ...result, matchedPatterns };
|
|
75
94
|
}
|
|
@@ -124,6 +124,17 @@ class MemoryVault {
|
|
|
124
124
|
deleteSession(sessionId) {
|
|
125
125
|
this.drop(this.key(sessionId));
|
|
126
126
|
}
|
|
127
|
+
purgeAll() {
|
|
128
|
+
const keys = this.ownKeys();
|
|
129
|
+
const entries = keys.reduce((n, k) => { var _a, _b; return n + Object.keys((_b = (_a = memoryStore.get(k)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {}).length; }, 0);
|
|
130
|
+
keys.forEach((k) => this.drop(k));
|
|
131
|
+
return { sessions: keys.length, entries };
|
|
132
|
+
}
|
|
133
|
+
purgeSession(sessionId) {
|
|
134
|
+
const session = memoryStore.get(this.key(sessionId));
|
|
135
|
+
this.deleteSession(sessionId);
|
|
136
|
+
return { found: !!session, entries: session ? Object.keys(session.entries).length : null };
|
|
137
|
+
}
|
|
127
138
|
listSessions() {
|
|
128
139
|
return this.ownKeys()
|
|
129
140
|
.map((k) => memoryStore.get(k))
|
|
@@ -242,6 +253,25 @@ class WorkflowVault {
|
|
|
242
253
|
if (k in this.sessions)
|
|
243
254
|
delete this.writableSessions()[k];
|
|
244
255
|
}
|
|
256
|
+
purgeAll() {
|
|
257
|
+
const stored = Object.values(this.sessions);
|
|
258
|
+
const entries = stored.reduce((n, rec) => {
|
|
259
|
+
const session = rec && typeof rec === 'object' ? this.decode(rec) : null;
|
|
260
|
+
return n + (session ? Object.keys(session.entries).length : 0);
|
|
261
|
+
}, 0);
|
|
262
|
+
this.cache.clear();
|
|
263
|
+
if (stored.length)
|
|
264
|
+
this.store[STORE_KEY] = {};
|
|
265
|
+
return { sessions: stored.length, entries };
|
|
266
|
+
}
|
|
267
|
+
purgeSession(sessionId) {
|
|
268
|
+
var _a;
|
|
269
|
+
const stored = this.sessions[this.key(sessionId)];
|
|
270
|
+
const live = (_a = this.cache.get(sessionId)) !== null && _a !== void 0 ? _a : (stored && typeof stored === 'object' ? this.decode(stored) : null);
|
|
271
|
+
const found = !!stored || this.cache.has(sessionId);
|
|
272
|
+
this.deleteSession(sessionId);
|
|
273
|
+
return { found, entries: live && live.sessionId === sessionId ? Object.keys(live.entries).length : null };
|
|
274
|
+
}
|
|
245
275
|
listSessions() {
|
|
246
276
|
const out = [];
|
|
247
277
|
for (const stored of Object.values(this.sessions)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-redactor",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "Redact, mask and anonymize PII before it reaches an LLM, then restore it. 216 patterns for emails, phones, addresses, IBAN, cards, names, SSN, passport and tax IDs. Reversible encrypted vault, GDPR Art.30 ROPA record, prompt-injection check. Runs on n8n Cloud and self-hosted n8n 2.x.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|