@solongate/proxy 0.81.33 → 0.81.34

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.
@@ -2,3 +2,7 @@
2
2
  {"ms":4,"ts":1784136544239,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
3
3
  {"ms":2,"ts":1784136555335,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
4
4
  {"ms":4,"ts":1784136601528,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
5
+ {"ms":2,"ts":1784137482457,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
6
+ {"ms":4,"ts":1784137524268,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
7
+ {"ms":5,"ts":1784138159970,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
8
+ {"ms":2,"ts":1784138167398,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
@@ -1 +1 @@
1
- {"tool":"Bash","ts":1784136546307}
1
+ {"tool":"Bash","ts":1784138162061}
@@ -1 +1 @@
1
- {"ms":4,"ts":1784136601528,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
1
+ {"ms":2,"ts":1784138167398,"tool":"Bash","session":"4aefeb66-3bff-4335-b710-e4f70d3adadf"}
@@ -1 +1 @@
1
- 1784136557479
1
+ 1784138162060
package/hooks/audit.mjs CHANGED
@@ -8,10 +8,10 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } fr
8
8
  import { resolve, join, isAbsolute } from 'node:path';
9
9
  import { homedir } from 'node:os';
10
10
 
11
- // Bump on every audit.mjs change. The cloud serves the newest version; the guard
11
+ // Bump on every audit hook change. The cloud serves the newest version; the guard
12
12
  // hook installs it on its next run (no re-login needed). See guard.mjs
13
13
  // fetchAndInstallHook / maybeSelfUpdate.
14
- const HOOK_VERSION = 15;
14
+ const HOOK_VERSION = 16;
15
15
 
16
16
  function loadEnvKey(dir) {
17
17
  try {
@@ -268,6 +268,81 @@ function loadDlpRedact() {
268
268
  } catch { return null; }
269
269
  }
270
270
 
271
+ // ── DETECT-mode observation ──
272
+ // In DETECT mode the guard doesn't block (and doesn't log) — the call is ALLOWED
273
+ // and reaches THIS PostToolUse hook. To make "detect" mean observe-AND-record
274
+ // (not silent), we scan the ARGUMENTS for DLP hits and flag rate-limit bursts
275
+ // here, on the ALLOW entry. Block-mode hits are DENIED upstream and carry their
276
+ // own reason, so this only fires for detect-mode ALLOWs. Everything is wrapped
277
+ // fail-safe: any error just omits the field.
278
+
279
+ // Which DLP patterns match the stringified tool arguments (uses the enabled set
280
+ // the guard cached; block+detect both populate dlpRedact). Names only.
281
+ function dlpScanArgs(argsSummary, dlpCfg) {
282
+ try {
283
+ if (!dlpCfg || !Array.isArray(dlpCfg.patterns) || !dlpCfg.patterns.length) return [];
284
+ const text = typeof argsSummary === 'string' ? argsSummary : JSON.stringify(argsSummary || {});
285
+ if (!text) return [];
286
+ const enabled = new Set(dlpCfg.patterns);
287
+ const hits = [];
288
+ for (const p of DLP_PATTERNS) {
289
+ if (!enabled.has(p.name)) continue;
290
+ try { p.re.lastIndex = 0; if (p.re.test(text)) hits.push(p.name); } catch { /* skip */ }
291
+ if (hits.length >= 5) break;
292
+ }
293
+ for (const c of (dlpCfg.custom || [])) {
294
+ try { if (c && c.re && dlpGlobToRe(c.re, 'gi').test(text)) hits.push(c.name || 'custom'); } catch { /* skip */ }
295
+ if (hits.length >= 5) break;
296
+ }
297
+ return hits;
298
+ } catch { return []; }
299
+ }
300
+
301
+ // Detect-mode rate-limit config (delivered only when mode === 'detect').
302
+ function loadRateLimitObserve() {
303
+ try {
304
+ const sel = (process.env.SOLONGATE_AGENT_ID || process.argv[2] || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
305
+ const f = resolve(homedir(), '.solongate', '.policy-cache-' + sel + '.json');
306
+ if (!existsSync(f)) return null;
307
+ const c = JSON.parse(readFileSync(f, 'utf-8'));
308
+ const r = c && c.security && c.security.rateLimitObserve;
309
+ return r && typeof r === 'object' ? r : null;
310
+ } catch { return null; }
311
+ }
312
+
313
+ // Sliding-window burst check for DETECT mode. Uses a SEPARATE stamps file from
314
+ // the guard's enforcement file so observation never interferes with blocking.
315
+ // Returns true when this call is at/over an enabled window's limit.
316
+ function rateLimitObserveBurst(agentKey, limits) {
317
+ try {
318
+ if (!limits) return false;
319
+ const key = String(agentKey || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
320
+ const file = resolve(homedir(), '.solongate', '.ratelimit-observe-' + key + '.json');
321
+ const now = Date.now();
322
+ let stamps = [];
323
+ if (existsSync(file)) { try { stamps = JSON.parse(readFileSync(file, 'utf-8')); } catch { stamps = []; } }
324
+ if (!Array.isArray(stamps)) stamps = [];
325
+ stamps = stamps.filter((t) => typeof t === 'number' && now - t < 86400000);
326
+ if (stamps.length > 50000) stamps = stamps.slice(-50000);
327
+ const windows = [
328
+ { key: 'perDay', ms: 86400000 },
329
+ { key: 'perHour', ms: 3600000 },
330
+ { key: 'perMinute', ms: 60000 },
331
+ ];
332
+ let burst = false;
333
+ for (const w of windows) {
334
+ const limit = limits[w.key];
335
+ if (limit > 0) {
336
+ const count = stamps.reduce((n, t) => (now - t < w.ms ? n + 1 : n), 0);
337
+ if (count >= limit) { burst = true; break; }
338
+ }
339
+ }
340
+ stamps.push(now);
341
+ try { writeFileSync(file, JSON.stringify(stamps)); } catch { /* best-effort */ }
342
+ return burst;
343
+ } catch { return false; }
344
+ }
345
+
271
346
  // Local log storage: the user can opt to keep a full copy of every audit entry
272
347
  // in a file of their choosing (set from the dashboard survey / Settings, then
273
348
  // delivered to us via the same policy cache the guard writes). We append one
@@ -560,6 +635,17 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
560
635
  const permission = guessPermission(toolName);
561
636
  const evaluationTimeMs = readLastEvalMs(toolName, sessionId);
562
637
 
638
+ // DETECT-mode observation: record DLP matches / rate-limit bursts on this
639
+ // ALLOW entry so "detect" means observe-AND-log (not silent). Fail-safe —
640
+ // any error leaves the fields off. Only meaningful on ALLOW (block-mode hits
641
+ // are DENIED upstream with their own reason).
642
+ let dlpMatches = [];
643
+ let rateLimitBurst = false;
644
+ if (decision === 'ALLOW') {
645
+ try { dlpMatches = dlpScanArgs(argsSummary, loadDlpRedact()); } catch { dlpMatches = []; }
646
+ try { rateLimitBurst = rateLimitObserveBurst(AGENT_ID, loadRateLimitObserve()); } catch { rateLimitBurst = false; }
647
+ }
648
+
563
649
  // Local log storage (opt-in): when ON, logs are kept LOCAL ONLY — we append
564
650
  // this entry to the user's chosen file and do NOT send it to the cloud.
565
651
  const localLogs = loadLocalLogs();
@@ -568,6 +654,8 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
568
654
  ts: new Date().toISOString(),
569
655
  tool: toolName, arguments: argsSummary, decision, reason, permission,
570
656
  evaluation_time_ms: evaluationTimeMs, agent_id: AGENT_ID, agent_name: AGENT_NAME, session_id: sessionId,
657
+ ...(dlpMatches.length ? { dlp: dlpMatches } : {}),
658
+ ...(rateLimitBurst ? { rate_limit_burst: true } : {}),
571
659
  });
572
660
  fetchDone = true;
573
661
  maybeExit();
@@ -594,6 +682,8 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
594
682
  agent_id: AGENT_ID,
595
683
  agent_name: AGENT_NAME,
596
684
  session_id: sessionId,
685
+ ...(dlpMatches.length ? { dlp: dlpMatches } : {}),
686
+ ...(rateLimitBurst ? { rate_limit_burst: true } : {}),
597
687
  }),
598
688
  signal: AbortSignal.timeout(5000),
599
689
  }).catch(() => {}).finally(() => { fetchDone = true; maybeExit(); });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.33",
3
+ "version": "0.81.34",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {