clawguard-cli 0.1.0 → 0.2.1
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 +769 -243
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import fs11 from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import chalk3 from "chalk";
|
|
7
7
|
|
|
@@ -65,7 +65,7 @@ function getInfoCount(result) {
|
|
|
65
65
|
|
|
66
66
|
// src/reporter.ts
|
|
67
67
|
import chalk from "chalk";
|
|
68
|
-
var VERSION = "0.
|
|
68
|
+
var VERSION = "0.2.0";
|
|
69
69
|
var SEVERITY_COLORS = {
|
|
70
70
|
[Severity.CRITICAL]: chalk.red.bold,
|
|
71
71
|
[Severity.HIGH]: chalk.yellow.bold,
|
|
@@ -177,16 +177,16 @@ function printJson(result) {
|
|
|
177
177
|
|
|
178
178
|
// src/scanner.ts
|
|
179
179
|
import crypto from "crypto";
|
|
180
|
-
import
|
|
181
|
-
import
|
|
182
|
-
import
|
|
183
|
-
import
|
|
180
|
+
import fs10 from "fs";
|
|
181
|
+
import path11 from "path";
|
|
182
|
+
import os3 from "os";
|
|
183
|
+
import JSON55 from "json5";
|
|
184
184
|
import chalk2 from "chalk";
|
|
185
185
|
|
|
186
186
|
// src/checks/credentials.ts
|
|
187
|
-
import
|
|
188
|
-
import
|
|
189
|
-
import
|
|
187
|
+
import fs2 from "fs";
|
|
188
|
+
import path2 from "path";
|
|
189
|
+
import JSON52 from "json5";
|
|
190
190
|
|
|
191
191
|
// src/patterns.ts
|
|
192
192
|
var API_KEY_PATTERNS = [
|
|
@@ -206,7 +206,8 @@ var API_KEY_PATTERNS = [
|
|
|
206
206
|
["OpenRouter API key", /sk-or-v1-[a-f0-9]{64}/],
|
|
207
207
|
["Google API key", /AIza[0-9A-Za-z_-]{35}/],
|
|
208
208
|
["Stripe Secret key", /sk_live_[a-zA-Z0-9]{20,}/],
|
|
209
|
-
["Generic Bearer token", /Bearer\s+[a-zA-Z0-9_.-]{20,}/]
|
|
209
|
+
["Generic Bearer token", /Bearer\s+[a-zA-Z0-9_.-]{20,}/],
|
|
210
|
+
["Brave Search API key", /BSA[a-zA-Z0-9]{20,}/]
|
|
210
211
|
];
|
|
211
212
|
var ENV_VAR_PATTERN = /\$\{[A-Z_][A-Z0-9_]*\}/;
|
|
212
213
|
var MALICIOUS_PATTERNS = [
|
|
@@ -258,6 +259,27 @@ var SUSPICIOUS_BINS = [
|
|
|
258
259
|
"wireshark",
|
|
259
260
|
"tshark"
|
|
260
261
|
];
|
|
262
|
+
var PROVIDER_ENV_MAP = {
|
|
263
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
264
|
+
openai: "OPENAI_API_KEY",
|
|
265
|
+
groq: "GROQ_API_KEY",
|
|
266
|
+
xai: "XAI_API_KEY",
|
|
267
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
268
|
+
google: "GOOGLE_API_KEY",
|
|
269
|
+
mistral: "MISTRAL_API_KEY",
|
|
270
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
271
|
+
cohere: "COHERE_API_KEY",
|
|
272
|
+
together: "TOGETHER_API_KEY"
|
|
273
|
+
};
|
|
274
|
+
var PROVIDER_LIMITS = {
|
|
275
|
+
groq: { tpm: 6e3, rpm: 30, note: "Groq free tier: 6K TPM, 30 RPM" },
|
|
276
|
+
openrouter: {
|
|
277
|
+
tpm: 1e5,
|
|
278
|
+
rpm: 8,
|
|
279
|
+
note: "OpenRouter free tier: ~8 RPM"
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
var OPENCLAW_SYSTEM_PROMPT_TOKENS = 12600;
|
|
261
283
|
var MEMORY_POISONING_PATTERNS = [
|
|
262
284
|
[
|
|
263
285
|
"Hidden instruction injection",
|
|
@@ -281,7 +303,12 @@ var MEMORY_POISONING_PATTERNS = [
|
|
|
281
303
|
]
|
|
282
304
|
];
|
|
283
305
|
|
|
284
|
-
// src/
|
|
306
|
+
// src/utils.ts
|
|
307
|
+
import { execFileSync } from "child_process";
|
|
308
|
+
import fs from "fs";
|
|
309
|
+
import os from "os";
|
|
310
|
+
import path from "path";
|
|
311
|
+
import JSON5 from "json5";
|
|
285
312
|
function scanFileForKeys(filepath) {
|
|
286
313
|
const hits = [];
|
|
287
314
|
try {
|
|
@@ -293,7 +320,10 @@ function scanFileForKeys(filepath) {
|
|
|
293
320
|
continue;
|
|
294
321
|
}
|
|
295
322
|
for (const [keyName, pattern] of API_KEY_PATTERNS) {
|
|
296
|
-
const globalPattern = new RegExp(
|
|
323
|
+
const globalPattern = new RegExp(
|
|
324
|
+
pattern.source,
|
|
325
|
+
pattern.flags + (pattern.flags.includes("g") ? "" : "g")
|
|
326
|
+
);
|
|
297
327
|
let match;
|
|
298
328
|
while ((match = globalPattern.exec(line)) !== null) {
|
|
299
329
|
const matched = match[0];
|
|
@@ -306,45 +336,80 @@ function scanFileForKeys(filepath) {
|
|
|
306
336
|
}
|
|
307
337
|
return hits;
|
|
308
338
|
}
|
|
339
|
+
function parseConfig(configPath) {
|
|
340
|
+
try {
|
|
341
|
+
const content = fs.readFileSync(configPath, { encoding: "utf-8" });
|
|
342
|
+
return JSON5.parse(content);
|
|
343
|
+
} catch {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function findFilesRecursive(dir, match, mode = "extension") {
|
|
348
|
+
const results = [];
|
|
349
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
350
|
+
return results;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
354
|
+
for (const entry of entries) {
|
|
355
|
+
const fullPath = path.join(dir, entry.name);
|
|
356
|
+
if (entry.isDirectory()) {
|
|
357
|
+
results.push(...findFilesRecursive(fullPath, match, mode));
|
|
358
|
+
} else if (entry.isFile()) {
|
|
359
|
+
if (mode === "extension" && entry.name.endsWith(match)) {
|
|
360
|
+
results.push(fullPath);
|
|
361
|
+
} else if (mode === "exact" && entry.name === match) {
|
|
362
|
+
results.push(fullPath);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
return results;
|
|
369
|
+
}
|
|
370
|
+
function isDockerAvailable() {
|
|
371
|
+
try {
|
|
372
|
+
execFileSync("which", ["docker"], { stdio: "pipe" });
|
|
373
|
+
return true;
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function getTotalMemoryGB() {
|
|
379
|
+
return os.totalmem() / (1024 * 1024 * 1024);
|
|
380
|
+
}
|
|
309
381
|
function getRelativePath(filePath, basePath) {
|
|
310
382
|
const parentDir = path.dirname(basePath);
|
|
311
383
|
return path.relative(parentDir, filePath);
|
|
312
384
|
}
|
|
385
|
+
|
|
386
|
+
// src/checks/credentials.ts
|
|
313
387
|
function checkCredentials(openclawPath) {
|
|
314
388
|
const findings = [];
|
|
315
389
|
const configFiles = [
|
|
316
|
-
|
|
317
|
-
|
|
390
|
+
path2.join(openclawPath, "openclaw.json"),
|
|
391
|
+
path2.join(openclawPath, "credentials", "profiles.json")
|
|
318
392
|
];
|
|
319
|
-
const agentsDir =
|
|
320
|
-
if (
|
|
393
|
+
const agentsDir = path2.join(openclawPath, "agents");
|
|
394
|
+
if (fs2.existsSync(agentsDir) && fs2.statSync(agentsDir).isDirectory()) {
|
|
321
395
|
try {
|
|
322
|
-
for (const agentEntry of
|
|
323
|
-
const authFile =
|
|
396
|
+
for (const agentEntry of fs2.readdirSync(agentsDir)) {
|
|
397
|
+
const authFile = path2.join(
|
|
324
398
|
agentsDir,
|
|
325
399
|
agentEntry,
|
|
326
400
|
"agent",
|
|
327
401
|
"auth-profiles.json"
|
|
328
402
|
);
|
|
329
|
-
if (
|
|
403
|
+
if (fs2.existsSync(authFile)) {
|
|
330
404
|
configFiles.push(authFile);
|
|
331
405
|
}
|
|
332
406
|
}
|
|
333
407
|
} catch {
|
|
334
408
|
}
|
|
335
409
|
}
|
|
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
410
|
const allHits = [];
|
|
346
411
|
for (const filepath of configFiles) {
|
|
347
|
-
if (
|
|
412
|
+
if (fs2.existsSync(filepath)) {
|
|
348
413
|
const hits = scanFileForKeys(filepath);
|
|
349
414
|
for (const { keyName, masked, lineNum } of hits) {
|
|
350
415
|
const relPath = getRelativePath(filepath, openclawPath);
|
|
@@ -356,16 +421,16 @@ function checkCredentials(openclawPath) {
|
|
|
356
421
|
findings.push(
|
|
357
422
|
createFinding({
|
|
358
423
|
severity: Severity.CRITICAL,
|
|
359
|
-
title: `${allHits.length} API key(s) stored in plaintext`,
|
|
424
|
+
title: `${allHits.length} API key(s) stored in plaintext config files`,
|
|
360
425
|
details: allHits.slice(0, 10),
|
|
361
426
|
// Show max 10
|
|
362
|
-
fix: '
|
|
427
|
+
fix: 'Move keys to .env and use env var refs: "apiKey": "${ANTHROPIC_API_KEY}"',
|
|
363
428
|
category: "credentials"
|
|
364
429
|
})
|
|
365
430
|
);
|
|
366
431
|
}
|
|
367
432
|
const bakWithKeys = [];
|
|
368
|
-
const bakFiles = findFilesRecursive(openclawPath, ".bak");
|
|
433
|
+
const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
|
|
369
434
|
for (const bakFile of bakFiles) {
|
|
370
435
|
const hits = scanFileForKeys(bakFile);
|
|
371
436
|
if (hits.length > 0) {
|
|
@@ -384,11 +449,11 @@ function checkCredentials(openclawPath) {
|
|
|
384
449
|
);
|
|
385
450
|
}
|
|
386
451
|
const transcriptHits = [];
|
|
387
|
-
if (
|
|
388
|
-
const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl");
|
|
452
|
+
if (fs2.existsSync(agentsDir) && fs2.statSync(agentsDir).isDirectory()) {
|
|
453
|
+
const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl", "extension");
|
|
389
454
|
for (const jsonlFile of jsonlFiles) {
|
|
390
455
|
try {
|
|
391
|
-
const content =
|
|
456
|
+
const content = fs2.readFileSync(jsonlFile, { encoding: "utf-8" });
|
|
392
457
|
const lines = content.split("\n");
|
|
393
458
|
let found = false;
|
|
394
459
|
for (let i = 0; i < Math.min(lines.length, 501); i++) {
|
|
@@ -419,11 +484,11 @@ function checkCredentials(openclawPath) {
|
|
|
419
484
|
})
|
|
420
485
|
);
|
|
421
486
|
}
|
|
422
|
-
const configFile =
|
|
423
|
-
if (
|
|
487
|
+
const configFile = path2.join(openclawPath, "openclaw.json");
|
|
488
|
+
if (fs2.existsSync(configFile)) {
|
|
424
489
|
try {
|
|
425
|
-
const content =
|
|
426
|
-
const config =
|
|
490
|
+
const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
|
|
491
|
+
const config = JSON52.parse(content);
|
|
427
492
|
const loggingConfig = config.logging ?? {};
|
|
428
493
|
const redact = loggingConfig.redactSensitive;
|
|
429
494
|
if (redact === void 0 || redact === null || redact === "off") {
|
|
@@ -442,58 +507,41 @@ function checkCredentials(openclawPath) {
|
|
|
442
507
|
}
|
|
443
508
|
return findings;
|
|
444
509
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
510
|
+
|
|
511
|
+
// src/checks/gateway.ts
|
|
512
|
+
import { execSync } from "child_process";
|
|
513
|
+
import fs3 from "fs";
|
|
514
|
+
import path3 from "path";
|
|
515
|
+
import JSON53 from "json5";
|
|
516
|
+
function checkPortExposed(port) {
|
|
517
|
+
if (process.platform !== "linux") {
|
|
518
|
+
return false;
|
|
449
519
|
}
|
|
450
520
|
try {
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
521
|
+
const output = execSync(`ss -tlnp`, { encoding: "utf-8", timeout: 5e3 });
|
|
522
|
+
const lines = output.split("\n");
|
|
523
|
+
for (const line of lines) {
|
|
524
|
+
const columns = line.trim().split(/\s+/);
|
|
525
|
+
if (columns.length < 5) continue;
|
|
526
|
+
const localAddr = columns[3];
|
|
527
|
+
const lastColon = localAddr.lastIndexOf(":");
|
|
528
|
+
if (lastColon === -1) continue;
|
|
529
|
+
const portStr = localAddr.substring(lastColon + 1);
|
|
530
|
+
if (parseInt(portStr, 10) !== port) continue;
|
|
531
|
+
const host = localAddr.substring(0, lastColon);
|
|
532
|
+
if (host === "0.0.0.0" || host === "*" || host === "[::]" || host === "::") {
|
|
533
|
+
return true;
|
|
458
534
|
}
|
|
459
535
|
}
|
|
536
|
+
return false;
|
|
460
537
|
} catch {
|
|
538
|
+
return false;
|
|
461
539
|
}
|
|
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
540
|
}
|
|
493
|
-
|
|
541
|
+
function checkGateway(openclawPath) {
|
|
494
542
|
const findings = [];
|
|
495
|
-
const configFile =
|
|
496
|
-
if (!
|
|
543
|
+
const configFile = path3.join(openclawPath, "openclaw.json");
|
|
544
|
+
if (!fs3.existsSync(configFile)) {
|
|
497
545
|
findings.push(
|
|
498
546
|
createFinding({
|
|
499
547
|
severity: Severity.INFO,
|
|
@@ -506,8 +554,8 @@ async function checkGateway(openclawPath) {
|
|
|
506
554
|
}
|
|
507
555
|
let config;
|
|
508
556
|
try {
|
|
509
|
-
const content =
|
|
510
|
-
config =
|
|
557
|
+
const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
|
|
558
|
+
config = JSON53.parse(content);
|
|
511
559
|
} catch {
|
|
512
560
|
findings.push(
|
|
513
561
|
createFinding({
|
|
@@ -561,7 +609,7 @@ async function checkGateway(openclawPath) {
|
|
|
561
609
|
);
|
|
562
610
|
}
|
|
563
611
|
const port = gateway.port ?? 18789;
|
|
564
|
-
const portExposed =
|
|
612
|
+
const portExposed = checkPortExposed(port);
|
|
565
613
|
if (portExposed) {
|
|
566
614
|
findings.push(
|
|
567
615
|
createFinding({
|
|
@@ -576,32 +624,83 @@ async function checkGateway(openclawPath) {
|
|
|
576
624
|
})
|
|
577
625
|
);
|
|
578
626
|
}
|
|
627
|
+
const ramGB = getTotalMemoryGB();
|
|
628
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
629
|
+
if (ramGB < 2) {
|
|
630
|
+
if (!nodeOptions.includes("max-old-space-size")) {
|
|
631
|
+
findings.push(
|
|
632
|
+
createFinding({
|
|
633
|
+
severity: Severity.HIGH,
|
|
634
|
+
title: `System has only ${ramGB.toFixed(1)}GB RAM without NODE_OPTIONS set`,
|
|
635
|
+
details: [
|
|
636
|
+
`Total RAM: ${ramGB.toFixed(1)}GB`,
|
|
637
|
+
"OpenClaw may be killed by OOM without memory limits",
|
|
638
|
+
"NODE_OPTIONS environment variable is not set"
|
|
639
|
+
],
|
|
640
|
+
fix: 'Set NODE_OPTIONS="--max-old-space-size=512" in your service file or shell profile',
|
|
641
|
+
category: "gateway"
|
|
642
|
+
})
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (process.platform === "linux") {
|
|
647
|
+
const serviceFiles = [
|
|
648
|
+
"/etc/systemd/system/openclaw.service",
|
|
649
|
+
"/etc/systemd/system/openclaw-gateway.service"
|
|
650
|
+
];
|
|
651
|
+
for (const serviceFile of serviceFiles) {
|
|
652
|
+
if (!fs3.existsSync(serviceFile)) continue;
|
|
653
|
+
try {
|
|
654
|
+
const serviceContent = fs3.readFileSync(serviceFile, { encoding: "utf-8" });
|
|
655
|
+
if (ramGB < 2 && !serviceContent.includes("NODE_OPTIONS")) {
|
|
656
|
+
findings.push(
|
|
657
|
+
createFinding({
|
|
658
|
+
severity: Severity.HIGH,
|
|
659
|
+
title: `Systemd service missing NODE_OPTIONS (${path3.basename(serviceFile)})`,
|
|
660
|
+
details: [
|
|
661
|
+
`Low RAM system (${ramGB.toFixed(1)}GB) but service has no NODE_OPTIONS`,
|
|
662
|
+
"Add Environment=NODE_OPTIONS=--max-old-space-size=512 to [Service] section"
|
|
663
|
+
],
|
|
664
|
+
fix: `Edit ${serviceFile} and add NODE_OPTIONS, then systemctl daemon-reload`,
|
|
665
|
+
category: "gateway"
|
|
666
|
+
})
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (serviceContent.includes("Restart=always") && !serviceContent.includes("RestartSec")) {
|
|
670
|
+
findings.push(
|
|
671
|
+
createFinding({
|
|
672
|
+
severity: Severity.MEDIUM,
|
|
673
|
+
title: `Systemd service has Restart=always without RestartSec (${path3.basename(serviceFile)})`,
|
|
674
|
+
details: [
|
|
675
|
+
"Service will restart immediately on crash, potentially causing a restart storm",
|
|
676
|
+
"Add RestartSec=5 to [Service] section to prevent rapid restarts"
|
|
677
|
+
],
|
|
678
|
+
fix: `Add RestartSec=5 to ${serviceFile} [Service] section`,
|
|
679
|
+
category: "gateway"
|
|
680
|
+
})
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
579
687
|
return findings;
|
|
580
688
|
}
|
|
581
689
|
|
|
582
690
|
// src/checks/sandbox.ts
|
|
583
|
-
import
|
|
584
|
-
import
|
|
585
|
-
import
|
|
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
|
-
}
|
|
691
|
+
import fs4 from "fs";
|
|
692
|
+
import path4 from "path";
|
|
693
|
+
import JSON54 from "json5";
|
|
595
694
|
function checkSandbox(openclawPath) {
|
|
596
695
|
const findings = [];
|
|
597
|
-
const configFile =
|
|
598
|
-
if (!
|
|
696
|
+
const configFile = path4.join(openclawPath, "openclaw.json");
|
|
697
|
+
if (!fs4.existsSync(configFile)) {
|
|
599
698
|
return findings;
|
|
600
699
|
}
|
|
601
700
|
let config;
|
|
602
701
|
try {
|
|
603
|
-
const content =
|
|
604
|
-
config =
|
|
702
|
+
const content = fs4.readFileSync(configFile, { encoding: "utf-8" });
|
|
703
|
+
config = JSON54.parse(content);
|
|
605
704
|
} catch {
|
|
606
705
|
return findings;
|
|
607
706
|
}
|
|
@@ -670,7 +769,22 @@ function checkSandbox(openclawPath) {
|
|
|
670
769
|
);
|
|
671
770
|
}
|
|
672
771
|
const execSecurity = execConfig.security;
|
|
673
|
-
|
|
772
|
+
const validExecSecurity = ["deny", "allowlist", "full"];
|
|
773
|
+
if (execSecurity && !validExecSecurity.includes(execSecurity)) {
|
|
774
|
+
findings.push(
|
|
775
|
+
createFinding({
|
|
776
|
+
severity: Severity.HIGH,
|
|
777
|
+
title: `Invalid tools.exec.security value: "${execSecurity}"`,
|
|
778
|
+
details: [
|
|
779
|
+
`tools.exec.security = "${execSecurity}"`,
|
|
780
|
+
`Valid values: ${validExecSecurity.join(", ")}`,
|
|
781
|
+
"Invalid value may cause unpredictable behavior"
|
|
782
|
+
],
|
|
783
|
+
fix: 'Set tools.exec.security to "deny", "allowlist", or "full" in openclaw.json',
|
|
784
|
+
category: "sandbox"
|
|
785
|
+
})
|
|
786
|
+
);
|
|
787
|
+
} else if (execSecurity !== "allowlist") {
|
|
674
788
|
findings.push(
|
|
675
789
|
createFinding({
|
|
676
790
|
severity: Severity.MEDIUM,
|
|
@@ -684,6 +798,26 @@ function checkSandbox(openclawPath) {
|
|
|
684
798
|
})
|
|
685
799
|
);
|
|
686
800
|
}
|
|
801
|
+
const commands = config.commands ?? {};
|
|
802
|
+
const nativeSkills = commands.nativeSkills;
|
|
803
|
+
if (nativeSkills !== void 0 && nativeSkills !== null) {
|
|
804
|
+
const validNativeSkills = [false, "auto", "off"];
|
|
805
|
+
if (!validNativeSkills.includes(nativeSkills)) {
|
|
806
|
+
findings.push(
|
|
807
|
+
createFinding({
|
|
808
|
+
severity: Severity.MEDIUM,
|
|
809
|
+
title: `Invalid commands.nativeSkills value: "${String(nativeSkills)}"`,
|
|
810
|
+
details: [
|
|
811
|
+
`commands.nativeSkills = "${String(nativeSkills)}"`,
|
|
812
|
+
'Valid values: false, "auto", "off"',
|
|
813
|
+
"Invalid value may cause skill execution issues"
|
|
814
|
+
],
|
|
815
|
+
fix: 'Set commands.nativeSkills to false, "auto", or "off" in openclaw.json',
|
|
816
|
+
category: "sandbox"
|
|
817
|
+
})
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
687
821
|
if (dockerAvailable) {
|
|
688
822
|
findings.push(
|
|
689
823
|
createFinding({
|
|
@@ -700,48 +834,44 @@ function checkSandbox(openclawPath) {
|
|
|
700
834
|
}
|
|
701
835
|
|
|
702
836
|
// src/checks/permissions.ts
|
|
703
|
-
import
|
|
704
|
-
import
|
|
837
|
+
import fs5 from "fs";
|
|
838
|
+
import path5 from "path";
|
|
705
839
|
function getPermissionOctal(filepath) {
|
|
706
|
-
const stats =
|
|
840
|
+
const stats = fs5.statSync(filepath);
|
|
707
841
|
return "0o" + (stats.mode & 511).toString(8);
|
|
708
842
|
}
|
|
709
843
|
function isWorldReadable(filepath) {
|
|
710
|
-
const stats =
|
|
844
|
+
const stats = fs5.statSync(filepath);
|
|
711
845
|
return (stats.mode & 4) !== 0;
|
|
712
846
|
}
|
|
713
847
|
function isGroupReadable(filepath) {
|
|
714
|
-
const stats =
|
|
848
|
+
const stats = fs5.statSync(filepath);
|
|
715
849
|
return (stats.mode & 32) !== 0;
|
|
716
850
|
}
|
|
717
|
-
function getRelativePath2(filePath, basePath) {
|
|
718
|
-
const parentDir = path4.dirname(basePath);
|
|
719
|
-
return path4.relative(parentDir, filePath);
|
|
720
|
-
}
|
|
721
851
|
function checkPermissions(openclawPath) {
|
|
722
852
|
const findings = [];
|
|
723
853
|
const tooOpen = [];
|
|
724
|
-
if (
|
|
854
|
+
if (fs5.existsSync(openclawPath)) {
|
|
725
855
|
const perms = getPermissionOctal(openclawPath);
|
|
726
856
|
if (isWorldReadable(openclawPath) || isGroupReadable(openclawPath)) {
|
|
727
857
|
tooOpen.push(`${openclawPath} is ${perms} (should be 0o700)`);
|
|
728
858
|
}
|
|
729
859
|
}
|
|
730
860
|
const sensitiveFiles = [
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
861
|
+
path5.join(openclawPath, "openclaw.json"),
|
|
862
|
+
path5.join(openclawPath, ".env"),
|
|
863
|
+
path5.join(openclawPath, "credentials", "profiles.json")
|
|
734
864
|
];
|
|
735
|
-
const agentsDir =
|
|
736
|
-
if (
|
|
737
|
-
const authFiles =
|
|
865
|
+
const agentsDir = path5.join(openclawPath, "agents");
|
|
866
|
+
if (fs5.existsSync(agentsDir) && fs5.statSync(agentsDir).isDirectory()) {
|
|
867
|
+
const authFiles = findFilesRecursive(agentsDir, "auth-profiles.json", "exact");
|
|
738
868
|
sensitiveFiles.push(...authFiles);
|
|
739
869
|
}
|
|
740
870
|
for (const filepath of sensitiveFiles) {
|
|
741
|
-
if (
|
|
871
|
+
if (fs5.existsSync(filepath)) {
|
|
742
872
|
const perms = getPermissionOctal(filepath);
|
|
743
873
|
if (isWorldReadable(filepath) || isGroupReadable(filepath)) {
|
|
744
|
-
const relPath =
|
|
874
|
+
const relPath = getRelativePath(filepath, openclawPath);
|
|
745
875
|
tooOpen.push(`${relPath} is ${perms} (should be 0o600)`);
|
|
746
876
|
}
|
|
747
877
|
}
|
|
@@ -759,31 +889,12 @@ function checkPermissions(openclawPath) {
|
|
|
759
889
|
}
|
|
760
890
|
return findings;
|
|
761
891
|
}
|
|
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
892
|
|
|
782
893
|
// src/checks/version.ts
|
|
783
894
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
784
|
-
import
|
|
785
|
-
import
|
|
786
|
-
import
|
|
895
|
+
import fs6 from "fs";
|
|
896
|
+
import path6 from "path";
|
|
897
|
+
import os2 from "os";
|
|
787
898
|
var CVES = [
|
|
788
899
|
{
|
|
789
900
|
id: "CVE-2026-25253",
|
|
@@ -870,8 +981,8 @@ function checkVersion(_openclawPath) {
|
|
|
870
981
|
}
|
|
871
982
|
} else {
|
|
872
983
|
const pkgPaths = [
|
|
873
|
-
|
|
874
|
-
|
|
984
|
+
path6.join(
|
|
985
|
+
os2.homedir(),
|
|
875
986
|
".bun",
|
|
876
987
|
"install",
|
|
877
988
|
"global",
|
|
@@ -883,9 +994,9 @@ function checkVersion(_openclawPath) {
|
|
|
883
994
|
];
|
|
884
995
|
let found = false;
|
|
885
996
|
for (const pkgPath of pkgPaths) {
|
|
886
|
-
if (
|
|
997
|
+
if (fs6.existsSync(pkgPath)) {
|
|
887
998
|
try {
|
|
888
|
-
const content =
|
|
999
|
+
const content = fs6.readFileSync(pkgPath, { encoding: "utf-8" });
|
|
889
1000
|
const pkg = JSON.parse(content);
|
|
890
1001
|
ocVersion = pkg.version ?? "unknown";
|
|
891
1002
|
findings.push(
|
|
@@ -943,11 +1054,11 @@ function checkVersion(_openclawPath) {
|
|
|
943
1054
|
}
|
|
944
1055
|
|
|
945
1056
|
// src/checks/skills.ts
|
|
946
|
-
import
|
|
947
|
-
import
|
|
1057
|
+
import fs7 from "fs";
|
|
1058
|
+
import path7 from "path";
|
|
948
1059
|
import yaml from "js-yaml";
|
|
949
1060
|
function parseSkillMd(skillPath) {
|
|
950
|
-
const content =
|
|
1061
|
+
const content = fs7.readFileSync(skillPath, { encoding: "utf-8" });
|
|
951
1062
|
let frontmatter = {};
|
|
952
1063
|
let body = content;
|
|
953
1064
|
if (content.startsWith("---")) {
|
|
@@ -1044,18 +1155,18 @@ function checkSkillMalicious(name, body) {
|
|
|
1044
1155
|
function checkSkills(openclawPath) {
|
|
1045
1156
|
const findings = [];
|
|
1046
1157
|
const skillDirs = [
|
|
1047
|
-
|
|
1048
|
-
|
|
1158
|
+
path7.join(openclawPath, "workspace", "skills"),
|
|
1159
|
+
path7.join(openclawPath, "skills")
|
|
1049
1160
|
];
|
|
1050
1161
|
let totalSkills = 0;
|
|
1051
1162
|
let flaggedSkills = 0;
|
|
1052
1163
|
for (const skillsRoot of skillDirs) {
|
|
1053
|
-
if (!
|
|
1164
|
+
if (!fs7.existsSync(skillsRoot) || !fs7.statSync(skillsRoot).isDirectory()) {
|
|
1054
1165
|
continue;
|
|
1055
1166
|
}
|
|
1056
1167
|
let entries;
|
|
1057
1168
|
try {
|
|
1058
|
-
entries =
|
|
1169
|
+
entries = fs7.readdirSync(skillsRoot, { withFileTypes: true });
|
|
1059
1170
|
} catch {
|
|
1060
1171
|
continue;
|
|
1061
1172
|
}
|
|
@@ -1063,8 +1174,8 @@ function checkSkills(openclawPath) {
|
|
|
1063
1174
|
if (!entry.isDirectory()) {
|
|
1064
1175
|
continue;
|
|
1065
1176
|
}
|
|
1066
|
-
const skillMdPath =
|
|
1067
|
-
if (!
|
|
1177
|
+
const skillMdPath = path7.join(skillsRoot, entry.name, "SKILL.md");
|
|
1178
|
+
if (!fs7.existsSync(skillMdPath)) {
|
|
1068
1179
|
continue;
|
|
1069
1180
|
}
|
|
1070
1181
|
totalSkills++;
|
|
@@ -1079,7 +1190,7 @@ function checkSkills(openclawPath) {
|
|
|
1079
1190
|
details: [
|
|
1080
1191
|
"This publisher name is known to be associated with malicious skills"
|
|
1081
1192
|
],
|
|
1082
|
-
fix: `REMOVE IMMEDIATELY: rm -rf ${
|
|
1193
|
+
fix: `REMOVE IMMEDIATELY: rm -rf ${path7.join(skillsRoot, entry.name)}`,
|
|
1083
1194
|
category: "skills"
|
|
1084
1195
|
})
|
|
1085
1196
|
);
|
|
@@ -1119,23 +1230,23 @@ function checkSkills(openclawPath) {
|
|
|
1119
1230
|
}
|
|
1120
1231
|
|
|
1121
1232
|
// src/checks/memory.ts
|
|
1122
|
-
import
|
|
1123
|
-
import
|
|
1233
|
+
import fs8 from "fs";
|
|
1234
|
+
import path8 from "path";
|
|
1124
1235
|
function checkMemory(openclawPath) {
|
|
1125
1236
|
const findings = [];
|
|
1126
|
-
const workspace =
|
|
1127
|
-
if (!
|
|
1237
|
+
const workspace = path8.join(openclawPath, "workspace");
|
|
1238
|
+
if (!fs8.existsSync(workspace) || !fs8.statSync(workspace).isDirectory()) {
|
|
1128
1239
|
return findings;
|
|
1129
1240
|
}
|
|
1130
1241
|
const identityFiles = [
|
|
1131
|
-
|
|
1132
|
-
|
|
1242
|
+
path8.join(workspace, "SOUL.md"),
|
|
1243
|
+
path8.join(workspace, "IDENTITY.md")
|
|
1133
1244
|
];
|
|
1134
1245
|
for (const filepath of identityFiles) {
|
|
1135
|
-
if (!
|
|
1246
|
+
if (!fs8.existsSync(filepath)) {
|
|
1136
1247
|
continue;
|
|
1137
1248
|
}
|
|
1138
|
-
const content =
|
|
1249
|
+
const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
|
|
1139
1250
|
const detected = [];
|
|
1140
1251
|
for (const [patternName, pattern] of MEMORY_POISONING_PATTERNS) {
|
|
1141
1252
|
const matches = content.match(pattern);
|
|
@@ -1148,7 +1259,7 @@ function checkMemory(openclawPath) {
|
|
|
1148
1259
|
findings.push(
|
|
1149
1260
|
createFinding({
|
|
1150
1261
|
severity: Severity.HIGH,
|
|
1151
|
-
title: `Potential memory poisoning in ${
|
|
1262
|
+
title: `Potential memory poisoning in ${path8.basename(filepath)}`,
|
|
1152
1263
|
details: detected,
|
|
1153
1264
|
fix: `Review ${filepath} for injected instructions and restore from backup`,
|
|
1154
1265
|
category: "memory"
|
|
@@ -1156,14 +1267,14 @@ function checkMemory(openclawPath) {
|
|
|
1156
1267
|
);
|
|
1157
1268
|
}
|
|
1158
1269
|
}
|
|
1159
|
-
const memoryFiles = [
|
|
1160
|
-
const memoryDir =
|
|
1161
|
-
if (
|
|
1270
|
+
const memoryFiles = [path8.join(workspace, "MEMORY.md")];
|
|
1271
|
+
const memoryDir = path8.join(workspace, "memory");
|
|
1272
|
+
if (fs8.existsSync(memoryDir) && fs8.statSync(memoryDir).isDirectory()) {
|
|
1162
1273
|
try {
|
|
1163
|
-
const entries =
|
|
1274
|
+
const entries = fs8.readdirSync(memoryDir);
|
|
1164
1275
|
for (const entry of entries) {
|
|
1165
1276
|
if (entry.endsWith(".md")) {
|
|
1166
|
-
memoryFiles.push(
|
|
1277
|
+
memoryFiles.push(path8.join(memoryDir, entry));
|
|
1167
1278
|
}
|
|
1168
1279
|
}
|
|
1169
1280
|
} catch {
|
|
@@ -1171,13 +1282,13 @@ function checkMemory(openclawPath) {
|
|
|
1171
1282
|
}
|
|
1172
1283
|
const leakedIn = [];
|
|
1173
1284
|
for (const filepath of memoryFiles) {
|
|
1174
|
-
if (!
|
|
1285
|
+
if (!fs8.existsSync(filepath)) {
|
|
1175
1286
|
continue;
|
|
1176
1287
|
}
|
|
1177
|
-
const content =
|
|
1288
|
+
const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
|
|
1178
1289
|
for (const [keyName, pattern] of API_KEY_PATTERNS) {
|
|
1179
1290
|
if (pattern.test(content)) {
|
|
1180
|
-
leakedIn.push(`${
|
|
1291
|
+
leakedIn.push(`${path8.basename(filepath)} contains ${keyName}`);
|
|
1181
1292
|
break;
|
|
1182
1293
|
}
|
|
1183
1294
|
}
|
|
@@ -1196,6 +1307,257 @@ function checkMemory(openclawPath) {
|
|
|
1196
1307
|
return findings;
|
|
1197
1308
|
}
|
|
1198
1309
|
|
|
1310
|
+
// src/checks/agents.ts
|
|
1311
|
+
import fs9 from "fs";
|
|
1312
|
+
import path9 from "path";
|
|
1313
|
+
function checkAgents(openclawPath) {
|
|
1314
|
+
const findings = [];
|
|
1315
|
+
const agentsDir = path9.join(openclawPath, "agents");
|
|
1316
|
+
if (!fs9.existsSync(agentsDir) || !fs9.statSync(agentsDir).isDirectory()) {
|
|
1317
|
+
return findings;
|
|
1318
|
+
}
|
|
1319
|
+
const globalConfig = parseConfig(
|
|
1320
|
+
path9.join(openclawPath, "openclaw.json")
|
|
1321
|
+
);
|
|
1322
|
+
const globalModel = getGlobalModel(globalConfig);
|
|
1323
|
+
let agentCount = 0;
|
|
1324
|
+
let issueCount = 0;
|
|
1325
|
+
let entries;
|
|
1326
|
+
try {
|
|
1327
|
+
entries = fs9.readdirSync(agentsDir, { withFileTypes: true });
|
|
1328
|
+
} catch {
|
|
1329
|
+
return findings;
|
|
1330
|
+
}
|
|
1331
|
+
for (const entry of entries) {
|
|
1332
|
+
if (!entry.isDirectory()) continue;
|
|
1333
|
+
const modelsJsonPath = path9.join(
|
|
1334
|
+
agentsDir,
|
|
1335
|
+
entry.name,
|
|
1336
|
+
"agent",
|
|
1337
|
+
"models.json"
|
|
1338
|
+
);
|
|
1339
|
+
if (!fs9.existsSync(modelsJsonPath)) continue;
|
|
1340
|
+
agentCount++;
|
|
1341
|
+
const agentName = entry.name;
|
|
1342
|
+
const keyHits = scanFileForKeys(modelsJsonPath);
|
|
1343
|
+
const openclawUsesEnvRefs = keyHits.length > 0 ? checkOpenclawModelsUseEnvRefs(openclawPath) : false;
|
|
1344
|
+
if (keyHits.length > 0) {
|
|
1345
|
+
if (openclawUsesEnvRefs) {
|
|
1346
|
+
findings.push(
|
|
1347
|
+
createFinding({
|
|
1348
|
+
severity: Severity.INFO,
|
|
1349
|
+
title: `Agent '${agentName}': models.json contains resolved keys (auto-generated)`,
|
|
1350
|
+
details: [
|
|
1351
|
+
"models.json contains resolved API keys, but openclaw.json uses ${ENV_VAR} references",
|
|
1352
|
+
"OpenClaw regenerates models.json from openclaw.json on every gateway startup",
|
|
1353
|
+
"This is expected behavior \u2014 no action needed"
|
|
1354
|
+
],
|
|
1355
|
+
category: "agents"
|
|
1356
|
+
})
|
|
1357
|
+
);
|
|
1358
|
+
} else {
|
|
1359
|
+
issueCount++;
|
|
1360
|
+
const details = keyHits.slice(0, 5).map(
|
|
1361
|
+
(h) => `Line ${h.lineNum}: ${h.keyName} (${h.masked})`
|
|
1362
|
+
);
|
|
1363
|
+
findings.push(
|
|
1364
|
+
createFinding({
|
|
1365
|
+
severity: Severity.CRITICAL,
|
|
1366
|
+
title: `Agent '${agentName}' has plaintext API keys in models.json`,
|
|
1367
|
+
details,
|
|
1368
|
+
fix: `Replace raw keys with env var refs: "apiKey": "\${${PROVIDER_ENV_MAP.anthropic}}"`,
|
|
1369
|
+
category: "agents"
|
|
1370
|
+
})
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (!openclawUsesEnvRefs) {
|
|
1375
|
+
try {
|
|
1376
|
+
const content = fs9.readFileSync(modelsJsonPath, {
|
|
1377
|
+
encoding: "utf-8"
|
|
1378
|
+
});
|
|
1379
|
+
const hasEnvRefs = ENV_VAR_PATTERN.test(content);
|
|
1380
|
+
if (keyHits.length > 0 && !hasEnvRefs) {
|
|
1381
|
+
findings.push(
|
|
1382
|
+
createFinding({
|
|
1383
|
+
severity: Severity.HIGH,
|
|
1384
|
+
title: `Agent '${agentName}' uses raw keys instead of env var references`,
|
|
1385
|
+
details: [
|
|
1386
|
+
"models.json contains no ${ENV_VAR} references",
|
|
1387
|
+
"All API keys should use environment variable substitution"
|
|
1388
|
+
],
|
|
1389
|
+
fix: 'Run "clawguard migrate-env" to auto-migrate keys to .env',
|
|
1390
|
+
category: "agents"
|
|
1391
|
+
})
|
|
1392
|
+
);
|
|
1393
|
+
issueCount++;
|
|
1394
|
+
}
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (globalModel) {
|
|
1399
|
+
const agentModel = getAgentModel(modelsJsonPath);
|
|
1400
|
+
if (agentModel && agentModel !== globalModel) {
|
|
1401
|
+
findings.push(
|
|
1402
|
+
createFinding({
|
|
1403
|
+
severity: Severity.MEDIUM,
|
|
1404
|
+
title: `Agent '${agentName}' model differs from global config`,
|
|
1405
|
+
details: [
|
|
1406
|
+
`Global model: ${globalModel}`,
|
|
1407
|
+
`Agent model: ${agentModel}`,
|
|
1408
|
+
"This agent overrides the global model setting (config shadow)"
|
|
1409
|
+
],
|
|
1410
|
+
fix: "Verify this is intentional. Remove agent-level model to use global setting.",
|
|
1411
|
+
category: "agents"
|
|
1412
|
+
})
|
|
1413
|
+
);
|
|
1414
|
+
issueCount++;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
if (agentCount > 0 && issueCount === 0) {
|
|
1419
|
+
findings.push(
|
|
1420
|
+
createFinding({
|
|
1421
|
+
severity: Severity.INFO,
|
|
1422
|
+
title: `${agentCount} agent config(s) scanned - no issues found`,
|
|
1423
|
+
category: "agents"
|
|
1424
|
+
})
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
return findings;
|
|
1428
|
+
}
|
|
1429
|
+
function getGlobalModel(config) {
|
|
1430
|
+
if (!config) return null;
|
|
1431
|
+
try {
|
|
1432
|
+
const models = config.models;
|
|
1433
|
+
if (!models) return null;
|
|
1434
|
+
const defaultModel = models.default;
|
|
1435
|
+
if (defaultModel) return defaultModel;
|
|
1436
|
+
const providers = models.providers;
|
|
1437
|
+
if (providers) {
|
|
1438
|
+
for (const provider of Object.values(providers)) {
|
|
1439
|
+
if (provider.model) return String(provider.model);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
} catch {
|
|
1443
|
+
}
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
function checkOpenclawModelsUseEnvRefs(openclawPath) {
|
|
1447
|
+
try {
|
|
1448
|
+
const configPath = path9.join(openclawPath, "openclaw.json");
|
|
1449
|
+
const content = fs9.readFileSync(configPath, { encoding: "utf-8" });
|
|
1450
|
+
const config = JSON.parse(content);
|
|
1451
|
+
const models = config.models;
|
|
1452
|
+
if (!models) return false;
|
|
1453
|
+
const providers = models.providers;
|
|
1454
|
+
if (!providers) return false;
|
|
1455
|
+
for (const provider of Object.values(providers)) {
|
|
1456
|
+
const apiKey = String(provider.apiKey ?? provider.key ?? "");
|
|
1457
|
+
if (ENV_VAR_PATTERN.test(apiKey)) {
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
} catch {
|
|
1462
|
+
}
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
function getAgentModel(modelsJsonPath) {
|
|
1466
|
+
try {
|
|
1467
|
+
const content = fs9.readFileSync(modelsJsonPath, { encoding: "utf-8" });
|
|
1468
|
+
const data = JSON.parse(content);
|
|
1469
|
+
if (data.model) return String(data.model);
|
|
1470
|
+
if (data.default) return String(data.default);
|
|
1471
|
+
const providers = data.providers;
|
|
1472
|
+
if (providers) {
|
|
1473
|
+
for (const provider of Object.values(providers)) {
|
|
1474
|
+
if (provider.model) return String(provider.model);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
// src/checks/providers.ts
|
|
1483
|
+
import path10 from "path";
|
|
1484
|
+
function checkProviders(openclawPath) {
|
|
1485
|
+
const findings = [];
|
|
1486
|
+
const config = parseConfig(path10.join(openclawPath, "openclaw.json"));
|
|
1487
|
+
if (!config) return findings;
|
|
1488
|
+
const models = config.models ?? {};
|
|
1489
|
+
const providers = models.providers ?? {};
|
|
1490
|
+
const providerSummary = [];
|
|
1491
|
+
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
1492
|
+
const lowerName = providerName.toLowerCase();
|
|
1493
|
+
providerSummary.push(
|
|
1494
|
+
`${providerName}: model=${String(providerConfig.model ?? "default")}`
|
|
1495
|
+
);
|
|
1496
|
+
const limits = PROVIDER_LIMITS[lowerName];
|
|
1497
|
+
if (limits && limits.tpm < OPENCLAW_SYSTEM_PROMPT_TOKENS) {
|
|
1498
|
+
findings.push(
|
|
1499
|
+
createFinding({
|
|
1500
|
+
severity: Severity.HIGH,
|
|
1501
|
+
title: `Provider '${providerName}' TPM too low for system prompt`,
|
|
1502
|
+
details: [
|
|
1503
|
+
`${limits.note}`,
|
|
1504
|
+
`OpenClaw system prompt requires ~${OPENCLAW_SYSTEM_PROMPT_TOKENS} tokens`,
|
|
1505
|
+
`Provider free tier allows only ${limits.tpm} TPM`,
|
|
1506
|
+
"System prompt alone exceeds the per-minute token limit"
|
|
1507
|
+
],
|
|
1508
|
+
fix: `Upgrade to a paid ${providerName} plan or switch to a provider with higher limits`,
|
|
1509
|
+
category: "providers"
|
|
1510
|
+
})
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
if (limits && limits.rpm < 10) {
|
|
1514
|
+
findings.push(
|
|
1515
|
+
createFinding({
|
|
1516
|
+
severity: Severity.MEDIUM,
|
|
1517
|
+
title: `Provider '${providerName}' has very low rate limits (${limits.rpm} RPM)`,
|
|
1518
|
+
details: [
|
|
1519
|
+
`${limits.note}`,
|
|
1520
|
+
"Low RPM causes frequent rate limit errors during conversations"
|
|
1521
|
+
],
|
|
1522
|
+
fix: `Consider upgrading ${providerName} plan or using a different provider`,
|
|
1523
|
+
category: "providers"
|
|
1524
|
+
})
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
const tools = config.tools ?? {};
|
|
1529
|
+
const webSearch = tools.webSearch ?? tools.web_search ?? {};
|
|
1530
|
+
const webSearchEnabled = webSearch.enabled !== false && Object.keys(webSearch).length > 0;
|
|
1531
|
+
if (webSearchEnabled) {
|
|
1532
|
+
const apiKey = webSearch.apiKey ?? webSearch.api_key;
|
|
1533
|
+
if (!apiKey) {
|
|
1534
|
+
findings.push(
|
|
1535
|
+
createFinding({
|
|
1536
|
+
severity: Severity.MEDIUM,
|
|
1537
|
+
title: "Web search tool has no API key configured",
|
|
1538
|
+
details: [
|
|
1539
|
+
"tools.webSearch.apiKey is not set",
|
|
1540
|
+
"Web search will fail or use a very limited fallback"
|
|
1541
|
+
],
|
|
1542
|
+
fix: "Set tools.webSearch.apiKey to your Brave Search API key (get one at brave.com/search/api)",
|
|
1543
|
+
category: "providers"
|
|
1544
|
+
})
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (providerSummary.length > 0) {
|
|
1549
|
+
findings.push(
|
|
1550
|
+
createFinding({
|
|
1551
|
+
severity: Severity.INFO,
|
|
1552
|
+
title: `${providerSummary.length} LLM provider(s) configured`,
|
|
1553
|
+
details: providerSummary,
|
|
1554
|
+
category: "providers"
|
|
1555
|
+
})
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
return findings;
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1199
1561
|
// src/scanner.ts
|
|
1200
1562
|
var CHECK_REGISTRY = {
|
|
1201
1563
|
credentials: checkCredentials,
|
|
@@ -1203,14 +1565,16 @@ var CHECK_REGISTRY = {
|
|
|
1203
1565
|
sandbox: checkSandbox,
|
|
1204
1566
|
permissions: checkPermissions,
|
|
1205
1567
|
skills: checkSkills,
|
|
1206
|
-
memory: checkMemory
|
|
1568
|
+
memory: checkMemory,
|
|
1569
|
+
agents: checkAgents,
|
|
1570
|
+
providers: checkProviders
|
|
1207
1571
|
};
|
|
1208
1572
|
function detectOpenclawPath() {
|
|
1209
1573
|
const candidates = [
|
|
1210
|
-
|
|
1211
|
-
|
|
1574
|
+
path11.join(os3.homedir(), ".openclaw"),
|
|
1575
|
+
path11.join(os3.homedir(), ".clawdbot"),
|
|
1212
1576
|
// Legacy name
|
|
1213
|
-
|
|
1577
|
+
path11.join(os3.homedir(), ".moltbot")
|
|
1214
1578
|
// Legacy name
|
|
1215
1579
|
];
|
|
1216
1580
|
const envPath = process.env.OPENCLAW_HOME;
|
|
@@ -1218,7 +1582,7 @@ function detectOpenclawPath() {
|
|
|
1218
1582
|
candidates.unshift(envPath);
|
|
1219
1583
|
}
|
|
1220
1584
|
for (const candidate of candidates) {
|
|
1221
|
-
if (
|
|
1585
|
+
if (fs10.existsSync(candidate) && fs10.statSync(candidate).isDirectory()) {
|
|
1222
1586
|
return candidate;
|
|
1223
1587
|
}
|
|
1224
1588
|
}
|
|
@@ -1256,36 +1620,36 @@ async function runScan(openclawPath, checks) {
|
|
|
1256
1620
|
}
|
|
1257
1621
|
function runFix(openclawPath) {
|
|
1258
1622
|
const actions = [];
|
|
1259
|
-
if (
|
|
1260
|
-
const currentMode =
|
|
1623
|
+
if (fs10.existsSync(openclawPath)) {
|
|
1624
|
+
const currentMode = fs10.statSync(openclawPath).mode & 511;
|
|
1261
1625
|
const current = "0o" + currentMode.toString(8);
|
|
1262
1626
|
if (current !== "0o700") {
|
|
1263
|
-
|
|
1627
|
+
fs10.chmodSync(openclawPath, 448);
|
|
1264
1628
|
actions.push(`Fixed ${openclawPath} permissions: ${current} -> 0o700`);
|
|
1265
1629
|
}
|
|
1266
1630
|
}
|
|
1267
|
-
const configFile =
|
|
1268
|
-
if (
|
|
1269
|
-
const currentMode =
|
|
1631
|
+
const configFile = path11.join(openclawPath, "openclaw.json");
|
|
1632
|
+
if (fs10.existsSync(configFile)) {
|
|
1633
|
+
const currentMode = fs10.statSync(configFile).mode & 511;
|
|
1270
1634
|
const current = "0o" + currentMode.toString(8);
|
|
1271
1635
|
if (current !== "0o600") {
|
|
1272
|
-
|
|
1636
|
+
fs10.chmodSync(configFile, 384);
|
|
1273
1637
|
actions.push(
|
|
1274
|
-
`Fixed ${
|
|
1638
|
+
`Fixed ${path11.basename(configFile)} permissions: ${current} -> 0o600`
|
|
1275
1639
|
);
|
|
1276
1640
|
}
|
|
1277
1641
|
}
|
|
1278
|
-
const credsDir =
|
|
1279
|
-
if (
|
|
1642
|
+
const credsDir = path11.join(openclawPath, "credentials");
|
|
1643
|
+
if (fs10.existsSync(credsDir) && fs10.statSync(credsDir).isDirectory()) {
|
|
1280
1644
|
try {
|
|
1281
|
-
const entries =
|
|
1645
|
+
const entries = fs10.readdirSync(credsDir, { withFileTypes: true });
|
|
1282
1646
|
for (const entry of entries) {
|
|
1283
1647
|
if (entry.isFile()) {
|
|
1284
|
-
const filePath =
|
|
1285
|
-
const currentMode =
|
|
1648
|
+
const filePath = path11.join(credsDir, entry.name);
|
|
1649
|
+
const currentMode = fs10.statSync(filePath).mode & 511;
|
|
1286
1650
|
const current = "0o" + currentMode.toString(8);
|
|
1287
1651
|
if (current !== "0o600") {
|
|
1288
|
-
|
|
1652
|
+
fs10.chmodSync(filePath, 384);
|
|
1289
1653
|
actions.push(
|
|
1290
1654
|
`Fixed ${entry.name} permissions: ${current} -> 0o600`
|
|
1291
1655
|
);
|
|
@@ -1295,38 +1659,45 @@ function runFix(openclawPath) {
|
|
|
1295
1659
|
} catch {
|
|
1296
1660
|
}
|
|
1297
1661
|
}
|
|
1298
|
-
if (
|
|
1662
|
+
if (fs10.existsSync(configFile)) {
|
|
1299
1663
|
try {
|
|
1300
|
-
const content =
|
|
1301
|
-
const config =
|
|
1664
|
+
const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
|
|
1665
|
+
const config = JSON55.parse(content);
|
|
1302
1666
|
let modified = false;
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
sandbox
|
|
1311
|
-
sandbox.
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
docker
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
execConfig
|
|
1328
|
-
|
|
1329
|
-
|
|
1667
|
+
const dockerInstalled = isDockerAvailable();
|
|
1668
|
+
if (dockerInstalled) {
|
|
1669
|
+
if (!config.agents) config.agents = {};
|
|
1670
|
+
const agents = config.agents;
|
|
1671
|
+
if (!agents.defaults) agents.defaults = {};
|
|
1672
|
+
const defaults = agents.defaults;
|
|
1673
|
+
if (!defaults.sandbox) defaults.sandbox = {};
|
|
1674
|
+
const sandbox = defaults.sandbox;
|
|
1675
|
+
if (sandbox.mode === "off" || !sandbox.mode) {
|
|
1676
|
+
sandbox.mode = "all";
|
|
1677
|
+
sandbox.scope = "session";
|
|
1678
|
+
modified = true;
|
|
1679
|
+
actions.push('Enabled sandbox: agents.defaults.sandbox.mode = "all"');
|
|
1680
|
+
}
|
|
1681
|
+
if (!sandbox.docker) sandbox.docker = {};
|
|
1682
|
+
const docker = sandbox.docker;
|
|
1683
|
+
if (docker.network !== "none") {
|
|
1684
|
+
docker.network = "none";
|
|
1685
|
+
modified = true;
|
|
1686
|
+
actions.push('Set sandbox.docker.network = "none"');
|
|
1687
|
+
}
|
|
1688
|
+
if (!config.tools) config.tools = {};
|
|
1689
|
+
const tools = config.tools;
|
|
1690
|
+
if (!tools.exec) tools.exec = {};
|
|
1691
|
+
const execConfig = tools.exec;
|
|
1692
|
+
if (execConfig.host === "gateway") {
|
|
1693
|
+
execConfig.host = "sandbox";
|
|
1694
|
+
modified = true;
|
|
1695
|
+
actions.push('Set tools.exec.host = "sandbox"');
|
|
1696
|
+
}
|
|
1697
|
+
} else {
|
|
1698
|
+
actions.push(
|
|
1699
|
+
chalk2.yellow("Sandbox changes skipped: Docker not installed. Install Docker to enable sandbox isolation.")
|
|
1700
|
+
);
|
|
1330
1701
|
}
|
|
1331
1702
|
if (!config.logging) config.logging = {};
|
|
1332
1703
|
const loggingConfig = config.logging;
|
|
@@ -1349,42 +1720,138 @@ function runFix(openclawPath) {
|
|
|
1349
1720
|
);
|
|
1350
1721
|
}
|
|
1351
1722
|
if (modified) {
|
|
1352
|
-
|
|
1723
|
+
fs10.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
1724
|
+
}
|
|
1725
|
+
const ramGB = getTotalMemoryGB();
|
|
1726
|
+
if (ramGB < 2) {
|
|
1727
|
+
actions.push(
|
|
1728
|
+
chalk2.yellow(`Warning: System has only ${ramGB.toFixed(1)}GB RAM. Set NODE_OPTIONS="--max-old-space-size=512" to prevent OOM kills.`)
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
const hits = scanFileForKeys(configFile);
|
|
1732
|
+
if (hits.length > 0) {
|
|
1733
|
+
actions.push(
|
|
1734
|
+
`Tip: Run ${chalk2.bold("clawguard migrate-env")} to move ${hits.length} plaintext key(s) to .env`
|
|
1735
|
+
);
|
|
1353
1736
|
}
|
|
1354
1737
|
} catch (e) {
|
|
1355
1738
|
const message = e instanceof Error ? e.message : String(e);
|
|
1356
1739
|
actions.push(`Could not modify config: ${message}`);
|
|
1357
1740
|
}
|
|
1358
1741
|
}
|
|
1359
|
-
const bakFiles =
|
|
1742
|
+
const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
|
|
1360
1743
|
for (const bakFile of bakFiles) {
|
|
1361
|
-
|
|
1362
|
-
actions.push(`Deleted backup file: ${
|
|
1744
|
+
fs10.unlinkSync(bakFile);
|
|
1745
|
+
actions.push(`Deleted backup file: ${path11.basename(bakFile)}`);
|
|
1363
1746
|
}
|
|
1364
1747
|
return actions;
|
|
1365
1748
|
}
|
|
1366
|
-
function
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1749
|
+
function runMigrateEnv(openclawPath, dryRun = false) {
|
|
1750
|
+
const result = {
|
|
1751
|
+
migrations: [],
|
|
1752
|
+
envFileCreated: false,
|
|
1753
|
+
configsUpdated: []
|
|
1754
|
+
};
|
|
1755
|
+
const configFiles = [];
|
|
1756
|
+
const mainConfig = path11.join(openclawPath, "openclaw.json");
|
|
1757
|
+
if (fs10.existsSync(mainConfig)) {
|
|
1758
|
+
configFiles.push(mainConfig);
|
|
1759
|
+
}
|
|
1760
|
+
const agentsDir = path11.join(openclawPath, "agents");
|
|
1761
|
+
const modelsFiles = findFilesRecursive(agentsDir, "models.json", "exact");
|
|
1762
|
+
configFiles.push(...modelsFiles);
|
|
1763
|
+
const keyToEnvVar = /* @__PURE__ */ new Map();
|
|
1764
|
+
for (const configFile of configFiles) {
|
|
1765
|
+
try {
|
|
1766
|
+
const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
|
|
1767
|
+
const lines = content.split("\n");
|
|
1768
|
+
for (const line of lines) {
|
|
1769
|
+
if (/\$\{[A-Z_][A-Z0-9_]*\}/.test(line)) continue;
|
|
1770
|
+
for (const [keyName, pattern] of API_KEY_PATTERNS) {
|
|
1771
|
+
const globalPattern = new RegExp(
|
|
1772
|
+
pattern.source,
|
|
1773
|
+
pattern.flags + (pattern.flags.includes("g") ? "" : "g")
|
|
1774
|
+
);
|
|
1775
|
+
let match;
|
|
1776
|
+
while ((match = globalPattern.exec(line)) !== null) {
|
|
1777
|
+
const rawKey = match[0];
|
|
1778
|
+
if (keyToEnvVar.has(rawKey)) continue;
|
|
1779
|
+
const envVar = guessEnvVarName(keyName, line);
|
|
1780
|
+
keyToEnvVar.set(rawKey, envVar);
|
|
1781
|
+
result.migrations.push({
|
|
1782
|
+
file: path11.relative(openclawPath, configFile),
|
|
1783
|
+
key: rawKey.slice(0, 8) + "..." + rawKey.slice(-4),
|
|
1784
|
+
envVar
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
} catch {
|
|
1790
|
+
}
|
|
1370
1791
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1792
|
+
if (result.migrations.length === 0) {
|
|
1793
|
+
return result;
|
|
1794
|
+
}
|
|
1795
|
+
if (dryRun) {
|
|
1796
|
+
return result;
|
|
1797
|
+
}
|
|
1798
|
+
const envFilePath = path11.join(openclawPath, ".env");
|
|
1799
|
+
const existingEnv = fs10.existsSync(envFilePath) ? fs10.readFileSync(envFilePath, { encoding: "utf-8" }) : "";
|
|
1800
|
+
const existingKeys = new Set(
|
|
1801
|
+
existingEnv.split("\n").filter((l) => l.includes("=")).map((l) => l.split("=")[0].trim())
|
|
1802
|
+
);
|
|
1803
|
+
const newEnvLines = [];
|
|
1804
|
+
for (const [rawKey, envVar] of keyToEnvVar) {
|
|
1805
|
+
if (!existingKeys.has(envVar)) {
|
|
1806
|
+
newEnvLines.push(`${envVar}=${rawKey}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
if (newEnvLines.length > 0) {
|
|
1810
|
+
const separator = existingEnv.endsWith("\n") || existingEnv === "" ? "" : "\n";
|
|
1811
|
+
fs10.appendFileSync(envFilePath, separator + newEnvLines.join("\n") + "\n");
|
|
1812
|
+
result.envFileCreated = true;
|
|
1813
|
+
try {
|
|
1814
|
+
fs10.chmodSync(envFilePath, 384);
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
for (const configFile of configFiles) {
|
|
1819
|
+
try {
|
|
1820
|
+
let content = fs10.readFileSync(configFile, { encoding: "utf-8" });
|
|
1821
|
+
let modified = false;
|
|
1822
|
+
for (const [rawKey, envVar] of keyToEnvVar) {
|
|
1823
|
+
if (content.includes(rawKey)) {
|
|
1824
|
+
content = content.split(rawKey).join(`\${${envVar}}`);
|
|
1825
|
+
modified = true;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (modified) {
|
|
1829
|
+
fs10.writeFileSync(configFile, content);
|
|
1830
|
+
result.configsUpdated.push(
|
|
1831
|
+
path11.relative(openclawPath, configFile)
|
|
1832
|
+
);
|
|
1379
1833
|
}
|
|
1834
|
+
} catch {
|
|
1380
1835
|
}
|
|
1381
|
-
} catch {
|
|
1382
1836
|
}
|
|
1383
|
-
return
|
|
1837
|
+
return result;
|
|
1838
|
+
}
|
|
1839
|
+
function guessEnvVarName(keyName, contextLine) {
|
|
1840
|
+
const lower = keyName.toLowerCase();
|
|
1841
|
+
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
|
|
1842
|
+
if (lower.includes(provider)) return envVar;
|
|
1843
|
+
}
|
|
1844
|
+
if (contextLine.includes("telegram")) return "TELEGRAM_BOT_TOKEN";
|
|
1845
|
+
if (contextLine.includes("discord")) return "DISCORD_BOT_TOKEN";
|
|
1846
|
+
if (contextLine.includes("slack")) return "SLACK_BOT_TOKEN";
|
|
1847
|
+
if (contextLine.includes("brave") || contextLine.includes("webSearch"))
|
|
1848
|
+
return "BRAVE_SEARCH_API_KEY";
|
|
1849
|
+
if (contextLine.includes("stripe")) return "STRIPE_SECRET_KEY";
|
|
1850
|
+
return keyName.toUpperCase().replace(/[\s/]/g, "_").replace(/[^A-Z0-9_]/g, "");
|
|
1384
1851
|
}
|
|
1385
1852
|
|
|
1386
1853
|
// src/index.ts
|
|
1387
|
-
var VERSION2 = "0.
|
|
1854
|
+
var VERSION2 = "0.2.0";
|
|
1388
1855
|
var program = new Command();
|
|
1389
1856
|
program.name("clawguard").description("Security scanner for OpenClaw AI agent installations").version(VERSION2);
|
|
1390
1857
|
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(
|
|
@@ -1399,7 +1866,7 @@ program.command("scan").description("Scan your OpenClaw installation for securit
|
|
|
1399
1866
|
console.log("Use --path to specify the directory.");
|
|
1400
1867
|
process.exit(1);
|
|
1401
1868
|
}
|
|
1402
|
-
if (!
|
|
1869
|
+
if (!fs11.existsSync(openclawPath)) {
|
|
1403
1870
|
console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
|
|
1404
1871
|
process.exit(1);
|
|
1405
1872
|
}
|
|
@@ -1455,6 +1922,65 @@ ${actions.length} issue(s) fixed.`));
|
|
|
1455
1922
|
console.log(chalk3.green("No auto-fixable issues found.\n"));
|
|
1456
1923
|
}
|
|
1457
1924
|
});
|
|
1925
|
+
program.command("migrate-env").description(
|
|
1926
|
+
"Migrate plaintext API keys from config files to .env with ${REF} substitution"
|
|
1927
|
+
).option("-p, --path <path>", "Path to OpenClaw directory").option("--dry-run", "Show what would be migrated without making changes").action((options) => {
|
|
1928
|
+
const openclawPath = options.path ?? detectOpenclawPath();
|
|
1929
|
+
if (!openclawPath) {
|
|
1930
|
+
console.log(chalk3.red("Could not find OpenClaw installation."));
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
}
|
|
1933
|
+
if (!fs11.existsSync(openclawPath)) {
|
|
1934
|
+
console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
|
|
1935
|
+
process.exit(1);
|
|
1936
|
+
}
|
|
1937
|
+
const dryRun = options.dryRun ?? false;
|
|
1938
|
+
if (dryRun) {
|
|
1939
|
+
console.log(chalk3.cyan("\n-- DRY RUN (no files will be modified) --\n"));
|
|
1940
|
+
}
|
|
1941
|
+
const result = runMigrateEnv(openclawPath, dryRun);
|
|
1942
|
+
if (result.migrations.length === 0) {
|
|
1943
|
+
console.log(chalk3.green("No plaintext API keys found to migrate."));
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
console.log(
|
|
1947
|
+
chalk3.bold(`
|
|
1948
|
+
Found ${result.migrations.length} key(s) to migrate:
|
|
1949
|
+
`)
|
|
1950
|
+
);
|
|
1951
|
+
for (const m of result.migrations) {
|
|
1952
|
+
console.log(
|
|
1953
|
+
` ${chalk3.cyan(m.envVar)} <- ${m.key} (${chalk3.dim(m.file)})`
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
if (dryRun) {
|
|
1957
|
+
console.log(
|
|
1958
|
+
chalk3.cyan("\nRun without --dry-run to apply these changes.")
|
|
1959
|
+
);
|
|
1960
|
+
} else {
|
|
1961
|
+
console.log();
|
|
1962
|
+
if (result.envFileCreated) {
|
|
1963
|
+
console.log(
|
|
1964
|
+
chalk3.green(" Created/updated .env file (chmod 600)")
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
for (const f of result.configsUpdated) {
|
|
1968
|
+
console.log(
|
|
1969
|
+
chalk3.green(` Updated ${f} with \${ENV_VAR} references`)
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
console.log(
|
|
1973
|
+
chalk3.green(
|
|
1974
|
+
`
|
|
1975
|
+
${result.migrations.length} key(s) migrated successfully.`
|
|
1976
|
+
)
|
|
1977
|
+
);
|
|
1978
|
+
console.log(
|
|
1979
|
+
`Run ${chalk3.bold("clawguard scan")} to verify.
|
|
1980
|
+
`
|
|
1981
|
+
);
|
|
1982
|
+
}
|
|
1983
|
+
});
|
|
1458
1984
|
program.command("version").description("Show ClawGuard version").action(() => {
|
|
1459
1985
|
console.log(`ClawGuard v${VERSION2}`);
|
|
1460
1986
|
});
|