@privacyscrubber/mcp-server 2.4.5 → 2.4.6
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 +542 -155
- package/install.js +93 -2
- package/manifest.json +8 -10
- package/package.json +1 -3
- package/pii-masking-run.js +202 -39
- 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/install.js
CHANGED
|
@@ -234,6 +234,8 @@ export function parseArgs(rawArgs = process.argv.slice(2)) {
|
|
|
234
234
|
dryRun: false,
|
|
235
235
|
remove: false,
|
|
236
236
|
all: false,
|
|
237
|
+
rules: false,
|
|
238
|
+
hooks: false,
|
|
237
239
|
targets: [],
|
|
238
240
|
help: false
|
|
239
241
|
};
|
|
@@ -242,6 +244,8 @@ export function parseArgs(rawArgs = process.argv.slice(2)) {
|
|
|
242
244
|
if (arg === '--dry-run') options.dryRun = true;
|
|
243
245
|
else if (arg === '--remove' || arg === '--uninstall') options.remove = true;
|
|
244
246
|
else if (arg === '--all') options.all = true;
|
|
247
|
+
else if (arg === '--rules') options.rules = true;
|
|
248
|
+
else if (arg === '--hooks') options.hooks = true;
|
|
245
249
|
else if (arg === '--claude') options.targets.push('claude');
|
|
246
250
|
else if (arg === '--claude-code') options.targets.push('claude-code', 'claude-code-project');
|
|
247
251
|
else if (arg === '--cursor') options.targets.push('cursor', 'cursor-storage', 'cursor-workspace');
|
|
@@ -249,7 +253,11 @@ export function parseArgs(rawArgs = process.argv.slice(2)) {
|
|
|
249
253
|
else if (arg === '--zed') options.targets.push('zed');
|
|
250
254
|
else if (arg === '--windsurf') options.targets.push('windsurf');
|
|
251
255
|
else if (arg === '--cline') options.targets.push('cline', 'roo');
|
|
252
|
-
else if (arg === '--workspace')
|
|
256
|
+
else if (arg === '--workspace') {
|
|
257
|
+
options.targets.push('cursor-workspace', 'claude-code-project', 'vscode');
|
|
258
|
+
options.rules = true;
|
|
259
|
+
options.hooks = true;
|
|
260
|
+
}
|
|
253
261
|
else if (arg === '--help' || arg === '-h') options.help = true;
|
|
254
262
|
}
|
|
255
263
|
|
|
@@ -327,7 +335,8 @@ Options:
|
|
|
327
335
|
--zed Configure Zed IDE (~/.config/zed/settings.json)
|
|
328
336
|
--windsurf Configure Windsurf only
|
|
329
337
|
--cline Configure Cline / Roo-Code only
|
|
330
|
-
--workspace Configure workspace paths (.cursor, .mcp.json, .vscode)
|
|
338
|
+
--workspace Configure workspace paths (.cursor, .mcp.json, .vscode) & agent rules
|
|
339
|
+
--rules Scaffold native AI agent rules (.cursor/rules/privacyscrubber.mdc, AGENTS.md)
|
|
331
340
|
--dry-run Preview configuration changes without writing files
|
|
332
341
|
--remove Remove PrivacyScrubber from all target client configurations
|
|
333
342
|
-h, --help Display this help guide
|
|
@@ -400,6 +409,88 @@ Options:
|
|
|
400
409
|
console.log('');
|
|
401
410
|
}
|
|
402
411
|
|
|
412
|
+
if (options.rules && !options.remove) {
|
|
413
|
+
const cwd = process.cwd();
|
|
414
|
+
const mdcPath = path.join(cwd, '.cursor', 'rules', 'privacyscrubber.mdc');
|
|
415
|
+
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
416
|
+
const ruleBody = [
|
|
417
|
+
'# PrivacyScrubber Zero-Trust Agentic Guard (ZTDS)',
|
|
418
|
+
'1. Never read `.env`, `*secret*`, `*credentials*`, `*.pem`, `*.key`, or database dumps with native `read_file` / `cat`. Call `guard_read_file` or `sanitize_file`.',
|
|
419
|
+
'2. Route shell commands that may print credentials or PII through `guard_exec`, and inspect git diffs via `guard_git_diff`.',
|
|
420
|
+
'3. Keep tokens like `[API_KEY_1]`, `[EMAIL_1]` intact during reasoning. Restore final answers via `reveal_text` or write to disk via `guard_apply_patch`.'
|
|
421
|
+
].join('\n');
|
|
422
|
+
const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) — Mandatory PII & Secret Guard\nglobs: "**/*"\nalwaysApply: true\n---\n\n${ruleBody}\n`;
|
|
423
|
+
|
|
424
|
+
if (options.dryRun) {
|
|
425
|
+
console.log(`[DRY-RUN OK]: Would scaffold agent rules in ${mdcPath} and ${agentsMdPath}\n`);
|
|
426
|
+
} else {
|
|
427
|
+
try {
|
|
428
|
+
fs.mkdirSync(path.dirname(mdcPath), { recursive: true });
|
|
429
|
+
if (!fs.existsSync(mdcPath) || !fs.readFileSync(mdcPath, 'utf8').includes('PrivacyScrubber Zero-Trust Agentic Guard')) {
|
|
430
|
+
fs.writeFileSync(mdcPath, mdcContent, 'utf8');
|
|
431
|
+
console.log(`[RULES] Created ${mdcPath}`);
|
|
432
|
+
}
|
|
433
|
+
if (!fs.existsSync(agentsMdPath)) {
|
|
434
|
+
fs.writeFileSync(agentsMdPath, ruleBody + '\n', 'utf8');
|
|
435
|
+
console.log(`[RULES] Created ${agentsMdPath}`);
|
|
436
|
+
} else {
|
|
437
|
+
const existingAgents = fs.readFileSync(agentsMdPath, 'utf8');
|
|
438
|
+
if (!existingAgents.includes('PrivacyScrubber Zero-Trust Agentic Guard')) {
|
|
439
|
+
fs.writeFileSync(agentsMdPath, existingAgents.trimEnd() + '\n\n' + ruleBody + '\n', 'utf8');
|
|
440
|
+
console.log(`[RULES] Updated ${agentsMdPath}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} catch (_) {}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (options.hooks && !options.remove) {
|
|
448
|
+
const cwd = process.cwd();
|
|
449
|
+
const cursorHooksPath = path.join(cwd, '.cursor', 'hooks.json');
|
|
450
|
+
const claudeSettingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
451
|
+
const hookCmd = 'npx -y @privacyscrubber/mcp-server --hook';
|
|
452
|
+
|
|
453
|
+
if (options.dryRun) {
|
|
454
|
+
console.log(`[DRY-RUN OK]: Would scaffold IDE lifecycle hooks in ${cursorHooksPath} and ${claudeSettingsPath}\n`);
|
|
455
|
+
} else {
|
|
456
|
+
try {
|
|
457
|
+
fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
|
|
458
|
+
let cursorHooks = { version: 1, hooks: {} };
|
|
459
|
+
if (fs.existsSync(cursorHooksPath)) {
|
|
460
|
+
try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
|
|
461
|
+
}
|
|
462
|
+
if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
|
|
463
|
+
for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
|
|
464
|
+
if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
|
|
465
|
+
if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
|
|
466
|
+
cursorHooks.hooks[ev].push({ command: hookCmd });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
|
|
470
|
+
console.log(`[HOOKS] Configured ${cursorHooksPath}`);
|
|
471
|
+
|
|
472
|
+
fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
|
|
473
|
+
let claudeSettings = {};
|
|
474
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
475
|
+
try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
|
|
476
|
+
}
|
|
477
|
+
if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
|
|
478
|
+
if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
|
|
479
|
+
const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
|
|
480
|
+
Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
|
|
481
|
+
);
|
|
482
|
+
if (!hasClaudeHook) {
|
|
483
|
+
claudeSettings.hooks.PreToolUse.push({
|
|
484
|
+
matcher: 'Read|Bash',
|
|
485
|
+
hooks: [{ type: 'command', command: hookCmd }]
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
|
|
489
|
+
console.log(`[HOOKS] Configured ${claudeSettingsPath}`);
|
|
490
|
+
} catch (_) {}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
403
494
|
console.log('--------------------------------------------------');
|
|
404
495
|
if (configuredCount > 0) {
|
|
405
496
|
console.log(`[SUMMARY] ${options.remove ? 'Cleaned' : 'Configured'} ${configuredCount} AI client(s) successfully.`);
|
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.6",
|
|
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",
|
|
@@ -30,21 +30,17 @@
|
|
|
30
30
|
"description": "STEP 3: Call this last. Restores masked tokens back with original private data from the local volatile RAM-only session map."
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
"name": "
|
|
34
|
-
"description": "STEP
|
|
33
|
+
"name": "stream_reveal_text",
|
|
34
|
+
"description": "STEP 3 (Streaming): Real-time streaming token detokenizer for LLM Server-Sent Events (SSE) and stdio chunk pipelines."
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
|
-
"name": "
|
|
38
|
-
"description": "
|
|
37
|
+
"name": "audit_context",
|
|
38
|
+
"description": "STEP 0 (Pre-Flight): Non-destructive security inspection of raw prompts or document chunks before sending to LLMs. Evaluates PII, secrets, risk severity, and triggered regulatory frameworks."
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"name": "sanitize_file",
|
|
42
42
|
"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."
|
|
43
43
|
},
|
|
44
|
-
{
|
|
45
|
-
"name": "scrub_file",
|
|
46
|
-
"description": "Alias for sanitize_file."
|
|
47
|
-
},
|
|
48
44
|
{
|
|
49
45
|
"name": "audit_directory_for_pii",
|
|
50
46
|
"description": "Scans a local directory for leaks of secrets, keys, and PII. Returns a structured security summary report."
|
|
@@ -116,7 +112,9 @@
|
|
|
116
112
|
{
|
|
117
113
|
"name": "generate_procurement_memo",
|
|
118
114
|
"description": "Zero-Trust Enterprise Procurement: Generates an executive-ready Procurement & ROI Memorandum for CISOs, DPOs, and VP of Engineering."
|
|
119
|
-
}
|
|
115
|
+
}
|
|
116
|
+
],
|
|
117
|
+
"prompts": [
|
|
120
118
|
{
|
|
121
119
|
"name": "secure_ai_workflow",
|
|
122
120
|
"description": "Zero-Trust Data Sanitization workflow instruction to automatically scrub PII before processing data."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@privacyscrubber/mcp-server",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
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",
|
|
@@ -13,10 +13,8 @@
|
|
|
13
13
|
"install.js",
|
|
14
14
|
"pii-masking-run.js",
|
|
15
15
|
"scrubber-core.cjs",
|
|
16
|
-
"ps-license-manager.js",
|
|
17
16
|
"ps-license-manager.cjs",
|
|
18
17
|
"ps-pii-engine.cjs",
|
|
19
|
-
"ps-pii-engine.js",
|
|
20
18
|
"config-schema.json",
|
|
21
19
|
"smithery.yaml",
|
|
22
20
|
"server.json",
|
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.6';
|
|
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,7 @@ 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");
|
|
38
39
|
const isJsonMode = args.includes("--json");
|
|
39
40
|
const isRestoreMode = args.includes("--restore") || args.includes("--unscrub");
|
|
40
41
|
let tokenMapInput = null;
|
|
@@ -42,10 +43,15 @@ const tokenMapIdx = args.indexOf("--token-map");
|
|
|
42
43
|
if (tokenMapIdx !== -1 && args[tokenMapIdx + 1]) {
|
|
43
44
|
tokenMapInput = args[tokenMapIdx + 1];
|
|
44
45
|
}
|
|
46
|
+
let hookPayloadArg = null;
|
|
47
|
+
const payloadIdx = args.indexOf("--payload");
|
|
48
|
+
if (payloadIdx !== -1 && args[payloadIdx + 1]) {
|
|
49
|
+
hookPayloadArg = args[payloadIdx + 1];
|
|
50
|
+
}
|
|
45
51
|
const hasCommand = cmdIndex !== -1 && args.slice(cmdIndex + 1).length > 0;
|
|
46
52
|
const hasStdin = !process.stdin.isTTY;
|
|
47
53
|
|
|
48
|
-
if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isRestoreMode && !hasStdin)) {
|
|
54
|
+
if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMode && !isRulesMode && !isHookMode && !isRestoreMode && !hasStdin)) {
|
|
49
55
|
console.log("PrivacyScrubber CLI & Agentic Guard: ps-guard / pii-masking-run");
|
|
50
56
|
console.log("\nUsage:");
|
|
51
57
|
console.log(" npx ps-guard [options] -- <command> [args...]");
|
|
@@ -53,6 +59,7 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
|
|
|
53
59
|
console.log(" npx ps-guard --diff [--staged] [options]");
|
|
54
60
|
console.log(" npx ps-guard --restore --token-map <file|json> [options]");
|
|
55
61
|
console.log(" npx ps-guard --rules [targets]");
|
|
62
|
+
console.log(" npx ps-guard --hook (Claude Code & Cursor IDE PreToolUse / beforeReadFile hook)");
|
|
56
63
|
console.log("\nOptions:");
|
|
57
64
|
console.log(" --profile <name> Specify detection profile (e.g. General, Dev, Medical, Legal, HR)");
|
|
58
65
|
console.log(" --key <licKey> PrivacyScrubber PRO License Key");
|
|
@@ -62,6 +69,7 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
|
|
|
62
69
|
console.log(" --diff Sanitize git diff output before committing or sending to AI");
|
|
63
70
|
console.log(" --staged Use staged git changes with --diff");
|
|
64
71
|
console.log(" --rules [targets] Generate AI agent rules (.cursorrules, .windsurfrules, CLAUDE.md, copilot, cline)");
|
|
72
|
+
console.log(" --hook Evaluate IDE lifecycle hook JSON from stdin (Claude Code & Cursor)");
|
|
65
73
|
console.log(" -q, --quiet Suppress stderr proof telemetry");
|
|
66
74
|
console.log(" -v, --version Print version");
|
|
67
75
|
console.log(" -h, --help Show this help screen");
|
|
@@ -75,6 +83,98 @@ if (args.includes("-h") || args.includes("--help") || (!hasCommand && !isDiffMod
|
|
|
75
83
|
process.exit(0);
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
// Fast-path Mode 0: Deterministic IDE Lifecycle Hook (--hook) for Claude Code & Cursor v1.7+
|
|
87
|
+
if (isHookMode) {
|
|
88
|
+
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;
|
|
89
|
+
const SAFE_TEMPLATE_RX = /\.(?:example|sample|template|dist|schema)$/i;
|
|
90
|
+
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;
|
|
91
|
+
|
|
92
|
+
const evaluateHookPayload = (rawStr) => {
|
|
93
|
+
let payload = {};
|
|
94
|
+
try {
|
|
95
|
+
payload = rawStr && rawStr.trim() ? JSON.parse(rawStr) : {};
|
|
96
|
+
} catch (_) {
|
|
97
|
+
process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const eventName = String(payload.hook_event_name || payload.hookEventName || payload.event || "");
|
|
102
|
+
const toolName = String(payload.tool_name || payload.toolName || "");
|
|
103
|
+
const toolInput = (payload.tool_input || payload.toolInput || payload) || {};
|
|
104
|
+
|
|
105
|
+
const candidatePath = String(
|
|
106
|
+
toolInput.file_path || toolInput.path || toolInput.target_file || payload.file_path || payload.path || ""
|
|
107
|
+
).trim();
|
|
108
|
+
const candidateCmd = String(
|
|
109
|
+
toolInput.command || toolInput.cmd || payload.command || ""
|
|
110
|
+
).trim();
|
|
111
|
+
|
|
112
|
+
const isCursorEvent = /^(?:beforeReadFile|beforeShellExecution|beforeSubmitPrompt|beforeMCPExecution)$/i.test(eventName) ||
|
|
113
|
+
(!toolName && (payload.file_path !== undefined || payload.command !== undefined || payload.attachments !== undefined));
|
|
114
|
+
|
|
115
|
+
const isReadTool = /^(?:Read|View|read_file|view_file|ReadFile)$/i.test(toolName) || /^beforeReadFile$/i.test(eventName) || (!toolName && Boolean(candidatePath));
|
|
116
|
+
const isShellTool = /^(?:Bash|Execute|run_command| run_terminal_cmd)$/i.test(toolName) || /^beforeShellExecution$/i.test(eventName) || (!toolName && Boolean(candidateCmd));
|
|
117
|
+
|
|
118
|
+
if (isReadTool && candidatePath && SENSITIVE_FILE_RX.test(candidatePath) && !SAFE_TEMPLATE_RX.test(candidatePath)) {
|
|
119
|
+
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.`;
|
|
120
|
+
if (isCursorEvent) {
|
|
121
|
+
process.stdout.write(JSON.stringify({
|
|
122
|
+
permission: "deny",
|
|
123
|
+
userMessage: `[PrivacyScrubber ZTDS] Direct read of ${path.basename(candidatePath)} blocked to prevent cloud egress.`,
|
|
124
|
+
agentMessage: reason
|
|
125
|
+
}) + "\n");
|
|
126
|
+
process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
process.stdout.write(JSON.stringify({
|
|
129
|
+
permission: "deny",
|
|
130
|
+
hookSpecificOutput: {
|
|
131
|
+
hookEventName: eventName || "PreToolUse",
|
|
132
|
+
permissionDecision: "deny",
|
|
133
|
+
permissionDecisionReason: reason
|
|
134
|
+
}
|
|
135
|
+
}) + "\n");
|
|
136
|
+
process.stderr.write(reason + "\n");
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isShellTool && candidateCmd && SENSITIVE_CMD_RX.test(candidateCmd)) {
|
|
141
|
+
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.`;
|
|
142
|
+
if (isCursorEvent) {
|
|
143
|
+
process.stdout.write(JSON.stringify({
|
|
144
|
+
permission: "deny",
|
|
145
|
+
userMessage: `[PrivacyScrubber ZTDS] Shell command exposing raw credentials blocked.`,
|
|
146
|
+
agentMessage: reason
|
|
147
|
+
}) + "\n");
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
process.stdout.write(JSON.stringify({
|
|
151
|
+
permission: "deny",
|
|
152
|
+
hookSpecificOutput: {
|
|
153
|
+
hookEventName: eventName || "PreToolUse",
|
|
154
|
+
permissionDecision: "deny",
|
|
155
|
+
permissionDecisionReason: reason
|
|
156
|
+
}
|
|
157
|
+
}) + "\n");
|
|
158
|
+
process.stderr.write(reason + "\n");
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
process.stdout.write(JSON.stringify({ permission: "allow", continue: true }) + "\n");
|
|
163
|
+
process.exit(0);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (hookPayloadArg !== null) {
|
|
167
|
+
evaluateHookPayload(hookPayloadArg);
|
|
168
|
+
} else if (!process.stdin.isTTY) {
|
|
169
|
+
let buf = "";
|
|
170
|
+
process.stdin.setEncoding("utf8");
|
|
171
|
+
process.stdin.on("data", (c) => { buf += c; });
|
|
172
|
+
process.stdin.on("end", () => evaluateHookPayload(buf));
|
|
173
|
+
} else {
|
|
174
|
+
evaluateHookPayload("{}");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
78
178
|
// Import the production core engine
|
|
79
179
|
const scrubberCorePath = path.resolve(__dirname, './scrubber-core.cjs');
|
|
80
180
|
const PrivacyScrubberCore = require(scrubberCorePath);
|
|
@@ -154,6 +254,13 @@ function resolveConfigPath() {
|
|
|
154
254
|
return configPath;
|
|
155
255
|
}
|
|
156
256
|
|
|
257
|
+
function isCatastrophicRegex(pattern) {
|
|
258
|
+
if (!pattern || typeof pattern !== 'string' || pattern.length > 500) return true;
|
|
259
|
+
if (/(\([^)]*[+*][^)]*\))[+*?{]/.test(pattern)) return true;
|
|
260
|
+
if (/(\.\*){3,}/.test(pattern)) return true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
157
264
|
function loadCustomRules() {
|
|
158
265
|
const license = checkLicenseStatus();
|
|
159
266
|
const configPath = resolveConfigPath();
|
|
@@ -164,12 +271,20 @@ function loadCustomRules() {
|
|
|
164
271
|
const rules = config.customRules || [];
|
|
165
272
|
if (rules.length > 0) {
|
|
166
273
|
if (license.isPro) {
|
|
167
|
-
return rules.map(r =>
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
274
|
+
return rules.map(r => {
|
|
275
|
+
const pat = r.pattern || r.regex || "";
|
|
276
|
+
if (!pat || isCatastrophicRegex(pat)) return null;
|
|
277
|
+
try {
|
|
278
|
+
return {
|
|
279
|
+
pattern: pat,
|
|
280
|
+
label: (r.label || r.category || "CUSTOM").toUpperCase(),
|
|
281
|
+
regex: new RegExp(pat, "gi"),
|
|
282
|
+
category: (r.label || r.category || "CUSTOM").toUpperCase()
|
|
283
|
+
};
|
|
284
|
+
} catch (_) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}).filter(Boolean);
|
|
173
288
|
} else {
|
|
174
289
|
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
290
|
}
|
|
@@ -246,34 +361,6 @@ function performSanitization(text) {
|
|
|
246
361
|
if (result && result.tokenMap) {
|
|
247
362
|
Object.assign(localSessionMap, result.tokenMap);
|
|
248
363
|
}
|
|
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
364
|
|
|
278
365
|
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
366
|
}
|
|
@@ -453,10 +540,26 @@ if (isRestoreMode) {
|
|
|
453
540
|
let tokenMap = {};
|
|
454
541
|
if (tokenMapInput) {
|
|
455
542
|
try {
|
|
543
|
+
const safeReviver = (k, v) => (k === '__proto__' || k === 'constructor' || k === 'prototype') ? undefined : v;
|
|
456
544
|
if (tokenMapInput.trim().startsWith('{')) {
|
|
457
|
-
tokenMap = JSON.parse(tokenMapInput);
|
|
458
|
-
} else
|
|
459
|
-
|
|
545
|
+
tokenMap = JSON.parse(tokenMapInput, safeReviver);
|
|
546
|
+
} else {
|
|
547
|
+
const workspaceRoot = fs.existsSync(process.cwd()) ? fs.realpathSync(process.cwd()) : path.resolve(process.cwd());
|
|
548
|
+
const resolvedMapPath = path.resolve(workspaceRoot, tokenMapInput);
|
|
549
|
+
const relToRoot = path.relative(workspaceRoot, resolvedMapPath);
|
|
550
|
+
if (relToRoot.startsWith('..') || path.isAbsolute(relToRoot)) {
|
|
551
|
+
throw new Error('Security policy violation: --token-map path resolves outside active workspace.');
|
|
552
|
+
}
|
|
553
|
+
if (fs.existsSync(resolvedMapPath)) {
|
|
554
|
+
const lstat = fs.lstatSync(resolvedMapPath);
|
|
555
|
+
if (lstat.isSymbolicLink() || !lstat.isFile()) {
|
|
556
|
+
throw new Error('Security policy violation: --token-map must be a regular non-symlink file.');
|
|
557
|
+
}
|
|
558
|
+
if (lstat.size > 10 * 1024 * 1024) {
|
|
559
|
+
throw new Error('Token map file exceeds 10MB limit.');
|
|
560
|
+
}
|
|
561
|
+
tokenMap = JSON.parse(fs.readFileSync(resolvedMapPath, 'utf8'), safeReviver);
|
|
562
|
+
}
|
|
460
563
|
}
|
|
461
564
|
} catch (e) {
|
|
462
565
|
console.error(`Error parsing token map: ${e.message}`);
|
|
@@ -526,6 +629,11 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
526
629
|
if (targets.includes("cursor")) {
|
|
527
630
|
fs.writeFileSync(path.join(targetDir, '.cursorrules'), RULE_TEXT, 'utf8');
|
|
528
631
|
generatedFiles.push('.cursorrules');
|
|
632
|
+
const cursorRulesDir = path.join(targetDir, '.cursor', 'rules');
|
|
633
|
+
if (!fs.existsSync(cursorRulesDir)) fs.mkdirSync(cursorRulesDir, { recursive: true });
|
|
634
|
+
const mdcContent = `---\ndescription: PrivacyScrubber Zero-Trust Data Sanitization (ZTDS) — Mandatory PII & Secret Guard\nglobs: "**/*"\nalwaysApply: true\n---\n\n${RULE_TEXT}`;
|
|
635
|
+
fs.writeFileSync(path.join(cursorRulesDir, 'privacyscrubber.mdc'), mdcContent, 'utf8');
|
|
636
|
+
generatedFiles.push('.cursor/rules/privacyscrubber.mdc');
|
|
529
637
|
}
|
|
530
638
|
if (targets.includes("windsurf")) {
|
|
531
639
|
fs.writeFileSync(path.join(targetDir, '.windsurfrules'), RULE_TEXT, 'utf8');
|
|
@@ -546,6 +654,21 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
546
654
|
generatedFiles.push('CLAUDE.md');
|
|
547
655
|
}
|
|
548
656
|
}
|
|
657
|
+
if (targets.includes("agents_md") || targets.includes("codex") || rulesTargets.includes("all")) {
|
|
658
|
+
const agentsPath = path.join(targetDir, 'AGENTS.md');
|
|
659
|
+
if (fs.existsSync(agentsPath)) {
|
|
660
|
+
const existing = fs.readFileSync(agentsPath, 'utf8');
|
|
661
|
+
if (!existing.includes("Zero-Trust Agentic Guard")) {
|
|
662
|
+
fs.writeFileSync(agentsPath, existing.trimEnd() + "\n\n" + RULE_TEXT, 'utf8');
|
|
663
|
+
generatedFiles.push('AGENTS.md (appended)');
|
|
664
|
+
} else {
|
|
665
|
+
generatedFiles.push('AGENTS.md (already present)');
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
fs.writeFileSync(agentsPath, RULE_TEXT, 'utf8');
|
|
669
|
+
generatedFiles.push('AGENTS.md');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
549
672
|
if (targets.includes("copilot")) {
|
|
550
673
|
const copilotDir = path.join(targetDir, '.github');
|
|
551
674
|
if (!fs.existsSync(copilotDir)) fs.mkdirSync(copilotDir, { recursive: true });
|
|
@@ -556,6 +679,46 @@ Before reading sensitive files, running terminal commands that may print credent
|
|
|
556
679
|
fs.writeFileSync(path.join(targetDir, '.clinerules'), RULE_TEXT, 'utf8');
|
|
557
680
|
generatedFiles.push('.clinerules');
|
|
558
681
|
}
|
|
682
|
+
if (args.includes("--hooks") || targets.includes("hooks") || rulesTargets.includes("all")) {
|
|
683
|
+
try {
|
|
684
|
+
const cursorHooksPath = path.join(targetDir, '.cursor', 'hooks.json');
|
|
685
|
+
fs.mkdirSync(path.dirname(cursorHooksPath), { recursive: true });
|
|
686
|
+
let cursorHooks = { version: 1, hooks: {} };
|
|
687
|
+
if (fs.existsSync(cursorHooksPath)) {
|
|
688
|
+
try { cursorHooks = JSON.parse(fs.readFileSync(cursorHooksPath, 'utf8')); } catch (_) {}
|
|
689
|
+
}
|
|
690
|
+
if (!cursorHooks.hooks || typeof cursorHooks.hooks !== 'object') cursorHooks.hooks = {};
|
|
691
|
+
const hookCmd = "npx -y @privacyscrubber/mcp-server --hook";
|
|
692
|
+
for (const ev of ['beforeReadFile', 'beforeShellExecution']) {
|
|
693
|
+
if (!Array.isArray(cursorHooks.hooks[ev])) cursorHooks.hooks[ev] = [];
|
|
694
|
+
if (!cursorHooks.hooks[ev].some(h => h && h.command && h.command.includes('privacyscrubber'))) {
|
|
695
|
+
cursorHooks.hooks[ev].push({ command: hookCmd });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
fs.writeFileSync(cursorHooksPath, JSON.stringify(cursorHooks, null, 2) + '\n', 'utf8');
|
|
699
|
+
generatedFiles.push('.cursor/hooks.json');
|
|
700
|
+
|
|
701
|
+
const claudeSettingsPath = path.join(targetDir, '.claude', 'settings.json');
|
|
702
|
+
fs.mkdirSync(path.dirname(claudeSettingsPath), { recursive: true });
|
|
703
|
+
let claudeSettings = {};
|
|
704
|
+
if (fs.existsSync(claudeSettingsPath)) {
|
|
705
|
+
try { claudeSettings = JSON.parse(fs.readFileSync(claudeSettingsPath, 'utf8')); } catch (_) {}
|
|
706
|
+
}
|
|
707
|
+
if (!claudeSettings.hooks || typeof claudeSettings.hooks !== 'object') claudeSettings.hooks = {};
|
|
708
|
+
if (!Array.isArray(claudeSettings.hooks.PreToolUse)) claudeSettings.hooks.PreToolUse = [];
|
|
709
|
+
const hasClaudeHook = claudeSettings.hooks.PreToolUse.some(entry =>
|
|
710
|
+
Array.isArray(entry?.hooks) && entry.hooks.some(h => h && h.command && h.command.includes('privacyscrubber'))
|
|
711
|
+
);
|
|
712
|
+
if (!hasClaudeHook) {
|
|
713
|
+
claudeSettings.hooks.PreToolUse.push({
|
|
714
|
+
matcher: "Read|Bash",
|
|
715
|
+
hooks: [{ type: "command", command: hookCmd }]
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
fs.writeFileSync(claudeSettingsPath, JSON.stringify(claudeSettings, null, 2) + '\n', 'utf8');
|
|
719
|
+
generatedFiles.push('.claude/settings.json');
|
|
720
|
+
} catch (_) {}
|
|
721
|
+
}
|
|
559
722
|
|
|
560
723
|
console.log(`[Zero-Trust Agentic Guard: Agent Rules Generated]`);
|
|
561
724
|
console.log(`Workspace: ${targetDir}`);
|