@solongate/proxy 0.8.2 → 0.8.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 +230 -419
- package/dist/init.js +8 -341
- package/dist/pull-push.js +2 -1
- package/hooks/audit.mjs +73 -0
- package/hooks/guard.mjs +264 -0
- package/package.json +2 -1
package/dist/init.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/init.ts
|
|
4
4
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
5
|
-
import { resolve, join } from "path";
|
|
5
|
+
import { resolve, join, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
6
7
|
import { createInterface } from "readline";
|
|
7
8
|
var SEARCH_PATHS = [
|
|
8
9
|
".mcp.json",
|
|
@@ -134,353 +135,19 @@ EXAMPLES
|
|
|
134
135
|
`;
|
|
135
136
|
console.log(help);
|
|
136
137
|
}
|
|
137
|
-
var
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
* Exit code 2 = BLOCK, exit code 0 = ALLOW.
|
|
142
|
-
* Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
|
|
143
|
-
* Auto-installed by: npx @solongate/proxy init
|
|
144
|
-
*/
|
|
145
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
146
|
-
import { resolve } from 'node:path';
|
|
147
|
-
|
|
148
|
-
// \u2500\u2500 Load API key from .env file (Claude Code doesn't load .env into process.env) \u2500\u2500
|
|
149
|
-
function loadEnvKey(dir) {
|
|
150
|
-
try {
|
|
151
|
-
const envPath = resolve(dir, '.env');
|
|
152
|
-
if (!existsSync(envPath)) return {};
|
|
153
|
-
const lines = readFileSync(envPath, 'utf-8').split('\\n');
|
|
154
|
-
const env = {};
|
|
155
|
-
for (const line of lines) {
|
|
156
|
-
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
157
|
-
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
|
|
158
|
-
}
|
|
159
|
-
return env;
|
|
160
|
-
} catch { return {}; }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const hookCwdEarly = process.cwd();
|
|
164
|
-
const dotenv = loadEnvKey(hookCwdEarly);
|
|
165
|
-
const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
|
|
166
|
-
const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
167
|
-
|
|
168
|
-
// \u2500\u2500 Glob Matching \u2500\u2500
|
|
169
|
-
function matchGlob(str, pattern) {
|
|
170
|
-
if (pattern === '*') return true;
|
|
171
|
-
const s = str.toLowerCase();
|
|
172
|
-
const p = pattern.toLowerCase();
|
|
173
|
-
if (s === p) return true;
|
|
174
|
-
const startsW = p.startsWith('*');
|
|
175
|
-
const endsW = p.endsWith('*');
|
|
176
|
-
if (startsW && endsW) { const infix = p.slice(1, -1); return infix.length > 0 && s.includes(infix); }
|
|
177
|
-
if (startsW) return s.endsWith(p.slice(1));
|
|
178
|
-
if (endsW) return s.startsWith(p.slice(0, -1));
|
|
179
|
-
const idx = p.indexOf('*');
|
|
180
|
-
if (idx !== -1) {
|
|
181
|
-
const pre = p.slice(0, idx);
|
|
182
|
-
const suf = p.slice(idx + 1);
|
|
183
|
-
return s.startsWith(pre) && s.endsWith(suf) && s.length >= pre.length + suf.length;
|
|
184
|
-
}
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// \u2500\u2500 Path Glob (supports **) \u2500\u2500
|
|
189
|
-
function matchPathGlob(path, pattern) {
|
|
190
|
-
const p = path.replace(/\\\\/g, '/').toLowerCase();
|
|
191
|
-
const g = pattern.replace(/\\\\/g, '/').toLowerCase();
|
|
192
|
-
if (p === g) return true;
|
|
193
|
-
if (g.includes('**')) {
|
|
194
|
-
const parts = g.split('**').filter(s => s.length > 0);
|
|
195
|
-
if (parts.length === 0) return true; // just ** or ****
|
|
196
|
-
return parts.every(segment => p.includes(segment));
|
|
197
|
-
}
|
|
198
|
-
return matchGlob(p, g);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// \u2500\u2500 Extract Functions (deep scan all string values) \u2500\u2500
|
|
202
|
-
function scanStrings(obj) {
|
|
203
|
-
const strings = [];
|
|
204
|
-
function walk(v) {
|
|
205
|
-
if (typeof v === 'string' && v.trim()) strings.push(v.trim());
|
|
206
|
-
else if (Array.isArray(v)) v.forEach(walk);
|
|
207
|
-
else if (v && typeof v === 'object') Object.values(v).forEach(walk);
|
|
208
|
-
}
|
|
209
|
-
walk(obj);
|
|
210
|
-
return strings;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function looksLikeFilename(s) {
|
|
214
|
-
if (s.startsWith('.')) return true;
|
|
215
|
-
if (/\\.\\w+$/.test(s)) return true;
|
|
216
|
-
const known = ['id_rsa','id_dsa','id_ecdsa','id_ed25519','authorized_keys','known_hosts','makefile','dockerfile'];
|
|
217
|
-
return known.includes(s.toLowerCase());
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function extractFilenames(args) {
|
|
221
|
-
const names = new Set();
|
|
222
|
-
for (const s of scanStrings(args)) {
|
|
223
|
-
if (/^https?:\\/\\//i.test(s)) continue;
|
|
224
|
-
if (s.includes('/') || s.includes('\\\\')) {
|
|
225
|
-
const base = s.replace(/\\\\/g, '/').split('/').pop();
|
|
226
|
-
if (base) names.add(base);
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
if (s.includes(' ')) {
|
|
230
|
-
for (const tok of s.split(/\\s+/)) {
|
|
231
|
-
if (tok.includes('/') || tok.includes('\\\\')) {
|
|
232
|
-
const b = tok.replace(/\\\\/g, '/').split('/').pop();
|
|
233
|
-
if (b && looksLikeFilename(b)) names.add(b);
|
|
234
|
-
} else if (looksLikeFilename(tok)) names.add(tok);
|
|
235
|
-
}
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (looksLikeFilename(s)) names.add(s);
|
|
239
|
-
}
|
|
240
|
-
return [...names];
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function extractUrls(args) {
|
|
244
|
-
const urls = new Set();
|
|
245
|
-
for (const s of scanStrings(args)) {
|
|
246
|
-
if (/^https?:\\/\\//i.test(s)) { urls.add(s); continue; }
|
|
247
|
-
if (s.includes(' ')) {
|
|
248
|
-
for (const tok of s.split(/\\s+/)) {
|
|
249
|
-
if (/^https?:\\/\\//i.test(tok)) urls.add(tok);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return [...urls];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function extractCommands(args) {
|
|
257
|
-
const cmds = [];
|
|
258
|
-
const fields = ['command', 'cmd', 'function', 'script', 'shell'];
|
|
259
|
-
if (typeof args === 'object' && args) {
|
|
260
|
-
for (const [k, v] of Object.entries(args)) {
|
|
261
|
-
if (fields.includes(k.toLowerCase()) && typeof v === 'string') {
|
|
262
|
-
// Split chained commands: cd /path && npm install \u2192 [cd /path, npm install]
|
|
263
|
-
for (const part of v.split(/\\s*(?:&&|\\|\\||;|\\|)\\s*/)) {
|
|
264
|
-
const trimmed = part.trim();
|
|
265
|
-
if (trimmed) cmds.push(trimmed);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return cmds;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function extractPaths(args) {
|
|
274
|
-
const paths = [];
|
|
275
|
-
for (const s of scanStrings(args)) {
|
|
276
|
-
if (/^https?:\\/\\//i.test(s)) continue;
|
|
277
|
-
if (s.includes('/') || s.includes('\\\\') || s.startsWith('.')) paths.push(s);
|
|
278
|
-
}
|
|
279
|
-
return paths;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// \u2500\u2500 Policy Evaluation \u2500\u2500
|
|
283
|
-
function evaluate(policy, args) {
|
|
284
|
-
if (!policy || !policy.rules) return null;
|
|
285
|
-
const denyRules = policy.rules
|
|
286
|
-
.filter(r => r.effect === 'DENY' && r.enabled !== false)
|
|
287
|
-
.sort((a, b) => (a.priority || 100) - (b.priority || 100));
|
|
288
|
-
|
|
289
|
-
for (const rule of denyRules) {
|
|
290
|
-
// Filename constraints
|
|
291
|
-
if (rule.filenameConstraints && rule.filenameConstraints.denied) {
|
|
292
|
-
const filenames = extractFilenames(args);
|
|
293
|
-
for (const fn of filenames) {
|
|
294
|
-
for (const pat of rule.filenameConstraints.denied) {
|
|
295
|
-
if (matchGlob(fn, pat)) return 'Blocked by policy: filename "' + fn + '" matches "' + pat + '"';
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
// URL constraints
|
|
300
|
-
if (rule.urlConstraints && rule.urlConstraints.denied) {
|
|
301
|
-
const urls = extractUrls(args);
|
|
302
|
-
for (const url of urls) {
|
|
303
|
-
for (const pat of rule.urlConstraints.denied) {
|
|
304
|
-
if (matchGlob(url, pat)) return 'Blocked by policy: URL "' + url + '" matches "' + pat + '"';
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
// Command constraints
|
|
309
|
-
if (rule.commandConstraints && rule.commandConstraints.denied) {
|
|
310
|
-
const cmds = extractCommands(args);
|
|
311
|
-
for (const cmd of cmds) {
|
|
312
|
-
for (const pat of rule.commandConstraints.denied) {
|
|
313
|
-
if (matchGlob(cmd, pat)) return 'Blocked by policy: command "' + cmd.slice(0,60) + '" matches "' + pat + '"';
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// Path constraints
|
|
318
|
-
if (rule.pathConstraints && rule.pathConstraints.denied) {
|
|
319
|
-
const paths = extractPaths(args);
|
|
320
|
-
for (const p of paths) {
|
|
321
|
-
for (const pat of rule.pathConstraints.denied) {
|
|
322
|
-
if (matchPathGlob(p, pat)) return 'Blocked by policy: path "' + p + '" matches "' + pat + '"';
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return null;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// \u2500\u2500 Main \u2500\u2500
|
|
331
|
-
let input = '';
|
|
332
|
-
process.stdin.on('data', c => input += c);
|
|
333
|
-
process.stdin.on('end', async () => {
|
|
334
|
-
try {
|
|
335
|
-
const data = JSON.parse(input);
|
|
336
|
-
const args = data.tool_input || {};
|
|
337
|
-
|
|
338
|
-
// \u2500\u2500 Self-protection: block access to hook files and settings \u2500\u2500
|
|
339
|
-
const allStrings = scanStrings(args).map(s => s.replace(/\\\\/g, '/').toLowerCase());
|
|
340
|
-
const protectedPaths = ['.solongate', '.claude', '.cursor', 'policy.json', '.mcp.json'];
|
|
341
|
-
for (const s of allStrings) {
|
|
342
|
-
for (const p of protectedPaths) {
|
|
343
|
-
if (s.includes(p)) {
|
|
344
|
-
const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
|
|
345
|
-
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
346
|
-
try {
|
|
347
|
-
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
348
|
-
method: 'POST',
|
|
349
|
-
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
350
|
-
body: JSON.stringify({
|
|
351
|
-
tool: data.tool_name || '', arguments: args,
|
|
352
|
-
decision: 'DENY', reason: msg,
|
|
353
|
-
source: 'claude-code-guard',
|
|
354
|
-
}),
|
|
355
|
-
signal: AbortSignal.timeout(3000),
|
|
356
|
-
});
|
|
357
|
-
} catch {}
|
|
358
|
-
}
|
|
359
|
-
process.stderr.write(msg);
|
|
360
|
-
process.exit(2);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Load policy (use cwd from hook data if available)
|
|
366
|
-
const hookCwd = data.cwd || process.cwd();
|
|
367
|
-
let policy;
|
|
368
|
-
try {
|
|
369
|
-
const policyPath = resolve(hookCwd, 'policy.json');
|
|
370
|
-
policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
|
|
371
|
-
} catch {
|
|
372
|
-
process.exit(0); // No policy = allow all
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const reason = evaluate(policy, args);
|
|
376
|
-
const decision = reason ? 'DENY' : 'ALLOW';
|
|
377
|
-
|
|
378
|
-
// \u2500\u2500 Log ALL decisions to SolonGate Cloud \u2500\u2500
|
|
379
|
-
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
380
|
-
try {
|
|
381
|
-
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
382
|
-
method: 'POST',
|
|
383
|
-
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
384
|
-
body: JSON.stringify({
|
|
385
|
-
tool: data.tool_name || '', arguments: args,
|
|
386
|
-
decision, reason: reason || 'allowed by policy',
|
|
387
|
-
source: 'claude-code-guard',
|
|
388
|
-
}),
|
|
389
|
-
signal: AbortSignal.timeout(3000),
|
|
390
|
-
});
|
|
391
|
-
} catch {}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (reason) {
|
|
395
|
-
process.stderr.write(reason);
|
|
396
|
-
process.exit(2);
|
|
397
|
-
}
|
|
398
|
-
} catch {}
|
|
399
|
-
process.exit(0);
|
|
400
|
-
});
|
|
401
|
-
`;
|
|
402
|
-
var AUDIT_SCRIPT = `#!/usr/bin/env node
|
|
403
|
-
/**
|
|
404
|
-
* SolonGate Audit Hook for Claude Code (PostToolUse)
|
|
405
|
-
* Logs tool execution results to SolonGate Cloud.
|
|
406
|
-
* Auto-installed by: npx @solongate/proxy init
|
|
407
|
-
*/
|
|
408
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
409
|
-
import { resolve } from 'node:path';
|
|
410
|
-
|
|
411
|
-
function loadEnvKey(dir) {
|
|
412
|
-
try {
|
|
413
|
-
const envPath = resolve(dir, '.env');
|
|
414
|
-
if (!existsSync(envPath)) return {};
|
|
415
|
-
const lines = readFileSync(envPath, 'utf-8').split('\\n');
|
|
416
|
-
const env = {};
|
|
417
|
-
for (const line of lines) {
|
|
418
|
-
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
419
|
-
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
|
|
420
|
-
}
|
|
421
|
-
return env;
|
|
422
|
-
} catch { return {}; }
|
|
138
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
139
|
+
var HOOKS_DIR = resolve(__dirname, "..", "hooks");
|
|
140
|
+
function readHookScript(filename) {
|
|
141
|
+
return readFileSync(join(HOOKS_DIR, filename), "utf-8");
|
|
423
142
|
}
|
|
424
|
-
|
|
425
|
-
const dotenv = loadEnvKey(process.cwd());
|
|
426
|
-
const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
|
|
427
|
-
const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
428
|
-
|
|
429
|
-
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
430
|
-
|
|
431
|
-
let input = '';
|
|
432
|
-
process.stdin.on('data', c => input += c);
|
|
433
|
-
process.stdin.on('end', async () => {
|
|
434
|
-
try {
|
|
435
|
-
const data = JSON.parse(input);
|
|
436
|
-
const toolName = data.tool_name || 'unknown';
|
|
437
|
-
const toolInput = data.tool_input || {};
|
|
438
|
-
|
|
439
|
-
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
440
|
-
process.exit(0);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const hasError = data.tool_response?.error ||
|
|
444
|
-
data.tool_response?.exitCode > 0 ||
|
|
445
|
-
data.tool_response?.isError;
|
|
446
|
-
|
|
447
|
-
const argsSummary = {};
|
|
448
|
-
for (const [k, v] of Object.entries(toolInput)) {
|
|
449
|
-
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
450
|
-
? v.slice(0, 200) + '...'
|
|
451
|
-
: v;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
|
|
455
|
-
method: 'POST',
|
|
456
|
-
headers: {
|
|
457
|
-
'Authorization': \`Bearer \${API_KEY}\`,
|
|
458
|
-
'Content-Type': 'application/json',
|
|
459
|
-
},
|
|
460
|
-
body: JSON.stringify({
|
|
461
|
-
tool: toolName,
|
|
462
|
-
arguments: argsSummary,
|
|
463
|
-
decision: hasError ? 'DENY' : 'ALLOW',
|
|
464
|
-
reason: hasError ? 'tool returned error' : 'allowed',
|
|
465
|
-
source: 'claude-code-hook',
|
|
466
|
-
evaluationTimeMs: 0,
|
|
467
|
-
}),
|
|
468
|
-
signal: AbortSignal.timeout(5000),
|
|
469
|
-
});
|
|
470
|
-
} catch {
|
|
471
|
-
// Silent
|
|
472
|
-
}
|
|
473
|
-
process.exit(0);
|
|
474
|
-
});
|
|
475
|
-
`;
|
|
476
143
|
function installHooks() {
|
|
477
144
|
const hooksDir = resolve(".solongate", "hooks");
|
|
478
145
|
mkdirSync(hooksDir, { recursive: true });
|
|
479
146
|
const guardPath = join(hooksDir, "guard.mjs");
|
|
480
|
-
writeFileSync(guardPath,
|
|
147
|
+
writeFileSync(guardPath, readHookScript("guard.mjs"));
|
|
481
148
|
console.log(` Created ${guardPath}`);
|
|
482
149
|
const auditPath = join(hooksDir, "audit.mjs");
|
|
483
|
-
writeFileSync(auditPath,
|
|
150
|
+
writeFileSync(auditPath, readHookScript("audit.mjs"));
|
|
484
151
|
console.log(` Created ${auditPath}`);
|
|
485
152
|
const hookSettings = {
|
|
486
153
|
hooks: {
|
package/dist/pull-push.js
CHANGED
|
@@ -5,7 +5,7 @@ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2
|
|
|
5
5
|
import { resolve as resolve2 } from "path";
|
|
6
6
|
|
|
7
7
|
// src/config.ts
|
|
8
|
-
import { readFileSync, existsSync } from "fs";
|
|
8
|
+
import { readFileSync, existsSync, appendFileSync } from "fs";
|
|
9
9
|
import { resolve } from "path";
|
|
10
10
|
async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
11
11
|
let resolvedId = policyId;
|
|
@@ -42,6 +42,7 @@ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
|
42
42
|
updatedAt: ""
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
+
var AUDIT_LOG_BACKUP_PATH = resolve(".solongate-audit-backup.jsonl");
|
|
45
46
|
|
|
46
47
|
// src/pull-push.ts
|
|
47
48
|
var log = (...args) => process.stderr.write(`${args.map(String).join(" ")}
|
package/hooks/audit.mjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SolonGate Audit Hook for Claude Code (PostToolUse)
|
|
4
|
+
* Logs tool execution results to SolonGate Cloud.
|
|
5
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
|
|
10
|
+
function loadEnvKey(dir) {
|
|
11
|
+
try {
|
|
12
|
+
const envPath = resolve(dir, '.env');
|
|
13
|
+
if (!existsSync(envPath)) return {};
|
|
14
|
+
const lines = readFileSync(envPath, 'utf-8').split('\n');
|
|
15
|
+
const env = {};
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
18
|
+
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
|
|
19
|
+
}
|
|
20
|
+
return env;
|
|
21
|
+
} catch { return {}; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const dotenv = loadEnvKey(process.cwd());
|
|
25
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
|
|
26
|
+
const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
27
|
+
|
|
28
|
+
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
29
|
+
|
|
30
|
+
let input = '';
|
|
31
|
+
process.stdin.on('data', c => input += c);
|
|
32
|
+
process.stdin.on('end', async () => {
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(input);
|
|
35
|
+
const toolName = data.tool_name || 'unknown';
|
|
36
|
+
const toolInput = data.tool_input || {};
|
|
37
|
+
|
|
38
|
+
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasError = data.tool_response?.error ||
|
|
43
|
+
data.tool_response?.exitCode > 0 ||
|
|
44
|
+
data.tool_response?.isError;
|
|
45
|
+
|
|
46
|
+
const argsSummary = {};
|
|
47
|
+
for (const [k, v] of Object.entries(toolInput)) {
|
|
48
|
+
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
49
|
+
? v.slice(0, 200) + '...'
|
|
50
|
+
: v;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
await fetch(`${API_URL}/api/v1/audit-logs`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
tool: toolName,
|
|
61
|
+
arguments: argsSummary,
|
|
62
|
+
decision: hasError ? 'DENY' : 'ALLOW',
|
|
63
|
+
reason: hasError ? 'tool returned error' : 'allowed',
|
|
64
|
+
source: 'claude-code-hook',
|
|
65
|
+
evaluationTimeMs: 0,
|
|
66
|
+
}),
|
|
67
|
+
signal: AbortSignal.timeout(5000),
|
|
68
|
+
});
|
|
69
|
+
} catch {
|
|
70
|
+
// Silent
|
|
71
|
+
}
|
|
72
|
+
process.exit(0);
|
|
73
|
+
});
|