@privacyscrubber/sdk 2.3.9 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -27
- package/benchmark.js +347 -0
- package/browser.js +2 -2
- package/cli.js +1 -1
- package/examples/python-bridge/MANIFEST.in +5 -0
- package/examples/python-bridge/README.md +265 -0
- package/examples/python-bridge/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
- package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
- package/examples/python-bridge/fastapi_example.py +206 -0
- package/examples/python-bridge/langchain_adapter.py +20 -0
- package/examples/python-bridge/langchain_example.py +126 -0
- package/examples/python-bridge/llamaindex_example.py +103 -0
- package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
- package/examples/python-bridge/privacyscrubber/cli.py +138 -0
- package/examples/python-bridge/privacyscrubber/client.py +542 -0
- package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
- package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
- package/examples/python-bridge/privacyscrubber/types.py +52 -0
- package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +311 -0
- package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +18 -0
- package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +1 -0
- package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +3 -0
- package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +18 -0
- package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +1 -0
- package/examples/python-bridge/privacyscrubber.py +36 -0
- package/examples/python-bridge/pyproject.toml +62 -0
- package/examples/python-bridge/setup.py +66 -0
- package/examples/python-bridge/test_python_bridge.py +320 -0
- package/examples/vercel-ai-stream.js +195 -0
- package/fastmcp.cjs +271 -1
- package/fastmcp.d.ts +18 -0
- package/fastmcp.js +4 -0
- package/index.cjs +89 -36
- package/index.js +89 -36
- package/package.json +15 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +400 -0
- package/vercel-ai.d.ts +156 -0
- package/vercel-ai.js +36 -0
- package/vision.cjs +13 -5
- package/vision.js +13 -5
package/fastmcp.cjs
CHANGED
|
@@ -26,6 +26,204 @@ const ZTDS_PATTERNS = {
|
|
|
26
26
|
|
|
27
27
|
const TOKEN_PATTERN = /\[([A-Z_]+)_TOKEN_([a-zA-Z0-9_-]+)\]/g;
|
|
28
28
|
|
|
29
|
+
function escapeRegex(s) {
|
|
30
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
34
|
+
if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
|
|
35
|
+
return (text) => text || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lookup = new Map();
|
|
39
|
+
const patterns = [];
|
|
40
|
+
const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
|
|
41
|
+
|
|
42
|
+
for (const k of sortedKeys) {
|
|
43
|
+
const val = tokenMap[k];
|
|
44
|
+
const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
|
|
45
|
+
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
46
|
+
|
|
47
|
+
if (match) {
|
|
48
|
+
const label = escapeRegex(match[1]);
|
|
49
|
+
const idx = match[2];
|
|
50
|
+
const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
51
|
+
patterns.push(patternStr);
|
|
52
|
+
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
53
|
+
lookup.set(normKey, val);
|
|
54
|
+
lookup.set(k, val);
|
|
55
|
+
lookup.set(k.toUpperCase(), val);
|
|
56
|
+
} else {
|
|
57
|
+
const safe = escapeRegex(k);
|
|
58
|
+
patterns.push(safe);
|
|
59
|
+
lookup.set(k, val);
|
|
60
|
+
lookup.set(k.toUpperCase(), val);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
|
|
65
|
+
|
|
66
|
+
return function replaceTokens(text) {
|
|
67
|
+
if (!text) return '';
|
|
68
|
+
unifiedRegex.lastIndex = 0;
|
|
69
|
+
return text.replace(unifiedRegex, (match, suffix) => {
|
|
70
|
+
const isPossessive = suffix && /('s|’s)$/i.test(suffix);
|
|
71
|
+
const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
|
|
72
|
+
let restored = null;
|
|
73
|
+
|
|
74
|
+
if (lookup.has(coreMatch)) {
|
|
75
|
+
restored = lookup.get(coreMatch);
|
|
76
|
+
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
77
|
+
restored = lookup.get(coreMatch.toUpperCase());
|
|
78
|
+
} else {
|
|
79
|
+
const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
|
|
80
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
81
|
+
if (m) {
|
|
82
|
+
const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
83
|
+
if (lookup.has(normKey)) {
|
|
84
|
+
restored = lookup.get(normKey);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (restored !== null) {
|
|
90
|
+
return restored + (isPossessive ? "'s" : '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (lookup.has(match)) {
|
|
94
|
+
return lookup.get(match);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return match;
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class StreamingDetokenizer {
|
|
103
|
+
constructor(tokenMap = {}, options = {}) {
|
|
104
|
+
this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
|
|
105
|
+
this.options = options || {};
|
|
106
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
107
|
+
this.pendingBuffer = '';
|
|
108
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
109
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
110
|
+
this.decoder = new TextDecoder('utf-8', { fatal: false });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get buffer() {
|
|
114
|
+
return this.pendingBuffer;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
set buffer(val) {
|
|
118
|
+
this.pendingBuffer = typeof val === 'string' ? val : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
updateTokenMap(newMap) {
|
|
122
|
+
if (newMap && typeof newMap === 'object') {
|
|
123
|
+
this.tokenMap = newMap;
|
|
124
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
125
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
126
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
reset() {
|
|
131
|
+
this.pendingBuffer = '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
feed(chunk) {
|
|
135
|
+
if (chunk === undefined || chunk === null) return '';
|
|
136
|
+
let text = '';
|
|
137
|
+
if (typeof chunk === 'string') {
|
|
138
|
+
text = chunk;
|
|
139
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
140
|
+
text = this.decoder.decode(chunk, { stream: true });
|
|
141
|
+
} else {
|
|
142
|
+
text = String(chunk);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!text) return '';
|
|
146
|
+
|
|
147
|
+
const currentTokenCount = Object.keys(this.tokenMap).length;
|
|
148
|
+
if (currentTokenCount !== this._lastTokenCount) {
|
|
149
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
150
|
+
this.hasTokens = currentTokenCount > 0;
|
|
151
|
+
this._lastTokenCount = currentTokenCount;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!this.hasTokens) return text;
|
|
155
|
+
|
|
156
|
+
this.pendingBuffer += text;
|
|
157
|
+
|
|
158
|
+
const tailLen = 160;
|
|
159
|
+
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
160
|
+
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
161
|
+
const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
|
|
162
|
+
|
|
163
|
+
if (m) {
|
|
164
|
+
const candidate = m[0];
|
|
165
|
+
const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
|
|
166
|
+
if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
|
|
167
|
+
const toEmit = this.pendingBuffer;
|
|
168
|
+
this.pendingBuffer = '';
|
|
169
|
+
return this.replacer(toEmit);
|
|
170
|
+
}
|
|
171
|
+
const splitIdx = offset + m.index;
|
|
172
|
+
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
|
173
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
|
|
174
|
+
return safeToEmit ? this.replacer(safeToEmit) : '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const toEmit = this.pendingBuffer;
|
|
178
|
+
this.pendingBuffer = '';
|
|
179
|
+
return this.replacer(toEmit);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
process(chunk) {
|
|
183
|
+
return this.feed(chunk);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
flush() {
|
|
187
|
+
let text = '';
|
|
188
|
+
try {
|
|
189
|
+
text = this.decoder.decode(new Uint8Array(), { stream: false });
|
|
190
|
+
} catch (_) {}
|
|
191
|
+
|
|
192
|
+
if (text) {
|
|
193
|
+
this.pendingBuffer += text;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!this.pendingBuffer) return '';
|
|
197
|
+
const remaining = this.pendingBuffer;
|
|
198
|
+
this.pendingBuffer = '';
|
|
199
|
+
return this.replacer(remaining);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async* wrapStream(asyncIterable) {
|
|
203
|
+
for await (const chunk of asyncIterable) {
|
|
204
|
+
if (chunk === null || chunk === undefined) continue;
|
|
205
|
+
let text = '';
|
|
206
|
+
if (typeof chunk === 'string') {
|
|
207
|
+
text = chunk;
|
|
208
|
+
} else if (typeof chunk?.text === 'string') {
|
|
209
|
+
text = chunk.text;
|
|
210
|
+
} else if (typeof chunk?.content === 'string') {
|
|
211
|
+
text = chunk.content;
|
|
212
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
213
|
+
text = chunk;
|
|
214
|
+
} else {
|
|
215
|
+
text = String(chunk);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const emitted = this.feed(text);
|
|
219
|
+
if (emitted) yield emitted;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const finalChunk = this.flush();
|
|
223
|
+
if (finalChunk) yield finalChunk;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
29
227
|
class ZTDSFastMCPGuard {
|
|
30
228
|
constructor(options = {}) {
|
|
31
229
|
this.enabledEntities = options.enabledEntities || Object.keys(ZTDS_PATTERNS);
|
|
@@ -36,6 +234,7 @@ class ZTDSFastMCPGuard {
|
|
|
36
234
|
this.maxSessionTokens = options.maxSessionTokens || 5000;
|
|
37
235
|
this._sessionMaps = new Map();
|
|
38
236
|
this._entityMaps = new Map();
|
|
237
|
+
this._streamDetokenizers = new Map();
|
|
39
238
|
}
|
|
40
239
|
|
|
41
240
|
_generateToken(entityType, tokenMap) {
|
|
@@ -157,6 +356,65 @@ class ZTDSFastMCPGuard {
|
|
|
157
356
|
return val;
|
|
158
357
|
}
|
|
159
358
|
|
|
359
|
+
getStreamDetokenizer(sessionId) {
|
|
360
|
+
if (!this._streamDetokenizers.has(sessionId)) {
|
|
361
|
+
const mapProxy = this._sessionMaps.get(sessionId) || new Map();
|
|
362
|
+
const objMap = {};
|
|
363
|
+
for (const [k, v] of mapProxy.entries()) {
|
|
364
|
+
objMap[k] = v;
|
|
365
|
+
}
|
|
366
|
+
this._streamDetokenizers.set(sessionId, new StreamingDetokenizer(objMap));
|
|
367
|
+
} else {
|
|
368
|
+
const mapProxy = this._sessionMaps.get(sessionId);
|
|
369
|
+
if (mapProxy) {
|
|
370
|
+
const objMap = {};
|
|
371
|
+
for (const [k, v] of mapProxy.entries()) {
|
|
372
|
+
objMap[k] = v;
|
|
373
|
+
}
|
|
374
|
+
this._streamDetokenizers.get(sessionId).updateTokenMap(objMap);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return this._streamDetokenizers.get(sessionId);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
feedStream(chunk, sessionId) {
|
|
381
|
+
return this.getStreamDetokenizer(sessionId).feed(chunk);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
flushStream(sessionId) {
|
|
385
|
+
const detok = this._streamDetokenizers.get(sessionId);
|
|
386
|
+
return detok ? detok.flush() : "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async *restoreStream(asyncIterable, sessionId) {
|
|
390
|
+
const detok = this.getStreamDetokenizer(sessionId);
|
|
391
|
+
try {
|
|
392
|
+
for await (const chunk of asyncIterable) {
|
|
393
|
+
if (chunk === null || chunk === undefined) continue;
|
|
394
|
+
let text = "";
|
|
395
|
+
if (typeof chunk === "string") {
|
|
396
|
+
text = chunk;
|
|
397
|
+
} else if (typeof chunk?.text === "string") {
|
|
398
|
+
text = chunk.text;
|
|
399
|
+
} else if (typeof chunk?.content === "string") {
|
|
400
|
+
text = chunk.content;
|
|
401
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(chunk))) {
|
|
402
|
+
text = chunk;
|
|
403
|
+
} else {
|
|
404
|
+
text = String(chunk);
|
|
405
|
+
}
|
|
406
|
+
const emitted = detok.feed(text);
|
|
407
|
+
if (emitted) yield emitted;
|
|
408
|
+
}
|
|
409
|
+
const finalChunk = detok.flush();
|
|
410
|
+
if (finalChunk) yield finalChunk;
|
|
411
|
+
} finally {
|
|
412
|
+
if (this.autoZeroize) {
|
|
413
|
+
this.zeroizeSession(sessionId);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
160
418
|
wrapTool(toolName, handler) {
|
|
161
419
|
return async (args, context = {}) => {
|
|
162
420
|
const sessionId =
|
|
@@ -169,6 +427,7 @@ class ZTDSFastMCPGuard {
|
|
|
169
427
|
unmask: (val) => this.restoreObject(val, sessionId),
|
|
170
428
|
};
|
|
171
429
|
|
|
430
|
+
let isStreaming = false;
|
|
172
431
|
try {
|
|
173
432
|
let processedArgs = args;
|
|
174
433
|
if (this.sanitizeInputs && args !== undefined && args !== null) {
|
|
@@ -177,6 +436,11 @@ class ZTDSFastMCPGuard {
|
|
|
177
436
|
|
|
178
437
|
const rawResult = await handler(processedArgs, enrichedContext);
|
|
179
438
|
|
|
439
|
+
if (rawResult && typeof rawResult[Symbol.asyncIterator] === "function") {
|
|
440
|
+
isStreaming = true;
|
|
441
|
+
return this.restoreStream(rawResult, sessionId);
|
|
442
|
+
}
|
|
443
|
+
|
|
180
444
|
let finalResult = rawResult;
|
|
181
445
|
if (this.sanitizeOutputs && rawResult !== undefined && rawResult !== null) {
|
|
182
446
|
if (typeof rawResult === "string") {
|
|
@@ -234,7 +498,7 @@ class ZTDSFastMCPGuard {
|
|
|
234
498
|
_ztds: ztdsMeta,
|
|
235
499
|
};
|
|
236
500
|
} finally {
|
|
237
|
-
if (this.autoZeroize) {
|
|
501
|
+
if (!isStreaming && this.autoZeroize) {
|
|
238
502
|
this.zeroizeSession(sessionId);
|
|
239
503
|
}
|
|
240
504
|
}
|
|
@@ -250,6 +514,10 @@ class ZTDSFastMCPGuard {
|
|
|
250
514
|
this._entityMaps.get(sessionId).clear();
|
|
251
515
|
this._entityMaps.delete(sessionId);
|
|
252
516
|
}
|
|
517
|
+
if (this._streamDetokenizers.has(sessionId)) {
|
|
518
|
+
this._streamDetokenizers.get(sessionId).reset();
|
|
519
|
+
this._streamDetokenizers.delete(sessionId);
|
|
520
|
+
}
|
|
253
521
|
}
|
|
254
522
|
}
|
|
255
523
|
|
|
@@ -285,6 +553,8 @@ module.exports = {
|
|
|
285
553
|
ZTDSFastMCPGuard,
|
|
286
554
|
ZTDS_PATTERNS,
|
|
287
555
|
TOKEN_PATTERN,
|
|
556
|
+
StreamingDetokenizer,
|
|
557
|
+
createStreamTokenRestorer: (tokenMap, options) => new StreamingDetokenizer(tokenMap, options),
|
|
288
558
|
createZTDSGuard,
|
|
289
559
|
ztdsGuardTool,
|
|
290
560
|
wrapExecute,
|
package/fastmcp.d.ts
CHANGED
|
@@ -26,6 +26,19 @@ export interface ZTDSContext {
|
|
|
26
26
|
export declare const ZTDS_PATTERNS: Record<string, RegExp>;
|
|
27
27
|
export declare const TOKEN_PATTERN: RegExp;
|
|
28
28
|
|
|
29
|
+
export declare class StreamingDetokenizer {
|
|
30
|
+
constructor(tokenMap?: Record<string, string>, options?: object);
|
|
31
|
+
buffer: string;
|
|
32
|
+
feed(chunk: string | Uint8Array | Buffer): string;
|
|
33
|
+
process(chunk: string | Uint8Array | Buffer): string;
|
|
34
|
+
flush(): string;
|
|
35
|
+
reset(): void;
|
|
36
|
+
updateTokenMap(newMap: Record<string, string>): void;
|
|
37
|
+
wrapStream<T = any>(asyncIterable: AsyncIterable<T>): AsyncGenerator<T>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export declare function createStreamTokenRestorer(tokenMap: Record<string, string>, options?: object): StreamingDetokenizer;
|
|
41
|
+
|
|
29
42
|
export declare class ZTDSFastMCPGuard {
|
|
30
43
|
constructor(options?: ZTDSGuardOptions);
|
|
31
44
|
enabledEntities: string[];
|
|
@@ -39,6 +52,11 @@ export declare class ZTDSFastMCPGuard {
|
|
|
39
52
|
sanitizeObject(val: unknown, sessionId: string): unknown;
|
|
40
53
|
restoreObject(val: unknown, sessionId: string): unknown;
|
|
41
54
|
|
|
55
|
+
getStreamDetokenizer(sessionId: string): StreamingDetokenizer;
|
|
56
|
+
feedStream(chunk: string, sessionId: string): string;
|
|
57
|
+
flushStream(sessionId: string): string;
|
|
58
|
+
restoreStream<T = any>(asyncIterable: AsyncIterable<T>, sessionId: string): AsyncGenerator<T>;
|
|
59
|
+
|
|
42
60
|
wrapTool<TArgs = unknown, TResult = unknown>(
|
|
43
61
|
toolName: string,
|
|
44
62
|
handler: (args: TArgs, context: ZTDSContext) => Promise<TResult>
|
package/fastmcp.js
CHANGED
|
@@ -11,6 +11,8 @@ const fastmcpGuard = require('./fastmcp.cjs');
|
|
|
11
11
|
export const ZTDSFastMCPGuard = fastmcpGuard.ZTDSFastMCPGuard;
|
|
12
12
|
export const ZTDS_PATTERNS = fastmcpGuard.ZTDS_PATTERNS;
|
|
13
13
|
export const TOKEN_PATTERN = fastmcpGuard.TOKEN_PATTERN;
|
|
14
|
+
export const StreamingDetokenizer = fastmcpGuard.StreamingDetokenizer;
|
|
15
|
+
export const createStreamTokenRestorer = fastmcpGuard.createStreamTokenRestorer;
|
|
14
16
|
export const createZTDSGuard = fastmcpGuard.createZTDSGuard;
|
|
15
17
|
export const ztdsGuardTool = fastmcpGuard.ztdsGuardTool;
|
|
16
18
|
export const wrapExecute = fastmcpGuard.wrapExecute;
|
|
@@ -20,6 +22,8 @@ export default {
|
|
|
20
22
|
ZTDSFastMCPGuard,
|
|
21
23
|
ZTDS_PATTERNS,
|
|
22
24
|
TOKEN_PATTERN,
|
|
25
|
+
StreamingDetokenizer,
|
|
26
|
+
createStreamTokenRestorer,
|
|
23
27
|
createZTDSGuard,
|
|
24
28
|
ztdsGuardTool,
|
|
25
29
|
wrapExecute,
|
package/index.cjs
CHANGED
|
@@ -36,16 +36,33 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
|
|
|
36
36
|
* Built-in patterns for DevOps & Cloud Secrets detection.
|
|
37
37
|
*/
|
|
38
38
|
const DEVOPS_SECRETS_DETECTOR = [
|
|
39
|
-
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{
|
|
39
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g, label: 'AWS_KEY' },
|
|
40
|
+
{ name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: 'API_TOKEN' },
|
|
41
|
+
{ name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g, label: 'API_TOKEN' },
|
|
42
|
+
{ name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g, label: 'API_TOKEN' },
|
|
43
|
+
{ name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g, label: 'API_TOKEN' },
|
|
44
|
+
{ name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g, label: 'API_TOKEN' },
|
|
45
|
+
{ name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g, label: 'API_TOKEN' },
|
|
46
|
+
{ name: 'Slack Enterprise / Refresh Token', regex: /\bxoxe\.xox[baprs]-\d-[a-zA-Z0-9-]{10,}\b|\bxox[eapbrs]-[a-zA-Z0-9-]{10,}\b/g, label: 'API_TOKEN' },
|
|
47
|
+
{ name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g, label: 'API_TOKEN' },
|
|
48
|
+
{ name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g, label: 'SECRET' },
|
|
49
|
+
{ name: 'Modern Cloud & AI Platform Key', regex: /\b(?:sbp_[a-fA-F0-9]{40}|dop_v1_[a-fA-F0-9]{64}|doo_v1_[a-fA-F0-9]{64}|dp\.(?:st|pt|ct|sa)\.[a-zA-Z0-9_-]{20,}|lin_api_[a-zA-Z0-9]{32,64}|ntn_[a-zA-Z0-9]{32,64}|secret_[a-zA-Z0-9]{32,64}|gsk_[a-zA-Z0-9]{32,64}|r8_[a-zA-Z0-9]{24,64}|tvly-[a-zA-Z0-9_-]{20,64}|fc-[a-fA-F0-9]{32}|dapi[a-fA-F0-9]{32}(?:-\d)?|re_[a-zA-Z0-9_]{20,64}|napi_[a-zA-Z0-9]{32,64}|[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9]{50,80}|pypi-[A-Za-z0-9_-]{16,128}|whsec_[A-Za-z0-9+/=_-]{24,128}|shpat_[a-fA-F0-9]{32}|shpss_[a-fA-F0-9]{32}|shpca_[a-fA-F0-9]{32}|shppa_[a-fA-F0-9]{32}|hvs\.[A-Za-z0-9_-]{24,128}|hvb\.[A-Za-z0-9_-]{24,128}|glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}|glc_[A-Za-z0-9+/=_-]{20,128}|pscale_tkn_[A-Za-z0-9_.-]{20,64}|pscale_pw_[A-Za-z0-9_.-]{20,64}|pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}|pat-(?:na1|eu1)-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|sq0atp-[A-Za-z0-9_-]{22,64}|sq0csp-[A-Za-z0-9_-]{32,64}|NRAK-[A-Z0-9]{27}|v1\.0-[a-fA-F0-9]{24}-[a-fA-F0-9]{16,160}|ATATT3xFfGF0[A-Za-z0-9+/=_-]{24,128}|tskey-(?:auth|api|client)-[A-Za-z0-9_-]{20,96}|sntry[su]_[A-Za-z0-9+/=_-]{32,160}|(?:pk|sk)\.eyJ1[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{16,}|key-[a-f0-9]{32}|[a-f0-9]{32}-us\d{1,2})\b/g, label: 'API_TOKEN' },
|
|
50
|
+
{ name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g, label: 'API_TOKEN' },
|
|
51
|
+
{ name: 'Bcrypt / Argon2 Password Hash', regex: /(?<![A-Za-z0-9])(?:\$2[aby]\$\d{2}\$[./A-Za-z0-9=]{20,60}|\$argon2(?:id|i|d)\$[^\s"']{15,120})/g, label: 'SECRET' },
|
|
40
52
|
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
|
|
41
|
-
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g, label: 'API_TOKEN' },
|
|
42
|
-
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{
|
|
53
|
+
{ name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g, label: 'API_TOKEN' },
|
|
54
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g, label: 'STRIPE_KEY' },
|
|
43
55
|
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
|
|
56
|
+
{ name: 'Slack / Discord Incoming Webhook URL', regex: /\bhttps:\/\/(?:hooks\.slack\.com\/services\/T[A-Z0-9_]+\/B[A-Z0-9_]+\/[A-Za-z0-9_-]+|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d{17,20}\/[A-Za-z0-9_-]{60,70})\b/gi, label: 'SECRET' },
|
|
57
|
+
{ name: 'Azure Connection String / SAS Token', regex: /(?:DefaultEndpointsProtocol=https?;AccountName=[^;\s"']+;AccountKey=[A-Za-z0-9+/=]{20,}(?:;EndpointSuffix=[^;\s"']+)?|Endpoint=sb:\/\/[^;\s"']+;SharedAccessKeyName=[^;\s"']+;SharedAccessKey=[A-Za-z0-9+/=]{16,}|\bSharedAccessSignature=sr=[^\s"']+&sig=[A-Za-z0-9%/+=]{16,}|\bAccountKey=[A-Za-z0-9+/=]{40,88}==)/gi, label: 'SECRET' },
|
|
44
58
|
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
|
|
45
|
-
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
59
|
+
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|hxxps?|s?ftp|fxp|wss?):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?:\[\.\][a-zA-Z0-9_.-]+|\(\.\)[a-zA-Z0-9_.-]+)*(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
|
|
46
60
|
{ name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi, label: 'SECRET' },
|
|
61
|
+
{ name: 'CLI Credential Flag / Curl User Auth', regex: /(?:\bcurl\b[^\r\n]*?\s(?:-u|--user)\s+["']?[A-Za-z0-9_.+-]+:[^\s"']+|\b(?:--password|--api-key|--apikey|--access-token|--auth-token|--client-secret|--secret-key|--private-key)(?:=|\s+)["']?[^\s"']{6,}["']?)/gi, label: 'SECRET' },
|
|
62
|
+
{ name: 'HTTP API Key Header', regex: /\b(?:X-API-Key|X-Amz-Security-Token|X-Functions-Key|X-Shopify-Access-Token|X-Auth-Token|Api-Key)\s*:\s*["']?[A-Za-z0-9_.\-~+/=]{12,}["']?/gi, label: 'SECRET' },
|
|
63
|
+
{ name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi, label: 'SECRET' },
|
|
47
64
|
{ name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
|
|
48
|
-
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}
|
|
65
|
+
{ name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}/gi, label: 'SECRET' },
|
|
49
66
|
{ name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' },
|
|
50
67
|
{ name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
|
|
51
68
|
];
|
|
@@ -141,8 +158,8 @@ function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
141
158
|
frameworksSet.add('CCPA/CPRA');
|
|
142
159
|
}
|
|
143
160
|
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
|
|
144
|
-
frameworksSet.add('SOC 2
|
|
145
|
-
frameworksSet.add('ISO 27001
|
|
161
|
+
frameworksSet.add('SOC 2 controls');
|
|
162
|
+
frameworksSet.add('ISO 27001 A.8.11 controls');
|
|
146
163
|
}
|
|
147
164
|
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
|
|
148
165
|
frameworksSet.add('PCI DSS v4.0');
|
|
@@ -1466,13 +1483,13 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
1466
1483
|
|
|
1467
1484
|
for (const k of sortedKeys) {
|
|
1468
1485
|
const val = tokenMap[k];
|
|
1469
|
-
const inner = k.replace(/^\[|<|\{\{
|
|
1486
|
+
const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
|
|
1470
1487
|
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
1471
1488
|
|
|
1472
1489
|
if (match) {
|
|
1473
|
-
const label = escapeRegex(match[1]);
|
|
1490
|
+
const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
|
|
1474
1491
|
const idx = match[2];
|
|
1475
|
-
const patternStr = '(?:\\[\\s*' + label + '[-_
|
|
1492
|
+
const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
1476
1493
|
patterns.push(patternStr);
|
|
1477
1494
|
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
1478
1495
|
lookup.set(normKey, val);
|
|
@@ -1501,10 +1518,14 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
|
1501
1518
|
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
1502
1519
|
restored = lookup.get(coreMatch.toUpperCase());
|
|
1503
1520
|
} else {
|
|
1504
|
-
const inner = coreMatch
|
|
1505
|
-
|
|
1521
|
+
const inner = coreMatch
|
|
1522
|
+
.replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
|
|
1523
|
+
.replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
|
|
1524
|
+
.trim();
|
|
1525
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
|
|
1506
1526
|
if (m) {
|
|
1507
|
-
const
|
|
1527
|
+
const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
|
|
1528
|
+
const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
1508
1529
|
if (lookup.has(normKey)) {
|
|
1509
1530
|
restored = lookup.get(normKey);
|
|
1510
1531
|
}
|
|
@@ -1610,18 +1631,17 @@ class StreamingDetokenizer {
|
|
|
1610
1631
|
this.pendingBuffer += text;
|
|
1611
1632
|
|
|
1612
1633
|
// Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
|
|
1613
|
-
// Matches incomplete tokens starting with [, <, {
|
|
1634
|
+
// Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity [ / [
|
|
1614
1635
|
const tailLen = 160;
|
|
1615
1636
|
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
1616
1637
|
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
1617
|
-
const m = tail.match(/(?:\[
|
|
1638
|
+
const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)$/);
|
|
1618
1639
|
|
|
1619
1640
|
if (m) {
|
|
1620
1641
|
const candidate = m[0];
|
|
1621
|
-
//
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
|
|
1642
|
+
// Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
|
|
1643
|
+
const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
|
|
1644
|
+
if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
|
|
1625
1645
|
const toEmit = this.pendingBuffer;
|
|
1626
1646
|
this.pendingBuffer = '';
|
|
1627
1647
|
return this.replacer(toEmit);
|
|
@@ -1843,13 +1863,8 @@ function withPrivacyScrubber(handler, options = {}) {
|
|
|
1843
1863
|
tokenMap = res.tokenMap;
|
|
1844
1864
|
telemetry = res.telemetry;
|
|
1845
1865
|
} else if (rawBody && typeof rawBody === 'object') {
|
|
1846
|
-
const
|
|
1847
|
-
|
|
1848
|
-
try {
|
|
1849
|
-
scrubbedBody = JSON.parse(res.scrubbedText);
|
|
1850
|
-
} catch (_) {
|
|
1851
|
-
scrubbedBody = rawBody;
|
|
1852
|
-
}
|
|
1866
|
+
const res = sanitizeObject(rawBody, options);
|
|
1867
|
+
scrubbedBody = res.sanitized;
|
|
1853
1868
|
tokenMap = res.tokenMap;
|
|
1854
1869
|
telemetry = res.telemetry;
|
|
1855
1870
|
}
|
|
@@ -1860,7 +1875,8 @@ function withPrivacyScrubber(handler, options = {}) {
|
|
|
1860
1875
|
scrubbedBody,
|
|
1861
1876
|
tokenMap,
|
|
1862
1877
|
telemetry,
|
|
1863
|
-
restore: (text) => restore(text, tokenMap),
|
|
1878
|
+
restore: (text) => (text && typeof text === 'object') ? restoreObject(text, tokenMap) : restore(text, tokenMap),
|
|
1879
|
+
restoreObject: (obj) => restoreObject(obj, tokenMap),
|
|
1864
1880
|
createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
|
|
1865
1881
|
};
|
|
1866
1882
|
|
|
@@ -1878,15 +1894,13 @@ function withPrivacyScrubber(handler, options = {}) {
|
|
|
1878
1894
|
function createExpressMiddleware(options = {}) {
|
|
1879
1895
|
return function privacyScrubberExpress(req, res, next) {
|
|
1880
1896
|
if (req && req.body && typeof req.body === 'object') {
|
|
1881
|
-
const
|
|
1882
|
-
const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
|
|
1897
|
+
const { sanitized, tokenMap, telemetry } = sanitizeObject(req.body, options);
|
|
1883
1898
|
req.rawBody = req.body;
|
|
1884
|
-
|
|
1885
|
-
req.body = JSON.parse(scrubbedText);
|
|
1886
|
-
} catch (_) {}
|
|
1899
|
+
req.body = sanitized;
|
|
1887
1900
|
req.psTokenMap = tokenMap;
|
|
1888
1901
|
req.psTelemetry = telemetry;
|
|
1889
|
-
req.restore = (aiResponse) => restore(aiResponse, tokenMap);
|
|
1902
|
+
req.restore = (aiResponse) => (aiResponse && typeof aiResponse === 'object') ? restoreObject(aiResponse, tokenMap) : restore(aiResponse, tokenMap);
|
|
1903
|
+
req.restoreObject = (obj) => restoreObject(obj, tokenMap);
|
|
1890
1904
|
req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
|
|
1891
1905
|
}
|
|
1892
1906
|
if (typeof next === 'function') next();
|
|
@@ -1903,7 +1917,7 @@ function formatCefEvent(eventData = {}) {
|
|
|
1903
1917
|
const {
|
|
1904
1918
|
vendor = 'PrivacyScrubber',
|
|
1905
1919
|
product = 'ZTDS',
|
|
1906
|
-
version = '2.
|
|
1920
|
+
version = '2.4.0',
|
|
1907
1921
|
eventClassId = 'PII_REDACTION',
|
|
1908
1922
|
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
1909
1923
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -1972,7 +1986,7 @@ function formatSyslogEvent(eventData = {}) {
|
|
|
1972
1986
|
function formatJsonlEvent(eventData = {}) {
|
|
1973
1987
|
const {
|
|
1974
1988
|
timestamp = new Date().toISOString(),
|
|
1975
|
-
version = '2.
|
|
1989
|
+
version = '2.4.0',
|
|
1976
1990
|
protocol = 'ZTDS',
|
|
1977
1991
|
event = 'sanitization',
|
|
1978
1992
|
riskLevel = 'LOW EXPOSURE',
|
|
@@ -2508,7 +2522,46 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
|
|
|
2508
2522
|
throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
|
|
2509
2523
|
}
|
|
2510
2524
|
|
|
2511
|
-
const
|
|
2525
|
+
const rawBuf = fs.readFileSync(resolved);
|
|
2526
|
+
const checkLen = Math.min(rawBuf.length, 8000);
|
|
2527
|
+
for (let i = 0; i < checkLen; i++) {
|
|
2528
|
+
if (rawBuf[i] === 0) {
|
|
2529
|
+
throw new Error(`[guard_read_file] Binary file detected (${path.basename(resolved)}). Only text, code, and MIME files are supported.`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
let raw = rawBuf.toString('utf8');
|
|
2533
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
2534
|
+
if (ext === '.eml') {
|
|
2535
|
+
const decodeQpUtf8 = (str) =>
|
|
2536
|
+
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2537
|
+
try {
|
|
2538
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2539
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2540
|
+
bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2541
|
+
}
|
|
2542
|
+
return bytes.toString('utf8');
|
|
2543
|
+
} catch (_) { return seq; }
|
|
2544
|
+
});
|
|
2545
|
+
raw = raw.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, _cs, enc, payload) => {
|
|
2546
|
+
try {
|
|
2547
|
+
return enc.toUpperCase() === 'B'
|
|
2548
|
+
? Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8')
|
|
2549
|
+
: decodeQpUtf8(payload.replace(/_/g, ' '));
|
|
2550
|
+
} catch (_) { return full; }
|
|
2551
|
+
});
|
|
2552
|
+
raw = decodeQpUtf8(raw);
|
|
2553
|
+
} else if (ext === '.rtf') {
|
|
2554
|
+
raw = raw
|
|
2555
|
+
.replace(/\\par\b|\\line\b/gi, '\n')
|
|
2556
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2557
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2558
|
+
const n = parseInt(code, 10);
|
|
2559
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2560
|
+
})
|
|
2561
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
|
|
2562
|
+
.replace(/[{}]/g, ' ')
|
|
2563
|
+
.replace(/[ \t]+/g, ' ');
|
|
2564
|
+
}
|
|
2512
2565
|
const lines = raw.split(/\r?\n/);
|
|
2513
2566
|
const capped = lines.slice(0, maxLines).join('\n');
|
|
2514
2567
|
const wasTruncated = lines.length > maxLines;
|