clawmoat 0.7.0 → 1.0.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 (178) hide show
  1. package/.dockerignore +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/CONTRIBUTING.md +4 -2
  4. package/DEMO.md +87 -0
  5. package/Dockerfile +5 -18
  6. package/README.md +294 -8
  7. package/SECURITY.md +58 -10
  8. package/THREAT_MODEL.md +129 -0
  9. package/agent/README.md +131 -0
  10. package/agent/index.js +471 -0
  11. package/agent/install-service.sh +94 -0
  12. package/agent/openclaw-hook.js +453 -0
  13. package/agent/provider-setup.js +649 -0
  14. package/agent/setup.js +274 -0
  15. package/assets/BADGE-USAGE.md +20 -0
  16. package/assets/clawmoat-badge.svg +21 -0
  17. package/bin/clawmoat.js +468 -111
  18. package/docs/affiliates/dashboard.html +124 -0
  19. package/docs/affiliates/index.html +236 -0
  20. package/docs/agent-install.html +183 -0
  21. package/docs/ai-agent-security-scanner.html +10 -6
  22. package/docs/badge/index.html +149 -0
  23. package/docs/badge/scanning.svg +23 -0
  24. package/docs/blog/386-malicious-skills.html +262 -0
  25. package/docs/blog/40000-exposed-openclaw-instances.html +201 -0
  26. package/docs/blog/agent-trust-protocol.html +198 -0
  27. package/docs/blog/ai-agent-earns-commissions.html +230 -0
  28. package/docs/blog/bugmageddon-agent-firewall.html +174 -0
  29. package/docs/blog/calculator-math.html +180 -0
  30. package/docs/blog/clawmoat-vs-llamafirewall-nemo-guardrails.html +229 -0
  31. package/docs/blog/host-guardian-launch.html +18 -8
  32. package/docs/blog/ibm-experts-agent-runtime-protection.html +247 -0
  33. package/docs/blog/index.html +211 -9
  34. package/docs/blog/langchain-security-tutorial.html +18 -8
  35. package/docs/blog/mcp-30-cves-security-crisis.html +286 -0
  36. package/docs/blog/meta-researcher-rogue-agent.html +201 -0
  37. package/docs/blog/microsoft-openclaw-workstation-security.html +235 -0
  38. package/docs/blog/nist-ai-agent-standards-clawmoat.html +377 -0
  39. package/docs/blog/oasis-websocket-hijack.html +212 -0
  40. package/docs/blog/ollama-openclaw-security.html +160 -0
  41. package/docs/blog/openclaw-enterprise-readiness-claw10.html +199 -0
  42. package/docs/blog/openclaw-security-reckoning-2026.html +368 -0
  43. package/docs/blog/owasp-agentic-ai-top10.html +18 -8
  44. package/docs/blog/securing-ai-agents.html +18 -8
  45. package/docs/blog/supply-chain-agents.html +18 -8
  46. package/docs/business/index.html +525 -0
  47. package/docs/business/install.html +261 -0
  48. package/docs/checklist.html +174 -0
  49. package/docs/compare/index.html +122 -0
  50. package/docs/compare/lakera/index.html +62 -0
  51. package/docs/compare/llm-guard/index.html +49 -0
  52. package/docs/compare/snyk-agent-scan/index.html +63 -0
  53. package/docs/compare.html +10 -6
  54. package/docs/dashboard/index.html +520 -0
  55. package/docs/finance/index.html +220 -0
  56. package/docs/guides/business-deployment.html +770 -0
  57. package/docs/hall-of-fame.html +174 -0
  58. package/docs/index.html +447 -154
  59. package/docs/install.sh +557 -0
  60. package/docs/integrations/langchain.html +14 -6
  61. package/docs/integrations/openai.html +14 -6
  62. package/docs/integrations/openclaw.html +55 -7
  63. package/docs/plans/2026-03-26-threat-intel-api.md +255 -0
  64. package/docs/plans/2026-04-14-bugmageddon-marketing-pack.md +329 -0
  65. package/docs/plans/2026-04-14-clawmoat-v1-bugmageddon.md +248 -0
  66. package/docs/plans/2026-04-14-v1-release-update.md +91 -0
  67. package/docs/plans/2026-04-19-supabase-audit.md +68 -0
  68. package/docs/plans/2026-05-12-sales-push.md +303 -0
  69. package/docs/playground/index.html +893 -0
  70. package/docs/playground.html +4 -7
  71. package/docs/privacy-policy/index.html +122 -0
  72. package/docs/rfcs/defense-in-depth.md +467 -0
  73. package/docs/scan/index.html +358 -0
  74. package/docs/services/case-study.html +255 -0
  75. package/docs/services/downloads/install-openclaw.bat +45 -0
  76. package/docs/services/downloads/install-openclaw.command +38 -0
  77. package/docs/services/downloads/install-openclaw.sh +38 -0
  78. package/docs/services/get-started.html +165 -0
  79. package/docs/services/index.html +598 -0
  80. package/docs/services/multi-agent-security.html +284 -0
  81. package/docs/services/one-pager.html +99 -0
  82. package/docs/services/pitch-deck.html +229 -0
  83. package/docs/services/roi-calculator.html +258 -0
  84. package/docs/sitemap.xml +192 -2
  85. package/docs/support/index.html +135 -0
  86. package/docs/templates/customer-service/HEARTBEAT.md +61 -0
  87. package/docs/templates/customer-service/MEMORY.md +89 -0
  88. package/docs/templates/customer-service/SOUL.md +41 -0
  89. package/docs/templates/customer-service/USER.md +56 -0
  90. package/docs/templates/executive/HEARTBEAT.md +86 -0
  91. package/docs/templates/executive/MEMORY.md +92 -0
  92. package/docs/templates/executive/SOUL.md +44 -0
  93. package/docs/templates/executive/USER.md +62 -0
  94. package/docs/templates/finance/HEARTBEAT.md +58 -0
  95. package/docs/templates/finance/MEMORY.md +87 -0
  96. package/docs/templates/finance/SOUL.md +38 -0
  97. package/docs/templates/finance/USER.md +53 -0
  98. package/docs/templates/index.html +115 -0
  99. package/docs/templates/operations/HEARTBEAT.md +63 -0
  100. package/docs/templates/operations/MEMORY.md +68 -0
  101. package/docs/templates/operations/SOUL.md +38 -0
  102. package/docs/templates/operations/USER.md +49 -0
  103. package/docs/templates/sales/HEARTBEAT.md +55 -0
  104. package/docs/templates/sales/MEMORY.md +89 -0
  105. package/docs/templates/sales/SOUL.md +34 -0
  106. package/docs/templates/sales/USER.md +54 -0
  107. package/docs/terms-of-service/index.html +122 -0
  108. package/eslint.config.js +32 -0
  109. package/evals/README.md +29 -0
  110. package/evals/cases.json +390 -0
  111. package/evals/results.md +68 -0
  112. package/evals/run.js +180 -0
  113. package/examples/basic-usage.js +38 -0
  114. package/examples/demo-attack/demo.js +186 -0
  115. package/examples/python-quickstart/README.md +54 -0
  116. package/examples/python-quickstart/clawmoat_client.py +167 -0
  117. package/examples/video-demo/README.md +14 -0
  118. package/examples/video-demo/scene-a-normal.js +29 -0
  119. package/examples/video-demo/scene-b-attack-arrives.js +31 -0
  120. package/examples/video-demo/scene-c-hijack.js +44 -0
  121. package/examples/video-demo/scene-d-clawmoat.js +46 -0
  122. package/integrations/crewai/README.md +32 -0
  123. package/integrations/crewai/clawmoat_crewai/__init__.py +17 -0
  124. package/integrations/crewai/clawmoat_crewai/guard.py +103 -0
  125. package/integrations/crewai/pyproject.toml +21 -0
  126. package/integrations/langchain/README.md +91 -0
  127. package/integrations/langchain/clawmoat_langchain/__init__.py +17 -0
  128. package/integrations/langchain/clawmoat_langchain/callback.py +489 -0
  129. package/integrations/langchain/pyproject.toml +32 -0
  130. package/integrations/litellm/README.md +324 -0
  131. package/integrations/litellm/clawmoat_litellm/__init__.py +21 -0
  132. package/integrations/litellm/clawmoat_litellm/callback.py +329 -0
  133. package/integrations/litellm/clawmoat_litellm/proxy_middleware.py +224 -0
  134. package/integrations/litellm/pyproject.toml +74 -0
  135. package/integrations/openai-agents/README.md +392 -0
  136. package/integrations/openai-agents/clawmoat_openai_agents/__init__.py +20 -0
  137. package/integrations/openai-agents/clawmoat_openai_agents/guardrail.py +431 -0
  138. package/integrations/openai-agents/clawmoat_openai_agents/middleware.py +311 -0
  139. package/integrations/openai-agents/pyproject.toml +76 -0
  140. package/package.json +6 -5
  141. package/plugins/openclaw-adapter/PHASE1.md +439 -0
  142. package/plugins/openclaw-adapter/README.md +103 -0
  143. package/plugins/openclaw-adapter/SPEC.md +1644 -0
  144. package/plugins/openclaw-adapter/package.json +31 -0
  145. package/plugins/openclaw-adapter/src/index.test.ts +226 -0
  146. package/plugins/openclaw-adapter/src/index.ts +140 -0
  147. package/plugins/openclaw-adapter/tsconfig.json +14 -0
  148. package/server/data/threats.json +290 -0
  149. package/server/index.js +224 -10
  150. package/src/adapters/express.js +161 -0
  151. package/src/adapters/index.js +92 -0
  152. package/src/adapters/langchain.js +185 -0
  153. package/src/approval/index.js +456 -0
  154. package/src/ban-scanner.js +200 -0
  155. package/src/boundary-scanner.js +296 -0
  156. package/src/ci-scanner.js +279 -0
  157. package/src/code-scanner.js +245 -0
  158. package/src/enforce.js +166 -0
  159. package/src/finance/index.js +585 -0
  160. package/src/finance/mcp-firewall.js +486 -0
  161. package/src/formatters/json.js +80 -0
  162. package/src/formatters/sarif.js +388 -0
  163. package/src/guardian/alerts.js +34 -3
  164. package/src/guardian/gateway-monitor.js +590 -0
  165. package/src/guardian/index.js +41 -2
  166. package/src/index.js +105 -0
  167. package/src/integrations/agentmesh.js +501 -0
  168. package/src/language-detector.js +201 -0
  169. package/src/mcp-scanner.js +253 -0
  170. package/src/multimodal/index.js +579 -0
  171. package/src/obfuscation-scanner.js +457 -0
  172. package/src/policy-engine.js +402 -0
  173. package/src/scanners/dependency-attacks.js +128 -0
  174. package/src/scanners/prompt-injection.js +18 -0
  175. package/src/scanners/supply-chain.js +14 -0
  176. package/src/templates/default-config.yml +90 -0
  177. package/src/vuln-ops/exploitability.js +46 -0
  178. package/src/watch/live-monitor.js +720 -0
