@privacyscrubber/mcp-server 2.2.2 โ 2.2.5
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/LICENSE.md +23 -0
- package/README.md +59 -7
- package/index.js +837 -24
- package/install.js +291 -41
- package/manifest.json +1 -1
- package/package.json +10 -5
- package/pii-masking-run.js +1 -1
- package/ps-license-manager.cjs +11 -257
- package/ps-license-manager.js +11 -257
- package/ps-pii-engine.cjs +8 -2774
- package/ps-pii-engine.js +8 -2774
- package/scrubber-core.cjs +8 -1753
- package/server.json +2 -2
- package/.well-known/mcp/server-card.json +0 -78
- package/LICENSE +0 -21
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();
|
|
@@ -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 {
|
|
@@ -1234,8 +1670,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1234
1670
|
const tier = license.isPro ? 'PRO' : 'FREE';
|
|
1235
1671
|
const tierIcon = license.isPro ? 'โ
' : '๐';
|
|
1236
1672
|
const profileList = license.isPro
|
|
1237
|
-
? 'All
|
|
1238
|
-
: 'General only โ PRO unlocks
|
|
1673
|
+
? 'All 30 specialized profiles active (General, Dev, Medical, Legal, Finance, HRโฆ)'
|
|
1674
|
+
: 'General only โ PRO unlocks all 30 specialized industry profiles';
|
|
1239
1675
|
const sizeLimit = license.isPro ? 'Unlimited' : '15,000 characters per request';
|
|
1240
1676
|
|
|
1241
1677
|
const configPath = resolveConfigPath();
|
|
@@ -1492,7 +1928,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1492
1928
|
}
|
|
1493
1929
|
|
|
1494
1930
|
if (name === "guard_exec") {
|
|
1495
|
-
const { command, cwd, profile = "Dev", timeout_ms = 15000 } = args || {};
|
|
1931
|
+
const { command, cwd, profile = "Dev", timeout_ms = 15000, session_id = "default" } = args || {};
|
|
1496
1932
|
if (!command || typeof command !== "string") {
|
|
1497
1933
|
return {
|
|
1498
1934
|
isError: true,
|
|
@@ -1508,13 +1944,17 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1508
1944
|
};
|
|
1509
1945
|
}
|
|
1510
1946
|
|
|
1947
|
+
const targetSession = getSessionMap(session_id);
|
|
1948
|
+
// Transparent in-memory unmasking of any token placeholders before local execution
|
|
1949
|
+
const rawExecutableCommand = reverseTokens(command, targetSession);
|
|
1950
|
+
|
|
1511
1951
|
const execCwd = cwd ? path.resolve(cwd) : process.cwd();
|
|
1512
1952
|
let stdout = '';
|
|
1513
1953
|
let stderr = '';
|
|
1514
1954
|
let exitCode = 0;
|
|
1515
1955
|
|
|
1516
1956
|
try {
|
|
1517
|
-
stdout = execSync(
|
|
1957
|
+
stdout = execSync(rawExecutableCommand, {
|
|
1518
1958
|
cwd: execCwd,
|
|
1519
1959
|
timeout: Math.min(Math.max(timeout_ms, 1000), 60000),
|
|
1520
1960
|
maxBuffer: 10 * 1024 * 1024,
|
|
@@ -1534,9 +1974,9 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1534
1974
|
const { processedText: cleanStdout, wasTruncated: truncOut } = truncateIfFree(stdout, license.isPro, charLimit);
|
|
1535
1975
|
const { processedText: cleanStderr, wasTruncated: truncErr } = truncateIfFree(stderr, license.isPro, charLimit);
|
|
1536
1976
|
|
|
1537
|
-
const resCommand = performSanitization(command, targetProfile, sessionIgnoreList);
|
|
1538
|
-
const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList);
|
|
1539
|
-
const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList);
|
|
1977
|
+
const resCommand = performSanitization(command, targetProfile, sessionIgnoreList, targetSession);
|
|
1978
|
+
const resStdout = performSanitization(cleanStdout, targetProfile, sessionIgnoreList, targetSession);
|
|
1979
|
+
const resStderr = performSanitization(cleanStderr, targetProfile, sessionIgnoreList, targetSession);
|
|
1540
1980
|
|
|
1541
1981
|
const allNewTokens = { ...resCommand.newTokens, ...resStdout.newTokens, ...resStderr.newTokens };
|
|
1542
1982
|
const tokenCount = Object.keys(allNewTokens).length;
|
|
@@ -1670,7 +2110,7 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1670
2110
|
}
|
|
1671
2111
|
|
|
1672
2112
|
if (name === "guard_apply_patch") {
|
|
1673
|
-
const { file_path, content, create_backup = true } = args || {};
|
|
2113
|
+
const { file_path, content, create_backup = true, session_id = "default" } = args || {};
|
|
1674
2114
|
if (!file_path || typeof file_path !== "string") {
|
|
1675
2115
|
return {
|
|
1676
2116
|
isError: true,
|
|
@@ -1685,7 +2125,8 @@ ${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
|
1685
2125
|
}
|
|
1686
2126
|
|
|
1687
2127
|
const resolvedPath = path.resolve(process.cwd(), file_path);
|
|
1688
|
-
const
|
|
2128
|
+
const targetSession = getSessionMap(session_id);
|
|
2129
|
+
const restored = PrivacyScrubberCore.unscrubText(content, targetSession);
|
|
1689
2130
|
const authenticContent = restored.restoredText;
|
|
1690
2131
|
|
|
1691
2132
|
let backupCreated = false;
|
|
@@ -1785,6 +2226,377 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
1785
2226
|
};
|
|
1786
2227
|
}
|
|
1787
2228
|
|
|
2229
|
+
if (name === "guard_unmask_args") {
|
|
2230
|
+
const targetArgs = (args?.tool_args !== undefined) ? args.tool_args : args?.args;
|
|
2231
|
+
const { session_id = "default" } = args || {};
|
|
2232
|
+
if (targetArgs === undefined || targetArgs === null) {
|
|
2233
|
+
return {
|
|
2234
|
+
isError: true,
|
|
2235
|
+
content: [{ type: "text", text: "Error: Missing required parameter 'tool_args' (or 'args')." }]
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
const targetSession = getSessionMap(session_id);
|
|
2240
|
+
|
|
2241
|
+
function unmaskRecursive(val) {
|
|
2242
|
+
if (typeof val === 'string') {
|
|
2243
|
+
return reverseTokens(val, targetSession);
|
|
2244
|
+
}
|
|
2245
|
+
if (Array.isArray(val)) {
|
|
2246
|
+
return val.map(unmaskRecursive);
|
|
2247
|
+
}
|
|
2248
|
+
if (val !== null && typeof val === 'object') {
|
|
2249
|
+
const res = {};
|
|
2250
|
+
for (const [k, v] of Object.entries(val)) {
|
|
2251
|
+
res[k] = unmaskRecursive(v);
|
|
2252
|
+
}
|
|
2253
|
+
return res;
|
|
2254
|
+
}
|
|
2255
|
+
return val;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
const unmasked = unmaskRecursive(targetArgs);
|
|
2259
|
+
const originalStr = typeof targetArgs === 'string' ? targetArgs : JSON.stringify(targetArgs);
|
|
2260
|
+
|
|
2261
|
+
let tokensRestored = 0;
|
|
2262
|
+
for (const token of Object.keys(targetSession)) {
|
|
2263
|
+
if (originalStr.includes(token)) {
|
|
2264
|
+
tokensRestored += (originalStr.split(token).length - 1);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
return {
|
|
2269
|
+
content: [{
|
|
2270
|
+
type: "text",
|
|
2271
|
+
text: JSON.stringify({
|
|
2272
|
+
status: "success",
|
|
2273
|
+
success: true,
|
|
2274
|
+
session_id,
|
|
2275
|
+
tokens_restored: tokensRestored,
|
|
2276
|
+
unmasked_count: tokensRestored,
|
|
2277
|
+
unmasked_args: unmasked
|
|
2278
|
+
}, null, 2)
|
|
2279
|
+
}]
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
if (name === "guard_session_info") {
|
|
2284
|
+
const { session_id } = args || {};
|
|
2285
|
+
const memUsage = process.memoryUsage();
|
|
2286
|
+
|
|
2287
|
+
if (session_id) {
|
|
2288
|
+
const sid = session_id.toString().trim();
|
|
2289
|
+
const session = agentSessions.get(sid);
|
|
2290
|
+
if (!session) {
|
|
2291
|
+
return {
|
|
2292
|
+
content: [{
|
|
2293
|
+
type: "text",
|
|
2294
|
+
text: `[Zero-Trust Agentic Guard: Session Info]\nSession '${sid}' does not exist or has expired.`
|
|
2295
|
+
}]
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const tokenEntries = Object.entries(session.map);
|
|
2299
|
+
const telemetry = buildCisoAuditTelemetry(session.map);
|
|
2300
|
+
return {
|
|
2301
|
+
content: [{
|
|
2302
|
+
type: "text",
|
|
2303
|
+
text: `[Zero-Trust Agentic Guard: Session Info - ${sid}]\n` +
|
|
2304
|
+
`Created: ${new Date(session.createdAt).toISOString()}\n` +
|
|
2305
|
+
`Last Active: ${new Date(session.lastActive).toISOString()}\n` +
|
|
2306
|
+
`Total Tokens Stored in RAM: ${tokenEntries.length}\n` +
|
|
2307
|
+
`Entity Types: ${Object.keys(telemetry.entities).join(', ') || 'None'}\n` +
|
|
2308
|
+
`Risk Level: ${telemetry.riskLevel}\n` +
|
|
2309
|
+
`Status: Volatile in-memory mapping active (0 bytes disk/network).`
|
|
2310
|
+
}]
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
let totalTokens = 0;
|
|
2315
|
+
const sessionList = [];
|
|
2316
|
+
for (const [sid, sess] of agentSessions.entries()) {
|
|
2317
|
+
const count = Object.keys(sess.map).length;
|
|
2318
|
+
totalTokens += count;
|
|
2319
|
+
sessionList.push({ id: sid, tokens: count, lastActive: new Date(sess.lastActive).toISOString() });
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
return {
|
|
2323
|
+
content: [{
|
|
2324
|
+
type: "text",
|
|
2325
|
+
text: `[Zero-Trust Agentic Guard: Global Session Diagnostics]\n` +
|
|
2326
|
+
`Total Active Agent Sessions: ${agentSessions.size}\n` +
|
|
2327
|
+
`Total Tokens in Memory: ${totalTokens}\n` +
|
|
2328
|
+
`Process Heap Used: ${(memUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB\n` +
|
|
2329
|
+
`Uptime: ${Math.floor(process.uptime())} seconds\n` +
|
|
2330
|
+
`Active Sessions: ${sessionList.map(s => `${s.id} (${s.tokens} tokens)`).join(', ') || 'None'}`
|
|
2331
|
+
}]
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
if (name === "guard_session_reset") {
|
|
2336
|
+
const { session_id, all = false, reset_all = false } = args || {};
|
|
2337
|
+
if (all || reset_all) {
|
|
2338
|
+
const count = agentSessions.size;
|
|
2339
|
+
agentSessions.clear();
|
|
2340
|
+
getSessionMap('default');
|
|
2341
|
+
sessionIgnoreList.clear();
|
|
2342
|
+
return {
|
|
2343
|
+
content: [{
|
|
2344
|
+
type: "text",
|
|
2345
|
+
text: `[Zero-Trust Agentic Guard] All ${count} active in-memory agent sessions have been completely purged from RAM.`
|
|
2346
|
+
}]
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
const sid = (session_id || 'default').toString().trim();
|
|
2351
|
+
const existed = agentSessions.delete(sid);
|
|
2352
|
+
if (sid === 'default') {
|
|
2353
|
+
getSessionMap('default');
|
|
2354
|
+
}
|
|
2355
|
+
return {
|
|
2356
|
+
content: [{
|
|
2357
|
+
type: "text",
|
|
2358
|
+
text: existed
|
|
2359
|
+
? `[Zero-Trust Agentic Guard] Volatile RAM session '${sid}' successfully purged.`
|
|
2360
|
+
: `[Zero-Trust Agentic Guard] Session '${sid}' was not found.`
|
|
2361
|
+
}]
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
if (name === "guard_rag_chunk") {
|
|
2366
|
+
const { chunks, profile = "General", session_id = "default" } = args || {};
|
|
2367
|
+
if (!Array.isArray(chunks)) {
|
|
2368
|
+
return {
|
|
2369
|
+
isError: true,
|
|
2370
|
+
content: [{ type: "text", text: "Error: Missing or invalid parameter 'chunks'. Must be an array of chunk objects or strings." }]
|
|
2371
|
+
};
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
const targetSession = getSessionMap(session_id);
|
|
2375
|
+
let totalTokensRedacted = 0;
|
|
2376
|
+
const sanitizedChunks = [];
|
|
2377
|
+
|
|
2378
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2379
|
+
const item = chunks[i];
|
|
2380
|
+
if (typeof item === 'string') {
|
|
2381
|
+
const res = performSanitization(item, profile, null, targetSession);
|
|
2382
|
+
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2383
|
+
sanitizedChunks.push(res.scrubbedText);
|
|
2384
|
+
} else if (item && typeof item === 'object') {
|
|
2385
|
+
const copy = { ...item };
|
|
2386
|
+
const rawText = copy.text || copy.pageContent || copy.document || '';
|
|
2387
|
+
const res = performSanitization(rawText, profile, null, targetSession);
|
|
2388
|
+
totalTokensRedacted += Object.keys(res.newTokens || {}).length;
|
|
2389
|
+
|
|
2390
|
+
if (copy.text !== undefined) copy.text = res.scrubbedText;
|
|
2391
|
+
if (copy.pageContent !== undefined) copy.pageContent = res.scrubbedText;
|
|
2392
|
+
if (copy.document !== undefined) copy.document = res.scrubbedText;
|
|
2393
|
+
if (copy.text === undefined && copy.pageContent === undefined && copy.document === undefined) {
|
|
2394
|
+
copy.text = res.scrubbedText;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
copy.metadata = {
|
|
2398
|
+
...(copy.metadata || {}),
|
|
2399
|
+
_ztds_sanitized: true,
|
|
2400
|
+
_ztds_tokens_masked: Object.keys(res.newTokens || {}).length
|
|
2401
|
+
};
|
|
2402
|
+
sanitizedChunks.push(copy);
|
|
2403
|
+
} else {
|
|
2404
|
+
sanitizedChunks.push(item);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2409
|
+
const auditReceipt = formatAuditReceipt(telemetry);
|
|
2410
|
+
|
|
2411
|
+
return {
|
|
2412
|
+
content: [{
|
|
2413
|
+
type: "text",
|
|
2414
|
+
text: JSON.stringify({
|
|
2415
|
+
status: "success",
|
|
2416
|
+
session_id,
|
|
2417
|
+
total_chunks: chunks.length,
|
|
2418
|
+
tokens_redacted_in_batch: totalTokensRedacted,
|
|
2419
|
+
total_session_tokens: Object.keys(targetSession).length,
|
|
2420
|
+
risk_level: telemetry.riskLevel,
|
|
2421
|
+
sanitized_chunks: sanitizedChunks
|
|
2422
|
+
}, null, 2) + `\n\n${auditReceipt}`
|
|
2423
|
+
}]
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
if (name === "guard_rag_restore") {
|
|
2428
|
+
const { results, session_id = "default" } = args || {};
|
|
2429
|
+
if (!Array.isArray(results)) {
|
|
2430
|
+
return {
|
|
2431
|
+
isError: true,
|
|
2432
|
+
content: [{ type: "text", text: "Error: Missing or invalid parameter 'results'. Must be an array of search result objects or strings." }]
|
|
2433
|
+
};
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
const targetSession = getSessionMap(session_id);
|
|
2437
|
+
let totalTokensRestored = 0;
|
|
2438
|
+
const restoredResults = [];
|
|
2439
|
+
|
|
2440
|
+
for (let i = 0; i < results.length; i++) {
|
|
2441
|
+
const item = results[i];
|
|
2442
|
+
if (typeof item === 'string') {
|
|
2443
|
+
const restored = reverseTokens(item, targetSession);
|
|
2444
|
+
for (const token of Object.keys(targetSession)) {
|
|
2445
|
+
if (item.includes(token)) totalTokensRestored++;
|
|
2446
|
+
}
|
|
2447
|
+
restoredResults.push(restored);
|
|
2448
|
+
} else if (item && typeof item === 'object') {
|
|
2449
|
+
const copy = { ...item };
|
|
2450
|
+
const rawText = copy.text || copy.pageContent || copy.document || '';
|
|
2451
|
+
const restored = reverseTokens(rawText, targetSession);
|
|
2452
|
+
for (const token of Object.keys(targetSession)) {
|
|
2453
|
+
if (rawText.includes(token)) totalTokensRestored++;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
if (copy.text !== undefined) copy.text = restored;
|
|
2457
|
+
if (copy.pageContent !== undefined) copy.pageContent = restored;
|
|
2458
|
+
if (copy.document !== undefined) copy.document = restored;
|
|
2459
|
+
if (copy.text === undefined && copy.pageContent === undefined && copy.document === undefined) {
|
|
2460
|
+
copy.text = restored;
|
|
2461
|
+
}
|
|
2462
|
+
restoredResults.push(copy);
|
|
2463
|
+
} else {
|
|
2464
|
+
restoredResults.push(item);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
return {
|
|
2469
|
+
content: [{
|
|
2470
|
+
type: "text",
|
|
2471
|
+
text: JSON.stringify({
|
|
2472
|
+
status: "success",
|
|
2473
|
+
session_id,
|
|
2474
|
+
total_results: results.length,
|
|
2475
|
+
tokens_restored: totalTokensRestored,
|
|
2476
|
+
restored_results: restoredResults
|
|
2477
|
+
}, null, 2)
|
|
2478
|
+
}]
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (name === "export_audit_log") {
|
|
2483
|
+
const {
|
|
2484
|
+
format = "jsonl",
|
|
2485
|
+
session_id = "default",
|
|
2486
|
+
destination_file,
|
|
2487
|
+
company_name = "PrivacyScrubber Client",
|
|
2488
|
+
department = "SecOps / Compliance",
|
|
2489
|
+
text
|
|
2490
|
+
} = args || {};
|
|
2491
|
+
|
|
2492
|
+
const targetSession = getSessionMap(session_id);
|
|
2493
|
+
|
|
2494
|
+
// If text is provided, perform sanitization first to ensure session and telemetry are populated
|
|
2495
|
+
if (typeof text === 'string' && text.trim()) {
|
|
2496
|
+
performSanitization(text, "general", null, targetSession);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const telemetry = buildCisoAuditTelemetry(targetSession);
|
|
2500
|
+
|
|
2501
|
+
const sessionHash = crypto.createHash('sha256')
|
|
2502
|
+
.update(JSON.stringify(targetSession) + Date.now().toString())
|
|
2503
|
+
.digest('hex');
|
|
2504
|
+
|
|
2505
|
+
const timestamp = new Date().toISOString();
|
|
2506
|
+
|
|
2507
|
+
let outputText = "";
|
|
2508
|
+
const eventData = {
|
|
2509
|
+
vendor: "PrivacyScrubber",
|
|
2510
|
+
product: "ZTDS",
|
|
2511
|
+
version: MCP_VERSION,
|
|
2512
|
+
timestamp,
|
|
2513
|
+
riskLevel: telemetry.riskLevel,
|
|
2514
|
+
tokensMasked: telemetry.totalCount,
|
|
2515
|
+
entities: telemetry.entities,
|
|
2516
|
+
frameworks: telemetry.frameworksList,
|
|
2517
|
+
sessionHash,
|
|
2518
|
+
organization: company_name,
|
|
2519
|
+
department
|
|
2520
|
+
};
|
|
2521
|
+
|
|
2522
|
+
if (format === "cef") {
|
|
2523
|
+
outputText = formatCefEvent(eventData);
|
|
2524
|
+
} else if (format === "syslog") {
|
|
2525
|
+
outputText = formatSyslogEvent(eventData);
|
|
2526
|
+
} else if (format === "json") {
|
|
2527
|
+
outputText = JSON.stringify({
|
|
2528
|
+
protocol: "Zero-Trust Data Sanitization (ZTDS)",
|
|
2529
|
+
certificate: `ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}`,
|
|
2530
|
+
company: company_name,
|
|
2531
|
+
department,
|
|
2532
|
+
timestamp,
|
|
2533
|
+
session_hash: sessionHash,
|
|
2534
|
+
verification_mode: "100% Offline (Local In-Memory RAM)",
|
|
2535
|
+
compliance_status: "VERIFIED PASS",
|
|
2536
|
+
risk_level: telemetry.riskLevel,
|
|
2537
|
+
frameworks_enforced: telemetry.frameworksList,
|
|
2538
|
+
total_masked_tokens: telemetry.totalCount,
|
|
2539
|
+
entities_breakdown: telemetry.entities,
|
|
2540
|
+
zero_egress_verified: true,
|
|
2541
|
+
verification_url: `https://privacyscrubber.com/features/audit-receipt/#verify?hash=${sessionHash.substring(0, 16)}`
|
|
2542
|
+
}, null, 2);
|
|
2543
|
+
} else if (format === "markdown") {
|
|
2544
|
+
const entitySummary = Object.entries(telemetry.entities)
|
|
2545
|
+
.map(([t, count]) => `[${t}]: ${count}`)
|
|
2546
|
+
.join(', ') || 'None (Clean)';
|
|
2547
|
+
outputText = `# ๐ก๏ธ Zero-Trust Data Sanitization Compliance Certificate
|
|
2548
|
+
**Certificate ID:** \`ZTDS-CERT-${sessionHash.substring(0, 16).toUpperCase()}\`
|
|
2549
|
+
**Organization:** ${company_name} (${department})
|
|
2550
|
+
**Timestamp:** ${timestamp}
|
|
2551
|
+
**Verification Mode:** 100% Local In-Memory Processing (Air-Gapped)
|
|
2552
|
+
**Status:** **VERIFIED PASS** (Zero Network Egress)
|
|
2553
|
+
|
|
2554
|
+
---
|
|
2555
|
+
|
|
2556
|
+
### ๐ Sanitization Metrics & Risk Assessment
|
|
2557
|
+
* **Overall Risk Rating:** **${telemetry.riskLevel}**
|
|
2558
|
+
* **Total Sensitive Entities Masked:** \`${telemetry.totalCount}\`
|
|
2559
|
+
* **Entity Breakdown:** ${entitySummary}
|
|
2560
|
+
* **Network Data Transmitted:** \`0.00 KB (Zero-Trust Local RAM)\`
|
|
2561
|
+
|
|
2562
|
+
### ๐ Regulatory Frameworks Enforced
|
|
2563
|
+
${telemetry.frameworksList.map(f => `- **${f}**`).join('\n')}
|
|
2564
|
+
|
|
2565
|
+
### ๐ CISO Compliance Declaration
|
|
2566
|
+
1. **EU AI Act (Art. 50) & GDPR (Art. 25 & 32):** Data minimization and local pseudonymization enforced prior to model interaction.
|
|
2567
|
+
2. **HIPAA Safe Harbor (ยง164.514) / SOC 2 Type II:** All direct and indirect identifiers sanitized locally without cloud processor liability.
|
|
2568
|
+
3. **Cryptographic Verification:** Tamper-evident session verification hash: \`${sessionHash}\`
|
|
2569
|
+
|
|
2570
|
+
*Certified Offline by PrivacyScrubber Engine v${MCP_VERSION}*
|
|
2571
|
+
*Verify at: https://privacyscrubber.com/features/audit-receipt/*`;
|
|
2572
|
+
} else {
|
|
2573
|
+
// Default: jsonl
|
|
2574
|
+
outputText = formatJsonlEvent(eventData);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
if (typeof destination_file === 'string' && destination_file.trim()) {
|
|
2578
|
+
try {
|
|
2579
|
+
const dir = path.dirname(destination_file);
|
|
2580
|
+
if (!fs.existsSync(dir)) {
|
|
2581
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2582
|
+
}
|
|
2583
|
+
fs.appendFileSync(destination_file, outputText + '\n', 'utf8');
|
|
2584
|
+
} catch (e) {
|
|
2585
|
+
return {
|
|
2586
|
+
isError: true,
|
|
2587
|
+
content: [{ type: "text", text: `Error writing audit log to destination_file: ${e.message}` }]
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
return {
|
|
2593
|
+
content: [{
|
|
2594
|
+
type: "text",
|
|
2595
|
+
text: outputText
|
|
2596
|
+
}]
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
|
|
1788
2600
|
return {
|
|
1789
2601
|
isError: true,
|
|
1790
2602
|
content: [{ type: "text", text: `Unknown tool: ${name}` }]
|
|
@@ -1878,17 +2690,18 @@ function detectSecrets(text) {
|
|
|
1878
2690
|
return detected;
|
|
1879
2691
|
}
|
|
1880
2692
|
|
|
1881
|
-
function performSanitization(text, profile, ignoreList = null) {
|
|
2693
|
+
function performSanitization(text, profile, ignoreList = null, customSessionMap = null) {
|
|
1882
2694
|
const customRules = loadCustomRules();
|
|
1883
2695
|
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
1884
2696
|
const license = checkLicenseStatus();
|
|
1885
|
-
const
|
|
2697
|
+
const targetMap = customSessionMap || sessionMap;
|
|
2698
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, targetMap, license.isPro, ignoreList);
|
|
1886
2699
|
|
|
1887
2700
|
const newTokens = {};
|
|
1888
2701
|
// Update our volatile map with new matches
|
|
1889
2702
|
if (result.tokenMap) {
|
|
1890
2703
|
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
1891
|
-
|
|
2704
|
+
targetMap[token] = original;
|
|
1892
2705
|
newTokens[token] = original;
|
|
1893
2706
|
});
|
|
1894
2707
|
}
|