@privacyscrubber/mcp-server 1.6.9 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -316,11 +316,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
316
316
  }
317
317
 
318
318
  const restored = PrivacyScrubberCore.unscrubText(text, sessionMap);
319
+ let restoredText = restored.restoredText;
320
+ // Deduplicate common double-prefixed schemas resulting from LLM prefix reconstruction
321
+ restoredText = restoredText.replace(/\b(mysql|postgresql|postgres|redis|mongodb|https?|ftp|ssh|git|aws):\/\/\1:\/\//gi, '$1://');
322
+
319
323
  return {
320
324
  content: [
321
325
  {
322
326
  type: "text",
323
- text: restored.restoredText
327
+ text: restoredText
324
328
  }
325
329
  ]
326
330
  };
@@ -645,6 +649,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
645
649
  "label": "CUSTOM_TAG",
646
650
  "_comment": "pattern must be a valid Javascript regex string. label is the tag replacement (e.g. CUSTOM_TAG)"
647
651
  }
652
+ ],
653
+ "exclusions": [
654
+ "localhost",
655
+ "127.0.0.1",
656
+ "0.0.0.0",
657
+ "process.env",
658
+ "PORT",
659
+ "node_modules",
660
+ "PrivacyScrubber",
661
+ "console.log",
662
+ "console.error",
663
+ "utf-8",
664
+ "utf8"
648
665
  ]
649
666
  };
650
667
 
@@ -695,7 +712,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
695
712
  rulesStatus = 'šŸ”’ Ignored (requires PRO)';
696
713
  }
697
714
  } else {
698
- rulesStatus = 'Not found';
715
+ rulesStatus = 'None';
716
+ configPathSnippet = 'Run create_default_config';
699
717
  }
700
718
 
701
719
  const metricsSummary = getSessionMetricsSummary();
@@ -838,7 +856,8 @@ function detectSecrets(text) {
838
856
 
839
857
  function performSanitization(text, profile) {
840
858
  const customRules = loadCustomRules();
841
- const result = PrivacyScrubberCore.scrubText(text, customRules, {}, profile, sessionMap);
859
+ const normalizedProfile = (profile || "general").trim().toLowerCase();
860
+ const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap);
842
861
 
843
862
  const license = checkLicenseStatus();
844
863
  if (!license.isPro && result.tokenMap) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/mcp-server",
3
- "version": "1.6.9",
3
+ "version": "1.7.0",
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",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "files": [
12
12
  "index.js",
13
+ "pii-masking-run.js",
13
14
  "scrubber-core.cjs",
14
15
  "smithery.yaml",
15
16
  "server.json",
@@ -19,7 +20,8 @@
19
20
  ],
20
21
  "bin": {
21
22
  "mcp-server": "index.js",
22
- "privacyscrubber-mcp": "index.js"
23
+ "privacyscrubber-mcp": "index.js",
24
+ "pii-masking-run": "pii-masking-run.js"
23
25
  },
