n8n-nodes-redactor 5.2.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 +24 -5
- package/dist/nodes/Redactor/classification.js +9 -6
- package/dist/nodes/Redactor/engine.js +264 -97
- package/dist/nodes/Redactor/injection.js +69 -86
- package/dist/nodes/Redactor/names.js +46 -16
- package/dist/nodes/Redactor/patterns.js +231 -27
- package/dist/nodes/Redactor/presidio.js +5 -1
- package/dist/nodes/Redactor/profiles.js +1 -1
- package/dist/nodes/Redactor/regexSafety.js +19 -2
- package/dist/nodes/Redactor/ropa.js +19 -3
- package/dist/nodes/Redactor/runtime.js +9 -0
- package/dist/nodes/Redactor/types.js +7 -0
- package/dist/nodes/Redactor/vault.js +33 -11
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ exports.buildReport = buildReport;
|
|
|
43
43
|
const crypto = __importStar(require("crypto"));
|
|
44
44
|
const patterns_1 = require("./patterns");
|
|
45
45
|
const regexSafety_1 = require("./regexSafety");
|
|
46
|
+
const types_1 = require("./types");
|
|
46
47
|
const context_1 = require("./context");
|
|
47
48
|
const names_1 = require("./names");
|
|
48
49
|
const pseudonymize_1 = require("./pseudonymize");
|
|
@@ -77,7 +78,6 @@ function generateToken(label, index) {
|
|
|
77
78
|
function setOwn(obj, key, value) {
|
|
78
79
|
Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
|
|
79
80
|
}
|
|
80
|
-
const TOKEN_RE = /^\[[A-Z][A-Z0-9_]*_\d+\]$/;
|
|
81
81
|
function maskValue(value, label) {
|
|
82
82
|
if (value.length <= 2)
|
|
83
83
|
return '*'.repeat(value.length);
|
|
@@ -136,13 +136,15 @@ function applyRedaction(match, pattern, ctx, vault, sessionId) {
|
|
|
136
136
|
const irreversible = IRREVERSIBLE[ctx.mode];
|
|
137
137
|
return irreversible ? irreversible(match, pattern.label) : match;
|
|
138
138
|
}
|
|
139
|
+
const nextIndex = new WeakMap();
|
|
139
140
|
function freshToken(label, vault, sessionId, reserved) {
|
|
140
|
-
var _a, _b;
|
|
141
|
+
var _a, _b, _c;
|
|
141
142
|
const entries = (_b = (_a = vault.getSession(sessionId)) === null || _a === void 0 ? void 0 : _a.entries) !== null && _b !== void 0 ? _b : {};
|
|
142
|
-
let index = Object.keys(entries).length;
|
|
143
|
+
let index = (_c = nextIndex.get(entries)) !== null && _c !== void 0 ? _c : Object.keys(entries).length;
|
|
143
144
|
let token = generateToken(label, index);
|
|
144
145
|
while (entries[token] || (reserved === null || reserved === void 0 ? void 0 : reserved.has(token)))
|
|
145
146
|
token = generateToken(label, ++index);
|
|
147
|
+
nextIndex.set(entries, index + 1);
|
|
146
148
|
return token;
|
|
147
149
|
}
|
|
148
150
|
const TOKEN_LIKE_RE = /\[[A-Z][A-Z0-9_]*_\d+\]/g;
|
|
@@ -455,7 +457,7 @@ function numericMapping(out, text, ctx, vault, sessionId) {
|
|
|
455
457
|
return token;
|
|
456
458
|
}
|
|
457
459
|
function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, display, siblingKeys) {
|
|
458
|
-
const text = String(value);
|
|
460
|
+
const text = Object.is(value, -0) ? '-0' : String(value);
|
|
459
461
|
const numCtx = { ...ctx, dedup: false, numeric: true };
|
|
460
462
|
let out = null;
|
|
461
463
|
if (!ctx.skipSemantic)
|
|
@@ -472,17 +474,12 @@ function redactNumber(value, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
472
474
|
ctx.lastLocked = [[0, mapped.length]];
|
|
473
475
|
return mapped;
|
|
474
476
|
}
|
|
475
|
-
function
|
|
477
|
+
function distinctKey(out, original, clashes, ctx, vault, sessionId, reserved) {
|
|
476
478
|
var _a;
|
|
477
|
-
const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
|
|
478
|
-
if (out === key)
|
|
479
|
-
return key;
|
|
480
|
-
const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
|
|
481
|
-
if (!clashes(out))
|
|
482
|
-
return out;
|
|
483
479
|
if (ctx.mode === 'token' || ctx.mode === 'pseudonymize') {
|
|
484
|
-
const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...
|
|
485
|
-
vault.addEntry(sessionId, { token, original
|
|
480
|
+
const token = freshToken('KEY', vault, sessionId, new Set([...((_a = ctx.reservedTokens) !== null && _a !== void 0 ? _a : []), ...reserved]));
|
|
481
|
+
vault.addEntry(sessionId, { token, original, patternLabel: 'KEY', category: 'other', createdAt: new Date().toISOString() });
|
|
482
|
+
ctx.lastLocked = [[0, token.length]];
|
|
486
483
|
return token;
|
|
487
484
|
}
|
|
488
485
|
let suffixed = out;
|
|
@@ -490,6 +487,24 @@ function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, key
|
|
|
490
487
|
suffixed = `${out}_${n}`;
|
|
491
488
|
return suffixed;
|
|
492
489
|
}
|
|
490
|
+
function redactKey(key, taken, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits) {
|
|
491
|
+
const out = redactText(key, ctx, vault, sessionId, keyHits, itemIndex, '');
|
|
492
|
+
if (out === key)
|
|
493
|
+
return key;
|
|
494
|
+
const clashes = (k) => k === '' || Object.prototype.hasOwnProperty.call(taken, k) || sourceKeys.has(k);
|
|
495
|
+
if (!clashes(out))
|
|
496
|
+
return out;
|
|
497
|
+
return distinctKey(out, key, clashes, ctx, vault, sessionId, [...sourceKeys, ...Object.keys(taken)]);
|
|
498
|
+
}
|
|
499
|
+
function rememberKeyLock(ctx, container, key) {
|
|
500
|
+
var _a;
|
|
501
|
+
if (ctx.lastLocked && ctx.keyLocks) {
|
|
502
|
+
const byKey = (_a = ctx.keyLocks.get(container)) !== null && _a !== void 0 ? _a : new Map();
|
|
503
|
+
byKey.set(key, ctx.lastLocked);
|
|
504
|
+
ctx.keyLocks.set(container, byKey);
|
|
505
|
+
}
|
|
506
|
+
ctx.lastLocked = undefined;
|
|
507
|
+
}
|
|
493
508
|
function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, display) {
|
|
494
509
|
var _a;
|
|
495
510
|
const entries = Object.entries(value);
|
|
@@ -500,9 +515,11 @@ function redactObject(value, ctx, vault, sessionId, hits, itemIndex, path, displ
|
|
|
500
515
|
for (const [k, v] of entries) {
|
|
501
516
|
const fieldPath = path ? `${path}.${k}` : k;
|
|
502
517
|
const keyHits = [];
|
|
518
|
+
ctx.lastLocked = undefined;
|
|
503
519
|
const outKey = shouldProcessField(fieldPath, ctx.fieldMode, ctx.fieldRules)
|
|
504
520
|
? redactKey(k, obj, sourceKeys, ctx, vault, sessionId, itemIndex, keyHits)
|
|
505
|
-
: k;
|
|
521
|
+
: protectLiterals(k, ctx, vault, sessionId, hits, itemIndex);
|
|
522
|
+
rememberKeyLock(ctx, obj, outKey);
|
|
506
523
|
origins.set(outKey, k);
|
|
507
524
|
const childDisplay = display ? `${display}.${outKey}` : outKey;
|
|
508
525
|
for (const h of keyHits)
|
|
@@ -516,7 +533,7 @@ function redactValue(value, ctx, vault, sessionId, hits, itemIndex, currentPath
|
|
|
516
533
|
const processed = shouldProcessField(currentPath, ctx.fieldMode, ctx.fieldRules);
|
|
517
534
|
if (typeof value === 'string') {
|
|
518
535
|
if (!processed)
|
|
519
|
-
return value;
|
|
536
|
+
return protectLiterals(value, ctx, vault, sessionId, hits, itemIndex);
|
|
520
537
|
if (!ctx.skipSemantic) {
|
|
521
538
|
const semantic = redactSemantic(value, ctx, vault, sessionId, hits, itemIndex, currentPath, displayPath, siblingKeys);
|
|
522
539
|
if (semantic !== null)
|
|
@@ -557,19 +574,18 @@ class SegmentCursor {
|
|
|
557
574
|
this.cur = segs[0];
|
|
558
575
|
}
|
|
559
576
|
freeAt(start, end) {
|
|
560
|
-
while (this.cur && (this.cur.locked || this.cur.
|
|
577
|
+
while (this.cur && (this.cur.locked || this.cur.end <= start))
|
|
561
578
|
this.take();
|
|
562
579
|
const cur = this.cur;
|
|
563
|
-
return cur && cur.src <= start && end <= cur.
|
|
580
|
+
return cur && cur.src <= start && end <= cur.end ? cur : undefined;
|
|
564
581
|
}
|
|
565
582
|
lock(start, end, replacement) {
|
|
566
583
|
const cur = this.cur;
|
|
567
584
|
if (start > cur.src)
|
|
568
|
-
this.next.push(
|
|
569
|
-
this.next.push({ text: replacement, locked: true, src:
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
this.cur = { text: cur.text.slice(tail), locked: false, src: end };
|
|
585
|
+
this.next.push(freeSlice(cur, cur.src, start));
|
|
586
|
+
this.next.push({ text: replacement, locked: true, src: start, end });
|
|
587
|
+
if (end < cur.end)
|
|
588
|
+
this.cur = freeSlice(cur, end, cur.end);
|
|
573
589
|
else
|
|
574
590
|
this.cur = this.segs[++this.i];
|
|
575
591
|
}
|
|
@@ -583,33 +599,104 @@ class SegmentCursor {
|
|
|
583
599
|
this.cur = this.segs[++this.i];
|
|
584
600
|
}
|
|
585
601
|
}
|
|
586
|
-
function
|
|
602
|
+
function freeSlice(seg, start, end) {
|
|
603
|
+
return { text: seg.text.slice(start - seg.src, end - seg.src), locked: false, src: start, end };
|
|
604
|
+
}
|
|
605
|
+
function redactMatch(pass, match, decision) {
|
|
606
|
+
if (decision.store)
|
|
607
|
+
return decision.store(match);
|
|
608
|
+
const replacement = applyRedaction(match, decision.pattern, pass.ctx, pass.vault, pass.sessionId);
|
|
609
|
+
if (decision.detail) {
|
|
610
|
+
pass.hits.push({ token: replacement, original: '***', field: pass.fieldPath, itemIndex: pass.itemIndex, ...decision.detail });
|
|
611
|
+
}
|
|
612
|
+
return replacement;
|
|
613
|
+
}
|
|
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];
|
|
620
|
+
}
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
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) {
|
|
628
|
+
const end = Math.min(seg.end, p.end);
|
|
629
|
+
into.push({ text: redactMatch(pass, pass.source.slice(pos, end), p.decision), locked: true, src: pos, end });
|
|
630
|
+
pos = end;
|
|
631
|
+
continue;
|
|
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);
|
|
651
|
+
}
|
|
652
|
+
pass.segs = next;
|
|
653
|
+
}
|
|
654
|
+
function claimOne(pass, cursor, pending, start, end, decide) {
|
|
655
|
+
const text = pass.source.slice(start, end);
|
|
656
|
+
const free = cursor.freeAt(start, end) !== undefined;
|
|
657
|
+
const decision = decide(text, start);
|
|
658
|
+
if (decision && free)
|
|
659
|
+
cursor.lock(start, end, redactMatch(pass, text, decision));
|
|
660
|
+
else if (decision)
|
|
661
|
+
pending.push({ start, end, decision });
|
|
662
|
+
return free;
|
|
663
|
+
}
|
|
664
|
+
function replaceFree(pass, re, decide) {
|
|
587
665
|
const global = new RegExp(re.source, re.flags.includes('g') ? re.flags : `${re.flags}g`);
|
|
588
666
|
const cursor = new SegmentCursor(pass.segs);
|
|
667
|
+
const pending = [];
|
|
589
668
|
let m;
|
|
590
669
|
while ((m = global.exec(pass.source)) !== null) {
|
|
591
670
|
const start = m.index;
|
|
592
|
-
|
|
593
|
-
if (m[0] === '' || !cursor.freeAt(start, end)) {
|
|
671
|
+
if (m[0] === '' || !claimOne(pass, cursor, pending, start, start + m[0].length, decide))
|
|
594
672
|
global.lastIndex = start + 1;
|
|
595
|
-
continue;
|
|
596
|
-
}
|
|
597
|
-
const replacement = fn(m[0], start);
|
|
598
|
-
if (replacement !== null)
|
|
599
|
-
cursor.lock(start, end, replacement);
|
|
600
673
|
}
|
|
601
674
|
pass.segs = cursor.finish();
|
|
675
|
+
claimPending(pass, pending);
|
|
602
676
|
}
|
|
603
|
-
function
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
677
|
+
function replaceSpans(pass, spans, decide) {
|
|
678
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
679
|
+
const cursor = new SegmentCursor(pass.segs);
|
|
680
|
+
const pending = [];
|
|
681
|
+
let reach = -1;
|
|
682
|
+
for (const span of ordered) {
|
|
683
|
+
if (span.end <= reach)
|
|
684
|
+
continue;
|
|
685
|
+
claimOne(pass, cursor, pending, span.start, span.end, decide);
|
|
686
|
+
reach = span.end;
|
|
687
|
+
}
|
|
688
|
+
pass.segs = cursor.finish();
|
|
689
|
+
claimPending(pass, pending);
|
|
607
690
|
}
|
|
608
691
|
function denyListRegex(entry) {
|
|
609
692
|
if (entry.type === 'regex')
|
|
610
693
|
return isSafeRegex(entry.value) ? new RegExp(entry.value, 'gi') : null;
|
|
611
694
|
return new RegExp(escapeRe(entry.value), 'gi');
|
|
612
695
|
}
|
|
696
|
+
const DENY_DECISION = {
|
|
697
|
+
pattern: { label: 'DENY_LIST', category: 'other' },
|
|
698
|
+
detail: { patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE },
|
|
699
|
+
};
|
|
613
700
|
function denyListStage(pass) {
|
|
614
701
|
var _a;
|
|
615
702
|
for (const entry of (_a = pass.ctx.denyList) !== null && _a !== void 0 ? _a : []) {
|
|
@@ -617,61 +704,58 @@ function denyListStage(pass) {
|
|
|
617
704
|
assertUserRegexInput(pass.source, `Deny list regex "${entry.value}"`);
|
|
618
705
|
try {
|
|
619
706
|
const re = denyListRegex(entry);
|
|
620
|
-
if (
|
|
621
|
-
|
|
622
|
-
replaceFree(pass, re, (match) => TOKEN_RE.test(match)
|
|
623
|
-
? null
|
|
624
|
-
: redactMatch(pass, match, { label: 'DENY_LIST', category: 'other' }, {
|
|
625
|
-
patternName: 'denyList', patternLabel: 'DENY_LIST', category: 'other', confidence: context_1.DENY_LIST_CONFIDENCE,
|
|
626
|
-
}));
|
|
707
|
+
if (re)
|
|
708
|
+
replaceFree(pass, re, () => DENY_DECISION);
|
|
627
709
|
}
|
|
628
710
|
catch { }
|
|
629
711
|
}
|
|
630
712
|
}
|
|
631
|
-
function
|
|
632
|
-
if (TOKEN_RE.test(match) || (match.startsWith('[') && match.endsWith(']')))
|
|
633
|
-
return null;
|
|
713
|
+
function builtInDecision(pass, pattern, match, offset) {
|
|
634
714
|
const validatorPassed = pattern.validate ? pattern.validate(match) : true;
|
|
635
715
|
if (!validatorPassed || isAllowListed(match, pass.ctx.allowList))
|
|
636
716
|
return null;
|
|
637
717
|
const confidence = (0, context_1.calculateConfidence)(pattern.name, !!pattern.validate, validatorPassed, pass.source, offset, offset + match.length);
|
|
638
718
|
if (confidence < pass.threshold)
|
|
639
719
|
return null;
|
|
640
|
-
return
|
|
641
|
-
patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence,
|
|
642
|
-
});
|
|
720
|
+
return { pattern, detail: { patternName: pattern.name, patternLabel: pattern.label, category: pattern.category, confidence } };
|
|
643
721
|
}
|
|
644
722
|
function builtInStage(pass) {
|
|
645
723
|
const enabled = (0, patterns_1.getPatternsByNames)(pass.ctx.enabledPatterns);
|
|
646
724
|
for (const pattern of [...enabled.filter((p) => p.validate), ...enabled.filter((p) => !p.validate)]) {
|
|
647
|
-
|
|
725
|
+
if (pattern.anchor && !pass.source.includes(pattern.anchor))
|
|
726
|
+
continue;
|
|
727
|
+
const decide = (match, offset) => builtInDecision(pass, pattern, match, offset);
|
|
728
|
+
if (pattern.scan)
|
|
729
|
+
replaceSpans(pass, pattern.scan(pass.source), decide);
|
|
730
|
+
else
|
|
731
|
+
replaceFree(pass, pattern.regex, decide);
|
|
648
732
|
}
|
|
649
733
|
}
|
|
650
|
-
function
|
|
651
|
-
const
|
|
652
|
-
for (const seg of pass.segs)
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
734
|
+
function nameSpans(pass) {
|
|
735
|
+
const spans = (0, names_1.detectNamesInText)(pass.source).map((nm) => ({ start: nm.start, end: nm.end }));
|
|
736
|
+
for (const seg of pass.segs) {
|
|
737
|
+
if (seg.locked)
|
|
738
|
+
continue;
|
|
739
|
+
for (const nm of (0, names_1.detectNamesInText)(seg.text))
|
|
740
|
+
spans.push({ start: seg.src + nm.start, end: seg.src + nm.end });
|
|
741
|
+
}
|
|
742
|
+
return spans;
|
|
657
743
|
}
|
|
658
744
|
function nameStage(pass) {
|
|
659
|
-
|
|
745
|
+
var _a;
|
|
746
|
+
if ((_a = pass.ctx.skipNames) !== null && _a !== void 0 ? _a : pass.ctx.skipSemantic)
|
|
660
747
|
return;
|
|
661
|
-
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return replacement;
|
|
673
|
-
});
|
|
674
|
-
}
|
|
748
|
+
replaceSpans(pass, nameSpans(pass), (match, start) => {
|
|
749
|
+
if (isAllowListed(match, pass.ctx.allowList))
|
|
750
|
+
return null;
|
|
751
|
+
const confidence = (0, context_1.hasContextWords)(pass.source, start, start + match.length, names_1.NAME_CONTEXT_WORDS) ? 0.8 : 0.65;
|
|
752
|
+
if (confidence < pass.threshold)
|
|
753
|
+
return null;
|
|
754
|
+
return {
|
|
755
|
+
pattern: { label: 'PERSON_NAME', category: 'identity' },
|
|
756
|
+
detail: { patternName: 'personNameDict', patternLabel: 'PERSON_NAME', category: 'identity', confidence },
|
|
757
|
+
};
|
|
758
|
+
});
|
|
675
759
|
}
|
|
676
760
|
function customStage(pass) {
|
|
677
761
|
if (context_1.CUSTOM_PATTERN_CONFIDENCE < pass.threshold)
|
|
@@ -679,15 +763,15 @@ function customStage(pass) {
|
|
|
679
763
|
for (const cp of pass.ctx.customPatterns) {
|
|
680
764
|
if (!cp.label || !cp.regex || !isSafeRegex(cp.regex))
|
|
681
765
|
continue;
|
|
682
|
-
const
|
|
766
|
+
const category = (0, types_1.isPiiCategory)(cp.category) ? cp.category : cp.category ? 'other' : 'identity';
|
|
767
|
+
const pattern = { label: cp.label.toUpperCase(), category };
|
|
768
|
+
const decision = {
|
|
769
|
+
pattern,
|
|
770
|
+
detail: { patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category, confidence: context_1.CUSTOM_PATTERN_CONFIDENCE },
|
|
771
|
+
};
|
|
683
772
|
assertUserRegexInput(pass.source, `Custom pattern "${cp.label}"`);
|
|
684
773
|
try {
|
|
685
|
-
replaceFree(pass, new RegExp(cp.regex, 'g'), (match) =>
|
|
686
|
-
? null
|
|
687
|
-
: redactMatch(pass, match, pattern, {
|
|
688
|
-
patternName: `custom_${cp.label}`, patternLabel: pattern.label, category: pattern.category,
|
|
689
|
-
confidence: context_1.CUSTOM_PATTERN_CONFIDENCE,
|
|
690
|
-
}));
|
|
774
|
+
replaceFree(pass, new RegExp(cp.regex, 'g'), (match) => (isAllowListed(match, pass.ctx.allowList) ? null : decision));
|
|
691
775
|
}
|
|
692
776
|
catch { }
|
|
693
777
|
}
|
|
@@ -722,16 +806,31 @@ function literalStage(pass) {
|
|
|
722
806
|
const { ctx, vault, sessionId } = pass;
|
|
723
807
|
if (!ctx.priorTokens || (ctx.mode !== 'token' && ctx.mode !== 'pseudonymize'))
|
|
724
808
|
return;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
809
|
+
const decision = {
|
|
810
|
+
pattern: { label: 'LITERAL', category: 'other' },
|
|
811
|
+
store: (match) => {
|
|
812
|
+
const token = freshToken('LITERAL', vault, sessionId, ctx.reservedTokens);
|
|
813
|
+
vault.addEntry(sessionId, { token, original: match, patternLabel: 'LITERAL', category: 'other', createdAt: new Date().toISOString() });
|
|
814
|
+
return token;
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
replaceFree(pass, ctx.priorTokens, () => decision);
|
|
818
|
+
}
|
|
819
|
+
function protectLiterals(text, ctx, vault, sessionId, hits, itemIndex) {
|
|
820
|
+
if (!ctx.priorTokens)
|
|
821
|
+
return text;
|
|
822
|
+
const pass = {
|
|
823
|
+
source: text, segs: [{ text, locked: false, src: 0, end: text.length }], threshold: 0,
|
|
824
|
+
ctx, vault, sessionId, hits, itemIndex, fieldPath: '',
|
|
825
|
+
};
|
|
826
|
+
literalStage(pass);
|
|
827
|
+
recordLocked(ctx, pass.segs);
|
|
828
|
+
return pass.segs.map((seg) => seg.text).join('');
|
|
730
829
|
}
|
|
731
830
|
function redactText(text, ctx, vault, sessionId, hits, itemIndex, fieldPath) {
|
|
732
831
|
var _a;
|
|
733
832
|
const pass = {
|
|
734
|
-
source: text, segs: [{ text, locked: false, src: 0 }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
|
|
833
|
+
source: text, segs: [{ text, locked: false, src: 0, end: text.length }], threshold: (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0,
|
|
735
834
|
ctx, vault, sessionId, hits, itemIndex, fieldPath,
|
|
736
835
|
};
|
|
737
836
|
literalStage(pass);
|
|
@@ -781,44 +880,104 @@ function toUtf16Spans(text, entities) {
|
|
|
781
880
|
.filter((e) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.end > e.start && e.end < offsets.length)
|
|
782
881
|
.map((e) => ({ start: offsets[e.start], end: offsets[e.end], score: e.score, entityType: e.entity_type }));
|
|
783
882
|
}
|
|
784
|
-
function
|
|
785
|
-
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);
|
|
786
885
|
const kept = [];
|
|
787
886
|
for (const span of ranked) {
|
|
788
887
|
if (!kept.some((k) => span.start < k.end && k.start < span.end))
|
|
789
888
|
kept.push(span);
|
|
790
889
|
}
|
|
791
|
-
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;
|
|
792
925
|
}
|
|
793
926
|
function presidioSpans(value, entities, ctx, locked) {
|
|
794
927
|
var _a;
|
|
795
928
|
const threshold = (_a = ctx.confidenceThreshold) !== null && _a !== void 0 ? _a : 0;
|
|
796
|
-
|
|
797
|
-
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);
|
|
798
930
|
}
|
|
799
931
|
async function presidioRedactString(value, ctx, vault, sessionId, hits, itemIndex, path, locked) {
|
|
800
|
-
if (value.trim().length === 0
|
|
932
|
+
if (value.trim().length === 0)
|
|
801
933
|
return value;
|
|
802
934
|
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
803
|
-
const
|
|
804
|
-
let
|
|
805
|
-
for (const span of
|
|
935
|
+
const parts = [];
|
|
936
|
+
let last = 0;
|
|
937
|
+
for (const span of presidioSpans(value, entities, ctx, locked)) {
|
|
806
938
|
const matchedText = value.slice(span.start, span.end);
|
|
807
|
-
if (TOKEN_RE.test(matchedText) || (matchedText.startsWith('[') && matchedText.endsWith(']')))
|
|
808
|
-
continue;
|
|
809
939
|
if (isAllowListed(matchedText, ctx.allowList))
|
|
810
940
|
continue;
|
|
811
941
|
const label = (0, presidio_1.getRedactorLabel)(span.entityType);
|
|
812
942
|
const category = (0, presidio_1.getRedactorCategory)(span.entityType);
|
|
813
943
|
const replacement = applyRedaction(matchedText, { label, category }, ctx, vault, sessionId);
|
|
814
|
-
|
|
944
|
+
parts.push(value.slice(last, span.start), replacement);
|
|
945
|
+
last = span.end;
|
|
815
946
|
hits.push({
|
|
816
947
|
token: replacement, original: '***',
|
|
817
948
|
patternName: `presidio_${span.entityType}`, patternLabel: label,
|
|
818
949
|
category, field: path, itemIndex, confidence: span.score,
|
|
819
950
|
});
|
|
820
951
|
}
|
|
821
|
-
|
|
952
|
+
parts.push(value.slice(last));
|
|
953
|
+
return parts.join('');
|
|
954
|
+
}
|
|
955
|
+
const IDENTIFIER_KEY = /^[\p{L}_$][\p{L}\p{N}_$-]*$/u;
|
|
956
|
+
async function presidioKey(key, original, source, taken, ctx, vault, sessionId, hits, itemIndex, path, policyPath) {
|
|
957
|
+
var _a, _b, _c;
|
|
958
|
+
if (IDENTIFIER_KEY.test(key) || !shouldProcessField(policyPath, ctx.fieldMode, ctx.fieldRules))
|
|
959
|
+
return key;
|
|
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 : [];
|
|
961
|
+
let out = key;
|
|
962
|
+
try {
|
|
963
|
+
out = await presidioRedactString(key, ctx, vault, sessionId, hits, itemIndex, path, locked);
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
return key;
|
|
967
|
+
}
|
|
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);
|
|
970
|
+
if (out === key || !clashes(out))
|
|
971
|
+
return out;
|
|
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
|
+
}
|
|
822
981
|
}
|
|
823
982
|
async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path, policyPath, locked = []) {
|
|
824
983
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -848,7 +1007,15 @@ async function presidioWalk(value, ctx, vault, sessionId, hits, itemIndex, path,
|
|
|
848
1007
|
const child = path ? `${path}.${k}` : k;
|
|
849
1008
|
const childPolicy = policyPath ? `${policyPath}.${original}` : original;
|
|
850
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);
|
|
851
|
-
|
|
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));
|
|
852
1019
|
}
|
|
853
1020
|
return out;
|
|
854
1021
|
}
|