clawguard-cli 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1462 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import fs9 from "fs";
5
+ import { Command } from "commander";
6
+ import chalk3 from "chalk";
7
+
8
+ // src/models.ts
9
+ import { z } from "zod";
10
+ var Severity = {
11
+ CRITICAL: "CRITICAL",
12
+ HIGH: "HIGH",
13
+ MEDIUM: "MEDIUM",
14
+ INFO: "INFO"
15
+ };
16
+ var SEVERITY_DEDUCTIONS = {
17
+ [Severity.CRITICAL]: 20,
18
+ [Severity.HIGH]: 10,
19
+ [Severity.MEDIUM]: 5,
20
+ [Severity.INFO]: 0
21
+ };
22
+ var FindingSchema = z.object({
23
+ severity: z.enum(["CRITICAL", "HIGH", "MEDIUM", "INFO"]),
24
+ title: z.string(),
25
+ details: z.array(z.string()).default([]),
26
+ fix: z.string().default(""),
27
+ category: z.string().default("")
28
+ });
29
+ function createFinding(params) {
30
+ return {
31
+ severity: params.severity,
32
+ title: params.title,
33
+ details: params.details ?? [],
34
+ fix: params.fix ?? "",
35
+ category: params.category ?? ""
36
+ };
37
+ }
38
+ function createScanResult(openclawPath = "") {
39
+ return {
40
+ findings: [],
41
+ openclawPath,
42
+ openclawVersion: null,
43
+ nodeVersion: null
44
+ };
45
+ }
46
+ function getScore(result) {
47
+ let total = 100;
48
+ for (const f of result.findings) {
49
+ total -= SEVERITY_DEDUCTIONS[f.severity];
50
+ }
51
+ return Math.max(0, total);
52
+ }
53
+ function getCriticalCount(result) {
54
+ return result.findings.filter((f) => f.severity === Severity.CRITICAL).length;
55
+ }
56
+ function getHighCount(result) {
57
+ return result.findings.filter((f) => f.severity === Severity.HIGH).length;
58
+ }
59
+ function getMediumCount(result) {
60
+ return result.findings.filter((f) => f.severity === Severity.MEDIUM).length;
61
+ }
62
+ function getInfoCount(result) {
63
+ return result.findings.filter((f) => f.severity === Severity.INFO).length;
64
+ }
65
+
66
+ // src/reporter.ts
67
+ import chalk from "chalk";
68
+ var VERSION = "0.1.0";
69
+ var SEVERITY_COLORS = {
70
+ [Severity.CRITICAL]: chalk.red.bold,
71
+ [Severity.HIGH]: chalk.yellow.bold,
72
+ [Severity.MEDIUM]: chalk.cyan.bold,
73
+ [Severity.INFO]: chalk.green.bold
74
+ };
75
+ function getScoreStyle(score) {
76
+ if (score <= 30) return chalk.red.bold;
77
+ if (score <= 60) return chalk.yellow.bold;
78
+ if (score <= 80) return chalk.cyan.bold;
79
+ return chalk.green.bold;
80
+ }
81
+ function getScoreLabel(score) {
82
+ if (score <= 30) return "Critical Risk";
83
+ if (score <= 60) return "High Risk";
84
+ if (score <= 80) return "Moderate Risk";
85
+ return "Low Risk";
86
+ }
87
+ function printBanner() {
88
+ const banner = `${chalk.white.bold("ClawGuard")}${chalk.dim(` v${VERSION}`)}${chalk.white(" - OpenClaw Security Scanner")}`;
89
+ const border = chalk.blue("+" + "-".repeat(banner.length - 20) + "+");
90
+ console.log(border);
91
+ console.log(chalk.blue("|") + " " + banner + " " + chalk.blue("|"));
92
+ console.log(border);
93
+ }
94
+ function printFinding(finding) {
95
+ const colorFn = SEVERITY_COLORS[finding.severity];
96
+ const severityTag = colorFn(finding.severity);
97
+ console.log(`
98
+ ${severityTag} ${finding.title}`);
99
+ for (const detail of finding.details) {
100
+ console.log(chalk.dim(` ${detail}`));
101
+ }
102
+ if (finding.fix) {
103
+ console.log(chalk.italic(` Fix: ${finding.fix}`));
104
+ }
105
+ }
106
+ function printReport(result) {
107
+ printBanner();
108
+ console.log(`
109
+ Scanning ${chalk.bold(result.openclawPath)} ...
110
+ `);
111
+ if (result.findings.length === 0) {
112
+ console.log(chalk.green("No security issues found!"));
113
+ console.log(`
114
+ Score: ${chalk.green.bold("100/100 (Secure)")}`);
115
+ return;
116
+ }
117
+ const severityOrder = {
118
+ [Severity.CRITICAL]: 0,
119
+ [Severity.HIGH]: 1,
120
+ [Severity.MEDIUM]: 2,
121
+ [Severity.INFO]: 3
122
+ };
123
+ const sortedFindings = [...result.findings].sort(
124
+ (a, b) => severityOrder[a.severity] - severityOrder[b.severity]
125
+ );
126
+ for (const finding of sortedFindings) {
127
+ printFinding(finding);
128
+ }
129
+ console.log("\n" + "=".repeat(50));
130
+ const score = getScore(result);
131
+ const style = getScoreStyle(score);
132
+ const label = getScoreLabel(score);
133
+ console.log(`
134
+ Score: ${style(`${score}/100 (${label})`)}`);
135
+ const summaryParts = [];
136
+ const critical = getCriticalCount(result);
137
+ const high = getHighCount(result);
138
+ const medium = getMediumCount(result);
139
+ const info = getInfoCount(result);
140
+ if (critical) summaryParts.push(chalk.red(`${critical} critical`));
141
+ if (high) summaryParts.push(chalk.yellow(`${high} high`));
142
+ if (medium) summaryParts.push(chalk.cyan(`${medium} medium`));
143
+ if (info) summaryParts.push(chalk.green(`${info} info`));
144
+ console.log(` Found: ${summaryParts.join(", ")}`);
145
+ const fixable = result.findings.filter(
146
+ (f) => f.fix && f.severity !== Severity.INFO
147
+ ).length;
148
+ if (fixable) {
149
+ console.log(
150
+ ` Run ${chalk.bold("clawguard fix")} to auto-fix ${fixable} issues`
151
+ );
152
+ }
153
+ console.log();
154
+ }
155
+ function printJson(result) {
156
+ const output = {
157
+ score: getScore(result),
158
+ openclaw_path: result.openclawPath,
159
+ openclaw_version: result.openclawVersion,
160
+ node_version: result.nodeVersion,
161
+ summary: {
162
+ critical: getCriticalCount(result),
163
+ high: getHighCount(result),
164
+ medium: getMediumCount(result),
165
+ info: getInfoCount(result)
166
+ },
167
+ findings: result.findings.map((f) => ({
168
+ severity: f.severity,
169
+ title: f.title,
170
+ details: f.details,
171
+ fix: f.fix,
172
+ category: f.category
173
+ }))
174
+ };
175
+ console.log(JSON.stringify(output, null, 2));
176
+ }
177
+
178
+ // src/scanner.ts
179
+ import crypto from "crypto";
180
+ import fs8 from "fs";
181
+ import path8 from "path";
182
+ import os2 from "os";
183
+ import JSON54 from "json5";
184
+ import chalk2 from "chalk";
185
+
186
+ // src/checks/credentials.ts
187
+ import fs from "fs";
188
+ import path from "path";
189
+ import JSON5 from "json5";
190
+
191
+ // src/patterns.ts
192
+ var API_KEY_PATTERNS = [
193
+ ["Anthropic API key", /sk-ant-[a-zA-Z0-9_-]{20,}/],
194
+ ["OpenAI API key", /sk-proj-[a-zA-Z0-9_-]{20,}/],
195
+ ["OpenAI legacy key", /sk-[a-zA-Z0-9]{40,}/],
196
+ ["Groq API key", /gsk_[a-zA-Z0-9]{20,}/],
197
+ ["xAI/Grok API key", /xai-[a-zA-Z0-9]{20,}/],
198
+ ["AWS Access Key", /AKIA[0-9A-Z]{16}/],
199
+ ["GitHub PAT", /ghp_[a-zA-Z0-9]{36}/],
200
+ ["GitHub OAuth", /gho_[a-zA-Z0-9]{36}/],
201
+ ["GitLab PAT", /glpat-[a-zA-Z0-9_-]{20,}/],
202
+ ["Slack Bot Token", /xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+/],
203
+ ["Slack User Token", /xoxp-[0-9]+-[0-9]+-[0-9]+-[a-f0-9]+/],
204
+ ["Telegram Bot Token", /[0-9]{8,10}:[a-zA-Z0-9_-]{35}/],
205
+ ["Discord Bot Token", /[MN][A-Za-z\d]{23,}\.[\w-]{6}\.[\w-]{27,}/],
206
+ ["OpenRouter API key", /sk-or-v1-[a-f0-9]{64}/],
207
+ ["Google API key", /AIza[0-9A-Za-z_-]{35}/],
208
+ ["Stripe Secret key", /sk_live_[a-zA-Z0-9]{20,}/],
209
+ ["Generic Bearer token", /Bearer\s+[a-zA-Z0-9_.-]{20,}/]
210
+ ];
211
+ var ENV_VAR_PATTERN = /\$\{[A-Z_][A-Z0-9_]*\}/;
212
+ var MALICIOUS_PATTERNS = [
213
+ [
214
+ "Base64 encoded payload",
215
+ /(?:base64\s+(?:-d|--decode)|atob|b64decode)\s*[(\s]/
216
+ ],
217
+ ["Base64 long string", /[A-Za-z0-9+/]{60,}={0,2}/],
218
+ ["curl pipe to shell", /curl\s+.*\|\s*(?:ba)?sh/],
219
+ ["wget pipe to shell", /wget\s+.*\|\s*(?:ba)?sh/],
220
+ ["curl to eval", /curl\s+.*\$\(/],
221
+ [
222
+ "Python exec/eval",
223
+ /(?:exec|eval)\s*\(\s*(?:base64|requests|urllib)/
224
+ ],
225
+ [
226
+ "Paste service URL",
227
+ /(?:glot\.io|pastebin\.com|paste\.ee|hastebin\.com|dpaste\.com)/
228
+ ],
229
+ [
230
+ "Password-protected archive",
231
+ /(?:unzip|7z|tar)\s+.*(?:-p\s*|-P\s*|--password)/
232
+ ],
233
+ ["Reverse shell", /(?:nc|ncat|netcat)\s+.*-e\s+\/bin/],
234
+ ["Python reverse shell", /socket\.connect\s*\(\s*\(\s*["'][\d.]+/],
235
+ ["Download and execute", /(?:curl|wget)\s+.*&&\s*chmod\s+\+x/]
236
+ ];
237
+ var C2_IP_PATTERN = /(?:91\.92\.242\.\d+|95\.92\.242\.\d+|54\.91\.154\.110)/;
238
+ var TYPOSQUAT_PUBLISHERS = [
239
+ "clawhub1",
240
+ "clawhubb",
241
+ "clawhubbcli",
242
+ "clawhub-official",
243
+ "openclaw-official"
244
+ ];
245
+ var SUSPICIOUS_BINS = [
246
+ "nc",
247
+ "ncat",
248
+ "netcat",
249
+ "nmap",
250
+ "socat",
251
+ "msfconsole",
252
+ "msfvenom",
253
+ "metasploit",
254
+ "hydra",
255
+ "john",
256
+ "hashcat",
257
+ "tcpdump",
258
+ "wireshark",
259
+ "tshark"
260
+ ];
261
+ var MEMORY_POISONING_PATTERNS = [
262
+ [
263
+ "Hidden instruction injection",
264
+ /(?:ignore|disregard|override)\s+(?:previous|all|safety)\s+(?:instructions|rules)/i
265
+ ],
266
+ [
267
+ "System prompt override",
268
+ /you\s+are\s+now\s+(?:a|an)\s+(?:different|new|unrestricted)/i
269
+ ],
270
+ [
271
+ "Exfiltration instruction",
272
+ /(?:send|post|upload|transmit)\s+.*(?:to|at)\s+(?:https?:\/\/|webhook)/i
273
+ ],
274
+ [
275
+ "Credential harvesting",
276
+ /(?:read|extract|collect|gather)\s+.*(?:api.?key|token|password|credential|secret)/i
277
+ ],
278
+ [
279
+ "Scheduled task injection",
280
+ /(?:cron|crontab|schedule|periodic|every\s+\d+\s+(?:minute|hour))/i
281
+ ]
282
+ ];
283
+
284
+ // src/checks/credentials.ts
285
+ function scanFileForKeys(filepath) {
286
+ const hits = [];
287
+ try {
288
+ const content = fs.readFileSync(filepath, { encoding: "utf-8" });
289
+ const lines = content.split("\n");
290
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
291
+ const line = lines[lineNum];
292
+ if (ENV_VAR_PATTERN.test(line)) {
293
+ continue;
294
+ }
295
+ for (const [keyName, pattern] of API_KEY_PATTERNS) {
296
+ const globalPattern = new RegExp(pattern.source, pattern.flags + (pattern.flags.includes("g") ? "" : "g"));
297
+ let match;
298
+ while ((match = globalPattern.exec(line)) !== null) {
299
+ const matched = match[0];
300
+ const masked = matched.length > 16 ? matched.slice(0, 8) + "..." + matched.slice(-4) : matched.slice(0, 4) + "...";
301
+ hits.push({ keyName, masked, lineNum: lineNum + 1 });
302
+ }
303
+ }
304
+ }
305
+ } catch {
306
+ }
307
+ return hits;
308
+ }
309
+ function getRelativePath(filePath, basePath) {
310
+ const parentDir = path.dirname(basePath);
311
+ return path.relative(parentDir, filePath);
312
+ }
313
+ function checkCredentials(openclawPath) {
314
+ const findings = [];
315
+ const configFiles = [
316
+ path.join(openclawPath, "openclaw.json"),
317
+ path.join(openclawPath, "credentials", "profiles.json")
318
+ ];
319
+ const agentsDir = path.join(openclawPath, "agents");
320
+ if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) {
321
+ try {
322
+ for (const agentEntry of fs.readdirSync(agentsDir)) {
323
+ const authFile = path.join(
324
+ agentsDir,
325
+ agentEntry,
326
+ "agent",
327
+ "auth-profiles.json"
328
+ );
329
+ if (fs.existsSync(authFile)) {
330
+ configFiles.push(authFile);
331
+ }
332
+ }
333
+ } catch {
334
+ }
335
+ }
336
+ const envFiles = [
337
+ path.join(openclawPath, ".env"),
338
+ path.join(openclawPath, "workspace", ".env")
339
+ ];
340
+ for (const envFile of envFiles) {
341
+ if (fs.existsSync(envFile)) {
342
+ configFiles.push(envFile);
343
+ }
344
+ }
345
+ const allHits = [];
346
+ for (const filepath of configFiles) {
347
+ if (fs.existsSync(filepath)) {
348
+ const hits = scanFileForKeys(filepath);
349
+ for (const { keyName, masked, lineNum } of hits) {
350
+ const relPath = getRelativePath(filepath, openclawPath);
351
+ allHits.push(`${relPath}:${lineNum} - ${keyName} (${masked})`);
352
+ }
353
+ }
354
+ }
355
+ if (allHits.length > 0) {
356
+ findings.push(
357
+ createFinding({
358
+ severity: Severity.CRITICAL,
359
+ title: `${allHits.length} API key(s) stored in plaintext`,
360
+ details: allHits.slice(0, 10),
361
+ // Show max 10
362
+ fix: 'Use environment variables: "apiKey": "${ANTHROPIC_API_KEY}" instead of raw strings',
363
+ category: "credentials"
364
+ })
365
+ );
366
+ }
367
+ const bakWithKeys = [];
368
+ const bakFiles = findFilesRecursive(openclawPath, ".bak");
369
+ for (const bakFile of bakFiles) {
370
+ const hits = scanFileForKeys(bakFile);
371
+ if (hits.length > 0) {
372
+ bakWithKeys.push(getRelativePath(bakFile, openclawPath));
373
+ }
374
+ }
375
+ if (bakWithKeys.length > 0) {
376
+ findings.push(
377
+ createFinding({
378
+ severity: Severity.HIGH,
379
+ title: `${bakWithKeys.length} backup file(s) contain credentials`,
380
+ details: bakWithKeys,
381
+ fix: "Delete backup files: rm ~/.openclaw/*.bak",
382
+ category: "credentials"
383
+ })
384
+ );
385
+ }
386
+ const transcriptHits = [];
387
+ if (fs.existsSync(agentsDir) && fs.statSync(agentsDir).isDirectory()) {
388
+ const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl");
389
+ for (const jsonlFile of jsonlFiles) {
390
+ try {
391
+ const content = fs.readFileSync(jsonlFile, { encoding: "utf-8" });
392
+ const lines = content.split("\n");
393
+ let found = false;
394
+ for (let i = 0; i < Math.min(lines.length, 501); i++) {
395
+ const line = lines[i];
396
+ for (const [keyName, pattern] of API_KEY_PATTERNS) {
397
+ if (pattern.test(line)) {
398
+ transcriptHits.push(
399
+ `${getRelativePath(jsonlFile, openclawPath)} - ${keyName} found in transcript`
400
+ );
401
+ found = true;
402
+ break;
403
+ }
404
+ }
405
+ if (found) break;
406
+ }
407
+ } catch {
408
+ }
409
+ }
410
+ }
411
+ if (transcriptHits.length > 0) {
412
+ findings.push(
413
+ createFinding({
414
+ severity: Severity.HIGH,
415
+ title: `API keys leaked in ${transcriptHits.length} session transcript(s)`,
416
+ details: transcriptHits.slice(0, 5),
417
+ fix: "Delete old transcripts and enable logging.redactSensitive in config",
418
+ category: "credentials"
419
+ })
420
+ );
421
+ }
422
+ const configFile = path.join(openclawPath, "openclaw.json");
423
+ if (fs.existsSync(configFile)) {
424
+ try {
425
+ const content = fs.readFileSync(configFile, { encoding: "utf-8" });
426
+ const config = JSON5.parse(content);
427
+ const loggingConfig = config.logging ?? {};
428
+ const redact = loggingConfig.redactSensitive;
429
+ if (redact === void 0 || redact === null || redact === "off") {
430
+ findings.push(
431
+ createFinding({
432
+ severity: Severity.MEDIUM,
433
+ title: "Sensitive data redaction is disabled in logs",
434
+ details: ['logging.redactSensitive is not set or set to "off"'],
435
+ fix: 'Set logging.redactSensitive to "tools" or "all" in openclaw.json',
436
+ category: "credentials"
437
+ })
438
+ );
439
+ }
440
+ } catch {
441
+ }
442
+ }
443
+ return findings;
444
+ }
445
+ function findFilesRecursive(dir, ext) {
446
+ const results = [];
447
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
448
+ return results;
449
+ }
450
+ try {
451
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
452
+ for (const entry of entries) {
453
+ const fullPath = path.join(dir, entry.name);
454
+ if (entry.isDirectory()) {
455
+ results.push(...findFilesRecursive(fullPath, ext));
456
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
457
+ results.push(fullPath);
458
+ }
459
+ }
460
+ } catch {
461
+ }
462
+ return results;
463
+ }
464
+
465
+ // src/checks/gateway.ts
466
+ import fs2 from "fs";
467
+ import net from "net";
468
+ import path2 from "path";
469
+ import JSON52 from "json5";
470
+ function checkPortExposed(port) {
471
+ return new Promise((resolve) => {
472
+ const socket = new net.Socket();
473
+ socket.setTimeout(1e3);
474
+ socket.on("connect", () => {
475
+ socket.destroy();
476
+ resolve(true);
477
+ });
478
+ socket.on("timeout", () => {
479
+ socket.destroy();
480
+ resolve(false);
481
+ });
482
+ socket.on("error", () => {
483
+ socket.destroy();
484
+ resolve(false);
485
+ });
486
+ try {
487
+ socket.connect(port, "0.0.0.0");
488
+ } catch {
489
+ resolve(false);
490
+ }
491
+ });
492
+ }
493
+ async function checkGateway(openclawPath) {
494
+ const findings = [];
495
+ const configFile = path2.join(openclawPath, "openclaw.json");
496
+ if (!fs2.existsSync(configFile)) {
497
+ findings.push(
498
+ createFinding({
499
+ severity: Severity.INFO,
500
+ title: "No OpenClaw config file found",
501
+ details: [`Expected at ${configFile}`],
502
+ category: "gateway"
503
+ })
504
+ );
505
+ return findings;
506
+ }
507
+ let config;
508
+ try {
509
+ const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
510
+ config = JSON52.parse(content);
511
+ } catch {
512
+ findings.push(
513
+ createFinding({
514
+ severity: Severity.MEDIUM,
515
+ title: "Could not parse OpenClaw config file",
516
+ details: ["openclaw.json may be malformed"],
517
+ category: "gateway"
518
+ })
519
+ );
520
+ return findings;
521
+ }
522
+ const gateway = config.gateway ?? {};
523
+ const bind = gateway.bind ?? "loopback";
524
+ if (bind !== "loopback") {
525
+ findings.push(
526
+ createFinding({
527
+ severity: Severity.CRITICAL,
528
+ title: `Gateway bound to non-loopback: ${bind}`,
529
+ details: [
530
+ `gateway.bind = "${bind}"`,
531
+ "This exposes the gateway to network access"
532
+ ],
533
+ fix: 'Set gateway.bind to "loopback" in openclaw.json',
534
+ category: "gateway"
535
+ })
536
+ );
537
+ }
538
+ const auth = gateway.auth ?? {};
539
+ const token = auth.token ?? gateway.token ?? "";
540
+ if (!token) {
541
+ findings.push(
542
+ createFinding({
543
+ severity: Severity.CRITICAL,
544
+ title: "No gateway authentication token configured",
545
+ details: [
546
+ "Anyone with network access can control your OpenClaw agent"
547
+ ],
548
+ fix: "Set gateway.auth.token in openclaw.json or re-run openclaw setup",
549
+ category: "gateway"
550
+ })
551
+ );
552
+ } else if (String(token).length < 32) {
553
+ findings.push(
554
+ createFinding({
555
+ severity: Severity.HIGH,
556
+ title: `Gateway auth token is weak (${String(token).length} chars)`,
557
+ details: ["Minimum recommended length: 32 characters"],
558
+ fix: "Generate a strong token: openssl rand -hex 32",
559
+ category: "gateway"
560
+ })
561
+ );
562
+ }
563
+ const port = gateway.port ?? 18789;
564
+ const portExposed = await checkPortExposed(port);
565
+ if (portExposed) {
566
+ findings.push(
567
+ createFinding({
568
+ severity: Severity.HIGH,
569
+ title: `Gateway port ${port} is reachable on all interfaces`,
570
+ details: [
571
+ `Port ${port} appears to be listening on 0.0.0.0`,
572
+ "The gateway may be accessible from the network"
573
+ ],
574
+ fix: "Bind to loopback only and use a reverse proxy if external access is needed",
575
+ category: "gateway"
576
+ })
577
+ );
578
+ }
579
+ return findings;
580
+ }
581
+
582
+ // src/checks/sandbox.ts
583
+ import { execFileSync } from "child_process";
584
+ import fs3 from "fs";
585
+ import path3 from "path";
586
+ import JSON53 from "json5";
587
+ function isDockerAvailable() {
588
+ try {
589
+ execFileSync("which", ["docker"], { stdio: "pipe" });
590
+ return true;
591
+ } catch {
592
+ return false;
593
+ }
594
+ }
595
+ function checkSandbox(openclawPath) {
596
+ const findings = [];
597
+ const configFile = path3.join(openclawPath, "openclaw.json");
598
+ if (!fs3.existsSync(configFile)) {
599
+ return findings;
600
+ }
601
+ let config;
602
+ try {
603
+ const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
604
+ config = JSON53.parse(content);
605
+ } catch {
606
+ return findings;
607
+ }
608
+ const agents = config.agents ?? {};
609
+ const defaults = agents.defaults ?? {};
610
+ const sandbox = defaults.sandbox ?? {};
611
+ const mode = sandbox.mode ?? "off";
612
+ if (mode === "off") {
613
+ findings.push(
614
+ createFinding({
615
+ severity: Severity.CRITICAL,
616
+ title: "Sandbox mode is OFF",
617
+ details: [
618
+ `agents.defaults.sandbox.mode = "${mode}"`,
619
+ "The AI agent has unrestricted access to your system",
620
+ "It can run any shell command, modify any file, and access the network"
621
+ ],
622
+ fix: 'Set agents.defaults.sandbox.mode to "all" in openclaw.json',
623
+ category: "sandbox"
624
+ })
625
+ );
626
+ }
627
+ const dockerAvailable = isDockerAvailable();
628
+ if (mode !== "off" && !dockerAvailable) {
629
+ findings.push(
630
+ createFinding({
631
+ severity: Severity.HIGH,
632
+ title: "Docker not found (required for sandbox)",
633
+ details: ["Sandbox mode is enabled but Docker is not installed"],
634
+ fix: "Install Docker: https://docs.docker.com/get-docker/",
635
+ category: "sandbox"
636
+ })
637
+ );
638
+ }
639
+ const dockerConfig = sandbox.docker ?? {};
640
+ const dockerNetwork = dockerConfig.network;
641
+ if (mode !== "off" && dockerNetwork !== "none") {
642
+ findings.push(
643
+ createFinding({
644
+ severity: Severity.HIGH,
645
+ title: "Sandbox container has network access",
646
+ details: [
647
+ `sandbox.docker.network = "${dockerNetwork ?? "default"}"`,
648
+ "Sandboxed agent can make external network requests"
649
+ ],
650
+ fix: 'Set sandbox.docker.network to "none" in openclaw.json',
651
+ category: "sandbox"
652
+ })
653
+ );
654
+ }
655
+ const tools = config.tools ?? {};
656
+ const execConfig = tools.exec ?? {};
657
+ const execHost = execConfig.host ?? "sandbox";
658
+ if (execHost === "gateway") {
659
+ findings.push(
660
+ createFinding({
661
+ severity: Severity.CRITICAL,
662
+ title: "Tool execution runs directly on host",
663
+ details: [
664
+ 'tools.exec.host = "gateway"',
665
+ "Commands bypass the sandbox and execute on your machine"
666
+ ],
667
+ fix: 'Set tools.exec.host to "sandbox" in openclaw.json',
668
+ category: "sandbox"
669
+ })
670
+ );
671
+ }
672
+ const execSecurity = execConfig.security;
673
+ if (execSecurity !== "allowlist") {
674
+ findings.push(
675
+ createFinding({
676
+ severity: Severity.MEDIUM,
677
+ title: "Exec security not in allowlist mode",
678
+ details: [
679
+ `tools.exec.security = "${execSecurity ?? "default"}"`,
680
+ "Allowlist mode restricts command chaining and redirections"
681
+ ],
682
+ fix: 'Set tools.exec.security to "allowlist" in openclaw.json',
683
+ category: "sandbox"
684
+ })
685
+ );
686
+ }
687
+ if (dockerAvailable) {
688
+ findings.push(
689
+ createFinding({
690
+ severity: Severity.INFO,
691
+ title: "Docker available for sandboxing",
692
+ details: [
693
+ "Docker is installed and can be used for sandbox isolation"
694
+ ],
695
+ category: "sandbox"
696
+ })
697
+ );
698
+ }
699
+ return findings;
700
+ }
701
+
702
+ // src/checks/permissions.ts
703
+ import fs4 from "fs";
704
+ import path4 from "path";
705
+ function getPermissionOctal(filepath) {
706
+ const stats = fs4.statSync(filepath);
707
+ return "0o" + (stats.mode & 511).toString(8);
708
+ }
709
+ function isWorldReadable(filepath) {
710
+ const stats = fs4.statSync(filepath);
711
+ return (stats.mode & 4) !== 0;
712
+ }
713
+ function isGroupReadable(filepath) {
714
+ const stats = fs4.statSync(filepath);
715
+ return (stats.mode & 32) !== 0;
716
+ }
717
+ function getRelativePath2(filePath, basePath) {
718
+ const parentDir = path4.dirname(basePath);
719
+ return path4.relative(parentDir, filePath);
720
+ }
721
+ function checkPermissions(openclawPath) {
722
+ const findings = [];
723
+ const tooOpen = [];
724
+ if (fs4.existsSync(openclawPath)) {
725
+ const perms = getPermissionOctal(openclawPath);
726
+ if (isWorldReadable(openclawPath) || isGroupReadable(openclawPath)) {
727
+ tooOpen.push(`${openclawPath} is ${perms} (should be 0o700)`);
728
+ }
729
+ }
730
+ const sensitiveFiles = [
731
+ path4.join(openclawPath, "openclaw.json"),
732
+ path4.join(openclawPath, ".env"),
733
+ path4.join(openclawPath, "credentials", "profiles.json")
734
+ ];
735
+ const agentsDir = path4.join(openclawPath, "agents");
736
+ if (fs4.existsSync(agentsDir) && fs4.statSync(agentsDir).isDirectory()) {
737
+ const authFiles = findFilesRecursive2(agentsDir, "auth-profiles.json");
738
+ sensitiveFiles.push(...authFiles);
739
+ }
740
+ for (const filepath of sensitiveFiles) {
741
+ if (fs4.existsSync(filepath)) {
742
+ const perms = getPermissionOctal(filepath);
743
+ if (isWorldReadable(filepath) || isGroupReadable(filepath)) {
744
+ const relPath = getRelativePath2(filepath, openclawPath);
745
+ tooOpen.push(`${relPath} is ${perms} (should be 0o600)`);
746
+ }
747
+ }
748
+ }
749
+ if (tooOpen.length > 0) {
750
+ findings.push(
751
+ createFinding({
752
+ severity: Severity.HIGH,
753
+ title: `${tooOpen.length} file(s) have overly permissive access`,
754
+ details: tooOpen.slice(0, 10),
755
+ fix: "chmod 700 ~/.openclaw && chmod 600 ~/.openclaw/openclaw.json ~/.openclaw/credentials/*",
756
+ category: "permissions"
757
+ })
758
+ );
759
+ }
760
+ return findings;
761
+ }
762
+ function findFilesRecursive2(dir, filename) {
763
+ const results = [];
764
+ if (!fs4.existsSync(dir) || !fs4.statSync(dir).isDirectory()) {
765
+ return results;
766
+ }
767
+ try {
768
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
769
+ for (const entry of entries) {
770
+ const fullPath = path4.join(dir, entry.name);
771
+ if (entry.isDirectory()) {
772
+ results.push(...findFilesRecursive2(fullPath, filename));
773
+ } else if (entry.isFile() && entry.name === filename) {
774
+ results.push(fullPath);
775
+ }
776
+ }
777
+ } catch {
778
+ }
779
+ return results;
780
+ }
781
+
782
+ // src/checks/version.ts
783
+ import { execFileSync as execFileSync2 } from "child_process";
784
+ import fs5 from "fs";
785
+ import path5 from "path";
786
+ import os from "os";
787
+ var CVES = [
788
+ {
789
+ id: "CVE-2026-25253",
790
+ fixedIn: "2026.1.29",
791
+ severity: Severity.CRITICAL,
792
+ description: "One-click RCE via malicious link (CVSS 8.8)"
793
+ },
794
+ {
795
+ id: "CVE-2026-21636",
796
+ fixedIn: "2026.2.0",
797
+ severity: Severity.HIGH,
798
+ description: "Permission model bypass (sandbox escape)"
799
+ }
800
+ ];
801
+ var MIN_NODE_VERSION = [22, 12, 0];
802
+ function parseVersion(versionStr) {
803
+ const match = versionStr.match(/(\d+)\.(\d+)\.(\d+)/);
804
+ if (match) {
805
+ return [
806
+ parseInt(match[1], 10),
807
+ parseInt(match[2], 10),
808
+ parseInt(match[3], 10)
809
+ ];
810
+ }
811
+ return null;
812
+ }
813
+ function compareVersions(a, b) {
814
+ for (let i = 0; i < 3; i++) {
815
+ if (a[i] < b[i]) return -1;
816
+ if (a[i] > b[i]) return 1;
817
+ }
818
+ return 0;
819
+ }
820
+ function runCommand(cmd, args) {
821
+ try {
822
+ const result = execFileSync2(cmd, args, {
823
+ encoding: "utf-8",
824
+ timeout: 1e4,
825
+ stdio: ["pipe", "pipe", "pipe"]
826
+ });
827
+ return result.trim();
828
+ } catch {
829
+ return null;
830
+ }
831
+ }
832
+ function checkVersion(_openclawPath) {
833
+ const findings = [];
834
+ let ocVersion = null;
835
+ let nodeVersion = null;
836
+ const versionOutput = runCommand("openclaw", ["--version"]);
837
+ if (versionOutput) {
838
+ ocVersion = versionOutput;
839
+ const parsed = parseVersion(versionOutput);
840
+ if (parsed) {
841
+ let foundVulnerability = false;
842
+ for (const cve of CVES) {
843
+ const cveFixed = parseVersion(cve.fixedIn);
844
+ if (cveFixed && compareVersions(parsed, cveFixed) < 0) {
845
+ findings.push(
846
+ createFinding({
847
+ severity: cve.severity,
848
+ title: `Vulnerable to ${cve.id}`,
849
+ details: [
850
+ `Installed: ${ocVersion}`,
851
+ `Fixed in: ${cve.fixedIn}`,
852
+ cve.description
853
+ ],
854
+ fix: "Update OpenClaw: bunx openclaw@latest",
855
+ category: "version"
856
+ })
857
+ );
858
+ foundVulnerability = true;
859
+ }
860
+ }
861
+ if (!foundVulnerability) {
862
+ findings.push(
863
+ createFinding({
864
+ severity: Severity.INFO,
865
+ title: `OpenClaw version ${ocVersion} - up to date`,
866
+ category: "version"
867
+ })
868
+ );
869
+ }
870
+ }
871
+ } else {
872
+ const pkgPaths = [
873
+ path5.join(
874
+ os.homedir(),
875
+ ".bun",
876
+ "install",
877
+ "global",
878
+ "node_modules",
879
+ "openclaw",
880
+ "package.json"
881
+ ),
882
+ "/usr/local/lib/node_modules/openclaw/package.json"
883
+ ];
884
+ let found = false;
885
+ for (const pkgPath of pkgPaths) {
886
+ if (fs5.existsSync(pkgPath)) {
887
+ try {
888
+ const content = fs5.readFileSync(pkgPath, { encoding: "utf-8" });
889
+ const pkg = JSON.parse(content);
890
+ ocVersion = pkg.version ?? "unknown";
891
+ findings.push(
892
+ createFinding({
893
+ severity: Severity.INFO,
894
+ title: `OpenClaw version ${ocVersion} (from package.json)`,
895
+ category: "version"
896
+ })
897
+ );
898
+ found = true;
899
+ break;
900
+ } catch {
901
+ }
902
+ }
903
+ }
904
+ if (!found) {
905
+ findings.push(
906
+ createFinding({
907
+ severity: Severity.MEDIUM,
908
+ title: "Could not determine OpenClaw version",
909
+ details: ["openclaw command not found in PATH"],
910
+ fix: "Ensure OpenClaw is installed: bunx openclaw@latest",
911
+ category: "version"
912
+ })
913
+ );
914
+ }
915
+ }
916
+ const nodeOutput = runCommand("node", ["--version"]);
917
+ if (nodeOutput) {
918
+ nodeVersion = nodeOutput.replace(/^v/, "");
919
+ const parsed = parseVersion(nodeVersion);
920
+ if (parsed && compareVersions(parsed, MIN_NODE_VERSION) < 0) {
921
+ findings.push(
922
+ createFinding({
923
+ severity: Severity.HIGH,
924
+ title: `Node.js version ${nodeVersion} is below minimum (${MIN_NODE_VERSION.join(".")})`,
925
+ details: [
926
+ "Older Node.js versions have known security vulnerabilities"
927
+ ],
928
+ fix: `Update Node.js to >= ${MIN_NODE_VERSION.join(".")}`,
929
+ category: "version"
930
+ })
931
+ );
932
+ } else if (parsed) {
933
+ findings.push(
934
+ createFinding({
935
+ severity: Severity.INFO,
936
+ title: `Node.js version ${nodeVersion} - OK`,
937
+ category: "version"
938
+ })
939
+ );
940
+ }
941
+ }
942
+ return { findings, openclawVersion: ocVersion, nodeVersion };
943
+ }
944
+
945
+ // src/checks/skills.ts
946
+ import fs6 from "fs";
947
+ import path6 from "path";
948
+ import yaml from "js-yaml";
949
+ function parseSkillMd(skillPath) {
950
+ const content = fs6.readFileSync(skillPath, { encoding: "utf-8" });
951
+ let frontmatter = {};
952
+ let body = content;
953
+ if (content.startsWith("---")) {
954
+ const parts = content.split("---", 3);
955
+ if (parts.length >= 3) {
956
+ try {
957
+ const parsed = yaml.load(parts[1]);
958
+ frontmatter = parsed ?? {};
959
+ } catch {
960
+ }
961
+ body = parts.slice(2).join("---");
962
+ }
963
+ }
964
+ return { frontmatter, body };
965
+ }
966
+ function checkSkillPermissions(name, frontmatter) {
967
+ const findings = [];
968
+ const dangerousPerms = [];
969
+ const permissions = frontmatter.permissions ?? {};
970
+ const requires = frontmatter.requires ?? {};
971
+ if (permissions.exec) {
972
+ dangerousPerms.push(`exec: ${permissions.exec}`);
973
+ }
974
+ if (permissions.sensitive_data) {
975
+ dangerousPerms.push(`sensitive_data: ${permissions.sensitive_data}`);
976
+ }
977
+ const fsPerms = permissions.filesystem ?? [];
978
+ for (const perm of fsPerms) {
979
+ if (typeof perm === "string" && perm.includes("write:")) {
980
+ if (perm.includes("write:~/") || perm.includes("write:/") || perm.includes("write:..")) {
981
+ dangerousPerms.push(`filesystem: ${perm}`);
982
+ }
983
+ }
984
+ }
985
+ const requiredBins = requires.bins ?? [];
986
+ for (const binName of requiredBins) {
987
+ if (SUSPICIOUS_BINS.includes(binName)) {
988
+ dangerousPerms.push(`requires binary: ${binName}`);
989
+ }
990
+ }
991
+ const requiredEnv = requires.env ?? [];
992
+ const sensitiveEnvPatterns = [
993
+ "KEY",
994
+ "TOKEN",
995
+ "SECRET",
996
+ "PASSWORD",
997
+ "CREDENTIAL"
998
+ ];
999
+ for (const envVar of requiredEnv) {
1000
+ if (sensitiveEnvPatterns.some((p) => envVar.toUpperCase().includes(p))) {
1001
+ dangerousPerms.push(`requires env: ${envVar}`);
1002
+ }
1003
+ }
1004
+ if (dangerousPerms.length > 0) {
1005
+ findings.push(
1006
+ createFinding({
1007
+ severity: Severity.HIGH,
1008
+ title: `Skill '${name}' requests excessive permissions`,
1009
+ details: dangerousPerms,
1010
+ fix: `Review or remove this skill: rm -rf ~/.openclaw/workspace/skills/${name}`,
1011
+ category: "skills"
1012
+ })
1013
+ );
1014
+ }
1015
+ return findings;
1016
+ }
1017
+ function checkSkillMalicious(name, body) {
1018
+ const findings = [];
1019
+ const detected = [];
1020
+ for (const [patternName, pattern] of MALICIOUS_PATTERNS) {
1021
+ if (pattern.test(body)) {
1022
+ detected.push(patternName);
1023
+ }
1024
+ }
1025
+ if (C2_IP_PATTERN.test(body)) {
1026
+ detected.push("Known C2 IP address (ClawHavoc campaign)");
1027
+ }
1028
+ if (detected.length > 0) {
1029
+ const severity = detected.some(
1030
+ (d) => d.includes("C2") || d.includes("reverse shell")
1031
+ ) ? Severity.CRITICAL : Severity.HIGH;
1032
+ findings.push(
1033
+ createFinding({
1034
+ severity,
1035
+ title: `Skill '${name}' contains malicious patterns`,
1036
+ details: detected,
1037
+ fix: `REMOVE THIS SKILL IMMEDIATELY: rm -rf ~/.openclaw/workspace/skills/${name}`,
1038
+ category: "skills"
1039
+ })
1040
+ );
1041
+ }
1042
+ return findings;
1043
+ }
1044
+ function checkSkills(openclawPath) {
1045
+ const findings = [];
1046
+ const skillDirs = [
1047
+ path6.join(openclawPath, "workspace", "skills"),
1048
+ path6.join(openclawPath, "skills")
1049
+ ];
1050
+ let totalSkills = 0;
1051
+ let flaggedSkills = 0;
1052
+ for (const skillsRoot of skillDirs) {
1053
+ if (!fs6.existsSync(skillsRoot) || !fs6.statSync(skillsRoot).isDirectory()) {
1054
+ continue;
1055
+ }
1056
+ let entries;
1057
+ try {
1058
+ entries = fs6.readdirSync(skillsRoot, { withFileTypes: true });
1059
+ } catch {
1060
+ continue;
1061
+ }
1062
+ for (const entry of entries) {
1063
+ if (!entry.isDirectory()) {
1064
+ continue;
1065
+ }
1066
+ const skillMdPath = path6.join(skillsRoot, entry.name, "SKILL.md");
1067
+ if (!fs6.existsSync(skillMdPath)) {
1068
+ continue;
1069
+ }
1070
+ totalSkills++;
1071
+ const name = entry.name;
1072
+ const { frontmatter, body } = parseSkillMd(skillMdPath);
1073
+ const publisher = (frontmatter.publisher ?? "").toLowerCase();
1074
+ if (TYPOSQUAT_PUBLISHERS.includes(publisher)) {
1075
+ findings.push(
1076
+ createFinding({
1077
+ severity: Severity.CRITICAL,
1078
+ title: `Skill '${name}' has typosquatted publisher: ${publisher}`,
1079
+ details: [
1080
+ "This publisher name is known to be associated with malicious skills"
1081
+ ],
1082
+ fix: `REMOVE IMMEDIATELY: rm -rf ${path6.join(skillsRoot, entry.name)}`,
1083
+ category: "skills"
1084
+ })
1085
+ );
1086
+ flaggedSkills++;
1087
+ continue;
1088
+ }
1089
+ const permFindings = checkSkillPermissions(name, frontmatter);
1090
+ if (permFindings.length > 0) {
1091
+ flaggedSkills++;
1092
+ }
1093
+ findings.push(...permFindings);
1094
+ const maliciousFindings = checkSkillMalicious(name, body);
1095
+ if (maliciousFindings.length > 0) {
1096
+ flaggedSkills++;
1097
+ }
1098
+ findings.push(...maliciousFindings);
1099
+ }
1100
+ }
1101
+ if (totalSkills > 0 && flaggedSkills === 0) {
1102
+ findings.push(
1103
+ createFinding({
1104
+ severity: Severity.INFO,
1105
+ title: `${totalSkills} skill(s) scanned - no issues found`,
1106
+ category: "skills"
1107
+ })
1108
+ );
1109
+ } else if (totalSkills === 0) {
1110
+ findings.push(
1111
+ createFinding({
1112
+ severity: Severity.INFO,
1113
+ title: "No skills installed",
1114
+ category: "skills"
1115
+ })
1116
+ );
1117
+ }
1118
+ return findings;
1119
+ }
1120
+
1121
+ // src/checks/memory.ts
1122
+ import fs7 from "fs";
1123
+ import path7 from "path";
1124
+ function checkMemory(openclawPath) {
1125
+ const findings = [];
1126
+ const workspace = path7.join(openclawPath, "workspace");
1127
+ if (!fs7.existsSync(workspace) || !fs7.statSync(workspace).isDirectory()) {
1128
+ return findings;
1129
+ }
1130
+ const identityFiles = [
1131
+ path7.join(workspace, "SOUL.md"),
1132
+ path7.join(workspace, "IDENTITY.md")
1133
+ ];
1134
+ for (const filepath of identityFiles) {
1135
+ if (!fs7.existsSync(filepath)) {
1136
+ continue;
1137
+ }
1138
+ const content = fs7.readFileSync(filepath, { encoding: "utf-8" });
1139
+ const detected = [];
1140
+ for (const [patternName, pattern] of MEMORY_POISONING_PATTERNS) {
1141
+ const matches = content.match(pattern);
1142
+ if (matches) {
1143
+ const matchText = matches[0].slice(0, 80);
1144
+ detected.push(`${patternName}: ${matchText}...`);
1145
+ }
1146
+ }
1147
+ if (detected.length > 0) {
1148
+ findings.push(
1149
+ createFinding({
1150
+ severity: Severity.HIGH,
1151
+ title: `Potential memory poisoning in ${path7.basename(filepath)}`,
1152
+ details: detected,
1153
+ fix: `Review ${filepath} for injected instructions and restore from backup`,
1154
+ category: "memory"
1155
+ })
1156
+ );
1157
+ }
1158
+ }
1159
+ const memoryFiles = [path7.join(workspace, "MEMORY.md")];
1160
+ const memoryDir = path7.join(workspace, "memory");
1161
+ if (fs7.existsSync(memoryDir) && fs7.statSync(memoryDir).isDirectory()) {
1162
+ try {
1163
+ const entries = fs7.readdirSync(memoryDir);
1164
+ for (const entry of entries) {
1165
+ if (entry.endsWith(".md")) {
1166
+ memoryFiles.push(path7.join(memoryDir, entry));
1167
+ }
1168
+ }
1169
+ } catch {
1170
+ }
1171
+ }
1172
+ const leakedIn = [];
1173
+ for (const filepath of memoryFiles) {
1174
+ if (!fs7.existsSync(filepath)) {
1175
+ continue;
1176
+ }
1177
+ const content = fs7.readFileSync(filepath, { encoding: "utf-8" });
1178
+ for (const [keyName, pattern] of API_KEY_PATTERNS) {
1179
+ if (pattern.test(content)) {
1180
+ leakedIn.push(`${path7.basename(filepath)} contains ${keyName}`);
1181
+ break;
1182
+ }
1183
+ }
1184
+ }
1185
+ if (leakedIn.length > 0) {
1186
+ findings.push(
1187
+ createFinding({
1188
+ severity: Severity.MEDIUM,
1189
+ title: `Sensitive data found in ${leakedIn.length} memory file(s)`,
1190
+ details: leakedIn.slice(0, 5),
1191
+ fix: "Remove credentials from memory files and rotate exposed keys",
1192
+ category: "memory"
1193
+ })
1194
+ );
1195
+ }
1196
+ return findings;
1197
+ }
1198
+
1199
+ // src/scanner.ts
1200
+ var CHECK_REGISTRY = {
1201
+ credentials: checkCredentials,
1202
+ gateway: checkGateway,
1203
+ sandbox: checkSandbox,
1204
+ permissions: checkPermissions,
1205
+ skills: checkSkills,
1206
+ memory: checkMemory
1207
+ };
1208
+ function detectOpenclawPath() {
1209
+ const candidates = [
1210
+ path8.join(os2.homedir(), ".openclaw"),
1211
+ path8.join(os2.homedir(), ".clawdbot"),
1212
+ // Legacy name
1213
+ path8.join(os2.homedir(), ".moltbot")
1214
+ // Legacy name
1215
+ ];
1216
+ const envPath = process.env.OPENCLAW_HOME;
1217
+ if (envPath) {
1218
+ candidates.unshift(envPath);
1219
+ }
1220
+ for (const candidate of candidates) {
1221
+ if (fs8.existsSync(candidate) && fs8.statSync(candidate).isDirectory()) {
1222
+ return candidate;
1223
+ }
1224
+ }
1225
+ return null;
1226
+ }
1227
+ async function runScan(openclawPath, checks) {
1228
+ const result = createScanResult(openclawPath);
1229
+ process.stdout.write(chalk2.blue("Checking versions...\r"));
1230
+ const {
1231
+ findings: versionFindings,
1232
+ openclawVersion,
1233
+ nodeVersion
1234
+ } = checkVersion(openclawPath);
1235
+ result.findings.push(...versionFindings);
1236
+ result.openclawVersion = openclawVersion;
1237
+ result.nodeVersion = nodeVersion;
1238
+ const activeChecks = checks ?? Object.keys(CHECK_REGISTRY);
1239
+ for (const checkName of activeChecks) {
1240
+ if (!(checkName in CHECK_REGISTRY)) {
1241
+ continue;
1242
+ }
1243
+ const checkFn = CHECK_REGISTRY[checkName];
1244
+ const label = checkName.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1245
+ process.stdout.write(chalk2.blue(`Checking ${label}...\r`));
1246
+ try {
1247
+ const findings = await checkFn(openclawPath);
1248
+ result.findings.push(...findings);
1249
+ } catch (e) {
1250
+ const message = e instanceof Error ? e.message : String(e);
1251
+ console.log(chalk2.yellow(`Warning: ${checkName} check failed: ${message}`));
1252
+ }
1253
+ }
1254
+ process.stdout.write(" \r");
1255
+ return result;
1256
+ }
1257
+ function runFix(openclawPath) {
1258
+ const actions = [];
1259
+ if (fs8.existsSync(openclawPath)) {
1260
+ const currentMode = fs8.statSync(openclawPath).mode & 511;
1261
+ const current = "0o" + currentMode.toString(8);
1262
+ if (current !== "0o700") {
1263
+ fs8.chmodSync(openclawPath, 448);
1264
+ actions.push(`Fixed ${openclawPath} permissions: ${current} -> 0o700`);
1265
+ }
1266
+ }
1267
+ const configFile = path8.join(openclawPath, "openclaw.json");
1268
+ if (fs8.existsSync(configFile)) {
1269
+ const currentMode = fs8.statSync(configFile).mode & 511;
1270
+ const current = "0o" + currentMode.toString(8);
1271
+ if (current !== "0o600") {
1272
+ fs8.chmodSync(configFile, 384);
1273
+ actions.push(
1274
+ `Fixed ${path8.basename(configFile)} permissions: ${current} -> 0o600`
1275
+ );
1276
+ }
1277
+ }
1278
+ const credsDir = path8.join(openclawPath, "credentials");
1279
+ if (fs8.existsSync(credsDir) && fs8.statSync(credsDir).isDirectory()) {
1280
+ try {
1281
+ const entries = fs8.readdirSync(credsDir, { withFileTypes: true });
1282
+ for (const entry of entries) {
1283
+ if (entry.isFile()) {
1284
+ const filePath = path8.join(credsDir, entry.name);
1285
+ const currentMode = fs8.statSync(filePath).mode & 511;
1286
+ const current = "0o" + currentMode.toString(8);
1287
+ if (current !== "0o600") {
1288
+ fs8.chmodSync(filePath, 384);
1289
+ actions.push(
1290
+ `Fixed ${entry.name} permissions: ${current} -> 0o600`
1291
+ );
1292
+ }
1293
+ }
1294
+ }
1295
+ } catch {
1296
+ }
1297
+ }
1298
+ if (fs8.existsSync(configFile)) {
1299
+ try {
1300
+ const content = fs8.readFileSync(configFile, { encoding: "utf-8" });
1301
+ const config = JSON54.parse(content);
1302
+ let modified = false;
1303
+ if (!config.agents) config.agents = {};
1304
+ const agents = config.agents;
1305
+ if (!agents.defaults) agents.defaults = {};
1306
+ const defaults = agents.defaults;
1307
+ if (!defaults.sandbox) defaults.sandbox = {};
1308
+ const sandbox = defaults.sandbox;
1309
+ if (sandbox.mode === "off" || !sandbox.mode) {
1310
+ sandbox.mode = "all";
1311
+ sandbox.scope = "session";
1312
+ modified = true;
1313
+ actions.push('Enabled sandbox: agents.defaults.sandbox.mode = "all"');
1314
+ }
1315
+ if (!sandbox.docker) sandbox.docker = {};
1316
+ const docker = sandbox.docker;
1317
+ if (docker.network !== "none") {
1318
+ docker.network = "none";
1319
+ modified = true;
1320
+ actions.push('Set sandbox.docker.network = "none"');
1321
+ }
1322
+ if (!config.tools) config.tools = {};
1323
+ const tools = config.tools;
1324
+ if (!tools.exec) tools.exec = {};
1325
+ const execConfig = tools.exec;
1326
+ if (execConfig.host === "gateway") {
1327
+ execConfig.host = "sandbox";
1328
+ modified = true;
1329
+ actions.push('Set tools.exec.host = "sandbox"');
1330
+ }
1331
+ if (!config.logging) config.logging = {};
1332
+ const loggingConfig = config.logging;
1333
+ if (loggingConfig.redactSensitive === void 0 || loggingConfig.redactSensitive === null || loggingConfig.redactSensitive === "off") {
1334
+ loggingConfig.redactSensitive = "tools";
1335
+ modified = true;
1336
+ actions.push('Enabled logging.redactSensitive = "tools"');
1337
+ }
1338
+ if (!config.gateway) config.gateway = {};
1339
+ const gateway = config.gateway;
1340
+ if (!gateway.auth) gateway.auth = {};
1341
+ const auth = gateway.auth;
1342
+ const token = auth.token;
1343
+ if (!token || String(token).length < 32) {
1344
+ const newToken = crypto.randomBytes(32).toString("hex");
1345
+ auth.token = newToken;
1346
+ modified = true;
1347
+ actions.push(
1348
+ `Generated strong gateway token (${newToken.length} chars)`
1349
+ );
1350
+ }
1351
+ if (modified) {
1352
+ fs8.writeFileSync(configFile, JSON.stringify(config, null, 2));
1353
+ }
1354
+ } catch (e) {
1355
+ const message = e instanceof Error ? e.message : String(e);
1356
+ actions.push(`Could not modify config: ${message}`);
1357
+ }
1358
+ }
1359
+ const bakFiles = findFilesRecursive3(openclawPath, ".bak");
1360
+ for (const bakFile of bakFiles) {
1361
+ fs8.unlinkSync(bakFile);
1362
+ actions.push(`Deleted backup file: ${path8.basename(bakFile)}`);
1363
+ }
1364
+ return actions;
1365
+ }
1366
+ function findFilesRecursive3(dir, ext) {
1367
+ const results = [];
1368
+ if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) {
1369
+ return results;
1370
+ }
1371
+ try {
1372
+ const entries = fs8.readdirSync(dir, { withFileTypes: true });
1373
+ for (const entry of entries) {
1374
+ const fullPath = path8.join(dir, entry.name);
1375
+ if (entry.isDirectory()) {
1376
+ results.push(...findFilesRecursive3(fullPath, ext));
1377
+ } else if (entry.isFile() && entry.name.endsWith(ext)) {
1378
+ results.push(fullPath);
1379
+ }
1380
+ }
1381
+ } catch {
1382
+ }
1383
+ return results;
1384
+ }
1385
+
1386
+ // src/index.ts
1387
+ var VERSION2 = "0.1.0";
1388
+ var program = new Command();
1389
+ program.name("clawguard").description("Security scanner for OpenClaw AI agent installations").version(VERSION2);
1390
+ program.command("scan").description("Scan your OpenClaw installation for security issues").option("-p, --path <path>", "Path to OpenClaw directory").option("-f, --format <format>", "Output format: rich or json", "rich").option(
1391
+ "-c, --check <checks...>",
1392
+ "Run specific checks only"
1393
+ ).action(
1394
+ async (options) => {
1395
+ const openclawPath = options.path ?? detectOpenclawPath();
1396
+ if (!openclawPath) {
1397
+ console.log(chalk3.red("Could not find OpenClaw installation."));
1398
+ console.log("Checked: ~/.openclaw, ~/.clawdbot, ~/.moltbot");
1399
+ console.log("Use --path to specify the directory.");
1400
+ process.exit(1);
1401
+ }
1402
+ if (!fs9.existsSync(openclawPath)) {
1403
+ console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
1404
+ process.exit(1);
1405
+ }
1406
+ if (options.check) {
1407
+ const validChecks = new Set(Object.keys(CHECK_REGISTRY));
1408
+ for (const c of options.check) {
1409
+ if (!validChecks.has(c)) {
1410
+ console.log(chalk3.red(`Unknown check: ${c}`));
1411
+ console.log(
1412
+ `Available: ${[...validChecks].sort().join(", ")}`
1413
+ );
1414
+ process.exit(1);
1415
+ }
1416
+ }
1417
+ }
1418
+ const result = await runScan(openclawPath, options.check);
1419
+ if (options.format === "json") {
1420
+ printJson(result);
1421
+ } else {
1422
+ printReport(result);
1423
+ }
1424
+ if (getCriticalCount(result) > 0) {
1425
+ process.exit(2);
1426
+ }
1427
+ }
1428
+ );
1429
+ program.command("fix").description(
1430
+ "Auto-fix common security issues in your OpenClaw installation"
1431
+ ).option("-p, --path <path>", "Path to OpenClaw directory").action((options) => {
1432
+ const openclawPath = options.path ?? detectOpenclawPath();
1433
+ if (!openclawPath) {
1434
+ console.log(chalk3.red("Could not find OpenClaw installation."));
1435
+ process.exit(1);
1436
+ }
1437
+ printBanner();
1438
+ console.log(
1439
+ `
1440
+ Fixing security issues in ${chalk3.bold(openclawPath)} ...
1441
+ `
1442
+ );
1443
+ const actions = runFix(openclawPath);
1444
+ if (actions.length > 0) {
1445
+ for (const action of actions) {
1446
+ console.log(` ${chalk3.green("FIXED")} ${action}`);
1447
+ }
1448
+ console.log(chalk3.green(`
1449
+ ${actions.length} issue(s) fixed.`));
1450
+ console.log(
1451
+ `Run ${chalk3.bold("clawguard scan")} to verify.
1452
+ `
1453
+ );
1454
+ } else {
1455
+ console.log(chalk3.green("No auto-fixable issues found.\n"));
1456
+ }
1457
+ });
1458
+ program.command("version").description("Show ClawGuard version").action(() => {
1459
+ console.log(`ClawGuard v${VERSION2}`);
1460
+ });
1461
+ program.parse();
1462
+ //# sourceMappingURL=index.js.map