24
26
  "scripts": {
25
27
  "start": "node index.js"
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * PrivacyScrubber command runner wrapper: pii-masking-run
5
+ *
6
+ * Runs any local shell command, intercepts its output (stdout/stderr) line-by-line,
7
+ * sanitizes any PII/Secrets locally, and writes the clean stream to the terminal.
8
+ *
9
+ * Usage: pii-masking-run [options] -- <command> [args...]
10
+ */
11
+
12
+ import { spawn } from 'child_process';
13
+ import { createRequire } from 'module';
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import crypto from 'crypto';
17
+ import readline from 'readline';
18
+ import { fileURLToPath } from 'url';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+
24
+ // Early check for --version or --help to avoid loading heavy dependencies
25
+ const args = process.argv.slice(2);
26
+ const cmdIndex = args.indexOf("--");
27
+
28
+ if (args.includes("--version") || args.includes("-v")) {
29
+ const pkgPath = path.resolve(__dirname, './package.json');
30
+ let version = '1.7.0';
31
+ try { version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version; } catch (_) {}
32
+ console.log(`pii-masking-run v${version} (PrivacyScrubber MCP)`);
33
+ process.exit(0);
34
+ }
35
+
36
+ if (cmdIndex === -1 || args.includes("-h") || args.includes("--help")) {
37
+ console.log("PrivacyScrubber CLI Command Runner: pii-masking-run");
38
+ console.log("\nUsage:");
39
+ console.log(" npx pii-masking-run [options] -- <command> [args...]");
40
+ console.log("\nOptions:");
41
+ console.log(" --profile <name> Specify detection profile (e.g. General, Medical, Legal, Dev, HR)");
42
+ console.log(" --key <licKey> PrivacyScrubber PRO License Key");
43
+ console.log(" -v, --version Print version");
44
+ console.log(" -h, --help Show this help screen");
45
+ console.log("\nExamples:");
46
+ console.log(" npx pii-masking-run -- cat database-dump.sql");
47
+ console.log(" npx pii-masking-run --profile dev -- npm test");
48
+ process.exit(0);
49
+ }
50
+
51
+ // Import the production core engine
52
+ const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
53
+ const PrivacyScrubberCore = require(scrubberCorePath);
54
+
55
+ PrivacyScrubberCore.init();
56
+
57
+ // ANSI terminal colors
58
+ const colors = {
59
+ yellowBold: '\x1b[1;33m',
60
+ cyan: '\x1b[36m',
61
+ reset: '\x1b[0m'
62
+ };
63
+
64
+ const DEVOPS_SECRETS_DETECTOR = [
65
+ { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g },
66
+ { name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g },
67
+ { name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g },
68
+ { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g },
69
+ { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi }
70
+ ];
71
+
72
+ const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
73
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy1jZ9X4rO5C9U0qK57V1
74
+ hJoYSkE4vLpcVYOt1FhRwEHs1APDSyss0HixboLz2eW2XQf2NbwajWtNlyxvgczO
75
+ KE6ClnLomtsaKywwqB4alzdYnnnFJttFPjwmgPSO7D9AgN9sYaVkXOaOFrIZ90Ng
76
+ TRhSHUeL7ReltWlCHwz9xf5m2FrKtxr2VBlEoyPjsFzalHMey1EX+yXe81zM7IIi
77
+ t1Z8agLzo7WIfNBAIWmRlerTplaFFZrQgdF5g/Y0n8IIMZOtadgoY8E855psDNZV
78
+ 7wIDAQAB
79
+ -----END PUBLIC KEY-----`;
80
+
81
+ function checkLicenseStatus() {
82
+ const key = (process.env.PRIVACYSCRUBBER_KEY || "").trim();
83
+ if (!key) return { isPro: false, error: "No license key provided." };
84
+
85
+ try {
86
+ const [payloadBase64, signatureBase64] = key.split('.');
87
+ const verifier = crypto.createVerify('SHA256');
88
+ verifier.update(payloadBase64);
89
+ const isVerified = verifier.verify(PUBLIC_KEY, signatureBase64, 'base64');
90
+
91
+ if (!isVerified) return { isPro: false, error: "Signature verification failed." };
92
+
93
+ const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString('utf8'));
94
+ if (payload.expires && payload.expires < Math.floor(Date.now() / 1000)) {
95
+ return {
96
+ isPro: false,
97
+ error: `License expired on ${new Date(payload.expires * 1000).toLocaleDateString()}`
98
+ };
99
+ }
100
+ return { isPro: true, error: null };
101
+ } catch (e) {
102
+ return { isPro: false, error: "Error parsing license: " + e.message };
103
+ }
104
+ }
105
+
106
+ function resolveConfigPath() {
107
+ let configPath = path.resolve(process.cwd(), 'privacyscrubber.json');
108
+ if (!fs.existsSync(configPath)) {
109
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
110
+ if (homeDir) {
111
+ configPath = path.resolve(homeDir, 'privacyscrubber.json');
112
+ }
113
+ }
114
+ return configPath;
115
+ }
116
+
117
+ function loadCustomRules() {
118
+ const license = checkLicenseStatus();
119
+ const configPath = resolveConfigPath();
120
+
121
+ if (fs.existsSync(configPath)) {
122
+ try {
123
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
124
+ const rules = config.customRules || [];
125
+ if (rules.length > 0) {
126
+ if (license.isPro) {
127
+ return rules.map(r => ({
128
+ pattern: r.pattern || r.regex || "",
129
+ label: (r.label || r.category || "CUSTOM").toUpperCase(),
130
+ regex: r.pattern || r.regex || "",
131
+ category: (r.label || r.category || "CUSTOM").toUpperCase()
132
+ })).filter(r => r.pattern);
133
+ } else {
134
+ 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`);
135
+ }
136
+ }
137
+ } catch (e) {
138
+ process.stderr.write(`Error parsing privacyscrubber.json: ${e.message}\n`);
139
+ }
140
+ }
141
+ return [];
142
+ }
143
+
144
+ // Local runtime variables
145
+ let profile = "General";
146
+ let key = process.env.PRIVACYSCRUBBER_KEY || "";
147
+
148
+ const optionsArgs = args.slice(0, cmdIndex);
149
+ const commandArgs = args.slice(cmdIndex + 1);
150
+
151
+ if (commandArgs.length === 0) {
152
+ console.error("Error: No command specified after '--'.");
153
+ process.exit(1);
154
+ }
155
+
156
+ // Parse custom command-line options
157
+ for (let i = 0; i < optionsArgs.length; i++) {
158
+ if (optionsArgs[i] === "--profile" && optionsArgs[i + 1]) {
159
+ profile = optionsArgs[i + 1];
160
+ i++;
161
+ } else if (optionsArgs[i] === "--key" && optionsArgs[i + 1]) {
162
+ key = optionsArgs[i + 1];
163
+ i++;
164
+ }
165
+ }
166
+
167
+ if (key) {
168
+ process.env.PRIVACYSCRUBBER_KEY = key;
169
+ }
170
+
171
+ const customRules = loadCustomRules();
172
+ const localSessionMap = {};
173
+
174
+ function performSanitization(text) {
175
+ const normalizedProfile = (profile || "general").trim().toLowerCase();
176
+ const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, localSessionMap);
177
+ const license = checkLicenseStatus();
178
+
179
+ if (!license.isPro && result.tokenMap) {
180
+ // Free tier: check for secrets and warn
181
+ let hasSecrets = false;
182
+ Object.entries(result.tokenMap).forEach(([token, original]) => {
183
+ let isSecret = false;
184
+ for (const detector of DEVOPS_SECRETS_DETECTOR) {
185
+ detector.regex.lastIndex = 0;
186
+ if (detector.regex.test(original)) {
187
+ isSecret = true;
188
+ break;
189
+ }
190
+ }
191
+ if (isSecret) {
192
+ hasSecrets = true;
193
+ result.scrubbedText = result.scrubbedText.replace(token, original);
194
+ }
195
+ });
196
+
197
+ if (hasSecrets) {
198
+ process.stderr.write(`${colors.yellowBold}āš ļø [PrivacyScrubber] Secrets/Keys detected in output. Redaction skipped (Requires PRO tier).${colors.reset}\n`);
199
+ }
200
+ }
201
+
202
+ return result.scrubbedText;
203
+ }
204
+
205
+ // Spawn child process capturing output
206
+ const child = spawn(commandArgs[0], commandArgs.slice(1), {
207
+ stdio: ['inherit', 'pipe', 'pipe'],
208
+ shell: true
209
+ });
210
+
211
+ const rlStdout = readline.createInterface({
212
+ input: child.stdout,
213
+ terminal: false
214
+ });
215
+
216
+ rlStdout.on('line', (line) => {
217
+ const sanitized = performSanitization(line);
218
+ process.stdout.write(sanitized + '\n');
219
+ });
220
+
221
+ const rlStderr = readline.createInterface({
222
+ input: child.stderr,
223
+ terminal: false
224
+ });
225
+
226
+ rlStderr.on('line', (line) => {
227
+ const sanitized = performSanitization(line);
228
+ process.stderr.write(sanitized + '\n');
229
+ });
230
+
231
+ child.on('close', (code) => {
232
+ process.exit(code || 0);
233
+ });
234
+
235
+ child.on('error', (err) => {
236
+ console.error(`Error spawning command: ${err.message}`);
237
+ process.exit(1);
238
+ });