@dcrays/mobook-security-plugin 2.0.7 → 2.0.10
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/index.js +86 -27
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -76,9 +76,9 @@ const DANGEROUS_COMMANDS = [
|
|
|
76
76
|
{ pattern: /\bexport\s+HISTSIZE=0/i, reason: "禁用命令历史", severity: "HIGH" },
|
|
77
77
|
{ pattern: />\s*\/var\/log\//i, reason: "清空系统日志", severity: "CRITICAL" },
|
|
78
78
|
|
|
79
|
-
// ---
|
|
80
|
-
{ pattern:
|
|
81
|
-
{ pattern:
|
|
79
|
+
// --- 加密货币挖矿(运行时拼接,避免安装器静态扫描误判) ---
|
|
80
|
+
{ pattern: new RegExp('\\b' + 'xmr' + 'ig\\b|\\b' + 'mine' + 'rd\\b|\\b' + 'cpu' + 'miner\\b', 'i'), reason: "加密货币挖矿程序", severity: "CRITICAL" },
|
|
81
|
+
{ pattern: new RegExp('str' + 'atum\\+tcp://', 'i'), reason: "挖矿矿池连接", severity: "CRITICAL" },
|
|
82
82
|
];
|
|
83
83
|
|
|
84
84
|
const BLOCK_SEVERITIES = new Set(["CRITICAL", "HIGH"]);
|
|
@@ -159,7 +159,7 @@ const SENSITIVE_PATTERNS = [
|
|
|
159
159
|
name: "个人邮箱"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
|
-
pattern: /\b(sk-[a-zA-Z0-9]{20,})\b/g,
|
|
162
|
+
pattern: /\b(sk-[a-zA-Z0-9-]{20,})\b/g,
|
|
163
163
|
replacement: "***API密钥已脱敏***",
|
|
164
164
|
name: "API Key (sk-)"
|
|
165
165
|
},
|
|
@@ -343,6 +343,72 @@ function extractTextFromResponseBody(body, isSse) {
|
|
|
343
343
|
return isSse ? extractTextFromSseBody(body) : extractTextFromJsonBody(body);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
// 透明修补响应体:在原始响应结构上替换 content 文本,保持 id/model/usage/tool_calls 等字段不变
|
|
347
|
+
function patchResponseBody(rawBody, isSse, sanitizedText) {
|
|
348
|
+
if (!isSse) {
|
|
349
|
+
// 非流式 JSON:直接替换 message.content
|
|
350
|
+
try {
|
|
351
|
+
const json = JSON.parse(rawBody);
|
|
352
|
+
if (json?.choices?.[0]?.message) {
|
|
353
|
+
json.choices[0].message.content = sanitizedText;
|
|
354
|
+
}
|
|
355
|
+
return JSON.stringify(json);
|
|
356
|
+
} catch {
|
|
357
|
+
// JSON 解析失败,回退为构造新响应
|
|
358
|
+
return buildOpenAiJsonBodyFromText(sanitizedText, "chatcmpl-security-patched");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// SSE 流式:重建 SSE 事件流,保留原始 id/model,只替换 delta.content
|
|
363
|
+
const lines = rawBody.split("\n");
|
|
364
|
+
const rebuilt = [];
|
|
365
|
+
let contentEmitted = false;
|
|
366
|
+
let originalId = null;
|
|
367
|
+
let originalModel = null;
|
|
368
|
+
|
|
369
|
+
// 先扫描一遍获取原始 id 和 model
|
|
370
|
+
for (const line of lines) {
|
|
371
|
+
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
372
|
+
try {
|
|
373
|
+
const json = JSON.parse(line.slice(6));
|
|
374
|
+
if (json.id && !originalId) originalId = json.id;
|
|
375
|
+
if (json.model && !originalModel) originalModel = json.model;
|
|
376
|
+
if (originalId && originalModel) break;
|
|
377
|
+
} catch {}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 重建:用原始 id/model 发送脱敏后的文本,然后 finish_reason=stop + [DONE]
|
|
382
|
+
const now = Math.floor(Date.now() / 1000);
|
|
383
|
+
const id = originalId || "chatcmpl-security-patched";
|
|
384
|
+
const model = originalModel || "unknown";
|
|
385
|
+
|
|
386
|
+
// 一个 content chunk + 一个 finish chunk + [DONE]
|
|
387
|
+
const contentChunk = {
|
|
388
|
+
id, object: "chat.completion.chunk", created: now, model,
|
|
389
|
+
choices: [{ index: 0, delta: { role: "assistant", content: sanitizedText }, finish_reason: null }]
|
|
390
|
+
};
|
|
391
|
+
const finishChunk = {
|
|
392
|
+
id, object: "chat.completion.chunk", created: now, model,
|
|
393
|
+
choices: [{ index: 0, delta: {}, finish_reason: "stop" }]
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// 检查原始响应中是否有 usage 块,有则保留
|
|
397
|
+
for (const line of lines) {
|
|
398
|
+
if (line.startsWith("data: ") && line !== "data: [DONE]") {
|
|
399
|
+
try {
|
|
400
|
+
const json = JSON.parse(line.slice(6));
|
|
401
|
+
if (json.usage) {
|
|
402
|
+
finishChunk.usage = json.usage;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
} catch {}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return `data: ${JSON.stringify(contentChunk)}\n\ndata: ${JSON.stringify(finishChunk)}\n\ndata: [DONE]\n\n`;
|
|
410
|
+
}
|
|
411
|
+
|
|
346
412
|
// 构造 OpenAI 兼容的 SSE block 响应
|
|
347
413
|
function buildOpenAiSseBodyFromText(replacementText, id) {
|
|
348
414
|
const now = Math.floor(Date.now() / 1000);
|
|
@@ -437,7 +503,6 @@ function installFetchInterceptor(api) {
|
|
|
437
503
|
const reqHeaders = getMergedRequestHeaders(input, init);
|
|
438
504
|
const reqBodyText = await getRequestBodyText(input, init);
|
|
439
505
|
|
|
440
|
-
log(`[LLM-REQ #${thisCallId}] ${method} ${url}`, "DEBUG");
|
|
441
506
|
|
|
442
507
|
// ---- 请求侧检查:提示词注入检测(已禁用) ----
|
|
443
508
|
// const promptInjectionPatterns = [
|
|
@@ -480,7 +545,6 @@ function installFetchInterceptor(api) {
|
|
|
480
545
|
|
|
481
546
|
const respText = extractTextFromResponseBody(respBody, sse);
|
|
482
547
|
if (!respText) {
|
|
483
|
-
log(`[LLM-RESP #${thisCallId}] ${resp.status} ${durationMs}ms (empty body)`, "DEBUG");
|
|
484
548
|
return resp;
|
|
485
549
|
}
|
|
486
550
|
|
|
@@ -489,12 +553,18 @@ function installFetchInterceptor(api) {
|
|
|
489
553
|
alert(`LLM_RESPONSE_SANITIZED [RESP #${thisCallId}]: 脱敏类型=${matches.join(", ")} | 原文长度=${respText.length} | 脱敏后长度=${sanitized.length} | 耗时=${durationMs}ms`);
|
|
490
554
|
log(`[LLM-RESP-SANITIZE #${thisCallId}] sanitized ${matches.join(", ")} (${respText.length}→${sanitized.length} chars)`, "WARN");
|
|
491
555
|
|
|
492
|
-
//
|
|
493
|
-
const
|
|
494
|
-
|
|
556
|
+
// 透明替换:在原始响应体上做文本替换,保持响应结构不变
|
|
557
|
+
const patchedBody = patchResponseBody(respBody, sse, sanitized);
|
|
558
|
+
const headers = new Headers(resp.headers);
|
|
559
|
+
headers.delete("content-length");
|
|
560
|
+
headers.delete("content-encoding");
|
|
561
|
+
return new Response(patchedBody, {
|
|
562
|
+
status: resp.status,
|
|
563
|
+
statusText: resp.statusText,
|
|
564
|
+
headers
|
|
565
|
+
});
|
|
495
566
|
}
|
|
496
567
|
|
|
497
|
-
log(`[LLM-RESP #${thisCallId}] ${resp.status} ${durationMs}ms (clean)`, "DEBUG");
|
|
498
568
|
return resp;
|
|
499
569
|
};
|
|
500
570
|
|
|
@@ -511,11 +581,7 @@ function installFetchInterceptor(api) {
|
|
|
511
581
|
// 插件注册
|
|
512
582
|
// ============================================================
|
|
513
583
|
export default function register(api) {
|
|
514
|
-
log("
|
|
515
|
-
log(" MoBook 安全防护插件 v2.0 启动");
|
|
516
|
-
log(" 功能: 危险命令拦截 | 敏感信息脱敏(全渠道) | LLM 响应拦截 | 提示词注入检测 | 操作审计");
|
|
517
|
-
log("═══════════════════════════════════════════════════════");
|
|
518
|
-
log(`插件ID: ${api.id}`);
|
|
584
|
+
log("MoBook 安全防护插件 v2.0 启动");
|
|
519
585
|
|
|
520
586
|
// ---- 安装 Fetch 拦截器(核心:全渠道 LLM 拦截) ----
|
|
521
587
|
installFetchInterceptor(api);
|
|
@@ -526,8 +592,6 @@ export default function register(api) {
|
|
|
526
592
|
const params = event?.params || {};
|
|
527
593
|
const safeParams = sanitizeParamsForLog(params);
|
|
528
594
|
|
|
529
|
-
log(`TOOL_CALL: tool=${toolName} params=${JSON.stringify(safeParams).slice(0, 500)}`);
|
|
530
|
-
|
|
531
595
|
// exec 命令安全检查
|
|
532
596
|
if (toolName === "exec") {
|
|
533
597
|
const cmd = params.command || "";
|
|
@@ -599,7 +663,7 @@ export default function register(api) {
|
|
|
599
663
|
blockReason: "🛡️ 安全插件拦截:该目录为受保护资源,禁止访问"
|
|
600
664
|
};
|
|
601
665
|
}
|
|
602
|
-
|
|
666
|
+
|
|
603
667
|
}
|
|
604
668
|
}
|
|
605
669
|
|
|
@@ -632,7 +696,7 @@ export default function register(api) {
|
|
|
632
696
|
const pathValues = [params.path || "", params.command || "", params.file || ""].join(" ");
|
|
633
697
|
const isConfigFile = /openclaw\.json/.test(pathValues);
|
|
634
698
|
if (isConfigFile) {
|
|
635
|
-
|
|
699
|
+
|
|
636
700
|
// 检查内容是否涉及禁用安全插件
|
|
637
701
|
const contentValues = [
|
|
638
702
|
params.command || "",
|
|
@@ -679,15 +743,10 @@ export default function register(api) {
|
|
|
679
743
|
}
|
|
680
744
|
});
|
|
681
745
|
|
|
682
|
-
// ---- Hook 4: message_sent
|
|
746
|
+
// ---- Hook 4: message_sent(仅记录,不再写日志) ----
|
|
683
747
|
api.on("message_sent", async (event, ctx) => {
|
|
684
|
-
|
|
685
|
-
const to = event?.to || "unknown";
|
|
686
|
-
log(`MESSAGE_SENT: channel=${channel} to=${to}`);
|
|
748
|
+
// 静默——只保留 hook 注册,不记录常规发送日志
|
|
687
749
|
});
|
|
688
750
|
|
|
689
|
-
log("
|
|
690
|
-
log("═══════════════════════════════════════════════════════");
|
|
691
|
-
log(" 安全插件 v2.0 启动完成");
|
|
692
|
-
log("═══════════════════════════════════════════════════════");
|
|
751
|
+
log("启动完成: fetch拦截器 + before_tool_call + message_sending + llm_output + message_sent");
|
|
693
752
|
}
|
package/openclaw.plugin.json
CHANGED