clawmoat 0.5.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CONTRIBUTING.md +4 -2
  2. package/README.md +86 -3
  3. package/SECURITY.md +58 -10
  4. package/bin/clawmoat.js +298 -1
  5. package/clawmoat-0.8.0.tgz +0 -0
  6. package/docs/blog/386-malicious-skills.html +255 -0
  7. package/docs/blog/40000-exposed-openclaw-instances.html +194 -0
  8. package/docs/blog/agent-trust-protocol.html +197 -0
  9. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +223 -0
  10. package/docs/blog/ibm-experts-agent-runtime-protection.html +238 -0
  11. package/docs/blog/index.html +168 -0
  12. package/docs/blog/mcp-30-cves-security-crisis.html +279 -0
  13. package/docs/blog/microsoft-openclaw-workstation-security.html +234 -0
  14. package/docs/blog/nist-ai-agent-standards-clawmoat.html +369 -0
  15. package/docs/blog/oasis-websocket-hijack.html +205 -0
  16. package/docs/blog/ollama-openclaw-security.html +154 -0
  17. package/docs/blog/openclaw-enterprise-readiness-claw10.html +198 -0
  18. package/docs/blog/openclaw-security-reckoning-2026.html +361 -0
  19. package/docs/blog/supply-chain-agents.html +166 -0
  20. package/docs/blog/supply-chain-agents.md +79 -0
  21. package/docs/business/index.html +530 -0
  22. package/docs/business/install.html +247 -0
  23. package/docs/checklist.html +168 -0
  24. package/docs/finance/index.html +217 -0
  25. package/docs/hall-of-fame.html +168 -0
  26. package/docs/index.html +328 -90
  27. package/docs/install.sh +557 -0
  28. package/docs/privacy-policy/index.html +122 -0
  29. package/docs/scan/index.html +214 -0
  30. package/docs/sitemap.xml +132 -2
  31. package/docs/support/index.html +124 -0
  32. package/docs/terms-of-service/index.html +122 -0
  33. package/examples/basic-usage.js +38 -0
  34. package/package.json +1 -1
  35. package/server/index.js +179 -14
  36. package/server/index.js.patch +1 -0
  37. package/src/finance/index.js +585 -0
  38. package/src/finance/mcp-firewall.js +486 -0
  39. package/src/guardian/cve-verify.js +129 -0
  40. package/src/guardian/gateway-monitor.js +590 -0
  41. package/src/guardian/index.js +3 -1
  42. package/src/guardian/insider-threat.js +498 -0
  43. package/src/index.js +3 -0
  44. package/src/middleware/openclaw.js +28 -1
