@privacyscrubber/mcp-server 1.6.8 ā 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/.well-known/mcp/server-card.json +21 -3
- package/bundle.mcpb +0 -0
- package/index.js +27 -4
- package/manifest.json +4 -42
- package/package.json +4 -2
- package/pii-masking-run.js +238 -0
- package/scrubber-core.cjs +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"serverInfo": {
|
|
3
3
|
"name": "privacyscrubber/pii-masking-mcp",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.9"
|
|
5
5
|
},
|
|
6
6
|
"tools": [
|
|
7
7
|
{
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"profile": {
|
|
18
18
|
"type": "string",
|
|
19
|
-
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles. Defaults to 'General'."
|
|
19
|
+
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"required": ["text"]
|
|
@@ -48,11 +48,29 @@
|
|
|
48
48
|
},
|
|
49
49
|
"profile": {
|
|
50
50
|
"type": "string",
|
|
51
|
-
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles. Defaults to 'General'."
|
|
51
|
+
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
|
|
52
52
|
}
|
|
53
53
|
},
|
|
54
54
|
"required": ["file_path"]
|
|
55
55
|
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "create_default_config",
|
|
59
|
+
"description": "Creates a default 'privacyscrubber.json' configuration file in the active workspace root directory if one does not exist. Includes template structures for custom regex rules and exclusion bypass patterns.",
|
|
60
|
+
"inputSchema": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {},
|
|
63
|
+
"required": []
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "check_status",
|
|
68
|
+
"description": "Returns the current PrivacyScrubber MCP tier, session usage, available profiles, and PRO upgrade instructions. Call this to see your license status or get setup help.",
|
|
69
|
+
"inputSchema": {
|
|
70
|
+
"type": "object",
|
|
71
|
+
"properties": {},
|
|
72
|
+
"required": []
|
|
73
|
+
}
|
|
56
74
|
}
|
|
57
75
|
],
|
|
58
76
|
"resources": [],
|
package/bundle.mcpb
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -44,6 +44,7 @@ const colors = {
|
|
|
44
44
|
yellowBold: '\x1b[1;33m',
|
|
45
45
|
yellow: '\x1b[33m',
|
|
46
46
|
cyan: '\x1b[36m',
|
|
47
|
+
greenBold: '\x1b[1;32m',
|
|
47
48
|
redBold: '\x1b[1;31m',
|
|
48
49
|
reset: '\x1b[0m'
|
|
49
50
|
};
|
|
@@ -315,11 +316,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
315
316
|
}
|
|
316
317
|
|
|
317
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
|
+
|
|
318
323
|
return {
|
|
319
324
|
content: [
|
|
320
325
|
{
|
|
321
326
|
type: "text",
|
|
322
|
-
text:
|
|
327
|
+
text: restoredText
|
|
323
328
|
}
|
|
324
329
|
]
|
|
325
330
|
};
|
|
@@ -644,6 +649,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
644
649
|
"label": "CUSTOM_TAG",
|
|
645
650
|
"_comment": "pattern must be a valid Javascript regex string. label is the tag replacement (e.g. CUSTOM_TAG)"
|
|
646
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"
|
|
647
665
|
]
|
|
648
666
|
};
|
|
649
667
|
|
|
@@ -694,7 +712,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
694
712
|
rulesStatus = 'š Ignored (requires PRO)';
|
|
695
713
|
}
|
|
696
714
|
} else {
|
|
697
|
-
rulesStatus = '
|
|
715
|
+
rulesStatus = 'None';
|
|
716
|
+
configPathSnippet = 'Run create_default_config';
|
|
698
717
|
}
|
|
699
718
|
|
|
700
719
|
const metricsSummary = getSessionMetricsSummary();
|
|
@@ -837,7 +856,8 @@ function detectSecrets(text) {
|
|
|
837
856
|
|
|
838
857
|
function performSanitization(text, profile) {
|
|
839
858
|
const customRules = loadCustomRules();
|
|
840
|
-
const
|
|
859
|
+
const normalizedProfile = (profile || "general").trim().toLowerCase();
|
|
860
|
+
const result = PrivacyScrubberCore.scrubText(text, customRules, {}, normalizedProfile, sessionMap);
|
|
841
861
|
|
|
842
862
|
const license = checkLicenseStatus();
|
|
843
863
|
if (!license.isPro && result.tokenMap) {
|
|
@@ -881,7 +901,10 @@ function truncateIfFree(text, isPro) {
|
|
|
881
901
|
|
|
882
902
|
// Start the server transport
|
|
883
903
|
const transport = new StdioServerTransport();
|
|
884
|
-
server.connect(transport).
|
|
904
|
+
server.connect(transport).then(() => {
|
|
905
|
+
process.stderr.write(`${colors.greenBold}ā
PrivacyScrubber ZTDS MCP Server started successfully.${colors.reset}\n`);
|
|
906
|
+
process.stderr.write(`${colors.cyan}ā Star us on GitHub: https://github.com/moxno/privacyscrubber-mcp${colors.reset}\n`);
|
|
907
|
+
}).catch((error) => {
|
|
885
908
|
console.error("Failed to connect MCP server transport:", error);
|
|
886
909
|
process.exit(1);
|
|
887
910
|
});
|
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": "1.6.
|
|
4
|
+
"version": "1.6.9",
|
|
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",
|
|
@@ -21,53 +21,15 @@
|
|
|
21
21
|
"tools": [
|
|
22
22
|
{
|
|
23
23
|
"name": "sanitize_text",
|
|
24
|
-
"description": "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."
|
|
25
|
-
"inputSchema": {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"properties": {
|
|
28
|
-
"text": {
|
|
29
|
-
"type": "string",
|
|
30
|
-
"description": "The raw text, code, or logs to sanitize."
|
|
31
|
-
},
|
|
32
|
-
"profile": {
|
|
33
|
-
"type": "string",
|
|
34
|
-
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
"required": ["text"]
|
|
38
|
-
}
|
|
24
|
+
"description": "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."
|
|
39
25
|
},
|
|
40
26
|
{
|
|
41
27
|
"name": "reveal_text",
|
|
42
|
-
"description": "Replaces masked tokens (e.g., [EMAIL_1], [API_KEY_1]) in the LLM's response back with the original private data from the local volatile RAM-only session map."
|
|
43
|
-
"inputSchema": {
|
|
44
|
-
"type": "object",
|
|
45
|
-
"properties": {
|
|
46
|
-
"text": {
|
|
47
|
-
"type": "string",
|
|
48
|
-
"description": "The AI generated response containing placeholders to restore."
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
"required": ["text"]
|
|
52
|
-
}
|
|
28
|
+
"description": "Replaces masked tokens (e.g., [EMAIL_1], [API_KEY_1]) in the LLM's response back with the original private data from the local volatile RAM-only session map."
|
|
53
29
|
},
|
|
54
30
|
{
|
|
55
31
|
"name": "sanitize_file",
|
|
56
|
-
"description": "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory."
|
|
57
|
-
"inputSchema": {
|
|
58
|
-
"type": "object",
|
|
59
|
-
"properties": {
|
|
60
|
-
"file_path": {
|
|
61
|
-
"type": "string",
|
|
62
|
-
"description": "Absolute path to the file to sanitize."
|
|
63
|
-
},
|
|
64
|
-
"profile": {
|
|
65
|
-
"type": "string",
|
|
66
|
-
"description": "The detection profile to use. Available: 'General' (Free), or PRO profiles: 'Dev' (Engineering/Code), 'Medical', 'Pharma', 'Legal', 'Compliance', 'CCPA', 'Finance', 'Bizops', 'Sales', 'WealthMgmt', 'Insurance', 'Accounting', 'HR', 'Security', 'Marketing', 'Support', 'RealEstate', 'Agents', 'Academic', 'Creative', 'Tech', 'Personal'. Defaults to 'General'."
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
"required": ["file_path"]
|
|
70
|
-
}
|
|
32
|
+
"description": "Reads a local file, sanitizes its contents using the selected profile, and outputs the safe version for AI analysis. Securely keeps original identifiers in memory."
|
|
71
33
|
}
|
|
72
34
|
],
|
|
73
35
|
"user_config": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "1.
|
|
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
|
+
});
|