@solongate/proxy 0.2.1 → 0.2.3
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/dist/index.js +296 -41
- package/dist/init.js +292 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,20 +6,9 @@ var __esm = (fn, res) => function __init() {
|
|
|
6
6
|
|
|
7
7
|
// src/init.ts
|
|
8
8
|
var init_exports = {};
|
|
9
|
-
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, copyFileSync } from "fs";
|
|
10
|
-
import { resolve as resolve2, join
|
|
9
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, copyFileSync, mkdirSync } from "fs";
|
|
10
|
+
import { resolve as resolve2, join } from "path";
|
|
11
11
|
import { createInterface } from "readline";
|
|
12
|
-
function findProxyPath() {
|
|
13
|
-
const candidates = [
|
|
14
|
-
resolve2(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "index.js"),
|
|
15
|
-
resolve2("node_modules", "@solongate", "proxy", "dist", "index.js"),
|
|
16
|
-
resolve2("packages", "proxy", "dist", "index.js")
|
|
17
|
-
];
|
|
18
|
-
for (const p of candidates) {
|
|
19
|
-
if (existsSync2(p)) return p.replace(/\\/g, "/");
|
|
20
|
-
}
|
|
21
|
-
return "solongate-proxy";
|
|
22
|
-
}
|
|
23
12
|
function findConfigFile(explicitPath) {
|
|
24
13
|
if (explicitPath) {
|
|
25
14
|
if (existsSync2(explicitPath)) {
|
|
@@ -46,11 +35,14 @@ function isAlreadyProtected(server) {
|
|
|
46
35
|
const cmdStr = [server.command, ...server.args ?? []].join(" ");
|
|
47
36
|
return cmdStr.includes("solongate") || cmdStr.includes("solongate-proxy");
|
|
48
37
|
}
|
|
49
|
-
function wrapServer(server, policy,
|
|
38
|
+
function wrapServer(server, policy, apiKey) {
|
|
50
39
|
return {
|
|
51
|
-
command: "
|
|
40
|
+
command: "npx",
|
|
52
41
|
args: [
|
|
53
|
-
|
|
42
|
+
"-y",
|
|
43
|
+
"@solongate/proxy",
|
|
44
|
+
"--api-key",
|
|
45
|
+
apiKey,
|
|
54
46
|
"--policy",
|
|
55
47
|
policy,
|
|
56
48
|
"--verbose",
|
|
@@ -58,10 +50,7 @@ function wrapServer(server, policy, proxyPath) {
|
|
|
58
50
|
server.command,
|
|
59
51
|
...server.args ?? []
|
|
60
52
|
],
|
|
61
|
-
env: {
|
|
62
|
-
...server.env,
|
|
63
|
-
SOLONGATE_API_KEY: server.env?.SOLONGATE_API_KEY ?? "sg_live_YOUR_KEY_HERE"
|
|
64
|
-
}
|
|
53
|
+
...server.env ? { env: server.env } : {}
|
|
65
54
|
};
|
|
66
55
|
}
|
|
67
56
|
async function prompt(question) {
|
|
@@ -89,6 +78,9 @@ function parseInitArgs(argv) {
|
|
|
89
78
|
case "--policy":
|
|
90
79
|
options.policy = args[++i];
|
|
91
80
|
break;
|
|
81
|
+
case "--api-key":
|
|
82
|
+
options.apiKey = args[++i];
|
|
83
|
+
break;
|
|
92
84
|
case "--all":
|
|
93
85
|
options.all = true;
|
|
94
86
|
break;
|
|
@@ -124,13 +116,14 @@ OPTIONS
|
|
|
124
116
|
--config <path> Path to MCP config file (default: auto-detect)
|
|
125
117
|
--policy <preset> Policy preset or JSON file (default: restricted)
|
|
126
118
|
Presets: ${POLICY_PRESETS.join(", ")}
|
|
119
|
+
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
127
120
|
--all Protect all servers without prompting
|
|
128
121
|
--dry-run Preview changes without writing
|
|
129
122
|
--restore Restore original config from backup
|
|
130
123
|
-h, --help Show this help message
|
|
131
124
|
|
|
132
125
|
EXAMPLES
|
|
133
|
-
solongate-init
|
|
126
|
+
solongate-init --api-key sg_live_xxx # Setup with API key
|
|
134
127
|
solongate-init --all # Protect everything
|
|
135
128
|
solongate-init --policy read-only # Use read-only policy
|
|
136
129
|
solongate-init --dry-run # Preview changes
|
|
@@ -144,6 +137,60 @@ POLICY PRESETS
|
|
|
144
137
|
`;
|
|
145
138
|
console.error(help);
|
|
146
139
|
}
|
|
140
|
+
function installClaudeCodeHooks(apiKey) {
|
|
141
|
+
const hooksDir = resolve2(".claude", "hooks");
|
|
142
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
143
|
+
const guardPath = join(hooksDir, "guard.mjs");
|
|
144
|
+
writeFileSync(guardPath, GUARD_SCRIPT);
|
|
145
|
+
console.error(` Created ${guardPath}`);
|
|
146
|
+
const auditPath = join(hooksDir, "audit.mjs");
|
|
147
|
+
writeFileSync(auditPath, AUDIT_SCRIPT);
|
|
148
|
+
console.error(` Created ${auditPath}`);
|
|
149
|
+
const settingsPath = resolve2(".claude", "settings.json");
|
|
150
|
+
let settings = {};
|
|
151
|
+
if (existsSync2(settingsPath)) {
|
|
152
|
+
try {
|
|
153
|
+
settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
settings.hooks = {
|
|
158
|
+
PreToolUse: [
|
|
159
|
+
{
|
|
160
|
+
matcher: ".*",
|
|
161
|
+
hooks: [
|
|
162
|
+
{
|
|
163
|
+
type: "command",
|
|
164
|
+
command: "node .claude/hooks/guard.mjs",
|
|
165
|
+
timeout: 5
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
PostToolUse: [
|
|
171
|
+
{
|
|
172
|
+
matcher: ".*",
|
|
173
|
+
hooks: [
|
|
174
|
+
{
|
|
175
|
+
type: "command",
|
|
176
|
+
command: "node .claude/hooks/audit.mjs",
|
|
177
|
+
timeout: 10,
|
|
178
|
+
async: true
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
const envObj = settings.env || {};
|
|
185
|
+
envObj.SOLONGATE_API_KEY = apiKey;
|
|
186
|
+
settings.env = envObj;
|
|
187
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
188
|
+
console.error(` Created ${settingsPath}`);
|
|
189
|
+
console.error("");
|
|
190
|
+
console.error(" Claude Code hooks installed!");
|
|
191
|
+
console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
|
|
192
|
+
console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
|
|
193
|
+
}
|
|
147
194
|
function ensureEnvFile() {
|
|
148
195
|
const envPath = resolve2(".env");
|
|
149
196
|
if (!existsSync2(envPath)) {
|
|
@@ -281,14 +328,34 @@ async function main() {
|
|
|
281
328
|
process.exit(0);
|
|
282
329
|
}
|
|
283
330
|
console.error("");
|
|
284
|
-
|
|
331
|
+
let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
332
|
+
if (!apiKey) {
|
|
333
|
+
const envPath = resolve2(".env");
|
|
334
|
+
if (existsSync2(envPath)) {
|
|
335
|
+
const envContent = readFileSync2(envPath, "utf-8");
|
|
336
|
+
const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
|
|
337
|
+
if (match) apiKey = match[1];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!apiKey || apiKey === "sg_live_your_key_here") {
|
|
341
|
+
apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
|
|
342
|
+
if (!apiKey) {
|
|
343
|
+
console.error(" API key is required. Get one at https://dashboard.solongate.com");
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
348
|
+
console.error(" Invalid API key format. Must start with sg_live_ or sg_test_");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
console.error(` Policy: ${options.policy}`);
|
|
352
|
+
console.error(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
|
|
285
353
|
console.error(` Protecting: ${toProtect.join(", ")}`);
|
|
286
354
|
console.error("");
|
|
287
|
-
const proxyPath = findProxyPath();
|
|
288
355
|
const newConfig = { mcpServers: {} };
|
|
289
356
|
for (const name of serverNames) {
|
|
290
357
|
if (toProtect.includes(name)) {
|
|
291
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy,
|
|
358
|
+
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, apiKey);
|
|
292
359
|
} else {
|
|
293
360
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
294
361
|
}
|
|
@@ -313,22 +380,12 @@ async function main() {
|
|
|
313
380
|
}
|
|
314
381
|
console.error(" Config updated!");
|
|
315
382
|
console.error("");
|
|
316
|
-
|
|
317
|
-
console.error(" \u2502 Your MCP servers are now protected by \u2502");
|
|
318
|
-
console.error(" \u2502 SolonGate security policies. \u2502");
|
|
319
|
-
console.error(" \u2502 \u2502");
|
|
320
|
-
console.error(" \u2502 Next steps: \u2502");
|
|
321
|
-
console.error(" \u2502 1. Replace sg_live_YOUR_KEY_HERE in your \u2502");
|
|
322
|
-
console.error(" \u2502 config with your real API key from \u2502");
|
|
323
|
-
console.error(" \u2502 https://solongate.com \u2502");
|
|
324
|
-
console.error(" \u2502 2. Restart your MCP client (Claude Code \u2502");
|
|
325
|
-
console.error(" \u2502 or Claude Desktop) to apply changes. \u2502");
|
|
326
|
-
console.error(" \u2502 \u2502");
|
|
327
|
-
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
328
|
-
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
383
|
+
installClaudeCodeHooks(apiKey);
|
|
329
384
|
console.error("");
|
|
330
385
|
ensureEnvFile();
|
|
331
386
|
console.error("");
|
|
387
|
+
console.error(" \u2500\u2500 Summary \u2500\u2500");
|
|
388
|
+
console.error("");
|
|
332
389
|
for (const name of toProtect) {
|
|
333
390
|
console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
|
|
334
391
|
}
|
|
@@ -339,8 +396,20 @@ async function main() {
|
|
|
339
396
|
console.error(` \u25CB ${name} \u2014 skipped`);
|
|
340
397
|
}
|
|
341
398
|
console.error("");
|
|
399
|
+
console.error(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
400
|
+
console.error(" \u2502 Setup complete! \u2502");
|
|
401
|
+
console.error(" \u2502 \u2502");
|
|
402
|
+
console.error(" \u2502 MCP tools \u2192 Proxy audit logging \u2502");
|
|
403
|
+
console.error(" \u2502 Built-in tools \u2192 Hook audit logging \u2502");
|
|
404
|
+
console.error(" \u2502 \u2502");
|
|
405
|
+
console.error(" \u2502 View logs: https://dashboard.solongate.com \u2502");
|
|
406
|
+
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
407
|
+
console.error(" \u2502 \u2502");
|
|
408
|
+
console.error(" \u2502 Restart your MCP client to apply changes. \u2502");
|
|
409
|
+
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
410
|
+
console.error("");
|
|
342
411
|
}
|
|
343
|
-
var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS;
|
|
412
|
+
var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, GUARD_SCRIPT, AUDIT_SCRIPT;
|
|
344
413
|
var init_init = __esm({
|
|
345
414
|
"src/init.ts"() {
|
|
346
415
|
"use strict";
|
|
@@ -351,6 +420,192 @@ var init_init = __esm({
|
|
|
351
420
|
".claude/mcp.json"
|
|
352
421
|
];
|
|
353
422
|
CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
|
|
423
|
+
GUARD_SCRIPT = `#!/usr/bin/env node
|
|
424
|
+
/**
|
|
425
|
+
* SolonGate Guard Hook for Claude Code (PreToolUse)
|
|
426
|
+
* Blocks dangerous tool calls BEFORE they execute.
|
|
427
|
+
* Exit code 2 = BLOCK, exit code 0 = ALLOW.
|
|
428
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
429
|
+
*/
|
|
430
|
+
|
|
431
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || '';
|
|
432
|
+
const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
433
|
+
|
|
434
|
+
// \u2500\u2500 Input Guard Patterns \u2500\u2500
|
|
435
|
+
const PATH_TRAVERSAL = [
|
|
436
|
+
/\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
|
|
437
|
+
];
|
|
438
|
+
const SENSITIVE_PATHS = [
|
|
439
|
+
/\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
|
|
440
|
+
/c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
|
|
441
|
+
/\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
|
|
442
|
+
/\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
|
|
443
|
+
];
|
|
444
|
+
const SHELL_INJECTION = [
|
|
445
|
+
/\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
|
|
446
|
+
/%0a/i, /%0d/i,
|
|
447
|
+
];
|
|
448
|
+
const SSRF = [
|
|
449
|
+
/^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
|
|
450
|
+
/^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
|
|
451
|
+
/^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
|
|
452
|
+
/metadata\\.google\\.internal/i,
|
|
453
|
+
];
|
|
454
|
+
const SQL_INJECTION = [
|
|
455
|
+
/'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
|
|
456
|
+
/'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
457
|
+
/UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
// Dangerous command patterns for Bash tool
|
|
461
|
+
const DANGEROUS_COMMANDS = [
|
|
462
|
+
/\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
|
|
463
|
+
/\\brm\\s+-rf\\b/i,
|
|
464
|
+
/\\bmkfs\\b/i, /\\bdd\\s+if=/i,
|
|
465
|
+
/\\b(shutdown|reboot|halt|poweroff)\\b/i,
|
|
466
|
+
/\\bchmod\\s+777\\b/,
|
|
467
|
+
/\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
|
|
468
|
+
/\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
|
|
469
|
+
/\\bnc\\s+-[a-z]*l/i, // netcat listener
|
|
470
|
+
/>(\\s*)\\/dev\\/sd/, // writing to raw disk
|
|
471
|
+
/\\bgit\\s+push\\s+.*--force\\b/i,
|
|
472
|
+
/\\bgit\\s+reset\\s+--hard\\b/i,
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
function checkValue(val) {
|
|
476
|
+
if (typeof val !== 'string' || val.length < 2) return null;
|
|
477
|
+
for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
|
|
478
|
+
for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
|
|
479
|
+
for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
|
|
480
|
+
for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
|
|
481
|
+
if (val.length > 10000) return 'Input too long (max 10000 chars)';
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function checkBashCommand(cmd) {
|
|
486
|
+
if (typeof cmd !== 'string') return null;
|
|
487
|
+
for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
|
|
488
|
+
for (const p of SHELL_INJECTION) if (p.test(cmd)) return null; // shell injection is normal for Bash
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let input = '';
|
|
493
|
+
process.stdin.on('data', c => input += c);
|
|
494
|
+
process.stdin.on('end', async () => {
|
|
495
|
+
try {
|
|
496
|
+
const data = JSON.parse(input);
|
|
497
|
+
const tool = data.tool_name || '';
|
|
498
|
+
const args = data.tool_input || {};
|
|
499
|
+
const start = Date.now();
|
|
500
|
+
|
|
501
|
+
let threat = null;
|
|
502
|
+
|
|
503
|
+
// Check Bash commands for dangerous patterns
|
|
504
|
+
if (tool === 'Bash' && args.command) {
|
|
505
|
+
threat = checkBashCommand(args.command);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Check all string arguments for injection patterns
|
|
509
|
+
if (!threat) {
|
|
510
|
+
for (const [key, val] of Object.entries(args)) {
|
|
511
|
+
if (tool === 'Bash' && key === 'command') continue; // already checked
|
|
512
|
+
threat = checkValue(val);
|
|
513
|
+
if (threat) { threat = threat + ' (in ' + key + ')'; break; }
|
|
514
|
+
// Check nested strings
|
|
515
|
+
if (typeof val === 'object' && val) {
|
|
516
|
+
for (const v of Object.values(val)) {
|
|
517
|
+
threat = checkValue(v);
|
|
518
|
+
if (threat) { threat = threat + ' (in ' + key + ')'; break; }
|
|
519
|
+
}
|
|
520
|
+
if (threat) break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const ms = Date.now() - start;
|
|
526
|
+
|
|
527
|
+
if (threat) {
|
|
528
|
+
// Send DENY audit log
|
|
529
|
+
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
530
|
+
fetch(API_URL + '/api/v1/audit-logs', {
|
|
531
|
+
method: 'POST',
|
|
532
|
+
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
533
|
+
body: JSON.stringify({
|
|
534
|
+
tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
|
|
535
|
+
[k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
|
|
536
|
+
decision: 'DENY', reason: threat, source: 'claude-code-guard',
|
|
537
|
+
evaluationTimeMs: ms,
|
|
538
|
+
}),
|
|
539
|
+
signal: AbortSignal.timeout(5000),
|
|
540
|
+
}).catch(() => {});
|
|
541
|
+
}
|
|
542
|
+
// Exit 2 = BLOCK. Message printed to stdout is shown to user.
|
|
543
|
+
process.stdout.write('SolonGate BLOCKED: ' + threat);
|
|
544
|
+
process.exit(2);
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
// On error, allow (fail-open)
|
|
548
|
+
}
|
|
549
|
+
process.exit(0);
|
|
550
|
+
});
|
|
551
|
+
`;
|
|
552
|
+
AUDIT_SCRIPT = `#!/usr/bin/env node
|
|
553
|
+
/**
|
|
554
|
+
* SolonGate Audit Hook for Claude Code (PostToolUse)
|
|
555
|
+
* Logs ALL tool calls to SolonGate Cloud after execution.
|
|
556
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || '';
|
|
560
|
+
const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
561
|
+
|
|
562
|
+
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
563
|
+
|
|
564
|
+
let input = '';
|
|
565
|
+
process.stdin.on('data', c => input += c);
|
|
566
|
+
process.stdin.on('end', async () => {
|
|
567
|
+
try {
|
|
568
|
+
const data = JSON.parse(input);
|
|
569
|
+
const toolName = data.tool_name || 'unknown';
|
|
570
|
+
const toolInput = data.tool_input || {};
|
|
571
|
+
|
|
572
|
+
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
573
|
+
process.exit(0);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const hasError = data.tool_response?.error ||
|
|
577
|
+
data.tool_response?.exitCode > 0 ||
|
|
578
|
+
data.tool_response?.isError;
|
|
579
|
+
|
|
580
|
+
const argsSummary = {};
|
|
581
|
+
for (const [k, v] of Object.entries(toolInput)) {
|
|
582
|
+
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
583
|
+
? v.slice(0, 200) + '...'
|
|
584
|
+
: v;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
|
|
588
|
+
method: 'POST',
|
|
589
|
+
headers: {
|
|
590
|
+
'Authorization': \`Bearer \${API_KEY}\`,
|
|
591
|
+
'Content-Type': 'application/json',
|
|
592
|
+
},
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
tool: toolName,
|
|
595
|
+
arguments: argsSummary,
|
|
596
|
+
decision: hasError ? 'DENY' : 'ALLOW',
|
|
597
|
+
reason: hasError ? 'tool returned error' : 'allowed',
|
|
598
|
+
source: 'claude-code-hook',
|
|
599
|
+
evaluationTimeMs: 0,
|
|
600
|
+
}),
|
|
601
|
+
signal: AbortSignal.timeout(5000),
|
|
602
|
+
});
|
|
603
|
+
} catch {
|
|
604
|
+
// Silent
|
|
605
|
+
}
|
|
606
|
+
process.exit(0);
|
|
607
|
+
});
|
|
608
|
+
`;
|
|
354
609
|
main().catch((err) => {
|
|
355
610
|
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
356
611
|
process.exit(1);
|
|
@@ -732,7 +987,7 @@ var init_inject = __esm({
|
|
|
732
987
|
|
|
733
988
|
// src/create.ts
|
|
734
989
|
var create_exports = {};
|
|
735
|
-
import { mkdirSync, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
990
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
736
991
|
import { resolve as resolve4, join as join2 } from "path";
|
|
737
992
|
import { execSync as execSync2 } from "child_process";
|
|
738
993
|
function log3(msg) {
|
|
@@ -884,7 +1139,7 @@ function createProject(dir, name, _policy) {
|
|
|
884
1139
|
2
|
|
885
1140
|
) + "\n"
|
|
886
1141
|
);
|
|
887
|
-
|
|
1142
|
+
mkdirSync2(join2(dir, "src"), { recursive: true });
|
|
888
1143
|
writeFileSync3(
|
|
889
1144
|
join2(dir, "src", "index.ts"),
|
|
890
1145
|
`#!/usr/bin/env node
|
|
@@ -973,7 +1228,7 @@ async function main3() {
|
|
|
973
1228
|
process.exit(1);
|
|
974
1229
|
}
|
|
975
1230
|
withSpinner(`Setting up ${opts.name}...`, () => {
|
|
976
|
-
|
|
1231
|
+
mkdirSync2(dir, { recursive: true });
|
|
977
1232
|
createProject(dir, opts.name, opts.policy);
|
|
978
1233
|
});
|
|
979
1234
|
if (!opts.noInstall) {
|
package/dist/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/init.ts
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
|
|
5
|
-
import { resolve, join
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { resolve, join } from "path";
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
7
|
var POLICY_PRESETS = ["restricted", "read-only", "permissive", "deny-all"];
|
|
8
8
|
var SEARCH_PATHS = [
|
|
@@ -11,17 +11,6 @@ var SEARCH_PATHS = [
|
|
|
11
11
|
".claude/mcp.json"
|
|
12
12
|
];
|
|
13
13
|
var CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
|
|
14
|
-
function findProxyPath() {
|
|
15
|
-
const candidates = [
|
|
16
|
-
resolve(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "index.js"),
|
|
17
|
-
resolve("node_modules", "@solongate", "proxy", "dist", "index.js"),
|
|
18
|
-
resolve("packages", "proxy", "dist", "index.js")
|
|
19
|
-
];
|
|
20
|
-
for (const p of candidates) {
|
|
21
|
-
if (existsSync(p)) return p.replace(/\\/g, "/");
|
|
22
|
-
}
|
|
23
|
-
return "solongate-proxy";
|
|
24
|
-
}
|
|
25
14
|
function findConfigFile(explicitPath) {
|
|
26
15
|
if (explicitPath) {
|
|
27
16
|
if (existsSync(explicitPath)) {
|
|
@@ -48,11 +37,14 @@ function isAlreadyProtected(server) {
|
|
|
48
37
|
const cmdStr = [server.command, ...server.args ?? []].join(" ");
|
|
49
38
|
return cmdStr.includes("solongate") || cmdStr.includes("solongate-proxy");
|
|
50
39
|
}
|
|
51
|
-
function wrapServer(server, policy,
|
|
40
|
+
function wrapServer(server, policy, apiKey) {
|
|
52
41
|
return {
|
|
53
|
-
command: "
|
|
42
|
+
command: "npx",
|
|
54
43
|
args: [
|
|
55
|
-
|
|
44
|
+
"-y",
|
|
45
|
+
"@solongate/proxy",
|
|
46
|
+
"--api-key",
|
|
47
|
+
apiKey,
|
|
56
48
|
"--policy",
|
|
57
49
|
policy,
|
|
58
50
|
"--verbose",
|
|
@@ -60,10 +52,7 @@ function wrapServer(server, policy, proxyPath) {
|
|
|
60
52
|
server.command,
|
|
61
53
|
...server.args ?? []
|
|
62
54
|
],
|
|
63
|
-
env: {
|
|
64
|
-
...server.env,
|
|
65
|
-
SOLONGATE_API_KEY: server.env?.SOLONGATE_API_KEY ?? "sg_live_YOUR_KEY_HERE"
|
|
66
|
-
}
|
|
55
|
+
...server.env ? { env: server.env } : {}
|
|
67
56
|
};
|
|
68
57
|
}
|
|
69
58
|
async function prompt(question) {
|
|
@@ -91,6 +80,9 @@ function parseInitArgs(argv) {
|
|
|
91
80
|
case "--policy":
|
|
92
81
|
options.policy = args[++i];
|
|
93
82
|
break;
|
|
83
|
+
case "--api-key":
|
|
84
|
+
options.apiKey = args[++i];
|
|
85
|
+
break;
|
|
94
86
|
case "--all":
|
|
95
87
|
options.all = true;
|
|
96
88
|
break;
|
|
@@ -126,13 +118,14 @@ OPTIONS
|
|
|
126
118
|
--config <path> Path to MCP config file (default: auto-detect)
|
|
127
119
|
--policy <preset> Policy preset or JSON file (default: restricted)
|
|
128
120
|
Presets: ${POLICY_PRESETS.join(", ")}
|
|
121
|
+
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
129
122
|
--all Protect all servers without prompting
|
|
130
123
|
--dry-run Preview changes without writing
|
|
131
124
|
--restore Restore original config from backup
|
|
132
125
|
-h, --help Show this help message
|
|
133
126
|
|
|
134
127
|
EXAMPLES
|
|
135
|
-
solongate-init
|
|
128
|
+
solongate-init --api-key sg_live_xxx # Setup with API key
|
|
136
129
|
solongate-init --all # Protect everything
|
|
137
130
|
solongate-init --policy read-only # Use read-only policy
|
|
138
131
|
solongate-init --dry-run # Preview changes
|
|
@@ -146,6 +139,246 @@ POLICY PRESETS
|
|
|
146
139
|
`;
|
|
147
140
|
console.error(help);
|
|
148
141
|
}
|
|
142
|
+
var GUARD_SCRIPT = `#!/usr/bin/env node
|
|
143
|
+
/**
|
|
144
|
+
* SolonGate Guard Hook for Claude Code (PreToolUse)
|
|
145
|
+
* Blocks dangerous tool calls BEFORE they execute.
|
|
146
|
+
* Exit code 2 = BLOCK, exit code 0 = ALLOW.
|
|
147
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || '';
|
|
151
|
+
const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
152
|
+
|
|
153
|
+
// \u2500\u2500 Input Guard Patterns \u2500\u2500
|
|
154
|
+
const PATH_TRAVERSAL = [
|
|
155
|
+
/\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
|
|
156
|
+
];
|
|
157
|
+
const SENSITIVE_PATHS = [
|
|
158
|
+
/\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
|
|
159
|
+
/c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
|
|
160
|
+
/\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
|
|
161
|
+
/\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
|
|
162
|
+
];
|
|
163
|
+
const SHELL_INJECTION = [
|
|
164
|
+
/\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
|
|
165
|
+
/%0a/i, /%0d/i,
|
|
166
|
+
];
|
|
167
|
+
const SSRF = [
|
|
168
|
+
/^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
|
|
169
|
+
/^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
|
|
170
|
+
/^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
|
|
171
|
+
/metadata\\.google\\.internal/i,
|
|
172
|
+
];
|
|
173
|
+
const SQL_INJECTION = [
|
|
174
|
+
/'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
|
|
175
|
+
/'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
176
|
+
/UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
// Dangerous command patterns for Bash tool
|
|
180
|
+
const DANGEROUS_COMMANDS = [
|
|
181
|
+
/\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
|
|
182
|
+
/\\brm\\s+-rf\\b/i,
|
|
183
|
+
/\\bmkfs\\b/i, /\\bdd\\s+if=/i,
|
|
184
|
+
/\\b(shutdown|reboot|halt|poweroff)\\b/i,
|
|
185
|
+
/\\bchmod\\s+777\\b/,
|
|
186
|
+
/\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
|
|
187
|
+
/\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
|
|
188
|
+
/\\bnc\\s+-[a-z]*l/i, // netcat listener
|
|
189
|
+
/>(\\s*)\\/dev\\/sd/, // writing to raw disk
|
|
190
|
+
/\\bgit\\s+push\\s+.*--force\\b/i,
|
|
191
|
+
/\\bgit\\s+reset\\s+--hard\\b/i,
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
function checkValue(val) {
|
|
195
|
+
if (typeof val !== 'string' || val.length < 2) return null;
|
|
196
|
+
for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
|
|
197
|
+
for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
|
|
198
|
+
for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
|
|
199
|
+
for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
|
|
200
|
+
if (val.length > 10000) return 'Input too long (max 10000 chars)';
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function checkBashCommand(cmd) {
|
|
205
|
+
if (typeof cmd !== 'string') return null;
|
|
206
|
+
for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
|
|
207
|
+
for (const p of SHELL_INJECTION) if (p.test(cmd)) return null; // shell injection is normal for Bash
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let input = '';
|
|
212
|
+
process.stdin.on('data', c => input += c);
|
|
213
|
+
process.stdin.on('end', async () => {
|
|
214
|
+
try {
|
|
215
|
+
const data = JSON.parse(input);
|
|
216
|
+
const tool = data.tool_name || '';
|
|
217
|
+
const args = data.tool_input || {};
|
|
218
|
+
const start = Date.now();
|
|
219
|
+
|
|
220
|
+
let threat = null;
|
|
221
|
+
|
|
222
|
+
// Check Bash commands for dangerous patterns
|
|
223
|
+
if (tool === 'Bash' && args.command) {
|
|
224
|
+
threat = checkBashCommand(args.command);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check all string arguments for injection patterns
|
|
228
|
+
if (!threat) {
|
|
229
|
+
for (const [key, val] of Object.entries(args)) {
|
|
230
|
+
if (tool === 'Bash' && key === 'command') continue; // already checked
|
|
231
|
+
threat = checkValue(val);
|
|
232
|
+
if (threat) { threat = threat + ' (in ' + key + ')'; break; }
|
|
233
|
+
// Check nested strings
|
|
234
|
+
if (typeof val === 'object' && val) {
|
|
235
|
+
for (const v of Object.values(val)) {
|
|
236
|
+
threat = checkValue(v);
|
|
237
|
+
if (threat) { threat = threat + ' (in ' + key + ')'; break; }
|
|
238
|
+
}
|
|
239
|
+
if (threat) break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const ms = Date.now() - start;
|
|
245
|
+
|
|
246
|
+
if (threat) {
|
|
247
|
+
// Send DENY audit log
|
|
248
|
+
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
249
|
+
fetch(API_URL + '/api/v1/audit-logs', {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
252
|
+
body: JSON.stringify({
|
|
253
|
+
tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
|
|
254
|
+
[k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
|
|
255
|
+
decision: 'DENY', reason: threat, source: 'claude-code-guard',
|
|
256
|
+
evaluationTimeMs: ms,
|
|
257
|
+
}),
|
|
258
|
+
signal: AbortSignal.timeout(5000),
|
|
259
|
+
}).catch(() => {});
|
|
260
|
+
}
|
|
261
|
+
// Exit 2 = BLOCK. Message printed to stdout is shown to user.
|
|
262
|
+
process.stdout.write('SolonGate BLOCKED: ' + threat);
|
|
263
|
+
process.exit(2);
|
|
264
|
+
}
|
|
265
|
+
} catch {
|
|
266
|
+
// On error, allow (fail-open)
|
|
267
|
+
}
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
270
|
+
`;
|
|
271
|
+
var AUDIT_SCRIPT = `#!/usr/bin/env node
|
|
272
|
+
/**
|
|
273
|
+
* SolonGate Audit Hook for Claude Code (PostToolUse)
|
|
274
|
+
* Logs ALL tool calls to SolonGate Cloud after execution.
|
|
275
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || '';
|
|
279
|
+
const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
280
|
+
|
|
281
|
+
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
282
|
+
|
|
283
|
+
let input = '';
|
|
284
|
+
process.stdin.on('data', c => input += c);
|
|
285
|
+
process.stdin.on('end', async () => {
|
|
286
|
+
try {
|
|
287
|
+
const data = JSON.parse(input);
|
|
288
|
+
const toolName = data.tool_name || 'unknown';
|
|
289
|
+
const toolInput = data.tool_input || {};
|
|
290
|
+
|
|
291
|
+
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
292
|
+
process.exit(0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const hasError = data.tool_response?.error ||
|
|
296
|
+
data.tool_response?.exitCode > 0 ||
|
|
297
|
+
data.tool_response?.isError;
|
|
298
|
+
|
|
299
|
+
const argsSummary = {};
|
|
300
|
+
for (const [k, v] of Object.entries(toolInput)) {
|
|
301
|
+
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
302
|
+
? v.slice(0, 200) + '...'
|
|
303
|
+
: v;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: {
|
|
309
|
+
'Authorization': \`Bearer \${API_KEY}\`,
|
|
310
|
+
'Content-Type': 'application/json',
|
|
311
|
+
},
|
|
312
|
+
body: JSON.stringify({
|
|
313
|
+
tool: toolName,
|
|
314
|
+
arguments: argsSummary,
|
|
315
|
+
decision: hasError ? 'DENY' : 'ALLOW',
|
|
316
|
+
reason: hasError ? 'tool returned error' : 'allowed',
|
|
317
|
+
source: 'claude-code-hook',
|
|
318
|
+
evaluationTimeMs: 0,
|
|
319
|
+
}),
|
|
320
|
+
signal: AbortSignal.timeout(5000),
|
|
321
|
+
});
|
|
322
|
+
} catch {
|
|
323
|
+
// Silent
|
|
324
|
+
}
|
|
325
|
+
process.exit(0);
|
|
326
|
+
});
|
|
327
|
+
`;
|
|
328
|
+
function installClaudeCodeHooks(apiKey) {
|
|
329
|
+
const hooksDir = resolve(".claude", "hooks");
|
|
330
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
331
|
+
const guardPath = join(hooksDir, "guard.mjs");
|
|
332
|
+
writeFileSync(guardPath, GUARD_SCRIPT);
|
|
333
|
+
console.error(` Created ${guardPath}`);
|
|
334
|
+
const auditPath = join(hooksDir, "audit.mjs");
|
|
335
|
+
writeFileSync(auditPath, AUDIT_SCRIPT);
|
|
336
|
+
console.error(` Created ${auditPath}`);
|
|
337
|
+
const settingsPath = resolve(".claude", "settings.json");
|
|
338
|
+
let settings = {};
|
|
339
|
+
if (existsSync(settingsPath)) {
|
|
340
|
+
try {
|
|
341
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
342
|
+
} catch {
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
settings.hooks = {
|
|
346
|
+
PreToolUse: [
|
|
347
|
+
{
|
|
348
|
+
matcher: ".*",
|
|
349
|
+
hooks: [
|
|
350
|
+
{
|
|
351
|
+
type: "command",
|
|
352
|
+
command: "node .claude/hooks/guard.mjs",
|
|
353
|
+
timeout: 5
|
|
354
|
+
}
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
],
|
|
358
|
+
PostToolUse: [
|
|
359
|
+
{
|
|
360
|
+
matcher: ".*",
|
|
361
|
+
hooks: [
|
|
362
|
+
{
|
|
363
|
+
type: "command",
|
|
364
|
+
command: "node .claude/hooks/audit.mjs",
|
|
365
|
+
timeout: 10,
|
|
366
|
+
async: true
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
};
|
|
372
|
+
const envObj = settings.env || {};
|
|
373
|
+
envObj.SOLONGATE_API_KEY = apiKey;
|
|
374
|
+
settings.env = envObj;
|
|
375
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
376
|
+
console.error(` Created ${settingsPath}`);
|
|
377
|
+
console.error("");
|
|
378
|
+
console.error(" Claude Code hooks installed!");
|
|
379
|
+
console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
|
|
380
|
+
console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
|
|
381
|
+
}
|
|
149
382
|
function ensureEnvFile() {
|
|
150
383
|
const envPath = resolve(".env");
|
|
151
384
|
if (!existsSync(envPath)) {
|
|
@@ -283,14 +516,34 @@ async function main() {
|
|
|
283
516
|
process.exit(0);
|
|
284
517
|
}
|
|
285
518
|
console.error("");
|
|
286
|
-
|
|
519
|
+
let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
520
|
+
if (!apiKey) {
|
|
521
|
+
const envPath = resolve(".env");
|
|
522
|
+
if (existsSync(envPath)) {
|
|
523
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
524
|
+
const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
|
|
525
|
+
if (match) apiKey = match[1];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (!apiKey || apiKey === "sg_live_your_key_here") {
|
|
529
|
+
apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
|
|
530
|
+
if (!apiKey) {
|
|
531
|
+
console.error(" API key is required. Get one at https://dashboard.solongate.com");
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
536
|
+
console.error(" Invalid API key format. Must start with sg_live_ or sg_test_");
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
console.error(` Policy: ${options.policy}`);
|
|
540
|
+
console.error(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
|
|
287
541
|
console.error(` Protecting: ${toProtect.join(", ")}`);
|
|
288
542
|
console.error("");
|
|
289
|
-
const proxyPath = findProxyPath();
|
|
290
543
|
const newConfig = { mcpServers: {} };
|
|
291
544
|
for (const name of serverNames) {
|
|
292
545
|
if (toProtect.includes(name)) {
|
|
293
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy,
|
|
546
|
+
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, apiKey);
|
|
294
547
|
} else {
|
|
295
548
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
296
549
|
}
|
|
@@ -315,22 +568,12 @@ async function main() {
|
|
|
315
568
|
}
|
|
316
569
|
console.error(" Config updated!");
|
|
317
570
|
console.error("");
|
|
318
|
-
|
|
319
|
-
console.error(" \u2502 Your MCP servers are now protected by \u2502");
|
|
320
|
-
console.error(" \u2502 SolonGate security policies. \u2502");
|
|
321
|
-
console.error(" \u2502 \u2502");
|
|
322
|
-
console.error(" \u2502 Next steps: \u2502");
|
|
323
|
-
console.error(" \u2502 1. Replace sg_live_YOUR_KEY_HERE in your \u2502");
|
|
324
|
-
console.error(" \u2502 config with your real API key from \u2502");
|
|
325
|
-
console.error(" \u2502 https://solongate.com \u2502");
|
|
326
|
-
console.error(" \u2502 2. Restart your MCP client (Claude Code \u2502");
|
|
327
|
-
console.error(" \u2502 or Claude Desktop) to apply changes. \u2502");
|
|
328
|
-
console.error(" \u2502 \u2502");
|
|
329
|
-
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
330
|
-
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
571
|
+
installClaudeCodeHooks(apiKey);
|
|
331
572
|
console.error("");
|
|
332
573
|
ensureEnvFile();
|
|
333
574
|
console.error("");
|
|
575
|
+
console.error(" \u2500\u2500 Summary \u2500\u2500");
|
|
576
|
+
console.error("");
|
|
334
577
|
for (const name of toProtect) {
|
|
335
578
|
console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
|
|
336
579
|
}
|
|
@@ -341,6 +584,18 @@ async function main() {
|
|
|
341
584
|
console.error(` \u25CB ${name} \u2014 skipped`);
|
|
342
585
|
}
|
|
343
586
|
console.error("");
|
|
587
|
+
console.error(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
588
|
+
console.error(" \u2502 Setup complete! \u2502");
|
|
589
|
+
console.error(" \u2502 \u2502");
|
|
590
|
+
console.error(" \u2502 MCP tools \u2192 Proxy audit logging \u2502");
|
|
591
|
+
console.error(" \u2502 Built-in tools \u2192 Hook audit logging \u2502");
|
|
592
|
+
console.error(" \u2502 \u2502");
|
|
593
|
+
console.error(" \u2502 View logs: https://dashboard.solongate.com \u2502");
|
|
594
|
+
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
595
|
+
console.error(" \u2502 \u2502");
|
|
596
|
+
console.error(" \u2502 Restart your MCP client to apply changes. \u2502");
|
|
597
|
+
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
598
|
+
console.error("");
|
|
344
599
|
}
|
|
345
600
|
main().catch((err) => {
|
|
346
601
|
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "MCP security proxy \u00e2\u20ac\u201d protect any MCP server with policies, input validation, rate limiting, and audit logging. Zero code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|