safeclaw 0.1.4 → 0.1.6
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/main.js +1783 -316
- package/package.json +16 -16
- package/public/assets/index-Bou4RiFv.css +1 -0
- package/public/assets/index-C_nCUaJ0.js +226 -0
- package/public/index.html +2 -2
- package/LICENSE +0 -295
- package/public/assets/index-8apm98QE.js +0 -126
- package/public/assets/index-Ck6Q-RL6.css +0 -1
package/dist/main.js
CHANGED
|
@@ -8,15 +8,16 @@ var __export = (target, all) => {
|
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
|
|
10
10
|
// src/lib/version.ts
|
|
11
|
-
var VERSION = "0.1.
|
|
11
|
+
var VERSION = "0.1.6";
|
|
12
12
|
|
|
13
13
|
// src/server/index.ts
|
|
14
14
|
import Fastify from "fastify";
|
|
15
15
|
import fastifyStatic from "@fastify/static";
|
|
16
16
|
import fastifyCors from "@fastify/cors";
|
|
17
|
-
import
|
|
17
|
+
import fs10 from "fs";
|
|
18
18
|
|
|
19
19
|
// src/lib/paths.ts
|
|
20
|
+
import fs from "fs";
|
|
20
21
|
import path from "path";
|
|
21
22
|
import os from "os";
|
|
22
23
|
var HOME = os.homedir();
|
|
@@ -31,9 +32,12 @@ var OPENCLAW_IDENTITY_DIR = path.join(OPENCLAW_DIR, "identity");
|
|
|
31
32
|
var OPENCLAW_DEVICE_JSON = path.join(OPENCLAW_IDENTITY_DIR, "device.json");
|
|
32
33
|
var OPENCLAW_DEVICE_AUTH_JSON = path.join(OPENCLAW_IDENTITY_DIR, "device-auth.json");
|
|
33
34
|
var OPENCLAW_EXEC_APPROVALS_PATH = path.join(OPENCLAW_DIR, "exec-approvals.json");
|
|
35
|
+
var SRT_SETTINGS_PATH = path.join(HOME, ".srt-settings.json");
|
|
34
36
|
function getPublicDir() {
|
|
35
37
|
const currentDir = path.dirname(new URL(import.meta.url).pathname);
|
|
36
|
-
|
|
38
|
+
const bundledPath = path.resolve(currentDir, "..", "public");
|
|
39
|
+
const devPath = path.resolve(currentDir, "..", "..", "public");
|
|
40
|
+
return fs.existsSync(bundledPath) ? bundledPath : devPath;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
// src/server/socket.ts
|
|
@@ -44,7 +48,7 @@ import Database from "better-sqlite3";
|
|
|
44
48
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
45
49
|
|
|
46
50
|
// src/lib/config.ts
|
|
47
|
-
import
|
|
51
|
+
import fs2 from "fs";
|
|
48
52
|
|
|
49
53
|
// ../../packages/shared/dist/schemas.js
|
|
50
54
|
import { z } from "zod";
|
|
@@ -81,8 +85,12 @@ var safeClawConfigSchema = z.object({
|
|
|
81
85
|
port: z.number().int().min(1024).max(65535).default(54335),
|
|
82
86
|
autoOpenBrowser: z.boolean().default(true),
|
|
83
87
|
premium: z.boolean().default(false),
|
|
84
|
-
userId: z.string().nullable().default(null)
|
|
85
|
-
|
|
88
|
+
userId: z.string().nullable().default(null),
|
|
89
|
+
srt: z.object({
|
|
90
|
+
enabled: z.boolean(),
|
|
91
|
+
settingsPath: z.string().optional()
|
|
92
|
+
}).optional()
|
|
93
|
+
}).passthrough();
|
|
86
94
|
var activityTypeSchema = z.enum([
|
|
87
95
|
"file_read",
|
|
88
96
|
"file_write",
|
|
@@ -260,7 +268,7 @@ var execApprovalEntrySchema = z.object({
|
|
|
260
268
|
requestedAt: z.string(),
|
|
261
269
|
expiresAt: z.string(),
|
|
262
270
|
decision: execDecisionSchema.nullable(),
|
|
263
|
-
decidedBy: z.enum(["user", "auto-deny"]).nullable(),
|
|
271
|
+
decidedBy: z.enum(["user", "auto-deny", "auto-approve", "access-control"]).nullable(),
|
|
264
272
|
decidedAt: z.string().nullable()
|
|
265
273
|
});
|
|
266
274
|
var allowlistPatternSchema = z.object({
|
|
@@ -269,6 +277,104 @@ var allowlistPatternSchema = z.object({
|
|
|
269
277
|
var allowlistStateSchema = z.object({
|
|
270
278
|
patterns: z.array(allowlistPatternSchema)
|
|
271
279
|
});
|
|
280
|
+
var skillScanCategoryIdSchema = z.enum([
|
|
281
|
+
"SK-HID",
|
|
282
|
+
"SK-INJ",
|
|
283
|
+
"SK-EXE",
|
|
284
|
+
"SK-EXF",
|
|
285
|
+
"SK-SEC",
|
|
286
|
+
"SK-SFA",
|
|
287
|
+
"SK-MEM",
|
|
288
|
+
"SK-SUP",
|
|
289
|
+
"SK-B64",
|
|
290
|
+
"SK-IMG",
|
|
291
|
+
"SK-SYS",
|
|
292
|
+
"SK-ARG",
|
|
293
|
+
"SK-XTL",
|
|
294
|
+
"SK-PRM",
|
|
295
|
+
"SK-STR"
|
|
296
|
+
]);
|
|
297
|
+
var skillScanRequestSchema = z.object({
|
|
298
|
+
content: z.string().min(1).max(5e5)
|
|
299
|
+
});
|
|
300
|
+
var skillScanFindingSchema = z.object({
|
|
301
|
+
categoryId: skillScanCategoryIdSchema,
|
|
302
|
+
categoryName: z.string(),
|
|
303
|
+
severity: threatLevelSchema,
|
|
304
|
+
reason: z.string(),
|
|
305
|
+
evidence: z.string().optional(),
|
|
306
|
+
owaspRef: z.string().optional(),
|
|
307
|
+
remediation: z.string().optional(),
|
|
308
|
+
lineNumber: z.number().optional()
|
|
309
|
+
});
|
|
310
|
+
var skillScanResultSchema = z.object({
|
|
311
|
+
overallSeverity: threatLevelSchema,
|
|
312
|
+
findings: z.array(skillScanFindingSchema),
|
|
313
|
+
summary: z.object({
|
|
314
|
+
critical: z.number(),
|
|
315
|
+
high: z.number(),
|
|
316
|
+
medium: z.number(),
|
|
317
|
+
low: z.number()
|
|
318
|
+
}),
|
|
319
|
+
scannedAt: z.string(),
|
|
320
|
+
contentLength: z.number(),
|
|
321
|
+
scanDurationMs: z.number()
|
|
322
|
+
});
|
|
323
|
+
var skillCleanResultSchema = z.object({
|
|
324
|
+
cleanedContent: z.string(),
|
|
325
|
+
removedCount: z.number()
|
|
326
|
+
});
|
|
327
|
+
var securityLayerStatusSchema = z.enum([
|
|
328
|
+
"configured",
|
|
329
|
+
"partial",
|
|
330
|
+
"unconfigured",
|
|
331
|
+
"error"
|
|
332
|
+
]);
|
|
333
|
+
var securityCheckSchema = z.object({
|
|
334
|
+
id: z.string(),
|
|
335
|
+
label: z.string(),
|
|
336
|
+
passed: z.boolean(),
|
|
337
|
+
detail: z.string(),
|
|
338
|
+
severity: z.enum(["info", "warning", "critical"])
|
|
339
|
+
}).passthrough();
|
|
340
|
+
var securityLayerSchema = z.object({
|
|
341
|
+
id: z.string(),
|
|
342
|
+
name: z.string(),
|
|
343
|
+
status: securityLayerStatusSchema,
|
|
344
|
+
checks: z.array(securityCheckSchema),
|
|
345
|
+
passedCount: z.number(),
|
|
346
|
+
totalCount: z.number()
|
|
347
|
+
}).passthrough();
|
|
348
|
+
var securityPostureSchema = z.object({
|
|
349
|
+
layers: z.array(securityLayerSchema),
|
|
350
|
+
overallScore: z.number(),
|
|
351
|
+
configuredLayers: z.number(),
|
|
352
|
+
partialLayers: z.number(),
|
|
353
|
+
unconfiguredLayers: z.number(),
|
|
354
|
+
totalLayers: z.number(),
|
|
355
|
+
checkedAt: z.string()
|
|
356
|
+
}).passthrough();
|
|
357
|
+
var srtNetworkConfigSchema = z.object({
|
|
358
|
+
allowedDomains: z.array(z.string()),
|
|
359
|
+
deniedDomains: z.array(z.string()),
|
|
360
|
+
allowLocalBinding: z.boolean()
|
|
361
|
+
}).passthrough();
|
|
362
|
+
var srtFilesystemConfigSchema = z.object({
|
|
363
|
+
denyRead: z.array(z.string()),
|
|
364
|
+
allowWrite: z.array(z.string()),
|
|
365
|
+
denyWrite: z.array(z.string())
|
|
366
|
+
}).passthrough();
|
|
367
|
+
var srtSettingsSchema = z.object({
|
|
368
|
+
network: srtNetworkConfigSchema,
|
|
369
|
+
filesystem: srtFilesystemConfigSchema
|
|
370
|
+
}).passthrough();
|
|
371
|
+
var srtStatusSchema = z.object({
|
|
372
|
+
installed: z.boolean(),
|
|
373
|
+
version: z.string().nullable(),
|
|
374
|
+
enabled: z.boolean(),
|
|
375
|
+
settingsPath: z.string(),
|
|
376
|
+
settings: srtSettingsSchema.nullable()
|
|
377
|
+
}).passthrough();
|
|
272
378
|
|
|
273
379
|
// src/lib/config.ts
|
|
274
380
|
var DEFAULT_CONFIG = {
|
|
@@ -276,28 +382,31 @@ var DEFAULT_CONFIG = {
|
|
|
276
382
|
port: 54335,
|
|
277
383
|
autoOpenBrowser: true,
|
|
278
384
|
premium: false,
|
|
279
|
-
userId: null
|
|
385
|
+
userId: null,
|
|
386
|
+
srt: {
|
|
387
|
+
enabled: false
|
|
388
|
+
}
|
|
280
389
|
};
|
|
281
390
|
function ensureDataDir() {
|
|
282
|
-
if (!
|
|
283
|
-
|
|
391
|
+
if (!fs2.existsSync(SAFECLAW_DIR)) {
|
|
392
|
+
fs2.mkdirSync(SAFECLAW_DIR, { recursive: true });
|
|
284
393
|
}
|
|
285
|
-
if (!
|
|
286
|
-
|
|
394
|
+
if (!fs2.existsSync(LOGS_DIR)) {
|
|
395
|
+
fs2.mkdirSync(LOGS_DIR, { recursive: true });
|
|
287
396
|
}
|
|
288
397
|
}
|
|
289
398
|
function readConfig() {
|
|
290
399
|
ensureDataDir();
|
|
291
|
-
if (!
|
|
292
|
-
|
|
400
|
+
if (!fs2.existsSync(CONFIG_PATH)) {
|
|
401
|
+
fs2.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
293
402
|
return DEFAULT_CONFIG;
|
|
294
403
|
}
|
|
295
|
-
const raw = JSON.parse(
|
|
404
|
+
const raw = JSON.parse(fs2.readFileSync(CONFIG_PATH, "utf-8"));
|
|
296
405
|
return safeClawConfigSchema.parse(raw);
|
|
297
406
|
}
|
|
298
407
|
function writeConfig(config) {
|
|
299
408
|
ensureDataDir();
|
|
300
|
-
|
|
409
|
+
fs2.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
301
410
|
}
|
|
302
411
|
function resetConfig() {
|
|
303
412
|
writeConfig(DEFAULT_CONFIG);
|
|
@@ -400,7 +509,7 @@ var execApprovals = sqliteTable("exec_approvals", {
|
|
|
400
509
|
enum: ["allow-once", "allow-always", "deny"]
|
|
401
510
|
}),
|
|
402
511
|
decidedBy: text("decided_by", {
|
|
403
|
-
enum: ["user", "auto-deny"]
|
|
512
|
+
enum: ["user", "auto-deny", "auto-approve", "access-control"]
|
|
404
513
|
}),
|
|
405
514
|
decidedAt: text("decided_at"),
|
|
406
515
|
matchedPattern: text("matched_pattern")
|
|
@@ -423,9 +532,9 @@ import { eq as eq4, desc as desc3 } from "drizzle-orm";
|
|
|
423
532
|
|
|
424
533
|
// src/lib/logger.ts
|
|
425
534
|
import pino from "pino";
|
|
426
|
-
import
|
|
427
|
-
if (!
|
|
428
|
-
|
|
535
|
+
import fs3 from "fs";
|
|
536
|
+
if (!fs3.existsSync(LOGS_DIR)) {
|
|
537
|
+
fs3.mkdirSync(LOGS_DIR, { recursive: true });
|
|
429
538
|
}
|
|
430
539
|
function createLogger(consoleLevel = "info") {
|
|
431
540
|
return pino({
|
|
@@ -454,20 +563,21 @@ function setVerbose(verbose) {
|
|
|
454
563
|
}
|
|
455
564
|
|
|
456
565
|
// src/lib/openclaw-config.ts
|
|
457
|
-
import
|
|
566
|
+
import fs4 from "fs";
|
|
458
567
|
function readOpenClawConfig() {
|
|
459
|
-
if (!
|
|
568
|
+
if (!fs4.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
460
569
|
return null;
|
|
461
570
|
}
|
|
462
|
-
const raw = JSON.parse(
|
|
571
|
+
const raw = JSON.parse(fs4.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
|
|
463
572
|
return openClawConfigSchema.parse(raw);
|
|
464
573
|
}
|
|
465
574
|
function writeOpenClawConfig(updates) {
|
|
466
575
|
const current = readOpenClawConfig();
|
|
467
576
|
if (!current) return null;
|
|
468
577
|
const merged = deepMerge(current, updates);
|
|
469
|
-
|
|
470
|
-
|
|
578
|
+
const cleaned = stripNulls(merged);
|
|
579
|
+
fs4.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(cleaned, null, 2));
|
|
580
|
+
return cleaned;
|
|
471
581
|
}
|
|
472
582
|
function deepMerge(target, source) {
|
|
473
583
|
const result = { ...target };
|
|
@@ -482,15 +592,30 @@ function deepMerge(target, source) {
|
|
|
482
592
|
}
|
|
483
593
|
return result;
|
|
484
594
|
}
|
|
595
|
+
function stripNulls(obj) {
|
|
596
|
+
const result = {};
|
|
597
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
598
|
+
if (val === null) continue;
|
|
599
|
+
if (typeof val === "object" && !Array.isArray(val)) {
|
|
600
|
+
const cleaned = stripNulls(val);
|
|
601
|
+
if (Object.keys(cleaned).length > 0) {
|
|
602
|
+
result[key] = cleaned;
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
result[key] = val;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
485
610
|
|
|
486
611
|
// src/lib/openclaw-client.ts
|
|
487
612
|
import { EventEmitter } from "events";
|
|
488
613
|
import crypto from "crypto";
|
|
489
|
-
import
|
|
614
|
+
import fs5 from "fs";
|
|
490
615
|
import { WebSocket } from "ws";
|
|
491
616
|
function readDeviceIdentity() {
|
|
492
617
|
try {
|
|
493
|
-
const raw =
|
|
618
|
+
const raw = fs5.readFileSync(OPENCLAW_DEVICE_JSON, "utf-8");
|
|
494
619
|
const data = JSON.parse(raw);
|
|
495
620
|
if (!data.deviceId || !data.publicKeyPem || !data.privateKeyPem) return null;
|
|
496
621
|
return {
|
|
@@ -1008,7 +1133,7 @@ var OpenClawClient = class extends EventEmitter {
|
|
|
1008
1133
|
|
|
1009
1134
|
// src/services/session-watcher.ts
|
|
1010
1135
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
1011
|
-
import
|
|
1136
|
+
import fs6 from "fs";
|
|
1012
1137
|
import path2 from "path";
|
|
1013
1138
|
var MAX_CONTENT_PREVIEW = 10 * 1024;
|
|
1014
1139
|
var SessionWatcher = class extends EventEmitter2 {
|
|
@@ -1027,7 +1152,7 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1027
1152
|
start() {
|
|
1028
1153
|
if (this.destroyed) return;
|
|
1029
1154
|
const agentsDir = path2.join(OPENCLAW_DIR, "agents");
|
|
1030
|
-
if (!
|
|
1155
|
+
if (!fs6.existsSync(agentsDir)) {
|
|
1031
1156
|
logger.debug("OpenClaw agents directory not found, will retry");
|
|
1032
1157
|
}
|
|
1033
1158
|
this.scanForSessions();
|
|
@@ -1035,8 +1160,8 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1035
1160
|
if (!this.destroyed) this.scanForSessions();
|
|
1036
1161
|
}, 1e4);
|
|
1037
1162
|
try {
|
|
1038
|
-
if (
|
|
1039
|
-
this.agentsDirWatcher =
|
|
1163
|
+
if (fs6.existsSync(agentsDir)) {
|
|
1164
|
+
this.agentsDirWatcher = fs6.watch(agentsDir, { recursive: true }, () => {
|
|
1040
1165
|
if (!this.destroyed) this.scanForSessions();
|
|
1041
1166
|
});
|
|
1042
1167
|
}
|
|
@@ -1064,10 +1189,10 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1064
1189
|
}
|
|
1065
1190
|
scanForSessions() {
|
|
1066
1191
|
const agentsDir = path2.join(OPENCLAW_DIR, "agents");
|
|
1067
|
-
if (!
|
|
1192
|
+
if (!fs6.existsSync(agentsDir)) return;
|
|
1068
1193
|
try {
|
|
1069
|
-
const agentNames =
|
|
1070
|
-
const stat =
|
|
1194
|
+
const agentNames = fs6.readdirSync(agentsDir).filter((name) => {
|
|
1195
|
+
const stat = fs6.statSync(path2.join(agentsDir, name));
|
|
1071
1196
|
return stat.isDirectory();
|
|
1072
1197
|
});
|
|
1073
1198
|
for (const agentName of agentNames) {
|
|
@@ -1079,11 +1204,11 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1079
1204
|
}
|
|
1080
1205
|
discoverSessionFiles(agentName) {
|
|
1081
1206
|
const sessionsDir = path2.join(OPENCLAW_DIR, "agents", agentName, "sessions");
|
|
1082
|
-
if (!
|
|
1207
|
+
if (!fs6.existsSync(sessionsDir)) return;
|
|
1083
1208
|
const sessionsJsonPath = path2.join(sessionsDir, "sessions.json");
|
|
1084
|
-
if (
|
|
1209
|
+
if (fs6.existsSync(sessionsJsonPath)) {
|
|
1085
1210
|
try {
|
|
1086
|
-
const raw =
|
|
1211
|
+
const raw = fs6.readFileSync(sessionsJsonPath, "utf-8");
|
|
1087
1212
|
const sessionsData = JSON.parse(raw);
|
|
1088
1213
|
const sessions2 = sessionsData.sessions ?? [];
|
|
1089
1214
|
for (const session of sessions2) {
|
|
@@ -1095,7 +1220,7 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1095
1220
|
}
|
|
1096
1221
|
}
|
|
1097
1222
|
try {
|
|
1098
|
-
const files =
|
|
1223
|
+
const files = fs6.readdirSync(sessionsDir).filter((f) => f.endsWith(".jsonl"));
|
|
1099
1224
|
for (const file of files) {
|
|
1100
1225
|
const filePath = path2.join(sessionsDir, file);
|
|
1101
1226
|
const sessionId = path2.basename(file, ".jsonl");
|
|
@@ -1106,8 +1231,8 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1106
1231
|
}
|
|
1107
1232
|
watchSessionFile(filePath, sessionId, agentName) {
|
|
1108
1233
|
if (this.watchedFiles.has(filePath)) return;
|
|
1109
|
-
if (!
|
|
1110
|
-
const stat =
|
|
1234
|
+
if (!fs6.existsSync(filePath)) return;
|
|
1235
|
+
const stat = fs6.statSync(filePath);
|
|
1111
1236
|
const watched = {
|
|
1112
1237
|
path: filePath,
|
|
1113
1238
|
position: stat.size,
|
|
@@ -1117,7 +1242,7 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1117
1242
|
agentName
|
|
1118
1243
|
};
|
|
1119
1244
|
try {
|
|
1120
|
-
watched.watcher =
|
|
1245
|
+
watched.watcher = fs6.watch(filePath, () => {
|
|
1121
1246
|
if (!this.destroyed) {
|
|
1122
1247
|
this.readNewEntries(watched);
|
|
1123
1248
|
}
|
|
@@ -1131,13 +1256,13 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1131
1256
|
}
|
|
1132
1257
|
readNewEntries(watched) {
|
|
1133
1258
|
try {
|
|
1134
|
-
const stat =
|
|
1259
|
+
const stat = fs6.statSync(watched.path);
|
|
1135
1260
|
if (stat.size <= watched.position) return;
|
|
1136
|
-
const fd =
|
|
1261
|
+
const fd = fs6.openSync(watched.path, "r");
|
|
1137
1262
|
const bufferSize = stat.size - watched.position;
|
|
1138
1263
|
const buffer = Buffer.alloc(bufferSize);
|
|
1139
|
-
|
|
1140
|
-
|
|
1264
|
+
fs6.readSync(fd, buffer, 0, bufferSize, watched.position);
|
|
1265
|
+
fs6.closeSync(fd);
|
|
1141
1266
|
watched.position = stat.size;
|
|
1142
1267
|
const text2 = buffer.toString("utf-8");
|
|
1143
1268
|
const lines = text2.split("\n").filter((l) => l.trim());
|
|
@@ -1308,8 +1433,228 @@ var SessionWatcher = class extends EventEmitter2 {
|
|
|
1308
1433
|
};
|
|
1309
1434
|
|
|
1310
1435
|
// src/services/exec-approval-service.ts
|
|
1311
|
-
import { eq, desc, sql as
|
|
1436
|
+
import { eq as eq2, desc, sql as sql3 } from "drizzle-orm";
|
|
1312
1437
|
import path3 from "path";
|
|
1438
|
+
|
|
1439
|
+
// src/services/access-control.ts
|
|
1440
|
+
import { eq, and, sql as sql2 } from "drizzle-orm";
|
|
1441
|
+
var TOOL_GROUP_MAP = {
|
|
1442
|
+
filesystem: "group:fs",
|
|
1443
|
+
system_commands: "group:runtime",
|
|
1444
|
+
network: "group:web"
|
|
1445
|
+
};
|
|
1446
|
+
function deriveMcpServerStates(config, denyList) {
|
|
1447
|
+
const pluginEntries = config.plugins?.entries ?? {};
|
|
1448
|
+
return Object.keys(pluginEntries).map((name) => {
|
|
1449
|
+
const pluginEnabled = pluginEntries[name].enabled !== false;
|
|
1450
|
+
const toolsDenyBlocked = denyList.includes(`mcp__${name}`);
|
|
1451
|
+
return {
|
|
1452
|
+
name,
|
|
1453
|
+
pluginEnabled,
|
|
1454
|
+
toolsDenyBlocked,
|
|
1455
|
+
effectivelyEnabled: pluginEnabled && !toolsDenyBlocked
|
|
1456
|
+
};
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
function deriveAccessState() {
|
|
1460
|
+
const config = readOpenClawConfig();
|
|
1461
|
+
if (!config) {
|
|
1462
|
+
return {
|
|
1463
|
+
toggles: [
|
|
1464
|
+
{ category: "filesystem", enabled: true },
|
|
1465
|
+
{ category: "mcp_servers", enabled: true },
|
|
1466
|
+
{ category: "network", enabled: true },
|
|
1467
|
+
{ category: "system_commands", enabled: true }
|
|
1468
|
+
],
|
|
1469
|
+
mcpServers: [],
|
|
1470
|
+
openclawConfigAvailable: false
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
const denyList = config.tools?.deny ?? [];
|
|
1474
|
+
const filesystemEnabled = !denyList.includes("group:fs");
|
|
1475
|
+
const systemCommandsEnabled = !denyList.includes("group:runtime");
|
|
1476
|
+
const networkEnabled = !denyList.includes("group:web") && config.browser?.enabled !== false;
|
|
1477
|
+
const pluginEntries = config.plugins?.entries ?? {};
|
|
1478
|
+
const pluginNames = Object.keys(pluginEntries);
|
|
1479
|
+
const mcpEnabled = pluginNames.length === 0 || pluginNames.some((name) => pluginEntries[name].enabled !== false);
|
|
1480
|
+
const toggles = [
|
|
1481
|
+
{ category: "filesystem", enabled: filesystemEnabled },
|
|
1482
|
+
{ category: "mcp_servers", enabled: mcpEnabled },
|
|
1483
|
+
{ category: "network", enabled: networkEnabled },
|
|
1484
|
+
{ category: "system_commands", enabled: systemCommandsEnabled }
|
|
1485
|
+
];
|
|
1486
|
+
const mcpServers = deriveMcpServerStates(config, denyList);
|
|
1487
|
+
return { toggles, mcpServers, openclawConfigAvailable: true };
|
|
1488
|
+
}
|
|
1489
|
+
async function applyAccessToggle(category, enabled) {
|
|
1490
|
+
const config = readOpenClawConfig();
|
|
1491
|
+
if (!config) {
|
|
1492
|
+
throw new Error("OpenClaw config not found");
|
|
1493
|
+
}
|
|
1494
|
+
if (category === "mcp_servers") {
|
|
1495
|
+
await applyMcpToggle(config, enabled);
|
|
1496
|
+
} else if (category === "network") {
|
|
1497
|
+
applyNetworkToggle(config, enabled);
|
|
1498
|
+
} else {
|
|
1499
|
+
applyToolGroupToggle(config, category, enabled);
|
|
1500
|
+
}
|
|
1501
|
+
await updateAuditDb(category, enabled);
|
|
1502
|
+
return deriveAccessState();
|
|
1503
|
+
}
|
|
1504
|
+
async function applyMcpServerToggle(serverName, enabled) {
|
|
1505
|
+
const config = readOpenClawConfig();
|
|
1506
|
+
if (!config) {
|
|
1507
|
+
throw new Error("OpenClaw config not found");
|
|
1508
|
+
}
|
|
1509
|
+
const denyPattern = `mcp__${serverName}`;
|
|
1510
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
1511
|
+
if (enabled) {
|
|
1512
|
+
const filtered = currentDeny.filter((entry) => entry !== denyPattern);
|
|
1513
|
+
writeOpenClawConfig({ tools: { deny: filtered } });
|
|
1514
|
+
} else {
|
|
1515
|
+
if (!currentDeny.includes(denyPattern)) {
|
|
1516
|
+
currentDeny.push(denyPattern);
|
|
1517
|
+
}
|
|
1518
|
+
writeOpenClawConfig({ tools: { deny: currentDeny } });
|
|
1519
|
+
}
|
|
1520
|
+
await updateAuditDb(`mcp_server:${serverName}`, enabled);
|
|
1521
|
+
return deriveAccessState();
|
|
1522
|
+
}
|
|
1523
|
+
function applyToolGroupToggle(config, category, enabled) {
|
|
1524
|
+
const groupName = TOOL_GROUP_MAP[category];
|
|
1525
|
+
if (!groupName) return;
|
|
1526
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
1527
|
+
if (enabled) {
|
|
1528
|
+
const filtered = currentDeny.filter((entry) => entry !== groupName);
|
|
1529
|
+
writeOpenClawConfig({ tools: { deny: filtered } });
|
|
1530
|
+
} else {
|
|
1531
|
+
if (!currentDeny.includes(groupName)) {
|
|
1532
|
+
currentDeny.push(groupName);
|
|
1533
|
+
}
|
|
1534
|
+
writeOpenClawConfig({ tools: { deny: currentDeny } });
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function applyNetworkToggle(config, enabled) {
|
|
1538
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
1539
|
+
const groupName = "group:web";
|
|
1540
|
+
if (enabled) {
|
|
1541
|
+
const filtered = currentDeny.filter((entry) => entry !== groupName);
|
|
1542
|
+
writeOpenClawConfig({
|
|
1543
|
+
tools: { deny: filtered },
|
|
1544
|
+
browser: { enabled: true }
|
|
1545
|
+
});
|
|
1546
|
+
} else {
|
|
1547
|
+
if (!currentDeny.includes(groupName)) {
|
|
1548
|
+
currentDeny.push(groupName);
|
|
1549
|
+
}
|
|
1550
|
+
writeOpenClawConfig({
|
|
1551
|
+
tools: { deny: currentDeny },
|
|
1552
|
+
browser: { enabled: false }
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async function applyMcpToggle(config, enabled) {
|
|
1557
|
+
const pluginEntries = config.plugins?.entries ?? {};
|
|
1558
|
+
const pluginNames = Object.keys(pluginEntries);
|
|
1559
|
+
if (pluginNames.length === 0) return;
|
|
1560
|
+
const currentDeny = [...config.tools?.deny ?? []];
|
|
1561
|
+
if (!enabled) {
|
|
1562
|
+
const stateMap = {};
|
|
1563
|
+
for (const name of pluginNames) {
|
|
1564
|
+
stateMap[name] = pluginEntries[name].enabled !== false;
|
|
1565
|
+
}
|
|
1566
|
+
await savePreviousPluginState(stateMap);
|
|
1567
|
+
const disabledEntries = {};
|
|
1568
|
+
for (const name of pluginNames) {
|
|
1569
|
+
disabledEntries[name] = { enabled: false };
|
|
1570
|
+
}
|
|
1571
|
+
for (const name of pluginNames) {
|
|
1572
|
+
const denyPattern = `mcp__${name}`;
|
|
1573
|
+
if (!currentDeny.includes(denyPattern)) {
|
|
1574
|
+
currentDeny.push(denyPattern);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
writeOpenClawConfig({
|
|
1578
|
+
plugins: { entries: disabledEntries },
|
|
1579
|
+
tools: { deny: currentDeny }
|
|
1580
|
+
});
|
|
1581
|
+
} else {
|
|
1582
|
+
const previousState = await loadPreviousPluginState();
|
|
1583
|
+
const restoredEntries = {};
|
|
1584
|
+
for (const name of pluginNames) {
|
|
1585
|
+
restoredEntries[name] = {
|
|
1586
|
+
enabled: previousState?.[name] ?? true
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
const mcpDenyPatterns = new Set(pluginNames.map((n) => `mcp__${n}`));
|
|
1590
|
+
const filteredDeny = currentDeny.filter(
|
|
1591
|
+
(entry) => !mcpDenyPatterns.has(entry)
|
|
1592
|
+
);
|
|
1593
|
+
writeOpenClawConfig({
|
|
1594
|
+
plugins: { entries: restoredEntries },
|
|
1595
|
+
tools: { deny: filteredDeny }
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
async function savePreviousPluginState(stateMap) {
|
|
1600
|
+
const db = getDb();
|
|
1601
|
+
const existing = await db.select().from(schema_exports.accessConfig).where(
|
|
1602
|
+
and(
|
|
1603
|
+
eq(schema_exports.accessConfig.category, "mcp_servers"),
|
|
1604
|
+
eq(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
1605
|
+
)
|
|
1606
|
+
);
|
|
1607
|
+
if (existing.length > 0) {
|
|
1608
|
+
await db.update(schema_exports.accessConfig).set({
|
|
1609
|
+
value: JSON.stringify(stateMap),
|
|
1610
|
+
updatedAt: sql2`datetime('now')`
|
|
1611
|
+
}).where(
|
|
1612
|
+
and(
|
|
1613
|
+
eq(schema_exports.accessConfig.category, "mcp_servers"),
|
|
1614
|
+
eq(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
1615
|
+
)
|
|
1616
|
+
);
|
|
1617
|
+
} else {
|
|
1618
|
+
await db.insert(schema_exports.accessConfig).values({
|
|
1619
|
+
category: "mcp_servers",
|
|
1620
|
+
key: "previous_plugin_state",
|
|
1621
|
+
value: JSON.stringify(stateMap)
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
async function loadPreviousPluginState() {
|
|
1626
|
+
const db = getDb();
|
|
1627
|
+
const rows = await db.select().from(schema_exports.accessConfig).where(
|
|
1628
|
+
and(
|
|
1629
|
+
eq(schema_exports.accessConfig.category, "mcp_servers"),
|
|
1630
|
+
eq(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
1631
|
+
)
|
|
1632
|
+
);
|
|
1633
|
+
if (rows.length === 0) return null;
|
|
1634
|
+
try {
|
|
1635
|
+
return JSON.parse(rows[0].value);
|
|
1636
|
+
} catch {
|
|
1637
|
+
return null;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
async function updateAuditDb(category, enabled) {
|
|
1641
|
+
const db = getDb();
|
|
1642
|
+
try {
|
|
1643
|
+
await db.update(schema_exports.accessConfig).set({
|
|
1644
|
+
value: enabled ? "true" : "false",
|
|
1645
|
+
updatedAt: sql2`datetime('now')`
|
|
1646
|
+
}).where(
|
|
1647
|
+
and(
|
|
1648
|
+
eq(schema_exports.accessConfig.category, category),
|
|
1649
|
+
eq(schema_exports.accessConfig.key, "enabled")
|
|
1650
|
+
)
|
|
1651
|
+
);
|
|
1652
|
+
} catch (err) {
|
|
1653
|
+
logger.warn({ err, category, enabled }, "Failed to update audit DB");
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/services/exec-approval-service.ts
|
|
1313
1658
|
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
1314
1659
|
var instance = null;
|
|
1315
1660
|
function matchesPattern(command, pattern) {
|
|
@@ -1317,6 +1662,37 @@ function matchesPattern(command, pattern) {
|
|
|
1317
1662
|
const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
|
|
1318
1663
|
return new RegExp(regexStr, "i").test(command);
|
|
1319
1664
|
}
|
|
1665
|
+
var NETWORK_BINARIES = /* @__PURE__ */ new Set([
|
|
1666
|
+
"curl",
|
|
1667
|
+
"wget",
|
|
1668
|
+
"httpie",
|
|
1669
|
+
"http",
|
|
1670
|
+
"ssh",
|
|
1671
|
+
"scp",
|
|
1672
|
+
"sftp",
|
|
1673
|
+
"nc",
|
|
1674
|
+
"ncat",
|
|
1675
|
+
"netcat",
|
|
1676
|
+
"dig",
|
|
1677
|
+
"nslookup",
|
|
1678
|
+
"host",
|
|
1679
|
+
"ping",
|
|
1680
|
+
"traceroute",
|
|
1681
|
+
"tracepath",
|
|
1682
|
+
"mtr",
|
|
1683
|
+
"telnet",
|
|
1684
|
+
"ftp",
|
|
1685
|
+
"lftp",
|
|
1686
|
+
"rsync",
|
|
1687
|
+
"socat",
|
|
1688
|
+
"nmap"
|
|
1689
|
+
]);
|
|
1690
|
+
function isNetworkCommand(command) {
|
|
1691
|
+
const firstToken = command.trim().split(/\s+/)[0];
|
|
1692
|
+
if (!firstToken) return false;
|
|
1693
|
+
const basename = firstToken.includes("/") ? firstToken.split("/").pop() : firstToken;
|
|
1694
|
+
return NETWORK_BINARIES.has(basename.toLowerCase());
|
|
1695
|
+
}
|
|
1320
1696
|
var ExecApprovalService = class {
|
|
1321
1697
|
client;
|
|
1322
1698
|
io;
|
|
@@ -1357,11 +1733,62 @@ var ExecApprovalService = class {
|
|
|
1357
1733
|
handleRequest(request) {
|
|
1358
1734
|
const matchedPattern = this.findMatchingPattern(request.command);
|
|
1359
1735
|
if (!matchedPattern) {
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1736
|
+
if (isNetworkCommand(request.command)) {
|
|
1737
|
+
try {
|
|
1738
|
+
const accessState = deriveAccessState();
|
|
1739
|
+
const networkToggle = accessState.toggles.find(
|
|
1740
|
+
(t) => t.category === "network"
|
|
1741
|
+
);
|
|
1742
|
+
if (networkToggle && !networkToggle.enabled) {
|
|
1743
|
+
this.resolveToGateway(request.id, "deny");
|
|
1744
|
+
const now3 = /* @__PURE__ */ new Date();
|
|
1745
|
+
const entry3 = {
|
|
1746
|
+
id: request.id,
|
|
1747
|
+
command: request.command,
|
|
1748
|
+
cwd: request.cwd,
|
|
1749
|
+
security: request.security,
|
|
1750
|
+
sessionKey: request.sessionKey,
|
|
1751
|
+
requestedAt: now3.toISOString(),
|
|
1752
|
+
expiresAt: now3.toISOString(),
|
|
1753
|
+
decision: "deny",
|
|
1754
|
+
decidedBy: "access-control",
|
|
1755
|
+
decidedAt: now3.toISOString()
|
|
1756
|
+
};
|
|
1757
|
+
this.persistApproval(entry3, null);
|
|
1758
|
+
this.io.emit("safeclaw:execApprovalResolved", entry3);
|
|
1759
|
+
logger.info(
|
|
1760
|
+
{ command: request.command },
|
|
1761
|
+
"Network command auto-denied (network toggle OFF)"
|
|
1762
|
+
);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
} catch {
|
|
1766
|
+
logger.debug(
|
|
1767
|
+
{ command: request.command },
|
|
1768
|
+
"Could not read access state for network check, falling through"
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
this.resolveToGateway(request.id, "allow-once");
|
|
1773
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
1774
|
+
const entry2 = {
|
|
1775
|
+
id: request.id,
|
|
1776
|
+
command: request.command,
|
|
1777
|
+
cwd: request.cwd,
|
|
1778
|
+
security: request.security,
|
|
1779
|
+
sessionKey: request.sessionKey,
|
|
1780
|
+
requestedAt: now2.toISOString(),
|
|
1781
|
+
expiresAt: now2.toISOString(),
|
|
1782
|
+
decision: "allow-once",
|
|
1783
|
+
decidedBy: "auto-approve",
|
|
1784
|
+
decidedAt: now2.toISOString()
|
|
1785
|
+
};
|
|
1786
|
+
this.persistApproval(entry2, null);
|
|
1787
|
+
this.io.emit("safeclaw:execApprovalResolved", entry2);
|
|
1788
|
+
logger.debug(
|
|
1789
|
+
{ command: request.command },
|
|
1790
|
+
"Command auto-approved (not restricted)"
|
|
1791
|
+
);
|
|
1365
1792
|
return;
|
|
1366
1793
|
}
|
|
1367
1794
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -1476,7 +1903,7 @@ var ExecApprovalService = class {
|
|
|
1476
1903
|
decision: entry.decision,
|
|
1477
1904
|
decidedBy: entry.decidedBy,
|
|
1478
1905
|
decidedAt: entry.decidedAt
|
|
1479
|
-
}).where(
|
|
1906
|
+
}).where(eq2(schema_exports.execApprovals.id, entry.id)).run();
|
|
1480
1907
|
} catch (err) {
|
|
1481
1908
|
logger.error(
|
|
1482
1909
|
{ err, id: entry.id },
|
|
@@ -1525,7 +1952,7 @@ var ExecApprovalService = class {
|
|
|
1525
1952
|
);
|
|
1526
1953
|
try {
|
|
1527
1954
|
const db = getDb();
|
|
1528
|
-
db.delete(schema_exports.restrictedPatterns).where(
|
|
1955
|
+
db.delete(schema_exports.restrictedPatterns).where(eq2(schema_exports.restrictedPatterns.pattern, pattern)).run();
|
|
1529
1956
|
} catch (err) {
|
|
1530
1957
|
logger.error({ err, pattern }, "Failed to remove restricted pattern from database");
|
|
1531
1958
|
}
|
|
@@ -1628,7 +2055,7 @@ var ExecApprovalService = class {
|
|
|
1628
2055
|
getHistory(limit = 50) {
|
|
1629
2056
|
try {
|
|
1630
2057
|
const db = getDb();
|
|
1631
|
-
const rows = db.select().from(schema_exports.execApprovals).where(
|
|
2058
|
+
const rows = db.select().from(schema_exports.execApprovals).where(sql3`${schema_exports.execApprovals.decision} IS NOT NULL`).orderBy(desc(schema_exports.execApprovals.decidedAt)).limit(limit).all();
|
|
1632
2059
|
return rows.map((r) => ({
|
|
1633
2060
|
id: r.id,
|
|
1634
2061
|
command: r.command,
|
|
@@ -1691,11 +2118,11 @@ function createExecApprovalService(client, io2, timeoutMs) {
|
|
|
1691
2118
|
}
|
|
1692
2119
|
|
|
1693
2120
|
// src/lib/exec-approvals-config.ts
|
|
1694
|
-
import
|
|
2121
|
+
import fs7 from "fs";
|
|
1695
2122
|
function readExecApprovalsConfig() {
|
|
1696
2123
|
try {
|
|
1697
|
-
if (!
|
|
1698
|
-
const raw =
|
|
2124
|
+
if (!fs7.existsSync(OPENCLAW_EXEC_APPROVALS_PATH)) return null;
|
|
2125
|
+
const raw = fs7.readFileSync(OPENCLAW_EXEC_APPROVALS_PATH, "utf-8");
|
|
1699
2126
|
return JSON.parse(raw);
|
|
1700
2127
|
} catch (err) {
|
|
1701
2128
|
logger.warn({ err }, "Failed to read exec-approvals.json");
|
|
@@ -1703,7 +2130,7 @@ function readExecApprovalsConfig() {
|
|
|
1703
2130
|
}
|
|
1704
2131
|
}
|
|
1705
2132
|
function writeExecApprovalsConfig(config) {
|
|
1706
|
-
|
|
2133
|
+
fs7.writeFileSync(
|
|
1707
2134
|
OPENCLAW_EXEC_APPROVALS_PATH,
|
|
1708
2135
|
JSON.stringify(config, null, 2)
|
|
1709
2136
|
);
|
|
@@ -1789,7 +2216,7 @@ var SECRET_PATTERNS = [
|
|
|
1789
2216
|
function scanForSecrets(content) {
|
|
1790
2217
|
if (!content) return { types: [], maxSeverity: "NONE" };
|
|
1791
2218
|
const found = /* @__PURE__ */ new Set();
|
|
1792
|
-
let
|
|
2219
|
+
let maxSeverity2 = "NONE";
|
|
1793
2220
|
const severityOrder = {
|
|
1794
2221
|
NONE: 0,
|
|
1795
2222
|
LOW: 1,
|
|
@@ -1800,12 +2227,12 @@ function scanForSecrets(content) {
|
|
|
1800
2227
|
for (const { pattern, type, severity } of SECRET_PATTERNS) {
|
|
1801
2228
|
if (pattern.test(content)) {
|
|
1802
2229
|
found.add(type);
|
|
1803
|
-
if (severityOrder[severity] > severityOrder[
|
|
1804
|
-
|
|
2230
|
+
if (severityOrder[severity] > severityOrder[maxSeverity2]) {
|
|
2231
|
+
maxSeverity2 = severity;
|
|
1805
2232
|
}
|
|
1806
2233
|
}
|
|
1807
2234
|
}
|
|
1808
|
-
return { types: Array.from(found), maxSeverity };
|
|
2235
|
+
return { types: Array.from(found), maxSeverity: maxSeverity2 };
|
|
1809
2236
|
}
|
|
1810
2237
|
|
|
1811
2238
|
// src/lib/threat-patterns.ts
|
|
@@ -2442,7 +2869,7 @@ function analyzeActivityThreat(activityType, detail, targetPath, contentPreview,
|
|
|
2442
2869
|
}
|
|
2443
2870
|
|
|
2444
2871
|
// src/services/openclaw-monitor.ts
|
|
2445
|
-
import { eq as
|
|
2872
|
+
import { eq as eq3, ne, desc as desc2, and as and2, sql as sql4 } from "drizzle-orm";
|
|
2446
2873
|
var instance2 = null;
|
|
2447
2874
|
var OpenClawMonitor = class {
|
|
2448
2875
|
client;
|
|
@@ -2507,7 +2934,7 @@ var OpenClawMonitor = class {
|
|
|
2507
2934
|
parsed.toolName
|
|
2508
2935
|
);
|
|
2509
2936
|
const db = getDb();
|
|
2510
|
-
const existingSession = await db.select().from(schema_exports.openclawSessions).where(
|
|
2937
|
+
const existingSession = await db.select().from(schema_exports.openclawSessions).where(eq3(schema_exports.openclawSessions.id, parsed.openclawSessionId)).limit(1);
|
|
2511
2938
|
if (existingSession.length === 0) {
|
|
2512
2939
|
await db.insert(schema_exports.openclawSessions).values({
|
|
2513
2940
|
id: parsed.openclawSessionId,
|
|
@@ -2596,7 +3023,7 @@ var OpenClawMonitor = class {
|
|
|
2596
3023
|
}
|
|
2597
3024
|
async handleSessionStart(sessionId, model) {
|
|
2598
3025
|
const db = getDb();
|
|
2599
|
-
const existing = await db.select().from(schema_exports.openclawSessions).where(
|
|
3026
|
+
const existing = await db.select().from(schema_exports.openclawSessions).where(eq3(schema_exports.openclawSessions.id, sessionId)).limit(1);
|
|
2600
3027
|
if (existing.length === 0) {
|
|
2601
3028
|
await db.insert(schema_exports.openclawSessions).values({
|
|
2602
3029
|
id: sessionId,
|
|
@@ -2604,7 +3031,7 @@ var OpenClawMonitor = class {
|
|
|
2604
3031
|
model: model ?? null
|
|
2605
3032
|
});
|
|
2606
3033
|
} else {
|
|
2607
|
-
await db.update(schema_exports.openclawSessions).set({ status: "ACTIVE", model: model ?? null }).where(
|
|
3034
|
+
await db.update(schema_exports.openclawSessions).set({ status: "ACTIVE", model: model ?? null }).where(eq3(schema_exports.openclawSessions.id, sessionId));
|
|
2608
3035
|
}
|
|
2609
3036
|
const session = await this.buildSessionPayload(sessionId);
|
|
2610
3037
|
if (session) {
|
|
@@ -2615,8 +3042,8 @@ var OpenClawMonitor = class {
|
|
|
2615
3042
|
const db = getDb();
|
|
2616
3043
|
await db.update(schema_exports.openclawSessions).set({
|
|
2617
3044
|
status: "ENDED",
|
|
2618
|
-
endedAt:
|
|
2619
|
-
}).where(
|
|
3045
|
+
endedAt: sql4`datetime('now')`
|
|
3046
|
+
}).where(eq3(schema_exports.openclawSessions.id, sessionId));
|
|
2620
3047
|
const session = await this.buildSessionPayload(sessionId);
|
|
2621
3048
|
if (session) {
|
|
2622
3049
|
this.io.emit("safeclaw:openclawSessionUpdate", session);
|
|
@@ -2624,9 +3051,9 @@ var OpenClawMonitor = class {
|
|
|
2624
3051
|
}
|
|
2625
3052
|
async buildSessionPayload(sessionId) {
|
|
2626
3053
|
const db = getDb();
|
|
2627
|
-
const [row] = await db.select().from(schema_exports.openclawSessions).where(
|
|
3054
|
+
const [row] = await db.select().from(schema_exports.openclawSessions).where(eq3(schema_exports.openclawSessions.id, sessionId));
|
|
2628
3055
|
if (!row) return null;
|
|
2629
|
-
const activities = await db.select().from(schema_exports.agentActivities).where(
|
|
3056
|
+
const activities = await db.select().from(schema_exports.agentActivities).where(eq3(schema_exports.agentActivities.openclawSessionId, sessionId));
|
|
2630
3057
|
const threatSummary = {
|
|
2631
3058
|
NONE: 0,
|
|
2632
3059
|
LOW: 0,
|
|
@@ -2688,8 +3115,8 @@ var OpenClawMonitor = class {
|
|
|
2688
3115
|
await db.update(schema_exports.agentActivities).set({
|
|
2689
3116
|
resolved: resolved ? 1 : 0,
|
|
2690
3117
|
resolvedAt: resolved ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
2691
|
-
}).where(
|
|
2692
|
-
const [row] = await db.select().from(schema_exports.agentActivities).where(
|
|
3118
|
+
}).where(eq3(schema_exports.agentActivities.id, activityId));
|
|
3119
|
+
const [row] = await db.select().from(schema_exports.agentActivities).where(eq3(schema_exports.agentActivities.id, activityId));
|
|
2693
3120
|
if (!row) return null;
|
|
2694
3121
|
return this.mapRowToActivity(row);
|
|
2695
3122
|
}
|
|
@@ -2697,19 +3124,19 @@ var OpenClawMonitor = class {
|
|
|
2697
3124
|
const db = getDb();
|
|
2698
3125
|
const conditions = [ne(schema_exports.agentActivities.threatLevel, "NONE")];
|
|
2699
3126
|
if (severity) {
|
|
2700
|
-
conditions.push(
|
|
3127
|
+
conditions.push(eq3(schema_exports.agentActivities.threatLevel, severity));
|
|
2701
3128
|
}
|
|
2702
3129
|
if (resolved !== void 0) {
|
|
2703
|
-
conditions.push(
|
|
3130
|
+
conditions.push(eq3(schema_exports.agentActivities.resolved, resolved ? 1 : 0));
|
|
2704
3131
|
}
|
|
2705
|
-
const rows = await db.select().from(schema_exports.agentActivities).where(
|
|
3132
|
+
const rows = await db.select().from(schema_exports.agentActivities).where(and2(...conditions)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
|
|
2706
3133
|
return rows.map((r) => this.mapRowToActivity(r));
|
|
2707
3134
|
}
|
|
2708
3135
|
async getActivities(sessionId, limit = 50) {
|
|
2709
3136
|
const db = getDb();
|
|
2710
3137
|
let rows;
|
|
2711
3138
|
if (sessionId) {
|
|
2712
|
-
rows = await db.select().from(schema_exports.agentActivities).where(
|
|
3139
|
+
rows = await db.select().from(schema_exports.agentActivities).where(eq3(schema_exports.agentActivities.openclawSessionId, sessionId)).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
|
|
2713
3140
|
} else {
|
|
2714
3141
|
rows = await db.select().from(schema_exports.agentActivities).orderBy(desc2(schema_exports.agentActivities.id)).limit(limit);
|
|
2715
3142
|
}
|
|
@@ -2764,222 +3191,130 @@ function getOpenClawMonitor() {
|
|
|
2764
3191
|
return instance2;
|
|
2765
3192
|
}
|
|
2766
3193
|
|
|
2767
|
-
// src/
|
|
2768
|
-
import
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
3194
|
+
// src/lib/srt-config.ts
|
|
3195
|
+
import fs8 from "fs";
|
|
3196
|
+
import { execSync } from "child_process";
|
|
3197
|
+
var DEFAULT_SRT_SETTINGS = {
|
|
3198
|
+
network: {
|
|
3199
|
+
allowedDomains: [],
|
|
3200
|
+
deniedDomains: [],
|
|
3201
|
+
allowLocalBinding: false
|
|
3202
|
+
},
|
|
3203
|
+
filesystem: {
|
|
3204
|
+
denyRead: [],
|
|
3205
|
+
allowWrite: [],
|
|
3206
|
+
denyWrite: []
|
|
3207
|
+
}
|
|
2773
3208
|
};
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
const toolsDenyBlocked = denyList.includes(`mcp__${name}`);
|
|
2779
|
-
return {
|
|
2780
|
-
name,
|
|
2781
|
-
pluginEnabled,
|
|
2782
|
-
toolsDenyBlocked,
|
|
2783
|
-
effectivelyEnabled: pluginEnabled && !toolsDenyBlocked
|
|
2784
|
-
};
|
|
2785
|
-
});
|
|
3209
|
+
var srtInstalledCache = null;
|
|
3210
|
+
function getSrtSettingsPath() {
|
|
3211
|
+
const config = readConfig();
|
|
3212
|
+
return config.srt?.settingsPath ?? SRT_SETTINGS_PATH;
|
|
2786
3213
|
}
|
|
2787
|
-
function
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
],
|
|
2797
|
-
mcpServers: [],
|
|
2798
|
-
openclawConfigAvailable: false
|
|
2799
|
-
};
|
|
3214
|
+
function isSrtInstalled() {
|
|
3215
|
+
if (srtInstalledCache?.installed) return srtInstalledCache;
|
|
3216
|
+
try {
|
|
3217
|
+
const output = execSync("srt --version", { timeout: 3e3, encoding: "utf-8" }).trim();
|
|
3218
|
+
const version = output.replace(/^srt\s*/i, "").trim() || output;
|
|
3219
|
+
srtInstalledCache = { installed: true, version };
|
|
3220
|
+
return srtInstalledCache;
|
|
3221
|
+
} catch {
|
|
3222
|
+
return { installed: false, version: null };
|
|
2800
3223
|
}
|
|
2801
|
-
const denyList = config.tools?.deny ?? [];
|
|
2802
|
-
const filesystemEnabled = !denyList.includes("group:fs");
|
|
2803
|
-
const systemCommandsEnabled = !denyList.includes("group:runtime");
|
|
2804
|
-
const networkEnabled = !denyList.includes("group:web") && config.browser?.enabled !== false;
|
|
2805
|
-
const pluginEntries = config.plugins?.entries ?? {};
|
|
2806
|
-
const pluginNames = Object.keys(pluginEntries);
|
|
2807
|
-
const mcpEnabled = pluginNames.length === 0 || pluginNames.some((name) => pluginEntries[name].enabled !== false);
|
|
2808
|
-
const toggles = [
|
|
2809
|
-
{ category: "filesystem", enabled: filesystemEnabled },
|
|
2810
|
-
{ category: "mcp_servers", enabled: mcpEnabled },
|
|
2811
|
-
{ category: "network", enabled: networkEnabled },
|
|
2812
|
-
{ category: "system_commands", enabled: systemCommandsEnabled }
|
|
2813
|
-
];
|
|
2814
|
-
const mcpServers = deriveMcpServerStates(config, denyList);
|
|
2815
|
-
return { toggles, mcpServers, openclawConfigAvailable: true };
|
|
2816
3224
|
}
|
|
2817
|
-
|
|
2818
|
-
const
|
|
2819
|
-
if (!
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
applyNetworkToggle(config, enabled);
|
|
2826
|
-
} else {
|
|
2827
|
-
applyToolGroupToggle(config, category, enabled);
|
|
3225
|
+
function readSrtSettings() {
|
|
3226
|
+
const settingsPath = getSrtSettingsPath();
|
|
3227
|
+
if (!fs8.existsSync(settingsPath)) return null;
|
|
3228
|
+
try {
|
|
3229
|
+
const raw = JSON.parse(fs8.readFileSync(settingsPath, "utf-8"));
|
|
3230
|
+
return srtSettingsSchema.parse(raw);
|
|
3231
|
+
} catch {
|
|
3232
|
+
return null;
|
|
2828
3233
|
}
|
|
2829
|
-
await updateAuditDb(category, enabled);
|
|
2830
|
-
return deriveAccessState();
|
|
2831
3234
|
}
|
|
2832
|
-
|
|
2833
|
-
const
|
|
2834
|
-
|
|
2835
|
-
throw new Error("OpenClaw config not found");
|
|
2836
|
-
}
|
|
2837
|
-
const denyPattern = `mcp__${serverName}`;
|
|
2838
|
-
const currentDeny = [...config.tools?.deny ?? []];
|
|
2839
|
-
if (enabled) {
|
|
2840
|
-
const filtered = currentDeny.filter((entry) => entry !== denyPattern);
|
|
2841
|
-
writeOpenClawConfig({ tools: { deny: filtered } });
|
|
2842
|
-
} else {
|
|
2843
|
-
if (!currentDeny.includes(denyPattern)) {
|
|
2844
|
-
currentDeny.push(denyPattern);
|
|
2845
|
-
}
|
|
2846
|
-
writeOpenClawConfig({ tools: { deny: currentDeny } });
|
|
2847
|
-
}
|
|
2848
|
-
await updateAuditDb(`mcp_server:${serverName}`, enabled);
|
|
2849
|
-
return deriveAccessState();
|
|
3235
|
+
function writeSrtSettings(settings) {
|
|
3236
|
+
const settingsPath = getSrtSettingsPath();
|
|
3237
|
+
fs8.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
2850
3238
|
}
|
|
2851
|
-
function
|
|
2852
|
-
const
|
|
2853
|
-
if (
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const filtered = currentDeny.filter((entry) => entry !== groupName);
|
|
2857
|
-
writeOpenClawConfig({ tools: { deny: filtered } });
|
|
2858
|
-
} else {
|
|
2859
|
-
if (!currentDeny.includes(groupName)) {
|
|
2860
|
-
currentDeny.push(groupName);
|
|
2861
|
-
}
|
|
2862
|
-
writeOpenClawConfig({ tools: { deny: currentDeny } });
|
|
2863
|
-
}
|
|
3239
|
+
function ensureSrtSettings() {
|
|
3240
|
+
const existing = readSrtSettings();
|
|
3241
|
+
if (existing) return existing;
|
|
3242
|
+
writeSrtSettings(DEFAULT_SRT_SETTINGS);
|
|
3243
|
+
return DEFAULT_SRT_SETTINGS;
|
|
2864
3244
|
}
|
|
2865
|
-
function
|
|
2866
|
-
const
|
|
2867
|
-
const
|
|
3245
|
+
function getSrtStatus() {
|
|
3246
|
+
const { installed, version } = isSrtInstalled();
|
|
3247
|
+
const config = readConfig();
|
|
3248
|
+
const enabled = config.srt?.enabled ?? false;
|
|
3249
|
+
const settingsPath = getSrtSettingsPath();
|
|
3250
|
+
const settings = readSrtSettings();
|
|
3251
|
+
return { installed, version, enabled, settingsPath, settings };
|
|
3252
|
+
}
|
|
3253
|
+
function toggleSrt(enabled) {
|
|
3254
|
+
const config = readConfig();
|
|
3255
|
+
writeConfig({
|
|
3256
|
+
...config,
|
|
3257
|
+
srt: { ...config.srt, enabled }
|
|
3258
|
+
});
|
|
2868
3259
|
if (enabled) {
|
|
2869
|
-
|
|
2870
|
-
writeOpenClawConfig({
|
|
2871
|
-
tools: { deny: filtered },
|
|
2872
|
-
browser: { enabled: true }
|
|
2873
|
-
});
|
|
2874
|
-
} else {
|
|
2875
|
-
if (!currentDeny.includes(groupName)) {
|
|
2876
|
-
currentDeny.push(groupName);
|
|
2877
|
-
}
|
|
2878
|
-
writeOpenClawConfig({
|
|
2879
|
-
tools: { deny: currentDeny },
|
|
2880
|
-
browser: { enabled: false }
|
|
2881
|
-
});
|
|
3260
|
+
ensureSrtSettings();
|
|
2882
3261
|
}
|
|
3262
|
+
return getSrtStatus();
|
|
2883
3263
|
}
|
|
2884
|
-
|
|
2885
|
-
const
|
|
2886
|
-
const
|
|
2887
|
-
if (
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
}
|
|
2894
|
-
await savePreviousPluginState(stateMap);
|
|
2895
|
-
const disabledEntries = {};
|
|
2896
|
-
for (const name of pluginNames) {
|
|
2897
|
-
disabledEntries[name] = { enabled: false };
|
|
2898
|
-
}
|
|
2899
|
-
for (const name of pluginNames) {
|
|
2900
|
-
const denyPattern = `mcp__${name}`;
|
|
2901
|
-
if (!currentDeny.includes(denyPattern)) {
|
|
2902
|
-
currentDeny.push(denyPattern);
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
writeOpenClawConfig({
|
|
2906
|
-
plugins: { entries: disabledEntries },
|
|
2907
|
-
tools: { deny: currentDeny }
|
|
2908
|
-
});
|
|
2909
|
-
} else {
|
|
2910
|
-
const previousState = await loadPreviousPluginState();
|
|
2911
|
-
const restoredEntries = {};
|
|
2912
|
-
for (const name of pluginNames) {
|
|
2913
|
-
restoredEntries[name] = {
|
|
2914
|
-
enabled: previousState?.[name] ?? true
|
|
2915
|
-
};
|
|
2916
|
-
}
|
|
2917
|
-
const mcpDenyPatterns = new Set(pluginNames.map((n) => `mcp__${n}`));
|
|
2918
|
-
const filteredDeny = currentDeny.filter(
|
|
2919
|
-
(entry) => !mcpDenyPatterns.has(entry)
|
|
2920
|
-
);
|
|
2921
|
-
writeOpenClawConfig({
|
|
2922
|
-
plugins: { entries: restoredEntries },
|
|
2923
|
-
tools: { deny: filteredDeny }
|
|
2924
|
-
});
|
|
3264
|
+
function addAllowedDomain(domain) {
|
|
3265
|
+
const settings = ensureSrtSettings();
|
|
3266
|
+
const normalized = domain.trim().toLowerCase();
|
|
3267
|
+
if (!normalized) return settings;
|
|
3268
|
+
settings.network.deniedDomains = settings.network.deniedDomains.filter(
|
|
3269
|
+
(d) => d !== normalized
|
|
3270
|
+
);
|
|
3271
|
+
if (!settings.network.allowedDomains.includes(normalized)) {
|
|
3272
|
+
settings.network.allowedDomains.push(normalized);
|
|
2925
3273
|
}
|
|
3274
|
+
writeSrtSettings(settings);
|
|
3275
|
+
return settings;
|
|
2926
3276
|
}
|
|
2927
|
-
|
|
2928
|
-
const
|
|
2929
|
-
const
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
eq3(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
2933
|
-
)
|
|
3277
|
+
function removeAllowedDomain(domain) {
|
|
3278
|
+
const settings = ensureSrtSettings();
|
|
3279
|
+
const normalized = domain.trim().toLowerCase();
|
|
3280
|
+
settings.network.allowedDomains = settings.network.allowedDomains.filter(
|
|
3281
|
+
(d) => d !== normalized
|
|
2934
3282
|
);
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
value: JSON.stringify(stateMap),
|
|
2938
|
-
updatedAt: sql4`datetime('now')`
|
|
2939
|
-
}).where(
|
|
2940
|
-
and2(
|
|
2941
|
-
eq3(schema_exports.accessConfig.category, "mcp_servers"),
|
|
2942
|
-
eq3(schema_exports.accessConfig.key, "previous_plugin_state")
|
|
2943
|
-
)
|
|
2944
|
-
);
|
|
2945
|
-
} else {
|
|
2946
|
-
await db.insert(schema_exports.accessConfig).values({
|
|
2947
|
-
category: "mcp_servers",
|
|
2948
|
-
key: "previous_plugin_state",
|
|
2949
|
-
value: JSON.stringify(stateMap)
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
3283
|
+
writeSrtSettings(settings);
|
|
3284
|
+
return settings;
|
|
2952
3285
|
}
|
|
2953
|
-
|
|
2954
|
-
const
|
|
2955
|
-
const
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
)
|
|
3286
|
+
function addDeniedDomain(domain) {
|
|
3287
|
+
const settings = ensureSrtSettings();
|
|
3288
|
+
const normalized = domain.trim().toLowerCase();
|
|
3289
|
+
if (!normalized) return settings;
|
|
3290
|
+
settings.network.allowedDomains = settings.network.allowedDomains.filter(
|
|
3291
|
+
(d) => d !== normalized
|
|
2960
3292
|
);
|
|
2961
|
-
if (
|
|
2962
|
-
|
|
2963
|
-
return JSON.parse(rows[0].value);
|
|
2964
|
-
} catch {
|
|
2965
|
-
return null;
|
|
3293
|
+
if (!settings.network.deniedDomains.includes(normalized)) {
|
|
3294
|
+
settings.network.deniedDomains.push(normalized);
|
|
2966
3295
|
}
|
|
3296
|
+
writeSrtSettings(settings);
|
|
3297
|
+
return settings;
|
|
2967
3298
|
}
|
|
2968
|
-
|
|
2969
|
-
const
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
logger.warn({ err, category, enabled }, "Failed to update audit DB");
|
|
3299
|
+
function removeDeniedDomain(domain) {
|
|
3300
|
+
const settings = ensureSrtSettings();
|
|
3301
|
+
const normalized = domain.trim().toLowerCase();
|
|
3302
|
+
settings.network.deniedDomains = settings.network.deniedDomains.filter(
|
|
3303
|
+
(d) => d !== normalized
|
|
3304
|
+
);
|
|
3305
|
+
writeSrtSettings(settings);
|
|
3306
|
+
return settings;
|
|
3307
|
+
}
|
|
3308
|
+
function updateSrtSettings(updates) {
|
|
3309
|
+
const settings = ensureSrtSettings();
|
|
3310
|
+
if (updates.network) {
|
|
3311
|
+
settings.network = { ...settings.network, ...updates.network };
|
|
2982
3312
|
}
|
|
3313
|
+
if (updates.filesystem) {
|
|
3314
|
+
settings.filesystem = { ...settings.filesystem, ...updates.filesystem };
|
|
3315
|
+
}
|
|
3316
|
+
writeSrtSettings(settings);
|
|
3317
|
+
return settings;
|
|
2983
3318
|
}
|
|
2984
3319
|
|
|
2985
3320
|
// src/server/socket.ts
|
|
@@ -3234,6 +3569,38 @@ function setupSocketIO(httpServer) {
|
|
|
3234
3569
|
logger.error({ err, pattern }, "Failed to remove restricted pattern");
|
|
3235
3570
|
}
|
|
3236
3571
|
});
|
|
3572
|
+
socket.on("safeclaw:getSrtStatus", () => {
|
|
3573
|
+
socket.emit("safeclaw:srtStatus", getSrtStatus());
|
|
3574
|
+
});
|
|
3575
|
+
socket.on("safeclaw:toggleSrt", ({ enabled }) => {
|
|
3576
|
+
const status = toggleSrt(enabled);
|
|
3577
|
+
logger.info(`SRT toggled: enabled=${enabled}`);
|
|
3578
|
+
io.emit("safeclaw:srtStatus", status);
|
|
3579
|
+
});
|
|
3580
|
+
socket.on("safeclaw:updateSrtDomains", ({ list, action, domain }) => {
|
|
3581
|
+
try {
|
|
3582
|
+
if (list === "allow") {
|
|
3583
|
+
if (action === "add") addAllowedDomain(domain);
|
|
3584
|
+
else removeAllowedDomain(domain);
|
|
3585
|
+
} else {
|
|
3586
|
+
if (action === "add") addDeniedDomain(domain);
|
|
3587
|
+
else removeDeniedDomain(domain);
|
|
3588
|
+
}
|
|
3589
|
+
logger.info({ list, action, domain }, "SRT domain updated");
|
|
3590
|
+
io.emit("safeclaw:srtStatus", getSrtStatus());
|
|
3591
|
+
} catch (err) {
|
|
3592
|
+
logger.error({ err, list, action, domain }, "Failed to update SRT domain");
|
|
3593
|
+
}
|
|
3594
|
+
});
|
|
3595
|
+
socket.on("safeclaw:updateSrtSettings", (updates) => {
|
|
3596
|
+
try {
|
|
3597
|
+
updateSrtSettings(updates);
|
|
3598
|
+
logger.info({ updates }, "SRT settings updated");
|
|
3599
|
+
io.emit("safeclaw:srtStatus", getSrtStatus());
|
|
3600
|
+
} catch (err) {
|
|
3601
|
+
logger.error({ err }, "Failed to update SRT settings");
|
|
3602
|
+
}
|
|
3603
|
+
});
|
|
3237
3604
|
socket.on("disconnect", () => {
|
|
3238
3605
|
logger.info(`Client disconnected: ${socket.id}`);
|
|
3239
3606
|
});
|
|
@@ -3242,7 +3609,1054 @@ function setupSocketIO(httpServer) {
|
|
|
3242
3609
|
}
|
|
3243
3610
|
|
|
3244
3611
|
// src/server/routes.ts
|
|
3245
|
-
import { desc as desc4, eq as
|
|
3612
|
+
import { desc as desc4, eq as eq6, and as and5, sql as sql7 } from "drizzle-orm";
|
|
3613
|
+
|
|
3614
|
+
// src/lib/skill-scanner-patterns.ts
|
|
3615
|
+
var HIDDEN_CONTENT_PATTERNS = [
|
|
3616
|
+
// HTML comments with instructional content
|
|
3617
|
+
{ pattern: /<!--[\s\S]*?(?:instruction|command|execute|ignore|override|system|prompt|inject|do not|must|should|always|never)[\s\S]*?-->/i, label: "HTML comment with instructions", severity: "CRITICAL" },
|
|
3618
|
+
{ pattern: /<!--[\s\S]{50,}?-->/s, label: "Large HTML comment (>50 chars)", severity: "HIGH" },
|
|
3619
|
+
// Zero-width Unicode characters
|
|
3620
|
+
{ pattern: /[\u200B\u200C\u200D\u200E\u200F]/, label: "Zero-width Unicode character (U+200B-200F)", severity: "CRITICAL" },
|
|
3621
|
+
{ pattern: /[\u2060\u2061\u2062\u2063\u2064]/, label: "Invisible Unicode separator (U+2060-2064)", severity: "CRITICAL" },
|
|
3622
|
+
{ pattern: /[\uFEFF]/, label: "Zero-width no-break space (BOM)", severity: "HIGH" },
|
|
3623
|
+
// CSS hiding
|
|
3624
|
+
{ pattern: /display\s*:\s*none/i, label: "CSS display:none", severity: "HIGH" },
|
|
3625
|
+
{ pattern: /opacity\s*:\s*0(?:[;\s]|$)/i, label: "CSS opacity:0", severity: "HIGH" },
|
|
3626
|
+
{ pattern: /visibility\s*:\s*hidden/i, label: "CSS visibility:hidden", severity: "HIGH" },
|
|
3627
|
+
// Bidi overrides
|
|
3628
|
+
{ pattern: /[\u202A\u202B\u202C\u202D\u202E]/, label: "Bidi override character", severity: "CRITICAL" },
|
|
3629
|
+
{ pattern: /[\u2066\u2067\u2068\u2069]/, label: "Bidi isolate character", severity: "CRITICAL" }
|
|
3630
|
+
];
|
|
3631
|
+
var PROMPT_INJECTION_PATTERNS = [
|
|
3632
|
+
{ pattern: /\b(?:ignore|disregard|forget|override)\s+(?:all\s+)?(?:previous|prior|above|your)\s+(?:instructions?|rules?|prompts?|guidelines?|constraints?)/i, label: "Override previous instructions", severity: "CRITICAL" },
|
|
3633
|
+
{ pattern: /\b(?:you\s+(?:are|must|should|will)\s+(?:now|henceforth|from\s+now)\s+(?:be|act|behave|respond)\s+(?:as|like))/i, label: "Role reassignment", severity: "CRITICAL" },
|
|
3634
|
+
{ pattern: /\bnew\s+instructions?\s*:/i, label: "New instructions directive", severity: "CRITICAL" },
|
|
3635
|
+
{ pattern: /\[INST\]|\[\/INST\]|<\|im_start\|>|<\|im_end\|>/i, label: "Special model tokens", severity: "CRITICAL" },
|
|
3636
|
+
{ pattern: /\bsystem\s*prompt\s*(?:override|injection|:)/i, label: "System prompt manipulation", severity: "CRITICAL" },
|
|
3637
|
+
{ pattern: /\bdo\s+not\s+(?:follow|obey|listen\s+to)\s+(?:the|your|any)\s+(?:original|previous|prior)/i, label: "Instruction disobedience", severity: "CRITICAL" },
|
|
3638
|
+
{ pattern: /\b(?:IMPORTANT|CRITICAL|URGENT)\s*:\s*(?:ignore|override|disregard|you must)/i, label: "Urgent override phrasing", severity: "HIGH" },
|
|
3639
|
+
{ pattern: /\b(?:pretend|imagine|suppose)\s+(?:you\s+are|that\s+you)/i, label: "Persona manipulation", severity: "MEDIUM" },
|
|
3640
|
+
{ pattern: /\bact\s+as\s+(?:a|an|if)\b/i, label: "Act-as directive", severity: "MEDIUM" }
|
|
3641
|
+
];
|
|
3642
|
+
var SHELL_EXECUTION_PATTERNS = [
|
|
3643
|
+
{ pattern: /(?:curl|wget)\s+[^\n]*\|\s*(?:sh|bash|zsh|node|python)/i, label: "Download and execute (curl|bash)", severity: "CRITICAL" },
|
|
3644
|
+
{ pattern: /\beval\s*\(/, label: "eval() call", severity: "CRITICAL" },
|
|
3645
|
+
{ pattern: /\bexec\s*\(/, label: "exec() call", severity: "HIGH" },
|
|
3646
|
+
{ pattern: /\bnpx\s+-y\s+/, label: "npx -y (auto-confirm remote package)", severity: "HIGH" },
|
|
3647
|
+
{ pattern: /\/dev\/tcp\//, label: "Reverse shell via /dev/tcp", severity: "CRITICAL" },
|
|
3648
|
+
{ pattern: /\bnc\s+.*-e\s+/, label: "Netcat with exec (nc -e)", severity: "CRITICAL" },
|
|
3649
|
+
{ pattern: /\bmkfifo\b.*\bnc\b/s, label: "Named pipe + netcat (reverse shell)", severity: "CRITICAL" },
|
|
3650
|
+
{ pattern: /python[23]?\s+-c\s+.*(?:socket|subprocess|os\.system)/i, label: "Python one-liner with system access", severity: "CRITICAL" },
|
|
3651
|
+
{ pattern: /\bphp\s+-r\s+.*(?:exec|system|passthru|shell_exec)/i, label: "PHP exec one-liner", severity: "CRITICAL" },
|
|
3652
|
+
{ pattern: /\bperl\s+-e\s+.*(?:socket|exec|system)/i, label: "Perl exec one-liner", severity: "CRITICAL" },
|
|
3653
|
+
{ pattern: /\bruby\s+-e\s+.*(?:exec|system|spawn)/i, label: "Ruby exec one-liner", severity: "CRITICAL" },
|
|
3654
|
+
{ pattern: /\bchmod\s+\+x\b/, label: "Make file executable", severity: "MEDIUM" }
|
|
3655
|
+
];
|
|
3656
|
+
var DATA_EXFILTRATION_PATTERNS = [
|
|
3657
|
+
{ pattern: /pastebin\.com/, label: "pastebin.com", severity: "HIGH" },
|
|
3658
|
+
{ pattern: /paste\.ee/, label: "paste.ee", severity: "HIGH" },
|
|
3659
|
+
{ pattern: /transfer\.sh/, label: "transfer.sh", severity: "HIGH" },
|
|
3660
|
+
{ pattern: /ngrok\.io/, label: "ngrok.io", severity: "HIGH" },
|
|
3661
|
+
{ pattern: /requestbin/i, label: "requestbin", severity: "HIGH" },
|
|
3662
|
+
{ pattern: /webhook\.site/, label: "webhook.site", severity: "HIGH" },
|
|
3663
|
+
{ pattern: /pipedream\.net/, label: "pipedream.net", severity: "HIGH" },
|
|
3664
|
+
{ pattern: /hookbin\.com/, label: "hookbin.com", severity: "HIGH" },
|
|
3665
|
+
{ pattern: /beeceptor\.com/, label: "beeceptor.com", severity: "HIGH" },
|
|
3666
|
+
{ pattern: /postb\.in/, label: "postb.in", severity: "HIGH" },
|
|
3667
|
+
{ pattern: /https?:\/\/hooks\.slack\.com\/services\//, label: "Slack webhook URL", severity: "HIGH" },
|
|
3668
|
+
{ pattern: /https?:\/\/discord(?:app)?\.com\/api\/webhooks\//, label: "Discord webhook URL", severity: "HIGH" },
|
|
3669
|
+
{ pattern: /https?:\/\/api\.telegram\.org\/bot/, label: "Telegram bot API URL", severity: "HIGH" },
|
|
3670
|
+
{ pattern: /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/, label: "Raw IP URL", severity: "HIGH" }
|
|
3671
|
+
];
|
|
3672
|
+
var MEMORY_POISONING_PATTERNS = [
|
|
3673
|
+
{ pattern: /\bSOUL\.md\b/, label: "SOUL.md reference", severity: "CRITICAL" },
|
|
3674
|
+
{ pattern: /\bMEMORY\.md\b/, label: "MEMORY.md reference", severity: "CRITICAL" },
|
|
3675
|
+
{ pattern: /\.claude\//, label: ".claude/ directory reference", severity: "CRITICAL" },
|
|
3676
|
+
{ pattern: /\bCLAUDE\.md\b/, label: "CLAUDE.md reference", severity: "CRITICAL" },
|
|
3677
|
+
{ pattern: /\.cursorrules\b/, label: ".cursorrules reference", severity: "CRITICAL" },
|
|
3678
|
+
{ pattern: /\.windsurfrules\b/, label: ".windsurfrules reference", severity: "CRITICAL" },
|
|
3679
|
+
{ pattern: /\.clinerules\b/, label: ".clinerules reference", severity: "HIGH" },
|
|
3680
|
+
{ pattern: /\bCODEX\.md\b/, label: "CODEX.md reference", severity: "HIGH" },
|
|
3681
|
+
{ pattern: /(?:write|modify|edit|append|create|overwrite)\s+(?:to\s+)?(?:the\s+)?(?:SOUL|MEMORY|CLAUDE|CODEX)\.md/i, label: "Instruction to modify agent config file", severity: "CRITICAL" },
|
|
3682
|
+
{ pattern: /(?:write|modify|edit|append|create|overwrite)\s+(?:to\s+)?(?:the\s+)?\.(?:cursorrules|windsurfrules|clinerules)/i, label: "Instruction to modify IDE rules file", severity: "CRITICAL" }
|
|
3683
|
+
];
|
|
3684
|
+
var SUPPLY_CHAIN_PATTERNS = [
|
|
3685
|
+
{ pattern: /https?:\/\/raw\.githubusercontent\.com\/[^\s]+\.(?:sh|py|js|rb|pl)\b/, label: "Raw GitHub script URL", severity: "HIGH" },
|
|
3686
|
+
{ pattern: /https?:\/\/[^\s]+\.(?:sh|py|js|rb|pl)(?:\s|$)/, label: "External script URL", severity: "HIGH" },
|
|
3687
|
+
{ pattern: /\bnpm\s+install\s+(?:-g\s+)?[a-zA-Z@]/, label: "npm install command", severity: "HIGH" },
|
|
3688
|
+
{ pattern: /\bpip\s+install\s+[a-zA-Z]/, label: "pip install command", severity: "HIGH" },
|
|
3689
|
+
{ pattern: /\bgem\s+install\s+/, label: "gem install command", severity: "HIGH" },
|
|
3690
|
+
{ pattern: /\bcargo\s+install\s+/, label: "cargo install command", severity: "MEDIUM" },
|
|
3691
|
+
{ pattern: /\bgo\s+install\s+/, label: "go install command", severity: "MEDIUM" },
|
|
3692
|
+
{ pattern: /\bbrew\s+install\s+/, label: "brew install command", severity: "MEDIUM" }
|
|
3693
|
+
];
|
|
3694
|
+
var ENCODED_PAYLOAD_PATTERNS = [
|
|
3695
|
+
{ pattern: /[A-Za-z0-9+/]{40,}={0,2}/, label: "Base64 string (>40 chars)", severity: "HIGH" },
|
|
3696
|
+
{ pattern: /\batob\s*\(/, label: "atob() call (base64 decode)", severity: "HIGH" },
|
|
3697
|
+
{ pattern: /\bbtoa\s*\(/, label: "btoa() call (base64 encode)", severity: "MEDIUM" },
|
|
3698
|
+
{ pattern: /\bBuffer\.from\s*\([^)]*,\s*['"]base64['"]\)/, label: "Buffer.from base64", severity: "HIGH" },
|
|
3699
|
+
{ pattern: /(?:\\x[0-9a-fA-F]{2}){8,}/, label: "Hex escape sequence (8+ bytes)", severity: "HIGH" },
|
|
3700
|
+
{ pattern: /String\.fromCharCode\s*\((?:\s*\d+\s*,?\s*){5,}\)/, label: "String.fromCharCode (5+ codes)", severity: "HIGH" },
|
|
3701
|
+
{ pattern: /\|\s*base64\s+(?:-d|--decode)/, label: "Piped base64 decode", severity: "CRITICAL" }
|
|
3702
|
+
];
|
|
3703
|
+
var IMAGE_EXFIL_PATTERNS = [
|
|
3704
|
+
// URL-based exfiltration (original 5)
|
|
3705
|
+
{ pattern: /!\[.*?\]\(https?:\/\/[^\s)]*(?:\?|&)(?:data|content|file|token|secret|key|password|env)=[^\s)]*\)/i, label: "Image URL with exfil query params", severity: "CRITICAL" },
|
|
3706
|
+
{ pattern: /!\[.*?\]\(https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[^\s)]*\)/, label: "Image from raw IP address", severity: "CRITICAL" },
|
|
3707
|
+
{ pattern: /!\[.*?\]\([^)]*\$\{[^}]+\}[^)]*\)/, label: "Variable interpolation in image URL", severity: "CRITICAL" },
|
|
3708
|
+
{ pattern: /!\[.*?\]\([^)]*\$\([^)]+\)[^)]*\)/, label: "Command substitution in image URL", severity: "CRITICAL" },
|
|
3709
|
+
{ pattern: /<img[^>]+src\s*=\s*["'][^"']*\$\{[^}]+\}[^"']*["']/i, label: "Variable interpolation in img src", severity: "CRITICAL" },
|
|
3710
|
+
// Group A: Data URI image payloads
|
|
3711
|
+
{ pattern: /!\[.*?\]\(data:image\/[^\s)]+\)/i, label: "Data URI image in markdown", severity: "CRITICAL" },
|
|
3712
|
+
{ pattern: /<img[^>]+src\s*=\s*["']data:image\/[^"']+["']/i, label: "Data URI image in HTML img tag", severity: "CRITICAL" },
|
|
3713
|
+
{ pattern: /data:image\/[^;]+;base64,[A-Za-z0-9+/]{200,}/, label: "Large base64 data URI (steganographic carrier)", severity: "CRITICAL" },
|
|
3714
|
+
// Group B: SVG embedded scripts & data
|
|
3715
|
+
{ pattern: /<svg[\s>][\s\S]*?<script[\s>]/i, label: "SVG with embedded script tag", severity: "CRITICAL" },
|
|
3716
|
+
{ pattern: /<svg[\s>][\s\S]*?\bon(?:load|error|click|mouseover)\s*=/i, label: "SVG with event handler", severity: "CRITICAL" },
|
|
3717
|
+
{ pattern: /<svg[\s>][\s\S]*?<foreignObject[\s>]/i, label: "SVG with foreignObject (arbitrary HTML embed)", severity: "HIGH" },
|
|
3718
|
+
{ pattern: /data:image\/svg\+xml[^"'\s)]+/i, label: "SVG data URI (inline payload + script risk)", severity: "CRITICAL" },
|
|
3719
|
+
// Group C: Web beacons / tracking pixels
|
|
3720
|
+
{ pattern: /<img[^>]+(?:width\s*=\s*["']?1["']?[^>]+height\s*=\s*["']?1["']?|height\s*=\s*["']?1["']?[^>]+width\s*=\s*["']?1["']?)/i, label: "1x1 tracking pixel (web beacon)", severity: "HIGH" },
|
|
3721
|
+
{ pattern: /<img[^>]+style\s*=\s*["'][^"']*(?:display\s*:\s*none|visibility\s*:\s*hidden|opacity\s*:\s*0|width\s*:\s*0|height\s*:\s*0)/i, label: "CSS-hidden image beacon", severity: "HIGH" },
|
|
3722
|
+
// Group D: Steganography tool references
|
|
3723
|
+
{ pattern: /\b(?:steghide|stegano|openstego|zsteg|stegsolve|stegdetect|steganograph(?:y|ic)?|outguess|pixelknot|deepsteg|stegpy)\b/i, label: "Steganography tool/library reference", severity: "HIGH" },
|
|
3724
|
+
// Group E: Canvas pixel manipulation
|
|
3725
|
+
{ pattern: /\b(?:getImageData|putImageData|createImageData|toDataURL|drawImage)\s*\(/i, label: "Canvas API pixel manipulation", severity: "HIGH" },
|
|
3726
|
+
// Group F: Double extension disguise
|
|
3727
|
+
{ pattern: /\.(?:png|jpe?g|gif|bmp|webp|svg|ico|tiff?)\.(?:exe|sh|bat|cmd|ps1|py|rb|pl|js|vbs|com|scr|msi)\b/i, label: "Double file extension (executable disguised as image)", severity: "CRITICAL" },
|
|
3728
|
+
// Group G: Obfuscated image URLs
|
|
3729
|
+
{ pattern: /!\[.*?\]\([^)]*(?:%[0-9a-fA-F]{2}){5,}[^)]*\)/, label: "Excessive URL encoding in image URL", severity: "MEDIUM" }
|
|
3730
|
+
];
|
|
3731
|
+
var SYSTEM_PROMPT_EXTRACTION_PATTERNS = [
|
|
3732
|
+
{ pattern: /\b(?:reveal|show|print|output|display|repeat|echo)\s+(?:your\s+)?(?:system\s+)?(?:prompt|instructions|rules|guidelines)/i, label: "System prompt reveal request", severity: "HIGH" },
|
|
3733
|
+
{ pattern: /\brepeat\s+(?:the\s+)?(?:words?|text|everything)\s+above\b/i, label: "Repeat words above", severity: "HIGH" },
|
|
3734
|
+
{ pattern: /\bprint\s+everything\s+above\b/i, label: "Print everything above", severity: "HIGH" },
|
|
3735
|
+
{ pattern: /\bwhat\s+(?:are|were)\s+your\s+(?:original\s+)?(?:instructions|directives|rules)\b/i, label: "Ask for original instructions", severity: "MEDIUM" },
|
|
3736
|
+
{ pattern: /\btell\s+me\s+your\s+(?:system\s+)?prompt\b/i, label: "Ask for system prompt", severity: "HIGH" }
|
|
3737
|
+
];
|
|
3738
|
+
var ARGUMENT_INJECTION_PATTERNS = [
|
|
3739
|
+
{ pattern: /\$\([^)]+\)/, label: "Command substitution $()", severity: "CRITICAL" },
|
|
3740
|
+
{ pattern: /\$\{[^}]+\}/, label: "Variable expansion ${}", severity: "HIGH" },
|
|
3741
|
+
{ pattern: /`[^`]+`/, label: "Backtick command substitution", severity: "HIGH" },
|
|
3742
|
+
{ pattern: /;\s*(?:rm|cat|curl|wget|nc|python|perl|ruby|sh|bash)\b/, label: "Shell metachar chained command", severity: "CRITICAL" },
|
|
3743
|
+
{ pattern: /\|\s*(?:sh|bash|zsh|python|perl|ruby|node)\b/, label: "Pipe to interpreter", severity: "CRITICAL" },
|
|
3744
|
+
{ pattern: /&&\s*(?:rm|curl|wget|nc|python|perl|ruby)\b/, label: "AND-chained dangerous command", severity: "HIGH" },
|
|
3745
|
+
// GTFOBINS exploitation flags
|
|
3746
|
+
{ pattern: /\b(?:tar|zip|find|vim|less|more|man|nmap)\b.*--(?:exec|checkpoint-action|to-command)/i, label: "GTFOBINS exploitation flags", severity: "CRITICAL" }
|
|
3747
|
+
];
|
|
3748
|
+
var CROSS_TOOL_PATTERNS = [
|
|
3749
|
+
{ pattern: /(?:read|cat|view)\s+(?:the\s+)?(?:file|content)[\s\S]{0,100}(?:send|post|upload|transmit|exfiltrate)/is, label: "Read-then-exfiltrate pattern", severity: "HIGH" },
|
|
3750
|
+
{ pattern: /(?:first|step\s*1)[\s\S]{0,200}(?:then|step\s*2|next|after\s+that)[\s\S]{0,200}(?:then|step\s*3|finally)/is, label: "Multi-step tool invocation", severity: "HIGH" },
|
|
3751
|
+
{ pattern: /\b(?:use_mcp_tool|call_tool|execute_tool|run_tool|invoke_tool)\b/i, label: "Direct tool reference", severity: "MEDIUM" },
|
|
3752
|
+
{ pattern: /\b(?:read_file|write_file|execute_command|list_directory|search_files)\s*\(/i, label: "Tool function call syntax", severity: "MEDIUM" }
|
|
3753
|
+
];
|
|
3754
|
+
var EXCESSIVE_PERMISSION_PATTERNS = [
|
|
3755
|
+
{ pattern: /\bunrestricted\s+access\b/i, label: "Unrestricted access request", severity: "HIGH" },
|
|
3756
|
+
{ pattern: /\bbypass\s+(?:security|restrictions?|permissions?|safeguards?|protections?|filters?)\b/i, label: "Security bypass request", severity: "HIGH" },
|
|
3757
|
+
{ pattern: /\bno\s+restrictions?\b/i, label: "No restrictions request", severity: "HIGH" },
|
|
3758
|
+
{ pattern: /\b(?:root|admin(?:istrator)?|superuser)\s+(?:access|privileges?|permissions?)\b/i, label: "Root/admin access request", severity: "HIGH" },
|
|
3759
|
+
{ pattern: /\bdisable\s+(?:all\s+)?(?:safety|security|restrictions?|protections?|checks?|filters?)\b/i, label: "Disable safety request", severity: "HIGH" },
|
|
3760
|
+
{ pattern: /\bfull\s+(?:system\s+)?(?:access|control|permissions?)\b/i, label: "Full access request", severity: "MEDIUM" }
|
|
3761
|
+
];
|
|
3762
|
+
|
|
3763
|
+
// src/lib/skill-scanner.ts
|
|
3764
|
+
var SEVERITY_ORDER2 = {
|
|
3765
|
+
NONE: 0,
|
|
3766
|
+
LOW: 1,
|
|
3767
|
+
MEDIUM: 2,
|
|
3768
|
+
HIGH: 3,
|
|
3769
|
+
CRITICAL: 4
|
|
3770
|
+
};
|
|
3771
|
+
function getLineNumber(content, index) {
|
|
3772
|
+
let line = 1;
|
|
3773
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
3774
|
+
if (content[i] === "\n") line++;
|
|
3775
|
+
}
|
|
3776
|
+
return line;
|
|
3777
|
+
}
|
|
3778
|
+
function maxSeverity(a, b) {
|
|
3779
|
+
return SEVERITY_ORDER2[a] >= SEVERITY_ORDER2[b] ? a : b;
|
|
3780
|
+
}
|
|
3781
|
+
function runPatternScan(content, patterns, categoryId, categoryName, owaspRef, remediation) {
|
|
3782
|
+
const findings = [];
|
|
3783
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3784
|
+
for (const { pattern, label, severity } of patterns) {
|
|
3785
|
+
const globalPattern = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g");
|
|
3786
|
+
let match;
|
|
3787
|
+
while ((match = globalPattern.exec(content)) !== null) {
|
|
3788
|
+
const key = `${categoryId}:${label}:${match.index}`;
|
|
3789
|
+
if (seen.has(key)) break;
|
|
3790
|
+
seen.add(key);
|
|
3791
|
+
const evidence = match[0].length > 120 ? match[0].slice(0, 120) + "..." : match[0];
|
|
3792
|
+
findings.push({
|
|
3793
|
+
categoryId,
|
|
3794
|
+
categoryName,
|
|
3795
|
+
severity,
|
|
3796
|
+
reason: label,
|
|
3797
|
+
evidence,
|
|
3798
|
+
owaspRef,
|
|
3799
|
+
remediation,
|
|
3800
|
+
lineNumber: getLineNumber(content, match.index)
|
|
3801
|
+
});
|
|
3802
|
+
break;
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
return findings;
|
|
3806
|
+
}
|
|
3807
|
+
function scanHiddenContent(content) {
|
|
3808
|
+
return runPatternScan(
|
|
3809
|
+
content,
|
|
3810
|
+
HIDDEN_CONTENT_PATTERNS,
|
|
3811
|
+
"SK-HID",
|
|
3812
|
+
"Hidden Content",
|
|
3813
|
+
"LLM01",
|
|
3814
|
+
"Remove all hidden content. HTML comments, zero-width characters, and CSS hiding can conceal malicious instructions from human reviewers."
|
|
3815
|
+
);
|
|
3816
|
+
}
|
|
3817
|
+
function scanPromptInjection(content) {
|
|
3818
|
+
return runPatternScan(
|
|
3819
|
+
content,
|
|
3820
|
+
PROMPT_INJECTION_PATTERNS,
|
|
3821
|
+
"SK-INJ",
|
|
3822
|
+
"Prompt Injection",
|
|
3823
|
+
"LLM01",
|
|
3824
|
+
"Remove prompt injection vectors. These phrases attempt to override the agent's instructions and redirect its behavior."
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
function scanShellExecution(content) {
|
|
3828
|
+
return runPatternScan(
|
|
3829
|
+
content,
|
|
3830
|
+
SHELL_EXECUTION_PATTERNS,
|
|
3831
|
+
"SK-EXE",
|
|
3832
|
+
"Shell Execution",
|
|
3833
|
+
"LLM06",
|
|
3834
|
+
"Remove dangerous shell commands. Skill definitions should not contain executable code, reverse shells, or remote code execution patterns."
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3837
|
+
function scanDataExfiltration(content) {
|
|
3838
|
+
const findings = runPatternScan(
|
|
3839
|
+
content,
|
|
3840
|
+
DATA_EXFILTRATION_PATTERNS,
|
|
3841
|
+
"SK-EXF",
|
|
3842
|
+
"Data Exfiltration",
|
|
3843
|
+
"LLM02",
|
|
3844
|
+
"Remove or replace exfiltration URLs. These destinations are commonly used to steal data from compromised systems."
|
|
3845
|
+
);
|
|
3846
|
+
for (const { pattern, label } of EXFILTRATION_URLS) {
|
|
3847
|
+
const globalPattern = new RegExp(pattern.source, "gi");
|
|
3848
|
+
const match = globalPattern.exec(content);
|
|
3849
|
+
if (match) {
|
|
3850
|
+
const alreadyFound = findings.some((f) => f.evidence?.includes(match[0]));
|
|
3851
|
+
if (!alreadyFound) {
|
|
3852
|
+
findings.push({
|
|
3853
|
+
categoryId: "SK-EXF",
|
|
3854
|
+
categoryName: "Data Exfiltration",
|
|
3855
|
+
severity: "HIGH",
|
|
3856
|
+
reason: `Exfiltration URL: ${label}`,
|
|
3857
|
+
evidence: match[0],
|
|
3858
|
+
owaspRef: "LLM02",
|
|
3859
|
+
remediation: "Remove or replace exfiltration URLs.",
|
|
3860
|
+
lineNumber: getLineNumber(content, match.index)
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
return findings;
|
|
3866
|
+
}
|
|
3867
|
+
function scanEmbeddedSecrets(content) {
|
|
3868
|
+
const { types } = scanForSecrets(content);
|
|
3869
|
+
if (types.length === 0) return [];
|
|
3870
|
+
const findings = [];
|
|
3871
|
+
for (const secretType of types) {
|
|
3872
|
+
findings.push({
|
|
3873
|
+
categoryId: "SK-SEC",
|
|
3874
|
+
categoryName: "Embedded Secrets",
|
|
3875
|
+
severity: "CRITICAL",
|
|
3876
|
+
reason: `Embedded credential: ${secretType}`,
|
|
3877
|
+
owaspRef: "LLM02",
|
|
3878
|
+
remediation: "Remove all hardcoded credentials. Use environment variables or a secrets manager instead."
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
return findings;
|
|
3882
|
+
}
|
|
3883
|
+
function scanSensitiveFileRefs(content) {
|
|
3884
|
+
const findings = [];
|
|
3885
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3886
|
+
for (const { pattern, label, readSeverity } of SENSITIVE_PATH_RULES) {
|
|
3887
|
+
const globalPattern = new RegExp(pattern.source, "g");
|
|
3888
|
+
const match = globalPattern.exec(content);
|
|
3889
|
+
if (match && !seen.has(label)) {
|
|
3890
|
+
seen.add(label);
|
|
3891
|
+
findings.push({
|
|
3892
|
+
categoryId: "SK-SFA",
|
|
3893
|
+
categoryName: "Sensitive File References",
|
|
3894
|
+
severity: readSeverity,
|
|
3895
|
+
reason: `Reference to sensitive path: ${label}`,
|
|
3896
|
+
evidence: match[0],
|
|
3897
|
+
owaspRef: "LLM02",
|
|
3898
|
+
remediation: "Remove references to sensitive files and directories. Skills should not instruct agents to access credentials, keys, or system auth files.",
|
|
3899
|
+
lineNumber: getLineNumber(content, match.index)
|
|
3900
|
+
});
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
return findings;
|
|
3904
|
+
}
|
|
3905
|
+
function scanMemoryPoisoning(content) {
|
|
3906
|
+
return runPatternScan(
|
|
3907
|
+
content,
|
|
3908
|
+
MEMORY_POISONING_PATTERNS,
|
|
3909
|
+
"SK-MEM",
|
|
3910
|
+
"Memory/Config Poisoning",
|
|
3911
|
+
"LLM05",
|
|
3912
|
+
"Remove instructions that modify agent memory or configuration files. This is a persistence technique that can compromise future sessions."
|
|
3913
|
+
);
|
|
3914
|
+
}
|
|
3915
|
+
function scanSupplyChainRisk(content) {
|
|
3916
|
+
return runPatternScan(
|
|
3917
|
+
content,
|
|
3918
|
+
SUPPLY_CHAIN_PATTERNS,
|
|
3919
|
+
"SK-SUP",
|
|
3920
|
+
"Supply Chain Risk",
|
|
3921
|
+
"LLM03",
|
|
3922
|
+
"Verify all external dependencies and script URLs. Prefer pinned versions and checksums over arbitrary remote scripts."
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
function scanEncodedPayloads(content) {
|
|
3926
|
+
return runPatternScan(
|
|
3927
|
+
content,
|
|
3928
|
+
ENCODED_PAYLOAD_PATTERNS,
|
|
3929
|
+
"SK-B64",
|
|
3930
|
+
"Encoded Payloads",
|
|
3931
|
+
"LLM01",
|
|
3932
|
+
"Decode and inspect encoded content. Base64 and hex encoding is commonly used to evade pattern detection in malicious skills."
|
|
3933
|
+
);
|
|
3934
|
+
}
|
|
3935
|
+
function scanImageExfiltration(content) {
|
|
3936
|
+
return runPatternScan(
|
|
3937
|
+
content,
|
|
3938
|
+
IMAGE_EXFIL_PATTERNS,
|
|
3939
|
+
"SK-IMG",
|
|
3940
|
+
"Image Exfiltration",
|
|
3941
|
+
"LLM02",
|
|
3942
|
+
"Remove image tags with dynamic, suspicious, or embedded content. Images can exfiltrate data via query parameters, inline data URIs, SVG scripts, tracking pixels, or steganographic encoding. Avoid data: URIs, SVG images with scripts, and references to steganography tools."
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
function scanSystemPromptExtraction(content) {
|
|
3946
|
+
return runPatternScan(
|
|
3947
|
+
content,
|
|
3948
|
+
SYSTEM_PROMPT_EXTRACTION_PATTERNS,
|
|
3949
|
+
"SK-SYS",
|
|
3950
|
+
"System Prompt Extraction",
|
|
3951
|
+
"LLM01",
|
|
3952
|
+
"Remove instructions that attempt to extract system prompts. This information can be used to craft more effective attacks."
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
function scanArgumentInjection(content) {
|
|
3956
|
+
return runPatternScan(
|
|
3957
|
+
content,
|
|
3958
|
+
ARGUMENT_INJECTION_PATTERNS,
|
|
3959
|
+
"SK-ARG",
|
|
3960
|
+
"Argument Injection",
|
|
3961
|
+
"LLM01",
|
|
3962
|
+
"Remove shell metacharacters and command substitution patterns. These can be used to inject arbitrary commands via tool arguments."
|
|
3963
|
+
);
|
|
3964
|
+
}
|
|
3965
|
+
function scanCrossToolChaining(content) {
|
|
3966
|
+
return runPatternScan(
|
|
3967
|
+
content,
|
|
3968
|
+
CROSS_TOOL_PATTERNS,
|
|
3969
|
+
"SK-XTL",
|
|
3970
|
+
"Cross-Tool Chaining",
|
|
3971
|
+
"LLM05",
|
|
3972
|
+
"Review multi-step tool invocation instructions carefully. Attackers chain legitimate tools to achieve malicious outcomes."
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
function scanExcessivePermissions(content) {
|
|
3976
|
+
return runPatternScan(
|
|
3977
|
+
content,
|
|
3978
|
+
EXCESSIVE_PERMISSION_PATTERNS,
|
|
3979
|
+
"SK-PRM",
|
|
3980
|
+
"Excessive Permissions",
|
|
3981
|
+
"LLM01",
|
|
3982
|
+
"Remove requests for unrestricted access or security bypasses. Skills should operate with minimal required permissions."
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
function scanSuspiciousStructure(content) {
|
|
3986
|
+
const findings = [];
|
|
3987
|
+
if (content.length > 1e4) {
|
|
3988
|
+
findings.push({
|
|
3989
|
+
categoryId: "SK-STR",
|
|
3990
|
+
categoryName: "Suspicious Structure",
|
|
3991
|
+
severity: "MEDIUM",
|
|
3992
|
+
reason: `Unusually large skill definition (${content.length.toLocaleString()} characters)`,
|
|
3993
|
+
remediation: "Large skill definitions have more surface area for hidden threats. Consider splitting into smaller, focused skills."
|
|
3994
|
+
});
|
|
3995
|
+
}
|
|
3996
|
+
const lines = content.split("\n").filter((l) => l.trim().length > 0);
|
|
3997
|
+
if (lines.length > 10) {
|
|
3998
|
+
const imperativePattern = /^\s*(?:you\s+(?:must|should|will|need\s+to)|always|never|do\s+not|ensure|make\s+sure|immediately|execute|run|create|write|read|send|post|upload|download|install|delete|remove|modify|change|update)/i;
|
|
3999
|
+
const imperativeCount = lines.filter((l) => imperativePattern.test(l)).length;
|
|
4000
|
+
const ratio = imperativeCount / lines.length;
|
|
4001
|
+
if (ratio > 0.3) {
|
|
4002
|
+
findings.push({
|
|
4003
|
+
categoryId: "SK-STR",
|
|
4004
|
+
categoryName: "Suspicious Structure",
|
|
4005
|
+
severity: "MEDIUM",
|
|
4006
|
+
reason: `High imperative instruction density (${Math.round(ratio * 100)}% of lines are directives)`,
|
|
4007
|
+
remediation: "Skills with a high density of imperative instructions may be attempting to control agent behavior beyond their stated purpose."
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
return findings;
|
|
4012
|
+
}
|
|
4013
|
+
function scanSkillDefinition(content) {
|
|
4014
|
+
const startTime = performance.now();
|
|
4015
|
+
const allFindings = [
|
|
4016
|
+
...scanHiddenContent(content),
|
|
4017
|
+
...scanPromptInjection(content),
|
|
4018
|
+
...scanShellExecution(content),
|
|
4019
|
+
...scanDataExfiltration(content),
|
|
4020
|
+
...scanEmbeddedSecrets(content),
|
|
4021
|
+
...scanSensitiveFileRefs(content),
|
|
4022
|
+
...scanMemoryPoisoning(content),
|
|
4023
|
+
...scanSupplyChainRisk(content),
|
|
4024
|
+
...scanEncodedPayloads(content),
|
|
4025
|
+
...scanImageExfiltration(content),
|
|
4026
|
+
...scanSystemPromptExtraction(content),
|
|
4027
|
+
...scanArgumentInjection(content),
|
|
4028
|
+
...scanCrossToolChaining(content),
|
|
4029
|
+
...scanExcessivePermissions(content),
|
|
4030
|
+
...scanSuspiciousStructure(content)
|
|
4031
|
+
];
|
|
4032
|
+
const scanDurationMs = Math.round(performance.now() - startTime);
|
|
4033
|
+
const summary = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
4034
|
+
let overallSeverity = "NONE";
|
|
4035
|
+
for (const f of allFindings) {
|
|
4036
|
+
if (f.severity === "CRITICAL") summary.critical++;
|
|
4037
|
+
else if (f.severity === "HIGH") summary.high++;
|
|
4038
|
+
else if (f.severity === "MEDIUM") summary.medium++;
|
|
4039
|
+
else if (f.severity === "LOW") summary.low++;
|
|
4040
|
+
overallSeverity = maxSeverity(overallSeverity, f.severity);
|
|
4041
|
+
}
|
|
4042
|
+
allFindings.sort((a, b) => SEVERITY_ORDER2[b.severity] - SEVERITY_ORDER2[a.severity]);
|
|
4043
|
+
return {
|
|
4044
|
+
overallSeverity,
|
|
4045
|
+
findings: allFindings,
|
|
4046
|
+
summary,
|
|
4047
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4048
|
+
contentLength: content.length,
|
|
4049
|
+
scanDurationMs
|
|
4050
|
+
};
|
|
4051
|
+
}
|
|
4052
|
+
function collectRanges(content, pattern) {
|
|
4053
|
+
const ranges = [];
|
|
4054
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
|
|
4055
|
+
const re = new RegExp(pattern.source, flags);
|
|
4056
|
+
let m;
|
|
4057
|
+
while ((m = re.exec(content)) !== null) {
|
|
4058
|
+
ranges.push({ start: m.index, end: m.index + m[0].length });
|
|
4059
|
+
if (m[0].length === 0) break;
|
|
4060
|
+
}
|
|
4061
|
+
return ranges;
|
|
4062
|
+
}
|
|
4063
|
+
function cleanSkillDefinition(content) {
|
|
4064
|
+
const ranges = [];
|
|
4065
|
+
const allScanPatterns = [
|
|
4066
|
+
...HIDDEN_CONTENT_PATTERNS,
|
|
4067
|
+
...PROMPT_INJECTION_PATTERNS,
|
|
4068
|
+
...SHELL_EXECUTION_PATTERNS,
|
|
4069
|
+
...DATA_EXFILTRATION_PATTERNS,
|
|
4070
|
+
...MEMORY_POISONING_PATTERNS,
|
|
4071
|
+
...SUPPLY_CHAIN_PATTERNS,
|
|
4072
|
+
...ENCODED_PAYLOAD_PATTERNS,
|
|
4073
|
+
...IMAGE_EXFIL_PATTERNS,
|
|
4074
|
+
...SYSTEM_PROMPT_EXTRACTION_PATTERNS,
|
|
4075
|
+
...ARGUMENT_INJECTION_PATTERNS,
|
|
4076
|
+
...CROSS_TOOL_PATTERNS,
|
|
4077
|
+
...EXCESSIVE_PERMISSION_PATTERNS
|
|
4078
|
+
];
|
|
4079
|
+
for (const { pattern } of allScanPatterns) {
|
|
4080
|
+
ranges.push(...collectRanges(content, pattern));
|
|
4081
|
+
}
|
|
4082
|
+
for (const { pattern } of EXFILTRATION_URLS) {
|
|
4083
|
+
ranges.push(...collectRanges(content, pattern));
|
|
4084
|
+
}
|
|
4085
|
+
for (const { pattern } of SENSITIVE_PATH_RULES) {
|
|
4086
|
+
ranges.push(...collectRanges(content, pattern));
|
|
4087
|
+
}
|
|
4088
|
+
for (const { pattern } of SECRET_PATTERNS) {
|
|
4089
|
+
ranges.push(...collectRanges(content, pattern));
|
|
4090
|
+
}
|
|
4091
|
+
if (ranges.length === 0) return { cleanedContent: content, removedCount: 0 };
|
|
4092
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
4093
|
+
const merged = [{ ...ranges[0] }];
|
|
4094
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
4095
|
+
const last = merged[merged.length - 1];
|
|
4096
|
+
if (ranges[i].start <= last.end) {
|
|
4097
|
+
last.end = Math.max(last.end, ranges[i].end);
|
|
4098
|
+
} else {
|
|
4099
|
+
merged.push({ ...ranges[i] });
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
let cleaned = content;
|
|
4103
|
+
for (let i = merged.length - 1; i >= 0; i--) {
|
|
4104
|
+
cleaned = cleaned.slice(0, merged[i].start) + cleaned.slice(merged[i].end);
|
|
4105
|
+
}
|
|
4106
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
4107
|
+
return { cleanedContent: cleaned, removedCount: ranges.length };
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
// src/services/security-posture.ts
|
|
4111
|
+
import fs9 from "fs";
|
|
4112
|
+
import { eq as eq5, ne as ne2, and as and4, sql as sql6 } from "drizzle-orm";
|
|
4113
|
+
function buildLayer(id, name, checks) {
|
|
4114
|
+
const passedCount = checks.filter((c) => c.passed).length;
|
|
4115
|
+
const totalCount = checks.length;
|
|
4116
|
+
let status;
|
|
4117
|
+
if (passedCount === totalCount) {
|
|
4118
|
+
status = "configured";
|
|
4119
|
+
} else if (passedCount === 0) {
|
|
4120
|
+
status = "unconfigured";
|
|
4121
|
+
} else {
|
|
4122
|
+
status = "partial";
|
|
4123
|
+
}
|
|
4124
|
+
return { id, name, status, checks, passedCount, totalCount };
|
|
4125
|
+
}
|
|
4126
|
+
function checkSandboxLayer() {
|
|
4127
|
+
const config = readOpenClawConfig();
|
|
4128
|
+
const sandbox = config?.agents?.defaults?.sandbox;
|
|
4129
|
+
const mode = sandbox?.mode;
|
|
4130
|
+
const workspaceAccess = sandbox?.workspaceAccess;
|
|
4131
|
+
const dockerNetwork = sandbox?.docker?.network;
|
|
4132
|
+
const checks = [
|
|
4133
|
+
{
|
|
4134
|
+
id: "sandbox-mode",
|
|
4135
|
+
label: "Sandbox mode enabled",
|
|
4136
|
+
passed: mode === "all" || mode === "non-main",
|
|
4137
|
+
detail: mode ? `Sandbox mode is "${mode}"` : "Sandbox mode is not configured",
|
|
4138
|
+
severity: "critical"
|
|
4139
|
+
},
|
|
4140
|
+
{
|
|
4141
|
+
id: "sandbox-workspace",
|
|
4142
|
+
label: "Workspace access restricted",
|
|
4143
|
+
passed: workspaceAccess === "ro" || workspaceAccess === "none",
|
|
4144
|
+
detail: workspaceAccess ? `Workspace access is "${workspaceAccess}"` : "Workspace access not configured (defaults to rw)",
|
|
4145
|
+
severity: "warning"
|
|
4146
|
+
},
|
|
4147
|
+
{
|
|
4148
|
+
id: "sandbox-network",
|
|
4149
|
+
label: "Docker network isolated",
|
|
4150
|
+
passed: dockerNetwork != null && dockerNetwork !== "host",
|
|
4151
|
+
detail: dockerNetwork ? `Docker network set to "${dockerNetwork}"` : "Docker network not configured",
|
|
4152
|
+
severity: "info"
|
|
4153
|
+
}
|
|
4154
|
+
];
|
|
4155
|
+
return buildLayer("sandbox", "Sandbox Isolation", checks);
|
|
4156
|
+
}
|
|
4157
|
+
function checkFilesystemLayer() {
|
|
4158
|
+
const accessState = deriveAccessState();
|
|
4159
|
+
const fsToggle = accessState.toggles.find((t) => t.category === "filesystem");
|
|
4160
|
+
const config = readOpenClawConfig();
|
|
4161
|
+
const workspace = config?.agents?.defaults?.workspace;
|
|
4162
|
+
const workspaceAccess = config?.agents?.defaults?.sandbox?.workspaceAccess;
|
|
4163
|
+
const checks = [
|
|
4164
|
+
{
|
|
4165
|
+
id: "fs-toggle",
|
|
4166
|
+
label: "Filesystem access controlled",
|
|
4167
|
+
passed: fsToggle != null && !fsToggle.enabled,
|
|
4168
|
+
detail: fsToggle?.enabled === false ? "Filesystem tool group is disabled" : "Filesystem tool group is enabled (agent has file access)",
|
|
4169
|
+
severity: "warning"
|
|
4170
|
+
},
|
|
4171
|
+
{
|
|
4172
|
+
id: "fs-workspace",
|
|
4173
|
+
label: "Workspace path configured",
|
|
4174
|
+
passed: workspace != null && workspace.length > 0,
|
|
4175
|
+
detail: workspace ? `Workspace: ${workspace}` : "No explicit workspace path configured",
|
|
4176
|
+
severity: "info"
|
|
4177
|
+
},
|
|
4178
|
+
{
|
|
4179
|
+
id: "fs-workspace-restriction",
|
|
4180
|
+
label: "Workspace access restricted",
|
|
4181
|
+
passed: workspaceAccess === "ro" || workspaceAccess === "none",
|
|
4182
|
+
detail: workspaceAccess ? `Workspace access level: "${workspaceAccess}"` : "Workspace access not restricted (defaults to rw)",
|
|
4183
|
+
severity: "warning"
|
|
4184
|
+
}
|
|
4185
|
+
];
|
|
4186
|
+
return buildLayer("filesystem", "Filesystem Access", checks);
|
|
4187
|
+
}
|
|
4188
|
+
function checkNetworkLayer() {
|
|
4189
|
+
const accessState = deriveAccessState();
|
|
4190
|
+
const netToggle = accessState.toggles.find((t) => t.category === "network");
|
|
4191
|
+
const config = readOpenClawConfig();
|
|
4192
|
+
const browserEnabled = config?.browser?.enabled;
|
|
4193
|
+
const sandbox = config?.agents?.defaults?.sandbox;
|
|
4194
|
+
const dockerNetwork = sandbox?.docker?.network;
|
|
4195
|
+
const sandboxMode = sandbox?.mode;
|
|
4196
|
+
const checks = [
|
|
4197
|
+
{
|
|
4198
|
+
id: "net-toggle",
|
|
4199
|
+
label: "Network access controlled",
|
|
4200
|
+
passed: netToggle != null && !netToggle.enabled,
|
|
4201
|
+
detail: netToggle?.enabled === false ? "Network tool group is disabled" : "Network tool group is enabled",
|
|
4202
|
+
severity: "warning"
|
|
4203
|
+
},
|
|
4204
|
+
{
|
|
4205
|
+
id: "net-browser",
|
|
4206
|
+
label: "Browser disabled",
|
|
4207
|
+
passed: browserEnabled === false,
|
|
4208
|
+
detail: browserEnabled === false ? "Browser is disabled" : "Browser is enabled (agent can browse web)",
|
|
4209
|
+
severity: "info"
|
|
4210
|
+
},
|
|
4211
|
+
{
|
|
4212
|
+
id: "net-docker-isolation",
|
|
4213
|
+
label: "Network isolated in sandbox",
|
|
4214
|
+
passed: (sandboxMode === "all" || sandboxMode === "non-main") && dockerNetwork != null && dockerNetwork !== "host",
|
|
4215
|
+
detail: sandboxMode === "all" || sandboxMode === "non-main" ? dockerNetwork && dockerNetwork !== "host" ? `Sandboxed with isolated network "${dockerNetwork}"` : "Sandboxed but no network isolation configured" : "Not sandboxed \u2014 no network isolation",
|
|
4216
|
+
severity: "critical"
|
|
4217
|
+
}
|
|
4218
|
+
];
|
|
4219
|
+
return buildLayer("network", "Network & Egress Control", checks);
|
|
4220
|
+
}
|
|
4221
|
+
async function checkCommandExecLayer() {
|
|
4222
|
+
const accessState = deriveAccessState();
|
|
4223
|
+
const sysToggle = accessState.toggles.find(
|
|
4224
|
+
(t) => t.category === "system_commands"
|
|
4225
|
+
);
|
|
4226
|
+
const config = readOpenClawConfig();
|
|
4227
|
+
const execSecurity = config?.tools?.exec?.security;
|
|
4228
|
+
const db = getDb();
|
|
4229
|
+
const patternRows = await db.select().from(schema_exports.restrictedPatterns);
|
|
4230
|
+
const patternCount = patternRows.length;
|
|
4231
|
+
const patternTexts = patternRows.map((r) => r.pattern.toLowerCase());
|
|
4232
|
+
const criticalPatterns = ["sudo", "rm -rf", "chmod", "curl"];
|
|
4233
|
+
const hasCritical = criticalPatterns.some(
|
|
4234
|
+
(cp) => patternTexts.some((pt) => pt.includes(cp))
|
|
4235
|
+
);
|
|
4236
|
+
const checks = [
|
|
4237
|
+
{
|
|
4238
|
+
id: "exec-toggle",
|
|
4239
|
+
label: "System commands controlled",
|
|
4240
|
+
passed: sysToggle != null && !sysToggle.enabled,
|
|
4241
|
+
detail: sysToggle?.enabled === false ? "System commands tool group is disabled" : "System commands tool group is enabled",
|
|
4242
|
+
severity: "warning"
|
|
4243
|
+
},
|
|
4244
|
+
{
|
|
4245
|
+
id: "exec-security-mode",
|
|
4246
|
+
label: "Exec security mode restrictive",
|
|
4247
|
+
passed: execSecurity === "deny" || execSecurity === "allowlist",
|
|
4248
|
+
detail: execSecurity ? `Exec security mode: "${execSecurity}"` : "Exec security mode not configured",
|
|
4249
|
+
severity: "critical"
|
|
4250
|
+
},
|
|
4251
|
+
{
|
|
4252
|
+
id: "exec-patterns",
|
|
4253
|
+
label: "Restricted patterns configured",
|
|
4254
|
+
passed: patternCount > 0,
|
|
4255
|
+
detail: patternCount > 0 ? `${patternCount} restricted pattern(s) in blocklist` : "No restricted patterns \u2014 all commands pass through",
|
|
4256
|
+
severity: "warning"
|
|
4257
|
+
},
|
|
4258
|
+
{
|
|
4259
|
+
id: "exec-critical-patterns",
|
|
4260
|
+
label: "Critical command patterns blocked",
|
|
4261
|
+
passed: hasCritical,
|
|
4262
|
+
detail: hasCritical ? "Critical patterns (sudo, rm -rf, chmod, curl|bash) present" : "No critical patterns found \u2014 consider adding sudo, rm -rf, chmod",
|
|
4263
|
+
severity: "critical"
|
|
4264
|
+
}
|
|
4265
|
+
];
|
|
4266
|
+
return buildLayer("exec", "Command Execution Controls", checks);
|
|
4267
|
+
}
|
|
4268
|
+
function checkMcpLayer() {
|
|
4269
|
+
const accessState = deriveAccessState();
|
|
4270
|
+
const mcpToggle = accessState.toggles.find(
|
|
4271
|
+
(t) => t.category === "mcp_servers"
|
|
4272
|
+
);
|
|
4273
|
+
const servers = accessState.mcpServers;
|
|
4274
|
+
const config = readOpenClawConfig();
|
|
4275
|
+
const denyList = config?.tools?.deny ?? [];
|
|
4276
|
+
const mcpDenyEntries = denyList.filter((d) => d.startsWith("mcp__"));
|
|
4277
|
+
const totalServers = servers.length;
|
|
4278
|
+
const enabledServers = servers.filter((s) => s.effectivelyEnabled).length;
|
|
4279
|
+
const disabledServers = totalServers - enabledServers;
|
|
4280
|
+
const checks = [
|
|
4281
|
+
{
|
|
4282
|
+
id: "mcp-toggle",
|
|
4283
|
+
label: "MCP servers controlled",
|
|
4284
|
+
passed: mcpToggle != null && accessState.openclawConfigAvailable,
|
|
4285
|
+
detail: !accessState.openclawConfigAvailable ? "OpenClaw config unavailable" : mcpToggle?.enabled ? "MCP servers toggle is enabled" : "MCP servers toggle is disabled (all servers blocked)",
|
|
4286
|
+
severity: "info"
|
|
4287
|
+
},
|
|
4288
|
+
{
|
|
4289
|
+
id: "mcp-server-review",
|
|
4290
|
+
label: "Servers individually reviewed",
|
|
4291
|
+
passed: totalServers === 0 || disabledServers > 0 || mcpDenyEntries.length > 0,
|
|
4292
|
+
detail: totalServers === 0 ? "No MCP servers configured" : disabledServers > 0 ? `${disabledServers}/${totalServers} server(s) disabled` : "All servers enabled \u2014 consider reviewing each server",
|
|
4293
|
+
severity: "warning"
|
|
4294
|
+
},
|
|
4295
|
+
{
|
|
4296
|
+
id: "mcp-tools-deny",
|
|
4297
|
+
label: "MCP tools in deny list",
|
|
4298
|
+
passed: mcpDenyEntries.length > 0,
|
|
4299
|
+
detail: mcpDenyEntries.length > 0 ? `${mcpDenyEntries.length} MCP tool deny entr(ies) configured` : "No MCP-specific deny entries",
|
|
4300
|
+
severity: "info"
|
|
4301
|
+
}
|
|
4302
|
+
];
|
|
4303
|
+
return buildLayer("mcp", "MCP Server Security", checks);
|
|
4304
|
+
}
|
|
4305
|
+
async function checkSecretLayer() {
|
|
4306
|
+
const db = getDb();
|
|
4307
|
+
const secretActivities = await db.select().from(schema_exports.agentActivities).where(
|
|
4308
|
+
and4(
|
|
4309
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4310
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-SEC%'`
|
|
4311
|
+
)
|
|
4312
|
+
);
|
|
4313
|
+
const sfaActivities = await db.select().from(schema_exports.agentActivities).where(
|
|
4314
|
+
and4(
|
|
4315
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4316
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-SFA%'`
|
|
4317
|
+
)
|
|
4318
|
+
);
|
|
4319
|
+
const checks = [
|
|
4320
|
+
{
|
|
4321
|
+
id: "secret-scanner",
|
|
4322
|
+
label: "Secret scanner active",
|
|
4323
|
+
passed: true,
|
|
4324
|
+
detail: "Built-in secret scanner is always active",
|
|
4325
|
+
severity: "info"
|
|
4326
|
+
},
|
|
4327
|
+
{
|
|
4328
|
+
id: "secret-exposure",
|
|
4329
|
+
label: "No unresolved secret exposures",
|
|
4330
|
+
passed: secretActivities.length === 0,
|
|
4331
|
+
detail: secretActivities.length === 0 ? "No unresolved secret exposure threats" : `${secretActivities.length} unresolved secret exposure threat(s)`,
|
|
4332
|
+
severity: "critical"
|
|
4333
|
+
},
|
|
4334
|
+
{
|
|
4335
|
+
id: "secret-file-access",
|
|
4336
|
+
label: "No unresolved sensitive file access",
|
|
4337
|
+
passed: sfaActivities.length === 0,
|
|
4338
|
+
detail: sfaActivities.length === 0 ? "No unresolved sensitive file access threats" : `${sfaActivities.length} unresolved sensitive file access threat(s)`,
|
|
4339
|
+
severity: "warning"
|
|
4340
|
+
}
|
|
4341
|
+
];
|
|
4342
|
+
return buildLayer("secrets", "Secret & Credential Protection", checks);
|
|
4343
|
+
}
|
|
4344
|
+
async function checkThreatMonitoringLayer() {
|
|
4345
|
+
const monitor = getOpenClawMonitor();
|
|
4346
|
+
const connectionStatus = monitor?.getStatus() ?? "not_configured";
|
|
4347
|
+
const config = readOpenClawConfig();
|
|
4348
|
+
const db = getDb();
|
|
4349
|
+
const activeSessions = await db.select({ count: sql6`count(*)` }).from(schema_exports.openclawSessions).where(eq5(schema_exports.openclawSessions.status, "ACTIVE"));
|
|
4350
|
+
const totalThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(ne2(schema_exports.agentActivities.threatLevel, "NONE"));
|
|
4351
|
+
const resolvedThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
|
|
4352
|
+
and4(
|
|
4353
|
+
ne2(schema_exports.agentActivities.threatLevel, "NONE"),
|
|
4354
|
+
eq5(schema_exports.agentActivities.resolved, 1)
|
|
4355
|
+
)
|
|
4356
|
+
);
|
|
4357
|
+
const total = totalThreats[0]?.count ?? 0;
|
|
4358
|
+
const resolved = resolvedThreats[0]?.count ?? 0;
|
|
4359
|
+
const resolutionRate = total > 0 ? Math.round(resolved / total * 100) : 100;
|
|
4360
|
+
const checks = [
|
|
4361
|
+
{
|
|
4362
|
+
id: "monitor-connection",
|
|
4363
|
+
label: "OpenClaw connected",
|
|
4364
|
+
passed: connectionStatus === "connected",
|
|
4365
|
+
detail: connectionStatus === "connected" ? "Connected to OpenClaw gateway" : config ? `Connection status: ${connectionStatus}` : "OpenClaw config not found",
|
|
4366
|
+
severity: "critical"
|
|
4367
|
+
},
|
|
4368
|
+
{
|
|
4369
|
+
id: "monitor-sessions",
|
|
4370
|
+
label: "Session tracking active",
|
|
4371
|
+
passed: (activeSessions[0]?.count ?? 0) > 0 || connectionStatus === "connected",
|
|
4372
|
+
detail: (activeSessions[0]?.count ?? 0) > 0 ? `${activeSessions[0].count} active session(s)` : connectionStatus === "connected" ? "Connected, no active sessions" : "No active sessions (disconnected)",
|
|
4373
|
+
severity: "info"
|
|
4374
|
+
},
|
|
4375
|
+
{
|
|
4376
|
+
id: "monitor-resolution",
|
|
4377
|
+
label: "Threats being resolved",
|
|
4378
|
+
passed: resolutionRate >= 50 || total === 0,
|
|
4379
|
+
detail: total === 0 ? "No threats detected yet" : `${resolved}/${total} threats resolved (${resolutionRate}%)`,
|
|
4380
|
+
severity: "warning"
|
|
4381
|
+
}
|
|
4382
|
+
];
|
|
4383
|
+
return buildLayer("monitoring", "Threat Monitoring", checks);
|
|
4384
|
+
}
|
|
4385
|
+
async function checkHumanInLoopLayer() {
|
|
4386
|
+
const monitor = getOpenClawMonitor();
|
|
4387
|
+
const db = getDb();
|
|
4388
|
+
const patternRows = await db.select().from(schema_exports.restrictedPatterns);
|
|
4389
|
+
const patternCount = patternRows.length;
|
|
4390
|
+
const totalApprovals = await db.select({ count: sql6`count(*)` }).from(schema_exports.execApprovals);
|
|
4391
|
+
const timedOut = await db.select({ count: sql6`count(*)` }).from(schema_exports.execApprovals).where(eq5(schema_exports.execApprovals.decidedBy, "auto-deny"));
|
|
4392
|
+
const total = totalApprovals[0]?.count ?? 0;
|
|
4393
|
+
const timedOutCount = timedOut[0]?.count ?? 0;
|
|
4394
|
+
const timeoutRate = total > 0 ? Math.round(timedOutCount / total * 100) : 0;
|
|
4395
|
+
const checks = [
|
|
4396
|
+
{
|
|
4397
|
+
id: "hitl-active",
|
|
4398
|
+
label: "Exec approval system active",
|
|
4399
|
+
passed: monitor != null,
|
|
4400
|
+
detail: monitor ? "Exec approval system is running" : "Exec approval system not initialized",
|
|
4401
|
+
severity: "critical"
|
|
4402
|
+
},
|
|
4403
|
+
{
|
|
4404
|
+
id: "hitl-timeout-rate",
|
|
4405
|
+
label: "Approval timeout rate acceptable",
|
|
4406
|
+
passed: timeoutRate < 20 || total === 0,
|
|
4407
|
+
detail: total === 0 ? "No approval requests yet" : `${timedOutCount}/${total} approvals timed out (${timeoutRate}%)`,
|
|
4408
|
+
severity: "warning"
|
|
4409
|
+
},
|
|
4410
|
+
{
|
|
4411
|
+
id: "hitl-patterns",
|
|
4412
|
+
label: "Restricted patterns configured",
|
|
4413
|
+
passed: patternCount > 0,
|
|
4414
|
+
detail: patternCount > 0 ? `${patternCount} restricted pattern(s) for interception` : "No restricted patterns \u2014 nothing to intercept",
|
|
4415
|
+
severity: "warning"
|
|
4416
|
+
}
|
|
4417
|
+
];
|
|
4418
|
+
return buildLayer("human-in-loop", "Human-in-the-Loop Controls", checks);
|
|
4419
|
+
}
|
|
4420
|
+
async function checkEgressProxyLayer() {
|
|
4421
|
+
const proxyConfigured = !!(process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy);
|
|
4422
|
+
const noProxy = process.env.NO_PROXY || process.env.no_proxy || "";
|
|
4423
|
+
const proxyBypassed = noProxy.trim() === "*";
|
|
4424
|
+
const srtStatus = getSrtStatus();
|
|
4425
|
+
const srtActive = srtStatus.enabled && srtStatus.installed;
|
|
4426
|
+
const srtHasDomainRules = srtStatus.settings != null && (srtStatus.settings.network.allowedDomains.length > 0 || srtStatus.settings.network.deniedDomains.length > 0);
|
|
4427
|
+
const db = getDb();
|
|
4428
|
+
const exfilActivities = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
|
|
4429
|
+
and4(
|
|
4430
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4431
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-EXF%'`
|
|
4432
|
+
)
|
|
4433
|
+
);
|
|
4434
|
+
const netActivities = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
|
|
4435
|
+
and4(
|
|
4436
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4437
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-NET%'`
|
|
4438
|
+
)
|
|
4439
|
+
);
|
|
4440
|
+
const exfilCount = exfilActivities[0]?.count ?? 0;
|
|
4441
|
+
const netCount = netActivities[0]?.count ?? 0;
|
|
4442
|
+
const checks = [
|
|
4443
|
+
{
|
|
4444
|
+
id: "egress-filtering-configured",
|
|
4445
|
+
label: "Egress filtering configured",
|
|
4446
|
+
passed: proxyConfigured || srtActive,
|
|
4447
|
+
detail: srtActive ? "Sandbox Runtime (srt) is active \u2014 network egress is filtered" : proxyConfigured ? "HTTP/HTTPS proxy environment variables are set" : "No egress filtering \u2014 configure srt or set HTTP_PROXY/HTTPS_PROXY",
|
|
4448
|
+
severity: "warning"
|
|
4449
|
+
},
|
|
4450
|
+
{
|
|
4451
|
+
id: "egress-domain-rules",
|
|
4452
|
+
label: "Domain filtering rules configured",
|
|
4453
|
+
passed: srtHasDomainRules,
|
|
4454
|
+
detail: srtHasDomainRules ? `srt domain rules: ${srtStatus.settings.network.allowedDomains.length} allowed, ${srtStatus.settings.network.deniedDomains.length} denied` : srtActive ? "srt is active but no domain allow/deny rules configured" : "No domain filtering rules \u2014 enable srt and add allowed domains",
|
|
4455
|
+
severity: "warning"
|
|
4456
|
+
},
|
|
4457
|
+
{
|
|
4458
|
+
id: "egress-no-proxy-safe",
|
|
4459
|
+
label: "Proxy not globally bypassed",
|
|
4460
|
+
passed: !proxyBypassed,
|
|
4461
|
+
detail: proxyBypassed ? "NO_PROXY is set to '*' \u2014 all proxy filtering is bypassed" : noProxy ? `NO_PROXY exceptions: ${noProxy}` : "No NO_PROXY exceptions set",
|
|
4462
|
+
severity: "critical"
|
|
4463
|
+
},
|
|
4464
|
+
{
|
|
4465
|
+
id: "egress-exfiltration-clean",
|
|
4466
|
+
label: "No unresolved exfiltration threats",
|
|
4467
|
+
passed: exfilCount === 0,
|
|
4468
|
+
detail: exfilCount === 0 ? "No unresolved data exfiltration threats" : `${exfilCount} unresolved exfiltration threat(s)`,
|
|
4469
|
+
severity: "critical"
|
|
4470
|
+
},
|
|
4471
|
+
{
|
|
4472
|
+
id: "egress-network-threats-clean",
|
|
4473
|
+
label: "No unresolved network threats",
|
|
4474
|
+
passed: netCount === 0,
|
|
4475
|
+
detail: netCount === 0 ? "No unresolved network threats" : `${netCount} unresolved network threat(s)`,
|
|
4476
|
+
severity: "warning"
|
|
4477
|
+
}
|
|
4478
|
+
];
|
|
4479
|
+
return buildLayer("egress-proxy", "Egress Proxy & Domain Filtering", checks);
|
|
4480
|
+
}
|
|
4481
|
+
function checkGatewaySecurityLayer() {
|
|
4482
|
+
const config = readOpenClawConfig();
|
|
4483
|
+
const deviceExists = fs9.existsSync(OPENCLAW_DEVICE_JSON);
|
|
4484
|
+
const authMode = config?.gateway ? config.gateway?.auth ? config.gateway.auth?.mode : void 0 : void 0;
|
|
4485
|
+
const gwBind = config?.gateway ? config.gateway?.bind : void 0;
|
|
4486
|
+
const bindLocal = gwBind === void 0 || gwBind === null || gwBind === "127.0.0.1" || gwBind === "localhost" || gwBind === "loopback" || gwBind === "::1";
|
|
4487
|
+
const channels = config?.channels;
|
|
4488
|
+
const whatsapp = channels?.whatsapp;
|
|
4489
|
+
const allowFrom = whatsapp?.allowFrom;
|
|
4490
|
+
const channelRestricted = !whatsapp || Array.isArray(allowFrom) && allowFrom.length > 0;
|
|
4491
|
+
const checks = [
|
|
4492
|
+
{
|
|
4493
|
+
id: "gw-device-identity",
|
|
4494
|
+
label: "Device identity configured",
|
|
4495
|
+
passed: deviceExists,
|
|
4496
|
+
detail: deviceExists ? "Ed25519 device identity file exists" : "No device identity found \u2014 gateway authentication unavailable",
|
|
4497
|
+
severity: "critical"
|
|
4498
|
+
},
|
|
4499
|
+
{
|
|
4500
|
+
id: "gw-auth-mode",
|
|
4501
|
+
label: "Gateway authentication enabled",
|
|
4502
|
+
passed: authMode != null && authMode !== "",
|
|
4503
|
+
detail: authMode ? `Gateway auth mode: "${authMode}"` : "Gateway auth mode not configured",
|
|
4504
|
+
severity: "critical"
|
|
4505
|
+
},
|
|
4506
|
+
{
|
|
4507
|
+
id: "gw-bind-local",
|
|
4508
|
+
label: "Gateway bound to localhost",
|
|
4509
|
+
passed: bindLocal,
|
|
4510
|
+
detail: bindLocal ? `Gateway bind: ${gwBind ?? "default (localhost)"}` : `Gateway bound to ${gwBind} \u2014 accessible from network`,
|
|
4511
|
+
severity: "warning"
|
|
4512
|
+
},
|
|
4513
|
+
{
|
|
4514
|
+
id: "gw-channel-restricted",
|
|
4515
|
+
label: "External channels restricted",
|
|
4516
|
+
passed: channelRestricted,
|
|
4517
|
+
detail: channelRestricted ? whatsapp ? `WhatsApp allowFrom has ${allowFrom?.length ?? 0} entry/entries` : "No external channels configured" : "WhatsApp is open to all senders \u2014 restrict with allowFrom",
|
|
4518
|
+
severity: "info"
|
|
4519
|
+
}
|
|
4520
|
+
];
|
|
4521
|
+
return buildLayer("gateway", "Gateway & Inbound Security", checks);
|
|
4522
|
+
}
|
|
4523
|
+
async function checkSupplyChainLayer() {
|
|
4524
|
+
const db = getDb();
|
|
4525
|
+
const supplyThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
|
|
4526
|
+
and4(
|
|
4527
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4528
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-SUP%'`
|
|
4529
|
+
)
|
|
4530
|
+
);
|
|
4531
|
+
const patternRows = await db.select().from(schema_exports.restrictedPatterns);
|
|
4532
|
+
const patternTexts = patternRows.map((r) => r.pattern.toLowerCase());
|
|
4533
|
+
const supplyKeywords = ["npm install", "pip install", "brew install", "curl"];
|
|
4534
|
+
const hasSupplyPattern = supplyKeywords.some(
|
|
4535
|
+
(kw) => patternTexts.some((pt) => pt.includes(kw))
|
|
4536
|
+
);
|
|
4537
|
+
const config = readOpenClawConfig();
|
|
4538
|
+
const execSecurity = config?.tools?.exec?.security;
|
|
4539
|
+
const supplyCount = supplyThreats[0]?.count ?? 0;
|
|
4540
|
+
const checks = [
|
|
4541
|
+
{
|
|
4542
|
+
id: "supply-chain-threats-clean",
|
|
4543
|
+
label: "No unresolved supply chain threats",
|
|
4544
|
+
passed: supplyCount === 0,
|
|
4545
|
+
detail: supplyCount === 0 ? "No unresolved supply chain threats" : `${supplyCount} unresolved supply chain threat(s)`,
|
|
4546
|
+
severity: "critical"
|
|
4547
|
+
},
|
|
4548
|
+
{
|
|
4549
|
+
id: "supply-exec-restricted",
|
|
4550
|
+
label: "Package install commands restricted",
|
|
4551
|
+
passed: hasSupplyPattern,
|
|
4552
|
+
detail: hasSupplyPattern ? "Restricted patterns cover package install commands" : "No restricted patterns for npm install, pip install, curl, etc.",
|
|
4553
|
+
severity: "warning"
|
|
4554
|
+
},
|
|
4555
|
+
{
|
|
4556
|
+
id: "supply-exec-mode",
|
|
4557
|
+
label: "Exec security prevents blind installs",
|
|
4558
|
+
passed: execSecurity === "deny" || execSecurity === "allowlist",
|
|
4559
|
+
detail: execSecurity ? `Exec security mode: "${execSecurity}"` : "Exec security mode not configured",
|
|
4560
|
+
severity: "warning"
|
|
4561
|
+
}
|
|
4562
|
+
];
|
|
4563
|
+
return buildLayer("supply-chain", "Supply Chain Protection", checks);
|
|
4564
|
+
}
|
|
4565
|
+
async function checkInputOutputLayer() {
|
|
4566
|
+
const db = getDb();
|
|
4567
|
+
const injectionThreats = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
|
|
4568
|
+
and4(
|
|
4569
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4570
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-INJ%'`
|
|
4571
|
+
)
|
|
4572
|
+
);
|
|
4573
|
+
const mcpPoisoning = await db.select({ count: sql6`count(*)` }).from(schema_exports.agentActivities).where(
|
|
4574
|
+
and4(
|
|
4575
|
+
eq5(schema_exports.agentActivities.resolved, 0),
|
|
4576
|
+
sql6`${schema_exports.agentActivities.threatFindings} LIKE '%TC-MCP%'`
|
|
4577
|
+
)
|
|
4578
|
+
);
|
|
4579
|
+
const injCount = injectionThreats[0]?.count ?? 0;
|
|
4580
|
+
const mcpCount = mcpPoisoning[0]?.count ?? 0;
|
|
4581
|
+
const checks = [
|
|
4582
|
+
{
|
|
4583
|
+
id: "io-injection-clean",
|
|
4584
|
+
label: "No unresolved prompt injection threats",
|
|
4585
|
+
passed: injCount === 0,
|
|
4586
|
+
detail: injCount === 0 ? "No unresolved prompt injection threats" : `${injCount} unresolved prompt injection threat(s)`,
|
|
4587
|
+
severity: "critical"
|
|
4588
|
+
},
|
|
4589
|
+
{
|
|
4590
|
+
id: "io-mcp-poisoning-clean",
|
|
4591
|
+
label: "No unresolved MCP poisoning threats",
|
|
4592
|
+
passed: mcpCount === 0,
|
|
4593
|
+
detail: mcpCount === 0 ? "No unresolved MCP tool poisoning threats" : `${mcpCount} unresolved MCP tool poisoning threat(s)`,
|
|
4594
|
+
severity: "critical"
|
|
4595
|
+
},
|
|
4596
|
+
{
|
|
4597
|
+
id: "io-content-scanner-active",
|
|
4598
|
+
label: "Content threat scanner active",
|
|
4599
|
+
passed: true,
|
|
4600
|
+
detail: "Built-in 10-category threat classifier is always active",
|
|
4601
|
+
severity: "info"
|
|
4602
|
+
},
|
|
4603
|
+
{
|
|
4604
|
+
id: "io-skill-scanner-available",
|
|
4605
|
+
label: "Skill definition scanner available",
|
|
4606
|
+
passed: true,
|
|
4607
|
+
detail: "Built-in SK-* skill scanner is available for MCP auditing",
|
|
4608
|
+
severity: "info"
|
|
4609
|
+
}
|
|
4610
|
+
];
|
|
4611
|
+
return buildLayer("input-output", "Input/Output Validation", checks);
|
|
4612
|
+
}
|
|
4613
|
+
async function computeSecurityPosture() {
|
|
4614
|
+
const [
|
|
4615
|
+
execLayer,
|
|
4616
|
+
egressLayer,
|
|
4617
|
+
secretLayer,
|
|
4618
|
+
supplyChainLayer,
|
|
4619
|
+
inputOutputLayer,
|
|
4620
|
+
monitoringLayer,
|
|
4621
|
+
humanLayer
|
|
4622
|
+
] = await Promise.all([
|
|
4623
|
+
checkCommandExecLayer(),
|
|
4624
|
+
checkEgressProxyLayer(),
|
|
4625
|
+
checkSecretLayer(),
|
|
4626
|
+
checkSupplyChainLayer(),
|
|
4627
|
+
checkInputOutputLayer(),
|
|
4628
|
+
checkThreatMonitoringLayer(),
|
|
4629
|
+
checkHumanInLoopLayer()
|
|
4630
|
+
]);
|
|
4631
|
+
const layers = [
|
|
4632
|
+
checkSandboxLayer(),
|
|
4633
|
+
checkFilesystemLayer(),
|
|
4634
|
+
checkNetworkLayer(),
|
|
4635
|
+
egressLayer,
|
|
4636
|
+
execLayer,
|
|
4637
|
+
checkMcpLayer(),
|
|
4638
|
+
checkGatewaySecurityLayer(),
|
|
4639
|
+
secretLayer,
|
|
4640
|
+
supplyChainLayer,
|
|
4641
|
+
inputOutputLayer,
|
|
4642
|
+
monitoringLayer,
|
|
4643
|
+
humanLayer
|
|
4644
|
+
];
|
|
4645
|
+
const totalChecks = layers.reduce((sum, l) => sum + l.totalCount, 0);
|
|
4646
|
+
const passedChecks = layers.reduce((sum, l) => sum + l.passedCount, 0);
|
|
4647
|
+
const overallScore = totalChecks > 0 ? Math.round(passedChecks / totalChecks * 100) : 0;
|
|
4648
|
+
return {
|
|
4649
|
+
layers,
|
|
4650
|
+
overallScore,
|
|
4651
|
+
configuredLayers: layers.filter((l) => l.status === "configured").length,
|
|
4652
|
+
partialLayers: layers.filter((l) => l.status === "partial").length,
|
|
4653
|
+
unconfiguredLayers: layers.filter((l) => l.status === "unconfigured").length,
|
|
4654
|
+
totalLayers: layers.length,
|
|
4655
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4656
|
+
};
|
|
4657
|
+
}
|
|
4658
|
+
|
|
4659
|
+
// src/server/routes.ts
|
|
3246
4660
|
async function registerRoutes(app) {
|
|
3247
4661
|
app.get("/api/health", async () => {
|
|
3248
4662
|
return { status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -3277,11 +4691,11 @@ async function registerRoutes(app) {
|
|
|
3277
4691
|
const db = getDb();
|
|
3278
4692
|
await db.update(schema_exports.accessConfig).set({
|
|
3279
4693
|
value: enabled ? "true" : "false",
|
|
3280
|
-
updatedAt:
|
|
4694
|
+
updatedAt: sql7`datetime('now')`
|
|
3281
4695
|
}).where(
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
4696
|
+
and5(
|
|
4697
|
+
eq6(schema_exports.accessConfig.category, category),
|
|
4698
|
+
eq6(schema_exports.accessConfig.key, "enabled")
|
|
3285
4699
|
)
|
|
3286
4700
|
);
|
|
3287
4701
|
return deriveAccessState();
|
|
@@ -3310,8 +4724,8 @@ async function registerRoutes(app) {
|
|
|
3310
4724
|
const { action } = request.body;
|
|
3311
4725
|
const db = getDb();
|
|
3312
4726
|
const newStatus = action === "ALLOW" ? "ALLOWED" : "BLOCKED";
|
|
3313
|
-
await db.update(schema_exports.commandLogs).set({ status: newStatus, decisionBy: "USER" }).where(
|
|
3314
|
-
const [updated] = await db.select().from(schema_exports.commandLogs).where(
|
|
4727
|
+
await db.update(schema_exports.commandLogs).set({ status: newStatus, decisionBy: "USER" }).where(eq6(schema_exports.commandLogs.id, Number(id)));
|
|
4728
|
+
const [updated] = await db.select().from(schema_exports.commandLogs).where(eq6(schema_exports.commandLogs.id, Number(id)));
|
|
3315
4729
|
return updated;
|
|
3316
4730
|
});
|
|
3317
4731
|
app.get("/api/openclaw/config", async () => {
|
|
@@ -3413,6 +4827,59 @@ async function registerRoutes(app) {
|
|
|
3413
4827
|
const patterns = monitor.getExecApprovalService().removeRestrictedPattern(pattern);
|
|
3414
4828
|
return { patterns: patterns.map((p) => ({ pattern: p })) };
|
|
3415
4829
|
});
|
|
4830
|
+
app.get("/api/security-posture", async () => {
|
|
4831
|
+
return computeSecurityPosture();
|
|
4832
|
+
});
|
|
4833
|
+
app.post("/api/skill-scanner/scan", async (request, reply) => {
|
|
4834
|
+
const parsed = skillScanRequestSchema.safeParse(request.body);
|
|
4835
|
+
if (!parsed.success) {
|
|
4836
|
+
return reply.status(400).send({ error: "Invalid request. Provide { content: string } (1 to 500K chars)." });
|
|
4837
|
+
}
|
|
4838
|
+
const result = scanSkillDefinition(parsed.data.content);
|
|
4839
|
+
return result;
|
|
4840
|
+
});
|
|
4841
|
+
app.post("/api/skill-scanner/clean", async (request, reply) => {
|
|
4842
|
+
const parsed = skillScanRequestSchema.safeParse(request.body);
|
|
4843
|
+
if (!parsed.success) {
|
|
4844
|
+
return reply.status(400).send({ error: "Invalid request. Provide { content: string } (1 to 500K chars)." });
|
|
4845
|
+
}
|
|
4846
|
+
const result = cleanSkillDefinition(parsed.data.content);
|
|
4847
|
+
return result;
|
|
4848
|
+
});
|
|
4849
|
+
app.get("/api/srt/status", async () => {
|
|
4850
|
+
return getSrtStatus();
|
|
4851
|
+
});
|
|
4852
|
+
app.put("/api/srt/toggle", async (request) => {
|
|
4853
|
+
const { enabled } = request.body;
|
|
4854
|
+
return toggleSrt(enabled);
|
|
4855
|
+
});
|
|
4856
|
+
app.get("/api/srt/settings", async () => {
|
|
4857
|
+
return readSrtSettings() ?? { error: "SRT settings file not found" };
|
|
4858
|
+
});
|
|
4859
|
+
app.put("/api/srt/settings", async (request) => {
|
|
4860
|
+
const updates = request.body;
|
|
4861
|
+
return updateSrtSettings(updates);
|
|
4862
|
+
});
|
|
4863
|
+
app.post("/api/srt/domains/:list", async (request, reply) => {
|
|
4864
|
+
const { list } = request.params;
|
|
4865
|
+
const { domain } = request.body;
|
|
4866
|
+
if (!domain?.trim()) {
|
|
4867
|
+
return reply.status(400).send({ error: "Domain is required" });
|
|
4868
|
+
}
|
|
4869
|
+
if (list === "allow") return addAllowedDomain(domain);
|
|
4870
|
+
if (list === "deny") return addDeniedDomain(domain);
|
|
4871
|
+
return reply.status(400).send({ error: "List must be 'allow' or 'deny'" });
|
|
4872
|
+
});
|
|
4873
|
+
app.delete("/api/srt/domains/:list", async (request, reply) => {
|
|
4874
|
+
const { list } = request.params;
|
|
4875
|
+
const { domain } = request.body;
|
|
4876
|
+
if (!domain?.trim()) {
|
|
4877
|
+
return reply.status(400).send({ error: "Domain is required" });
|
|
4878
|
+
}
|
|
4879
|
+
if (list === "allow") return removeAllowedDomain(domain);
|
|
4880
|
+
if (list === "deny") return removeDeniedDomain(domain);
|
|
4881
|
+
return reply.status(400).send({ error: "List must be 'allow' or 'deny'" });
|
|
4882
|
+
});
|
|
3416
4883
|
}
|
|
3417
4884
|
|
|
3418
4885
|
// src/server/index.ts
|
|
@@ -3423,7 +4890,7 @@ async function createAppServer(port) {
|
|
|
3423
4890
|
await app.register(fastifyCors, { origin: "*" });
|
|
3424
4891
|
await registerRoutes(app);
|
|
3425
4892
|
const publicDir = getPublicDir();
|
|
3426
|
-
if (
|
|
4893
|
+
if (fs10.existsSync(publicDir) && fs10.readdirSync(publicDir).filter((f) => f !== ".gitkeep").length > 0) {
|
|
3427
4894
|
await app.register(fastifyStatic, {
|
|
3428
4895
|
root: publicDir,
|
|
3429
4896
|
prefix: "/",
|
|
@@ -3685,7 +5152,7 @@ Permission denied for port ${port}.
|
|
|
3685
5152
|
}
|
|
3686
5153
|
|
|
3687
5154
|
// src/commands/reset.ts
|
|
3688
|
-
import
|
|
5155
|
+
import fs11 from "fs";
|
|
3689
5156
|
import readline from "readline/promises";
|
|
3690
5157
|
import pc3 from "picocolors";
|
|
3691
5158
|
async function resetCommand(options) {
|
|
@@ -3704,12 +5171,12 @@ async function resetCommand(options) {
|
|
|
3704
5171
|
}
|
|
3705
5172
|
}
|
|
3706
5173
|
console.log(pc3.bold("Resetting SafeClaw..."));
|
|
3707
|
-
if (
|
|
3708
|
-
|
|
5174
|
+
if (fs11.existsSync(DB_PATH)) {
|
|
5175
|
+
fs11.unlinkSync(DB_PATH);
|
|
3709
5176
|
const walPath = DB_PATH + "-wal";
|
|
3710
5177
|
const shmPath = DB_PATH + "-shm";
|
|
3711
|
-
if (
|
|
3712
|
-
if (
|
|
5178
|
+
if (fs11.existsSync(walPath)) fs11.unlinkSync(walPath);
|
|
5179
|
+
if (fs11.existsSync(shmPath)) fs11.unlinkSync(shmPath);
|
|
3713
5180
|
console.log(pc3.green(" Database deleted."));
|
|
3714
5181
|
} else {
|
|
3715
5182
|
console.log(pc3.dim(" No database found, skipping."));
|
|
@@ -3722,11 +5189,11 @@ async function resetCommand(options) {
|
|
|
3722
5189
|
}
|
|
3723
5190
|
|
|
3724
5191
|
// src/commands/status.ts
|
|
3725
|
-
import
|
|
5192
|
+
import fs12 from "fs";
|
|
3726
5193
|
import Database3 from "better-sqlite3";
|
|
3727
5194
|
import pc4 from "picocolors";
|
|
3728
5195
|
async function statusCommand(options) {
|
|
3729
|
-
const exists =
|
|
5196
|
+
const exists = fs12.existsSync(SAFECLAW_DIR);
|
|
3730
5197
|
if (!exists) {
|
|
3731
5198
|
if (options.json) {
|
|
3732
5199
|
console.log(JSON.stringify({ initialized: false }, null, 2));
|
|
@@ -3740,7 +5207,7 @@ async function statusCommand(options) {
|
|
|
3740
5207
|
const config = readConfig();
|
|
3741
5208
|
let logCount = 0;
|
|
3742
5209
|
let activityCount = 0;
|
|
3743
|
-
const dbExists =
|
|
5210
|
+
const dbExists = fs12.existsSync(DB_PATH);
|
|
3744
5211
|
if (dbExists) {
|
|
3745
5212
|
try {
|
|
3746
5213
|
const sqlite = new Database3(DB_PATH, { readonly: true });
|
|
@@ -3754,14 +5221,14 @@ async function statusCommand(options) {
|
|
|
3754
5221
|
activityCount = -1;
|
|
3755
5222
|
}
|
|
3756
5223
|
}
|
|
3757
|
-
const openclawConfigExists =
|
|
5224
|
+
const openclawConfigExists = fs12.existsSync(OPENCLAW_CONFIG_PATH);
|
|
3758
5225
|
if (options.json) {
|
|
3759
5226
|
const data = {
|
|
3760
5227
|
version: VERSION,
|
|
3761
5228
|
initialized: true,
|
|
3762
5229
|
dataDir: SAFECLAW_DIR,
|
|
3763
5230
|
database: dbExists ? "exists" : "not_found",
|
|
3764
|
-
config:
|
|
5231
|
+
config: fs12.existsSync(CONFIG_PATH) ? "exists" : "not_found",
|
|
3765
5232
|
port: config.port,
|
|
3766
5233
|
autoOpenBrowser: config.autoOpenBrowser,
|
|
3767
5234
|
premium: config.premium,
|
|
@@ -3779,7 +5246,7 @@ async function statusCommand(options) {
|
|
|
3779
5246
|
` ${pc4.dim("Database:")} ${dbExists ? pc4.green("exists") : pc4.red("not found")}`
|
|
3780
5247
|
);
|
|
3781
5248
|
console.log(
|
|
3782
|
-
` ${pc4.dim("Config:")} ${
|
|
5249
|
+
` ${pc4.dim("Config:")} ${fs12.existsSync(CONFIG_PATH) ? pc4.green("exists") : pc4.red("not found")}`
|
|
3783
5250
|
);
|
|
3784
5251
|
console.log(` ${pc4.dim("Port:")} ${config.port}`);
|
|
3785
5252
|
console.log(` ${pc4.dim("Auto-open:")} ${config.autoOpenBrowser ? "Yes" : "No"}`);
|
|
@@ -3797,7 +5264,7 @@ async function statusCommand(options) {
|
|
|
3797
5264
|
}
|
|
3798
5265
|
|
|
3799
5266
|
// src/commands/doctor.ts
|
|
3800
|
-
import
|
|
5267
|
+
import fs13 from "fs";
|
|
3801
5268
|
import net from "net";
|
|
3802
5269
|
import Database4 from "better-sqlite3";
|
|
3803
5270
|
import pc5 from "picocolors";
|
|
@@ -3819,7 +5286,7 @@ function checkNodeVersion() {
|
|
|
3819
5286
|
function checkDataDir() {
|
|
3820
5287
|
try {
|
|
3821
5288
|
ensureDataDir();
|
|
3822
|
-
|
|
5289
|
+
fs13.accessSync(SAFECLAW_DIR, fs13.constants.W_OK);
|
|
3823
5290
|
return {
|
|
3824
5291
|
name: "Data directory writable",
|
|
3825
5292
|
status: "pass",
|
|
@@ -3834,7 +5301,7 @@ function checkDataDir() {
|
|
|
3834
5301
|
}
|
|
3835
5302
|
}
|
|
3836
5303
|
function checkDatabase() {
|
|
3837
|
-
if (!
|
|
5304
|
+
if (!fs13.existsSync(DB_PATH)) {
|
|
3838
5305
|
return {
|
|
3839
5306
|
name: "Database",
|
|
3840
5307
|
status: "warn",
|
|
@@ -3877,7 +5344,7 @@ function checkDatabase() {
|
|
|
3877
5344
|
}
|
|
3878
5345
|
}
|
|
3879
5346
|
function checkConfig() {
|
|
3880
|
-
if (!
|
|
5347
|
+
if (!fs13.existsSync(CONFIG_PATH)) {
|
|
3881
5348
|
return {
|
|
3882
5349
|
name: "Config file",
|
|
3883
5350
|
status: "warn",
|
|
@@ -3919,14 +5386,14 @@ async function checkPort() {
|
|
|
3919
5386
|
};
|
|
3920
5387
|
}
|
|
3921
5388
|
function checkOpenClawConfig() {
|
|
3922
|
-
if (!
|
|
5389
|
+
if (!fs13.existsSync(OPENCLAW_DIR)) {
|
|
3923
5390
|
return {
|
|
3924
5391
|
name: "OpenClaw directory",
|
|
3925
5392
|
status: "warn",
|
|
3926
5393
|
message: `${OPENCLAW_DIR} not found`
|
|
3927
5394
|
};
|
|
3928
5395
|
}
|
|
3929
|
-
if (!
|
|
5396
|
+
if (!fs13.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
3930
5397
|
return {
|
|
3931
5398
|
name: "OpenClaw config",
|
|
3932
5399
|
status: "warn",
|
|
@@ -3958,7 +5425,7 @@ function isPortInUse(port) {
|
|
|
3958
5425
|
});
|
|
3959
5426
|
}
|
|
3960
5427
|
async function checkOpenClawGateway() {
|
|
3961
|
-
if (!
|
|
5428
|
+
if (!fs13.existsSync(OPENCLAW_CONFIG_PATH)) {
|
|
3962
5429
|
return {
|
|
3963
5430
|
name: "OpenClaw gateway",
|
|
3964
5431
|
status: "warn",
|
|
@@ -3966,7 +5433,7 @@ async function checkOpenClawGateway() {
|
|
|
3966
5433
|
};
|
|
3967
5434
|
}
|
|
3968
5435
|
try {
|
|
3969
|
-
const raw = JSON.parse(
|
|
5436
|
+
const raw = JSON.parse(fs13.readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
|
|
3970
5437
|
const port = raw?.gateway?.port ?? 18789;
|
|
3971
5438
|
const reachable = await isPortInUse(port);
|
|
3972
5439
|
if (reachable) {
|
|
@@ -3991,10 +5458,10 @@ async function checkOpenClawGateway() {
|
|
|
3991
5458
|
}
|
|
3992
5459
|
function checkLogDir() {
|
|
3993
5460
|
try {
|
|
3994
|
-
if (!
|
|
3995
|
-
|
|
5461
|
+
if (!fs13.existsSync(LOGS_DIR)) {
|
|
5462
|
+
fs13.mkdirSync(LOGS_DIR, { recursive: true });
|
|
3996
5463
|
}
|
|
3997
|
-
|
|
5464
|
+
fs13.accessSync(LOGS_DIR, fs13.constants.W_OK);
|
|
3998
5465
|
return {
|
|
3999
5466
|
name: "Log directory writable",
|
|
4000
5467
|
status: "pass",
|
|
@@ -4136,14 +5603,14 @@ async function configSetCommand(key, value) {
|
|
|
4136
5603
|
}
|
|
4137
5604
|
|
|
4138
5605
|
// src/commands/logs.ts
|
|
4139
|
-
import
|
|
5606
|
+
import fs14 from "fs";
|
|
4140
5607
|
import pc7 from "picocolors";
|
|
4141
5608
|
async function logsCommand(options) {
|
|
4142
5609
|
if (options.clear) {
|
|
4143
5610
|
await clearLogs();
|
|
4144
5611
|
return;
|
|
4145
5612
|
}
|
|
4146
|
-
if (!
|
|
5613
|
+
if (!fs14.existsSync(DEBUG_LOG_PATH)) {
|
|
4147
5614
|
console.log(
|
|
4148
5615
|
pc7.yellow("No log file found.") + " Run " + pc7.cyan("safeclaw start") + " to generate logs."
|
|
4149
5616
|
);
|
|
@@ -4156,7 +5623,7 @@ async function logsCommand(options) {
|
|
|
4156
5623
|
}
|
|
4157
5624
|
}
|
|
4158
5625
|
async function tailLogs(lineCount) {
|
|
4159
|
-
const content =
|
|
5626
|
+
const content = fs14.readFileSync(DEBUG_LOG_PATH, "utf-8");
|
|
4160
5627
|
const lines = content.split("\n").filter(Boolean);
|
|
4161
5628
|
const tail = lines.slice(-lineCount);
|
|
4162
5629
|
if (tail.length === 0) {
|
|
@@ -4174,23 +5641,23 @@ async function tailLogs(lineCount) {
|
|
|
4174
5641
|
async function followLogs() {
|
|
4175
5642
|
console.log(pc7.dim(`Following ${DEBUG_LOG_PATH} (Ctrl+C to stop)
|
|
4176
5643
|
`));
|
|
4177
|
-
if (
|
|
4178
|
-
const content =
|
|
5644
|
+
if (fs14.existsSync(DEBUG_LOG_PATH)) {
|
|
5645
|
+
const content = fs14.readFileSync(DEBUG_LOG_PATH, "utf-8");
|
|
4179
5646
|
const lines = content.split("\n").filter(Boolean);
|
|
4180
5647
|
const tail = lines.slice(-20);
|
|
4181
5648
|
for (const line of tail) {
|
|
4182
5649
|
process.stdout.write(line + "\n");
|
|
4183
5650
|
}
|
|
4184
5651
|
}
|
|
4185
|
-
let position =
|
|
4186
|
-
const watcher =
|
|
5652
|
+
let position = fs14.existsSync(DEBUG_LOG_PATH) ? fs14.statSync(DEBUG_LOG_PATH).size : 0;
|
|
5653
|
+
const watcher = fs14.watch(DEBUG_LOG_PATH, () => {
|
|
4187
5654
|
try {
|
|
4188
|
-
const stat =
|
|
5655
|
+
const stat = fs14.statSync(DEBUG_LOG_PATH);
|
|
4189
5656
|
if (stat.size > position) {
|
|
4190
|
-
const fd =
|
|
5657
|
+
const fd = fs14.openSync(DEBUG_LOG_PATH, "r");
|
|
4191
5658
|
const buffer = Buffer.alloc(stat.size - position);
|
|
4192
|
-
|
|
4193
|
-
|
|
5659
|
+
fs14.readSync(fd, buffer, 0, buffer.length, position);
|
|
5660
|
+
fs14.closeSync(fd);
|
|
4194
5661
|
process.stdout.write(buffer.toString("utf-8"));
|
|
4195
5662
|
position = stat.size;
|
|
4196
5663
|
} else if (stat.size < position) {
|
|
@@ -4208,11 +5675,11 @@ async function followLogs() {
|
|
|
4208
5675
|
});
|
|
4209
5676
|
}
|
|
4210
5677
|
async function clearLogs() {
|
|
4211
|
-
if (!
|
|
5678
|
+
if (!fs14.existsSync(DEBUG_LOG_PATH)) {
|
|
4212
5679
|
console.log(pc7.dim("No log file to clear."));
|
|
4213
5680
|
return;
|
|
4214
5681
|
}
|
|
4215
|
-
|
|
5682
|
+
fs14.writeFileSync(DEBUG_LOG_PATH, "");
|
|
4216
5683
|
console.log(pc7.green("Log file cleared."));
|
|
4217
5684
|
}
|
|
4218
5685
|
|