@privacyscrubber/mcp-server 2.3.8 → 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/.well-known/mcp/server-card.json +78 -0
- package/LICENSE +23 -0
- package/README.md +50 -11
- package/index.js +987 -200
- package/manifest.json +84 -8
- package/package.json +3 -3
- package/pii-masking-run.js +43 -16
- package/ps-license-manager.cjs +1 -1
- package/ps-license-manager.js +11 -257
- package/ps-pii-engine.cjs +8 -3182
- package/ps-pii-engine.js +8 -3182
- package/scrubber-core.cjs +8 -1795
- package/server.json +27 -4
- package/bundle.mcpb +0 -0
package/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { fileURLToPath } from 'url';
|
|
|
26
26
|
import crypto from 'crypto';
|
|
27
27
|
import os from 'os';
|
|
28
28
|
import { execSync } from 'child_process';
|
|
29
|
+
import { Transform } from 'stream';
|
|
29
30
|
|
|
30
31
|
const require = createRequire(import.meta.url);
|
|
31
32
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -63,6 +64,54 @@ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
|
|
|
63
64
|
// Initialize core engine
|
|
64
65
|
PrivacyScrubberCore.init();
|
|
65
66
|
|
|
67
|
+
let piiEngine;
|
|
68
|
+
try {
|
|
69
|
+
piiEngine = require('./ps-pii-engine.cjs');
|
|
70
|
+
} catch (e) {
|
|
71
|
+
try {
|
|
72
|
+
piiEngine = require('./ps-pii-engine.js');
|
|
73
|
+
} catch (err) {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const CANONICAL_INDUSTRY_PROFILES = [
|
|
77
|
+
'General', 'Dev', 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA',
|
|
78
|
+
'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting',
|
|
79
|
+
'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support',
|
|
80
|
+
'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal', 'Engineering'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export function resolveRequestedProfile(args, defaultProfile = 'General') {
|
|
84
|
+
if (!args || typeof args !== 'object') return defaultProfile;
|
|
85
|
+
const sec = typeof args.sector === 'string' ? args.sector.trim() : '';
|
|
86
|
+
const prof = typeof args.profile === 'string' ? args.profile.trim() : '';
|
|
87
|
+
|
|
88
|
+
let candidate = defaultProfile;
|
|
89
|
+
if (sec && (!prof || prof.toLowerCase() === 'general')) {
|
|
90
|
+
candidate = sec;
|
|
91
|
+
} else if (prof) {
|
|
92
|
+
candidate = prof;
|
|
93
|
+
} else if (sec) {
|
|
94
|
+
candidate = sec;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Canonical normalization via PROFILE_ALIAS_MAP if available
|
|
98
|
+
if (candidate && candidate.toLowerCase() !== 'general' && piiEngine && piiEngine.PROFILE_ALIAS_MAP) {
|
|
99
|
+
const mapped = piiEngine.PROFILE_ALIAS_MAP[candidate.toLowerCase()];
|
|
100
|
+
if (mapped) {
|
|
101
|
+
if (candidate.toLowerCase() === mapped.toLowerCase()) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
return mapped.charAt(0).toUpperCase() + mapped.slice(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return candidate;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PROFILE_PARAM_DESCRIPTION = "The detection profile to use. Available: 'General' (Free, 15,000 chars), or 30 specialized industry profiles (5,000 chars Free trial quota; unlimited on PRO/TEAMS/SDK): 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Biotech', 'Telecom', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'Underwriting', 'Automotive', 'Energy', 'Hospitality', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal', 'Engineering'. Defaults to 'General'.";
|
|
112
|
+
|
|
113
|
+
const SECTOR_PARAM_DESCRIPTION = "Optional alias for 'profile'. The industry sector or compliance vertical to apply (e.g. 'medical', 'finance', 'legal', 'dev', 'hr', 'accounting', 'security', 'compliance', 'underwriting', 'telecom').";
|
|
114
|
+
|
|
66
115
|
// Volatile in-memory token map per session (multi-task agent isolation)
|
|
67
116
|
const agentSessions = new Map();
|
|
68
117
|
const MAX_CONCURRENT_SESSIONS = 200;
|
|
@@ -75,9 +124,13 @@ function getSession(sessionId = 'default') {
|
|
|
75
124
|
if (!session) {
|
|
76
125
|
if (agentSessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
77
126
|
const oldestKey = agentSessions.keys().next().value;
|
|
127
|
+
const oldestSession = agentSessions.get(oldestKey);
|
|
128
|
+
if (oldestSession && oldestSession.streamDetokenizer) {
|
|
129
|
+
oldestSession.streamDetokenizer.reset();
|
|
130
|
+
}
|
|
78
131
|
agentSessions.delete(oldestKey);
|
|
79
132
|
}
|
|
80
|
-
session = { map: {}, ignoreList: new Set(), createdAt: now, lastActive: now };
|
|
133
|
+
session = { map: {}, ignoreList: new Set(), streamDetokenizer: null, createdAt: now, lastActive: now };
|
|
81
134
|
agentSessions.set(sid, session);
|
|
82
135
|
}
|
|
83
136
|
session.lastActive = now;
|
|
@@ -96,6 +149,282 @@ function getSessionIgnoreList(sessionId = 'default') {
|
|
|
96
149
|
const sessionMap = getSessionMap('default');
|
|
97
150
|
const sessionIgnoreList = getSessionIgnoreList('default');
|
|
98
151
|
|
|
152
|
+
function escapeRegex(s) {
|
|
153
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds a fast streaming token replacement function for MCP.
|
|
158
|
+
* Preserves all whitespace, indentation, newlines, and code blocks.
|
|
159
|
+
* Handles descending key ordering, case-insensitivity, alternative delimiters (<...>, {{...}}, __...__),
|
|
160
|
+
* and possessive suffixes ('s).
|
|
161
|
+
*/
|
|
162
|
+
export function buildStreamReplacer(tokenMap = {}, options = {}) {
|
|
163
|
+
if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
|
|
164
|
+
return (text) => text || '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const onTokenRestored = typeof options?.onTokenRestored === 'function' ? options.onTokenRestored : null;
|
|
168
|
+
const lookup = new Map();
|
|
169
|
+
const patterns = [];
|
|
170
|
+
const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
|
|
171
|
+
|
|
172
|
+
for (const k of sortedKeys) {
|
|
173
|
+
const val = tokenMap[k];
|
|
174
|
+
const inner = k.replace(/^(?:\[|<|\{\{?|__|【|[|〔|「|『)|(?:\]|>|\}\}?|__|】|]|〕|」|』)$/g, '');
|
|
175
|
+
const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
176
|
+
|
|
177
|
+
if (match) {
|
|
178
|
+
const label = escapeRegex(match[1]);
|
|
179
|
+
const idx = match[2];
|
|
180
|
+
const corePat = '[*`_~\\s]*' + label + '[*`_~]*[-_\\s]*0*' + idx + '[*`_~.,;:!?\\s]*';
|
|
181
|
+
const patternStr = '(?:\\[' + corePat + '\\]|<' + corePat + '>|\\{\\{' + corePat + '\\}\\}|\\{' + corePat + '\\}|__' + corePat + '__|[【[〔「『]' + corePat + '[】]〕」』]|%5B' + corePat + '%5D|&#(?:91|091|x5[bB]);' + corePat + '&#(?:93|093|x5[dD]);|&(?:lbrack|lsqb);' + corePat + '&(?:rbrack|rsqb);|\\\\u005[bB]' + corePat + '\\\\u005[dD]|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
|
|
182
|
+
patterns.push(patternStr);
|
|
183
|
+
const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
|
|
184
|
+
lookup.set(normKey, val);
|
|
185
|
+
lookup.set(k, val);
|
|
186
|
+
lookup.set(k.toUpperCase(), val);
|
|
187
|
+
} else {
|
|
188
|
+
const safe = escapeRegex(k);
|
|
189
|
+
patterns.push(safe);
|
|
190
|
+
lookup.set(k, val);
|
|
191
|
+
lookup.set(k.toUpperCase(), val);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
|
|
196
|
+
|
|
197
|
+
return function replaceTokens(text) {
|
|
198
|
+
if (!text) return '';
|
|
199
|
+
unifiedRegex.lastIndex = 0;
|
|
200
|
+
return text.replace(unifiedRegex, (match, suffix) => {
|
|
201
|
+
const isPossessive = suffix && /('s|’s)$/i.test(suffix);
|
|
202
|
+
const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
|
|
203
|
+
let restored = null;
|
|
204
|
+
|
|
205
|
+
if (lookup.has(coreMatch)) {
|
|
206
|
+
restored = lookup.get(coreMatch);
|
|
207
|
+
} else if (lookup.has(coreMatch.toUpperCase())) {
|
|
208
|
+
restored = lookup.get(coreMatch.toUpperCase());
|
|
209
|
+
} else {
|
|
210
|
+
const inner = coreMatch
|
|
211
|
+
.replace(/^(?:%5B|&#(?:91|091|x5[bB]);|&(?:lbrack|lsqb);|\\u005[bB]|\[|<|\{\{?|__|【|[|〔|「|『)/i, '')
|
|
212
|
+
.replace(/(?:%5D|&#(?:93|093|x5[dD]);|&(?:rbrack|rsqb);|\\u005[dD]|\]|>|\}\}?|__|】|]|〕|」|』)$/i, '')
|
|
213
|
+
.replace(/[*`_~.,;:!?\s]+/g, ' ')
|
|
214
|
+
.trim();
|
|
215
|
+
const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
|
|
216
|
+
if (m) {
|
|
217
|
+
const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
|
|
218
|
+
if (lookup.has(normKey)) {
|
|
219
|
+
restored = lookup.get(normKey);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (restored !== null) {
|
|
225
|
+
if (onTokenRestored) {
|
|
226
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
227
|
+
}
|
|
228
|
+
return restored + (isPossessive ? "'s" : '');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (lookup.has(match)) {
|
|
232
|
+
restored = lookup.get(match);
|
|
233
|
+
if (onTokenRestored) {
|
|
234
|
+
try { onTokenRestored({ token: match, originalValue: restored }); } catch (_) {}
|
|
235
|
+
}
|
|
236
|
+
return restored;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return match;
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Streaming Token Detokenizer with sliding window buffer for LLM chunk boundary splits.
|
|
246
|
+
* Buffers partial token candidates at chunk boundaries across stdio and Server-Sent Events,
|
|
247
|
+
* preventing character loss and token corruption.
|
|
248
|
+
*/
|
|
249
|
+
export class StreamingDetokenizer {
|
|
250
|
+
constructor(tokenMap = {}, options = {}) {
|
|
251
|
+
this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
|
|
252
|
+
this.options = options || {};
|
|
253
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
254
|
+
this.pendingBuffer = '';
|
|
255
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
256
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
257
|
+
this.decoder = new TextDecoder('utf-8', { fatal: false });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get buffer() {
|
|
261
|
+
return this.pendingBuffer;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
set buffer(val) {
|
|
265
|
+
this.pendingBuffer = typeof val === 'string' ? val : '';
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
updateTokenMap(newMap) {
|
|
269
|
+
if (newMap && typeof newMap === 'object') {
|
|
270
|
+
this.tokenMap = newMap;
|
|
271
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
272
|
+
this.hasTokens = Object.keys(this.tokenMap).length > 0;
|
|
273
|
+
this._lastTokenCount = Object.keys(this.tokenMap).length;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
reset() {
|
|
278
|
+
this.pendingBuffer = '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
feed(chunk) {
|
|
282
|
+
if (chunk === undefined || chunk === null) return '';
|
|
283
|
+
let text = '';
|
|
284
|
+
if (typeof chunk === 'string') {
|
|
285
|
+
text = chunk;
|
|
286
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
287
|
+
text = this.decoder.decode(chunk, { stream: true });
|
|
288
|
+
} else {
|
|
289
|
+
text = String(chunk);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!text) return '';
|
|
293
|
+
|
|
294
|
+
const currentTokenCount = Object.keys(this.tokenMap).length;
|
|
295
|
+
if (currentTokenCount !== this._lastTokenCount) {
|
|
296
|
+
this.replacer = buildStreamReplacer(this.tokenMap, this.options);
|
|
297
|
+
this.hasTokens = currentTokenCount > 0;
|
|
298
|
+
this._lastTokenCount = currentTokenCount;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!this.hasTokens) return text;
|
|
302
|
+
|
|
303
|
+
this.pendingBuffer += text;
|
|
304
|
+
|
|
305
|
+
const tailLen = 160;
|
|
306
|
+
const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
|
|
307
|
+
const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
|
|
308
|
+
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)?)?)?)?)?)?)$/);
|
|
309
|
+
|
|
310
|
+
if (m) {
|
|
311
|
+
const candidate = m[0];
|
|
312
|
+
const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
|
|
313
|
+
if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
|
|
314
|
+
const toEmit = this.pendingBuffer;
|
|
315
|
+
this.pendingBuffer = '';
|
|
316
|
+
return this.replacer(toEmit);
|
|
317
|
+
}
|
|
318
|
+
const splitIdx = offset + m.index;
|
|
319
|
+
const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
|
|
320
|
+
this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
|
|
321
|
+
return safeToEmit ? this.replacer(safeToEmit) : '';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const toEmit = this.pendingBuffer;
|
|
325
|
+
this.pendingBuffer = '';
|
|
326
|
+
return this.replacer(toEmit);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
process(chunk) {
|
|
330
|
+
return this.feed(chunk);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
flush() {
|
|
334
|
+
let text = '';
|
|
335
|
+
try {
|
|
336
|
+
text = this.decoder.decode(new Uint8Array(), { stream: false });
|
|
337
|
+
} catch (_) {}
|
|
338
|
+
|
|
339
|
+
if (text) {
|
|
340
|
+
this.pendingBuffer += text;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!this.pendingBuffer) return '';
|
|
344
|
+
const remaining = this.pendingBuffer;
|
|
345
|
+
this.pendingBuffer = '';
|
|
346
|
+
return this.replacer(remaining);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
toNodeTransform() {
|
|
350
|
+
const self = this;
|
|
351
|
+
const transformStream = new Transform({
|
|
352
|
+
transform(chunk, encoding, callback) {
|
|
353
|
+
try {
|
|
354
|
+
const out = self.feed(chunk);
|
|
355
|
+
if (out) this.push(out);
|
|
356
|
+
callback();
|
|
357
|
+
} catch (err) {
|
|
358
|
+
callback(err);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
flush(callback) {
|
|
362
|
+
try {
|
|
363
|
+
const out = self.flush();
|
|
364
|
+
if (out) this.push(out);
|
|
365
|
+
callback();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
callback(err);
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
destroy(err, callback) {
|
|
371
|
+
self.reset();
|
|
372
|
+
callback(err);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
transformStream.detokenizer = self;
|
|
376
|
+
return transformStream;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async* wrapStream(asyncIterable) {
|
|
380
|
+
for await (const chunk of asyncIterable) {
|
|
381
|
+
if (chunk === null || chunk === undefined) continue;
|
|
382
|
+
if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
|
|
383
|
+
const delta = chunk.choices[0].delta.content;
|
|
384
|
+
if (typeof delta === 'string') {
|
|
385
|
+
const restored = this.feed(delta);
|
|
386
|
+
if (restored) {
|
|
387
|
+
chunk.choices[0].delta.content = restored;
|
|
388
|
+
yield chunk;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
yield chunk;
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let text = '';
|
|
397
|
+
if (typeof chunk === 'string') {
|
|
398
|
+
text = chunk;
|
|
399
|
+
} else if (typeof chunk?.text === 'string') {
|
|
400
|
+
text = chunk.text;
|
|
401
|
+
} else if (typeof chunk?.content === 'string') {
|
|
402
|
+
text = chunk.content;
|
|
403
|
+
} else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
|
|
404
|
+
text = chunk;
|
|
405
|
+
} else {
|
|
406
|
+
text = String(chunk);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const emitted = this.feed(text);
|
|
410
|
+
if (emitted) yield emitted;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const finalChunk = this.flush();
|
|
414
|
+
if (finalChunk) yield finalChunk;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function getStreamDetokenizer(sessionId = 'default') {
|
|
419
|
+
const session = getSession(sessionId);
|
|
420
|
+
if (!session.streamDetokenizer) {
|
|
421
|
+
session.streamDetokenizer = new StreamingDetokenizer(session.map);
|
|
422
|
+
} else {
|
|
423
|
+
session.streamDetokenizer.updateTokenMap(session.map);
|
|
424
|
+
}
|
|
425
|
+
return session.streamDetokenizer;
|
|
426
|
+
}
|
|
427
|
+
|
|
99
428
|
function reverseTokens(text, map) {
|
|
100
429
|
if (!text || typeof text !== 'string' || !map || Object.keys(map).length === 0) return text;
|
|
101
430
|
if (PrivacyScrubberCore && typeof PrivacyScrubberCore.unscrubText === 'function') {
|
|
@@ -130,16 +459,33 @@ function mcpLog(msg) {
|
|
|
130
459
|
|
|
131
460
|
// Secrets detection patterns — defined once here, shared by detectSecrets() and performSanitization()
|
|
132
461
|
const DEVOPS_SECRETS_DETECTOR = [
|
|
133
|
-
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{
|
|
462
|
+
{ name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g },
|
|
463
|
+
{ name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
464
|
+
{ name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g },
|
|
465
|
+
{ name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g },
|
|
466
|
+
{ name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g },
|
|
467
|
+
{ name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g },
|
|
468
|
+
{ name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g },
|
|
469
|
+
{ 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 },
|
|
470
|
+
{ name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g },
|
|
471
|
+
{ name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g },
|
|
472
|
+
{ 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 },
|
|
473
|
+
{ name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g },
|
|
474
|
+
{ 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 },
|
|
134
475
|
{ name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
|
|
135
|
-
{ 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 },
|
|
136
|
-
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{
|
|
476
|
+
{ 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 },
|
|
477
|
+
{ name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g },
|
|
137
478
|
{ name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi },
|
|
479
|
+
{ 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 },
|
|
480
|
+
{ 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 },
|
|
138
481
|
{ name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi },
|
|
139
|
-
{ name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi },
|
|
482
|
+
{ 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 },
|
|
140
483
|
{ name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi },
|
|
484
|
+
{ 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 },
|
|
485
|
+
{ 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 },
|
|
486
|
+
{ name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi },
|
|
141
487
|
{ name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi },
|
|
142
|
-
{ 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,}
|
|
488
|
+
{ 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 },
|
|
143
489
|
{ 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 },
|
|
144
490
|
{ name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g }
|
|
145
491
|
];
|
|
@@ -151,7 +497,9 @@ function checkLicenseStatus() {
|
|
|
151
497
|
const result = LicenseManager.validate(key);
|
|
152
498
|
|
|
153
499
|
if (!result.valid) {
|
|
154
|
-
|
|
500
|
+
if (process.env.DEBUG_PRIVACYSCRUBBER) {
|
|
501
|
+
mcpLog(`[License] Key validation notice: ${result.reason || "Unknown"}`);
|
|
502
|
+
}
|
|
155
503
|
return { isPro: false, type: null, error: result.reason || "Invalid license format or signature." };
|
|
156
504
|
}
|
|
157
505
|
|
|
@@ -264,8 +612,8 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
264
612
|
frameworksSet.add('CCPA/CPRA');
|
|
265
613
|
}
|
|
266
614
|
if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
|
|
267
|
-
frameworksSet.add('SOC 2
|
|
268
|
-
frameworksSet.add('ISO 27001
|
|
615
|
+
frameworksSet.add('SOC 2 controls');
|
|
616
|
+
frameworksSet.add('ISO 27001 A.8.11 controls');
|
|
269
617
|
}
|
|
270
618
|
if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
|
|
271
619
|
frameworksSet.add('PCI DSS v4.0');
|
|
@@ -331,6 +679,118 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
331
679
|
};
|
|
332
680
|
}
|
|
333
681
|
|
|
682
|
+
// ── PSR-2 Receipt Integrity (real SHA-256; parity with src/modules/receipt-integrity.js) ──
|
|
683
|
+
const PSR2_VERSION = 'PSR-2';
|
|
684
|
+
const PSR2_STATEMENTS = {
|
|
685
|
+
measured: [
|
|
686
|
+
'Token Manifest SHA-256: digest of the sorted token labels held in this session (original values are never hashed or exported).',
|
|
687
|
+
'Entity counts per type.',
|
|
688
|
+
'Receipt SHA-256: digest of the canonical field string, so edits to this receipt are detectable.'
|
|
689
|
+
],
|
|
690
|
+
byDesign: [
|
|
691
|
+
'The sanitization engine runs locally in process memory and does not transmit prompt content. This is an architectural property and is not measured by this receipt.',
|
|
692
|
+
'Token-to-original mappings are held in volatile memory for the session. This receipt does not record their contents.'
|
|
693
|
+
],
|
|
694
|
+
limits: [
|
|
695
|
+
'This receipt is not a digital signature and does not prove who generated it or when.',
|
|
696
|
+
'No final output digest is recorded by this export (Output SHA-256: not recorded).',
|
|
697
|
+
'Detection is pattern-based. Free-text narrative can contain indirect identifiers that remain after sanitization; review output before sharing.',
|
|
698
|
+
'Regulatory references are informational technical-safeguard mappings, not a legal determination of compliance.'
|
|
699
|
+
]
|
|
700
|
+
};
|
|
701
|
+
function psr2Sha256(input) {
|
|
702
|
+
return crypto.createHash('sha256').update(String(input == null ? '' : input), 'utf8').digest('hex');
|
|
703
|
+
}
|
|
704
|
+
function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
|
|
705
|
+
const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
|
|
706
|
+
const counts = {};
|
|
707
|
+
for (const t of labels) {
|
|
708
|
+
const m = t.match(/\[([A-Z_]+)_\d+\]/);
|
|
709
|
+
const type = m ? m[1] : 'CUSTOM';
|
|
710
|
+
counts[type] = (counts[type] || 0) + 1;
|
|
711
|
+
}
|
|
712
|
+
const entityCounts = Object.keys(counts).sort().map(k => `${k}=${counts[k]}`).join(',') || 'none';
|
|
713
|
+
const manifestSha256 = psr2Sha256(labels.join('\n'));
|
|
714
|
+
const canonical = [PSR2_VERSION, timestamp, 'none', manifestSha256, entityCounts, 'na'].join('|');
|
|
715
|
+
return { version: PSR2_VERSION, timestamp, outputSha256: null, manifestSha256, entityCounts, tokensTotal: labels.length, tokensPresentInOutput: null, canonical, receiptSha256: psr2Sha256(canonical) };
|
|
716
|
+
}
|
|
717
|
+
function psr2Lines(r) {
|
|
718
|
+
return [
|
|
719
|
+
`Receipt Version: ${r.version}`,
|
|
720
|
+
`Timestamp (UTC): ${r.timestamp}`,
|
|
721
|
+
`Output SHA-256: not recorded`,
|
|
722
|
+
`Token Manifest SHA-256: ${r.manifestSha256}`,
|
|
723
|
+
`Entity Counts: ${r.entityCounts}`,
|
|
724
|
+
`Tokens In Final Output: not recorded`,
|
|
725
|
+
`Canonical: ${r.canonical}`,
|
|
726
|
+
`Receipt SHA-256: ${r.receiptSha256}`
|
|
727
|
+
];
|
|
728
|
+
}
|
|
729
|
+
function formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource, utmMedium }) {
|
|
730
|
+
const entitySummary = Object.entries(telemetry.entities).map(([t, count]) => `[${t}]: ${count}`).join(', ') || 'None (Clean)';
|
|
731
|
+
const teamsUrl = `https://privacyscrubber.com/teams?tier=teams&utm_source=${utmSource}&utm_medium=${utmMedium}&utm_campaign=teams_upgrade`;
|
|
732
|
+
const sdkUrl = `https://privacyscrubber.com/sdk/?tier=sdk&utm_source=${utmSource}&utm_medium=${utmMedium}&utm_campaign=sdk_pipeline`;
|
|
733
|
+
if (format === 'json') {
|
|
734
|
+
return JSON.stringify({
|
|
735
|
+
protocol: 'Zero-Trust Data Sanitization (ZTDS)',
|
|
736
|
+
receipt_version: receipt.version,
|
|
737
|
+
company: company_name,
|
|
738
|
+
department,
|
|
739
|
+
timestamp: receipt.timestamp,
|
|
740
|
+
output_sha256: null,
|
|
741
|
+
token_manifest_sha256: receipt.manifestSha256,
|
|
742
|
+
entity_counts: receipt.entityCounts,
|
|
743
|
+
canonical: receipt.canonical,
|
|
744
|
+
receipt_sha256: receipt.receiptSha256,
|
|
745
|
+
risk_level: telemetry.riskLevel,
|
|
746
|
+
frameworks_referenced: telemetry.frameworksList,
|
|
747
|
+
total_masked_tokens: telemetry.totalCount,
|
|
748
|
+
entities_breakdown: telemetry.entities,
|
|
749
|
+
scope: { measured: PSR2_STATEMENTS.measured, by_design_not_measured: PSR2_STATEMENTS.byDesign, limits: PSR2_STATEMENTS.limits },
|
|
750
|
+
verification_url: 'https://privacyscrubber.com/features/audit-receipt/#verify',
|
|
751
|
+
licensing_and_governance: { teams_governance_url: teamsUrl, sdk_pipeline_url: sdkUrl }
|
|
752
|
+
}, null, 2);
|
|
753
|
+
}
|
|
754
|
+
if (format === 'summary') {
|
|
755
|
+
return `[PrivacyScrubber SHA-256 Receipt] ${receipt.version} | Receipt SHA-256: ${receipt.receiptSha256.substring(0, 16)}... | Organization: ${company_name} | Masked Tokens: ${telemetry.totalCount} | Risk: ${telemetry.riskLevel} | Frameworks (informational): ${telemetry.frameworksList.join(', ')}`;
|
|
756
|
+
}
|
|
757
|
+
return `# Zero-Trust Data Sanitization Receipt
|
|
758
|
+
**Organization:** ${company_name} (${department})
|
|
759
|
+
**Timestamp:** ${receipt.timestamp}
|
|
760
|
+
**Receipt Format:** ${receipt.version} (SHA-256 digests; not a digital signature)
|
|
761
|
+
|
|
762
|
+
---
|
|
763
|
+
|
|
764
|
+
### Sanitization Metrics & Risk Assessment
|
|
765
|
+
* **Overall Risk Rating:** **${telemetry.riskLevel}**
|
|
766
|
+
* **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
|
|
767
|
+
* **Entity Breakdown:** ${entitySummary}
|
|
768
|
+
|
|
769
|
+
### Informational Framework References
|
|
770
|
+
${telemetry.frameworksList.map(f => `- ${f}`).join('\n')}
|
|
771
|
+
|
|
772
|
+
These are technical-safeguard mappings, not a legal determination of compliance.
|
|
773
|
+
|
|
774
|
+
### Receipt Integrity (${receipt.version})
|
|
775
|
+
\`\`\`text
|
|
776
|
+
${psr2Lines(receipt).join('\n')}
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
### Scope of This Receipt
|
|
780
|
+
**Measured:** ${PSR2_STATEMENTS.measured.join(' ')}
|
|
781
|
+
|
|
782
|
+
**Stated by design, not measured:** ${PSR2_STATEMENTS.byDesign.join(' ')}
|
|
783
|
+
|
|
784
|
+
**Limits:** ${PSR2_STATEMENTS.limits.join(' ')}
|
|
785
|
+
|
|
786
|
+
### Enterprise & Team Governance
|
|
787
|
+
* **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules and encrypted session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](${teamsUrl})
|
|
788
|
+
* **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](${sdkUrl})
|
|
789
|
+
|
|
790
|
+
*Generated locally by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
791
|
+
*Verify locally (paste this receipt): https://privacyscrubber.com/features/audit-receipt/#verify*`;
|
|
792
|
+
}
|
|
793
|
+
|
|
334
794
|
function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, charLimit = 15000) {
|
|
335
795
|
const isCompact = compact === true || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === '1' || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === 'true';
|
|
336
796
|
const truncationLine = wasTruncated
|
|
@@ -360,7 +820,7 @@ function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, ch
|
|
|
360
820
|
|
|
361
821
|
const icon = telemetry.riskLevel.includes('CRITICAL') ? '🔴' : (telemetry.riskLevel.includes('MODERATE') ? '🟠' : '🟢');
|
|
362
822
|
|
|
363
|
-
return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **
|
|
823
|
+
return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **Framework References (informational):** ${telemetry.frameworksList.join(', ')}\n> * **Tokens Masked:** ${telemetry.totalCount} (${entitiesList})\n${truncationLine}> * ⭐ **Star on GitHub:** [moxno/privacyscrubber-mcp](https://github.com/moxno/privacyscrubber-mcp) (\`gh repo star moxno/privacyscrubber-mcp\`) | **SDK & Enterprise:** [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp&utm_medium=audit_receipt&utm_campaign=sdk_pipeline)\n`;
|
|
364
824
|
}
|
|
365
825
|
|
|
366
826
|
function formatCefEvent(eventData = {}) {
|
|
@@ -461,7 +921,7 @@ function formatJsonlEvent(eventData = {}) {
|
|
|
461
921
|
frameworks,
|
|
462
922
|
session_hash: sessionHash,
|
|
463
923
|
zero_egress: true,
|
|
464
|
-
|
|
924
|
+
processing_mode: "local in-memory (by design, not measured)",
|
|
465
925
|
...(organization && { organization }),
|
|
466
926
|
...(department && { department }),
|
|
467
927
|
...(executionMs && { execution_ms: executionMs }),
|
|
@@ -822,7 +1282,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
822
1282
|
},
|
|
823
1283
|
profile: {
|
|
824
1284
|
type: "string",
|
|
825
|
-
description:
|
|
1285
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1286
|
+
},
|
|
1287
|
+
sector: {
|
|
1288
|
+
type: "string",
|
|
1289
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
826
1290
|
},
|
|
827
1291
|
format: {
|
|
828
1292
|
type: "string",
|
|
@@ -835,7 +1299,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
835
1299
|
},
|
|
836
1300
|
{
|
|
837
1301
|
name: "sanitize_text",
|
|
838
|
-
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
|
|
1302
|
+
description: "STEP 1: Call this first. You MUST NOT process raw user data before calling this. Locally scrubs PII, secrets, and credentials (like API keys, passwords, emails, phones, names) from code, logs, or text. Replaces them with safe placeholders (e.g., [EMAIL_1], [API_KEY_1]). Keep your data secure before passing it to any LLM. Supports General and all 30 specialized industry profiles in RAM session map. (For in-code backend services or RAG vector pipelines outside of MCP, use '@privacyscrubber/sdk': npm i @privacyscrubber/sdk)",
|
|
839
1303
|
inputSchema: {
|
|
840
1304
|
type: "object",
|
|
841
1305
|
properties: {
|
|
@@ -845,7 +1309,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
845
1309
|
},
|
|
846
1310
|
profile: {
|
|
847
1311
|
type: "string",
|
|
848
|
-
description:
|
|
1312
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1313
|
+
},
|
|
1314
|
+
sector: {
|
|
1315
|
+
type: "string",
|
|
1316
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
849
1317
|
},
|
|
850
1318
|
compact: {
|
|
851
1319
|
type: "boolean",
|
|
@@ -866,7 +1334,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
866
1334
|
},
|
|
867
1335
|
{
|
|
868
1336
|
name: "scrub_text",
|
|
869
|
-
description: "Alias for 'sanitize_text'. Locally scrubs PII and
|
|
1337
|
+
description: "Alias for 'sanitize_text'. Locally scrubs PII, secrets, and credentials from prompts or text before LLM ingestion. Supports General and all 30 specialized industry profiles in RAM session map.",
|
|
870
1338
|
inputSchema: {
|
|
871
1339
|
type: "object",
|
|
872
1340
|
properties: {
|
|
@@ -876,7 +1344,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
876
1344
|
},
|
|
877
1345
|
profile: {
|
|
878
1346
|
type: "string",
|
|
879
|
-
description:
|
|
1347
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1348
|
+
},
|
|
1349
|
+
sector: {
|
|
1350
|
+
type: "string",
|
|
1351
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
880
1352
|
},
|
|
881
1353
|
compact: {
|
|
882
1354
|
type: "boolean",
|
|
@@ -897,13 +1369,21 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
897
1369
|
},
|
|
898
1370
|
{
|
|
899
1371
|
name: "reveal_text",
|
|
900
|
-
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user.",
|
|
1372
|
+
description: "STEP 3: Call this last. You MUST pass your final generated response through this tool to restore tokens (e.g., [EMAIL_1]) back with the original private data from the local volatile RAM-only session map before showing it to the user. Supports both standard batch mode and real-time streaming mode (stream: true).",
|
|
901
1373
|
inputSchema: {
|
|
902
1374
|
type: "object",
|
|
903
1375
|
properties: {
|
|
904
1376
|
text: {
|
|
905
1377
|
type: "string",
|
|
906
|
-
description: "The AI generated response containing placeholders to restore."
|
|
1378
|
+
description: "The AI generated response (or streaming chunk) containing placeholders to restore."
|
|
1379
|
+
},
|
|
1380
|
+
stream: {
|
|
1381
|
+
type: "boolean",
|
|
1382
|
+
description: "Optional. When true, uses the sliding-window StreamingDetokenizer to prevent token splitting across chunk boundaries."
|
|
1383
|
+
},
|
|
1384
|
+
flush: {
|
|
1385
|
+
type: "boolean",
|
|
1386
|
+
description: "Optional. When true in streaming mode, flushes any remaining buffered text at EOF."
|
|
907
1387
|
},
|
|
908
1388
|
session_id: {
|
|
909
1389
|
type: "string",
|
|
@@ -913,6 +1393,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
913
1393
|
required: ["text"]
|
|
914
1394
|
}
|
|
915
1395
|
},
|
|
1396
|
+
{
|
|
1397
|
+
name: "stream_reveal_text",
|
|
1398
|
+
description: "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines. Buffers split token candidates across chunk boundaries (e.g. '[EM' + 'AIL_1]') so no characters or tokens are corrupted. Pass 'flush: true' on the final chunk.",
|
|
1399
|
+
inputSchema: {
|
|
1400
|
+
type: "object",
|
|
1401
|
+
properties: {
|
|
1402
|
+
chunk: {
|
|
1403
|
+
type: "string",
|
|
1404
|
+
description: "The incoming text chunk from the LLM stream or stdout."
|
|
1405
|
+
},
|
|
1406
|
+
flush: {
|
|
1407
|
+
type: "boolean",
|
|
1408
|
+
description: "Optional. Set to true on the final chunk or at EOF to flush remaining buffer. Defaults to false."
|
|
1409
|
+
},
|
|
1410
|
+
reset: {
|
|
1411
|
+
type: "boolean",
|
|
1412
|
+
description: "Optional. Set to true to reset internal streaming buffer for this session. Defaults to false."
|
|
1413
|
+
},
|
|
1414
|
+
session_id: {
|
|
1415
|
+
type: "string",
|
|
1416
|
+
description: "Optional session ID matching the sanitize_text call for multi-agent isolation. Defaults to 'default'."
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
required: ["chunk"]
|
|
1420
|
+
}
|
|
1421
|
+
},
|
|
916
1422
|
{
|
|
917
1423
|
name: "sanitize_file",
|
|
918
1424
|
description: "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory.",
|
|
@@ -925,7 +1431,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
925
1431
|
},
|
|
926
1432
|
profile: {
|
|
927
1433
|
type: "string",
|
|
928
|
-
description:
|
|
1434
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1435
|
+
},
|
|
1436
|
+
sector: {
|
|
1437
|
+
type: "string",
|
|
1438
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
929
1439
|
},
|
|
930
1440
|
compact: {
|
|
931
1441
|
type: "boolean",
|
|
@@ -951,7 +1461,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
951
1461
|
},
|
|
952
1462
|
profile: {
|
|
953
1463
|
type: "string",
|
|
954
|
-
description:
|
|
1464
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1465
|
+
},
|
|
1466
|
+
sector: {
|
|
1467
|
+
type: "string",
|
|
1468
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
955
1469
|
},
|
|
956
1470
|
compact: {
|
|
957
1471
|
type: "boolean",
|
|
@@ -977,7 +1491,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
977
1491
|
},
|
|
978
1492
|
profile: {
|
|
979
1493
|
type: "string",
|
|
980
|
-
description:
|
|
1494
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1495
|
+
},
|
|
1496
|
+
sector: {
|
|
1497
|
+
type: "string",
|
|
1498
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
981
1499
|
},
|
|
982
1500
|
extensions: {
|
|
983
1501
|
type: "array",
|
|
@@ -1004,7 +1522,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1004
1522
|
},
|
|
1005
1523
|
profile: {
|
|
1006
1524
|
type: "string",
|
|
1007
|
-
description:
|
|
1525
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1526
|
+
},
|
|
1527
|
+
sector: {
|
|
1528
|
+
type: "string",
|
|
1529
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1008
1530
|
},
|
|
1009
1531
|
dry_run: {
|
|
1010
1532
|
type: "boolean",
|
|
@@ -1045,6 +1567,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1045
1567
|
department: {
|
|
1046
1568
|
type: "string",
|
|
1047
1569
|
description: "Optional department or auditor ID (e.g. 'SecOps / Engineering')."
|
|
1570
|
+
},
|
|
1571
|
+
session_id: {
|
|
1572
|
+
type: "string",
|
|
1573
|
+
description: "Optional session ID for multi-agent isolation. Defaults to 'default'."
|
|
1048
1574
|
}
|
|
1049
1575
|
},
|
|
1050
1576
|
required: []
|
|
@@ -1093,7 +1619,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1093
1619
|
},
|
|
1094
1620
|
profile: {
|
|
1095
1621
|
type: "string",
|
|
1096
|
-
description: "Detection profile. Available: '
|
|
1622
|
+
description: "Detection profile. Available: 'Dev' (default for code/secrets), 'General', or any of the 30 specialized industry profiles (e.g. 'Security', 'Finance', 'Medical'). Defaults to 'Dev'."
|
|
1623
|
+
},
|
|
1624
|
+
sector: {
|
|
1625
|
+
type: "string",
|
|
1626
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1097
1627
|
},
|
|
1098
1628
|
timeout_ms: {
|
|
1099
1629
|
type: "number",
|
|
@@ -1115,7 +1645,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1115
1645
|
},
|
|
1116
1646
|
profile: {
|
|
1117
1647
|
type: "string",
|
|
1118
|
-
description: "Detection profile. Defaults to 'Dev'."
|
|
1648
|
+
description: "Detection profile. Available: 'Dev' (default), 'General', or any of the 30 specialized industry profiles. Defaults to 'Dev'."
|
|
1649
|
+
},
|
|
1650
|
+
sector: {
|
|
1651
|
+
type: "string",
|
|
1652
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1119
1653
|
},
|
|
1120
1654
|
max_lines: {
|
|
1121
1655
|
type: "number",
|
|
@@ -1141,7 +1675,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1141
1675
|
},
|
|
1142
1676
|
profile: {
|
|
1143
1677
|
type: "string",
|
|
1144
|
-
description: "Detection profile. Defaults to 'Dev'."
|
|
1678
|
+
description: "Detection profile. Available: 'Dev' (default), 'General', or any of the 30 specialized industry profiles. Defaults to 'Dev'."
|
|
1679
|
+
},
|
|
1680
|
+
sector: {
|
|
1681
|
+
type: "string",
|
|
1682
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1145
1683
|
},
|
|
1146
1684
|
session_id: {
|
|
1147
1685
|
type: "string",
|
|
@@ -1270,7 +1808,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1270
1808
|
},
|
|
1271
1809
|
profile: {
|
|
1272
1810
|
type: "string",
|
|
1273
|
-
description:
|
|
1811
|
+
description: PROFILE_PARAM_DESCRIPTION
|
|
1812
|
+
},
|
|
1813
|
+
sector: {
|
|
1814
|
+
type: "string",
|
|
1815
|
+
description: SECTOR_PARAM_DESCRIPTION
|
|
1274
1816
|
},
|
|
1275
1817
|
session_id: {
|
|
1276
1818
|
type: "string",
|
|
@@ -1507,14 +2049,14 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
1507
2049
|
"biotech": "Biotech & Genomics profile. Scrubs accessions, patient phenotypes, HGVS variants, DNA/RNA samples, and flowcell IDs.",
|
|
1508
2050
|
"telecom": "Call Centers & Telephony profile. Scrubs CTI interaction IDs, Amazon Connect/Genesys sessions, agent extensions, ANI/DNIS, IVR DTMF, and audio transcript diarization headers."
|
|
1509
2051
|
};
|
|
1510
|
-
const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a
|
|
2052
|
+
const desc = descriptions[profileName.toLowerCase()] || `The '${profileName}' profile is a specialized detection ruleset tuned for specific industry compliance. It detects and sanitizes domain-specific identifiers.`;
|
|
1511
2053
|
|
|
1512
2054
|
return {
|
|
1513
2055
|
contents: [
|
|
1514
2056
|
{
|
|
1515
2057
|
uri,
|
|
1516
2058
|
mimeType: "text/markdown",
|
|
1517
|
-
text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` to
|
|
2059
|
+
text: `# Profile: ${profileName}\n\n**Description:** ${desc}\n\n*Note: To use this profile, pass \`"profile": "${profileName}"\` or \`"sector": "${profileName}"\` to tools like \`scrub_text\`, \`sanitize_text\`, or \`sanitize_file\`. Advanced profiles include a 5,000 character Free trial quota; PRO/TEAMS tiers grant unlimited characters.*`
|
|
1518
2060
|
}
|
|
1519
2061
|
]
|
|
1520
2062
|
};
|
|
@@ -1529,7 +2071,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1529
2071
|
|
|
1530
2072
|
try {
|
|
1531
2073
|
if (name === "audit_context") {
|
|
1532
|
-
const { text,
|
|
2074
|
+
const { text, format = "detailed" } = args || {};
|
|
1533
2075
|
|
|
1534
2076
|
if (text === undefined || text === null) {
|
|
1535
2077
|
return {
|
|
@@ -1544,7 +2086,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1544
2086
|
};
|
|
1545
2087
|
}
|
|
1546
2088
|
|
|
1547
|
-
const targetProfile = (
|
|
2089
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
1548
2090
|
const customRules = loadCustomRules();
|
|
1549
2091
|
const normalizedProfile = targetProfile.toLowerCase();
|
|
1550
2092
|
const license = checkLicenseStatus();
|
|
@@ -1595,7 +2137,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1595
2137
|
}
|
|
1596
2138
|
|
|
1597
2139
|
if (name === "sanitize_text" || name === "scrub_text") {
|
|
1598
|
-
const { text,
|
|
2140
|
+
const { text, ignore_list, compact = false, session_id = "default" } = args || {};
|
|
1599
2141
|
const currentSession = getSessionMap(session_id);
|
|
1600
2142
|
const currentIgnoreList = getSessionIgnoreList(session_id);
|
|
1601
2143
|
|
|
@@ -1615,7 +2157,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1615
2157
|
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'. Stringify objects before passing.` }]
|
|
1616
2158
|
};
|
|
1617
2159
|
}
|
|
1618
|
-
const targetProfile = (
|
|
2160
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
1619
2161
|
|
|
1620
2162
|
// Check tier gating for advanced profiles
|
|
1621
2163
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
@@ -1660,7 +2202,38 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1660
2202
|
}
|
|
1661
2203
|
|
|
1662
2204
|
if (name === "reveal_text") {
|
|
1663
|
-
const { text, session_id = "default" } = args || {};
|
|
2205
|
+
const { text, stream = false, flush = false, session_id = "default" } = args || {};
|
|
2206
|
+
|
|
2207
|
+
if (stream) {
|
|
2208
|
+
if (text === undefined || text === null) {
|
|
2209
|
+
if (flush) {
|
|
2210
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2211
|
+
const flushedText = detok.flush();
|
|
2212
|
+
return {
|
|
2213
|
+
content: [{ type: "text", text: flushedText }]
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
return {
|
|
2217
|
+
isError: true,
|
|
2218
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the stream chunk to restore." }]
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
if (typeof text !== "string") {
|
|
2222
|
+
return {
|
|
2223
|
+
isError: true,
|
|
2224
|
+
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2228
|
+
let emitted = detok.feed(text);
|
|
2229
|
+
if (flush) {
|
|
2230
|
+
emitted += detok.flush();
|
|
2231
|
+
}
|
|
2232
|
+
return {
|
|
2233
|
+
content: [{ type: "text", text: emitted }]
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
|
|
1664
2237
|
if (text === undefined || text === null) {
|
|
1665
2238
|
return {
|
|
1666
2239
|
isError: true,
|
|
@@ -1700,6 +2273,43 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1700
2273
|
};
|
|
1701
2274
|
}
|
|
1702
2275
|
|
|
2276
|
+
if (name === "stream_reveal_text") {
|
|
2277
|
+
const { chunk, flush = false, reset = false, session_id = "default" } = args || {};
|
|
2278
|
+
if (chunk === undefined || chunk === null) {
|
|
2279
|
+
if (flush) {
|
|
2280
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2281
|
+
const flushedText = detok.flush();
|
|
2282
|
+
return {
|
|
2283
|
+
content: [{ type: "text", text: flushedText }]
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
return {
|
|
2287
|
+
isError: true,
|
|
2288
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'chunk'. Provide the text chunk to process." }]
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
if (typeof chunk !== "string") {
|
|
2292
|
+
return {
|
|
2293
|
+
isError: true,
|
|
2294
|
+
content: [{ type: "text", text: `Error: Parameter 'chunk' must be a string, got '${typeof chunk}'.` }]
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
const detok = getStreamDetokenizer(session_id);
|
|
2299
|
+
if (reset) {
|
|
2300
|
+
detok.reset();
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
let emitted = detok.feed(chunk);
|
|
2304
|
+
if (flush) {
|
|
2305
|
+
emitted += detok.flush();
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
return {
|
|
2309
|
+
content: [{ type: "text", text: emitted }]
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
|
|
1703
2313
|
if (name === "mark_false_positive") {
|
|
1704
2314
|
const { token, session_id = "default" } = args || {};
|
|
1705
2315
|
if (!token || typeof token !== 'string') {
|
|
@@ -1748,7 +2358,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1748
2358
|
]
|
|
1749
2359
|
};
|
|
1750
2360
|
}
|
|
1751
|
-
const
|
|
2361
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
2362
|
+
const { compact = false, session_id = "default" } = args || {};
|
|
1752
2363
|
const targetSession = getSessionMap(session_id);
|
|
1753
2364
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
1754
2365
|
const filePath = rawPath;
|
|
@@ -1796,9 +2407,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1796
2407
|
if (resolvedPath.toLowerCase().endsWith(".docx")) {
|
|
1797
2408
|
try {
|
|
1798
2409
|
const mammoth = require('mammoth');
|
|
1799
|
-
const
|
|
1800
|
-
const
|
|
1801
|
-
const
|
|
2410
|
+
const zlib = require('zlib');
|
|
2411
|
+
const docxBuf = fs.readFileSync(resolvedPath);
|
|
2412
|
+
const result = await mammoth.extractRawText({ buffer: docxBuf });
|
|
2413
|
+
let extraXmlText = "";
|
|
2414
|
+
try {
|
|
2415
|
+
const zipEntries = {};
|
|
2416
|
+
let eocdOffset = -1;
|
|
2417
|
+
for (let i = docxBuf.length - 22; i >= Math.max(0, docxBuf.length - 65557); i--) {
|
|
2418
|
+
if (docxBuf.readUInt32LE(i) === 0x06054b50) {
|
|
2419
|
+
eocdOffset = i;
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
if (eocdOffset !== -1) {
|
|
2424
|
+
const cdEntries = docxBuf.readUInt16LE(eocdOffset + 10);
|
|
2425
|
+
let pos = docxBuf.readUInt32LE(eocdOffset + 16);
|
|
2426
|
+
for (let e = 0; e < cdEntries && pos + 46 <= docxBuf.length; e++) {
|
|
2427
|
+
if (docxBuf.readUInt32LE(pos) !== 0x02014b50) break;
|
|
2428
|
+
const method = docxBuf.readUInt16LE(pos + 10);
|
|
2429
|
+
const compSize = docxBuf.readUInt32LE(pos + 20);
|
|
2430
|
+
const nameLen = docxBuf.readUInt16LE(pos + 28);
|
|
2431
|
+
const extraLen = docxBuf.readUInt16LE(pos + 30);
|
|
2432
|
+
const commentLen = docxBuf.readUInt16LE(pos + 32);
|
|
2433
|
+
const localOffset = docxBuf.readUInt32LE(pos + 42);
|
|
2434
|
+
const entryName = docxBuf.toString('utf8', pos + 46, pos + 46 + nameLen);
|
|
2435
|
+
if (localOffset + 30 <= docxBuf.length && docxBuf.readUInt32LE(localOffset) === 0x04034b50) {
|
|
2436
|
+
const lNameLen = docxBuf.readUInt16LE(localOffset + 26);
|
|
2437
|
+
const lExtraLen = docxBuf.readUInt16LE(localOffset + 28);
|
|
2438
|
+
const dataStart = localOffset + 30 + lNameLen + lExtraLen;
|
|
2439
|
+
const compData = docxBuf.subarray(dataStart, dataStart + compSize);
|
|
2440
|
+
if (method === 0) {
|
|
2441
|
+
zipEntries[entryName] = compData.toString('utf8');
|
|
2442
|
+
} else if (method === 8) {
|
|
2443
|
+
zipEntries[entryName] = zlib.inflateRawSync(compData).toString('utf8');
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
pos += 46 + nameLen + extraLen + commentLen;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const xmlTargets = Object.keys(zipEntries).filter((n) =>
|
|
2450
|
+
/^(?:word\/(?:header\d*|footer\d*|footnotes|endnotes|comments|commentsExtended|people)\.xml|docProps\/(?:core|app|custom)\.xml)$/i.test(n)
|
|
2451
|
+
);
|
|
2452
|
+
const docAuthors = new Set();
|
|
2453
|
+
for (const xmlPath of xmlTargets) {
|
|
2454
|
+
const rawXml = zipEntries[xmlPath];
|
|
2455
|
+
const authorRx = /\b(?:w:author|w15:author|w15:userId)="([^"]+)"/gi;
|
|
2456
|
+
let am;
|
|
2457
|
+
while ((am = authorRx.exec(rawXml)) !== null) {
|
|
2458
|
+
if (am[1] && am[1].trim()) docAuthors.add(am[1].trim());
|
|
2459
|
+
}
|
|
2460
|
+
const stripped = rawXml
|
|
2461
|
+
.replace(/<\/w:p>/gi, "\n")
|
|
2462
|
+
.replace(/<w:tab\b[^>]*\/?>/gi, "\t")
|
|
2463
|
+
.replace(/<[^>]+>/g, " ")
|
|
2464
|
+
.replace(/&/g, "&")
|
|
2465
|
+
.replace(/</g, "<")
|
|
2466
|
+
.replace(/>/g, ">")
|
|
2467
|
+
.replace(/"/g, '"')
|
|
2468
|
+
.replace(/'/g, "'")
|
|
2469
|
+
.replace(/[ \t]+/g, " ")
|
|
2470
|
+
.trim();
|
|
2471
|
+
if (stripped) extraXmlText += `\n${stripped}\n`;
|
|
2472
|
+
}
|
|
2473
|
+
if (zipEntries["word/document.xml"]) {
|
|
2474
|
+
const docXml = zipEntries["word/document.xml"];
|
|
2475
|
+
const authorRx = /\bw:author="([^"]+)"/gi;
|
|
2476
|
+
let am;
|
|
2477
|
+
while ((am = authorRx.exec(docXml)) !== null) {
|
|
2478
|
+
if (am[1] && am[1].trim()) docAuthors.add(am[1].trim());
|
|
2479
|
+
}
|
|
2480
|
+
const delAndInstrRx = /<w:(?:delText|instrText)\b[^>]*>([\s\S]*?)<\/w:(?:delText|instrText)>/gi;
|
|
2481
|
+
let dm;
|
|
2482
|
+
const hiddenChunks = [];
|
|
2483
|
+
while ((dm = delAndInstrRx.exec(docXml)) !== null) {
|
|
2484
|
+
const val = dm[1]
|
|
2485
|
+
.replace(/<[^>]+>/g, " ")
|
|
2486
|
+
.replace(/&/g, "&")
|
|
2487
|
+
.replace(/</g, "<")
|
|
2488
|
+
.replace(/>/g, ">")
|
|
2489
|
+
.replace(/"/g, '"')
|
|
2490
|
+
.replace(/'/g, "'")
|
|
2491
|
+
.trim();
|
|
2492
|
+
if (val) hiddenChunks.push(val);
|
|
2493
|
+
}
|
|
2494
|
+
if (hiddenChunks.length > 0) {
|
|
2495
|
+
extraXmlText += `\n[Tracked Changes / Field Text]: ${hiddenChunks.join(" | ")}\n`;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (docAuthors.size > 0) {
|
|
2499
|
+
extraXmlText += `\n[Document Reviewers]: ${Array.from(docAuthors).join(", ")}\n`;
|
|
2500
|
+
}
|
|
2501
|
+
const relsTargets = Object.keys(zipEntries).filter((n) => /^word\/_rels\/.*\.rels$/i.test(n));
|
|
2502
|
+
for (const relPath of relsTargets) {
|
|
2503
|
+
const relXml = zipEntries[relPath];
|
|
2504
|
+
const targetRegex = /Target="([^"]+)"/gi;
|
|
2505
|
+
let tm;
|
|
2506
|
+
while ((tm = targetRegex.exec(relXml)) !== null) {
|
|
2507
|
+
const val = tm[1];
|
|
2508
|
+
if (/^(?:mailto:|tel:|https?:\/\/)/i.test(val) && !/schemas\.openxmlformats\.org|schemas\.microsoft\.com|w3\.org/i.test(val)) {
|
|
2509
|
+
extraXmlText += `\n[Document Link]: ${val}\n`;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
} catch (_) {}
|
|
2514
|
+
const content = (result.value || '') + (extraXmlText ? '\n' + extraXmlText : '');
|
|
1802
2515
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
1803
2516
|
const license = checkLicenseStatus();
|
|
1804
2517
|
let finalProfile = targetProfile;
|
|
@@ -1861,7 +2574,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1861
2574
|
const pdf = require('pdf-parse');
|
|
1862
2575
|
const data = await pdf(buffer);
|
|
1863
2576
|
const content = data.text;
|
|
1864
|
-
const targetProfile = profile.trim();
|
|
1865
2577
|
let finalProfile = targetProfile;
|
|
1866
2578
|
|
|
1867
2579
|
const sanitized = performSanitization(content, finalProfile, targetIgnoreList, targetSession);
|
|
@@ -1888,13 +2600,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1888
2600
|
const XLSX = require('xlsx');
|
|
1889
2601
|
const workbook = XLSX.read(buffer, { type: 'buffer' });
|
|
1890
2602
|
let content = '';
|
|
2603
|
+
if (workbook.Props && typeof workbook.Props === 'object') {
|
|
2604
|
+
const propFields = ['Author', 'LastAuthor', 'Company', 'Manager', 'Title', 'Subject', 'Comments'];
|
|
2605
|
+
for (const pf of propFields) {
|
|
2606
|
+
if (workbook.Props[pf] && String(workbook.Props[pf]).trim()) {
|
|
2607
|
+
content += `[Workbook ${pf}]: ${String(workbook.Props[pf]).trim()}\n`;
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (workbook.CustProps && typeof workbook.CustProps === 'object') {
|
|
2612
|
+
for (const [ck, cv] of Object.entries(workbook.CustProps)) {
|
|
2613
|
+
if (cv !== undefined && cv !== null && String(cv).trim()) {
|
|
2614
|
+
content += `[Custom Property ${ck}]: ${String(cv).trim()}\n`;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
1891
2618
|
workbook.SheetNames.forEach(sheetName => {
|
|
1892
2619
|
const worksheet = workbook.Sheets[sheetName];
|
|
1893
2620
|
content += `--- Sheet: ${sheetName} ---\n`;
|
|
1894
2621
|
content += XLSX.utils.sheet_to_csv(worksheet) + '\n';
|
|
2622
|
+
if (worksheet && typeof worksheet === 'object') {
|
|
2623
|
+
for (const cellKey of Object.keys(worksheet)) {
|
|
2624
|
+
if (cellKey.startsWith('!')) continue;
|
|
2625
|
+
const cell = worksheet[cellKey];
|
|
2626
|
+
if (!cell || typeof cell !== 'object') continue;
|
|
2627
|
+
if (cell.l && cell.l.Target) {
|
|
2628
|
+
content += `[Hyperlink ${cellKey}]: ${cell.l.Target}\n`;
|
|
2629
|
+
}
|
|
2630
|
+
if (Array.isArray(cell.c)) {
|
|
2631
|
+
for (const comm of cell.c) {
|
|
2632
|
+
const author = comm.a ? `${comm.a}: ` : '';
|
|
2633
|
+
const cText = (comm.t || '').trim();
|
|
2634
|
+
if (cText) {
|
|
2635
|
+
content += `[Comment ${cellKey}] ${author}${cText}\n`;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
1895
2641
|
});
|
|
1896
2642
|
|
|
1897
|
-
const targetProfile = profile.trim();
|
|
1898
2643
|
let finalProfile = targetProfile;
|
|
1899
2644
|
|
|
1900
2645
|
const sanitized = performSanitization(content, finalProfile, targetIgnoreList, targetSession);
|
|
@@ -1939,14 +2684,63 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1939
2684
|
};
|
|
1940
2685
|
}
|
|
1941
2686
|
|
|
1942
|
-
|
|
1943
|
-
const
|
|
2687
|
+
let content = buffer.toString("utf8");
|
|
2688
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
2689
|
+
if (ext === ".eml") {
|
|
2690
|
+
const decodeQpUtf8 = (str) =>
|
|
2691
|
+
str
|
|
2692
|
+
.replace(/=\r?\n/g, "")
|
|
2693
|
+
.replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2694
|
+
try {
|
|
2695
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2696
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
2697
|
+
bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2698
|
+
}
|
|
2699
|
+
return bytes.toString("utf8");
|
|
2700
|
+
} catch (_) {
|
|
2701
|
+
return seq;
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
content = content.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
|
|
2705
|
+
try {
|
|
2706
|
+
if (enc.toUpperCase() === "B") {
|
|
2707
|
+
return Buffer.from(payload.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
2708
|
+
}
|
|
2709
|
+
return decodeQpUtf8(payload.replace(/_/g, " "));
|
|
2710
|
+
} catch (_) {
|
|
2711
|
+
return full;
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
content = content.replace(
|
|
2715
|
+
/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi,
|
|
2716
|
+
(full, hdr, b64Block) => {
|
|
2717
|
+
try {
|
|
2718
|
+
const cleanB64 = b64Block.replace(/[\r\n\s]+/g, "");
|
|
2719
|
+
if (cleanB64.length < 8) return full;
|
|
2720
|
+
return hdr + Buffer.from(cleanB64, "base64").toString("utf8");
|
|
2721
|
+
} catch (_) {
|
|
2722
|
+
return full;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
);
|
|
2726
|
+
content = decodeQpUtf8(content);
|
|
2727
|
+
} else if (ext === ".rtf") {
|
|
2728
|
+
content = content
|
|
2729
|
+
.replace(/\\par\b|\\line\b/gi, "\n")
|
|
2730
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2731
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2732
|
+
const n = parseInt(code, 10);
|
|
2733
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2734
|
+
})
|
|
2735
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, " ")
|
|
2736
|
+
.replace(/[{}]/g, " ")
|
|
2737
|
+
.replace(/[ \t]+/g, " ");
|
|
2738
|
+
}
|
|
1944
2739
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
1945
2740
|
const license = checkLicenseStatus();
|
|
1946
2741
|
let finalProfile = targetProfile;
|
|
1947
2742
|
let isAutoElevated = false;
|
|
1948
2743
|
|
|
1949
|
-
const ext = path.extname(resolvedPath).toLowerCase();
|
|
1950
2744
|
const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.cs', '.sh', '.bash', '.zsh', '.env', '.yaml', '.yml', '.toml', '.sql']);
|
|
1951
2745
|
const DEV_HEURISTIC_REGEX = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{20,}|-----BEGIN (?:RSA )?PRIVATE KEY-----|Bearer\s+eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|(?:postgres|postgresql|mysql|mongodb|redis):\/\/[^\s"'>]+|export\s+(?:AWS_|DATABASE_|API_KEY|SECRET)|const\s+\w+\s*=\s*require\(|import\s+.*\s+from\s+['"])/i;
|
|
1952
2746
|
|
|
@@ -1983,7 +2777,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1983
2777
|
}
|
|
1984
2778
|
|
|
1985
2779
|
if (name === "redact_file") {
|
|
1986
|
-
const { file_path,
|
|
2780
|
+
const { file_path, dry_run, no_backup } = args;
|
|
1987
2781
|
if (!file_path || !fs.existsSync(file_path)) {
|
|
1988
2782
|
return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
|
|
1989
2783
|
}
|
|
@@ -1998,10 +2792,55 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1998
2792
|
if (buffer.indexOf(0) !== -1) {
|
|
1999
2793
|
return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
|
|
2000
2794
|
}
|
|
2001
|
-
|
|
2795
|
+
let content = buffer.toString('utf8');
|
|
2796
|
+
const ext = path.extname(file_path).toLowerCase();
|
|
2797
|
+
if (ext === ".eml") {
|
|
2798
|
+
const decodeQpUtf8 = (str) =>
|
|
2799
|
+
str.replace(/=\r?\n/g, "").replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
2800
|
+
try {
|
|
2801
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
2802
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2803
|
+
return bytes.toString("utf8");
|
|
2804
|
+
} catch (_) { return seq; }
|
|
2805
|
+
});
|
|
2806
|
+
content = content.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
|
|
2807
|
+
try {
|
|
2808
|
+
if (enc.toUpperCase() === "B") return Buffer.from(payload.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
2809
|
+
return decodeQpUtf8(payload.replace(/_/g, " "));
|
|
2810
|
+
} catch (_) { return full; }
|
|
2811
|
+
});
|
|
2812
|
+
content = content.replace(/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi, (full, hdr, b64Block) => {
|
|
2813
|
+
try {
|
|
2814
|
+
const cleanB64 = b64Block.replace(/[\r\n\s]+/g, "");
|
|
2815
|
+
if (cleanB64.length < 8) return full;
|
|
2816
|
+
return hdr + Buffer.from(cleanB64, "base64").toString("utf8");
|
|
2817
|
+
} catch (_) { return full; }
|
|
2818
|
+
});
|
|
2819
|
+
content = decodeQpUtf8(content);
|
|
2820
|
+
} else if (ext === ".rtf") {
|
|
2821
|
+
content = content
|
|
2822
|
+
.replace(/\\par\b|\\line\b/gi, "\n")
|
|
2823
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2824
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
2825
|
+
const n = parseInt(code, 10);
|
|
2826
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
2827
|
+
})
|
|
2828
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, " ")
|
|
2829
|
+
.replace(/[{}]/g, " ")
|
|
2830
|
+
.replace(/[ \t]+/g, " ");
|
|
2831
|
+
}
|
|
2002
2832
|
|
|
2003
2833
|
const customRules = loadCustomRules();
|
|
2004
|
-
const
|
|
2834
|
+
const activeRules = [...customRules];
|
|
2835
|
+
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
2836
|
+
activeRules.push({
|
|
2837
|
+
name: rule.name,
|
|
2838
|
+
pattern: rule.regex.source,
|
|
2839
|
+
regex: new RegExp(rule.regex.source, rule.regex.flags),
|
|
2840
|
+
label: rule.label || 'SECRET'
|
|
2841
|
+
});
|
|
2842
|
+
});
|
|
2843
|
+
const normalizedProfile = resolveRequestedProfile(args, "general").toLowerCase();
|
|
2005
2844
|
const license = checkLicenseStatus();
|
|
2006
2845
|
|
|
2007
2846
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
@@ -2013,7 +2852,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2013
2852
|
}
|
|
2014
2853
|
const localSessionMap = {};
|
|
2015
2854
|
|
|
2016
|
-
const result = PrivacyScrubberCore.scrubText(content,
|
|
2855
|
+
const result = PrivacyScrubberCore.scrubText(content, activeRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
2017
2856
|
|
|
2018
2857
|
let isSecretLeak = false;
|
|
2019
2858
|
if (!license.isPro && result.tokenMap) {
|
|
@@ -2128,8 +2967,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2128
2967
|
const tierIcon = license.isPro ? '✅' : '🔓';
|
|
2129
2968
|
const profileList = license.isPro
|
|
2130
2969
|
? 'All 30 specialized profiles active (General, Dev, Medical, Legal, Finance, HR…)'
|
|
2131
|
-
: '
|
|
2132
|
-
const sizeLimit = license.isPro ? 'Unlimited' : '15,000
|
|
2970
|
+
: 'All 30 profiles active (5k quota) · PRO unlocks unlimited';
|
|
2971
|
+
const sizeLimit = license.isPro ? 'Unlimited' : '15,000 char (General) / 5,000 char (Industry)';
|
|
2133
2972
|
|
|
2134
2973
|
const configPath = resolveConfigPath();
|
|
2135
2974
|
let rulesStatus = '';
|
|
@@ -2211,7 +3050,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2211
3050
|
}
|
|
2212
3051
|
|
|
2213
3052
|
if (name === "audit_directory_for_pii") {
|
|
2214
|
-
const { directory_path,
|
|
3053
|
+
const { directory_path, extensions, ignore_node_modules } = args;
|
|
2215
3054
|
if (!directory_path || !fs.existsSync(directory_path)) {
|
|
2216
3055
|
return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
|
|
2217
3056
|
}
|
|
@@ -2240,7 +3079,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2240
3079
|
let reportLines = [`## Security Audit Report: ${directory_path}`];
|
|
2241
3080
|
let filesWithIssues = 0;
|
|
2242
3081
|
const customRules = loadCustomRules();
|
|
2243
|
-
const
|
|
3082
|
+
const activeRules = [...customRules];
|
|
3083
|
+
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
3084
|
+
activeRules.push({
|
|
3085
|
+
name: rule.name,
|
|
3086
|
+
pattern: rule.regex.source,
|
|
3087
|
+
regex: new RegExp(rule.regex.source, rule.regex.flags),
|
|
3088
|
+
label: rule.label || 'SECRET'
|
|
3089
|
+
});
|
|
3090
|
+
});
|
|
3091
|
+
const normalizedProfile = resolveRequestedProfile(args, "general").toLowerCase();
|
|
2244
3092
|
const license = checkLicenseStatus();
|
|
2245
3093
|
|
|
2246
3094
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
@@ -2262,7 +3110,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2262
3110
|
const content = buffer.toString('utf8');
|
|
2263
3111
|
|
|
2264
3112
|
const localSessionMap = {};
|
|
2265
|
-
const result = PrivacyScrubberCore.scrubText(content,
|
|
3113
|
+
const result = PrivacyScrubberCore.scrubText(content, activeRules, {}, normalizedProfile, localSessionMap, license.isPro);
|
|
2266
3114
|
|
|
2267
3115
|
Object.assign(totalTokensForDirectory, localSessionMap);
|
|
2268
3116
|
|
|
@@ -2310,90 +3158,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2310
3158
|
}
|
|
2311
3159
|
|
|
2312
3160
|
if (name === "generate_compliance_report") {
|
|
2313
|
-
const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance" } = args || {};
|
|
2314
|
-
const
|
|
2315
|
-
|
|
2316
|
-
const sessionHash = crypto.createHash('sha256')
|
|
2317
|
-
.update(JSON.stringify(sessionMap) + Date.now().toString())
|
|
2318
|
-
.digest('hex');
|
|
3161
|
+
const { format = "markdown", company_name = "PrivacyScrubber Client", department = "SecOps / Compliance", session_id = "default" } = args || {};
|
|
3162
|
+
const targetSession = getSessionMap(session_id);
|
|
3163
|
+
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2319
3164
|
|
|
2320
|
-
const
|
|
2321
|
-
const entitySummary = Object.entries(telemetry.entities)
|
|
2322
|
-
.map(([t, count]) => `[${t}]: ${count}`)
|
|
2323
|
-
.join(', ') || 'None (Clean)';
|
|
2324
|
-
|
|
2325
|
-
if (format === "json") {
|
|
2326
|
-
const jsonReport = {
|
|
2327
|
-
protocol: "Zero-Trust Data Sanitization (ZTDS)",
|
|
2328
|
-
certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
|
|
2329
|
-
company: company_name,
|
|
2330
|
-
department: department,
|
|
2331
|
-
timestamp: timestamp,
|
|
2332
|
-
session_hash: sessionHash,
|
|
2333
|
-
verification_mode: "100% Offline (Local In-Memory RAM)",
|
|
2334
|
-
compliance_status: "VERIFIED PASS",
|
|
2335
|
-
risk_level: telemetry.riskLevel,
|
|
2336
|
-
frameworks_enforced: telemetry.frameworksList,
|
|
2337
|
-
total_masked_tokens: telemetry.totalCount,
|
|
2338
|
-
entities_breakdown: telemetry.entities,
|
|
2339
|
-
zero_egress_verified: true,
|
|
2340
|
-
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`,
|
|
2341
|
-
licensing_and_governance: {
|
|
2342
|
-
teams_governance_url: "https://privacyscrubber.com/teams?src=mcp_compliance_json",
|
|
2343
|
-
sdk_pipeline_url: "https://privacyscrubber.com/sdk/?src=mcp_compliance_json"
|
|
2344
|
-
}
|
|
2345
|
-
};
|
|
2346
|
-
|
|
2347
|
-
return {
|
|
2348
|
-
content: [{ type: "text", text: JSON.stringify(jsonReport, null, 2) }]
|
|
2349
|
-
};
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
if (format === "summary") {
|
|
2353
|
-
const summaryText = `[PrivacyScrubber Compliance Certificate] ID: ZTDS-${sessionHash.substring(0, 8).toUpperCase()} | Organization: ${company_name} | Masked Tokens: ${telemetry.totalCount} | Risk: ${telemetry.riskLevel} | Frameworks: ${telemetry.frameworksList.join(', ')} | Status: PASS (100% Air-Gapped)`;
|
|
2354
|
-
return {
|
|
2355
|
-
content: [{ type: "text", text: summaryText }]
|
|
2356
|
-
};
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// Default: Comprehensive Markdown Certificate
|
|
2360
|
-
const mdReport = `# 🛡️ Zero-Trust Data Sanitization Compliance Certificate
|
|
2361
|
-
**Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
|
|
2362
|
-
**Organization:** ${company_name} (${department})
|
|
2363
|
-
**Timestamp:** ${timestamp}
|
|
2364
|
-
**Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
|
|
2365
|
-
**Status:** **VERIFIED PASS** (Zero Network Egress)
|
|
2366
|
-
|
|
2367
|
-
---
|
|
2368
|
-
|
|
2369
|
-
### 📊 Sanitization Metrics & Risk Assessment
|
|
2370
|
-
* **Overall Risk Rating:** **${telemetry.riskLevel}**
|
|
2371
|
-
* **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
|
|
2372
|
-
* **Entity Breakdown:** ${entitySummary}
|
|
2373
|
-
* **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
|
|
2374
|
-
|
|
2375
|
-
### 📜 Regulatory Frameworks Enforced
|
|
2376
|
-
${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
2377
|
-
|
|
2378
|
-
### 🔒 CISO Compliance Declaration
|
|
2379
|
-
1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
|
|
2380
|
-
2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
|
|
2381
|
-
3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
|
|
2382
|
-
|
|
2383
|
-
### 🏢 Enterprise & Team Governance
|
|
2384
|
-
* **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules, audit logs, and zero-knowledge session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](https://privacyscrubber.com/teams?src=mcp_compliance_report)
|
|
2385
|
-
* **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?src=mcp_compliance_report)
|
|
2386
|
-
|
|
2387
|
-
*Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
2388
|
-
*Verify at: https://privacyscrubber.com/features/audit-receipt/* | ⭐ *Star on GitHub: https://github.com/moxno/privacyscrubber-mcp*`;
|
|
2389
|
-
|
|
3165
|
+
const receipt = buildPsr2Receipt(targetSession);
|
|
2390
3166
|
return {
|
|
2391
|
-
content: [{ type: "text", text:
|
|
3167
|
+
content: [{ type: "text", text: formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_compliance_report', utmMedium: format === 'json' ? 'json' : 'markdown' }) }]
|
|
2392
3168
|
};
|
|
2393
3169
|
}
|
|
2394
3170
|
|
|
2395
3171
|
if (name === "guard_exec") {
|
|
2396
|
-
const { command, cwd,
|
|
3172
|
+
const { command, cwd, timeout_ms = 15000, session_id = "default" } = args || {};
|
|
2397
3173
|
if (!command || typeof command !== "string") {
|
|
2398
3174
|
return {
|
|
2399
3175
|
isError: true,
|
|
@@ -2433,7 +3209,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
2433
3209
|
exitCode = typeof err.status === 'number' ? err.status : 1;
|
|
2434
3210
|
}
|
|
2435
3211
|
|
|
2436
|
-
const targetProfile = (
|
|
3212
|
+
const targetProfile = resolveRequestedProfile(args, "Dev");
|
|
2437
3213
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2438
3214
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
2439
3215
|
|
|
@@ -2463,7 +3239,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
2463
3239
|
}
|
|
2464
3240
|
|
|
2465
3241
|
if (name === "guard_read_file") {
|
|
2466
|
-
const { file_path,
|
|
3242
|
+
const { file_path, max_lines = 500, session_id = "default" } = args || {};
|
|
2467
3243
|
if (!file_path || typeof file_path !== "string") {
|
|
2468
3244
|
return {
|
|
2469
3245
|
isError: true,
|
|
@@ -2484,6 +3260,22 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
2484
3260
|
content: [{ type: "text", text: `Error: Path is a directory. Use audit_directory_for_pii to scan directories.` }]
|
|
2485
3261
|
};
|
|
2486
3262
|
}
|
|
3263
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
3264
|
+
return {
|
|
3265
|
+
isError: true,
|
|
3266
|
+
content: [{ type: "text", text: `Error: File is too large (${(stat.size / 1024 / 1024).toFixed(2)}MB). Maximum allowed size is 10MB.` }]
|
|
3267
|
+
};
|
|
3268
|
+
}
|
|
3269
|
+
const rawBuffer = fs.readFileSync(resolvedPath);
|
|
3270
|
+
const checkLen = Math.min(rawBuffer.length, 8000);
|
|
3271
|
+
for (let i = 0; i < checkLen; i++) {
|
|
3272
|
+
if (rawBuffer[i] === 0) {
|
|
3273
|
+
return {
|
|
3274
|
+
isError: true,
|
|
3275
|
+
content: [{ type: "text", text: `Error: Binary file format detected. Use sanitize_file for DOCX/PDF/XLSX documents.` }]
|
|
3276
|
+
};
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
2487
3279
|
const license = checkLicenseStatus();
|
|
2488
3280
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
2489
3281
|
if (limitStatus.blocked) {
|
|
@@ -2493,12 +3285,48 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
2493
3285
|
};
|
|
2494
3286
|
}
|
|
2495
3287
|
|
|
2496
|
-
let rawContent =
|
|
3288
|
+
let rawContent = rawBuffer.toString('utf8');
|
|
3289
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
3290
|
+
if (ext === '.eml') {
|
|
3291
|
+
const decodeQpUtf8 = (str) =>
|
|
3292
|
+
str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
3293
|
+
try {
|
|
3294
|
+
const bytes = Buffer.alloc(seq.length / 3);
|
|
3295
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
3296
|
+
return bytes.toString('utf8');
|
|
3297
|
+
} catch (_) { return seq; }
|
|
3298
|
+
});
|
|
3299
|
+
rawContent = rawContent.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, charset, enc, payload) => {
|
|
3300
|
+
try {
|
|
3301
|
+
if (enc.toUpperCase() === 'B') return Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8');
|
|
3302
|
+
return decodeQpUtf8(payload.replace(/_/g, ' '));
|
|
3303
|
+
} catch (_) { return full; }
|
|
3304
|
+
});
|
|
3305
|
+
rawContent = rawContent.replace(/(Content-Transfer-Encoding\s*:\s*base64\b[\s\S]*?\r?\n\r?\n)([A-Za-z0-9+/=\r\n]+)(?=\r?\n--|\s*$)/gi, (full, hdr, b64Block) => {
|
|
3306
|
+
try {
|
|
3307
|
+
const cleanB64 = b64Block.replace(/[\r\n\s]+/g, '');
|
|
3308
|
+
if (cleanB64.length < 8) return full;
|
|
3309
|
+
return hdr + Buffer.from(cleanB64, 'base64').toString('utf8');
|
|
3310
|
+
} catch (_) { return full; }
|
|
3311
|
+
});
|
|
3312
|
+
rawContent = decodeQpUtf8(rawContent);
|
|
3313
|
+
} else if (ext === '.rtf') {
|
|
3314
|
+
rawContent = rawContent
|
|
3315
|
+
.replace(/\\par\b|\\line\b/gi, '\n')
|
|
3316
|
+
.replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
3317
|
+
.replace(/\\u(-?\d+)\??/g, (_, code) => {
|
|
3318
|
+
const n = parseInt(code, 10);
|
|
3319
|
+
return String.fromCharCode(n < 0 ? n + 65536 : n);
|
|
3320
|
+
})
|
|
3321
|
+
.replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
|
|
3322
|
+
.replace(/[{}]/g, ' ')
|
|
3323
|
+
.replace(/[ \t]+/g, ' ');
|
|
3324
|
+
}
|
|
2497
3325
|
const lines = rawContent.split(/\r?\n/);
|
|
2498
3326
|
const capped = lines.slice(0, max_lines).join('\n');
|
|
2499
3327
|
const wasLineTruncated = lines.length > max_lines;
|
|
2500
3328
|
|
|
2501
|
-
const targetProfile = (
|
|
3329
|
+
const targetProfile = resolveRequestedProfile(args, "Dev");
|
|
2502
3330
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2503
3331
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
2504
3332
|
|
|
@@ -2524,7 +3352,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
2524
3352
|
}
|
|
2525
3353
|
|
|
2526
3354
|
if (name === "guard_git_diff") {
|
|
2527
|
-
const { staged = false, cwd,
|
|
3355
|
+
const { staged = false, cwd, session_id = "default" } = args || {};
|
|
2528
3356
|
const targetSession = getSessionMap(session_id);
|
|
2529
3357
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
2530
3358
|
const repoCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
@@ -2558,7 +3386,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
2558
3386
|
};
|
|
2559
3387
|
}
|
|
2560
3388
|
|
|
2561
|
-
const targetProfile = (
|
|
3389
|
+
const targetProfile = resolveRequestedProfile(args, "Dev");
|
|
2562
3390
|
const isAdvanced = targetProfile.toLowerCase() !== "general";
|
|
2563
3391
|
const charLimit = (isAdvanced && !license.isPro) ? 5000 : 15000;
|
|
2564
3392
|
|
|
@@ -2767,6 +3595,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2767
3595
|
}
|
|
2768
3596
|
const tokenEntries = Object.entries(session.map);
|
|
2769
3597
|
const telemetry = buildCisoAuditTelemetry(session.map);
|
|
3598
|
+
const streamBufferLen = session.streamDetokenizer ? session.streamDetokenizer.buffer.length : 0;
|
|
2770
3599
|
return {
|
|
2771
3600
|
content: [{
|
|
2772
3601
|
type: "text",
|
|
@@ -2774,6 +3603,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2774
3603
|
`Created: ${new Date(session.createdAt).toISOString()}\n` +
|
|
2775
3604
|
`Last Active: ${new Date(session.lastActive).toISOString()}\n` +
|
|
2776
3605
|
`Total Tokens Stored in RAM: ${tokenEntries.length}\n` +
|
|
3606
|
+
`Stream Buffer: ${streamBufferLen} bytes pending\n` +
|
|
2777
3607
|
`Entity Types: ${Object.keys(telemetry.entities).join(', ') || 'None'}\n` +
|
|
2778
3608
|
`Risk Level: ${telemetry.riskLevel}\n` +
|
|
2779
3609
|
`Status: Volatile in-memory mapping active (0 bytes disk/network).`
|
|
@@ -2786,7 +3616,8 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2786
3616
|
for (const [sid, sess] of agentSessions.entries()) {
|
|
2787
3617
|
const count = Object.keys(sess.map).length;
|
|
2788
3618
|
totalTokens += count;
|
|
2789
|
-
|
|
3619
|
+
const streamBufferLen = sess.streamDetokenizer ? sess.streamDetokenizer.buffer.length : 0;
|
|
3620
|
+
sessionList.push({ id: sid, tokens: count, streamBufferBytes: streamBufferLen, lastActive: new Date(sess.lastActive).toISOString() });
|
|
2790
3621
|
}
|
|
2791
3622
|
|
|
2792
3623
|
return {
|
|
@@ -2797,7 +3628,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2797
3628
|
`Total Tokens in Memory: ${totalTokens}\n` +
|
|
2798
3629
|
`Process Heap Used: ${(memUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB\n` +
|
|
2799
3630
|
`Uptime: ${Math.floor(process.uptime())} seconds\n` +
|
|
2800
|
-
`Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens)`).join(', ') || 'None'}`
|
|
3631
|
+
`Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens, ${s.streamBufferBytes} buf)`).join(', ') || 'None'}`
|
|
2801
3632
|
}]
|
|
2802
3633
|
};
|
|
2803
3634
|
}
|
|
@@ -2806,6 +3637,11 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2806
3637
|
const { session_id, all = false, reset_all = false } = args || {};
|
|
2807
3638
|
if (all || reset_all) {
|
|
2808
3639
|
const count = agentSessions.size;
|
|
3640
|
+
for (const sess of agentSessions.values()) {
|
|
3641
|
+
if (sess.streamDetokenizer) {
|
|
3642
|
+
sess.streamDetokenizer.reset();
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
2809
3645
|
agentSessions.clear();
|
|
2810
3646
|
getSessionMap('default');
|
|
2811
3647
|
sessionIgnoreList.clear();
|
|
@@ -2818,6 +3654,10 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2818
3654
|
}
|
|
2819
3655
|
|
|
2820
3656
|
const sid = (session_id || 'default').toString().trim();
|
|
3657
|
+
const sess = agentSessions.get(sid);
|
|
3658
|
+
if (sess && sess.streamDetokenizer) {
|
|
3659
|
+
sess.streamDetokenizer.reset();
|
|
3660
|
+
}
|
|
2821
3661
|
const existed = agentSessions.delete(sid);
|
|
2822
3662
|
if (sid === 'default') {
|
|
2823
3663
|
getSessionMap('default');
|
|
@@ -2833,7 +3673,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2833
3673
|
}
|
|
2834
3674
|
|
|
2835
3675
|
if (name === "guard_rag_chunk") {
|
|
2836
|
-
const { chunks,
|
|
3676
|
+
const { chunks, session_id = "default" } = args || {};
|
|
2837
3677
|
if (!Array.isArray(chunks)) {
|
|
2838
3678
|
return {
|
|
2839
3679
|
isError: true,
|
|
@@ -2841,6 +3681,7 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2841
3681
|
};
|
|
2842
3682
|
}
|
|
2843
3683
|
|
|
3684
|
+
const targetProfile = resolveRequestedProfile(args, "General");
|
|
2844
3685
|
const targetSession = getSessionMap(session_id);
|
|
2845
3686
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
2846
3687
|
let totalTokensRedacted = 0;
|
|
@@ -2849,13 +3690,13 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2849
3690
|
for (let i = 0; i < chunks.length; i++) {
|
|
2850
3691
|
const item = chunks[i];
|
|
2851
3692
|
if (typeof item === 'string') {
|
|
2852
|
-
const res = performSanitization(item,
|
|
3693
|
+
const res = performSanitization(item, targetProfile, targetIgnoreList, targetSession);
|
|
2853
3694
|
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2854
3695
|
sanitizedChunks.push(res.scrubbedText);
|
|
2855
3696
|
} else if (item && typeof item === 'object') {
|
|
2856
3697
|
const copy = { ...item };
|
|
2857
3698
|
const rawText = copy.text || copy.pageContent || copy.document || '';
|
|
2858
|
-
const res = performSanitization(rawText,
|
|
3699
|
+
const res = performSanitization(rawText, targetProfile, targetIgnoreList, targetSession);
|
|
2859
3700
|
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2860
3701
|
|
|
2861
3702
|
if (copy.text !== undefined) copy.text = res.scrubbedText;
|
|
@@ -2971,11 +3812,9 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2971
3812
|
|
|
2972
3813
|
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2973
3814
|
|
|
2974
|
-
const sessionHash = crypto.createHash('sha256')
|
|
2975
|
-
.update(JSON.stringify(targetSession) + Date.now().toString())
|
|
2976
|
-
.digest('hex');
|
|
2977
|
-
|
|
2978
3815
|
const timestamp = new Date().toISOString();
|
|
3816
|
+
const receipt = buildPsr2Receipt(targetSession, timestamp);
|
|
3817
|
+
const sessionHash = receipt.receiptSha256;
|
|
2979
3818
|
|
|
2980
3819
|
let outputText = "";
|
|
2981
3820
|
const eventData = {
|
|
@@ -2996,60 +3835,8 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
2996
3835
|
outputText = formatCefEvent(eventData);
|
|
2997
3836
|
} else if (format === "syslog") {
|
|
2998
3837
|
outputText = formatSyslogEvent(eventData);
|
|
2999
|
-
} else if (format === "json") {
|
|
3000
|
-
outputText =
|
|
3001
|
-
protocol: "Zero-Trust Data Sanitization (ZTDS)",
|
|
3002
|
-
certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
|
|
3003
|
-
company: company_name,
|
|
3004
|
-
department,
|
|
3005
|
-
timestamp,
|
|
3006
|
-
session_hash: sessionHash,
|
|
3007
|
-
verification_mode: "100% Offline (Local In-Memory RAM)",
|
|
3008
|
-
compliance_status: "VERIFIED PASS",
|
|
3009
|
-
risk_level: telemetry.riskLevel,
|
|
3010
|
-
frameworks_enforced: telemetry.frameworksList,
|
|
3011
|
-
total_masked_tokens: telemetry.totalCount,
|
|
3012
|
-
entities_breakdown: telemetry.entities,
|
|
3013
|
-
zero_egress_verified: true,
|
|
3014
|
-
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`,
|
|
3015
|
-
licensing_and_governance: {
|
|
3016
|
-
teams_governance_url: "https://privacyscrubber.com/teams?tier=teams&utm_source=mcp_audit_export&utm_medium=json&utm_campaign=teams_upgrade",
|
|
3017
|
-
sdk_pipeline_url: "https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp_audit_export&utm_medium=json&utm_campaign=sdk_pipeline"
|
|
3018
|
-
}
|
|
3019
|
-
}, null, 2);
|
|
3020
|
-
} else if (format === "markdown") {
|
|
3021
|
-
const entitySummary = Object.entries(telemetry.entities)
|
|
3022
|
-
.map(([t, count]) => `[${t}]: ${count}`)
|
|
3023
|
-
.join(', ') || 'None (Clean)';
|
|
3024
|
-
outputText = `# 🛡️ Zero-Trust Data Sanitization Compliance Certificate
|
|
3025
|
-
**Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
|
|
3026
|
-
**Organization:** ${company_name} (${department})
|
|
3027
|
-
**Timestamp:** ${timestamp}
|
|
3028
|
-
**Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
|
|
3029
|
-
**Status:** **VERIFIED PASS** (Zero Network Egress)
|
|
3030
|
-
|
|
3031
|
-
---
|
|
3032
|
-
|
|
3033
|
-
### 📊 Sanitization Metrics & Risk Assessment
|
|
3034
|
-
* **Overall Risk Rating:** **${telemetry.riskLevel}**
|
|
3035
|
-
* **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
|
|
3036
|
-
* **Entity Breakdown:** ${entitySummary}
|
|
3037
|
-
* **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
|
|
3038
|
-
|
|
3039
|
-
### 📜 Regulatory Frameworks Enforced
|
|
3040
|
-
${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
3041
|
-
|
|
3042
|
-
### 🔒 CISO Compliance Declaration
|
|
3043
|
-
1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
|
|
3044
|
-
2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
|
|
3045
|
-
3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
|
|
3046
|
-
|
|
3047
|
-
### 🏢 Enterprise & Team Governance
|
|
3048
|
-
* **Team Governance & Encrypted Handoff:** Centrally deploy redaction rules, audit logs, and zero-knowledge session handoff with **PrivacyScrubber TEAMS** ($99/mo flat): [privacyscrubber.com/teams](https://privacyscrubber.com/teams?tier=teams&utm_source=mcp_audit_export&utm_medium=markdown&utm_campaign=teams_upgrade)
|
|
3049
|
-
* **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/sdk](https://privacyscrubber.com/sdk/?tier=sdk&utm_source=mcp_audit_export&utm_medium=markdown&utm_campaign=sdk_pipeline)
|
|
3050
|
-
|
|
3051
|
-
*Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
3052
|
-
*Verify at: https://privacyscrubber.com/features/audit-receipt/* | ⭐ *Star on GitHub: https://github.com/moxno/privacyscrubber-mcp*`;
|
|
3838
|
+
} else if (format === "json" || format === "markdown") {
|
|
3839
|
+
outputText = formatPsr2Report({ format, receipt, telemetry, company_name, department, utmSource: 'mcp_audit_export', utmMedium: format });
|
|
3053
3840
|
} else {
|
|
3054
3841
|
// Default: jsonl
|
|
3055
3842
|
outputText = formatJsonlEvent(eventData);
|
|
@@ -3198,7 +3985,7 @@ function detectSecrets(text) {
|
|
|
3198
3985
|
return detected;
|
|
3199
3986
|
}
|
|
3200
3987
|
|
|
3201
|
-
function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
|
|
3988
|
+
export function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
|
|
3202
3989
|
const customRules = loadCustomRules();
|
|
3203
3990
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
3204
3991
|
const license = checkLicenseStatus();
|
|
@@ -3226,7 +4013,7 @@ function performSanitization(text, profile, ignoreList = null, customSessionMap
|
|
|
3226
4013
|
return { scrubbedText: result.scrubbedText, newTokens };
|
|
3227
4014
|
}
|
|
3228
4015
|
|
|
3229
|
-
function truncateIfFree(text, isPro, charLimit = 15000) {
|
|
4016
|
+
export function truncateIfFree(text, isPro, charLimit = 15000) {
|
|
3230
4017
|
if (!isPro && text.length > charLimit) {
|
|
3231
4018
|
mcpLog(`${colors.yellowBold}[PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}[Upgrade] Set PRIVACYSCRUBBER_KEY to your PRO/TEAMS/SDK license key for unlimited throughput: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade${colors.reset}\n`);
|
|
3232
4019
|
const upsellNotice = `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo) for unlimited payload processing: https://privacyscrubber.com/pricing?tier=teams&utm_source=mcp&utm_medium=truncation&utm_campaign=teams_upgrade]`;
|