clawguard-cli 0.1.0 → 0.2.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 +710 -214
- 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,27 +336,71 @@ 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
|
}
|
|
@@ -334,17 +408,17 @@ function checkCredentials(openclawPath) {
|
|
|
334
408
|
}
|
|
335
409
|
}
|
|
336
410
|
const envFiles = [
|
|
337
|
-
|
|
338
|
-
|
|
411
|
+
path2.join(openclawPath, ".env"),
|
|
412
|
+
path2.join(openclawPath, "workspace", ".env")
|
|
339
413
|
];
|
|
340
414
|
for (const envFile of envFiles) {
|
|
341
|
-
if (
|
|
415
|
+
if (fs2.existsSync(envFile)) {
|
|
342
416
|
configFiles.push(envFile);
|
|
343
417
|
}
|
|
344
418
|
}
|
|
345
419
|
const allHits = [];
|
|
346
420
|
for (const filepath of configFiles) {
|
|
347
|
-
if (
|
|
421
|
+
if (fs2.existsSync(filepath)) {
|
|
348
422
|
const hits = scanFileForKeys(filepath);
|
|
349
423
|
for (const { keyName, masked, lineNum } of hits) {
|
|
350
424
|
const relPath = getRelativePath(filepath, openclawPath);
|
|
@@ -365,7 +439,7 @@ function checkCredentials(openclawPath) {
|
|
|
365
439
|
);
|
|
366
440
|
}
|
|
367
441
|
const bakWithKeys = [];
|
|
368
|
-
const bakFiles = findFilesRecursive(openclawPath, ".bak");
|
|
442
|
+
const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
|
|
369
443
|
for (const bakFile of bakFiles) {
|
|
370
444
|
const hits = scanFileForKeys(bakFile);
|
|
371
445
|
if (hits.length > 0) {
|
|
@@ -384,11 +458,11 @@ function checkCredentials(openclawPath) {
|
|
|
384
458
|
);
|
|
385
459
|
}
|
|
386
460
|
const transcriptHits = [];
|
|
387
|
-
if (
|
|
388
|
-
const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl");
|
|
461
|
+
if (fs2.existsSync(agentsDir) && fs2.statSync(agentsDir).isDirectory()) {
|
|
462
|
+
const jsonlFiles = findFilesRecursive(agentsDir, ".jsonl", "extension");
|
|
389
463
|
for (const jsonlFile of jsonlFiles) {
|
|
390
464
|
try {
|
|
391
|
-
const content =
|
|
465
|
+
const content = fs2.readFileSync(jsonlFile, { encoding: "utf-8" });
|
|
392
466
|
const lines = content.split("\n");
|
|
393
467
|
let found = false;
|
|
394
468
|
for (let i = 0; i < Math.min(lines.length, 501); i++) {
|
|
@@ -419,11 +493,11 @@ function checkCredentials(openclawPath) {
|
|
|
419
493
|
})
|
|
420
494
|
);
|
|
421
495
|
}
|
|
422
|
-
const configFile =
|
|
423
|
-
if (
|
|
496
|
+
const configFile = path2.join(openclawPath, "openclaw.json");
|
|
497
|
+
if (fs2.existsSync(configFile)) {
|
|
424
498
|
try {
|
|
425
|
-
const content =
|
|
426
|
-
const config =
|
|
499
|
+
const content = fs2.readFileSync(configFile, { encoding: "utf-8" });
|
|
500
|
+
const config = JSON52.parse(content);
|
|
427
501
|
const loggingConfig = config.logging ?? {};
|
|
428
502
|
const redact = loggingConfig.redactSensitive;
|
|
429
503
|
if (redact === void 0 || redact === null || redact === "off") {
|
|
@@ -442,31 +516,12 @@ function checkCredentials(openclawPath) {
|
|
|
442
516
|
}
|
|
443
517
|
return findings;
|
|
444
518
|
}
|
|
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
519
|
|
|
465
520
|
// src/checks/gateway.ts
|
|
466
|
-
import
|
|
521
|
+
import fs3 from "fs";
|
|
467
522
|
import net from "net";
|
|
468
|
-
import
|
|
469
|
-
import
|
|
523
|
+
import path3 from "path";
|
|
524
|
+
import JSON53 from "json5";
|
|
470
525
|
function checkPortExposed(port) {
|
|
471
526
|
return new Promise((resolve) => {
|
|
472
527
|
const socket = new net.Socket();
|
|
@@ -492,8 +547,8 @@ function checkPortExposed(port) {
|
|
|
492
547
|
}
|
|
493
548
|
async function checkGateway(openclawPath) {
|
|
494
549
|
const findings = [];
|
|
495
|
-
const configFile =
|
|
496
|
-
if (!
|
|
550
|
+
const configFile = path3.join(openclawPath, "openclaw.json");
|
|
551
|
+
if (!fs3.existsSync(configFile)) {
|
|
497
552
|
findings.push(
|
|
498
553
|
createFinding({
|
|
499
554
|
severity: Severity.INFO,
|
|
@@ -506,8 +561,8 @@ async function checkGateway(openclawPath) {
|
|
|
506
561
|
}
|
|
507
562
|
let config;
|
|
508
563
|
try {
|
|
509
|
-
const content =
|
|
510
|
-
config =
|
|
564
|
+
const content = fs3.readFileSync(configFile, { encoding: "utf-8" });
|
|
565
|
+
config = JSON53.parse(content);
|
|
511
566
|
} catch {
|
|
512
567
|
findings.push(
|
|
513
568
|
createFinding({
|
|
@@ -576,32 +631,83 @@ async function checkGateway(openclawPath) {
|
|
|
576
631
|
})
|
|
577
632
|
);
|
|
578
633
|
}
|
|
634
|
+
const ramGB = getTotalMemoryGB();
|
|
635
|
+
const nodeOptions = process.env.NODE_OPTIONS ?? "";
|
|
636
|
+
if (ramGB < 2) {
|
|
637
|
+
if (!nodeOptions.includes("max-old-space-size")) {
|
|
638
|
+
findings.push(
|
|
639
|
+
createFinding({
|
|
640
|
+
severity: Severity.HIGH,
|
|
641
|
+
title: `System has only ${ramGB.toFixed(1)}GB RAM without NODE_OPTIONS set`,
|
|
642
|
+
details: [
|
|
643
|
+
`Total RAM: ${ramGB.toFixed(1)}GB`,
|
|
644
|
+
"OpenClaw may be killed by OOM without memory limits",
|
|
645
|
+
"NODE_OPTIONS environment variable is not set"
|
|
646
|
+
],
|
|
647
|
+
fix: 'Set NODE_OPTIONS="--max-old-space-size=512" in your service file or shell profile',
|
|
648
|
+
category: "gateway"
|
|
649
|
+
})
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (process.platform === "linux") {
|
|
654
|
+
const serviceFiles = [
|
|
655
|
+
"/etc/systemd/system/openclaw.service",
|
|
656
|
+
"/etc/systemd/system/openclaw-gateway.service"
|
|
657
|
+
];
|
|
658
|
+
for (const serviceFile of serviceFiles) {
|
|
659
|
+
if (!fs3.existsSync(serviceFile)) continue;
|
|
660
|
+
try {
|
|
661
|
+
const serviceContent = fs3.readFileSync(serviceFile, { encoding: "utf-8" });
|
|
662
|
+
if (ramGB < 2 && !serviceContent.includes("NODE_OPTIONS")) {
|
|
663
|
+
findings.push(
|
|
664
|
+
createFinding({
|
|
665
|
+
severity: Severity.HIGH,
|
|
666
|
+
title: `Systemd service missing NODE_OPTIONS (${path3.basename(serviceFile)})`,
|
|
667
|
+
details: [
|
|
668
|
+
`Low RAM system (${ramGB.toFixed(1)}GB) but service has no NODE_OPTIONS`,
|
|
669
|
+
"Add Environment=NODE_OPTIONS=--max-old-space-size=512 to [Service] section"
|
|
670
|
+
],
|
|
671
|
+
fix: `Edit ${serviceFile} and add NODE_OPTIONS, then systemctl daemon-reload`,
|
|
672
|
+
category: "gateway"
|
|
673
|
+
})
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
if (serviceContent.includes("Restart=always") && !serviceContent.includes("RestartSec")) {
|
|
677
|
+
findings.push(
|
|
678
|
+
createFinding({
|
|
679
|
+
severity: Severity.MEDIUM,
|
|
680
|
+
title: `Systemd service has Restart=always without RestartSec (${path3.basename(serviceFile)})`,
|
|
681
|
+
details: [
|
|
682
|
+
"Service will restart immediately on crash, potentially causing a restart storm",
|
|
683
|
+
"Add RestartSec=5 to [Service] section to prevent rapid restarts"
|
|
684
|
+
],
|
|
685
|
+
fix: `Add RestartSec=5 to ${serviceFile} [Service] section`,
|
|
686
|
+
category: "gateway"
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
579
694
|
return findings;
|
|
580
695
|
}
|
|
581
696
|
|
|
582
697
|
// 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
|
-
}
|
|
698
|
+
import fs4 from "fs";
|
|
699
|
+
import path4 from "path";
|
|
700
|
+
import JSON54 from "json5";
|
|
595
701
|
function checkSandbox(openclawPath) {
|
|
596
702
|
const findings = [];
|
|
597
|
-
const configFile =
|
|
598
|
-
if (!
|
|
703
|
+
const configFile = path4.join(openclawPath, "openclaw.json");
|
|
704
|
+
if (!fs4.existsSync(configFile)) {
|
|
599
705
|
return findings;
|
|
600
706
|
}
|
|
601
707
|
let config;
|
|
602
708
|
try {
|
|
603
|
-
const content =
|
|
604
|
-
config =
|
|
709
|
+
const content = fs4.readFileSync(configFile, { encoding: "utf-8" });
|
|
710
|
+
config = JSON54.parse(content);
|
|
605
711
|
} catch {
|
|
606
712
|
return findings;
|
|
607
713
|
}
|
|
@@ -670,7 +776,22 @@ function checkSandbox(openclawPath) {
|
|
|
670
776
|
);
|
|
671
777
|
}
|
|
672
778
|
const execSecurity = execConfig.security;
|
|
673
|
-
|
|
779
|
+
const validExecSecurity = ["deny", "allowlist", "full"];
|
|
780
|
+
if (execSecurity && !validExecSecurity.includes(execSecurity)) {
|
|
781
|
+
findings.push(
|
|
782
|
+
createFinding({
|
|
783
|
+
severity: Severity.HIGH,
|
|
784
|
+
title: `Invalid tools.exec.security value: "${execSecurity}"`,
|
|
785
|
+
details: [
|
|
786
|
+
`tools.exec.security = "${execSecurity}"`,
|
|
787
|
+
`Valid values: ${validExecSecurity.join(", ")}`,
|
|
788
|
+
"Invalid value may cause unpredictable behavior"
|
|
789
|
+
],
|
|
790
|
+
fix: 'Set tools.exec.security to "deny", "allowlist", or "full" in openclaw.json',
|
|
791
|
+
category: "sandbox"
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
} else if (execSecurity !== "allowlist") {
|
|
674
795
|
findings.push(
|
|
675
796
|
createFinding({
|
|
676
797
|
severity: Severity.MEDIUM,
|
|
@@ -684,6 +805,26 @@ function checkSandbox(openclawPath) {
|
|
|
684
805
|
})
|
|
685
806
|
);
|
|
686
807
|
}
|
|
808
|
+
const commands = config.commands ?? {};
|
|
809
|
+
const nativeSkills = commands.nativeSkills;
|
|
810
|
+
if (nativeSkills !== void 0 && nativeSkills !== null) {
|
|
811
|
+
const validNativeSkills = [false, "auto", "off"];
|
|
812
|
+
if (!validNativeSkills.includes(nativeSkills)) {
|
|
813
|
+
findings.push(
|
|
814
|
+
createFinding({
|
|
815
|
+
severity: Severity.MEDIUM,
|
|
816
|
+
title: `Invalid commands.nativeSkills value: "${String(nativeSkills)}"`,
|
|
817
|
+
details: [
|
|
818
|
+
`commands.nativeSkills = "${String(nativeSkills)}"`,
|
|
819
|
+
'Valid values: false, "auto", "off"',
|
|
820
|
+
"Invalid value may cause skill execution issues"
|
|
821
|
+
],
|
|
822
|
+
fix: 'Set commands.nativeSkills to false, "auto", or "off" in openclaw.json',
|
|
823
|
+
category: "sandbox"
|
|
824
|
+
})
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
687
828
|
if (dockerAvailable) {
|
|
688
829
|
findings.push(
|
|
689
830
|
createFinding({
|
|
@@ -700,48 +841,44 @@ function checkSandbox(openclawPath) {
|
|
|
700
841
|
}
|
|
701
842
|
|
|
702
843
|
// src/checks/permissions.ts
|
|
703
|
-
import
|
|
704
|
-
import
|
|
844
|
+
import fs5 from "fs";
|
|
845
|
+
import path5 from "path";
|
|
705
846
|
function getPermissionOctal(filepath) {
|
|
706
|
-
const stats =
|
|
847
|
+
const stats = fs5.statSync(filepath);
|
|
707
848
|
return "0o" + (stats.mode & 511).toString(8);
|
|
708
849
|
}
|
|
709
850
|
function isWorldReadable(filepath) {
|
|
710
|
-
const stats =
|
|
851
|
+
const stats = fs5.statSync(filepath);
|
|
711
852
|
return (stats.mode & 4) !== 0;
|
|
712
853
|
}
|
|
713
854
|
function isGroupReadable(filepath) {
|
|
714
|
-
const stats =
|
|
855
|
+
const stats = fs5.statSync(filepath);
|
|
715
856
|
return (stats.mode & 32) !== 0;
|
|
716
857
|
}
|
|
717
|
-
function getRelativePath2(filePath, basePath) {
|
|
718
|
-
const parentDir = path4.dirname(basePath);
|
|
719
|
-
return path4.relative(parentDir, filePath);
|
|
720
|
-
}
|
|
721
858
|
function checkPermissions(openclawPath) {
|
|
722
859
|
const findings = [];
|
|
723
860
|
const tooOpen = [];
|
|
724
|
-
if (
|
|
861
|
+
if (fs5.existsSync(openclawPath)) {
|
|
725
862
|
const perms = getPermissionOctal(openclawPath);
|
|
726
863
|
if (isWorldReadable(openclawPath) || isGroupReadable(openclawPath)) {
|
|
727
864
|
tooOpen.push(`${openclawPath} is ${perms} (should be 0o700)`);
|
|
728
865
|
}
|
|
729
866
|
}
|
|
730
867
|
const sensitiveFiles = [
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
868
|
+
path5.join(openclawPath, "openclaw.json"),
|
|
869
|
+
path5.join(openclawPath, ".env"),
|
|
870
|
+
path5.join(openclawPath, "credentials", "profiles.json")
|
|
734
871
|
];
|
|
735
|
-
const agentsDir =
|
|
736
|
-
if (
|
|
737
|
-
const authFiles =
|
|
872
|
+
const agentsDir = path5.join(openclawPath, "agents");
|
|
873
|
+
if (fs5.existsSync(agentsDir) && fs5.statSync(agentsDir).isDirectory()) {
|
|
874
|
+
const authFiles = findFilesRecursive(agentsDir, "auth-profiles.json", "exact");
|
|
738
875
|
sensitiveFiles.push(...authFiles);
|
|
739
876
|
}
|
|
740
877
|
for (const filepath of sensitiveFiles) {
|
|
741
|
-
if (
|
|
878
|
+
if (fs5.existsSync(filepath)) {
|
|
742
879
|
const perms = getPermissionOctal(filepath);
|
|
743
880
|
if (isWorldReadable(filepath) || isGroupReadable(filepath)) {
|
|
744
|
-
const relPath =
|
|
881
|
+
const relPath = getRelativePath(filepath, openclawPath);
|
|
745
882
|
tooOpen.push(`${relPath} is ${perms} (should be 0o600)`);
|
|
746
883
|
}
|
|
747
884
|
}
|
|
@@ -759,31 +896,12 @@ function checkPermissions(openclawPath) {
|
|
|
759
896
|
}
|
|
760
897
|
return findings;
|
|
761
898
|
}
|
|
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
899
|
|
|
782
900
|
// src/checks/version.ts
|
|
783
901
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
784
|
-
import
|
|
785
|
-
import
|
|
786
|
-
import
|
|
902
|
+
import fs6 from "fs";
|
|
903
|
+
import path6 from "path";
|
|
904
|
+
import os2 from "os";
|
|
787
905
|
var CVES = [
|
|
788
906
|
{
|
|
789
907
|
id: "CVE-2026-25253",
|
|
@@ -870,8 +988,8 @@ function checkVersion(_openclawPath) {
|
|
|
870
988
|
}
|
|
871
989
|
} else {
|
|
872
990
|
const pkgPaths = [
|
|
873
|
-
|
|
874
|
-
|
|
991
|
+
path6.join(
|
|
992
|
+
os2.homedir(),
|
|
875
993
|
".bun",
|
|
876
994
|
"install",
|
|
877
995
|
"global",
|
|
@@ -883,9 +1001,9 @@ function checkVersion(_openclawPath) {
|
|
|
883
1001
|
];
|
|
884
1002
|
let found = false;
|
|
885
1003
|
for (const pkgPath of pkgPaths) {
|
|
886
|
-
if (
|
|
1004
|
+
if (fs6.existsSync(pkgPath)) {
|
|
887
1005
|
try {
|
|
888
|
-
const content =
|
|
1006
|
+
const content = fs6.readFileSync(pkgPath, { encoding: "utf-8" });
|
|
889
1007
|
const pkg = JSON.parse(content);
|
|
890
1008
|
ocVersion = pkg.version ?? "unknown";
|
|
891
1009
|
findings.push(
|
|
@@ -943,11 +1061,11 @@ function checkVersion(_openclawPath) {
|
|
|
943
1061
|
}
|
|
944
1062
|
|
|
945
1063
|
// src/checks/skills.ts
|
|
946
|
-
import
|
|
947
|
-
import
|
|
1064
|
+
import fs7 from "fs";
|
|
1065
|
+
import path7 from "path";
|
|
948
1066
|
import yaml from "js-yaml";
|
|
949
1067
|
function parseSkillMd(skillPath) {
|
|
950
|
-
const content =
|
|
1068
|
+
const content = fs7.readFileSync(skillPath, { encoding: "utf-8" });
|
|
951
1069
|
let frontmatter = {};
|
|
952
1070
|
let body = content;
|
|
953
1071
|
if (content.startsWith("---")) {
|
|
@@ -1044,18 +1162,18 @@ function checkSkillMalicious(name, body) {
|
|
|
1044
1162
|
function checkSkills(openclawPath) {
|
|
1045
1163
|
const findings = [];
|
|
1046
1164
|
const skillDirs = [
|
|
1047
|
-
|
|
1048
|
-
|
|
1165
|
+
path7.join(openclawPath, "workspace", "skills"),
|
|
1166
|
+
path7.join(openclawPath, "skills")
|
|
1049
1167
|
];
|
|
1050
1168
|
let totalSkills = 0;
|
|
1051
1169
|
let flaggedSkills = 0;
|
|
1052
1170
|
for (const skillsRoot of skillDirs) {
|
|
1053
|
-
if (!
|
|
1171
|
+
if (!fs7.existsSync(skillsRoot) || !fs7.statSync(skillsRoot).isDirectory()) {
|
|
1054
1172
|
continue;
|
|
1055
1173
|
}
|
|
1056
1174
|
let entries;
|
|
1057
1175
|
try {
|
|
1058
|
-
entries =
|
|
1176
|
+
entries = fs7.readdirSync(skillsRoot, { withFileTypes: true });
|
|
1059
1177
|
} catch {
|
|
1060
1178
|
continue;
|
|
1061
1179
|
}
|
|
@@ -1063,8 +1181,8 @@ function checkSkills(openclawPath) {
|
|
|
1063
1181
|
if (!entry.isDirectory()) {
|
|
1064
1182
|
continue;
|
|
1065
1183
|
}
|
|
1066
|
-
const skillMdPath =
|
|
1067
|
-
if (!
|
|
1184
|
+
const skillMdPath = path7.join(skillsRoot, entry.name, "SKILL.md");
|
|
1185
|
+
if (!fs7.existsSync(skillMdPath)) {
|
|
1068
1186
|
continue;
|
|
1069
1187
|
}
|
|
1070
1188
|
totalSkills++;
|
|
@@ -1079,7 +1197,7 @@ function checkSkills(openclawPath) {
|
|
|
1079
1197
|
details: [
|
|
1080
1198
|
"This publisher name is known to be associated with malicious skills"
|
|
1081
1199
|
],
|
|
1082
|
-
fix: `REMOVE IMMEDIATELY: rm -rf ${
|
|
1200
|
+
fix: `REMOVE IMMEDIATELY: rm -rf ${path7.join(skillsRoot, entry.name)}`,
|
|
1083
1201
|
category: "skills"
|
|
1084
1202
|
})
|
|
1085
1203
|
);
|
|
@@ -1119,23 +1237,23 @@ function checkSkills(openclawPath) {
|
|
|
1119
1237
|
}
|
|
1120
1238
|
|
|
1121
1239
|
// src/checks/memory.ts
|
|
1122
|
-
import
|
|
1123
|
-
import
|
|
1240
|
+
import fs8 from "fs";
|
|
1241
|
+
import path8 from "path";
|
|
1124
1242
|
function checkMemory(openclawPath) {
|
|
1125
1243
|
const findings = [];
|
|
1126
|
-
const workspace =
|
|
1127
|
-
if (!
|
|
1244
|
+
const workspace = path8.join(openclawPath, "workspace");
|
|
1245
|
+
if (!fs8.existsSync(workspace) || !fs8.statSync(workspace).isDirectory()) {
|
|
1128
1246
|
return findings;
|
|
1129
1247
|
}
|
|
1130
1248
|
const identityFiles = [
|
|
1131
|
-
|
|
1132
|
-
|
|
1249
|
+
path8.join(workspace, "SOUL.md"),
|
|
1250
|
+
path8.join(workspace, "IDENTITY.md")
|
|
1133
1251
|
];
|
|
1134
1252
|
for (const filepath of identityFiles) {
|
|
1135
|
-
if (!
|
|
1253
|
+
if (!fs8.existsSync(filepath)) {
|
|
1136
1254
|
continue;
|
|
1137
1255
|
}
|
|
1138
|
-
const content =
|
|
1256
|
+
const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
|
|
1139
1257
|
const detected = [];
|
|
1140
1258
|
for (const [patternName, pattern] of MEMORY_POISONING_PATTERNS) {
|
|
1141
1259
|
const matches = content.match(pattern);
|
|
@@ -1148,7 +1266,7 @@ function checkMemory(openclawPath) {
|
|
|
1148
1266
|
findings.push(
|
|
1149
1267
|
createFinding({
|
|
1150
1268
|
severity: Severity.HIGH,
|
|
1151
|
-
title: `Potential memory poisoning in ${
|
|
1269
|
+
title: `Potential memory poisoning in ${path8.basename(filepath)}`,
|
|
1152
1270
|
details: detected,
|
|
1153
1271
|
fix: `Review ${filepath} for injected instructions and restore from backup`,
|
|
1154
1272
|
category: "memory"
|
|
@@ -1156,14 +1274,14 @@ function checkMemory(openclawPath) {
|
|
|
1156
1274
|
);
|
|
1157
1275
|
}
|
|
1158
1276
|
}
|
|
1159
|
-
const memoryFiles = [
|
|
1160
|
-
const memoryDir =
|
|
1161
|
-
if (
|
|
1277
|
+
const memoryFiles = [path8.join(workspace, "MEMORY.md")];
|
|
1278
|
+
const memoryDir = path8.join(workspace, "memory");
|
|
1279
|
+
if (fs8.existsSync(memoryDir) && fs8.statSync(memoryDir).isDirectory()) {
|
|
1162
1280
|
try {
|
|
1163
|
-
const entries =
|
|
1281
|
+
const entries = fs8.readdirSync(memoryDir);
|
|
1164
1282
|
for (const entry of entries) {
|
|
1165
1283
|
if (entry.endsWith(".md")) {
|
|
1166
|
-
memoryFiles.push(
|
|
1284
|
+
memoryFiles.push(path8.join(memoryDir, entry));
|
|
1167
1285
|
}
|
|
1168
1286
|
}
|
|
1169
1287
|
} catch {
|
|
@@ -1171,13 +1289,13 @@ function checkMemory(openclawPath) {
|
|
|
1171
1289
|
}
|
|
1172
1290
|
const leakedIn = [];
|
|
1173
1291
|
for (const filepath of memoryFiles) {
|
|
1174
|
-
if (!
|
|
1292
|
+
if (!fs8.existsSync(filepath)) {
|
|
1175
1293
|
continue;
|
|
1176
1294
|
}
|
|
1177
|
-
const content =
|
|
1295
|
+
const content = fs8.readFileSync(filepath, { encoding: "utf-8" });
|
|
1178
1296
|
for (const [keyName, pattern] of API_KEY_PATTERNS) {
|
|
1179
1297
|
if (pattern.test(content)) {
|
|
1180
|
-
leakedIn.push(`${
|
|
1298
|
+
leakedIn.push(`${path8.basename(filepath)} contains ${keyName}`);
|
|
1181
1299
|
break;
|
|
1182
1300
|
}
|
|
1183
1301
|
}
|
|
@@ -1196,6 +1314,220 @@ function checkMemory(openclawPath) {
|
|
|
1196
1314
|
return findings;
|
|
1197
1315
|
}
|
|
1198
1316
|
|
|
1317
|
+
// src/checks/agents.ts
|
|
1318
|
+
import fs9 from "fs";
|
|
1319
|
+
import path9 from "path";
|
|
1320
|
+
function checkAgents(openclawPath) {
|
|
1321
|
+
const findings = [];
|
|
1322
|
+
const agentsDir = path9.join(openclawPath, "agents");
|
|
1323
|
+
if (!fs9.existsSync(agentsDir) || !fs9.statSync(agentsDir).isDirectory()) {
|
|
1324
|
+
return findings;
|
|
1325
|
+
}
|
|
1326
|
+
const globalConfig = parseConfig(
|
|
1327
|
+
path9.join(openclawPath, "openclaw.json")
|
|
1328
|
+
);
|
|
1329
|
+
const globalModel = getGlobalModel(globalConfig);
|
|
1330
|
+
let agentCount = 0;
|
|
1331
|
+
let issueCount = 0;
|
|
1332
|
+
let entries;
|
|
1333
|
+
try {
|
|
1334
|
+
entries = fs9.readdirSync(agentsDir, { withFileTypes: true });
|
|
1335
|
+
} catch {
|
|
1336
|
+
return findings;
|
|
1337
|
+
}
|
|
1338
|
+
for (const entry of entries) {
|
|
1339
|
+
if (!entry.isDirectory()) continue;
|
|
1340
|
+
const modelsJsonPath = path9.join(
|
|
1341
|
+
agentsDir,
|
|
1342
|
+
entry.name,
|
|
1343
|
+
"agent",
|
|
1344
|
+
"models.json"
|
|
1345
|
+
);
|
|
1346
|
+
if (!fs9.existsSync(modelsJsonPath)) continue;
|
|
1347
|
+
agentCount++;
|
|
1348
|
+
const agentName = entry.name;
|
|
1349
|
+
const keyHits = scanFileForKeys(modelsJsonPath);
|
|
1350
|
+
if (keyHits.length > 0) {
|
|
1351
|
+
issueCount++;
|
|
1352
|
+
const details = keyHits.slice(0, 5).map(
|
|
1353
|
+
(h) => `Line ${h.lineNum}: ${h.keyName} (${h.masked})`
|
|
1354
|
+
);
|
|
1355
|
+
findings.push(
|
|
1356
|
+
createFinding({
|
|
1357
|
+
severity: Severity.CRITICAL,
|
|
1358
|
+
title: `Agent '${agentName}' has plaintext API keys in models.json`,
|
|
1359
|
+
details,
|
|
1360
|
+
fix: `Replace raw keys with env var refs: "apiKey": "\${${PROVIDER_ENV_MAP.anthropic}}"`,
|
|
1361
|
+
category: "agents"
|
|
1362
|
+
})
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const content = fs9.readFileSync(modelsJsonPath, {
|
|
1367
|
+
encoding: "utf-8"
|
|
1368
|
+
});
|
|
1369
|
+
const hasEnvRefs = ENV_VAR_PATTERN.test(content);
|
|
1370
|
+
if (keyHits.length > 0 && !hasEnvRefs) {
|
|
1371
|
+
findings.push(
|
|
1372
|
+
createFinding({
|
|
1373
|
+
severity: Severity.HIGH,
|
|
1374
|
+
title: `Agent '${agentName}' uses raw keys instead of env var references`,
|
|
1375
|
+
details: [
|
|
1376
|
+
"models.json contains no ${ENV_VAR} references",
|
|
1377
|
+
"All API keys should use environment variable substitution"
|
|
1378
|
+
],
|
|
1379
|
+
fix: 'Run "clawguard migrate-env" to auto-migrate keys to .env',
|
|
1380
|
+
category: "agents"
|
|
1381
|
+
})
|
|
1382
|
+
);
|
|
1383
|
+
issueCount++;
|
|
1384
|
+
}
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
if (globalModel) {
|
|
1388
|
+
const agentModel = getAgentModel(modelsJsonPath);
|
|
1389
|
+
if (agentModel && agentModel !== globalModel) {
|
|
1390
|
+
findings.push(
|
|
1391
|
+
createFinding({
|
|
1392
|
+
severity: Severity.MEDIUM,
|
|
1393
|
+
title: `Agent '${agentName}' model differs from global config`,
|
|
1394
|
+
details: [
|
|
1395
|
+
`Global model: ${globalModel}`,
|
|
1396
|
+
`Agent model: ${agentModel}`,
|
|
1397
|
+
"This agent overrides the global model setting (config shadow)"
|
|
1398
|
+
],
|
|
1399
|
+
fix: "Verify this is intentional. Remove agent-level model to use global setting.",
|
|
1400
|
+
category: "agents"
|
|
1401
|
+
})
|
|
1402
|
+
);
|
|
1403
|
+
issueCount++;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
if (agentCount > 0 && issueCount === 0) {
|
|
1408
|
+
findings.push(
|
|
1409
|
+
createFinding({
|
|
1410
|
+
severity: Severity.INFO,
|
|
1411
|
+
title: `${agentCount} agent config(s) scanned - no issues found`,
|
|
1412
|
+
category: "agents"
|
|
1413
|
+
})
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return findings;
|
|
1417
|
+
}
|
|
1418
|
+
function getGlobalModel(config) {
|
|
1419
|
+
if (!config) return null;
|
|
1420
|
+
try {
|
|
1421
|
+
const models = config.models;
|
|
1422
|
+
if (!models) return null;
|
|
1423
|
+
const defaultModel = models.default;
|
|
1424
|
+
if (defaultModel) return defaultModel;
|
|
1425
|
+
const providers = models.providers;
|
|
1426
|
+
if (providers) {
|
|
1427
|
+
for (const provider of Object.values(providers)) {
|
|
1428
|
+
if (provider.model) return String(provider.model);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
function getAgentModel(modelsJsonPath) {
|
|
1436
|
+
try {
|
|
1437
|
+
const content = fs9.readFileSync(modelsJsonPath, { encoding: "utf-8" });
|
|
1438
|
+
const data = JSON.parse(content);
|
|
1439
|
+
if (data.model) return String(data.model);
|
|
1440
|
+
if (data.default) return String(data.default);
|
|
1441
|
+
const providers = data.providers;
|
|
1442
|
+
if (providers) {
|
|
1443
|
+
for (const provider of Object.values(providers)) {
|
|
1444
|
+
if (provider.model) return String(provider.model);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
} catch {
|
|
1448
|
+
}
|
|
1449
|
+
return null;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
// src/checks/providers.ts
|
|
1453
|
+
import path10 from "path";
|
|
1454
|
+
function checkProviders(openclawPath) {
|
|
1455
|
+
const findings = [];
|
|
1456
|
+
const config = parseConfig(path10.join(openclawPath, "openclaw.json"));
|
|
1457
|
+
if (!config) return findings;
|
|
1458
|
+
const models = config.models ?? {};
|
|
1459
|
+
const providers = models.providers ?? {};
|
|
1460
|
+
const providerSummary = [];
|
|
1461
|
+
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
1462
|
+
const lowerName = providerName.toLowerCase();
|
|
1463
|
+
providerSummary.push(
|
|
1464
|
+
`${providerName}: model=${String(providerConfig.model ?? "default")}`
|
|
1465
|
+
);
|
|
1466
|
+
const limits = PROVIDER_LIMITS[lowerName];
|
|
1467
|
+
if (limits && limits.tpm < OPENCLAW_SYSTEM_PROMPT_TOKENS) {
|
|
1468
|
+
findings.push(
|
|
1469
|
+
createFinding({
|
|
1470
|
+
severity: Severity.HIGH,
|
|
1471
|
+
title: `Provider '${providerName}' TPM too low for system prompt`,
|
|
1472
|
+
details: [
|
|
1473
|
+
`${limits.note}`,
|
|
1474
|
+
`OpenClaw system prompt requires ~${OPENCLAW_SYSTEM_PROMPT_TOKENS} tokens`,
|
|
1475
|
+
`Provider free tier allows only ${limits.tpm} TPM`,
|
|
1476
|
+
"System prompt alone exceeds the per-minute token limit"
|
|
1477
|
+
],
|
|
1478
|
+
fix: `Upgrade to a paid ${providerName} plan or switch to a provider with higher limits`,
|
|
1479
|
+
category: "providers"
|
|
1480
|
+
})
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
if (limits && limits.rpm < 10) {
|
|
1484
|
+
findings.push(
|
|
1485
|
+
createFinding({
|
|
1486
|
+
severity: Severity.MEDIUM,
|
|
1487
|
+
title: `Provider '${providerName}' has very low rate limits (${limits.rpm} RPM)`,
|
|
1488
|
+
details: [
|
|
1489
|
+
`${limits.note}`,
|
|
1490
|
+
"Low RPM causes frequent rate limit errors during conversations"
|
|
1491
|
+
],
|
|
1492
|
+
fix: `Consider upgrading ${providerName} plan or using a different provider`,
|
|
1493
|
+
category: "providers"
|
|
1494
|
+
})
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const tools = config.tools ?? {};
|
|
1499
|
+
const webSearch = tools.webSearch ?? tools.web_search ?? {};
|
|
1500
|
+
const webSearchEnabled = webSearch.enabled !== false && Object.keys(webSearch).length > 0;
|
|
1501
|
+
if (webSearchEnabled) {
|
|
1502
|
+
const apiKey = webSearch.apiKey ?? webSearch.api_key;
|
|
1503
|
+
if (!apiKey) {
|
|
1504
|
+
findings.push(
|
|
1505
|
+
createFinding({
|
|
1506
|
+
severity: Severity.MEDIUM,
|
|
1507
|
+
title: "Web search tool has no API key configured",
|
|
1508
|
+
details: [
|
|
1509
|
+
"tools.webSearch.apiKey is not set",
|
|
1510
|
+
"Web search will fail or use a very limited fallback"
|
|
1511
|
+
],
|
|
1512
|
+
fix: "Set tools.webSearch.apiKey to your Brave Search API key (get one at brave.com/search/api)",
|
|
1513
|
+
category: "providers"
|
|
1514
|
+
})
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (providerSummary.length > 0) {
|
|
1519
|
+
findings.push(
|
|
1520
|
+
createFinding({
|
|
1521
|
+
severity: Severity.INFO,
|
|
1522
|
+
title: `${providerSummary.length} LLM provider(s) configured`,
|
|
1523
|
+
details: providerSummary,
|
|
1524
|
+
category: "providers"
|
|
1525
|
+
})
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
return findings;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1199
1531
|
// src/scanner.ts
|
|
1200
1532
|
var CHECK_REGISTRY = {
|
|
1201
1533
|
credentials: checkCredentials,
|
|
@@ -1203,14 +1535,16 @@ var CHECK_REGISTRY = {
|
|
|
1203
1535
|
sandbox: checkSandbox,
|
|
1204
1536
|
permissions: checkPermissions,
|
|
1205
1537
|
skills: checkSkills,
|
|
1206
|
-
memory: checkMemory
|
|
1538
|
+
memory: checkMemory,
|
|
1539
|
+
agents: checkAgents,
|
|
1540
|
+
providers: checkProviders
|
|
1207
1541
|
};
|
|
1208
1542
|
function detectOpenclawPath() {
|
|
1209
1543
|
const candidates = [
|
|
1210
|
-
|
|
1211
|
-
|
|
1544
|
+
path11.join(os3.homedir(), ".openclaw"),
|
|
1545
|
+
path11.join(os3.homedir(), ".clawdbot"),
|
|
1212
1546
|
// Legacy name
|
|
1213
|
-
|
|
1547
|
+
path11.join(os3.homedir(), ".moltbot")
|
|
1214
1548
|
// Legacy name
|
|
1215
1549
|
];
|
|
1216
1550
|
const envPath = process.env.OPENCLAW_HOME;
|
|
@@ -1218,7 +1552,7 @@ function detectOpenclawPath() {
|
|
|
1218
1552
|
candidates.unshift(envPath);
|
|
1219
1553
|
}
|
|
1220
1554
|
for (const candidate of candidates) {
|
|
1221
|
-
if (
|
|
1555
|
+
if (fs10.existsSync(candidate) && fs10.statSync(candidate).isDirectory()) {
|
|
1222
1556
|
return candidate;
|
|
1223
1557
|
}
|
|
1224
1558
|
}
|
|
@@ -1256,36 +1590,36 @@ async function runScan(openclawPath, checks) {
|
|
|
1256
1590
|
}
|
|
1257
1591
|
function runFix(openclawPath) {
|
|
1258
1592
|
const actions = [];
|
|
1259
|
-
if (
|
|
1260
|
-
const currentMode =
|
|
1593
|
+
if (fs10.existsSync(openclawPath)) {
|
|
1594
|
+
const currentMode = fs10.statSync(openclawPath).mode & 511;
|
|
1261
1595
|
const current = "0o" + currentMode.toString(8);
|
|
1262
1596
|
if (current !== "0o700") {
|
|
1263
|
-
|
|
1597
|
+
fs10.chmodSync(openclawPath, 448);
|
|
1264
1598
|
actions.push(`Fixed ${openclawPath} permissions: ${current} -> 0o700`);
|
|
1265
1599
|
}
|
|
1266
1600
|
}
|
|
1267
|
-
const configFile =
|
|
1268
|
-
if (
|
|
1269
|
-
const currentMode =
|
|
1601
|
+
const configFile = path11.join(openclawPath, "openclaw.json");
|
|
1602
|
+
if (fs10.existsSync(configFile)) {
|
|
1603
|
+
const currentMode = fs10.statSync(configFile).mode & 511;
|
|
1270
1604
|
const current = "0o" + currentMode.toString(8);
|
|
1271
1605
|
if (current !== "0o600") {
|
|
1272
|
-
|
|
1606
|
+
fs10.chmodSync(configFile, 384);
|
|
1273
1607
|
actions.push(
|
|
1274
|
-
`Fixed ${
|
|
1608
|
+
`Fixed ${path11.basename(configFile)} permissions: ${current} -> 0o600`
|
|
1275
1609
|
);
|
|
1276
1610
|
}
|
|
1277
1611
|
}
|
|
1278
|
-
const credsDir =
|
|
1279
|
-
if (
|
|
1612
|
+
const credsDir = path11.join(openclawPath, "credentials");
|
|
1613
|
+
if (fs10.existsSync(credsDir) && fs10.statSync(credsDir).isDirectory()) {
|
|
1280
1614
|
try {
|
|
1281
|
-
const entries =
|
|
1615
|
+
const entries = fs10.readdirSync(credsDir, { withFileTypes: true });
|
|
1282
1616
|
for (const entry of entries) {
|
|
1283
1617
|
if (entry.isFile()) {
|
|
1284
|
-
const filePath =
|
|
1285
|
-
const currentMode =
|
|
1618
|
+
const filePath = path11.join(credsDir, entry.name);
|
|
1619
|
+
const currentMode = fs10.statSync(filePath).mode & 511;
|
|
1286
1620
|
const current = "0o" + currentMode.toString(8);
|
|
1287
1621
|
if (current !== "0o600") {
|
|
1288
|
-
|
|
1622
|
+
fs10.chmodSync(filePath, 384);
|
|
1289
1623
|
actions.push(
|
|
1290
1624
|
`Fixed ${entry.name} permissions: ${current} -> 0o600`
|
|
1291
1625
|
);
|
|
@@ -1295,38 +1629,45 @@ function runFix(openclawPath) {
|
|
|
1295
1629
|
} catch {
|
|
1296
1630
|
}
|
|
1297
1631
|
}
|
|
1298
|
-
if (
|
|
1632
|
+
if (fs10.existsSync(configFile)) {
|
|
1299
1633
|
try {
|
|
1300
|
-
const content =
|
|
1301
|
-
const config =
|
|
1634
|
+
const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
|
|
1635
|
+
const config = JSON55.parse(content);
|
|
1302
1636
|
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
|
-
|
|
1637
|
+
const dockerInstalled = isDockerAvailable();
|
|
1638
|
+
if (dockerInstalled) {
|
|
1639
|
+
if (!config.agents) config.agents = {};
|
|
1640
|
+
const agents = config.agents;
|
|
1641
|
+
if (!agents.defaults) agents.defaults = {};
|
|
1642
|
+
const defaults = agents.defaults;
|
|
1643
|
+
if (!defaults.sandbox) defaults.sandbox = {};
|
|
1644
|
+
const sandbox = defaults.sandbox;
|
|
1645
|
+
if (sandbox.mode === "off" || !sandbox.mode) {
|
|
1646
|
+
sandbox.mode = "all";
|
|
1647
|
+
sandbox.scope = "session";
|
|
1648
|
+
modified = true;
|
|
1649
|
+
actions.push('Enabled sandbox: agents.defaults.sandbox.mode = "all"');
|
|
1650
|
+
}
|
|
1651
|
+
if (!sandbox.docker) sandbox.docker = {};
|
|
1652
|
+
const docker = sandbox.docker;
|
|
1653
|
+
if (docker.network !== "none") {
|
|
1654
|
+
docker.network = "none";
|
|
1655
|
+
modified = true;
|
|
1656
|
+
actions.push('Set sandbox.docker.network = "none"');
|
|
1657
|
+
}
|
|
1658
|
+
if (!config.tools) config.tools = {};
|
|
1659
|
+
const tools = config.tools;
|
|
1660
|
+
if (!tools.exec) tools.exec = {};
|
|
1661
|
+
const execConfig = tools.exec;
|
|
1662
|
+
if (execConfig.host === "gateway") {
|
|
1663
|
+
execConfig.host = "sandbox";
|
|
1664
|
+
modified = true;
|
|
1665
|
+
actions.push('Set tools.exec.host = "sandbox"');
|
|
1666
|
+
}
|
|
1667
|
+
} else {
|
|
1668
|
+
actions.push(
|
|
1669
|
+
chalk2.yellow("Sandbox changes skipped: Docker not installed. Install Docker to enable sandbox isolation.")
|
|
1670
|
+
);
|
|
1330
1671
|
}
|
|
1331
1672
|
if (!config.logging) config.logging = {};
|
|
1332
1673
|
const loggingConfig = config.logging;
|
|
@@ -1349,42 +1690,138 @@ function runFix(openclawPath) {
|
|
|
1349
1690
|
);
|
|
1350
1691
|
}
|
|
1351
1692
|
if (modified) {
|
|
1352
|
-
|
|
1693
|
+
fs10.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
1694
|
+
}
|
|
1695
|
+
const ramGB = getTotalMemoryGB();
|
|
1696
|
+
if (ramGB < 2) {
|
|
1697
|
+
actions.push(
|
|
1698
|
+
chalk2.yellow(`Warning: System has only ${ramGB.toFixed(1)}GB RAM. Set NODE_OPTIONS="--max-old-space-size=512" to prevent OOM kills.`)
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
const hits = scanFileForKeys(configFile);
|
|
1702
|
+
if (hits.length > 0) {
|
|
1703
|
+
actions.push(
|
|
1704
|
+
`Tip: Run ${chalk2.bold("clawguard migrate-env")} to move ${hits.length} plaintext key(s) to .env`
|
|
1705
|
+
);
|
|
1353
1706
|
}
|
|
1354
1707
|
} catch (e) {
|
|
1355
1708
|
const message = e instanceof Error ? e.message : String(e);
|
|
1356
1709
|
actions.push(`Could not modify config: ${message}`);
|
|
1357
1710
|
}
|
|
1358
1711
|
}
|
|
1359
|
-
const bakFiles =
|
|
1712
|
+
const bakFiles = findFilesRecursive(openclawPath, ".bak", "extension");
|
|
1360
1713
|
for (const bakFile of bakFiles) {
|
|
1361
|
-
|
|
1362
|
-
actions.push(`Deleted backup file: ${
|
|
1714
|
+
fs10.unlinkSync(bakFile);
|
|
1715
|
+
actions.push(`Deleted backup file: ${path11.basename(bakFile)}`);
|
|
1363
1716
|
}
|
|
1364
1717
|
return actions;
|
|
1365
1718
|
}
|
|
1366
|
-
function
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1719
|
+
function runMigrateEnv(openclawPath, dryRun = false) {
|
|
1720
|
+
const result = {
|
|
1721
|
+
migrations: [],
|
|
1722
|
+
envFileCreated: false,
|
|
1723
|
+
configsUpdated: []
|
|
1724
|
+
};
|
|
1725
|
+
const configFiles = [];
|
|
1726
|
+
const mainConfig = path11.join(openclawPath, "openclaw.json");
|
|
1727
|
+
if (fs10.existsSync(mainConfig)) {
|
|
1728
|
+
configFiles.push(mainConfig);
|
|
1729
|
+
}
|
|
1730
|
+
const agentsDir = path11.join(openclawPath, "agents");
|
|
1731
|
+
const modelsFiles = findFilesRecursive(agentsDir, "models.json", "exact");
|
|
1732
|
+
configFiles.push(...modelsFiles);
|
|
1733
|
+
const keyToEnvVar = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const configFile of configFiles) {
|
|
1735
|
+
try {
|
|
1736
|
+
const content = fs10.readFileSync(configFile, { encoding: "utf-8" });
|
|
1737
|
+
const lines = content.split("\n");
|
|
1738
|
+
for (const line of lines) {
|
|
1739
|
+
if (/\$\{[A-Z_][A-Z0-9_]*\}/.test(line)) continue;
|
|
1740
|
+
for (const [keyName, pattern] of API_KEY_PATTERNS) {
|
|
1741
|
+
const globalPattern = new RegExp(
|
|
1742
|
+
pattern.source,
|
|
1743
|
+
pattern.flags + (pattern.flags.includes("g") ? "" : "g")
|
|
1744
|
+
);
|
|
1745
|
+
let match;
|
|
1746
|
+
while ((match = globalPattern.exec(line)) !== null) {
|
|
1747
|
+
const rawKey = match[0];
|
|
1748
|
+
if (keyToEnvVar.has(rawKey)) continue;
|
|
1749
|
+
const envVar = guessEnvVarName(keyName, line);
|
|
1750
|
+
keyToEnvVar.set(rawKey, envVar);
|
|
1751
|
+
result.migrations.push({
|
|
1752
|
+
file: path11.relative(openclawPath, configFile),
|
|
1753
|
+
key: rawKey.slice(0, 8) + "..." + rawKey.slice(-4),
|
|
1754
|
+
envVar
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
} catch {
|
|
1760
|
+
}
|
|
1370
1761
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1762
|
+
if (result.migrations.length === 0) {
|
|
1763
|
+
return result;
|
|
1764
|
+
}
|
|
1765
|
+
if (dryRun) {
|
|
1766
|
+
return result;
|
|
1767
|
+
}
|
|
1768
|
+
const envFilePath = path11.join(openclawPath, ".env");
|
|
1769
|
+
const existingEnv = fs10.existsSync(envFilePath) ? fs10.readFileSync(envFilePath, { encoding: "utf-8" }) : "";
|
|
1770
|
+
const existingKeys = new Set(
|
|
1771
|
+
existingEnv.split("\n").filter((l) => l.includes("=")).map((l) => l.split("=")[0].trim())
|
|
1772
|
+
);
|
|
1773
|
+
const newEnvLines = [];
|
|
1774
|
+
for (const [rawKey, envVar] of keyToEnvVar) {
|
|
1775
|
+
if (!existingKeys.has(envVar)) {
|
|
1776
|
+
newEnvLines.push(`${envVar}=${rawKey}`);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
if (newEnvLines.length > 0) {
|
|
1780
|
+
const separator = existingEnv.endsWith("\n") || existingEnv === "" ? "" : "\n";
|
|
1781
|
+
fs10.appendFileSync(envFilePath, separator + newEnvLines.join("\n") + "\n");
|
|
1782
|
+
result.envFileCreated = true;
|
|
1783
|
+
try {
|
|
1784
|
+
fs10.chmodSync(envFilePath, 384);
|
|
1785
|
+
} catch {
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
for (const configFile of configFiles) {
|
|
1789
|
+
try {
|
|
1790
|
+
let content = fs10.readFileSync(configFile, { encoding: "utf-8" });
|
|
1791
|
+
let modified = false;
|
|
1792
|
+
for (const [rawKey, envVar] of keyToEnvVar) {
|
|
1793
|
+
if (content.includes(rawKey)) {
|
|
1794
|
+
content = content.split(rawKey).join(`\${${envVar}}`);
|
|
1795
|
+
modified = true;
|
|
1796
|
+
}
|
|
1379
1797
|
}
|
|
1798
|
+
if (modified) {
|
|
1799
|
+
fs10.writeFileSync(configFile, content);
|
|
1800
|
+
result.configsUpdated.push(
|
|
1801
|
+
path11.relative(openclawPath, configFile)
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
} catch {
|
|
1380
1805
|
}
|
|
1381
|
-
} catch {
|
|
1382
1806
|
}
|
|
1383
|
-
return
|
|
1807
|
+
return result;
|
|
1808
|
+
}
|
|
1809
|
+
function guessEnvVarName(keyName, contextLine) {
|
|
1810
|
+
const lower = keyName.toLowerCase();
|
|
1811
|
+
for (const [provider, envVar] of Object.entries(PROVIDER_ENV_MAP)) {
|
|
1812
|
+
if (lower.includes(provider)) return envVar;
|
|
1813
|
+
}
|
|
1814
|
+
if (contextLine.includes("telegram")) return "TELEGRAM_BOT_TOKEN";
|
|
1815
|
+
if (contextLine.includes("discord")) return "DISCORD_BOT_TOKEN";
|
|
1816
|
+
if (contextLine.includes("slack")) return "SLACK_BOT_TOKEN";
|
|
1817
|
+
if (contextLine.includes("brave") || contextLine.includes("webSearch"))
|
|
1818
|
+
return "BRAVE_SEARCH_API_KEY";
|
|
1819
|
+
if (contextLine.includes("stripe")) return "STRIPE_SECRET_KEY";
|
|
1820
|
+
return keyName.toUpperCase().replace(/[\s/]/g, "_").replace(/[^A-Z0-9_]/g, "");
|
|
1384
1821
|
}
|
|
1385
1822
|
|
|
1386
1823
|
// src/index.ts
|
|
1387
|
-
var VERSION2 = "0.
|
|
1824
|
+
var VERSION2 = "0.2.0";
|
|
1388
1825
|
var program = new Command();
|
|
1389
1826
|
program.name("clawguard").description("Security scanner for OpenClaw AI agent installations").version(VERSION2);
|
|
1390
1827
|
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 +1836,7 @@ program.command("scan").description("Scan your OpenClaw installation for securit
|
|
|
1399
1836
|
console.log("Use --path to specify the directory.");
|
|
1400
1837
|
process.exit(1);
|
|
1401
1838
|
}
|
|
1402
|
-
if (!
|
|
1839
|
+
if (!fs11.existsSync(openclawPath)) {
|
|
1403
1840
|
console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
|
|
1404
1841
|
process.exit(1);
|
|
1405
1842
|
}
|
|
@@ -1455,6 +1892,65 @@ ${actions.length} issue(s) fixed.`));
|
|
|
1455
1892
|
console.log(chalk3.green("No auto-fixable issues found.\n"));
|
|
1456
1893
|
}
|
|
1457
1894
|
});
|
|
1895
|
+
program.command("migrate-env").description(
|
|
1896
|
+
"Migrate plaintext API keys from config files to .env with ${REF} substitution"
|
|
1897
|
+
).option("-p, --path <path>", "Path to OpenClaw directory").option("--dry-run", "Show what would be migrated without making changes").action((options) => {
|
|
1898
|
+
const openclawPath = options.path ?? detectOpenclawPath();
|
|
1899
|
+
if (!openclawPath) {
|
|
1900
|
+
console.log(chalk3.red("Could not find OpenClaw installation."));
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
if (!fs11.existsSync(openclawPath)) {
|
|
1904
|
+
console.log(chalk3.red(`Path does not exist: ${openclawPath}`));
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
1907
|
+
const dryRun = options.dryRun ?? false;
|
|
1908
|
+
if (dryRun) {
|
|
1909
|
+
console.log(chalk3.cyan("\n-- DRY RUN (no files will be modified) --\n"));
|
|
1910
|
+
}
|
|
1911
|
+
const result = runMigrateEnv(openclawPath, dryRun);
|
|
1912
|
+
if (result.migrations.length === 0) {
|
|
1913
|
+
console.log(chalk3.green("No plaintext API keys found to migrate."));
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
console.log(
|
|
1917
|
+
chalk3.bold(`
|
|
1918
|
+
Found ${result.migrations.length} key(s) to migrate:
|
|
1919
|
+
`)
|
|
1920
|
+
);
|
|
1921
|
+
for (const m of result.migrations) {
|
|
1922
|
+
console.log(
|
|
1923
|
+
` ${chalk3.cyan(m.envVar)} <- ${m.key} (${chalk3.dim(m.file)})`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
if (dryRun) {
|
|
1927
|
+
console.log(
|
|
1928
|
+
chalk3.cyan("\nRun without --dry-run to apply these changes.")
|
|
1929
|
+
);
|
|
1930
|
+
} else {
|
|
1931
|
+
console.log();
|
|
1932
|
+
if (result.envFileCreated) {
|
|
1933
|
+
console.log(
|
|
1934
|
+
chalk3.green(" Created/updated .env file (chmod 600)")
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
for (const f of result.configsUpdated) {
|
|
1938
|
+
console.log(
|
|
1939
|
+
chalk3.green(` Updated ${f} with \${ENV_VAR} references`)
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
console.log(
|
|
1943
|
+
chalk3.green(
|
|
1944
|
+
`
|
|
1945
|
+
${result.migrations.length} key(s) migrated successfully.`
|
|
1946
|
+
)
|
|
1947
|
+
);
|
|
1948
|
+
console.log(
|
|
1949
|
+
`Run ${chalk3.bold("clawguard scan")} to verify.
|
|
1950
|
+
`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1458
1954
|
program.command("version").description("Show ClawGuard version").action(() => {
|
|
1459
1955
|
console.log(`ClawGuard v${VERSION2}`);
|
|
1460
1956
|
});
|