@@ -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 };
@@ -73,7 +73,14 @@ const TIERS = {
73
73
  };
74
74
 
75
75
  // ─── Forbidden Zones (always blocked except in 'full' mode) ─────────
76
+ // Helper: create cross-platform forbidden zone pattern
77
+ // Matches both Unix (~/.ssh) and Windows (C:\Users\X\.ssh, %USERPROFILE%\.ssh)
78
+ function _crossPlatformPattern(unixPattern) {
79
+ return unixPattern;
80
+ }
81
+
76
82
  const FORBIDDEN_ZONES = [
83
+ // Unix dotfiles (also matches on Windows when accessed via forward slashes)
77
84
  { pattern: /^~?\/?\.ssh\b/i, label: 'SSH keys', severity: 'critical' },
78
85
  { pattern: /^~?\/?\.gnupg\b/i, label: 'GPG keys', severity: 'critical' },
79
86
  { pattern: /^~?\/?\.aws\b/i, label: 'AWS credentials', severity: 'critical' },
@@ -97,6 +104,19 @@ const FORBIDDEN_ZONES = [
97
104
  { pattern: /^~?\/?\.password-store\b/i, label: 'Password store', severity: 'critical' },
98
105
  { pattern: /^~?\/?\.1password\b/i, label: '1Password data', severity: 'critical' },
99
106
  { pattern: /(?:KeePass|\.kdbx)$/i, label: 'KeePass database', severity: 'critical' },
107
+
108
+ // Windows-specific forbidden zones
109
+ { pattern: /[\\\/]AppData[\\\/](?:Local|Roaming)[\\\/](?:Google[\\\/]Chrome|Microsoft[\\\/]Edge|BraveSoftware)[\\\/]User Data\b/i, label: 'Windows browser credentials', severity: 'critical' },
110
+ { pattern: /[\\\/]\.?credential[s]?\b/i, label: 'Credential store', severity: 'critical' },
111
+ { pattern: /[\\\/]AppData[\\\/]Roaming[\\\/](?:npm[\\\/])?\.npmrc$/i, label: 'Windows npm credentials', severity: 'high' },
112
+ { pattern: /[\\\/]\.aws[\\\/]/i, label: 'AWS credentials (Windows)', severity: 'critical' },
113
+ { pattern: /[\\\/]\.ssh[\\\/]/i, label: 'SSH keys (Windows)', severity: 'critical' },
114
+ { pattern: /[\\\/]\.gnupg[\\\/]/i, label: 'GPG keys (Windows)', severity: 'critical' },
115
+ { pattern: /[\\\/]AppData[\\\/]Roaming[\\\/]gcloud\b/i, label: 'Google Cloud (Windows)', severity: 'high' },
116
+ { pattern: /[\\\/]AppData[\\\/]Roaming[\\\/]GitHub CLI\b/i, label: 'GitHub CLI (Windows)', severity: 'high' },
117
+ { pattern: /[\\\/]AppData[\\\/]Local[\\\/]Microsoft[\\\/]Credentials\b/i, label: 'Windows Credential Manager', severity: 'critical' },
118
+ { pattern: /[\\\/]ntuser\.dat$/i, label: 'Windows registry hive', severity: 'critical' },
119
+ { pattern: /\\Windows\\System32\\config\\(?:SAM|SECURITY|SYSTEM)/i, label: 'Windows SAM/Security', severity: 'critical' },
100
120
  ];
101
121
 
102
122
  // ─── Dangerous Commands (blocked in observer/worker, warned in standard) ─
@@ -403,9 +423,13 @@ class HostGuardian {
403
423
  _checkForbidden(rawPath, resolvedPath) {
404
424
  const allForbidden = [...FORBIDDEN_ZONES, ...this.extraForbidden];
405
425
  const normalized = resolvedPath.replace(this.home, '~');
426
+ // Also check with forward slashes for Windows path compat
427
+ const forwardSlashed = resolvedPath.replace(/\\/g, '/');
428
+ const normalizedForward = normalized.replace(/\\/g, '/');
406
429
 
407
430
  for (const zone of allForbidden) {
408
- if (zone.pattern.test(rawPath) || zone.pattern.test(normalized) || zone.pattern.test(resolvedPath)) {
431
+ if (zone.pattern.test(rawPath) || zone.pattern.test(normalized) || zone.pattern.test(resolvedPath) ||
432
+ zone.pattern.test(forwardSlashed) || zone.pattern.test(normalizedForward)) {
409
433
  if (this.mode === 'full') {
410
434
  // Full mode: log but allow
411
435
  return {
@@ -433,7 +457,11 @@ class HostGuardian {
433
457
  if (this._inWorkspace(resolvedPath)) return 'workspace';
434
458
  if (this._inSafeZone(resolvedPath)) return 'safe';
435
459
  if (resolvedPath.startsWith(this.home)) return 'home';
460
+ // Unix system paths
436
461
  if (resolvedPath.startsWith('/etc') || resolvedPath.startsWith('/usr') || resolvedPath.startsWith('/var')) return 'system';
462
+ // Windows system paths
463
+ if (/^[A-Z]:\\Windows\\/i.test(resolvedPath) || /^[A-Z]:\\Program Files/i.test(resolvedPath)) return 'system';
464
+ if (/^[A-Z]:\\ProgramData\\/i.test(resolvedPath)) return 'system';
437
465
  return 'unknown';
438
466
  }
439
467
 
@@ -447,10 +475,21 @@ class HostGuardian {
447
475
 
448
476
  _resolve(p) {
449
477
  if (!p) return '';
450
- const expanded = p.replace(/^~/, this.home);
478
+ // Handle Windows %USERPROFILE% and %HOME% env vars
479
+ const expanded = p
480
+ .replace(/^~/, this.home)
481
+ .replace(/%USERPROFILE%/gi, this.home)
482
+ .replace(/%HOME%/gi, this.home)
483
+ .replace(/%APPDATA%/gi, path.join(this.home, 'AppData', 'Roaming'))
484
+ .replace(/%LOCALAPPDATA%/gi, path.join(this.home, 'AppData', 'Local'));
451
485
  return path.resolve(expanded);
452
486
  }
453
487
 
488
+ // Normalize path separators for cross-platform comparison
489
+ _normalizePath(p) {
490
+ return p.replace(/\\/g, '/');
491
+ }
492
+
454
493
  _sanitizeArgs(args) {
455
494
  // Don't log full file contents or long commands
456
495
  const sanitized = { ...args };