@privacyscrubber/mcp-server 2.4.3 → 2.4.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/README.md +1 -1
- package/index.js +134 -19
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/pii-masking-run.js +1 -1
- 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/.well-known/mcp/server-card.json +0 -78
- package/LICENSE +0 -23
package/README.md
CHANGED
|
@@ -482,7 +482,7 @@ Returns a visual dashboard showing your current tier, session request count, act
|
|
|
482
482
|
* **Response Example (Free Tier):**
|
|
483
483
|
```
|
|
484
484
|
╔══════════════════════════════════════════════════╗
|
|
485
|
-
║ PrivacyScrubber MCP Server v2.4.
|
|
485
|
+
║ PrivacyScrubber MCP Server v2.4.5 ║
|
|
486
486
|
╠══════════════════════════════════════════════════╣
|
|
487
487
|
║ 🔓 Tier: FREE ║
|
|
488
488
|
║ 📊 Session requests: 5 ║
|
package/index.js
CHANGED
|
@@ -2178,6 +2178,35 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
|
2178
2178
|
throw new Error(`Resource not found: ${uri}`);
|
|
2179
2179
|
});
|
|
2180
2180
|
|
|
2181
|
+
function resolveWorkspaceSafePath(inputPath, paramName = "file_path") {
|
|
2182
|
+
const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
|
|
2183
|
+
const resolvedPath = path.resolve(workspaceRoot, inputPath);
|
|
2184
|
+
const relToRoot = path.relative(workspaceRoot, resolvedPath);
|
|
2185
|
+
if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
|
|
2186
|
+
return {
|
|
2187
|
+
ok: false,
|
|
2188
|
+
error: `Error: Security policy violation — target '${paramName}' resolves outside the active workspace directory (path traversal blocked).`
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
let checkCursor = resolvedPath;
|
|
2192
|
+
while (!fs.existsSync(checkCursor)) {
|
|
2193
|
+
const parentCursor = path.dirname(checkCursor);
|
|
2194
|
+
if (parentCursor === checkCursor) break;
|
|
2195
|
+
checkCursor = parentCursor;
|
|
2196
|
+
}
|
|
2197
|
+
if (fs.existsSync(checkCursor)) {
|
|
2198
|
+
const realCheck = fs.realpathSync(checkCursor);
|
|
2199
|
+
const relReal = path.relative(workspaceRoot, realCheck);
|
|
2200
|
+
if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
|
|
2201
|
+
return {
|
|
2202
|
+
ok: false,
|
|
2203
|
+
error: `Error: Security policy violation — target path symlink escapes the active workspace directory.`
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
return { ok: true, resolvedPath, workspaceRoot };
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2181
2210
|
// Handle tool execution calls
|
|
2182
2211
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2183
2212
|
const { name, arguments: args } = request.params;
|
|
@@ -2457,7 +2486,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2457
2486
|
}
|
|
2458
2487
|
|
|
2459
2488
|
if (name === "sanitize_file" || name === "scrub_file") {
|
|
2460
|
-
const rawPath = args
|
|
2489
|
+
const rawPath = args?.file_path || args?.filePath;
|
|
2461
2490
|
if (!rawPath) {
|
|
2462
2491
|
return {
|
|
2463
2492
|
isError: true,
|
|
@@ -2474,8 +2503,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2474
2503
|
const targetSession = getSessionMap(session_id);
|
|
2475
2504
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
2476
2505
|
const filePath = rawPath;
|
|
2477
|
-
|
|
2478
|
-
|
|
2506
|
+
const safeTarget = resolveWorkspaceSafePath(filePath, "file_path");
|
|
2507
|
+
if (!safeTarget.ok) {
|
|
2508
|
+
return {
|
|
2509
|
+
isError: true,
|
|
2510
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
2511
|
+
};
|
|
2512
|
+
}
|
|
2513
|
+
const resolvedPath = safeTarget.resolvedPath;
|
|
2479
2514
|
if (!fs.existsSync(resolvedPath)) {
|
|
2480
2515
|
return {
|
|
2481
2516
|
isError: true,
|
|
@@ -2882,23 +2917,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2882
2917
|
}
|
|
2883
2918
|
|
|
2884
2919
|
if (name === "redact_file") {
|
|
2885
|
-
const { file_path, dry_run, no_backup } = args;
|
|
2886
|
-
if (!file_path
|
|
2920
|
+
const { file_path, dry_run, no_backup } = args || {};
|
|
2921
|
+
if (!file_path) {
|
|
2922
|
+
return { isError: true, content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }] };
|
|
2923
|
+
}
|
|
2924
|
+
const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
|
|
2925
|
+
if (!safeTarget.ok) {
|
|
2926
|
+
return { isError: true, content: [{ type: "text", text: safeTarget.error }] };
|
|
2927
|
+
}
|
|
2928
|
+
const resolvedRedactPath = safeTarget.resolvedPath;
|
|
2929
|
+
if (!fs.existsSync(resolvedRedactPath)) {
|
|
2887
2930
|
return { isError: true, content: [{ type: "text", text: `Error: File not found at path: ${file_path}` }] };
|
|
2888
2931
|
}
|
|
2889
2932
|
|
|
2890
2933
|
try {
|
|
2891
|
-
const stat = await fs.promises.stat(
|
|
2934
|
+
const stat = await fs.promises.stat(resolvedRedactPath);
|
|
2892
2935
|
if (stat.size > 50 * 1024 * 1024) {
|
|
2893
2936
|
return { isError: true, content: [{ type: "text", text: `Error: File is too large (>50MB).` }] };
|
|
2894
2937
|
}
|
|
2895
2938
|
|
|
2896
|
-
const buffer = fs.readFileSync(
|
|
2939
|
+
const buffer = fs.readFileSync(resolvedRedactPath);
|
|
2897
2940
|
if (buffer.indexOf(0) !== -1) {
|
|
2898
2941
|
return { isError: true, content: [{ type: "text", text: `Error: Cannot redact binary file.` }] };
|
|
2899
2942
|
}
|
|
2900
2943
|
let content = buffer.toString('utf8');
|
|
2901
|
-
const ext = path.extname(
|
|
2944
|
+
const ext = path.extname(resolvedRedactPath).toLowerCase();
|
|
2902
2945
|
if (ext === ".eml") {
|
|
2903
2946
|
const decodeQpUtf8 = (str) =>
|
|
2904
2947
|
str.replace(/=\r?\n/g, "").replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
|
|
@@ -2992,15 +3035,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2992
3035
|
}
|
|
2993
3036
|
|
|
2994
3037
|
if (!no_backup) {
|
|
2995
|
-
fs.writeFileSync(`${
|
|
3038
|
+
fs.writeFileSync(`${resolvedRedactPath}.bak`, content, 'utf8');
|
|
2996
3039
|
}
|
|
2997
3040
|
|
|
2998
|
-
fs.writeFileSync(
|
|
3041
|
+
fs.writeFileSync(resolvedRedactPath, result.scrubbedText, 'utf8');
|
|
2999
3042
|
|
|
3000
3043
|
return {
|
|
3001
3044
|
content: [{
|
|
3002
3045
|
type: "text",
|
|
3003
|
-
text: `✅ File successfully redacted.\nReplaced ${Object.keys(localSessionMap).length} tokens (${summary}).\n${!no_backup ? `Backup saved to ${
|
|
3046
|
+
text: `✅ File successfully redacted.\nReplaced ${Object.keys(localSessionMap).length} tokens (${summary}).\n${!no_backup ? `Backup saved to ${resolvedRedactPath}.bak` : "No backup created (no_backup=true)."}`
|
|
3004
3047
|
}]
|
|
3005
3048
|
};
|
|
3006
3049
|
|
|
@@ -3168,8 +3211,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3168
3211
|
}
|
|
3169
3212
|
|
|
3170
3213
|
if (name === "audit_directory_for_pii") {
|
|
3171
|
-
const { directory_path, extensions, ignore_node_modules } = args;
|
|
3172
|
-
if (!directory_path
|
|
3214
|
+
const { directory_path, extensions, ignore_node_modules } = args || {};
|
|
3215
|
+
if (!directory_path) {
|
|
3216
|
+
return { isError: true, content: [{ type: "text", text: "Error: Missing required parameter 'directory_path'." }] };
|
|
3217
|
+
}
|
|
3218
|
+
const safeDir = resolveWorkspaceSafePath(directory_path, "directory_path");
|
|
3219
|
+
if (!safeDir.ok) {
|
|
3220
|
+
return { isError: true, content: [{ type: "text", text: safeDir.error }] };
|
|
3221
|
+
}
|
|
3222
|
+
if (!fs.existsSync(safeDir.resolvedPath)) {
|
|
3173
3223
|
return { isError: true, content: [{ type: "text", text: `Error: Directory not found at path: ${directory_path}` }] };
|
|
3174
3224
|
}
|
|
3175
3225
|
|
|
@@ -3302,7 +3352,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3302
3352
|
// Transparent in-memory unmasking of any token placeholders before local execution
|
|
3303
3353
|
const rawExecutableCommand = reverseTokens(command, targetSession);
|
|
3304
3354
|
|
|
3305
|
-
|
|
3355
|
+
let execCwd = process.cwd();
|
|
3356
|
+
if (cwd) {
|
|
3357
|
+
const safeCwd = resolveWorkspaceSafePath(cwd, "cwd");
|
|
3358
|
+
if (!safeCwd.ok) {
|
|
3359
|
+
return { isError: true, content: [{ type: "text", text: safeCwd.error }] };
|
|
3360
|
+
}
|
|
3361
|
+
execCwd = safeCwd.resolvedPath;
|
|
3362
|
+
}
|
|
3306
3363
|
let stdout = '';
|
|
3307
3364
|
let stderr = '';
|
|
3308
3365
|
let exitCode = 0;
|
|
@@ -3358,7 +3415,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3358
3415
|
content: [{ type: "text", text: "Error: Missing required parameter 'file_path'." }]
|
|
3359
3416
|
};
|
|
3360
3417
|
}
|
|
3361
|
-
const
|
|
3418
|
+
const safeTarget = resolveWorkspaceSafePath(file_path, "file_path");
|
|
3419
|
+
if (!safeTarget.ok) {
|
|
3420
|
+
return {
|
|
3421
|
+
isError: true,
|
|
3422
|
+
content: [{ type: "text", text: safeTarget.error }]
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
const resolvedPath = safeTarget.resolvedPath;
|
|
3362
3426
|
if (!fs.existsSync(resolvedPath)) {
|
|
3363
3427
|
return {
|
|
3364
3428
|
isError: true,
|
|
@@ -3464,7 +3528,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3464
3528
|
const { staged = false, cwd, session_id = "default" } = args || {};
|
|
3465
3529
|
const targetSession = getSessionMap(session_id);
|
|
3466
3530
|
const targetIgnoreList = getSessionIgnoreList(session_id);
|
|
3467
|
-
|
|
3531
|
+
let repoCwd = process.cwd();
|
|
3532
|
+
if (cwd) {
|
|
3533
|
+
const safeCwd = resolveWorkspaceSafePath(cwd, "cwd");
|
|
3534
|
+
if (!safeCwd.ok) {
|
|
3535
|
+
return { isError: true, content: [{ type: "text", text: safeCwd.error }] };
|
|
3536
|
+
}
|
|
3537
|
+
repoCwd = safeCwd.resolvedPath;
|
|
3538
|
+
}
|
|
3468
3539
|
const license = checkLicenseStatus();
|
|
3469
3540
|
const limitStatus = checkFreeTierLimit(license.isPro);
|
|
3470
3541
|
if (limitStatus.blocked) {
|
|
@@ -3528,7 +3599,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3528
3599
|
};
|
|
3529
3600
|
}
|
|
3530
3601
|
|
|
3531
|
-
const
|
|
3602
|
+
const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
|
|
3603
|
+
const resolvedPath = path.resolve(workspaceRoot, file_path);
|
|
3604
|
+
const relToRoot = path.relative(workspaceRoot, resolvedPath);
|
|
3605
|
+
if (relToRoot.startsWith("..") || path.isAbsolute(relToRoot)) {
|
|
3606
|
+
return {
|
|
3607
|
+
isError: true,
|
|
3608
|
+
content: [{ type: "text", text: "Error: Security policy violation — target 'file_path' resolves outside the active workspace directory (path traversal blocked)." }]
|
|
3609
|
+
};
|
|
3610
|
+
}
|
|
3611
|
+
let checkCursor = resolvedPath;
|
|
3612
|
+
while (!fs.existsSync(checkCursor)) {
|
|
3613
|
+
const parentCursor = path.dirname(checkCursor);
|
|
3614
|
+
if (parentCursor === checkCursor) break;
|
|
3615
|
+
checkCursor = parentCursor;
|
|
3616
|
+
}
|
|
3617
|
+
if (fs.existsSync(checkCursor)) {
|
|
3618
|
+
const realCheck = fs.realpathSync(checkCursor);
|
|
3619
|
+
const relReal = path.relative(workspaceRoot, realCheck);
|
|
3620
|
+
if (relReal.startsWith("..") || path.isAbsolute(relReal)) {
|
|
3621
|
+
return {
|
|
3622
|
+
isError: true,
|
|
3623
|
+
content: [{ type: "text", text: "Error: Security policy violation — target path symlink escapes the active workspace directory." }]
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3532
3628
|
const targetSession = getSessionMap(session_id);
|
|
3533
3629
|
const restored = PrivacyScrubberCore.unscrubText(content, targetSession);
|
|
3534
3630
|
const authenticContent = restored.restoredText;
|
|
@@ -3561,7 +3657,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3561
3657
|
|
|
3562
3658
|
if (name === "create_agent_rules") {
|
|
3563
3659
|
const { agent_types = ["all"], workspace_dir } = args || {};
|
|
3564
|
-
|
|
3660
|
+
let targetDir = process.cwd();
|
|
3661
|
+
if (workspace_dir) {
|
|
3662
|
+
const safeWorkspace = resolveWorkspaceSafePath(workspace_dir, "workspace_dir");
|
|
3663
|
+
if (!safeWorkspace.ok) {
|
|
3664
|
+
return { isError: true, content: [{ type: "text", text: safeWorkspace.error }] };
|
|
3665
|
+
}
|
|
3666
|
+
targetDir = safeWorkspace.resolvedPath;
|
|
3667
|
+
}
|
|
3565
3668
|
const targets = agent_types.includes("all") ? ["cursor", "windsurf", "claude_code", "copilot", "cline"] : agent_types;
|
|
3566
3669
|
|
|
3567
3670
|
const ruleText = `# PrivacyScrubber Zero-Trust Agentic Guard (CISO Security Rules)
|
|
@@ -4051,6 +4154,18 @@ function getSessionMetricsSummary() {
|
|
|
4051
4154
|
return parts.join(', ');
|
|
4052
4155
|
}
|
|
4053
4156
|
|
|
4157
|
+
function isCatastrophicRegex(pat) {
|
|
4158
|
+
if (!pat || typeof pat !== 'string' || pat.length > 500) return true;
|
|
4159
|
+
const nestedQuantifierRegex = /\([^)]*(\+|\*|\{\d+,?\d*\})\)[+*]|\([^)]*(\+|\*|\{\d+,?\d*\})\)\{\d+,?\d*\}|\([^)]*\|[^)]*\)(?:[+*]|\{\d+,?\d*\})/;
|
|
4160
|
+
if (nestedQuantifierRegex.test(pat)) return true;
|
|
4161
|
+
try {
|
|
4162
|
+
new RegExp(pat, 'gi');
|
|
4163
|
+
return false;
|
|
4164
|
+
} catch (_) {
|
|
4165
|
+
return true;
|
|
4166
|
+
}
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4054
4169
|
function loadCustomRules() {
|
|
4055
4170
|
const license = checkLicenseStatus();
|
|
4056
4171
|
const configPath = resolveConfigPath();
|
|
@@ -4066,7 +4181,7 @@ function loadCustomRules() {
|
|
|
4066
4181
|
label: (r.label || r.category || "CUSTOM").toUpperCase(),
|
|
4067
4182
|
regex: r.pattern || r.regex || "",
|
|
4068
4183
|
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
4069
|
-
})).filter(r => r.pattern);
|
|
4184
|
+
})).filter(r => r.pattern && !isCatastrophicRegex(r.pattern));
|
|
4070
4185
|
} else {
|
|
4071
4186
|
mcpLog(`${colors.yellowBold}⚠️ [PrivacyScrubber] Custom rules detected in privacyscrubber.json, but are ignored in the Free Tier.${colors.reset}\n${colors.cyan}👉 Set PRIVACYSCRUBBER_KEY to your PRO license key.${colors.reset}\n`);
|
|
4072
4187
|
}
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.5",
|
|
5
5
|
"description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "PrivacyScrubber",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
4
4
|
"mcpName": "io.github.moxno/privacyscrubber-mcp",
|
|
5
5
|
"description": "CISO-Approved Zero-Trust PII & Secrets Redaction MCP Server for Cursor, Windsurf, and Claude Desktop. Prevent API leaks and comply with HIPAA/SOC 2 locally.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"start": "node index.js",
|
|
37
|
-
"build": "node -e \"const fs=require('fs'); if(fs.existsSync('../scripts/build-mcp.js')) require('child_process').execSync('node ../scripts/build-mcp.js', {stdio:'inherit'}); else console.log('MCP assets are pre-built.');\"",
|
|
37
|
+
"build": "node -e \"const fs=require('fs'); if(fs.existsSync('../scripts/build-mcp.js')) require('child_process').execSync('node ../scripts/build-mcp.js --keep-obfuscated', {stdio:'inherit'}); else console.log('MCP assets are pre-built.');\"",
|
|
38
38
|
"test": "node test-mcp.js && node test-mcp-deep.js && node test-mcp-streaming.js",
|
|
39
39
|
"test:stream": "node test-mcp-streaming.js",
|
|
40
|
-
"prepublishOnly": "npm test"
|
|
40
|
+
"prepublishOnly": "npm run build && node -e \"const fs=require('fs'); for(const f of ['ps-license-manager.js','ps-license-manager.cjs','ps-pii-engine.js','ps-pii-engine.cjs','scrubber-core.cjs']){const c=fs.readFileSync(f,'utf8'); if(c.includes('OFFLINE_KEY_SALT')||c.includes('computeV2Signature')||!c.includes('_0x')){console.error('FATAL: Unobfuscated '+f+' detected!'); process.exit(1);}}\" && npm test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
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.5';
|
|
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);
|