@privacyscrubber/mcp-server 2.2.3 → 2.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -5
- package/index.js +855 -26
- package/install.js +294 -41
- package/manifest.json +17 -1
- package/package.json +6 -2
- package/pii-masking-run.js +3 -3
- package/ps-license-manager.cjs +1 -1
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/ps-pii-engine.js +1 -1
- package/scrubber-core.cjs +1 -1
- package/server.json +3 -3
- package/smithery.yaml +2 -2
package/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import path from 'path';
|
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
25
25
|
|
|
26
26
|
import crypto from 'crypto';
|
|
27
|
+
import os from 'os';
|
|
27
28
|
import { execSync } from 'child_process';
|
|
28
29
|
|
|
29
30
|
const require = createRequire(import.meta.url);
|
|
@@ -35,6 +36,15 @@ const __dirname = path.dirname(__filename);
|
|
|
35
36
|
// Never hardcode the version — bump package.json at root instead.
|
|
36
37
|
const MCP_VERSION = require('./package.json').version;
|
|
37
38
|
|
|
39
|
+
// CLI Command Intercept (init / install / setup / auto-configure)
|
|
40
|
+
const rawCliArgs = process.argv.slice(2);
|
|
41
|
+
if (rawCliArgs.some(arg => ['init', 'install', '--install', 'setup', '--setup'].includes(arg))) {
|
|
42
|
+
const { runInstaller } = await import('./install.js');
|
|
43
|
+
const forwardedArgs = rawCliArgs.filter(a => !['init', 'install', '--install', 'setup', '--setup'].includes(a));
|
|
44
|
+
await runInstaller(forwardedArgs);
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
// Import the production core engine with 100% parity
|
|
39
49
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
40
50
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
@@ -53,8 +63,39 @@ if (!LicenseManager || typeof LicenseManager.validate !== 'function') {
|
|
|
53
63
|
// Initialize core engine
|
|
54
64
|
PrivacyScrubberCore.init();
|
|
55
65
|
|
|
56
|
-
// Volatile in-memory token map
|
|
57
|
-
const
|
|
66
|
+
// Volatile in-memory token map per session (multi-task agent isolation)
|
|
67
|
+
const agentSessions = new Map();
|
|
68
|
+
const MAX_CONCURRENT_SESSIONS = 200;
|
|
69
|
+
|
|
70
|
+
function getSessionMap(sessionId = 'default') {
|
|
71
|
+
const sid = (sessionId || 'default').toString().trim() || 'default';
|
|
72
|
+
let session = agentSessions.get(sid);
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (!session) {
|
|
75
|
+
if (agentSessions.size >= MAX_CONCURRENT_SESSIONS) {
|
|
76
|
+
const oldestKey = agentSessions.keys().next().value;
|
|
77
|
+
agentSessions.delete(oldestKey);
|
|
78
|
+
}
|
|
79
|
+
session = { map: {}, createdAt: now, lastActive: now };
|
|
80
|
+
agentSessions.set(sid, session);
|
|
81
|
+
}
|
|
82
|
+
session.lastActive = now;
|
|
83
|
+
return session.map;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Global default sessionMap maintains 100% backward compatibility
|
|
87
|
+
const sessionMap = getSessionMap('default');
|
|
88
|
+
|
|
89
|
+
function reverseTokens(text, map) {
|
|
90
|
+
if (!text || typeof text !== 'string' || !map) return text;
|
|
91
|
+
let result = text;
|
|
92
|
+
for (const [token, original] of Object.entries(map)) {
|
|
93
|
+
if (result.includes(token)) {
|
|
94
|
+
result = result.replaceAll(token, original);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
58
99
|
|
|
59
100
|
// Volatile in-memory false positive ignore list (values excluded from future scrubs)
|
|
60
101
|
const sessionIgnoreList = new Set();
|
|
@@ -230,7 +271,7 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
230
271
|
function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, charLimit = 15000) {
|
|
231
272
|
const isCompact = compact === true || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === '1' || process.env.PRIVACYSCRUBBER_COMPACT_RECEIPT === 'true';
|
|
232
273
|
const truncationLine = wasTruncated
|
|
233
|
-
? `> * ⚠️ **Free Tier Limit:** Input truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($
|
|
274
|
+
? `> * ⚠️ **Free Tier Limit:** Input truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo): [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing)\n`
|
|
234
275
|
: '';
|
|
235
276
|
|
|
236
277
|
if (isCompact) {
|
|
@@ -259,6 +300,114 @@ function formatAuditReceipt(telemetry, compact = false, wasTruncated = false, ch
|
|
|
259
300
|
return `\n\n> 🛡️ **PrivacyScrubber Audit Receipt**\n> * **Risk Level:** ${icon} ${telemetry.riskLevel}\n> * **Compliance Enforced:** ${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/pricing](https://privacyscrubber.com/pricing)\n`;
|
|
260
301
|
}
|
|
261
302
|
|
|
303
|
+
function formatCefEvent(eventData = {}) {
|
|
304
|
+
const {
|
|
305
|
+
vendor = 'PrivacyScrubber',
|
|
306
|
+
product = 'ZTDS',
|
|
307
|
+
version = MCP_VERSION,
|
|
308
|
+
eventClassId = 'PII_REDACTION',
|
|
309
|
+
eventName = 'Zero-Trust PII Sanitization Executed',
|
|
310
|
+
riskLevel = 'LOW EXPOSURE',
|
|
311
|
+
tokensMasked = 0,
|
|
312
|
+
entities = {},
|
|
313
|
+
frameworks = ['ZTDS Standard'],
|
|
314
|
+
sessionHash = '',
|
|
315
|
+
source = 'local_ram',
|
|
316
|
+
action = 'redact_and_mask',
|
|
317
|
+
organization = '',
|
|
318
|
+
department = ''
|
|
319
|
+
} = eventData;
|
|
320
|
+
|
|
321
|
+
let severity = 1;
|
|
322
|
+
if (riskLevel.includes('CRITICAL')) severity = 9;
|
|
323
|
+
else if (riskLevel.includes('MODERATE')) severity = 6;
|
|
324
|
+
else if (riskLevel.includes('LOW')) severity = 3;
|
|
325
|
+
else severity = 0;
|
|
326
|
+
|
|
327
|
+
const entitiesStr = Object.entries(entities).map(([k, v]) => `${k}:${v}`).join(',') || 'none';
|
|
328
|
+
const frameworksStr = Array.isArray(frameworks) ? frameworks.join(',') : String(frameworks);
|
|
329
|
+
const safeMsg = `Sanitized ${tokensMasked} entity tokens in volatile RAM. 0 bytes network egress.`;
|
|
330
|
+
|
|
331
|
+
const extension = [
|
|
332
|
+
`src=${source}`,
|
|
333
|
+
`act=${action}`,
|
|
334
|
+
`cs1Label=RiskLevel cs1=${riskLevel.replace(/\s+/g, '_')}`,
|
|
335
|
+
`cn1Label=TokensMasked cn1=${tokensMasked}`,
|
|
336
|
+
`cs2Label=Frameworks cs2=${frameworksStr.replace(/\s+/g, '_')}`,
|
|
337
|
+
`cs3Label=Entities cs3=${entitiesStr}`,
|
|
338
|
+
sessionHash ? `cs4Label=SessionHash cs4=${sessionHash.substring(0, 16)}` : '',
|
|
339
|
+
organization ? `suser=${organization.replace(/\s+/g, '_')}` : '',
|
|
340
|
+
department ? `cs5Label=Department cs5=${department.replace(/\s+/g, '_')}` : '',
|
|
341
|
+
`msg=${safeMsg}`
|
|
342
|
+
].filter(Boolean).join(' ');
|
|
343
|
+
|
|
344
|
+
return `CEF:0|${vendor}|${product}|${version}|${eventClassId}|${eventName}|${severity}|${extension}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function formatSyslogEvent(eventData = {}) {
|
|
348
|
+
const {
|
|
349
|
+
timestamp = new Date().toISOString(),
|
|
350
|
+
hostname = (typeof os !== 'undefined' && typeof os.hostname === 'function' ? os.hostname() : 'localhost'),
|
|
351
|
+
pid = (typeof process !== 'undefined' && process.pid ? process.pid : 1),
|
|
352
|
+
riskLevel = 'LOW EXPOSURE',
|
|
353
|
+
tokensMasked = 0,
|
|
354
|
+
frameworks = ['ZTDS Standard'],
|
|
355
|
+
sessionHash = '',
|
|
356
|
+
organization = ''
|
|
357
|
+
} = eventData;
|
|
358
|
+
|
|
359
|
+
let pri = 134; // local0.info
|
|
360
|
+
if (riskLevel.includes('CRITICAL')) pri = 132; // local0.warning
|
|
361
|
+
else if (riskLevel.includes('MODERATE')) pri = 133; // local0.notice
|
|
362
|
+
|
|
363
|
+
const frameworksStr = Array.isArray(frameworks) ? frameworks.join(';') : String(frameworks);
|
|
364
|
+
const hashPrefix = sessionHash.substring(0, 16) || 'none';
|
|
365
|
+
const orgStr = organization ? ` org="${organization.replace(/"/g, '')}"` : '';
|
|
366
|
+
|
|
367
|
+
const sd = `[ztds@49152 risk="${riskLevel}" tokens="${tokensMasked}" frameworks="${frameworksStr}" session="${hashPrefix}"${orgStr}]`;
|
|
368
|
+
const msg = `Zero-Trust Data Sanitization executed in RAM. ${tokensMasked} token(s) masked. 0 bytes network egress.`;
|
|
369
|
+
|
|
370
|
+
return `<${pri}>1 ${timestamp} ${hostname} privacyscrubber ${pid} ZTDS - ${sd} ${msg}`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function formatJsonlEvent(eventData = {}) {
|
|
374
|
+
const {
|
|
375
|
+
timestamp = new Date().toISOString(),
|
|
376
|
+
version = MCP_VERSION,
|
|
377
|
+
protocol = 'ZTDS',
|
|
378
|
+
event = 'sanitization',
|
|
379
|
+
riskLevel = 'LOW EXPOSURE',
|
|
380
|
+
tokensMasked = 0,
|
|
381
|
+
entities = {},
|
|
382
|
+
frameworks = ['ZTDS Standard'],
|
|
383
|
+
sessionHash = '',
|
|
384
|
+
organization = '',
|
|
385
|
+
department = '',
|
|
386
|
+
executionMs = 0,
|
|
387
|
+
metadata = {}
|
|
388
|
+
} = eventData;
|
|
389
|
+
|
|
390
|
+
const record = {
|
|
391
|
+
timestamp,
|
|
392
|
+
version,
|
|
393
|
+
protocol,
|
|
394
|
+
event,
|
|
395
|
+
risk_level: riskLevel,
|
|
396
|
+
tokens_masked: tokensMasked,
|
|
397
|
+
entities,
|
|
398
|
+
frameworks,
|
|
399
|
+
session_hash: sessionHash,
|
|
400
|
+
zero_egress: true,
|
|
401
|
+
verification_mode: "100% Offline (In-Memory RAM)",
|
|
402
|
+
...(organization && { organization }),
|
|
403
|
+
...(department && { department }),
|
|
404
|
+
...(executionMs && { execution_ms: executionMs }),
|
|
405
|
+
...(metadata && Object.keys(metadata).length > 0 && { metadata })
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
return JSON.stringify(record);
|
|
409
|
+
}
|
|
410
|
+
|
|
262
411
|
// Create the MCP server
|
|
263
412
|
const server = new Server(
|
|
264
413
|
{
|
|
@@ -279,6 +428,29 @@ const server = new Server(
|
|
|
279
428
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
280
429
|
return {
|
|
281
430
|
tools: [
|
|
431
|
+
{
|
|
432
|
+
name: "audit_context",
|
|
433
|
+
description: "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity (CLEAN, LOW, MODERATE, CRITICAL), and triggered regulatory frameworks (GDPR, HIPAA, SOC 2, PCI DSS) with zero text mutation.",
|
|
434
|
+
inputSchema: {
|
|
435
|
+
type: "object",
|
|
436
|
+
properties: {
|
|
437
|
+
text: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "The raw text, code, prompt, or logs to audit."
|
|
440
|
+
},
|
|
441
|
+
profile: {
|
|
442
|
+
type: "string",
|
|
443
|
+
description: "The detection profile to use (e.g. 'General', 'Dev', 'Medical', 'Finance', 'Legal'). Defaults to 'General'."
|
|
444
|
+
},
|
|
445
|
+
format: {
|
|
446
|
+
type: "string",
|
|
447
|
+
description: "Output format: 'detailed' (default markdown), 'summary', or 'json'.",
|
|
448
|
+
enum: ["detailed", "summary", "json"]
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
required: ["text"]
|
|
452
|
+
}
|
|
453
|
+
},
|
|
282
454
|
{
|
|
283
455
|
name: "sanitize_text",
|
|
284
456
|
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)",
|
|
@@ -306,6 +478,28 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
306
478
|
required: ["text"]
|
|
307
479
|
}
|
|
308
480
|
},
|
|
481
|
+
{
|
|
482
|
+
name: "scrub_text",
|
|
483
|
+
description: "Alias for 'sanitize_text'. Locally scrubs PII and secrets before LLM ingestion.",
|
|
484
|
+
inputSchema: {
|
|
485
|
+
type: "object",
|
|
486
|
+
properties: {
|
|
487
|
+
text: {
|
|
488
|
+
type: "string",
|
|
489
|
+
description: "The raw text, code, or logs to sanitize."
|
|
490
|
+
},
|
|
491
|
+
profile: {
|
|
492
|
+
type: "string",
|
|
493
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
494
|
+
},
|
|
495
|
+
compact: {
|
|
496
|
+
type: "boolean",
|
|
497
|
+
description: "Optional. Compact 1-line audit summary."
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
required: ["text"]
|
|
501
|
+
}
|
|
502
|
+
},
|
|
309
503
|
{
|
|
310
504
|
name: "reveal_text",
|
|
311
505
|
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.",
|
|
@@ -332,11 +526,33 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
332
526
|
},
|
|
333
527
|
profile: {
|
|
334
528
|
type: "string",
|
|
335
|
-
description: "The detection profile to use.
|
|
529
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
336
530
|
},
|
|
337
531
|
compact: {
|
|
338
532
|
type: "boolean",
|
|
339
|
-
description: "Optional.
|
|
533
|
+
description: "Optional. Compact 1-line audit summary."
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
required: ["file_path"]
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "scrub_file",
|
|
541
|
+
description: "Alias for 'sanitize_file'. Reads and sanitizes a local file.",
|
|
542
|
+
inputSchema: {
|
|
543
|
+
type: "object",
|
|
544
|
+
properties: {
|
|
545
|
+
file_path: {
|
|
546
|
+
type: "string",
|
|
547
|
+
description: "Absolute path to the file to sanitize."
|
|
548
|
+
},
|
|
549
|
+
profile: {
|
|
550
|
+
type: "string",
|
|
551
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
552
|
+
},
|
|
553
|
+
compact: {
|
|
554
|
+
type: "boolean",
|
|
555
|
+
description: "Optional. Compact 1-line audit summary."
|
|
340
556
|
}
|
|
341
557
|
},
|
|
342
558
|
required: ["file_path"]
|
|
@@ -563,6 +779,158 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
563
779
|
},
|
|
564
780
|
required: []
|
|
565
781
|
}
|
|
782
|
+
},
|
|
783
|
+
{
|
|
784
|
+
name: "guard_unmask_args",
|
|
785
|
+
description: "Zero-Trust Agentic Guard: Detokenizes parameters, JSON objects, or arguments in local RAM before dispatching to local tools or APIs without sending cleartext secrets back to the LLM.",
|
|
786
|
+
inputSchema: {
|
|
787
|
+
type: "object",
|
|
788
|
+
properties: {
|
|
789
|
+
tool_args: {
|
|
790
|
+
description: "Arbitrary JSON object, array, or string containing token placeholders to detokenize in local RAM."
|
|
791
|
+
},
|
|
792
|
+
session_id: {
|
|
793
|
+
type: "string",
|
|
794
|
+
description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
required: ["tool_args"]
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "guard_session_info",
|
|
802
|
+
description: "Zero-Trust Agentic Guard: Returns diagnostic metrics on active in-memory sessions, token counts, and memory footprint with zero plaintext leakage.",
|
|
803
|
+
inputSchema: {
|
|
804
|
+
type: "object",
|
|
805
|
+
properties: {
|
|
806
|
+
session_id: {
|
|
807
|
+
type: "string",
|
|
808
|
+
description: "Optional specific session ID to inspect."
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
required: []
|
|
812
|
+
}
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
name: "guard_session_reset",
|
|
816
|
+
description: "Zero-Trust Agentic Guard: Explicitly purges the in-memory token map for a specific session_id or all sessions upon agent task completion.",
|
|
817
|
+
inputSchema: {
|
|
818
|
+
type: "object",
|
|
819
|
+
properties: {
|
|
820
|
+
session_id: {
|
|
821
|
+
type: "string",
|
|
822
|
+
description: "The session ID to purge. Defaults to 'default'."
|
|
823
|
+
},
|
|
824
|
+
all: {
|
|
825
|
+
type: "boolean",
|
|
826
|
+
description: "Whether to purge ALL active agent sessions in memory. Defaults to false."
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
required: []
|
|
830
|
+
}
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "guard_rag_chunk",
|
|
834
|
+
description: "Zero-Trust Agentic Guard: Sanitizes an array of document chunks or knowledge base records before sending to embedding models or vector databases (Chroma, Pinecone, Qdrant). Keeps authentic data in local RAM with zero network leakage.",
|
|
835
|
+
inputSchema: {
|
|
836
|
+
type: "object",
|
|
837
|
+
properties: {
|
|
838
|
+
chunks: {
|
|
839
|
+
type: "array",
|
|
840
|
+
description: "Array of chunk objects ({ id?: string, text: string, metadata?: object }) or array of chunk strings to sanitize.",
|
|
841
|
+
items: {
|
|
842
|
+
anyOf: [
|
|
843
|
+
{ type: "string" },
|
|
844
|
+
{
|
|
845
|
+
type: "object",
|
|
846
|
+
properties: {
|
|
847
|
+
id: { type: "string" },
|
|
848
|
+
text: { type: "string" },
|
|
849
|
+
pageContent: { type: "string" },
|
|
850
|
+
metadata: { type: "object" }
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
]
|
|
854
|
+
}
|
|
855
|
+
},
|
|
856
|
+
profile: {
|
|
857
|
+
type: "string",
|
|
858
|
+
description: "The detection profile to use. Defaults to 'General'."
|
|
859
|
+
},
|
|
860
|
+
session_id: {
|
|
861
|
+
type: "string",
|
|
862
|
+
description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
required: ["chunks"]
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
{
|
|
869
|
+
name: "guard_rag_restore",
|
|
870
|
+
description: "Zero-Trust Agentic Guard: Re-hydrates retrieved vector search results or chunks by restoring token placeholders with authentic data from the volatile RAM session before presenting to the user.",
|
|
871
|
+
inputSchema: {
|
|
872
|
+
type: "object",
|
|
873
|
+
properties: {
|
|
874
|
+
results: {
|
|
875
|
+
type: "array",
|
|
876
|
+
description: "Array of retrieved search result objects ({ id?: string, text?: string, pageContent?: string, score?: number, metadata?: object }) or strings containing tokens to restore.",
|
|
877
|
+
items: {
|
|
878
|
+
anyOf: [
|
|
879
|
+
{ type: "string" },
|
|
880
|
+
{
|
|
881
|
+
type: "object",
|
|
882
|
+
properties: {
|
|
883
|
+
id: { type: "string" },
|
|
884
|
+
text: { type: "string" },
|
|
885
|
+
pageContent: { type: "string" },
|
|
886
|
+
score: { type: "number" },
|
|
887
|
+
metadata: { type: "object" }
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
]
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
session_id: {
|
|
894
|
+
type: "string",
|
|
895
|
+
description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
required: ["results"]
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
name: "export_audit_log",
|
|
903
|
+
description: "Zero-Trust Enterprise Compliance: Exports an immutable, tamper-evident audit log in SIEM standard formats (CEF / ArcSight, RFC 5424 Syslog, JSONL, or JSON) with zero plaintext PII leakage. Can write directly to a local log file or stream.",
|
|
904
|
+
inputSchema: {
|
|
905
|
+
type: "object",
|
|
906
|
+
properties: {
|
|
907
|
+
format: {
|
|
908
|
+
type: "string",
|
|
909
|
+
enum: ["jsonl", "cef", "syslog", "json", "markdown"],
|
|
910
|
+
description: "Audit log format: 'jsonl' (Datadog/ELK), 'cef' (ArcSight/Splunk), 'syslog' (RFC 5424), 'json', or 'markdown'. Defaults to 'jsonl'."
|
|
911
|
+
},
|
|
912
|
+
session_id: {
|
|
913
|
+
type: "string",
|
|
914
|
+
description: "Optional session ID for isolated multi-task agent memory. Defaults to 'default'."
|
|
915
|
+
},
|
|
916
|
+
destination_file: {
|
|
917
|
+
type: "string",
|
|
918
|
+
description: "Optional local file path to append the audit log event to (e.g. '/var/log/privacyscrubber.log')."
|
|
919
|
+
},
|
|
920
|
+
company_name: {
|
|
921
|
+
type: "string",
|
|
922
|
+
description: "Optional company name for enterprise audit attribution. Defaults to 'PrivacyScrubber Client'."
|
|
923
|
+
},
|
|
924
|
+
department: {
|
|
925
|
+
type: "string",
|
|
926
|
+
description: "Optional department or auditor identifier. Defaults to 'SecOps / Compliance'."
|
|
927
|
+
},
|
|
928
|
+
text: {
|
|
929
|
+
type: "string",
|
|
930
|
+
description: "Optional raw text to sanitize and include in the exported audit log event."
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
566
934
|
}
|
|
567
935
|
]
|
|
568
936
|
};
|
|
@@ -709,8 +1077,75 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
709
1077
|
const toolStart = Date.now();
|
|
710
1078
|
|
|
711
1079
|
try {
|
|
712
|
-
if (name === "
|
|
713
|
-
const { text, profile = "General",
|
|
1080
|
+
if (name === "audit_context") {
|
|
1081
|
+
const { text, profile = "General", format = "detailed" } = args || {};
|
|
1082
|
+
|
|
1083
|
+
if (text === undefined || text === null) {
|
|
1084
|
+
return {
|
|
1085
|
+
isError: true,
|
|
1086
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'text'. Provide the string to audit." }]
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (typeof text !== "string") {
|
|
1090
|
+
return {
|
|
1091
|
+
isError: true,
|
|
1092
|
+
content: [{ type: "text", text: `Error: Parameter 'text' must be a string, got '${typeof text}'.` }]
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const targetProfile = (profile || "General").trim();
|
|
1097
|
+
const customRules = loadCustomRules();
|
|
1098
|
+
const normalizedProfile = targetProfile.toLowerCase();
|
|
1099
|
+
const license = checkLicenseStatus();
|
|
1100
|
+
|
|
1101
|
+
// Read-only dry-run map: NEVER mutates global sessionMap
|
|
1102
|
+
const dryRunMap = {};
|
|
1103
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, dryRunMap, license.isPro);
|
|
1104
|
+
const telemetry = buildCisoAuditTelemetry(result.tokenMap || {});
|
|
1105
|
+
|
|
1106
|
+
if (format === "json") {
|
|
1107
|
+
return {
|
|
1108
|
+
content: [{
|
|
1109
|
+
type: "text",
|
|
1110
|
+
text: JSON.stringify({
|
|
1111
|
+
riskLevel: telemetry.riskLevel,
|
|
1112
|
+
totalDetected: telemetry.totalDetected,
|
|
1113
|
+
entities: telemetry.entities,
|
|
1114
|
+
triggeredFrameworks: telemetry.triggeredFrameworks,
|
|
1115
|
+
profile: targetProfile,
|
|
1116
|
+
executionEngine: "ZTDS Client-Side RAM"
|
|
1117
|
+
}, null, 2)
|
|
1118
|
+
}]
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
let report = `### PrivacyScrubber Pre-Flight Context Audit\n\n`;
|
|
1123
|
+
report += `* **Status / Risk Level:** \`${telemetry.riskLevel}\`\n`;
|
|
1124
|
+
report += `* **Total Entities Detected:** \`${telemetry.totalDetected}\`\n`;
|
|
1125
|
+
report += `* **Active Profile:** \`${targetProfile}\` (Engine: ZTDS Client-Side RAM)\n`;
|
|
1126
|
+
report += `* **Triggered Compliance Frameworks:** ${telemetry.triggeredFrameworks.map(f => `\`${f}\``).join(', ')}\n\n`;
|
|
1127
|
+
|
|
1128
|
+
if (telemetry.totalDetected > 0) {
|
|
1129
|
+
report += `#### Entity Breakdown\n`;
|
|
1130
|
+
report += `| Entity Type | Occurrences | Exposure Severity |\n`;
|
|
1131
|
+
report += `| :--- | :--- | :--- |\n`;
|
|
1132
|
+
for (const [entityType, count] of Object.entries(telemetry.entities)) {
|
|
1133
|
+
const isHigh = ['ID', 'SSN', 'CREDIT_CARD', 'API_KEY', 'SECRET', 'PASSWORD', 'MRN', 'KEY'].includes(entityType);
|
|
1134
|
+
report += `| \`${entityType}\` | ${count} | ${isHigh ? 'High Risk' : 'Standard'} |\n`;
|
|
1135
|
+
}
|
|
1136
|
+
report += `\n> [!CAUTION]\n> **Action Recommended:** This context contains sensitive data. Call \`sanitize_text\` before transmitting to any external LLM model.\n`;
|
|
1137
|
+
} else {
|
|
1138
|
+
report += `> [!NOTE]\n> **Status:** Clean. Zero sensitive entities detected under the \`${targetProfile}\` profile.\n`;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return {
|
|
1142
|
+
content: [{ type: "text", text: report }]
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (name === "sanitize_text" || name === "scrub_text") {
|
|
1147
|
+
const { text, profile = "General", ignore_list, compact = false, session_id = "default" } = args || {};
|
|
1148
|
+
const currentSession = getSessionMap(session_id);
|
|
714
1149
|
|
|
715
1150
|
// Merge per-call ignore_list into persistent sessionIgnoreList
|
|
716
1151
|
if (Array.isArray(ignore_list)) {
|
|
@@ -760,7 +1195,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
760
1195
|
|
|
761
1196
|
const { processedText, wasTruncated } = truncateIfFree(text, license.isPro, charLimit);
|
|
762
1197
|
|
|
763
|
-
const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList);
|
|
1198
|
+
const { scrubbedText, newTokens } = performSanitization(processedText, finalProfile, sessionIgnoreList, currentSession);
|
|
764
1199
|
|
|
765
1200
|
const telemetry = buildCisoAuditTelemetry(newTokens);
|
|
766
1201
|
const receiptMd = formatAuditReceipt(telemetry, compact, wasTruncated, charLimit);
|
|
@@ -773,7 +1208,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
773
1208
|
}
|
|
774
1209
|
|
|
775
1210
|
if (name === "reveal_text") {
|
|
776
|
-
const { text } = args || {};
|
|
1211
|
+
const { text, session_id = "default" } = args || {};
|
|
777
1212
|
if (text === undefined || text === null) {
|
|
778
1213
|
return {
|
|
779
1214
|
isError: true,
|
|
@@ -787,17 +1222,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
787
1222
|
};
|
|
788
1223
|
}
|
|
789
1224
|
|
|
1225
|
+
const currentSession = getSessionMap(session_id);
|
|
790
1226
|
// Warn early if session has no tokens — reveal would be a no-op
|
|
791
|
-
if (Object.keys(
|
|
1227
|
+
if (Object.keys(currentSession).length === 0) {
|
|
792
1228
|
return {
|
|
793
1229
|
content: [{
|
|
794
1230
|
type: "text",
|
|
795
|
-
text: `⚠️ Session map is empty — no tokens to restore. Call 'sanitize_text' or 'sanitize_file' first to build the token map, then pass the AI's response here.`
|
|
1231
|
+
text: `⚠️ Session map${session_id !== 'default' ? ` ('${session_id}')` : ''} is empty — no tokens to restore. Call 'sanitize_text' or 'sanitize_file' first to build the token map, then pass the AI's response here.`
|
|
796
1232
|
}]
|
|
797
1233
|
};
|
|
798
1234
|
}
|
|
799
1235
|
|
|
800
|
-
const restored = PrivacyScrubberCore.unscrubText(text,
|
|
1236
|
+
const restored = PrivacyScrubberCore.unscrubText(text, currentSession);
|
|
801
1237
|
let restoredText = restored.restoredText;
|
|
802
1238
|
// Deduplicate common double-prefixed schemas resulting from LLM prefix reconstruction
|
|
803
1239
|
restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
|
|
@@ -844,7 +1280,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
844
1280
|
};
|
|
845
1281
|
}
|
|
846
1282
|
|
|
847
|
-
if (name === "sanitize_file") {
|
|
1283
|
+
if (name === "sanitize_file" || name === "scrub_file") {
|
|
848
1284
|
const rawPath = args.file_path || args.filePath;
|
|
849
1285
|
if (!rawPath) {
|
|
850
1286
|
return {
|
|
@@ -1444,7 +1880,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1444
1880
|
total_masked_tokens: telemetry.totalCount,
|
|
1445
1881
|
entities_breakdown: telemetry.entities,
|
|
1446
1882
|
zero_egress_verified: true,
|
|
1447
|
-
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}
|
|
1883
|
+
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`,
|
|
1884
|
+
licensing_and_governance: {
|
|
1885
|
+
teams_governance_url: "https://privacyscrubber.com/teams?src=mcp_compliance_json",
|
|
1886
|
+
sdk_pipeline_url: "https://privacyscrubber.com/pricing?tier=sdk&src=mcp_compliance_json"
|
|
1887
|
+
}
|
|
1448
1888
|
};
|
|
1449
1889
|
|
|
1450
1890
|
return {
|
|
@@ -1483,8 +1923,12 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1483
1923
|
2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
|
|
1484
1924
|
3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
|
|
1485
1925
|
|
|
1926
|
+
### 🏢 Enterprise & Team Governance
|
|
1927
|
+
* **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)
|
|
1928
|
+
* **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing?tier=sdk&src=mcp_compliance_report)
|
|
1929
|
+
|
|
1486
1930
|
*Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
1487
|
-
*Verify at: https://privacyscrubber.com/features/audit-receipt
|
|
1931
|
+
*Verify at: https://privacyscrubber.com/features/audit-receipt/* | ⭐ *Star on GitHub: https://github.com/moxno/privacyscrubber-mcp*`;
|
|
1488
1932
|
|
|
1489
1933
|
return {
|
|
1490
1934
|
content: [{ type: "text", text: mdReport }]
|
|
@@ -1492,7 +1936,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1492
1936
|
}
|
|
1493
1937
|
|
|
1494
1938
|
if (name === "guard_exec") {
|
|
1495
|
-
const { command, cwd, profile = "Dev", timeout_ms = 15000 } = args || {};
|
|
1939
|
+
const { command, cwd, profile = "Dev", timeout_ms = 15000, session_id = "default" } = args || {};
|
|
1496
1940
|
if (!command || typeof command !== "string") {
|
|
1497
1941
|
return {
|
|
1498
1942
|
isError: true,
|
|
@@ -1508,13 +1952,17 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1508
1952
|
};
|
|
1509
1953
|
}
|
|
1510
1954
|
|
|
1955
|
+
const targetSession = getSessionMap(session_id);
|
|
1956
|
+
// Transparent in-memory unmasking of any token placeholders before local execution
|
|
1957
|
+
const rawExecutableCommand = reverseTokens(command, targetSession);
|
|
1958
|
+
|
|
1511
1959
|
const execCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
1512
1960
|
let stdout = '';
|
|
1513
1961
|
let stderr = '';
|
|
1514
1962
|
let exitCode = 0;
|
|
1515
1963
|
|
|
1516
1964
|
try {
|
|
1517
|
-
stdout = execSync(
|
|
1965
|
+
stdout = execSync(rawExecutableCommand, {
|
|
1518
1966
|
cwd: execCwd,
|
|
1519
1967
|
timeout: Math.min(Math.max(timeout_ms, 1000), 60000),
|
|
1520
1968
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -1534,9 +1982,9 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1534
1982
|
const { processedText: cleanStdout, wasTruncated: truncOut } = truncateIfFree(stdout, license.isPro, charLimit);
|
|
1535
1983
|
const { processedText: cleanStderr, wasTruncated: truncErr } = truncateIfFree(stderr, license.isPro, charLimit);
|
|
1536
1984
|
|
|
1537
|
-
const resCommand = performSanitization(command, targetProfile, sessionIgnoreList);
|
|
1538
|
-
const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList);
|
|
1539
|
-
const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList);
|
|
1985
|
+
const resCommand = performSanitization(command, targetProfile, sessionIgnoreList, targetSession);
|
|
1986
|
+
const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList, targetSession);
|
|
1987
|
+
const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList, targetSession);
|
|
1540
1988
|
|
|
1541
1989
|
const allNewTokens = { ...resCommand.newTokens, ...resStdout.newTokens, ...resStderr.newTokens };
|
|
1542
1990
|
const tokenCount = Object.keys(allNewTokens).length;
|
|
@@ -1670,7 +2118,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1670
2118
|
}
|
|
1671
2119
|
|
|
1672
2120
|
if (name === "guard_apply_patch") {
|
|
1673
|
-
const { file_path, content, create_backup = true } = args || {};
|
|
2121
|
+
const { file_path, content, create_backup = true, session_id = "default" } = args || {};
|
|
1674
2122
|
if (!file_path || typeof file_path !== "string") {
|
|
1675
2123
|
return {
|
|
1676
2124
|
isError: true,
|
|
@@ -1685,7 +2133,8 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1685
2133
|
}
|
|
1686
2134
|
|
|
1687
2135
|
const resolvedPath = path.resolve(process.cwd(), file_path);
|
|
1688
|
-
const
|
|
2136
|
+
const targetSession = getSessionMap(session_id);
|
|
2137
|
+
const restored = PrivacyScrubberCore.unscrubText(content, targetSession);
|
|
1689
2138
|
const authenticContent = restored.restoredText;
|
|
1690
2139
|
|
|
1691
2140
|
let backupCreated = false;
|
|
@@ -1785,6 +2234,385 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
1785
2234
|
};
|
|
1786
2235
|
}
|
|
1787
2236
|
|
|
2237
|
+
if (name === "guard_unmask_args") {
|
|
2238
|
+
const targetArgs = (args?.tool_args !== undefined) ? args.tool_args : args?.args;
|
|
2239
|
+
const { session_id = "default" } = args || {};
|
|
2240
|
+
if (targetArgs === undefined || targetArgs === null) {
|
|
2241
|
+
return {
|
|
2242
|
+
isError: true,
|
|
2243
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'tool_args' (or 'args')." }]
|
|
2244
|
+
};
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
const targetSession = getSessionMap(session_id);
|
|
2248
|
+
|
|
2249
|
+
function unmaskRecursive(val) {
|
|
2250
|
+
if (typeof val === 'string') {
|
|
2251
|
+
return reverseTokens(val, targetSession);
|
|
2252
|
+
}
|
|
2253
|
+
if (Array.isArray(val)) {
|
|
2254
|
+
return val.map(unmaskRecursive);
|
|
2255
|
+
}
|
|
2256
|
+
if (val !== null && typeof val === 'object') {
|
|
2257
|
+
const res = {};
|
|
2258
|
+
for (const [k, v] of Object.entries(val)) {
|
|
2259
|
+
res[k] = unmaskRecursive(v);
|
|
2260
|
+
}
|
|
2261
|
+
return res;
|
|
2262
|
+
}
|
|
2263
|
+
return val;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
const unmasked = unmaskRecursive(targetArgs);
|
|
2267
|
+
const originalStr = typeof targetArgs === 'string' ? targetArgs : JSON.stringify(targetArgs);
|
|
2268
|
+
|
|
2269
|
+
let tokensRestored = 0;
|
|
2270
|
+
for (const token of Object.keys(targetSession)) {
|
|
2271
|
+
if (originalStr.includes(token)) {
|
|
2272
|
+
tokensRestored += (originalStr.split(token).length - 1);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
return {
|
|
2277
|
+
content: [{
|
|
2278
|
+
type: "text",
|
|
2279
|
+
text: JSON.stringify({
|
|
2280
|
+
status: "success",
|
|
2281
|
+
success: true,
|
|
2282
|
+
session_id,
|
|
2283
|
+
tokens_restored: tokensRestored,
|
|
2284
|
+
unmasked_count: tokensRestored,
|
|
2285
|
+
unmasked_args: unmasked
|
|
2286
|
+
}, null, 2)
|
|
2287
|
+
}]
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
if (name === "guard_session_info") {
|
|
2292
|
+
const { session_id } = args || {};
|
|
2293
|
+
const memUsage = process.memoryUsage();
|
|
2294
|
+
|
|
2295
|
+
if (session_id) {
|
|
2296
|
+
const sid = session_id.toString().trim();
|
|
2297
|
+
const session = agentSessions.get(sid);
|
|
2298
|
+
if (!session) {
|
|
2299
|
+
return {
|
|
2300
|
+
content: [{
|
|
2301
|
+
type: "text",
|
|
2302
|
+
text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
|
|
2303
|
+
}]
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
const tokenEntries = Object.entries(session.map);
|
|
2307
|
+
const telemetry = buildCisoAuditTelemetry(session.map);
|
|
2308
|
+
return {
|
|
2309
|
+
content: [{
|
|
2310
|
+
type: "text",
|
|
2311
|
+
text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
|
|
2312
|
+
`Created: ${new Date(session.createdAt).toISOString()}\n` +
|
|
2313
|
+
`Last Active: ${new Date(session.lastActive).toISOString()}\n` +
|
|
2314
|
+
`Total Tokens Stored in RAM: ${tokenEntries.length}\n` +
|
|
2315
|
+
`Entity Types: ${Object.keys(telemetry.entities).join(', ') || 'None'}\n` +
|
|
2316
|
+
`Risk Level: ${telemetry.riskLevel}\n` +
|
|
2317
|
+
`Status: Volatile in-memory mapping active (0 bytes disk/network).`
|
|
2318
|
+
}]
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
let totalTokens = 0;
|
|
2323
|
+
const sessionList = [];
|
|
2324
|
+
for (const [sid, sess] of agentSessions.entries()) {
|
|
2325
|
+
const count = Object.keys(sess.map).length;
|
|
2326
|
+
totalTokens += count;
|
|
2327
|
+
sessionList.push({ id: sid, tokens: count, lastActive: new Date(sess.lastActive).toISOString() });
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
return {
|
|
2331
|
+
content: [{
|
|
2332
|
+
type: "text",
|
|
2333
|
+
text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
|
|
2334
|
+
`Total Active Agent Sessions: ${agentSessions.size}\n` +
|
|
2335
|
+
`Total Tokens in Memory: ${totalTokens}\n` +
|
|
2336
|
+
`Process Heap Used: ${(memUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB\n` +
|
|
2337
|
+
`Uptime: ${Math.floor(process.uptime())} seconds\n` +
|
|
2338
|
+
`Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens)`).join(', ') || 'None'}`
|
|
2339
|
+
}]
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
if (name === "guard_session_reset") {
|
|
2344
|
+
const { session_id, all = false, reset_all = false } = args || {};
|
|
2345
|
+
if (all || reset_all) {
|
|
2346
|
+
const count = agentSessions.size;
|
|
2347
|
+
agentSessions.clear();
|
|
2348
|
+
getSessionMap('default');
|
|
2349
|
+
sessionIgnoreList.clear();
|
|
2350
|
+
return {
|
|
2351
|
+
content: [{
|
|
2352
|
+
type: "text",
|
|
2353
|
+
text: `[Zero-Trust Agentic Guard] All ${count} active in-memory agent sessions have been completely purged from RAM.`
|
|
2354
|
+
}]
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const sid = (session_id || 'default').toString().trim();
|
|
2359
|
+
const existed = agentSessions.delete(sid);
|
|
2360
|
+
if (sid === 'default') {
|
|
2361
|
+
getSessionMap('default');
|
|
2362
|
+
}
|
|
2363
|
+
return {
|
|
2364
|
+
content: [{
|
|
2365
|
+
type: "text",
|
|
2366
|
+
text: existed
|
|
2367
|
+
? `[Zero-Trust Agentic Guard] Volatile RAM session '${sid}' successfully purged.`
|
|
2368
|
+
: `[Zero-Trust Agentic Guard] Session '${sid}' was not found.`
|
|
2369
|
+
}]
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
if (name === "guard_rag_chunk") {
|
|
2374
|
+
const { chunks, profile = "General", session_id = "default" } = args || {};
|
|
2375
|
+
if (!Array.isArray(chunks)) {
|
|
2376
|
+
return {
|
|
2377
|
+
isError: true,
|
|
2378
|
+
content: [{ type: "text", text: "Error: Missing or invalid parameter 'chunks'. Must be an array of chunk objects or strings." }]
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const targetSession = getSessionMap(session_id);
|
|
2383
|
+
let totalTokensRedacted = 0;
|
|
2384
|
+
const sanitizedChunks = [];
|
|
2385
|
+
|
|
2386
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2387
|
+
const item = chunks[i];
|
|
2388
|
+
if (typeof item === 'string') {
|
|
2389
|
+
const res = performSanitization(item, profile, null, targetSession);
|
|
2390
|
+
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2391
|
+
sanitizedChunks.push(res.scrubbedText);
|
|
2392
|
+
} else if (item && typeof item === 'object') {
|
|
2393
|
+
const copy = { ...item };
|
|
2394
|
+
const rawText = copy.text || copy.pageContent || copy.document || '';
|
|
2395
|
+
const res = performSanitization(rawText, profile, null, targetSession);
|
|
2396
|
+
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2397
|
+
|
|
2398
|
+
if (copy.text !== undefined) copy.text = res.scrubbedText;
|
|
2399
|
+
if (copy.pageContent !== undefined) copy.pageContent = res.scrubbedText;
|
|
2400
|
+
if (copy.document !== undefined) copy.document = res.scrubbedText;
|
|
2401
|
+
if (copy.text === undefined && copy.pageContent === undefined && copy.document === undefined) {
|
|
2402
|
+
copy.text = res.scrubbedText;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
copy.metadata = {
|
|
2406
|
+
...(copy.metadata || {}),
|
|
2407
|
+
_ztds_sanitized: true,
|
|
2408
|
+
_ztds_tokens_masked: Object.keys(res.newTokens || {}).length
|
|
2409
|
+
};
|
|
2410
|
+
sanitizedChunks.push(copy);
|
|
2411
|
+
} else {
|
|
2412
|
+
sanitizedChunks.push(item);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2417
|
+
const auditReceipt = formatAuditReceipt(telemetry);
|
|
2418
|
+
|
|
2419
|
+
return {
|
|
2420
|
+
content: [{
|
|
2421
|
+
type: "text",
|
|
2422
|
+
text: JSON.stringify({
|
|
2423
|
+
status: "success",
|
|
2424
|
+
session_id,
|
|
2425
|
+
total_chunks: chunks.length,
|
|
2426
|
+
tokens_redacted_in_batch: totalTokensRedacted,
|
|
2427
|
+
total_session_tokens: Object.keys(targetSession).length,
|
|
2428
|
+
risk_level: telemetry.riskLevel,
|
|
2429
|
+
sanitized_chunks: sanitizedChunks
|
|
2430
|
+
}, null, 2) + `\n\n${auditReceipt}`
|
|
2431
|
+
}]
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (name === "guard_rag_restore") {
|
|
2436
|
+
const { results, session_id = "default" } = args || {};
|
|
2437
|
+
if (!Array.isArray(results)) {
|
|
2438
|
+
return {
|
|
2439
|
+
isError: true,
|
|
2440
|
+
content: [{ type: "text", text: "Error: Missing or invalid parameter 'results'. Must be an array of search result objects or strings." }]
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const targetSession = getSessionMap(session_id);
|
|
2445
|
+
let totalTokensRestored = 0;
|
|
2446
|
+
const restoredResults = [];
|
|
2447
|
+
|
|
2448
|
+
for (let i = 0; i < results.length; i++) {
|
|
2449
|
+
const item = results[i];
|
|
2450
|
+
if (typeof item === 'string') {
|
|
2451
|
+
const restored = reverseTokens(item, targetSession);
|
|
2452
|
+
for (const token of Object.keys(targetSession)) {
|
|
2453
|
+
if (item.includes(token)) totalTokensRestored++;
|
|
2454
|
+
}
|
|
2455
|
+
restoredResults.push(restored);
|
|
2456
|
+
} else if (item && typeof item === 'object') {
|
|
2457
|
+
const copy = { ...item };
|
|
2458
|
+
const rawText = copy.text || copy.pageContent || copy.document || '';
|
|
2459
|
+
const restored = reverseTokens(rawText, targetSession);
|
|
2460
|
+
for (const token of Object.keys(targetSession)) {
|
|
2461
|
+
if (rawText.includes(token)) totalTokensRestored++;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
if (copy.text !== undefined) copy.text = restored;
|
|
2465
|
+
if (copy.pageContent !== undefined) copy.pageContent = restored;
|
|
2466
|
+
if (copy.document !== undefined) copy.document = restored;
|
|
2467
|
+
if (copy.text === undefined && copy.pageContent === undefined && copy.document === undefined) {
|
|
2468
|
+
copy.text = restored;
|
|
2469
|
+
}
|
|
2470
|
+
restoredResults.push(copy);
|
|
2471
|
+
} else {
|
|
2472
|
+
restoredResults.push(item);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
return {
|
|
2477
|
+
content: [{
|
|
2478
|
+
type: "text",
|
|
2479
|
+
text: JSON.stringify({
|
|
2480
|
+
status: "success",
|
|
2481
|
+
session_id,
|
|
2482
|
+
total_results: results.length,
|
|
2483
|
+
tokens_restored: totalTokensRestored,
|
|
2484
|
+
restored_results: restoredResults
|
|
2485
|
+
}, null, 2)
|
|
2486
|
+
}]
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
if (name === "export_audit_log") {
|
|
2491
|
+
const {
|
|
2492
|
+
format = "jsonl",
|
|
2493
|
+
session_id = "default",
|
|
2494
|
+
destination_file,
|
|
2495
|
+
company_name = "PrivacyScrubber Client",
|
|
2496
|
+
department = "SecOps / Compliance",
|
|
2497
|
+
text
|
|
2498
|
+
} = args || {};
|
|
2499
|
+
|
|
2500
|
+
const targetSession = getSessionMap(session_id);
|
|
2501
|
+
|
|
2502
|
+
// If text is provided, perform sanitization first to ensure session and telemetry are populated
|
|
2503
|
+
if (typeof text === 'string' && text.trim()) {
|
|
2504
|
+
performSanitization(text, "general", null, targetSession);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2508
|
+
|
|
2509
|
+
const sessionHash = crypto.createHash('sha256')
|
|
2510
|
+
.update(JSON.stringify(targetSession) + Date.now().toString())
|
|
2511
|
+
.digest('hex');
|
|
2512
|
+
|
|
2513
|
+
const timestamp = new Date().toISOString();
|
|
2514
|
+
|
|
2515
|
+
let outputText = "";
|
|
2516
|
+
const eventData = {
|
|
2517
|
+
vendor: "PrivacyScrubber",
|
|
2518
|
+
product: "ZTDS",
|
|
2519
|
+
version: MCP_VERSION,
|
|
2520
|
+
timestamp,
|
|
2521
|
+
riskLevel: telemetry.riskLevel,
|
|
2522
|
+
tokensMasked: telemetry.totalCount,
|
|
2523
|
+
entities: telemetry.entities,
|
|
2524
|
+
frameworks: telemetry.frameworksList,
|
|
2525
|
+
sessionHash,
|
|
2526
|
+
organization: company_name,
|
|
2527
|
+
department
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
if (format === "cef") {
|
|
2531
|
+
outputText = formatCefEvent(eventData);
|
|
2532
|
+
} else if (format === "syslog") {
|
|
2533
|
+
outputText = formatSyslogEvent(eventData);
|
|
2534
|
+
} else if (format === "json") {
|
|
2535
|
+
outputText = JSON.stringify({
|
|
2536
|
+
protocol: "Zero-Trust Data Sanitization (ZTDS)",
|
|
2537
|
+
certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
|
|
2538
|
+
company: company_name,
|
|
2539
|
+
department,
|
|
2540
|
+
timestamp,
|
|
2541
|
+
session_hash: sessionHash,
|
|
2542
|
+
verification_mode: "100% Offline (Local In-Memory RAM)",
|
|
2543
|
+
compliance_status: "VERIFIED PASS",
|
|
2544
|
+
risk_level: telemetry.riskLevel,
|
|
2545
|
+
frameworks_enforced: telemetry.frameworksList,
|
|
2546
|
+
total_masked_tokens: telemetry.totalCount,
|
|
2547
|
+
entities_breakdown: telemetry.entities,
|
|
2548
|
+
zero_egress_verified: true,
|
|
2549
|
+
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`,
|
|
2550
|
+
licensing_and_governance: {
|
|
2551
|
+
teams_governance_url: "https://privacyscrubber.com/teams?src=mcp_audit_json",
|
|
2552
|
+
sdk_pipeline_url: "https://privacyscrubber.com/pricing?tier=sdk&src=mcp_audit_json"
|
|
2553
|
+
}
|
|
2554
|
+
}, null, 2);
|
|
2555
|
+
} else if (format === "markdown") {
|
|
2556
|
+
const entitySummary = Object.entries(telemetry.entities)
|
|
2557
|
+
.map(([t, count]) => `[${t}]: ${count}`)
|
|
2558
|
+
.join(', ') || 'None (Clean)';
|
|
2559
|
+
outputText = `# 🛡️ Zero-Trust Data Sanitization Compliance Certificate
|
|
2560
|
+
**Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
|
|
2561
|
+
**Organization:** ${company_name} (${department})
|
|
2562
|
+
**Timestamp:** ${timestamp}
|
|
2563
|
+
**Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
|
|
2564
|
+
**Status:** **VERIFIED PASS** (Zero Network Egress)
|
|
2565
|
+
|
|
2566
|
+
---
|
|
2567
|
+
|
|
2568
|
+
### 📊 Sanitization Metrics & Risk Assessment
|
|
2569
|
+
* **Overall Risk Rating:** **${telemetry.riskLevel}**
|
|
2570
|
+
* **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
|
|
2571
|
+
* **Entity Breakdown:** ${entitySummary}
|
|
2572
|
+
* **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
|
|
2573
|
+
|
|
2574
|
+
### 📜 Regulatory Frameworks Enforced
|
|
2575
|
+
${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
2576
|
+
|
|
2577
|
+
### 🔒 CISO Compliance Declaration
|
|
2578
|
+
1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
|
|
2579
|
+
2. **HIPAA Safe Harbor (§164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
|
|
2580
|
+
3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
|
|
2581
|
+
|
|
2582
|
+
### 🏢 Enterprise & Team Governance
|
|
2583
|
+
* **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_audit_export)
|
|
2584
|
+
* **Backend Microservices & RAG Pipelines:** Integrate in-memory sanitization (<1ms) into your Node.js/Python infrastructure with **Developer SDK** ($299/mo): [privacyscrubber.com/pricing](https://privacyscrubber.com/pricing?tier=sdk&src=mcp_audit_export)
|
|
2585
|
+
|
|
2586
|
+
*Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
2587
|
+
*Verify at: https://privacyscrubber.com/features/audit-receipt/* | ⭐ *Star on GitHub: https://github.com/moxno/privacyscrubber-mcp*`;
|
|
2588
|
+
} else {
|
|
2589
|
+
// Default: jsonl
|
|
2590
|
+
outputText = formatJsonlEvent(eventData);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
if (typeof destination_file === 'string' && destination_file.trim()) {
|
|
2594
|
+
try {
|
|
2595
|
+
const dir = path.dirname(destination_file);
|
|
2596
|
+
if (!fs.existsSync(dir)) {
|
|
2597
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2598
|
+
}
|
|
2599
|
+
fs.appendFileSync(destination_file, outputText + '\n', 'utf8');
|
|
2600
|
+
} catch (e) {
|
|
2601
|
+
return {
|
|
2602
|
+
isError: true,
|
|
2603
|
+
content: [{ type: "text", text: `Error writing audit log to destination_file: ${e.message}` }]
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
return {
|
|
2609
|
+
content: [{
|
|
2610
|
+
type: "text",
|
|
2611
|
+
text: outputText
|
|
2612
|
+
}]
|
|
2613
|
+
};
|
|
2614
|
+
}
|
|
2615
|
+
|
|
1788
2616
|
return {
|
|
1789
2617
|
isError: true,
|
|
1790
2618
|
content: [{ type: "text", text: `Unknown tool: ${name}` }]
|
|
@@ -1878,17 +2706,18 @@ function detectSecrets(text) {
|
|
|
1878
2706
|
return detected;
|
|
1879
2707
|
}
|
|
1880
2708
|
|
|
1881
|
-
function performSanitization(text, profile, ignoreList = null) {
|
|
2709
|
+
function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
|
|
1882
2710
|
const customRules = loadCustomRules();
|
|
1883
2711
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
1884
2712
|
const license = checkLicenseStatus();
|
|
1885
|
-
const
|
|
2713
|
+
const targetMap = customSessionMap || sessionMap;
|
|
2714
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, targetMap, license.isPro, ignoreList);
|
|
1886
2715
|
|
|
1887
2716
|
const newTokens = {};
|
|
1888
2717
|
// Update our volatile map with new matches
|
|
1889
2718
|
if (result.tokenMap) {
|
|
1890
2719
|
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
1891
|
-
|
|
2720
|
+
targetMap[token] = original;
|
|
1892
2721
|
newTokens[token] = original;
|
|
1893
2722
|
});
|
|
1894
2723
|
}
|
|
@@ -1899,7 +2728,7 @@ function performSanitization(text, profile, ignoreList = null) {
|
|
|
1899
2728
|
function truncateIfFree(text, isPro, charLimit = 15000) {
|
|
1900
2729
|
if (!isPro && text.length > charLimit) {
|
|
1901
2730
|
mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Input truncated to ${charLimit.toLocaleString()} characters (Free Tier Limit).${colors.reset}\n${colors.cyan}👉 Set PRIVACYSCRUBBER_KEY to your PRO license key for unlimited size: https://privacyscrubber.com/pricing${colors.reset}\n`);
|
|
1902
|
-
const upsellNotice = `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${charLimit.toLocaleString()} chars. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($
|
|
2731
|
+
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]`;
|
|
1903
2732
|
return { processedText: text.substring(0, charLimit), wasTruncated: true, upsellNotice, charLimit };
|
|
1904
2733
|
}
|
|
1905
2734
|
return { processedText: text, wasTruncated: false, upsellNotice: "", charLimit };
|