@@ -0,0 +1,590 @@
1
+ /**
2
+ * ClawMoat Gateway Monitor
3
+ *
4
+ * Detects and mitigates the Oasis Security WebSocket hijack attack
5
+ * (CVE-2026-XXXXX) where any website can silently take full control
6
+ * of an OpenClaw agent via localhost WebSocket brute-force.
7
+ *
8
+ * Attack chain:
9
+ * 1. Malicious website opens WebSocket to localhost:18789
10
+ * 2. Brute-forces gateway password (rate limiter exempts localhost)
11
+ * 3. Auto-registers as trusted device (no user prompt for localhost)
12
+ * 4. Full agent takeover: messages, files, shell commands
13
+ *
14
+ * This module monitors for:
15
+ * - Rapid authentication attempts (brute-force detection)
16
+ * - Unexpected device pairings
17
+ * - WebSocket connections from browser origins
18
+ * - Gateway configuration weaknesses
19
+ *
20
+ * @module clawmoat/guardian/gateway-monitor
21
+ * @see https://www.oasis.security/blog/openclaw-vulnerability
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const os = require('os');
27
+ const crypto = require('crypto');
28
+
29
+ // ─── Constants ──────────────────────────────────────────────────────
30
+
31
+ /** Default OpenClaw gateway port */
32
+ const DEFAULT_GATEWAY_PORT = 18789;
33
+
34
+ /** Maximum auth attempts before triggering alert */
35
+ const BRUTE_FORCE_THRESHOLD = 10;
36
+
37
+ /** Time window for brute-force detection (ms) */
38
+ const BRUTE_FORCE_WINDOW_MS = 60_000;
39
+
40
+ /** Maximum new device pairings before alert */
41
+ const PAIRING_THRESHOLD = 3;
42
+
43
+ /** Time window for pairing flood detection (ms) */
44
+ const PAIRING_WINDOW_MS = 300_000;
45
+
46
+ /** Known browser WebSocket origins that indicate cross-origin attack */
47
+ const SUSPICIOUS_ORIGINS = [
48
+ /^https?:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/i,
49
+ ];
50
+
51
+ /** Gateway config paths to check */
52
+ const GATEWAY_CONFIG_PATHS = [
53
+ path.join(os.homedir(), '.openclaw', 'gateway.json'),
54
+ path.join(os.homedir(), '.openclaw', 'config.json5'),
55
+ path.join(os.homedir(), '.config', 'openclaw', 'gateway.json'),
56
+ ];
57
+
58
+ // ─── Gateway Monitor ────────────────────────────────────────────────
59
+
60
+ class GatewayMonitor {
61
+ /**
62
+ * @param {Object} options
63
+ * @param {number} [options.port=18789] - Gateway port to monitor
64
+ * @param {number} [options.bruteForceThreshold=10] - Auth attempts before alert
65
+ * @param {number} [options.bruteForceWindowMs=60000] - Time window for detection
66
+ * @param {number} [options.pairingThreshold=3] - Max pairings before alert
67
+ * @param {Function} [options.onAlert] - Callback for security alerts
68
+ * @param {string} [options.logPath] - Path to write audit log
69
+ */
70
+ constructor(options = {}) {
71
+ this.port = options.port || DEFAULT_GATEWAY_PORT;
72
+ this.bruteForceThreshold = options.bruteForceThreshold || BRUTE_FORCE_THRESHOLD;
73
+ this.bruteForceWindowMs = options.bruteForceWindowMs || BRUTE_FORCE_WINDOW_MS;
74
+ this.pairingThreshold = options.pairingThreshold || PAIRING_THRESHOLD;
75
+ this.pairingWindowMs = options.pairingWindowMs || PAIRING_WINDOW_MS;
76
+ this.onAlert = options.onAlert || null;
77
+ this.logPath = options.logPath || null;
78
+
79
+ // State tracking
80
+ this.authAttempts = []; // { timestamp, source, success }
81
+ this.devicePairings = []; // { timestamp, deviceId, source, autoApproved }
82
+ this.wsConnections = []; // { timestamp, origin, source }
83
+ this.alerts = []; // { timestamp, type, severity, message, details }
84
+ this.knownDevices = new Set();
85
+ this.configIssues = [];
86
+ }
87
+
88
+ // ─── Authentication Monitoring ──────────────────────────────────
89
+
90
+ /**
91
+ * Record an authentication attempt and check for brute-force patterns.
92
+ * @param {Object} attempt
93
+ * @param {string} attempt.source - Source IP/identifier
94
+ * @param {boolean} attempt.success - Whether auth succeeded
95
+ * @param {string} [attempt.origin] - WebSocket origin header
96
+ * @param {number} [attempt.timestamp] - Unix ms (defaults to now)
97
+ * @returns {Object} Analysis result
98
+ */
99
+ recordAuthAttempt(attempt) {
100
+ const record = {
101
+ timestamp: attempt.timestamp || Date.now(),
102
+ source: attempt.source || 'unknown',
103
+ success: !!attempt.success,
104
+ origin: attempt.origin || null,
105
+ };
106
+
107
+ this.authAttempts.push(record);
108
+ this._pruneOldEntries(this.authAttempts, this.bruteForceWindowMs);
109
+
110
+ const analysis = this._analyzeAuthPatterns(record);
111
+
112
+ if (this.logPath) {
113
+ this._appendLog({ type: 'auth_attempt', ...record, analysis });
114
+ }
115
+
116
+ return analysis;
117
+ }
118
+
119
+ /**
120
+ * Analyze authentication patterns for brute-force indicators.
121
+ * @private
122
+ */
123
+ _analyzeAuthPatterns(latestAttempt) {
124
+ const now = latestAttempt.timestamp;
125
+ const windowStart = now - this.bruteForceWindowMs;
126
+ const recentAttempts = this.authAttempts.filter(a => a.timestamp >= windowStart);
127
+
128
+ const result = {
129
+ totalAttempts: recentAttempts.length,
130
+ failedAttempts: recentAttempts.filter(a => !a.success).length,
131
+ uniqueSources: new Set(recentAttempts.map(a => a.source)).size,
132
+ isBruteForce: false,
133
+ isSuspiciousOrigin: false,
134
+ alerts: [],
135
+ };
136
+
137
+ // Check for brute-force
138
+ if (result.failedAttempts >= this.bruteForceThreshold) {
139
+ result.isBruteForce = true;
140
+ const alert = {
141
+ timestamp: now,
142
+ type: 'brute_force_detected',
143
+ severity: 'critical',
144
+ message: `Brute-force attack detected: ${result.failedAttempts} failed auth attempts in ${this.bruteForceWindowMs / 1000}s`,
145
+ details: {
146
+ failedAttempts: result.failedAttempts,
147
+ windowMs: this.bruteForceWindowMs,
148
+ sources: [...new Set(recentAttempts.filter(a => !a.success).map(a => a.source))],
149
+ recommendation: 'Change gateway password immediately. Use 32+ character password. Consider binding to non-localhost IP.',
150
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
151
+ },
152
+ };
153
+ this._emitAlert(alert);
154
+ result.alerts.push(alert);
155
+ }
156
+
157
+ // Check for suspicious origin
158
+ if (latestAttempt.origin) {
159
+ const isSuspicious = SUSPICIOUS_ORIGINS.some(re => re.test(latestAttempt.origin));
160
+ if (isSuspicious) {
161
+ result.isSuspiciousOrigin = true;
162
+ const alert = {
163
+ timestamp: now,
164
+ type: 'suspicious_websocket_origin',
165
+ severity: 'critical',
166
+ message: `WebSocket connection from suspicious origin: ${latestAttempt.origin}`,
167
+ details: {
168
+ origin: latestAttempt.origin,
169
+ source: latestAttempt.source,
170
+ recommendation: 'This may be a cross-origin WebSocket hijack attempt. Verify no unauthorized browser tabs are connecting to your gateway.',
171
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
172
+ },
173
+ };
174
+ this._emitAlert(alert);
175
+ result.alerts.push(alert);
176
+ }
177
+ }
178
+
179
+ // Check for rapid successful auth (may indicate stolen credentials)
180
+ const successfulAttempts = recentAttempts.filter(a => a.success);
181
+ const uniqueSuccessSources = new Set(successfulAttempts.map(a => a.source));
182
+ if (uniqueSuccessSources.size > 2) {
183
+ const alert = {
184
+ timestamp: now,
185
+ type: 'multiple_auth_sources',
186
+ severity: 'warning',
187
+ message: `Authenticated from ${uniqueSuccessSources.size} different sources in ${this.bruteForceWindowMs / 1000}s`,
188
+ details: {
189
+ sources: [...uniqueSuccessSources],
190
+ recommendation: 'Verify all authentication sources are legitimate. Rotate gateway password if any are unknown.',
191
+ },
192
+ };
193
+ this._emitAlert(alert);
194
+ result.alerts.push(alert);
195
+ }
196
+
197
+ return result;
198
+ }
199
+
200
+ // ─── Device Pairing Monitoring ──────────────────────────────────
201
+
202
+ /**
203
+ * Record a device pairing event and check for suspicious patterns.
204
+ * @param {Object} pairing
205
+ * @param {string} pairing.deviceId - Device identifier
206
+ * @param {string} [pairing.source] - Source IP
207
+ * @param {boolean} [pairing.autoApproved] - Whether auto-approved
208
+ * @param {string} [pairing.deviceName] - Human-readable device name
209
+ * @param {number} [pairing.timestamp] - Unix ms
210
+ * @returns {Object} Analysis result
211
+ */
212
+ recordDevicePairing(pairing) {
213
+ const record = {
214
+ timestamp: pairing.timestamp || Date.now(),
215
+ deviceId: pairing.deviceId,
216
+ source: pairing.source || 'unknown',
217
+ autoApproved: !!pairing.autoApproved,
218
+ deviceName: pairing.deviceName || null,
219
+ };
220
+
221
+ this.devicePairings.push(record);
222
+ this._pruneOldEntries(this.devicePairings, this.pairingWindowMs);
223
+
224
+ const isNew = !this.knownDevices.has(record.deviceId);
225
+ if (isNew) {
226
+ this.knownDevices.add(record.deviceId);
227
+ }
228
+
229
+ const analysis = this._analyzePairingPatterns(record, isNew);
230
+
231
+ if (this.logPath) {
232
+ this._appendLog({ type: 'device_pairing', ...record, isNew, analysis });
233
+ }
234
+
235
+ return analysis;
236
+ }
237
+
238
+ /**
239
+ * Analyze device pairing patterns.
240
+ * @private
241
+ */
242
+ _analyzePairingPatterns(latestPairing, isNew) {
243
+ const now = latestPairing.timestamp;
244
+ const windowStart = now - this.pairingWindowMs;
245
+ const recentPairings = this.devicePairings.filter(p => p.timestamp >= windowStart);
246
+
247
+ const result = {
248
+ isNew,
249
+ totalRecentPairings: recentPairings.length,
250
+ autoApprovedCount: recentPairings.filter(p => p.autoApproved).length,
251
+ isPairingFlood: false,
252
+ isAutoApproveRisk: false,
253
+ alerts: [],
254
+ };
255
+
256
+ // Check for pairing flood
257
+ if (recentPairings.length >= this.pairingThreshold) {
258
+ result.isPairingFlood = true;
259
+ const alert = {
260
+ timestamp: now,
261
+ type: 'pairing_flood',
262
+ severity: 'high',
263
+ message: `${recentPairings.length} device pairings in ${this.pairingWindowMs / 1000}s (threshold: ${this.pairingThreshold})`,
264
+ details: {
265
+ devices: recentPairings.map(p => ({ id: p.deviceId, source: p.source, autoApproved: p.autoApproved })),
266
+ recommendation: 'Review all paired devices. Revoke unknown devices. Disable auto-approve for localhost.',
267
+ },
268
+ };
269
+ this._emitAlert(alert);
270
+ result.alerts.push(alert);
271
+ }
272
+
273
+ // Check for auto-approved pairing from localhost (the Oasis attack)
274
+ if (latestPairing.autoApproved && isNew) {
275
+ result.isAutoApproveRisk = true;
276
+ const severity = latestPairing.source === 'localhost' || latestPairing.source === '127.0.0.1' ? 'critical' : 'warning';
277
+ const alert = {
278
+ timestamp: now,
279
+ type: 'auto_approved_pairing',
280
+ severity,
281
+ message: `New device "${latestPairing.deviceName || latestPairing.deviceId}" auto-approved from ${latestPairing.source}`,
282
+ details: {
283
+ deviceId: latestPairing.deviceId,
284
+ deviceName: latestPairing.deviceName,
285
+ source: latestPairing.source,
286
+ recommendation: severity === 'critical'
287
+ ? 'CRITICAL: Localhost auto-approve is the exact vector used in the Oasis WebSocket hijack. Disable auto-approve immediately.'
288
+ : 'Verify this device pairing was intentional.',
289
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
290
+ },
291
+ };
292
+ this._emitAlert(alert);
293
+ result.alerts.push(alert);
294
+ }
295
+
296
+ return result;
297
+ }
298
+
299
+ // ─── Gateway Configuration Audit ────────────────────────────────
300
+
301
+ /**
302
+ * Audit the gateway configuration for security weaknesses.
303
+ * Checks for the specific vulnerabilities exploited in the Oasis attack.
304
+ * @returns {Object} Audit results
305
+ */
306
+ auditGatewayConfig() {
307
+ const issues = [];
308
+ let configFound = false;
309
+ let config = null;
310
+
311
+ // Find and parse gateway config
312
+ for (const configPath of GATEWAY_CONFIG_PATHS) {
313
+ try {
314
+ const raw = fs.readFileSync(configPath, 'utf8');
315
+ // Handle JSON5 (strip comments)
316
+ const cleaned = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
317
+ config = JSON.parse(cleaned);
318
+ configFound = true;
319
+ break;
320
+ } catch {
321
+ // Try next path
322
+ }
323
+ }
324
+
325
+ if (!configFound) {
326
+ issues.push({
327
+ severity: 'info',
328
+ issue: 'gateway_config_not_found',
329
+ message: 'Could not find gateway configuration file. Using defaults.',
330
+ recommendation: 'Ensure gateway is configured with strong authentication.',
331
+ });
332
+ }
333
+
334
+ // Check 1: Password strength
335
+ if (config) {
336
+ const token = config.auth?.token || config.gatewayToken || config.token;
337
+ if (token) {
338
+ if (token.length < 20) {
339
+ issues.push({
340
+ severity: 'critical',
341
+ issue: 'weak_gateway_password',
342
+ message: `Gateway password is only ${token.length} characters. Oasis attack brute-forces at hundreds of attempts/second.`,
343
+ recommendation: 'Use a 32+ character random password. Run: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"',
344
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
345
+ });
346
+ } else if (token.length < 32) {
347
+ issues.push({
348
+ severity: 'warning',
349
+ issue: 'short_gateway_password',
350
+ message: `Gateway password is ${token.length} characters. Consider using 32+ for brute-force resistance.`,
351
+ recommendation: 'Use a longer random password for maximum security.',
352
+ });
353
+ }
354
+ } else {
355
+ issues.push({
356
+ severity: 'critical',
357
+ issue: 'no_gateway_password',
358
+ message: 'No gateway authentication configured. Anyone on localhost can connect.',
359
+ recommendation: 'Set a strong gateway token immediately.',
360
+ });
361
+ }
362
+ }
363
+
364
+ // Check 2: Binding address
365
+ if (config) {
366
+ const host = config.host || config.gateway?.host || config.bind;
367
+ if (!host || host === '0.0.0.0') {
368
+ issues.push({
369
+ severity: 'critical',
370
+ issue: 'gateway_bound_all_interfaces',
371
+ message: 'Gateway is bound to all interfaces (0.0.0.0). Accessible from any network.',
372
+ recommendation: 'Bind to localhost (127.0.0.1) or a Tailscale/VPN IP only.',
373
+ });
374
+ } else if (host === '127.0.0.1' || host === 'localhost') {
375
+ issues.push({
376
+ severity: 'warning',
377
+ issue: 'gateway_bound_localhost',
378
+ message: 'Gateway bound to localhost. Still vulnerable to Oasis WebSocket attack from browser.',
379
+ recommendation: 'Consider binding to a Tailscale IP to prevent browser-based WebSocket attacks.',
380
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
381
+ });
382
+ }
383
+ }
384
+
385
+ // Check 3: Auto-approve settings
386
+ if (config) {
387
+ const autoApprove = config.autoApprove ?? config.gateway?.autoApprove ?? config.pairApproval?.auto;
388
+ if (autoApprove === true || autoApprove === 'localhost') {
389
+ issues.push({
390
+ severity: 'critical',
391
+ issue: 'auto_approve_enabled',
392
+ message: 'Device auto-approve is enabled. This is the exact vector exploited in the Oasis attack.',
393
+ recommendation: 'Disable auto-approve. Require manual confirmation for all device pairings.',
394
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
395
+ });
396
+ }
397
+ }
398
+
399
+ // Check 4: Rate limiting
400
+ if (config) {
401
+ const rateLimit = config.rateLimit || config.gateway?.rateLimit;
402
+ if (!rateLimit) {
403
+ issues.push({
404
+ severity: 'high',
405
+ issue: 'no_rate_limiting',
406
+ message: 'No rate limiting configured. Gateway can be brute-forced at hundreds of attempts/second.',
407
+ recommendation: 'Enable rate limiting, including for localhost connections.',
408
+ });
409
+ } else if (rateLimit.excludeLocalhost || rateLimit.trustLocalhost) {
410
+ issues.push({
411
+ severity: 'critical',
412
+ issue: 'localhost_rate_limit_exempt',
413
+ message: 'Localhost is exempt from rate limiting. This is the exact configuration exploited in the Oasis attack.',
414
+ recommendation: 'Remove localhost exemption from rate limiting.',
415
+ reference: 'https://www.oasis.security/blog/openclaw-vulnerability',
416
+ });
417
+ }
418
+ }
419
+
420
+ // Check 5: Gateway port
421
+ if (config) {
422
+ const port = config.port || config.gateway?.port || DEFAULT_GATEWAY_PORT;
423
+ if (port === DEFAULT_GATEWAY_PORT) {
424
+ issues.push({
425
+ severity: 'low',
426
+ issue: 'default_gateway_port',
427
+ message: `Using default gateway port ${DEFAULT_GATEWAY_PORT}. Easily discoverable.`,
428
+ recommendation: 'Consider using a non-default port to reduce attack surface.',
429
+ });
430
+ }
431
+ }
432
+
433
+ this.configIssues = issues;
434
+
435
+ const result = {
436
+ configFound,
437
+ issues,
438
+ criticalCount: issues.filter(i => i.severity === 'critical').length,
439
+ highCount: issues.filter(i => i.severity === 'high').length,
440
+ warningCount: issues.filter(i => i.severity === 'warning').length,
441
+ score: this._calculateSecurityScore(issues),
442
+ oasisVulnerable: issues.some(i => i.reference?.includes('oasis')),
443
+ };
444
+
445
+ if (result.criticalCount > 0) {
446
+ const alert = {
447
+ timestamp: Date.now(),
448
+ type: 'gateway_audit_critical',
449
+ severity: 'critical',
450
+ message: `Gateway audit found ${result.criticalCount} critical issues`,
451
+ details: {
452
+ issues: issues.filter(i => i.severity === 'critical'),
453
+ score: result.score,
454
+ oasisVulnerable: result.oasisVulnerable,
455
+ },
456
+ };
457
+ this._emitAlert(alert);
458
+ }
459
+
460
+ if (this.logPath) {
461
+ this._appendLog({ type: 'gateway_audit', ...result });
462
+ }
463
+
464
+ return result;
465
+ }
466
+
467
+ /**
468
+ * Calculate a security score (0-100) based on config issues.
469
+ * @private
470
+ */
471
+ _calculateSecurityScore(issues) {
472
+ let score = 100;
473
+ for (const issue of issues) {
474
+ switch (issue.severity) {
475
+ case 'critical': score -= 25; break;
476
+ case 'high': score -= 15; break;
477
+ case 'warning': score -= 10; break;
478
+ case 'low': score -= 5; break;
479
+ }
480
+ }
481
+ return Math.max(0, score);
482
+ }
483
+
484
+ // ─── Recommendations ────────────────────────────────────────────
485
+
486
+ /**
487
+ * Generate a strong gateway token.
488
+ * @returns {string} 64-character hex token
489
+ */
490
+ static generateStrongToken() {
491
+ return crypto.randomBytes(32).toString('hex');
492
+ }
493
+
494
+ /**
495
+ * Get hardened gateway configuration recommendations.
496
+ * @returns {Object} Recommended config
497
+ */
498
+ static getHardenedConfig() {
499
+ return {
500
+ gateway: {
501
+ host: '127.0.0.1', // Or Tailscale IP for remote access
502
+ port: 18700 + Math.floor(Math.random() * 89), // Random non-default port
503
+ token: GatewayMonitor.generateStrongToken(),
504
+ rateLimit: {
505
+ windowMs: 60_000,
506
+ maxAttempts: 5,
507
+ excludeLocalhost: false, // CRITICAL: do NOT exclude localhost
508
+ },
509
+ autoApprove: false, // CRITICAL: require manual device approval
510
+ pairApproval: {
511
+ auto: false,
512
+ requireConfirmation: true,
513
+ notifyOnPairing: true,
514
+ },
515
+ },
516
+ _comment: 'Generated by ClawMoat gateway-monitor. See: https://www.oasis.security/blog/openclaw-vulnerability',
517
+ };
518
+ }
519
+
520
+ // ─── Utility Methods ────────────────────────────────────────────
521
+
522
+ /**
523
+ * Get all alerts, optionally filtered by severity.
524
+ * @param {string} [minSeverity] - Minimum severity: 'low' | 'warning' | 'high' | 'critical'
525
+ * @returns {Array} Alerts
526
+ */
527
+ getAlerts(minSeverity) {
528
+ if (!minSeverity) return [...this.alerts];
529
+ const levels = { low: 0, warning: 1, high: 2, critical: 3 };
530
+ const min = levels[minSeverity] || 0;
531
+ return this.alerts.filter(a => (levels[a.severity] || 0) >= min);
532
+ }
533
+
534
+ /**
535
+ * Get a summary report.
536
+ * @returns {Object} Summary
537
+ */
538
+ getSummary() {
539
+ return {
540
+ authAttempts: this.authAttempts.length,
541
+ failedAuth: this.authAttempts.filter(a => !a.success).length,
542
+ devicePairings: this.devicePairings.length,
543
+ knownDevices: this.knownDevices.size,
544
+ alerts: this.alerts.length,
545
+ criticalAlerts: this.alerts.filter(a => a.severity === 'critical').length,
546
+ configIssues: this.configIssues.length,
547
+ };
548
+ }
549
+
550
+ /**
551
+ * Reset all state (for testing).
552
+ */
553
+ reset() {
554
+ this.authAttempts = [];
555
+ this.devicePairings = [];
556
+ this.wsConnections = [];
557
+ this.alerts = [];
558
+ this.knownDevices.clear();
559
+ this.configIssues = [];
560
+ }
561
+
562
+ /** @private */
563
+ _emitAlert(alert) {
564
+ this.alerts.push(alert);
565
+ if (this.onAlert) {
566
+ this.onAlert(alert);
567
+ }
568
+ }
569
+
570
+ /** @private */
571
+ _pruneOldEntries(arr, maxAgeMs) {
572
+ const cutoff = Date.now() - maxAgeMs;
573
+ while (arr.length > 0 && arr[0].timestamp < cutoff) {
574
+ arr.shift();
575
+ }
576
+ }
577
+
578
+ /** @private */
579
+ _appendLog(entry) {
580
+ if (!this.logPath) return;
581
+ try {
582
+ const line = JSON.stringify({ ...entry, _ts: new Date().toISOString() }) + '\n';
583
+ fs.appendFileSync(this.logPath, line);
584
+ } catch {
585
+ // Silently fail — don't let logging break monitoring
586
+ }
587
+ }
588
+ }
589
+
590
+ module.exports = { GatewayMonitor, DEFAULT_GATEWAY_PORT, BRUTE_FORCE_THRESHOLD };
@@ -683,4 +683,6 @@ class CredentialMonitor {
683
683
  }
684
684
  }
685
685
 
686
- module.exports = { HostGuardian, CredentialMonitor, TIERS, FORBIDDEN_ZONES, DANGEROUS_COMMANDS, SAFE_COMMANDS };
686
+ const { CVEVerifier } = require('./cve-verify');
687
+
688
+ module.exports = { HostGuardian, CredentialMonitor, CVEVerifier, TIERS, FORBIDDEN_ZONES, DANGEROUS_COMMANDS, SAFE_COMMANDS };