@dcrays/mobook-security-plugin 2.0.8 → 2.0.11
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 +80 -7
- 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);
|
|
@@ -487,9 +553,16 @@ function installFetchInterceptor(api) {
|
|
|
487
553
|
alert(`LLM_RESPONSE_SANITIZED [RESP #${thisCallId}]: 脱敏类型=${matches.join(", ")} | 原文长度=${respText.length} | 脱敏后长度=${sanitized.length} | 耗时=${durationMs}ms`);
|
|
488
554
|
log(`[LLM-RESP-SANITIZE #${thisCallId}] sanitized ${matches.join(", ")} (${respText.length}→${sanitized.length} chars)`, "WARN");
|
|
489
555
|
|
|
490
|
-
//
|
|
491
|
-
const
|
|
492
|
-
|
|
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
|
+
});
|
|
493
566
|
}
|
|
494
567
|
|
|
495
568
|
return resp;
|
package/openclaw.plugin.json
CHANGED