n8n-nodes-redactor 6.0.0 → 6.1.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 +3 -1
- package/dist/nodes/Redactor/engine.js +103 -45
- package/dist/nodes/Redactor/injection.js +31 -25
- package/dist/nodes/Redactor/names.js +37 -20
- package/dist/nodes/Redactor/patterns.js +192 -22
- package/dist/nodes/Redactor/presidio.js +5 -1
- package/dist/nodes/Redactor/regexSafety.js +19 -2
- package/dist/nodes/Redactor/vault.js +16 -10
- package/package.json +1 -1
|
@@ -798,6 +798,7 @@ class Redactor {
|
|
|
798
798
|
fieldRules: [],
|
|
799
799
|
fieldMode: 'all',
|
|
800
800
|
skipSemantic: operation === 'verify',
|
|
801
|
+
skipNames: operation === 'verify' && this.getNode().typeVersion < 2,
|
|
801
802
|
scanNumbers: true,
|
|
802
803
|
};
|
|
803
804
|
const allHits = [];
|
|
@@ -1032,7 +1033,8 @@ class Redactor {
|
|
|
1032
1033
|
{ name: 'Deny list', entries: denyList },
|
|
1033
1034
|
]);
|
|
1034
1035
|
try {
|
|
1035
|
-
vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
|
|
1036
|
+
const session = vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
|
|
1037
|
+
sessionTtl = session.ttl / 60000;
|
|
1036
1038
|
}
|
|
1037
1039
|
catch (error) {
|
|
1038
1040
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
|
|
@@ -611,39 +611,45 @@ function redactMatch(pass, match, decision) {
|
|
|
611
611
|
}
|
|
612
612
|
return replacement;
|
|
613
613
|
}
|
|
614
|
-
function
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
lo = mid + 1;
|
|
621
|
-
else
|
|
622
|
-
hi = mid;
|
|
614
|
+
function coveringPending(pending, cursor, pos) {
|
|
615
|
+
while (cursor.i < pending.length && pending[cursor.i].end <= pos)
|
|
616
|
+
cursor.i++;
|
|
617
|
+
for (let k = cursor.i; k < pending.length && pending[k].start <= pos; k++) {
|
|
618
|
+
if (pending[k].end > pos)
|
|
619
|
+
return pending[k];
|
|
623
620
|
}
|
|
624
|
-
return
|
|
621
|
+
return undefined;
|
|
625
622
|
}
|
|
626
|
-
function
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
if (seg.locked) {
|
|
632
|
-
i++;
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
const start = Math.max(seg.src, p.start);
|
|
623
|
+
function claimSegment(pass, seg, pending, cursor, into) {
|
|
624
|
+
let pos = seg.src;
|
|
625
|
+
while (pos < seg.end) {
|
|
626
|
+
const p = coveringPending(pending, cursor, pos);
|
|
627
|
+
if (p) {
|
|
636
628
|
const end = Math.min(seg.end, p.end);
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
parts.push({ text: redactMatch(pass, pass.source.slice(start, end), p.decision), locked: true, src: start, end });
|
|
641
|
-
if (end < seg.end)
|
|
642
|
-
parts.push(freeSlice(seg, end, seg.end));
|
|
643
|
-
pass.segs.splice(i, 1, ...parts);
|
|
644
|
-
i += parts.length;
|
|
629
|
+
into.push({ text: redactMatch(pass, pass.source.slice(pos, end), p.decision), locked: true, src: pos, end });
|
|
630
|
+
pos = end;
|
|
631
|
+
continue;
|
|
645
632
|
}
|
|
633
|
+
const next = cursor.i < pending.length ? Math.max(pos + 1, pending[cursor.i].start) : seg.end;
|
|
634
|
+
const end = Math.min(seg.end, next);
|
|
635
|
+
into.push(freeSlice(seg, pos, end));
|
|
636
|
+
pos = end;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function claimPending(pass, pending) {
|
|
640
|
+
var _a;
|
|
641
|
+
if (pending.length === 0)
|
|
642
|
+
return;
|
|
643
|
+
pending.sort((a, b) => a.start - b.start);
|
|
644
|
+
const next = [];
|
|
645
|
+
const cursor = { i: 0 };
|
|
646
|
+
for (const seg of pass.segs) {
|
|
647
|
+
if (seg.locked || seg.end <= ((_a = pending[cursor.i]) === null || _a === void 0 ? void 0 : _a.start) || cursor.i >= pending.length)
|
|
648
|
+
next.push(seg);
|
|
649
|
+
else
|
|
650
|
+
claimSegment(pass, seg, pending, cursor, next);
|
|
646
651
|
}
|
|
652
|
+
pass.segs = next;
|
|
647
653
|
}
|
|
648
654
|
function claimOne(pass, cursor, pending, start, end, decide) {
|
|
649
655
|
const text = pass.source.slice(start, end);
|
|
@@ -736,7 +742,8 @@ function nameSpans(pass) {
|
|
|
736
742
|
return spans;
|
|
737
743
|
}
|
|
738
744
|
function nameStage(pass) {
|
|
739
|
-
|
|
745
|
+
var _a;
|
|
746
|
+
if ((_a = pass.ctx.skipNames) !== null && _a !== void 0 ? _a : pass.ctx.skipSemantic)
|
|
740
747
|
return;
|
|
741
748
|
replaceSpans(pass, nameSpans(pass), (match, start) => {
|
|
742
749
|
if (isAllowListed(match, pass.ctx.allowList))
|
|
@@ -873,49 +880,84 @@ function toUtf16Spans(text, entities) {
|
|
|
873
880
|
.filter((e) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.end > e.start && e.end < offsets.length)
|
|
874
881
|
.map((e) => ({ start: offsets[e.start], end: offsets[e.end], score: e.score, entityType: e.entity_type }));
|
|
875
882
|
}
|
|
876
|
-
function
|
|
877
|
-
const ranked = [...
|
|
883
|
+
function resolveCluster(cluster) {
|
|
884
|
+
const ranked = [...cluster].sort((a, b) => b.score - a.score || (b.end - b.start) - (a.end - a.start) || a.start - b.start);
|
|
878
885
|
const kept = [];
|
|
879
886
|
for (const span of ranked) {
|
|
880
887
|
if (!kept.some((k) => span.start < k.end && k.start < span.end))
|
|
881
888
|
kept.push(span);
|
|
882
889
|
}
|
|
883
|
-
return kept
|
|
890
|
+
return kept;
|
|
891
|
+
}
|
|
892
|
+
function resolveOverlaps(spans) {
|
|
893
|
+
const sorted = [...spans].sort((a, b) => a.start - b.start);
|
|
894
|
+
const out = [];
|
|
895
|
+
let cluster = [];
|
|
896
|
+
let reach = -1;
|
|
897
|
+
for (const span of sorted) {
|
|
898
|
+
if (span.start >= reach && cluster.length > 0) {
|
|
899
|
+
out.push(...resolveCluster(cluster));
|
|
900
|
+
cluster = [];
|
|
901
|
+
}
|
|
902
|
+
cluster.push(span);
|
|
903
|
+
reach = Math.max(reach, span.end);
|
|
904
|
+
}
|
|
905
|
+
out.push(...resolveCluster(cluster));
|
|
906
|
+
return out.sort((a, b) => a.start - b.start);
|
|
907
|
+
}
|
|
908
|
+
function subtractLocked(spans, locked) {
|
|
909
|
+
const sortedLocks = [...locked].sort((a, b) => a[0] - b[0]);
|
|
910
|
+
const out = [];
|
|
911
|
+
let k = 0;
|
|
912
|
+
for (const span of spans) {
|
|
913
|
+
while (k < sortedLocks.length && sortedLocks[k][1] <= span.start)
|
|
914
|
+
k++;
|
|
915
|
+
let pos = span.start;
|
|
916
|
+
for (let j = k; j < sortedLocks.length && sortedLocks[j][0] < span.end; j++) {
|
|
917
|
+
if (sortedLocks[j][0] > pos)
|
|
918
|
+
out.push({ ...span, start: pos, end: sortedLocks[j][0] });
|
|
919
|
+
pos = Math.max(pos, sortedLocks[j][1]);
|
|
920
|
+
}
|
|
921
|
+
if (pos < span.end)
|
|
922
|
+
out.push({ ...span, start: pos, end: span.end });
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
884
925
|
}
|
|
885
926
|
function presidioSpans(value, entities, ctx, locked) {
|
|
886
927
|
var _a;
|
|
887
928
|
const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
|
|
888
|
-
|
|
889
|
-
return resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold && free(s)));
|
|
929
|
+
return subtractLocked(resolveOverlaps(toUtf16Spans(value, entities).filter((s) => s.score >= threshold)), locked);
|
|
890
930
|
}
|
|
891
931
|
async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
|
|
892
932
|
if (value.trim().length === 0)
|
|
893
933
|
return value;
|
|
894
934
|
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
895
|
-
const
|
|
896
|
-
let
|
|
897
|
-
for (const span of
|
|
935
|
+
const parts = [];
|
|
936
|
+
let last = 0;
|
|
937
|
+
for (const span of presidioSpans(value, entities, ctx, locked)) {
|
|
898
938
|
const matchedText = value.slice(span.start, span.end);
|
|
899
939
|
if (isAllowListed(matchedText, ctx.allowList))
|
|
900
940
|
continue;
|
|
901
941
|
const label = (0, presidio_1.getRedactorLabel)(span.entityType);
|
|
902
942
|
const category = (0, presidio_1.getRedactorCategory)(span.entityType);
|
|
903
943
|
const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
|
|
904
|
-
|
|
944
|
+
parts.push(value.slice(last, span.start), replacement);
|
|
945
|
+
last = span.end;
|
|
905
946
|
hits.push({
|
|
906
947
|
token: replacement, original: '***',
|
|
907
948
|
patternName: `presidio_${span.entityType}`, patternLabel: label,
|
|
908
949
|
category, field: path, itemIndex, confidence: span.score,
|
|
909
950
|
});
|
|
910
951
|
}
|
|
911
|
-
|
|
952
|
+
parts.push(value.slice(last));
|
|
953
|
+
return parts.join('');
|
|
912
954
|
}
|
|
913
955
|
const IDENTIFIER_KEY = /^[\p{L}_$][\p{L}\p{N}_$-]*$/u;
|
|
914
|
-
async function presidioKey(key, original, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
|
|
956
|
+
async function presidioKey(key, original, source, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
|
|
915
957
|
var _a, _b, _c;
|
|
916
958
|
if (IDENTIFIER_KEY.test(key) || !shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
|
|
917
959
|
return key;
|
|
918
|
-
const locked = (_c = (_b = (_a = ctx.keyLocks) === null || _a === void 0 ? void 0 : _a.get(
|
|
960
|
+
const locked = (_c = (_b = (_a = ctx.keyLocks) === null || _a === void 0 ? void 0 : _a.get(source)) === null || _b === void 0 ? void 0 : _b.get(key)) !== null && _c !== void 0 ? _c : [];
|
|
919
961
|
let out = key;
|
|
920
962
|
try {
|
|
921
963
|
out = await presidioRedactString(key, ctx, vault, sessionId, hits, itemIndex, path, locked);
|
|
@@ -923,10 +965,19 @@ async function presidioKey(key, original, taken, ctx, vault, sessionId, hits, it
|
|
|
923
965
|
catch {
|
|
924
966
|
return key;
|
|
925
967
|
}
|
|
926
|
-
const
|
|
968
|
+
const others = new Set(Object.keys(source).filter((k) => k !== key));
|
|
969
|
+
const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || others.has(k);
|
|
927
970
|
if (out === key || !clashes(out))
|
|
928
971
|
return out;
|
|
929
|
-
return distinctKey(out, original, clashes, ctx, vault, sessionId, Object.keys(taken));
|
|
972
|
+
return distinctKey(out, original, clashes, ctx, vault, sessionId, [...others, ...Object.keys(taken)]);
|
|
973
|
+
}
|
|
974
|
+
function renamePaths(hits, from, to) {
|
|
975
|
+
for (const h of hits) {
|
|
976
|
+
if (h.field === from)
|
|
977
|
+
h.field = to;
|
|
978
|
+
else if (h.field.startsWith(`${from}.`) || h.field.startsWith(`${from}[`))
|
|
979
|
+
h.field = to + h.field.slice(from.length);
|
|
980
|
+
}
|
|
930
981
|
}
|
|
931
982
|
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
|
|
932
983
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -956,8 +1007,15 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
|
|
|
956
1007
|
const child = path ? `${path}.${k}` : k;
|
|
957
1008
|
const childPolicy = policyPath ? `${policyPath}.${original}` : original;
|
|
958
1009
|
const lock = (_f = (_e = ctx.lockedSpans) === null || _e === void 0 ? void 0 : _e.get(value)) === null || _f === void 0 ? void 0 : _f.get(k);
|
|
959
|
-
const
|
|
960
|
-
|
|
1010
|
+
const keyHits = [];
|
|
1011
|
+
const source = value;
|
|
1012
|
+
const key = await presidioKey(k, original, source, out, ctx, vault, sessionId, keyHits, itemIndex, child, childPolicy);
|
|
1013
|
+
const finalChild = path ? `${path}.${key}` : key;
|
|
1014
|
+
if (key !== k)
|
|
1015
|
+
renamePaths(hits, child, finalChild);
|
|
1016
|
+
for (const h of keyHits)
|
|
1017
|
+
hits.push({ ...h, field: finalChild });
|
|
1018
|
+
setOwn(out, key, await presidioWalk(v, ctx, vault, sessionId, hits, itemIndex, finalChild, childPolicy, lock));
|
|
961
1019
|
}
|
|
962
1020
|
return out;
|
|
963
1021
|
}
|
|
@@ -206,12 +206,18 @@ exports.INJECTION_DESCRIPTIONS = [
|
|
|
206
206
|
'Invisible Unicode characters detected',
|
|
207
207
|
'Base64-encoded injection payload detected',
|
|
208
208
|
];
|
|
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)
|
|
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)/;
|
|
210
210
|
const BASE64_SUSPICIOUS_RE = /[A-Za-z0-9+/]{40,}={0,2}/g;
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
211
|
+
const INJECTION_SEED_RE = /ignore|override|system|prompt|instruction|jailbreak|developer mode|\bdan\b|bypass|unrestricted/i;
|
|
212
|
+
function messageFor(action, score, count) {
|
|
213
|
+
if (count === 0)
|
|
214
|
+
return 'No prompt injection detected.';
|
|
215
|
+
if (action === 'BLOCK')
|
|
216
|
+
return `BLOCKED: High-confidence prompt injection detected (score: ${score}). ${count} suspicious pattern(s) found.`;
|
|
217
|
+
if (action === 'FLAG')
|
|
218
|
+
return `FLAGGED: Potential prompt injection detected (score: ${score}). Review recommended.`;
|
|
219
|
+
return `Low-confidence match (score: ${score}). Likely safe but logged for review.`;
|
|
220
|
+
}
|
|
215
221
|
function detectPromptInjection(text) {
|
|
216
222
|
if (!text || text.trim().length === 0) {
|
|
217
223
|
return {
|
|
@@ -257,8 +263,7 @@ function detectPromptInjection(text) {
|
|
|
257
263
|
for (const b64 of base64Matches.slice(0, 5)) {
|
|
258
264
|
try {
|
|
259
265
|
const decoded = Buffer.from(b64, 'base64').toString('utf-8');
|
|
260
|
-
|
|
261
|
-
if (INJECTION_SEEDS.some((seed) => decodedLower.includes(seed))) {
|
|
266
|
+
if (INJECTION_SEED_RE.test(decoded)) {
|
|
262
267
|
encodedPayloadsFound = true;
|
|
263
268
|
totalScore += 0.35;
|
|
264
269
|
matches.push({
|
|
@@ -284,19 +289,7 @@ function detectPromptInjection(text) {
|
|
|
284
289
|
action = 'FLAG';
|
|
285
290
|
else if (matches.length > 0)
|
|
286
291
|
action = 'LOG_ONLY';
|
|
287
|
-
|
|
288
|
-
if (!matches.length) {
|
|
289
|
-
message = 'No prompt injection detected.';
|
|
290
|
-
}
|
|
291
|
-
else if (action === 'BLOCK') {
|
|
292
|
-
message = `BLOCKED: High-confidence prompt injection detected (score: ${totalScore}). ${matches.length} suspicious pattern(s) found.`;
|
|
293
|
-
}
|
|
294
|
-
else if (action === 'FLAG') {
|
|
295
|
-
message = `FLAGGED: Potential prompt injection detected (score: ${totalScore}). Review recommended.`;
|
|
296
|
-
}
|
|
297
|
-
else {
|
|
298
|
-
message = `Low-confidence match (score: ${totalScore}). Likely safe but logged for review.`;
|
|
299
|
-
}
|
|
292
|
+
const message = messageFor(action, totalScore, matches.length);
|
|
300
293
|
return {
|
|
301
294
|
detected: matches.length > 0,
|
|
302
295
|
score: totalScore,
|
|
@@ -316,11 +309,22 @@ function collectStrings(value, path, into) {
|
|
|
316
309
|
value.forEach((item, i) => collectStrings(item, `${path}[${i}]`, into));
|
|
317
310
|
}
|
|
318
311
|
else if (value !== null && typeof value === 'object') {
|
|
319
|
-
|
|
320
|
-
collectStrings(item, path ? `${path}.${key}` : key, into);
|
|
321
|
-
}
|
|
312
|
+
collectEntries(value, path, into);
|
|
322
313
|
}
|
|
323
314
|
}
|
|
315
|
+
function collectEntries(value, path, into) {
|
|
316
|
+
const keyPath = path ? `${path} (key)` : '(key)';
|
|
317
|
+
for (const [key, item] of Object.entries(value)) {
|
|
318
|
+
if (key.trim().length > 0)
|
|
319
|
+
into.push({ path: keyPath, text: key, key: true });
|
|
320
|
+
collectStrings(item, path ? `${path}.${key}` : key, into);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function withheld(result, key) {
|
|
324
|
+
if (!key)
|
|
325
|
+
return result;
|
|
326
|
+
return { ...result, matchedPatterns: result.matchedPatterns.map((m) => ({ ...m, matchedText: '(key text withheld)' })) };
|
|
327
|
+
}
|
|
324
328
|
function actionFor(score, detected) {
|
|
325
329
|
if (score >= 0.7)
|
|
326
330
|
return 'BLOCK';
|
|
@@ -332,7 +336,7 @@ function scanForInjection(data, currentPath = '') {
|
|
|
332
336
|
const fields = [];
|
|
333
337
|
collectStrings(data, currentPath, fields);
|
|
334
338
|
const found = fields
|
|
335
|
-
.map((f) => ({ path: f.path, result: detectPromptInjection(f.text) }))
|
|
339
|
+
.map((f) => ({ path: f.path, result: withheld(detectPromptInjection(f.text), f.key) }))
|
|
336
340
|
.filter((f) => f.result.detected)
|
|
337
341
|
.sort((a, b) => b.result.score - a.result.score || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
338
342
|
if (found.length === 0) {
|
|
@@ -341,13 +345,15 @@ function scanForInjection(data, currentPath = '') {
|
|
|
341
345
|
encodedPayloadsFound: false, zeroWidthCharsFound: false, message: 'No prompt injection detected.',
|
|
342
346
|
};
|
|
343
347
|
}
|
|
344
|
-
const
|
|
348
|
+
const raw = found.slice(1).reduce((sum, f) => sum + f.result.score * 0.3, found[0].result.score);
|
|
349
|
+
const score = Math.round(Math.min(1.0, raw) * 100) / 100;
|
|
345
350
|
const matchedPatterns = found.flatMap((f) => f.result.matchedPatterns.map((m) => ({ ...m, pattern: `[${f.path}] ${m.pattern}` })));
|
|
346
351
|
return {
|
|
347
352
|
...found[0].result,
|
|
348
353
|
detected: true,
|
|
349
354
|
score,
|
|
350
355
|
action: actionFor(score, true),
|
|
356
|
+
message: messageFor(actionFor(score, true), score, matchedPatterns.length),
|
|
351
357
|
matchedPatterns,
|
|
352
358
|
encodedPayloadsFound: found.some((f) => f.result.encodedPayloadsFound),
|
|
353
359
|
zeroWidthCharsFound: found.some((f) => f.result.zeroWidthCharsFound),
|
|
@@ -1613,37 +1613,54 @@ const NOT_NAMES = new Set([
|
|
|
1613
1613
|
for (const surname of ['Solangi', 'Ghaffar', 'Shaikh', 'Syed', 'Qureshi', 'Mirza', 'Abbasi', 'Awan', 'Bhutto', 'Butt', 'Chaudhry', 'Durrani', 'Gilani', 'Hashmi', 'Iqbal', 'Jatoi', 'Leghari', 'Mengal', 'Niazi', 'Raisani', 'Talpur', 'Zardari']) {
|
|
1614
1614
|
exports.LAST_NAMES.add(surname);
|
|
1615
1615
|
}
|
|
1616
|
+
const NAME_WORD = String.raw `\p{Lu}\p{Ll}*(?:\p{Lu}\p{Ll}+)*(?:['’-]\p{Lu}?\p{Ll}+(?:\p{Lu}\p{Ll}+)*)*`;
|
|
1617
|
+
const CAPITAL_RUN = new RegExp(String.raw `(?<![\p{L}\p{N}'’-])${NAME_WORD}(?:\s+${NAME_WORD})*(?![\p{L}\p{N}])`, 'gu');
|
|
1618
|
+
const MAX_NAME_WORDS = 4;
|
|
1619
|
+
function isKnownName(word) {
|
|
1620
|
+
const parts = word.split('-').flatMap((p) => [p, p.replace(/^(?:O['’]|Mc|Mac)(?=\p{Lu})/u, '')]);
|
|
1621
|
+
return parts.some((p) => exports.FIRST_NAMES.has(p) || exports.LAST_NAMES.has(p));
|
|
1622
|
+
}
|
|
1616
1623
|
function detectNamesInText(text) {
|
|
1617
1624
|
const results = [];
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1625
|
+
for (const match of text.matchAll(CAPITAL_RUN)) {
|
|
1626
|
+
if (!/\p{Ll}/u.test(match[0]))
|
|
1627
|
+
continue;
|
|
1628
|
+
const words = [...match[0].matchAll(/\S+/g)].map((w) => { var _a, _b; return ({ word: w[0], at: ((_a = match.index) !== null && _a !== void 0 ? _a : 0) + ((_b = w.index) !== null && _b !== void 0 ? _b : 0) }); });
|
|
1629
|
+
for (const segment of nameSegments(words))
|
|
1630
|
+
results.push(...chunkSegment(text, segment));
|
|
1623
1631
|
}
|
|
1624
1632
|
return results;
|
|
1625
1633
|
}
|
|
1626
|
-
function
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
let words = [];
|
|
1634
|
+
function nameSegments(words) {
|
|
1635
|
+
const segments = [];
|
|
1636
|
+
let current = [];
|
|
1630
1637
|
const flush = () => {
|
|
1631
|
-
const
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
runs.push({ name: sequence.slice(words[0].at, last.at + last.word.length), start, end: offset + last.at + last.word.length });
|
|
1636
|
-
}
|
|
1637
|
-
words = [];
|
|
1638
|
+
const anchored = current.some((w) => isKnownName(w.word) && !NOT_NAMES.has(w.word));
|
|
1639
|
+
if (anchored)
|
|
1640
|
+
segments.push(current);
|
|
1641
|
+
current = [];
|
|
1638
1642
|
};
|
|
1639
|
-
for (const
|
|
1640
|
-
if (NOT_NAMES.has(
|
|
1643
|
+
for (const w of words) {
|
|
1644
|
+
if (NOT_NAMES.has(w.word) && !isKnownName(w.word))
|
|
1641
1645
|
flush();
|
|
1642
1646
|
else
|
|
1643
|
-
|
|
1647
|
+
current.push(w);
|
|
1644
1648
|
}
|
|
1645
1649
|
flush();
|
|
1646
|
-
return
|
|
1650
|
+
return segments;
|
|
1651
|
+
}
|
|
1652
|
+
function chunkSegment(text, segment) {
|
|
1653
|
+
const spans = [];
|
|
1654
|
+
for (let i = 0; i < segment.length; i += MAX_NAME_WORDS) {
|
|
1655
|
+
const chunk = segment.slice(i, i + MAX_NAME_WORDS);
|
|
1656
|
+
if (chunk.length < 2 || !chunk.some((w) => isKnownName(w.word)))
|
|
1657
|
+
continue;
|
|
1658
|
+
const last = chunk[chunk.length - 1];
|
|
1659
|
+
const start = chunk[0].at;
|
|
1660
|
+
const end = last.at + last.word.length;
|
|
1661
|
+
spans.push({ name: text.slice(start, end), start, end });
|
|
1662
|
+
}
|
|
1663
|
+
return spans;
|
|
1647
1664
|
}
|
|
1648
1665
|
exports.NAME_CONTEXT_WORDS = [
|
|
1649
1666
|
'spoke with', 'contact', 'customer', 'client', 'patient', 'employee',
|
|
@@ -21,12 +21,35 @@ function luhnCheck(num) {
|
|
|
21
21
|
}
|
|
22
22
|
const EMAIL_LOCAL = /[A-Za-z0-9._%+'!#$&*/=?^`{|}~-]/;
|
|
23
23
|
const EMAIL_DOMAIN = /[\p{L}\p{N}.-]/u;
|
|
24
|
-
const EMAIL_DOMAIN_SHAPE = /^[\p{L}\p{N}-]+(?:[.][\p{L}\p{N}-]+)*[.]
|
|
24
|
+
const EMAIL_DOMAIN_SHAPE = /^[\p{L}\p{N}-]+(?:[.][\p{L}\p{N}-]+)*[.](?:\p{L}{2,}|xn--[a-z0-9-]{2,59})$/iu;
|
|
25
|
+
const QUOTED_LOCAL_MAX = 128;
|
|
26
|
+
function openingQuote(text, close) {
|
|
27
|
+
for (let j = close - 1; j >= Math.max(0, close - QUOTED_LOCAL_MAX); j--) {
|
|
28
|
+
if (text[j] === '\r' || text[j] === '\n')
|
|
29
|
+
return -1;
|
|
30
|
+
if (text[j] !== '"')
|
|
31
|
+
continue;
|
|
32
|
+
let slashes = 0;
|
|
33
|
+
while (text[j - 1 - slashes] === '\\')
|
|
34
|
+
slashes++;
|
|
35
|
+
if (slashes % 2 === 0)
|
|
36
|
+
return j;
|
|
37
|
+
}
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
function addressLiteralEnd(text, at) {
|
|
41
|
+
const close = text.indexOf(']', at + 2);
|
|
42
|
+
if (close < 0 || close - at > 64)
|
|
43
|
+
return -1;
|
|
44
|
+
const inner = text.slice(at + 2, close);
|
|
45
|
+
const v4 = /^\d{1,3}(?:[.]\d{1,3}){3}$/.test(inner) && inner.split('.').every((n) => Number(n) <= 255);
|
|
46
|
+
const v6 = /^IPv6:/i.test(inner) && isIpv6(inner.slice(5));
|
|
47
|
+
return v4 || v6 ? close + 1 : -1;
|
|
48
|
+
}
|
|
25
49
|
function emailLocalStart(text, at) {
|
|
26
50
|
if (text[at - 1] === '"') {
|
|
27
|
-
const open = text
|
|
28
|
-
|
|
29
|
-
return quoted.length > 0 && !/[\r\n"]/.test(quoted) ? open : -1;
|
|
51
|
+
const open = openingQuote(text, at - 1);
|
|
52
|
+
return open >= 0 && open < at - 2 ? open : -1;
|
|
30
53
|
}
|
|
31
54
|
let start = at;
|
|
32
55
|
while (start > 0 && EMAIL_LOCAL.test(text[start - 1]))
|
|
@@ -34,6 +57,8 @@ function emailLocalStart(text, at) {
|
|
|
34
57
|
return start < at ? start : -1;
|
|
35
58
|
}
|
|
36
59
|
function emailDomainEnd(text, at) {
|
|
60
|
+
if (text[at + 1] === '[')
|
|
61
|
+
return addressLiteralEnd(text, at);
|
|
37
62
|
let end = at + 1;
|
|
38
63
|
while (end < text.length && EMAIL_DOMAIN.test(text[end]))
|
|
39
64
|
end++;
|
|
@@ -62,6 +87,142 @@ function ibanCheck(iban) {
|
|
|
62
87
|
.reduce((acc, chunk) => (parseInt(acc + chunk, 10) % 97).toString(), '');
|
|
63
88
|
return parseInt(remainder, 10) === 1;
|
|
64
89
|
}
|
|
90
|
+
function firstAtLeast(sorted, min) {
|
|
91
|
+
let lo = 0;
|
|
92
|
+
let hi = sorted.length;
|
|
93
|
+
while (lo < hi) {
|
|
94
|
+
const mid = (lo + hi) >> 1;
|
|
95
|
+
if (sorted[mid] < min)
|
|
96
|
+
lo = mid + 1;
|
|
97
|
+
else
|
|
98
|
+
hi = mid;
|
|
99
|
+
}
|
|
100
|
+
return lo < sorted.length ? lo : -1;
|
|
101
|
+
}
|
|
102
|
+
function bodyEnd(text, from, limit) {
|
|
103
|
+
let end = from;
|
|
104
|
+
while (end < limit && /[A-Za-z0-9+/=:\s]/.test(text[end]))
|
|
105
|
+
end++;
|
|
106
|
+
return end;
|
|
107
|
+
}
|
|
108
|
+
function scanBlocks(text, header, endLine) {
|
|
109
|
+
const spans = [];
|
|
110
|
+
const starts = [...text.matchAll(header)].map((m) => { var _a; return ({ at: (_a = m.index) !== null && _a !== void 0 ? _a : 0, len: m[0].length }); });
|
|
111
|
+
const ends = [...text.matchAll(endLine)].map((m) => { var _a; return ({ at: (_a = m.index) !== null && _a !== void 0 ? _a : 0, len: m[0].length }); });
|
|
112
|
+
const endAt = ends.map((e) => e.at);
|
|
113
|
+
let reach = 0;
|
|
114
|
+
starts.forEach((s, i) => {
|
|
115
|
+
if (s.at < reach)
|
|
116
|
+
return;
|
|
117
|
+
const nextStart = i + 1 < starts.length ? starts[i + 1].at : text.length;
|
|
118
|
+
const k = firstAtLeast(endAt, s.at + s.len);
|
|
119
|
+
const end = k >= 0 && endAt[k] < nextStart ? endAt[k] + ends[k].len : bodyEnd(text, s.at + s.len, nextStart);
|
|
120
|
+
spans.push({ start: s.at, end });
|
|
121
|
+
reach = end;
|
|
122
|
+
});
|
|
123
|
+
return spans;
|
|
124
|
+
}
|
|
125
|
+
const scanPemKeys = (text) => scanBlocks(text, /-----BEGIN (?:RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g, /-----END (?:RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g);
|
|
126
|
+
const scanPgpKeys = (text) => scanBlocks(text, /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, /-----END PGP PRIVATE KEY BLOCK-----/g);
|
|
127
|
+
const scanEduEmails = (text) => scanEmails(text).filter((s) => /[.]edu$/i.test(text.slice(s.start, s.end)));
|
|
128
|
+
const IBAN_LENGTHS = {
|
|
129
|
+
AD: 24, AE: 23, AL: 28, AT: 20, AZ: 28, BA: 20, BE: 16, BG: 22, BH: 22, BI: 27, BR: 29, BY: 28, CH: 21, CR: 22,
|
|
130
|
+
CY: 28, CZ: 24, DE: 22, DJ: 27, DK: 18, DO: 28, EE: 20, EG: 29, ES: 24, FI: 18, FK: 18, FO: 18, FR: 27, GB: 22,
|
|
131
|
+
GE: 22, GI: 23, GL: 18, GR: 27, GT: 28, HR: 21, HU: 28, IE: 22, IL: 23, IQ: 23, IS: 26, IT: 27, JO: 30, KW: 30,
|
|
132
|
+
KZ: 20, LB: 28, LC: 32, LI: 21, LT: 20, LU: 20, LV: 21, LY: 25, MC: 27, MD: 24, ME: 22, MK: 19, MN: 20, MR: 27,
|
|
133
|
+
MT: 31, MU: 30, NI: 28, NL: 18, NO: 15, OM: 23, PK: 24, PL: 28, PS: 29, PT: 25, QA: 29, RO: 24, RS: 22, RU: 33,
|
|
134
|
+
SA: 24, SC: 31, SD: 18, SE: 24, SI: 19, SK: 24, SM: 27, SO: 23, ST: 25, SV: 28, TL: 23, TN: 24, TR: 26, UA: 29,
|
|
135
|
+
VA: 22, VG: 24, XK: 20, YE: 30,
|
|
136
|
+
};
|
|
137
|
+
const ALNUM = /[A-Za-z0-9]/;
|
|
138
|
+
const groupGap = (text, at, compact) => { var _a; return text[at] === ' ' && compact % 4 === 0 && ALNUM.test((_a = text[at + 1]) !== null && _a !== void 0 ? _a : ''); };
|
|
139
|
+
function ibanEnd(text, start, length) {
|
|
140
|
+
var _a;
|
|
141
|
+
let compact = 0;
|
|
142
|
+
let end = start;
|
|
143
|
+
while (end < text.length && compact < length) {
|
|
144
|
+
if (/[A-Z0-9]/.test(text[end]))
|
|
145
|
+
compact++;
|
|
146
|
+
else if (!groupGap(text, end, compact))
|
|
147
|
+
break;
|
|
148
|
+
end++;
|
|
149
|
+
}
|
|
150
|
+
return compact === length && !ALNUM.test((_a = text[end]) !== null && _a !== void 0 ? _a : '') ? end : -1;
|
|
151
|
+
}
|
|
152
|
+
function scanIbans(text) {
|
|
153
|
+
var _a;
|
|
154
|
+
const spans = [];
|
|
155
|
+
for (const m of text.matchAll(/(?<![A-Za-z0-9])([A-Z]{2})\d{2}/g)) {
|
|
156
|
+
const length = IBAN_LENGTHS[m[1]];
|
|
157
|
+
const start = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
158
|
+
const end = length ? ibanEnd(text, start, length) : -1;
|
|
159
|
+
if (end > 0)
|
|
160
|
+
spans.push({ start, end });
|
|
161
|
+
}
|
|
162
|
+
return spans;
|
|
163
|
+
}
|
|
164
|
+
function longestCard(groups, i) {
|
|
165
|
+
let best = -1;
|
|
166
|
+
let digits = '';
|
|
167
|
+
for (let j = i; j < groups.length && digits.length + groups[j].digits.length <= 19; j++) {
|
|
168
|
+
digits += groups[j].digits;
|
|
169
|
+
if (digits.length >= 13 && luhnCheck(digits))
|
|
170
|
+
best = j;
|
|
171
|
+
}
|
|
172
|
+
return best;
|
|
173
|
+
}
|
|
174
|
+
function cardsInRun(groups, spans) {
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < groups.length) {
|
|
177
|
+
const best = longestCard(groups, i);
|
|
178
|
+
if (best >= 0)
|
|
179
|
+
spans.push({ start: groups[i].start, end: groups[best].start + groups[best].digits.length });
|
|
180
|
+
i = best >= 0 ? best + 1 : i + 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function scanCards(text) {
|
|
184
|
+
var _a;
|
|
185
|
+
const spans = [];
|
|
186
|
+
for (const run of text.matchAll(/(?<![0-9A-Za-z_])\d+(?:[\s-]\d+)*(?![0-9A-Za-z_])/g)) {
|
|
187
|
+
const base = (_a = run.index) !== null && _a !== void 0 ? _a : 0;
|
|
188
|
+
cardsInRun([...run[0].matchAll(/\d+/g)].map((g) => { var _a; return ({ start: base + ((_a = g.index) !== null && _a !== void 0 ? _a : 0), digits: g[0] }); }), spans);
|
|
189
|
+
}
|
|
190
|
+
return spans;
|
|
191
|
+
}
|
|
192
|
+
function isIpv6(candidate) {
|
|
193
|
+
var _a;
|
|
194
|
+
const halves = candidate.split('::');
|
|
195
|
+
if (halves.length > 2)
|
|
196
|
+
return false;
|
|
197
|
+
const parts = (h) => (h === '' ? [] : h.split(':'));
|
|
198
|
+
const head = parts(halves[0]);
|
|
199
|
+
const tail = halves.length === 2 ? parts(halves[1]) : [];
|
|
200
|
+
const all = [...head, ...tail];
|
|
201
|
+
const last = (_a = all[all.length - 1]) !== null && _a !== void 0 ? _a : '';
|
|
202
|
+
const v4 = /^\d{1,3}(?:[.]\d{1,3}){3}$/.test(last) && last.split('.').every((n) => Number(n) <= 255);
|
|
203
|
+
const groups = all.length + (v4 ? 1 : 0);
|
|
204
|
+
if (!all.slice(0, v4 ? -1 : undefined).every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g)))
|
|
205
|
+
return false;
|
|
206
|
+
return halves.length === 2 ? groups <= 7 : groups === 8;
|
|
207
|
+
}
|
|
208
|
+
function trimIpv6(s) {
|
|
209
|
+
let out = s;
|
|
210
|
+
while (out.endsWith('.') || (out.endsWith(':') && !out.endsWith('::')))
|
|
211
|
+
out = out.slice(0, -1);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
const looksLikeIpv6 = (s) => s.split(':').length >= 3 && /[0-9A-Fa-f]/.test(s) && isIpv6(s);
|
|
215
|
+
function scanIpv6(text) {
|
|
216
|
+
var _a;
|
|
217
|
+
const spans = [];
|
|
218
|
+
for (const m of text.matchAll(/(?<![0-9A-Za-z:.])[0-9A-Fa-f:.]*:[0-9A-Fa-f:.]*/g)) {
|
|
219
|
+
const s = trimIpv6(m[0]);
|
|
220
|
+
const start = (_a = m.index) !== null && _a !== void 0 ? _a : 0;
|
|
221
|
+
if (looksLikeIpv6(s))
|
|
222
|
+
spans.push({ start, end: start + s.length });
|
|
223
|
+
}
|
|
224
|
+
return spans;
|
|
225
|
+
}
|
|
65
226
|
exports.PII_PATTERNS = [
|
|
66
227
|
{
|
|
67
228
|
name: 'email',
|
|
@@ -83,7 +244,7 @@ exports.PII_PATTERNS = [
|
|
|
83
244
|
name: 'phoneUK',
|
|
84
245
|
label: 'PHONE_UK',
|
|
85
246
|
category: 'contact',
|
|
86
|
-
regex:
|
|
247
|
+
regex: /(?<![\w+])(?:0|\+44)\s?\d{2,4}\s?\d{3,4}\s?\d{3,4}\b/g,
|
|
87
248
|
description: 'UK phone numbers',
|
|
88
249
|
},
|
|
89
250
|
{
|
|
@@ -111,21 +272,21 @@ exports.PII_PATTERNS = [
|
|
|
111
272
|
name: 'phoneFR',
|
|
112
273
|
label: 'PHONE_FR',
|
|
113
274
|
category: 'contact',
|
|
114
|
-
regex:
|
|
275
|
+
regex: /(?<![\w+])(?:0|\+33)\s?\d[\s.]\d{2}[\s.]\d{2}[\s.]\d{2}[\s.]\d{2}\b/g,
|
|
115
276
|
description: 'French phone numbers (06 12 34 56 78)',
|
|
116
277
|
},
|
|
117
278
|
{
|
|
118
279
|
name: 'phoneNL',
|
|
119
280
|
label: 'PHONE_NL',
|
|
120
281
|
category: 'contact',
|
|
121
|
-
regex:
|
|
282
|
+
regex: /(?<![\w+])(?:0|\+31)\s?\d{1,3}[\s-]\d{3,4}[\s-]\d{2,4}\b/g,
|
|
122
283
|
description: 'Dutch phone numbers',
|
|
123
284
|
},
|
|
124
285
|
{
|
|
125
286
|
name: 'personName',
|
|
126
287
|
label: 'PERSON',
|
|
127
288
|
category: 'identity',
|
|
128
|
-
regex:
|
|
289
|
+
regex: /(?<![\p{L}\p{N}])(?:Mr\.?|Mrs\.?|Ms\.?|Miss|Dr\.?|Prof\.?|Sr\.?|Jr\.?|Herr|Frau|Monsieur|Madame|Señor|Señora)\s+\p{Lu}\p{Ll}*(?:\p{Lu}\p{Ll}+)*(?:['’-]\p{Lu}?\p{Ll}+(?:\p{Lu}\p{Ll}+)*)*(?:\s+\p{Lu}\p{Ll}*(?:\p{Lu}\p{Ll}+)*(?:['’-]\p{Lu}?\p{Ll}+(?:\p{Lu}\p{Ll}+)*)*){0,3}(?![\p{L}\p{N}])/gu,
|
|
129
290
|
description: 'Person names with title prefixes (Mr., Mrs., Dr., etc.)',
|
|
130
291
|
},
|
|
131
292
|
{
|
|
@@ -312,6 +473,7 @@ exports.PII_PATTERNS = [
|
|
|
312
473
|
label: 'CREDIT_CARD',
|
|
313
474
|
category: 'financial',
|
|
314
475
|
regex: /\b(?:\d[\s-]?){12,18}\d\b/g,
|
|
476
|
+
scan: scanCards,
|
|
315
477
|
validate: (match) => luhnCheck(match),
|
|
316
478
|
description: 'Credit/debit card numbers, 13 to 19 digits (Luhn validated)',
|
|
317
479
|
},
|
|
@@ -320,6 +482,7 @@ exports.PII_PATTERNS = [
|
|
|
320
482
|
label: 'IBAN',
|
|
321
483
|
category: 'financial',
|
|
322
484
|
regex: /\b[A-Z]{2}\d{2}[\s]?[\dA-Z]{4}[\s]?(?:[\dA-Z]{4}[\s]?){2,7}[\dA-Z]{1,4}\b/g,
|
|
485
|
+
scan: scanIbans,
|
|
323
486
|
validate: (match) => ibanCheck(match),
|
|
324
487
|
description: 'International Bank Account Numbers (IBAN checksum validated)',
|
|
325
488
|
},
|
|
@@ -356,7 +519,8 @@ exports.PII_PATTERNS = [
|
|
|
356
519
|
label: 'IPV6_ADDRESS',
|
|
357
520
|
category: 'network',
|
|
358
521
|
regex: /\b(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:\b|\b(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b/g,
|
|
359
|
-
|
|
522
|
+
scan: scanIpv6,
|
|
523
|
+
description: 'IPv6 addresses, full, compressed (RFC 5952) and with an IPv4 tail',
|
|
360
524
|
},
|
|
361
525
|
{
|
|
362
526
|
name: 'macAddress',
|
|
@@ -460,7 +624,7 @@ exports.PII_PATTERNS = [
|
|
|
460
624
|
name: 'addressDE',
|
|
461
625
|
label: 'ADDRESS_DE',
|
|
462
626
|
category: 'location',
|
|
463
|
-
regex: /(?:[A-ZÀ-ÿa-zà-ÿ]
|
|
627
|
+
regex: /(?<![A-ZÀ-ÿa-zà-ÿ])(?:[A-ZÀ-ÿa-zà-ÿ]{1,60}(?:straße|strasse|str\.|weg|gasse|platz|allee|ring|damm|ufer|chaussee))\s+\d{1,5}[a-z]?(?:\s*[,/]\s*\d{5}\s+[A-ZÀ-ÿ][a-zà-ÿ]+(?:\s+[a-zà-ÿ]+\.?\s+[A-ZÀ-ÿ][a-zà-ÿ]+)?)?/gi,
|
|
464
628
|
description: 'German street addresses (Musterstraße 12, 80331 München)',
|
|
465
629
|
},
|
|
466
630
|
{
|
|
@@ -768,7 +932,7 @@ exports.PII_PATTERNS = [
|
|
|
768
932
|
name: 'ldapDN',
|
|
769
933
|
label: 'LDAP_DN',
|
|
770
934
|
category: 'enterprise',
|
|
771
|
-
regex: /(?:CN|OU|DC|O|C|L|ST)=[^,]
|
|
935
|
+
regex: /(?<![A-Za-z])(?:CN|OU|DC|O|C|L|ST)=[^,]{1,256}(?:,\s*(?:CN|OU|DC|O|C|L|ST)=[^,]{1,256}){1,32}/gi,
|
|
772
936
|
description: 'LDAP Distinguished Names (CN=...,OU=...,DC=...)',
|
|
773
937
|
},
|
|
774
938
|
{
|
|
@@ -796,7 +960,7 @@ exports.PII_PATTERNS = [
|
|
|
796
960
|
name: 'connStringKV',
|
|
797
961
|
label: 'CONN_STRING',
|
|
798
962
|
category: 'enterprise',
|
|
799
|
-
regex: /(?:Server|Data Source|Host)=[^;]
|
|
963
|
+
regex: /(?<![A-Za-z])(?:Server|Data Source|Host)=[^;]{1,512};[^;]{0,512}(?:;[^;]{0,512}){0,10}(?:Password|Pwd)=[^;]{1,512}/gi,
|
|
800
964
|
description: 'Connection strings with credentials (ADO.NET / key=value style)',
|
|
801
965
|
},
|
|
802
966
|
{
|
|
@@ -860,6 +1024,8 @@ exports.PII_PATTERNS = [
|
|
|
860
1024
|
label: 'PRIVATE_KEY',
|
|
861
1025
|
category: 'enterprise',
|
|
862
1026
|
regex: /-----BEGIN (RSA |DSA |EC |OPENSSH |ENCRYPTED )?PRIVATE KEY-----(?:[\s\S]*?-----END \1?PRIVATE KEY-----|[A-Za-z0-9+/=\s]*)/g,
|
|
1027
|
+
anchor: '-----BEGIN',
|
|
1028
|
+
scan: scanPemKeys,
|
|
863
1029
|
description: 'PEM private keys, the whole block through the END line',
|
|
864
1030
|
},
|
|
865
1031
|
{
|
|
@@ -978,35 +1144,35 @@ exports.PII_PATTERNS = [
|
|
|
978
1144
|
name: 'phoneES',
|
|
979
1145
|
label: 'PHONE_ES',
|
|
980
1146
|
category: 'contact',
|
|
981
|
-
regex:
|
|
1147
|
+
regex: /(?<![\w+])(?:\+?34[\s\-.]?)?\d{3}[\s\-.]\d{3}[\s\-.]\d{3}\b/g,
|
|
982
1148
|
description: 'Spanish phone numbers',
|
|
983
1149
|
},
|
|
984
1150
|
{
|
|
985
1151
|
name: 'phoneAU',
|
|
986
1152
|
label: 'PHONE_AU',
|
|
987
1153
|
category: 'contact',
|
|
988
|
-
regex:
|
|
1154
|
+
regex: /(?<![\w+])(?:\+?61[\s\-.]?)?0?\d[\s\-.]\d{4}[\s\-.]\d{4}\b/g,
|
|
989
1155
|
description: 'Australian phone numbers',
|
|
990
1156
|
},
|
|
991
1157
|
{
|
|
992
1158
|
name: 'phoneIN',
|
|
993
1159
|
label: 'PHONE_IN',
|
|
994
1160
|
category: 'contact',
|
|
995
|
-
regex:
|
|
1161
|
+
regex: /(?<![\w+])(?:\+?91[\s\-.]?)?[6-9]\d{4}[\s\-.]\d{5}\b/g,
|
|
996
1162
|
description: 'Indian phone numbers',
|
|
997
1163
|
},
|
|
998
1164
|
{
|
|
999
1165
|
name: 'phoneJP',
|
|
1000
1166
|
label: 'PHONE_JP',
|
|
1001
1167
|
category: 'contact',
|
|
1002
|
-
regex:
|
|
1168
|
+
regex: /(?<![\w+])(?:\+?81[\s\-.]?)?0?\d{1,4}[\s\-.]\d{1,4}[\s\-.]\d{4}\b/g,
|
|
1003
1169
|
description: 'Japanese phone numbers',
|
|
1004
1170
|
},
|
|
1005
1171
|
{
|
|
1006
1172
|
name: 'phoneBR',
|
|
1007
1173
|
label: 'PHONE_BR',
|
|
1008
1174
|
category: 'contact',
|
|
1009
|
-
regex:
|
|
1175
|
+
regex: /(?<![\w+])(?:\+?55[\s\-.]?)?\(?\d{2}\)?[\s\-.]?\d{4,5}[\s\-.]\d{4}\b/g,
|
|
1010
1176
|
description: 'Brazilian phone numbers',
|
|
1011
1177
|
},
|
|
1012
1178
|
{
|
|
@@ -1020,35 +1186,35 @@ exports.PII_PATTERNS = [
|
|
|
1020
1186
|
name: 'phoneCN',
|
|
1021
1187
|
label: 'PHONE_CN',
|
|
1022
1188
|
category: 'contact',
|
|
1023
|
-
regex:
|
|
1189
|
+
regex: /(?<![\w+])(?:\+?86[\s\-.]?)?1[3-9]\d{9}\b/g,
|
|
1024
1190
|
description: 'Chinese mobile phone numbers',
|
|
1025
1191
|
},
|
|
1026
1192
|
{
|
|
1027
1193
|
name: 'phoneKR',
|
|
1028
1194
|
label: 'PHONE_KR',
|
|
1029
1195
|
category: 'contact',
|
|
1030
|
-
regex:
|
|
1196
|
+
regex: /(?<![\w+])(?:\+?82[\s\-.]?)?0?\d{1,2}[\s\-.]\d{3,4}[\s\-.]\d{4}\b/g,
|
|
1031
1197
|
description: 'South Korean phone numbers',
|
|
1032
1198
|
},
|
|
1033
1199
|
{
|
|
1034
1200
|
name: 'phoneRU',
|
|
1035
1201
|
label: 'PHONE_RU',
|
|
1036
1202
|
category: 'contact',
|
|
1037
|
-
regex:
|
|
1203
|
+
regex: /(?<![\w+])(?:\+?7[\s\-.]?)?\d{3}[\s\-.]\d{3}[\s\-.]\d{2}[\s\-.]\d{2}\b/g,
|
|
1038
1204
|
description: 'Russian phone numbers',
|
|
1039
1205
|
},
|
|
1040
1206
|
{
|
|
1041
1207
|
name: 'phoneMX',
|
|
1042
1208
|
label: 'PHONE_MX',
|
|
1043
1209
|
category: 'contact',
|
|
1044
|
-
regex:
|
|
1210
|
+
regex: /(?<![\w+])(?:\+?52[\s\-.]?)?\d{2,3}[\s\-.]\d{3,4}[\s\-.]\d{4}\b/g,
|
|
1045
1211
|
description: 'Mexican phone numbers',
|
|
1046
1212
|
},
|
|
1047
1213
|
{
|
|
1048
1214
|
name: 'phoneZA',
|
|
1049
1215
|
label: 'PHONE_ZA',
|
|
1050
1216
|
category: 'contact',
|
|
1051
|
-
regex:
|
|
1217
|
+
regex: /(?<![\w+])(?:\+?27[\s\-.]?)?0?\d{2}[\s\-.]\d{3}[\s\-.]\d{4}\b/g,
|
|
1052
1218
|
description: 'South African phone numbers',
|
|
1053
1219
|
},
|
|
1054
1220
|
{
|
|
@@ -1483,6 +1649,8 @@ exports.PII_PATTERNS = [
|
|
|
1483
1649
|
label: 'PGP_KEY',
|
|
1484
1650
|
category: 'enterprise',
|
|
1485
1651
|
regex: /-----BEGIN PGP PRIVATE KEY BLOCK-----(?:[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----|[A-Za-z0-9+/=:\s]*)/g,
|
|
1652
|
+
anchor: '-----BEGIN',
|
|
1653
|
+
scan: scanPgpKeys,
|
|
1486
1654
|
description: 'PGP private key blocks, the whole block through the END line',
|
|
1487
1655
|
},
|
|
1488
1656
|
{
|
|
@@ -1574,6 +1742,8 @@ exports.PII_PATTERNS = [
|
|
|
1574
1742
|
label: 'EDU_EMAIL',
|
|
1575
1743
|
category: 'contact',
|
|
1576
1744
|
regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.edu/g,
|
|
1745
|
+
anchor: '@',
|
|
1746
|
+
scan: scanEduEmails,
|
|
1577
1747
|
description: 'Educational email addresses (.edu)',
|
|
1578
1748
|
},
|
|
1579
1749
|
{
|
|
@@ -6,6 +6,7 @@ exports.analyzeWithPresidio = analyzeWithPresidio;
|
|
|
6
6
|
exports.getRedactorLabel = getRedactorLabel;
|
|
7
7
|
exports.getRedactorCategory = getRedactorCategory;
|
|
8
8
|
exports.getPresidioEntities = getPresidioEntities;
|
|
9
|
+
const patterns_1 = require("./patterns");
|
|
9
10
|
const PRESIDIO_TO_REDACTOR_LABEL = {
|
|
10
11
|
PERSON: 'PERSON_NAME',
|
|
11
12
|
EMAIL_ADDRESS: 'EMAIL',
|
|
@@ -132,7 +133,10 @@ function getRedactorLabel(presidioEntityType) {
|
|
|
132
133
|
return PRESIDIO_TO_REDACTOR_LABEL[presidioEntityType] || presidioEntityType;
|
|
133
134
|
}
|
|
134
135
|
function getRedactorCategory(presidioEntityType) {
|
|
135
|
-
|
|
136
|
+
var _a, _b;
|
|
137
|
+
const label = getRedactorLabel(presidioEntityType);
|
|
138
|
+
const builtIn = patterns_1.PII_PATTERNS.find((p) => p.label === label);
|
|
139
|
+
return (_b = (_a = builtIn === null || builtIn === void 0 ? void 0 : builtIn.category) !== null && _a !== void 0 ? _a : PRESIDIO_TO_CATEGORY[presidioEntityType]) !== null && _b !== void 0 ? _b : 'identity';
|
|
136
140
|
}
|
|
137
141
|
async function getPresidioEntities(baseUrl, request) {
|
|
138
142
|
if (!request || !isHttpUrl(baseUrl))
|
|
@@ -4,7 +4,7 @@ exports.checkRegex = checkRegex;
|
|
|
4
4
|
const regexCharset_1 = require("./regexCharset");
|
|
5
5
|
const MAX_REGEX_LENGTH = 500;
|
|
6
6
|
const newFrame = (zeroWidth = false, inLook = zeroWidth) => ({
|
|
7
|
-
hasVariableQuantifier: false, hasAlternation: false, run: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
|
|
7
|
+
hasVariableQuantifier: false, hasAlternation: false, run: null, altRun: null, chars: (0, regexCharset_1.emptySet)(), zeroWidth,
|
|
8
8
|
branchConsumes: false, allConsume: true, nullable: false, inLook,
|
|
9
9
|
});
|
|
10
10
|
function endBranch(frame) {
|
|
@@ -58,6 +58,7 @@ function alternation(stack, i) {
|
|
|
58
58
|
const top = stack[stack.length - 1];
|
|
59
59
|
top.hasAlternation = true;
|
|
60
60
|
top.run = null;
|
|
61
|
+
top.altRun = null;
|
|
61
62
|
endBranch(top);
|
|
62
63
|
return { atomEnd: i, closed: null, next: i + 1 };
|
|
63
64
|
}
|
|
@@ -103,6 +104,7 @@ function applyQuantifier(q, closed, top) {
|
|
|
103
104
|
return null;
|
|
104
105
|
}
|
|
105
106
|
const SEQUENTIAL = 'two repetitions in a row can match the same characters (catastrophic backtracking)';
|
|
107
|
+
const CHAINED = 'two groups of alternatives in a row can match the same characters (catastrophic backtracking)';
|
|
106
108
|
function applySequence(chars, variable, required, top) {
|
|
107
109
|
if (chars === null)
|
|
108
110
|
return null;
|
|
@@ -117,6 +119,19 @@ function applySequence(chars, variable, required, top) {
|
|
|
117
119
|
top.run = null;
|
|
118
120
|
return null;
|
|
119
121
|
}
|
|
122
|
+
function applyBranching(chars, branching, required, top) {
|
|
123
|
+
if (chars === null)
|
|
124
|
+
return null;
|
|
125
|
+
const shared = top.altRun !== null && (0, regexCharset_1.overlaps)(top.altRun, chars);
|
|
126
|
+
if (branching) {
|
|
127
|
+
if (shared)
|
|
128
|
+
return CHAINED;
|
|
129
|
+
top.altRun = top.altRun ? (0, regexCharset_1.union)(top.altRun, chars) : chars;
|
|
130
|
+
}
|
|
131
|
+
else if (required && !shared)
|
|
132
|
+
top.altRun = null;
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
120
135
|
function stepChars(src, i, step) {
|
|
121
136
|
const closed = step.closed;
|
|
122
137
|
if (!closed)
|
|
@@ -140,8 +155,10 @@ function stepProblem(src, i, step, q, top) {
|
|
|
140
155
|
const required = consumes(q, chars, step.closed);
|
|
141
156
|
if (required)
|
|
142
157
|
top.branchConsumes = true;
|
|
143
|
-
return
|
|
158
|
+
return (_a = applyBranching(chars, isBranching(step.closed), required, top)) !== null && _a !== void 0 ? _a : applySequence(chars, variesInLength(q, step.closed), required, top);
|
|
144
159
|
}
|
|
160
|
+
const isBranching = (closed) => !!(closed === null || closed === void 0 ? void 0 : closed.hasAlternation) && !closed.zeroWidth;
|
|
161
|
+
const variesInLength = (q, closed) => isVariable(q) || !!(closed === null || closed === void 0 ? void 0 : closed.hasVariableQuantifier);
|
|
145
162
|
function structuralProblem(src) {
|
|
146
163
|
const stack = [newFrame()];
|
|
147
164
|
let i = 0;
|
|
@@ -106,20 +106,26 @@ class MemoryVault {
|
|
|
106
106
|
return;
|
|
107
107
|
assertNoConflict(session, entry);
|
|
108
108
|
session.entries[entry.token] = entry;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const index = this.index(key, session);
|
|
110
|
+
const first = index.get(entry.original);
|
|
111
|
+
if (!first || first.token === entry.token)
|
|
112
|
+
index.set(entry.original, entry);
|
|
112
113
|
}
|
|
113
114
|
findByOriginal(sessionId, original) {
|
|
114
|
-
var _a;
|
|
115
115
|
const key = this.key(sessionId);
|
|
116
|
-
const entry = (_a = reverseIndex.get(key)) === null || _a === void 0 ? void 0 : _a.get(original);
|
|
117
|
-
if (entry)
|
|
118
|
-
return entry;
|
|
119
116
|
const session = memoryStore.get(key);
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
return session ? this.index(key, session).get(original) : undefined;
|
|
118
|
+
}
|
|
119
|
+
index(key, session) {
|
|
120
|
+
let index = reverseIndex.get(key);
|
|
121
|
+
if (!index) {
|
|
122
|
+
index = new Map();
|
|
123
|
+
for (const e of Object.values(session.entries))
|
|
124
|
+
if (!index.has(e.original))
|
|
125
|
+
index.set(e.original, e);
|
|
126
|
+
reverseIndex.set(key, index);
|
|
127
|
+
}
|
|
128
|
+
return index;
|
|
123
129
|
}
|
|
124
130
|
deleteSession(sessionId) {
|
|
125
131
|
this.drop(this.key(sessionId));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-redactor",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.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",
|