@privacyscrubber/mcp-server 2.4.5 → 2.4.7
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 +14 -7
- package/index.js +578 -171
- package/install.js +94 -3
- package/manifest.json +8 -10
- package/package.json +1 -3
- package/pii-masking-run.js +427 -45
- package/ps-license-manager.cjs +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/scrubber-core.cjs +1 -1
- package/server.json +3 -3
- package/ps-license-manager.js +0 -11
- package/ps-pii-engine.js +0 -11
package/pii-masking-run.js
CHANGED
|
@@ -27,7 +27,7 @@ const cmdIndex = args.indexOf("--");
|
|
|
27
27
|
|
|
28
28
|
if (args.includes("--version") || args.includes("-v")) {
|
|
29
29
|
const pkgPath = path.resolve(__dirname, './package.json');
|
|
30
|
-
let version = '2.4.
|
|
30
|
+
let version = '2.4.7';
|
|
31
31
|
try { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch (_) {}
|
|
32
32
|
console.log(`ps-guard / pii-masking-run v${version} (PrivacyScrubber MCP)`);
|
|
33
33
|
process.exit(0);
|
|
@@ -35,6 +35,8 @@ if (args.includes("--version") || args.includes("-v")) {
|
|
|
35
35
|
|
|
36
36
|
const isDiffMode = args.includes("--diff");
|
|
37
37
|
const isRulesMode = args.includes("--rules");
|
|
38
|
+
const isHookMode = args.includes("--hook");
|
|
39
|
+
const isWrapMode = args.includes("--wrap");
|
|
38
40
|
const isJsonMode = args.includes("--json");
|
|
39
41
|
const isRestoreMode = args.includes("--restore") || args.includes("--unscrub");
|
|
40
42
|
let tokenMapInput = null;
|
|
@@ -42,18 +44,49 @@ const tokenMapIdx = args.indexOf("--token-map");
|
|
|
42
44
|
if (tokenMapIdx !== -1 && args[tokenMapIdx + 1]) {
|
|
43
45
|
tokenMapInput = args[tokenMapIdx + 1];
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
let hookPayloadArg = null;
|
|
48
|
+
const payloadIdx = args.indexOf("--payload");
|
|
49
|
+
if (payloadIdx !== -1 && args[payloadIdx + 1]) {
|
|
50
|
+
hookPayloadArg = args[payloadIdx + 1];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let wrapCommandArgs = [];
|
|
54
|
+
if (isWrapMode) {
|
|
55
|
+
if (cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0) {
|
|
56
|
+
wrapCommandArgs = args.slice(cmdIndex + 1);
|
|
57
|
+
} else {
|
|
58
|
+
const wrapIdx = args.indexOf("--wrap");
|
|
59
|
+
const afterWrap = args.slice(wrapIdx + 1);
|
|
60
|
+
for (let i = 0; i < afterWrap.length; i++) {
|
|
61
|
+
const a = afterWrap[i];
|
|
62
|
+
if ((a === "--profile" || a === "--key" || a === "--token-map") && afterWrap[i + 1]) {
|
|
63
|
+
i++;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === "--quiet" || a === "-q" || a === "--verbose" || a === "--json") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
wrapCommandArgs = afterWrap.slice(i);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const hasCommand = (cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0) || wrapCommandArgs.length > 0;
|
|
46
76
|
const hasStdin = !process.stdin.isTTY;
|
|
47
77
|
|
|
48
|
-
if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isRestoreMode && !hasStdin)) {
|
|
78
|
+
if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isHookMode && !isWrapMode && !isRestoreMode && !hasStdin)) {
|
|
49
79
|
console.log("PrivacyScrubber CLI & Agentic Guard: ps-guard / pii-masking-run");
|
|
50
80
|
console.log("\nUsage:");
|
|
51
81
|
console.log(" npx ps-guard [options] -- <command> [args...]");
|
|
82
|
+
console.log(" npx ps-guard --wrap [options] [--] <mcp-server-cmd> [args...]");
|
|
52
83
|
console.log(" cat <file> | npx ps-guard [options]");
|
|
53
84
|
console.log(" npx ps-guard --diff [--staged] [options]");
|
|
54
85
|
console.log(" npx ps-guard --restore --token-map <file|json> [options]");
|
|
55
86
|
console.log(" npx ps-guard --rules [targets]");
|
|
87
|
+
console.log(" npx ps-guard --hook (Claude Code & Cursor IDE PreToolUse / beforeReadFile hook)");
|
|
56
88
|
console.log("\nOptions:");
|
|
89
|
+
console.log(" --wrap Transparent JSON-RPC 2.0 stdio proxy for any 3rd-party MCP server");
|
|
57
90
|
console.log(" --profile <name> Specify detection profile (e.g. General, Dev, Medical, Legal, HR)");
|
|
58
91
|
console.log(" --key <licKey> PrivacyScrubber PRO License Key");
|
|
59
92
|
console.log(" --json Output structured JSON (scrubbedText, tokenMap, count, telemetry)");
|
|
@@ -62,11 +95,13 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
|
|
|
62
95
|
console.log(" --diff Sanitize git diff output before committing or sending to AI");
|
|
63
96
|
console.log(" --staged Use staged git changes with --diff");
|
|
64
97
|
console.log(" --rules [targets] Generate AI agent rules (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)");
|
|
98
|
+
console.log(" --hook Evaluate IDE lifecycle hook JSON from stdin (Claude Code & Cursor)");
|
|
65
99
|
console.log(" -q, --quiet Suppress stderr proof telemetry");
|
|
66
100
|
console.log(" -v, --version Print version");
|
|
67
101
|
console.log(" -h, --help Show this help screen");
|
|
68
102
|
console.log("\nExamples:");
|
|
69
103
|
console.log(" npx ps-guard -- cat database-dump.sql");
|
|
104
|
+
console.log(" npx ps-guard --wrap -- npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb");
|
|
70
105
|
console.log(" npx ps-guard --profile dev -- npm test");
|
|
71
106
|
console.log(" cat .env | npx ps-guard --profile dev --json");
|
|
72
107
|
console.log(" cat ai_reply.txt | npx ps-guard --restore --token-map map.json");
|
|
@@ -75,6 +110,98 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
|
|
|
75
110
|
process.exit(0);
|
|
76
111
|
}
|
|
77
112
|
|
|
113
|
+
// Fast-path Mode 0: Deterministic IDE Lifecycle Hook (--hook) for Claude Code & Cursor v1.7+
|
|
114
|
+
if (isHookMode) {
|
|
115
|
+
const SENSITIVE_FILE_RX = /(?:^|[\\/])(?:\.env(?:\.[a-zA-Z0-9_-]+)?|id_rsa|id_ed25519|id_ecdsa|.*\.pem|.*\.key|.*\.p12|.*\.pfx|.*credentials.*\.json|.*secret.*\.(?:json|ya?ml|env|txt)|\.npmrc|\.pypirc|\.netrc)$/i;
|
|
116
|
+
const SAFE_TEMPLATE_RX = /\.(?:example|sample|template|dist|schema)$/i;
|
|
117
|
+
const SENSITIVE_CMD_RX = /(?:^|[;&|]\s*)(?:cat|head|tail|less|more|bat|sed|awk|grep)\s+[^\n|;&]*(?:\.env\b|id_rsa|\.pem\b|\.key\b|credentials\.json|\bsecrets?\.)|(?:^|[;&|]\s*)(?:printenv|env\s*$|export\s+-p|aws\s+configure\s+get|gcloud\s+auth\s+print-access-token)/i;
|
|
118
|
+
|
|
119
|
+
const evaluateHookPayload = (rawStr) => {
|
|
120
|
+
let payload = {};
|
|
121
|
+
try {
|
|
122
|
+
payload = rawStr && rawStr.trim() ? JSON.parse(rawStr) : {};
|
|
123
|
+
} catch (_) {
|
|
124
|
+
process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const eventName = String(payload.hook_event_name || payload.hookEventName || payload.event || "");
|
|
129
|
+
const toolName = String(payload.tool_name || payload.toolName || "");
|
|
130
|
+
const toolInput = (payload.tool_input || payload.toolInput || payload) || {};
|
|
131
|
+
|
|
132
|
+
const candidatePath = String(
|
|
133
|
+
toolInput.file_path || toolInput.path || toolInput.target_file || payload.file_path || payload.path || ""
|
|
134
|
+
).trim();
|
|
135
|
+
const candidateCmd = String(
|
|
136
|
+
toolInput.command || toolInput.cmd || payload.command || ""
|
|
137
|
+
).trim();
|
|
138
|
+
|
|
139
|
+
const isCursorEvent = /^(?:beforeReadFile|beforeShellExecution|beforeSubmitPrompt|beforeMCPExecution)$/i.test(eventName) ||
|
|
140
|
+
(!toolName && (payload.file_path !== undefined || payload.command !== undefined || payload.attachments !== undefined));
|
|
141
|
+
|
|
142
|
+
const isReadTool = /^(?:Read|View|read_file|view_file|ReadFile)$/i.test(toolName) || /^beforeReadFile$/i.test(eventName) || (!toolName && Boolean(candidatePath));
|
|
143
|
+
const isShellTool = /^(?:Bash|Execute|run_command| run_terminal_cmd)$/i.test(toolName) || /^beforeShellExecution$/i.test(eventName) || (!toolName && Boolean(candidateCmd));
|
|
144
|
+
|
|
145
|
+
if (isReadTool && candidatePath && SENSITIVE_FILE_RX.test(candidatePath) && !SAFE_TEMPLATE_RX.test(candidatePath)) {
|
|
146
|
+
const reason = `Blocked by PrivacyScrubber ZTDS policy: Direct read of sensitive file '${path.basename(candidatePath)}' is prohibited. Call the PrivacyScrubber MCP tool 'guard_read_file' or 'sanitize_file' instead so secrets are tokenized in local RAM.`;
|
|
147
|
+
if (isCursorEvent) {
|
|
148
|
+
process.stdout.write(JSON.stringify({
|
|
149
|
+
permission: "deny",
|
|
150
|
+
userMessage: `[PrivacyScrubber ZTDS] Direct read of ${path.basename(candidatePath)} blocked to prevent cloud egress.`,
|
|
151
|
+
agentMessage: reason
|
|
152
|
+
}) + "\n");
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
process.stdout.write(JSON.stringify({
|
|
156
|
+
permission: "deny",
|
|
157
|
+
hookSpecificOutput: {
|
|
158
|
+
hookEventName: eventName || "PreToolUse",
|
|
159
|
+
permissionDecision: "deny",
|
|
160
|
+
permissionDecisionReason: reason
|
|
161
|
+
}
|
|
162
|
+
}) + "\n");
|
|
163
|
+
process.stderr.write(reason + "\n");
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (isShellTool && candidateCmd && SENSITIVE_CMD_RX.test(candidateCmd)) {
|
|
168
|
+
const reason = `Blocked by PrivacyScrubber ZTDS policy: Shell command exposing raw credentials ('${candidateCmd.slice(0, 60)}') is prohibited. Call the PrivacyScrubber MCP tool 'guard_exec' instead so stdout/stderr are sanitized in local RAM.`;
|
|
169
|
+
if (isCursorEvent) {
|
|
170
|
+
process.stdout.write(JSON.stringify({
|
|
171
|
+
permission: "deny",
|
|
172
|
+
userMessage: `[PrivacyScrubber ZTDS] Shell command exposing raw credentials blocked.`,
|
|
173
|
+
agentMessage: reason
|
|
174
|
+
}) + "\n");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write(JSON.stringify({
|
|
178
|
+
permission: "deny",
|
|
179
|
+
hookSpecificOutput: {
|
|
180
|
+
hookEventName: eventName || "PreToolUse",
|
|
181
|
+
permissionDecision: "deny",
|
|
182
|
+
permissionDecisionReason: reason
|
|
183
|
+
}
|
|
184
|
+
}) + "\n");
|
|
185
|
+
process.stderr.write(reason + "\n");
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
|
|
190
|
+
process.exit(0);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (hookPayloadArg !== null) {
|
|
194
|
+
evaluateHookPayload(hookPayloadArg);
|
|
195
|
+
} else if (!process.stdin.isTTY) {
|
|
196
|
+
let buf = "";
|
|
197
|
+
process.stdin.setEncoding("utf8");
|
|
198
|
+
process.stdin.on("data", (c) => { buf += c; });
|
|
199
|
+
process.stdin.on("end", () => evaluateHookPayload(buf));
|
|
200
|
+
} else {
|
|
201
|
+
evaluateHookPayload("{}");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
78
205
|
// Import the production core engine
|
|
79
206
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
80
207
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
@@ -154,6 +281,13 @@ function resolveConfigPath() {
|
|
|
154
281
|
return configPath;
|
|
155
282
|
}
|
|
156
283
|
|
|
284
|
+
function isCatastrophicRegex(pattern) {
|
|
285
|
+
if (!pattern || typeof pattern !== 'string' || pattern.length > 500) return true;
|
|
286
|
+
if (/(\([^)]*[+*][^)]*\))[+*?{]/.test(pattern)) return true;
|
|
287
|
+
if (/(\.\*){3,}/.test(pattern)) return true;
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
157
291
|
function loadCustomRules() {
|
|
158
292
|
const license = checkLicenseStatus();
|
|
159
293
|
const configPath = resolveConfigPath();
|
|
@@ -164,12 +298,20 @@ function loadCustomRules() {
|
|
|
164
298
|
const rules = config.customRules || [];
|
|
165
299
|
if (rules.length > 0) {
|
|
166
300
|
if (license.isPro) {
|
|
167
|
-
return rules.map(r =>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
301
|
+
return rules.map(r => {
|
|
302
|
+
const pat = r.pattern || r.regex || "";
|
|
303
|
+
if (!pat || isCatastrophicRegex(pat)) return null;
|
|
304
|
+
try {
|
|
305
|
+
return {
|
|
306
|
+
pattern: pat,
|
|
307
|
+
label: (r.label || r.category || "CUSTOM").toUpperCase(),
|
|
308
|
+
regex: new RegExp(pat, "gi"),
|
|
309
|
+
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
310
|
+
};
|
|
311
|
+
} catch (_) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}).filter(Boolean);
|
|
173
315
|
} else {
|
|
174
316
|
process.stderr.write(`⚠️ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier.\n👉 Set PRIVACYSCRUBBER_KEY to your PRO license key.\n`);
|
|
175
317
|
}
|
|
@@ -246,34 +388,6 @@ function performSanitization(text) {
|
|
|
246
388
|
if (result && result.tokenMap) {
|
|
247
389
|
Object.assign(localSessionMap, result.tokenMap);
|
|
248
390
|
}
|
|
249
|
-
|
|
250
|
-
if (!license.isPro && result.tokenMap) {
|
|
251
|
-
// Free tier: check for secrets and warn
|
|
252
|
-
let hasSecrets = false;
|
|
253
|
-
const SECRET_TOKEN_RE = /^\[(?:SECRET|API_TOKEN|AWS_KEY|JWT_TOKEN|STRIPE_KEY|OPENAI_KEY|DB_URI)_\d+\]$/;
|
|
254
|
-
Object.entries(result.tokenMap).forEach(([token, original]) => {
|
|
255
|
-
let isSecret = SECRET_TOKEN_RE.test(token);
|
|
256
|
-
if (!isSecret) {
|
|
257
|
-
for (const detector of DEVOPS_SECRETS_DETECTOR) {
|
|
258
|
-
detector.regex.lastIndex = 0;
|
|
259
|
-
if (detector.regex.test(original)) {
|
|
260
|
-
isSecret = true;
|
|
261
|
-
break;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (isSecret) {
|
|
266
|
-
hasSecrets = true;
|
|
267
|
-
result.scrubbedText = result.scrubbedText.replaceAll(token, '[SECRET_BLOCKED_UPGRADE_TO_PRO]');
|
|
268
|
-
delete result.tokenMap[token];
|
|
269
|
-
delete localSessionMap[token];
|
|
270
|
-
}
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
if (hasSecrets) {
|
|
274
|
-
process.stderr.write(`\x1b[1;31m🚫 [PrivacyScrubber] Secrets/Keys detected in output. Replaced with [SECRET_BLOCKED_UPGRADE_TO_PRO] to protect your data (Requires PRO tier).\x1b[0m\n`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
391
|
|
|
278
392
|
return result.scrubbedText + (wasTruncated ? `\n\n[PrivacyScrubber Free Tier: Payload truncated to ${limit.toLocaleString()} characters. Upgrade to PRO ($15/mo or $110 Lifetime) or Developer SDK ($299/mo) for unlimited payload processing: https://privacyscrubber.com/pricing]` : '');
|
|
279
393
|
}
|
|
@@ -338,11 +452,27 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
338
452
|
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
339
453
|
};
|
|
340
454
|
|
|
455
|
+
const agenticProtocol = {
|
|
456
|
+
protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
|
|
457
|
+
agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
|
|
458
|
+
ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
|
|
459
|
+
regulatorySafeHarbor: [
|
|
460
|
+
'GDPR-Art-4(5)-Pseudonymization',
|
|
461
|
+
'GDPR-Art-25-Privacy-by-Design',
|
|
462
|
+
'GDPR-Art-32-TOMs',
|
|
463
|
+
'EU-AI-Act-Art-50',
|
|
464
|
+
'EU-AI-Act-Art-17-EN-18286-QMS',
|
|
465
|
+
'CNIL-ICO-Agentic-Memory-Compartmentalization',
|
|
466
|
+
'IL-PPL-Amendment-13-Sec-17'
|
|
467
|
+
],
|
|
468
|
+
zeroCleartextEgress: true
|
|
469
|
+
};
|
|
470
|
+
|
|
341
471
|
const statutoryCitations = [
|
|
342
472
|
{
|
|
343
|
-
statute: 'EU AI Act (Art. 50)',
|
|
344
|
-
scope: 'AI Transparency &
|
|
345
|
-
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
|
|
473
|
+
statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
|
|
474
|
+
scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
|
|
475
|
+
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
|
|
346
476
|
},
|
|
347
477
|
{
|
|
348
478
|
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
@@ -374,6 +504,7 @@ function buildCisoAuditTelemetry(currentTokenMap = {}) {
|
|
|
374
504
|
frameworksList: Array.from(frameworksSet),
|
|
375
505
|
statutoryCitations,
|
|
376
506
|
ipHoldings,
|
|
507
|
+
agenticProtocol,
|
|
377
508
|
multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
|
|
378
509
|
};
|
|
379
510
|
}
|
|
@@ -453,10 +584,26 @@ if (isRestoreMode) {
|
|
|
453
584
|
let tokenMap = {};
|
|
454
585
|
if (tokenMapInput) {
|
|
455
586
|
try {
|
|
587
|
+
const safeReviver = (k, v) => (k === '__proto__' || k === 'constructor' || k === 'prototype') ? undefined : v;
|
|
456
588
|
if (tokenMapInput.trim().startsWith('{')) {
|
|
457
|
-
tokenMap = JSON.parse(tokenMapInput);
|
|
458
|
-
} else
|
|
459
|
-
|
|
589
|
+
tokenMap = JSON.parse(tokenMapInput, safeReviver);
|
|
590
|
+
} else {
|
|
591
|
+
const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
|
|
592
|
+
const resolvedMapPath = path.resolve(workspaceRoot, tokenMapInput);
|
|
593
|
+
const relToRoot = path.relative(workspaceRoot, resolvedMapPath);
|
|
594
|
+
if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
|
|
595
|
+
throw new Error('Security policy violation: --token-map path resolves outside active workspace.');
|
|
596
|
+
}
|
|
597
|
+
if (fs.existsSync(resolvedMapPath)) {
|
|
598
|
+
const lstat = fs.lstatSync(resolvedMapPath);
|
|
599
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
600
|
+
throw new Error('Security policy violation: --token-map must be a regular non-symlink file.');
|
|
601
|
+
}
|
|
602
|
+
if (lstat.size > 10 * 1024 * 1024) {
|
|
603
|
+
throw new Error('Token map file exceeds 10MB limit.');
|
|
604
|
+
}
|
|
605
|
+
tokenMap = JSON.parse(fs.readFileSync(resolvedMapPath, 'utf8'), safeReviver);
|
|
606
|
+
}
|
|
460
607
|
}
|
|
461
608
|
} catch (e) {
|
|
462
609
|
console.error(`Error parsing token map: ${e.message}`);
|
|
@@ -526,6 +673,11 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
526
673
|
if (targets.includes("cursor")) {
|
|
527
674
|
fs.writeFileSync(path.join(targetDir, '.cursorrules'), RULE_TEXT, 'utf8');
|
|
528
675
|
generatedFiles.push('.cursorrules');
|
|
676
|
+
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
677
|
+
if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
678
|
+
const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) — Mandatory PII & Secret Guard\nglobs: "**/*"\nalwaysApply: true\n---\n\n${RULE_TEXT}`;
|
|
679
|
+
fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
|
|
680
|
+
generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
|
|
529
681
|
}
|
|
530
682
|
if (targets.includes("windsurf")) {
|
|
531
683
|
fs.writeFileSync(path.join(targetDir, '.windsurfrules'), RULE_TEXT, 'utf8');
|
|
@@ -546,6 +698,21 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
546
698
|
generatedFiles.push('CLAUDE.md');
|
|
547
699
|
}
|
|
548
700
|
}
|
|
701
|
+
if (targets.includes("agents_md") || targets.includes("codex") || rulesTargets.includes("all")) {
|
|
702
|
+
const agentsPath = path.join(targetDir, 'AGENTS.md');
|
|
703
|
+
if (fs.existsSync(agentsPath)) {
|
|
704
|
+
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
705
|
+
if (!existing.includes("Zero-Trust Agentic Guard")) {
|
|
706
|
+
fs.writeFileSync(agentsPath, existing.trimEnd() + "\n\n" + RULE_TEXT, 'utf8');
|
|
707
|
+
generatedFiles.push('AGENTS.md (appended)');
|
|
708
|
+
} else {
|
|
709
|
+
generatedFiles.push('AGENTS.md (already present)');
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
fs.writeFileSync(agentsPath, RULE_TEXT, 'utf8');
|
|
713
|
+
generatedFiles.push('AGENTS.md');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
549
716
|
if (targets.includes("copilot")) {
|
|
550
717
|
const copilotDir = path.join(targetDir, '.github');
|
|
551
718
|
if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
|
|
@@ -556,6 +723,46 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
556
723
|
fs.writeFileSync(path.join(targetDir, '.clinerules'), RULE_TEXT, 'utf8');
|
|
557
724
|
generatedFiles.push('.clinerules');
|
|
558
725
|
}
|
|
726
|
+
if (args.includes("--hooks") || targets.includes("hooks") || rulesTargets.includes("all")) {
|
|
727
|
+
try {
|
|
728
|
+
const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
|
|
729
|
+
fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
|
|
730
|
+
let cursorHooks = { version: 1, hooks: {} };
|
|
731
|
+
if (fs.existsSync(cursorHooksPath)) {
|
|
732
|
+
try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
|
|
733
|
+
}
|
|
734
|
+
if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
|
|
735
|
+
const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
|
|
736
|
+
for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
|
|
737
|
+
if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
|
|
738
|
+
if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
|
|
739
|
+
cursorHooks.hooks[ev].push({ command: hookCmd });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
|
|
743
|
+
generatedFiles.push('.cursor/hooks.json');
|
|
744
|
+
|
|
745
|
+
const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
|
|
746
|
+
fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
|
|
747
|
+
let claudeSettings = {};
|
|
748
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
749
|
+
try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
|
|
750
|
+
}
|
|
751
|
+
if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
|
|
752
|
+
if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
|
|
753
|
+
const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
|
|
754
|
+
Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
|
|
755
|
+
);
|
|
756
|
+
if (!hasClaudeHook) {
|
|
757
|
+
claudeSettings.hooks.PreToolUse.push({
|
|
758
|
+
matcher: "Read|Bash",
|
|
759
|
+
hooks: [{ type: "command", command: hookCmd }]
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
|
|
763
|
+
generatedFiles.push('.claude/settings.json');
|
|
764
|
+
} catch (_) {}
|
|
765
|
+
}
|
|
559
766
|
|
|
560
767
|
console.log(`[Zero-Trust Agentic Guard: Agent Rules Generated]`);
|
|
561
768
|
console.log(`Workspace: ${targetDir}`);
|
|
@@ -612,8 +819,183 @@ if (isDiffMode) {
|
|
|
612
819
|
}
|
|
613
820
|
}
|
|
614
821
|
|
|
615
|
-
// Mode
|
|
616
|
-
if (
|
|
822
|
+
// Mode 2.5: Transparent MCP JSON-RPC 2.0 Stdio Proxy Wrapper (--wrap)
|
|
823
|
+
if (isWrapMode) {
|
|
824
|
+
if (wrapCommandArgs.length === 0) {
|
|
825
|
+
console.error('Error: --wrap requires an upstream MCP server command (e.g. npx ps-guard --wrap -- npx -y @modelcontextprotocol/server-postgres ...)');
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const SKIP_SANITIZE_KEYS = new Set([
|
|
830
|
+
'jsonrpc', 'id', 'method', 'type', 'mimeType', 'uri', 'name', 'role',
|
|
831
|
+
'protocolVersion', 'cursor', 'nextCursor', 'isError', 'version'
|
|
832
|
+
]);
|
|
833
|
+
|
|
834
|
+
const deepDetokenizeParams = (node, depth = 0) => {
|
|
835
|
+
if (depth > 20 || node === null || node === undefined) return node;
|
|
836
|
+
if (typeof node === 'string') {
|
|
837
|
+
if (Object.keys(localSessionMap).length === 0) return node;
|
|
838
|
+
const res = PrivacyScrubberCore.unscrubText(node, localSessionMap);
|
|
839
|
+
return (res && typeof res.restoredText === 'string') ? res.restoredText : node;
|
|
840
|
+
}
|
|
841
|
+
if (Array.isArray(node)) {
|
|
842
|
+
return node.map(item => deepDetokenizeParams(item, depth + 1));
|
|
843
|
+
}
|
|
844
|
+
if (typeof node === 'object') {
|
|
845
|
+
const out = {};
|
|
846
|
+
for (const [k, v] of Object.entries(node)) {
|
|
847
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
848
|
+
out[k] = deepDetokenizeParams(v, depth + 1);
|
|
849
|
+
}
|
|
850
|
+
return out;
|
|
851
|
+
}
|
|
852
|
+
return node;
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const deepSanitizeResult = (node, keyName = '', depth = 0) => {
|
|
856
|
+
if (depth > 20 || node === null || node === undefined) return node;
|
|
857
|
+
if (typeof node === 'string') {
|
|
858
|
+
if (SKIP_SANITIZE_KEYS.has(keyName) || node.length < 4) return node;
|
|
859
|
+
return performSanitization(node);
|
|
860
|
+
}
|
|
861
|
+
if (Array.isArray(node)) {
|
|
862
|
+
return node.map(item => deepSanitizeResult(item, keyName, depth + 1));
|
|
863
|
+
}
|
|
864
|
+
if (typeof node === 'object') {
|
|
865
|
+
const out = {};
|
|
866
|
+
for (const [k, v] of Object.entries(node)) {
|
|
867
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
868
|
+
if (k === 'inputSchema' || k === 'outputSchema' || k === 'capabilities' || k === 'serverInfo' || k === '_meta') {
|
|
869
|
+
out[k] = v;
|
|
870
|
+
} else {
|
|
871
|
+
out[k] = deepSanitizeResult(v, k, depth + 1);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return out;
|
|
875
|
+
}
|
|
876
|
+
return node;
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
const zeroizeSessionMap = () => {
|
|
880
|
+
for (const k of Object.keys(localSessionMap)) {
|
|
881
|
+
localSessionMap[k] = '';
|
|
882
|
+
delete localSessionMap[k];
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const [cmdBin, ...cmdRest] = wrapCommandArgs;
|
|
887
|
+
const child = spawn(cmdBin, cmdRest, {
|
|
888
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
889
|
+
shell: process.platform === 'win32'
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
// Intercept client -> upstream MCP server (JSON-RPC requests on stdin)
|
|
893
|
+
const rlClientIn = readline.createInterface({
|
|
894
|
+
input: process.stdin,
|
|
895
|
+
terminal: false
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
rlClientIn.on('line', (line) => {
|
|
899
|
+
if (!child.stdin || child.stdin.destroyed) return;
|
|
900
|
+
const trimmed = line.trim();
|
|
901
|
+
if (!trimmed.startsWith('{')) {
|
|
902
|
+
child.stdin.write(line + '\n');
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const msg = JSON.parse(trimmed);
|
|
907
|
+
if (msg && typeof msg === 'object' && msg.params && (msg.method === 'tools/call' || msg.method === 'prompts/get' || msg.method === 'resources/read' || msg.method === 'sampling/createMessage')) {
|
|
908
|
+
msg.params = deepDetokenizeParams(msg.params);
|
|
909
|
+
child.stdin.write(JSON.stringify(msg) + '\n');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
} catch (_) {}
|
|
913
|
+
child.stdin.write(line + '\n');
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
rlClientIn.on('close', () => {
|
|
917
|
+
if (child.stdin && !child.stdin.destroyed) {
|
|
918
|
+
child.stdin.end();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// Intercept upstream MCP server -> client (JSON-RPC responses/notifications on stdout)
|
|
923
|
+
const rlUpstreamOut = readline.createInterface({
|
|
924
|
+
input: child.stdout,
|
|
925
|
+
terminal: false
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
rlUpstreamOut.on('line', (line) => {
|
|
929
|
+
const trimmed = line.trim();
|
|
930
|
+
if (!trimmed.startsWith('{')) {
|
|
931
|
+
process.stdout.write(performSanitization(line) + '\n');
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
const msg = JSON.parse(trimmed);
|
|
936
|
+
if (msg && typeof msg === 'object') {
|
|
937
|
+
if (msg.result && typeof msg.result === 'object') {
|
|
938
|
+
const isSchemaListing = Array.isArray(msg.result.tools) || Array.isArray(msg.result.prompts) || Array.isArray(msg.result.resourceTemplates) || Boolean(msg.result.protocolVersion);
|
|
939
|
+
if (!isSchemaListing) {
|
|
940
|
+
msg.result = deepSanitizeResult(msg.result);
|
|
941
|
+
const receipt = buildPsr2Receipt(localSessionMap);
|
|
942
|
+
const telemetry = buildCisoAuditTelemetry(localSessionMap);
|
|
943
|
+
msg.result._meta = Object.assign({}, msg.result._meta || {}, {
|
|
944
|
+
ztds_receipt: {
|
|
945
|
+
version: receipt.version,
|
|
946
|
+
timestamp: receipt.timestamp,
|
|
947
|
+
manifestSha256: receipt.manifestSha256,
|
|
948
|
+
receiptSha256: receipt.receiptSha256,
|
|
949
|
+
entityCounts: receipt.entityCounts,
|
|
950
|
+
tokensTotal: receipt.tokensTotal,
|
|
951
|
+
riskLevel: telemetry.riskLevel,
|
|
952
|
+
regulatorySafeHarbor: telemetry.agenticProtocol ? telemetry.agenticProtocol.regulatorySafeHarbor : [],
|
|
953
|
+
zero_cleartext_egress: true
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
} else if (msg.params && typeof msg.params === 'object') {
|
|
958
|
+
msg.params = deepSanitizeResult(msg.params);
|
|
959
|
+
}
|
|
960
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
} catch (_) {}
|
|
964
|
+
process.stdout.write(performSanitization(line) + '\n');
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
const rlUpstreamErr = readline.createInterface({
|
|
968
|
+
input: child.stderr,
|
|
969
|
+
terminal: false
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
rlUpstreamErr.on('line', (line) => {
|
|
973
|
+
process.stderr.write(performSanitization(line) + '\n');
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
const handleSignal = (sig) => {
|
|
977
|
+
zeroizeSessionMap();
|
|
978
|
+
if (!child.killed) {
|
|
979
|
+
try { child.kill(sig); } catch (_) {}
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
process.on('SIGINT', () => handleSignal('SIGINT'));
|
|
983
|
+
process.on('SIGTERM', () => handleSignal('SIGTERM'));
|
|
984
|
+
process.on('exit', zeroizeSessionMap);
|
|
985
|
+
|
|
986
|
+
child.on('close', (code) => {
|
|
987
|
+
emitProofFeedback();
|
|
988
|
+
zeroizeSessionMap();
|
|
989
|
+
process.exit(code || 0);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
child.on('error', (err) => {
|
|
993
|
+
zeroizeSessionMap();
|
|
994
|
+
console.error(`Error spawning wrapped MCP server: ${err.message}`);
|
|
995
|
+
process.exit(1);
|
|
996
|
+
});
|
|
997
|
+
} else if (hasStdin && !hasCommand && !isRestoreMode && !isDiffMode && !isRulesMode) {
|
|
998
|
+
// Mode 3: Stdin Stream Mode (pipe)
|
|
617
999
|
if (isJsonMode) {
|
|
618
1000
|
let rawInput = '';
|
|
619
1001
|
process.stdin.setEncoding('utf8');
|