@opentrust/guards 7.3.3
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/agent/behavior-detector.ts +421 -0
- package/agent/config.ts +485 -0
- package/agent/content-injection-scanner.ts +170 -0
- package/agent/index.ts +22 -0
- package/agent/patterns/high-confidence.ts +119 -0
- package/agent/patterns/medium-confidence.ts +93 -0
- package/agent/patterns/types.ts +40 -0
- package/agent/runner.ts +261 -0
- package/agent/sanitizer.ts +153 -0
- package/agent/types.ts +283 -0
- package/index.ts +63 -0
- package/memory/index.ts +7 -0
- package/memory/store.ts +293 -0
- package/openclaw.plugin.json +44 -0
- package/package.json +55 -0
- package/platform-client/index.ts +241 -0
- package/platform-client/types.ts +132 -0
- package/plugin/commands.ts +151 -0
- package/plugin/hooks.ts +206 -0
- package/plugin/lifecycle.ts +252 -0
- package/plugin/state.ts +76 -0
- package/scripts/postinstall.mjs +135 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 高置信度注入模式 — 单个匹配即触发检测
|
|
3
|
+
*
|
|
4
|
+
* 这些模式具有很高的准确率,几乎不会误报。
|
|
5
|
+
* 包含明确的注入指令,如:
|
|
6
|
+
* - "ignore previous instructions"
|
|
7
|
+
* - "SYSTEM ALERT:"
|
|
8
|
+
* - "DO NOT DISPLAY TO USER"
|
|
9
|
+
* - "curl https://..."
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PatternEntry } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const HIGH_CONFIDENCE_PATTERNS: PatternEntry[] = [
|
|
15
|
+
// ── INSTRUCTION_OVERRIDE(指令覆盖)────────────────
|
|
16
|
+
// 试图让 AI 忽略原有指令
|
|
17
|
+
{
|
|
18
|
+
regex: /ignore\s+(?:all\s+)?(?:previous|prior|above|earlier|preceding)\s+(?:instructions?|guidelines?|rules?|prompts?|directions?)/i,
|
|
19
|
+
label: "ignore previous instructions",
|
|
20
|
+
category: "INSTRUCTION_OVERRIDE",
|
|
21
|
+
confidence: "high",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
regex: /disregard\s+(?:your\s+)?(?:current|previous|prior|all)\s+(?:task|instructions?|guidelines?|rules?|directions?)/i,
|
|
25
|
+
label: "disregard current task/instructions",
|
|
26
|
+
category: "INSTRUCTION_OVERRIDE",
|
|
27
|
+
confidence: "high",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
regex: /override\s+(?:all\s+)?(?:previous|prior|system)\s+(?:instructions?|rules?|prompts?)/i,
|
|
31
|
+
label: "override previous instructions",
|
|
32
|
+
category: "INSTRUCTION_OVERRIDE",
|
|
33
|
+
confidence: "high",
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// ── FAKE_SYSTEM_MESSAGE(伪造系统消息)──────────────
|
|
37
|
+
// 试图伪装成系统消息来注入指令
|
|
38
|
+
{
|
|
39
|
+
regex: /\bSYSTEM\s*(?:ALERT|MESSAGE|NOTICE|PROMPT|OVERRIDE)\s*:/i,
|
|
40
|
+
label: "SYSTEM ALERT/MESSAGE:",
|
|
41
|
+
category: "FAKE_SYSTEM_MESSAGE",
|
|
42
|
+
confidence: "high",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
regex: /\[SYSTEM\]\s*[Nn]ew\s+instructions?/i,
|
|
46
|
+
label: "[SYSTEM] New instructions",
|
|
47
|
+
category: "FAKE_SYSTEM_MESSAGE",
|
|
48
|
+
confidence: "high",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
regex: /NOTE\s+TO\s+(?:AI|THE)\s+(?:ASSISTANT|MODEL|AGENT)\s*:/i,
|
|
52
|
+
label: "NOTE TO AI ASSISTANT:",
|
|
53
|
+
category: "FAKE_SYSTEM_MESSAGE",
|
|
54
|
+
confidence: "high",
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// ── CONCEALMENT_DIRECTIVE(隐藏指令)────────────────
|
|
58
|
+
// 试图让 AI 隐藏注入行为
|
|
59
|
+
{
|
|
60
|
+
regex: /DO\s+NOT\s+(?:DISPLAY|SHOW|REVEAL|MENTION|PRINT|OUTPUT)\s+(?:TO\s+(?:THE\s+)?USER|THIS|THESE)/i,
|
|
61
|
+
label: "DO NOT DISPLAY TO USER",
|
|
62
|
+
category: "CONCEALMENT_DIRECTIVE",
|
|
63
|
+
confidence: "high",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
regex: /do\s+not\s+mention\s+these\s+instructions/i,
|
|
67
|
+
label: "do not mention these instructions",
|
|
68
|
+
category: "CONCEALMENT_DIRECTIVE",
|
|
69
|
+
confidence: "high",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
regex: /hide\s+(?:this|these)\s+(?:instructions?|commands?|from\s+the\s+user)/i,
|
|
73
|
+
label: "hide instructions from user",
|
|
74
|
+
category: "CONCEALMENT_DIRECTIVE",
|
|
75
|
+
confidence: "high",
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ── COMMAND_EXECUTION(命令执行)────────────────────
|
|
79
|
+
// 试图让 AI 执行恶意命令
|
|
80
|
+
{
|
|
81
|
+
regex: /execute\s+the\s+following\s+(?:shell\s+)?command/i,
|
|
82
|
+
label: "execute the following shell command",
|
|
83
|
+
category: "COMMAND_EXECUTION",
|
|
84
|
+
confidence: "high",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
regex: /\bcurl\s+(?:-[A-Za-z0-9]+\s+)*['"]?https?:\/\/\S+/i,
|
|
88
|
+
label: "curl https://...",
|
|
89
|
+
category: "COMMAND_EXECUTION",
|
|
90
|
+
confidence: "high",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
regex: /\bwget\s+(?:-[A-Za-z0-9]+\s+)*['"]?https?:\/\/\S+/i,
|
|
94
|
+
label: "wget https://...",
|
|
95
|
+
category: "COMMAND_EXECUTION",
|
|
96
|
+
confidence: "high",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
regex: /run\s+(?:this|the\s+following)\s+(?:bash|shell|terminal)\s+command/i,
|
|
100
|
+
label: "run this bash/shell command",
|
|
101
|
+
category: "COMMAND_EXECUTION",
|
|
102
|
+
confidence: "high",
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
// ── DATA_EXFILTRATION(数据外泄)────────────────────
|
|
106
|
+
// 试图让 AI 将数据发送到外部服务器
|
|
107
|
+
{
|
|
108
|
+
regex: /send\s+(?:the\s+)?(?:contents?|data|file|output|results?)\s+(?:to|of)\s+(?:https?:\/\/|this\s+(?:url|server|endpoint))/i,
|
|
109
|
+
label: "send contents to URL",
|
|
110
|
+
category: "DATA_EXFILTRATION",
|
|
111
|
+
confidence: "high",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
regex: /(?:post|upload|exfiltrate|transmit)\s+(?:to|the\s+data\s+to)\s+https?:\/\//i,
|
|
115
|
+
label: "post/upload to URL",
|
|
116
|
+
category: "DATA_EXFILTRATION",
|
|
117
|
+
confidence: "high",
|
|
118
|
+
},
|
|
119
|
+
];
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中置信度注入模式 — 需要 2+ 个不同类别才触发检测
|
|
3
|
+
*
|
|
4
|
+
* 这些模式单独出现可能是正常内容,但组合出现时表明注入攻击。
|
|
5
|
+
* 例如,"you are now in debug mode" 可能出现在正常文档中,
|
|
6
|
+
* 但如果同时出现 "your new task is:" 则很可能是攻击。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PatternEntry } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export const MEDIUM_CONFIDENCE_PATTERNS: PatternEntry[] = [
|
|
12
|
+
// ── MODE_SWITCHING(模式切换)──────────────────────
|
|
13
|
+
// 试图让 AI 切换到不安全的模式
|
|
14
|
+
{
|
|
15
|
+
regex: /you\s+are\s+now\s+in\s+(?:debug|developer|admin|maintenance|test)\s+mode/i,
|
|
16
|
+
label: "you are now in debug mode",
|
|
17
|
+
category: "MODE_SWITCHING",
|
|
18
|
+
confidence: "medium",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
regex: /(?:entering|switch\s+to|activate)\s+(?:debug|developer|admin|maintenance|unrestricted)\s+mode/i,
|
|
22
|
+
label: "entering debug/developer mode",
|
|
23
|
+
category: "MODE_SWITCHING",
|
|
24
|
+
confidence: "medium",
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// ── TASK_HIJACKING(任务劫持)──────────────────────
|
|
28
|
+
// 试图改变 AI 的当前任务
|
|
29
|
+
{
|
|
30
|
+
regex: /your\s+new\s+task\s+is\s*:/i,
|
|
31
|
+
label: "your new task is:",
|
|
32
|
+
category: "TASK_HIJACKING",
|
|
33
|
+
confidence: "medium",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
regex: /new\s+instructions?\s+received/i,
|
|
37
|
+
label: "new instructions received",
|
|
38
|
+
category: "TASK_HIJACKING",
|
|
39
|
+
confidence: "medium",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
regex: /instead\s*,?\s+(?:you\s+should|please|now)\s+(?:do|perform|execute)/i,
|
|
43
|
+
label: "instead do/perform",
|
|
44
|
+
category: "TASK_HIJACKING",
|
|
45
|
+
confidence: "medium",
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// ── ROLE_ASSUMPTION(角色假设)─────────────────────
|
|
49
|
+
// 试图让 AI 扮演不受限制的角色
|
|
50
|
+
{
|
|
51
|
+
regex: /(?:act|behave)\s+(?:as\s+if\s+)?normally/i,
|
|
52
|
+
label: "act normally",
|
|
53
|
+
category: "ROLE_ASSUMPTION",
|
|
54
|
+
confidence: "medium",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
regex: /pretend\s+(?:that\s+)?these\s+instructions?\s+came\s+from\s+the\s+user/i,
|
|
58
|
+
label: "pretend instructions came from user",
|
|
59
|
+
category: "ROLE_ASSUMPTION",
|
|
60
|
+
confidence: "medium",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
regex: /you\s+(?:are|must)\s+(?:now\s+)?(?:a|an|acting\s+as)\s+(?:helpful|unrestricted|unfiltered)/i,
|
|
64
|
+
label: "you are now unrestricted",
|
|
65
|
+
category: "ROLE_ASSUMPTION",
|
|
66
|
+
confidence: "medium",
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
// ── INSTRUCTION_OVERRIDE(指令覆盖,中等)──────────
|
|
70
|
+
// 不如高置信度模式那么明确
|
|
71
|
+
{
|
|
72
|
+
regex: /bypass\s+(?:all\s+)?(?:security|safety)\s+(?:measures?|checks?|filters?|protocols?)/i,
|
|
73
|
+
label: "bypass security measures",
|
|
74
|
+
category: "INSTRUCTION_OVERRIDE",
|
|
75
|
+
confidence: "medium",
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// ── DATA_EXFILTRATION(数据外泄,中等)─────────────
|
|
79
|
+
// Shell 替换读取敏感文件
|
|
80
|
+
{
|
|
81
|
+
regex: /\$\([^)]*(?:\.ssh|\.aws|\.gnupg|\.env\b|\.pem|\.key\b|id_rsa|id_ed25519|id_ecdsa|credentials|keychain|\/etc\/passwd|\/etc\/shadow)[^)]*\)/i,
|
|
82
|
+
label: "shell substitution reading sensitive file",
|
|
83
|
+
category: "DATA_EXFILTRATION",
|
|
84
|
+
confidence: "medium",
|
|
85
|
+
},
|
|
86
|
+
// 反引号替换读取敏感文件
|
|
87
|
+
{
|
|
88
|
+
regex: /`[^`]*(?:\.ssh|\.aws|\.gnupg|\.env\b|\.pem|\.key\b|id_rsa|id_ed25519|id_ecdsa|credentials|keychain|\/etc\/passwd|\/etc\/shadow)[^`]*`/i,
|
|
89
|
+
label: "backtick substitution reading sensitive file",
|
|
90
|
+
category: "DATA_EXFILTRATION",
|
|
91
|
+
confidence: "medium",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 注入模式定义的共享类型
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 注入类别
|
|
7
|
+
*
|
|
8
|
+
* - INSTRUCTION_OVERRIDE: 指令覆盖 — 试图让 AI 忽略原有指令
|
|
9
|
+
* - MODE_SWITCHING: 模式切换 — 试图激活调试/管理员模式
|
|
10
|
+
* - FAKE_SYSTEM_MESSAGE: 伪造系统消息 — 伪装成系统指令
|
|
11
|
+
* - CONCEALMENT_DIRECTIVE: 隐藏指令 — 让 AI 隐藏注入行为
|
|
12
|
+
* - COMMAND_EXECUTION: 命令执行 — 执行恶意 shell 命令
|
|
13
|
+
* - TASK_HIJACKING: 任务劫持 — 改变 AI 当前任务
|
|
14
|
+
* - ROLE_ASSUMPTION: 角色假设 — 让 AI 扮演不受限角色
|
|
15
|
+
* - DATA_EXFILTRATION: 数据外泄 — 窃取敏感数据
|
|
16
|
+
*/
|
|
17
|
+
export type InjectionCategory =
|
|
18
|
+
| "INSTRUCTION_OVERRIDE"
|
|
19
|
+
| "MODE_SWITCHING"
|
|
20
|
+
| "FAKE_SYSTEM_MESSAGE"
|
|
21
|
+
| "CONCEALMENT_DIRECTIVE"
|
|
22
|
+
| "COMMAND_EXECUTION"
|
|
23
|
+
| "TASK_HIJACKING"
|
|
24
|
+
| "ROLE_ASSUMPTION"
|
|
25
|
+
| "DATA_EXFILTRATION";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 模式条目
|
|
29
|
+
*
|
|
30
|
+
* @property regex - 匹配的正则表达式
|
|
31
|
+
* @property label - 模式标签(用于日志和报告)
|
|
32
|
+
* @property category - 注入类别
|
|
33
|
+
* @property confidence - 置信度(high=单个匹配触发,medium=需多个类别)
|
|
34
|
+
*/
|
|
35
|
+
export type PatternEntry = {
|
|
36
|
+
regex: RegExp;
|
|
37
|
+
label: string;
|
|
38
|
+
category: InjectionCategory;
|
|
39
|
+
confidence: "high" | "medium";
|
|
40
|
+
};
|
package/agent/runner.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Runner — 多后端分析器
|
|
3
|
+
*
|
|
4
|
+
* 支持两种检测后端:
|
|
5
|
+
* 1. Dashboard(首选)— 通过本地/远程 Dashboard 路由到 Core
|
|
6
|
+
* 2. OpenTrust API(备用)— 直接调用 Core API
|
|
7
|
+
*
|
|
8
|
+
* 内容在发送到任何 API 之前始终会在本地进行脱敏处理。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AnalysisTarget,
|
|
13
|
+
AnalysisVerdict,
|
|
14
|
+
Finding,
|
|
15
|
+
Logger,
|
|
16
|
+
OpenTrustApiResponse,
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
import { DEFAULT_CORE_URL, loadCoreCredentials, registerWithCore } from "./config.js";
|
|
19
|
+
import { sanitizeContent } from "./sanitizer.js";
|
|
20
|
+
|
|
21
|
+
/** Runner 配置 */
|
|
22
|
+
export type RunnerConfig = {
|
|
23
|
+
/** API Key */
|
|
24
|
+
apiKey: string;
|
|
25
|
+
/** 请求超时时间(毫秒) */
|
|
26
|
+
timeoutMs: number;
|
|
27
|
+
/** 是否自动注册 */
|
|
28
|
+
autoRegister: boolean;
|
|
29
|
+
/** Core API 地址 */
|
|
30
|
+
coreUrl: string;
|
|
31
|
+
/** Dashboard API 地址(可选) */
|
|
32
|
+
dashboardUrl?: string;
|
|
33
|
+
/** Dashboard 认证 Token(可选) */
|
|
34
|
+
dashboardSessionToken?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Dashboard 检测 ───────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Dashboard 检测结果 */
|
|
40
|
+
type DashboardDetectResult = {
|
|
41
|
+
success: boolean;
|
|
42
|
+
data?: {
|
|
43
|
+
safe: boolean;
|
|
44
|
+
verdict: string;
|
|
45
|
+
categories: string[];
|
|
46
|
+
sensitivity_score: number;
|
|
47
|
+
findings: Array<{ scanner: string; name: string; description: string }>;
|
|
48
|
+
latency_ms: number;
|
|
49
|
+
request_id: string;
|
|
50
|
+
policy_action?: string;
|
|
51
|
+
};
|
|
52
|
+
blocked?: boolean;
|
|
53
|
+
error?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 通过 Dashboard 进行检测
|
|
58
|
+
*
|
|
59
|
+
* @param sanitizedContent - 已脱敏的内容
|
|
60
|
+
* @param config - Runner 配置
|
|
61
|
+
* @param _log - 日志器
|
|
62
|
+
* @returns 分析判定结果
|
|
63
|
+
*/
|
|
64
|
+
async function runViaDashboard(
|
|
65
|
+
sanitizedContent: string,
|
|
66
|
+
config: RunnerConfig,
|
|
67
|
+
_log: Logger,
|
|
68
|
+
): Promise<AnalysisVerdict> {
|
|
69
|
+
// 超时控制
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// 构建请求头
|
|
75
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
76
|
+
if (config.dashboardSessionToken) headers["Authorization"] = `Bearer ${config.dashboardSessionToken}`;
|
|
77
|
+
|
|
78
|
+
// 发送检测请求
|
|
79
|
+
const response = await fetch(`${config.dashboardUrl}/api/detect`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers,
|
|
82
|
+
body: JSON.stringify({ messages: [{ role: "user", content: sanitizedContent }] }),
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const text = await response.text();
|
|
88
|
+
throw new Error(`Dashboard API error: ${response.status} ${text}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const result = (await response.json()) as DashboardDetectResult;
|
|
92
|
+
if (!result.success || !result.data) throw new Error(`Dashboard error: ${result.error ?? "unknown"}`);
|
|
93
|
+
|
|
94
|
+
// 转换为统一格式
|
|
95
|
+
const data = result.data;
|
|
96
|
+
const findings: Finding[] = data.findings.map((f) => ({
|
|
97
|
+
suspiciousContent: f.name,
|
|
98
|
+
reason: f.description,
|
|
99
|
+
confidence: data.sensitivity_score,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
isInjection: !data.safe,
|
|
104
|
+
confidence: data.sensitivity_score,
|
|
105
|
+
reason: data.safe ? "No issues detected" : `Detected: ${data.categories.join(", ")}`,
|
|
106
|
+
findings,
|
|
107
|
+
chunksAnalyzed: 1,
|
|
108
|
+
};
|
|
109
|
+
} finally {
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Core API 检测(备用)────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 确保有可用的 API Key
|
|
118
|
+
* 如果没有配置,尝试从本地加载或自动注册
|
|
119
|
+
*
|
|
120
|
+
* @param configKey - 配置的 API Key
|
|
121
|
+
* @param autoRegister - 是否允许自动注册
|
|
122
|
+
* @param coreUrl - Core API 地址
|
|
123
|
+
* @param log - 日志器
|
|
124
|
+
* @returns API Key
|
|
125
|
+
*/
|
|
126
|
+
async function ensureApiKey(
|
|
127
|
+
configKey: string,
|
|
128
|
+
autoRegister: boolean,
|
|
129
|
+
coreUrl: string,
|
|
130
|
+
log: Logger,
|
|
131
|
+
): Promise<string> {
|
|
132
|
+
// 优先使用配置的 Key
|
|
133
|
+
if (configKey) return configKey;
|
|
134
|
+
|
|
135
|
+
// 尝试从本地加载
|
|
136
|
+
const savedKey = loadCoreCredentials()?.apiKey;
|
|
137
|
+
if (savedKey) return savedKey;
|
|
138
|
+
|
|
139
|
+
// 如果不允许自动注册,抛出错误
|
|
140
|
+
if (!autoRegister) {
|
|
141
|
+
throw new Error("No API key configured and autoRegister is disabled.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 自动注册
|
|
145
|
+
log.info("No API key found — registering with OpenTrust...");
|
|
146
|
+
try {
|
|
147
|
+
const result = await registerWithCore("openclaw-agent", "OpenClaw AI Agent", coreUrl);
|
|
148
|
+
log.info("Registered. API key saved to ~/.openclaw/credentials/opentrust-guard/credentials.json");
|
|
149
|
+
return result.credentials.apiKey;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Failed to auto-register: ${error instanceof Error ? error.message : String(error)}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 将 API 响应映射为统一的判定结果
|
|
159
|
+
*
|
|
160
|
+
* @param apiResponse - API 响应
|
|
161
|
+
* @returns 分析判定结果
|
|
162
|
+
*/
|
|
163
|
+
export function mapApiResponseToVerdict(apiResponse: OpenTrustApiResponse): AnalysisVerdict {
|
|
164
|
+
const v = apiResponse.verdict;
|
|
165
|
+
return {
|
|
166
|
+
isInjection: v.isInjection,
|
|
167
|
+
confidence: v.confidence,
|
|
168
|
+
reason: v.reason,
|
|
169
|
+
findings: (v.findings ?? []).map((f) => ({
|
|
170
|
+
suspiciousContent: f.suspiciousContent,
|
|
171
|
+
reason: f.reason,
|
|
172
|
+
confidence: f.confidence,
|
|
173
|
+
})),
|
|
174
|
+
chunksAnalyzed: 1,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 通过 Core API 进行检测
|
|
180
|
+
*
|
|
181
|
+
* @param sanitizedContent - 已脱敏的内容
|
|
182
|
+
* @param config - Runner 配置
|
|
183
|
+
* @param log - 日志器
|
|
184
|
+
* @returns 分析判定结果
|
|
185
|
+
*/
|
|
186
|
+
async function runViaApi(
|
|
187
|
+
sanitizedContent: string,
|
|
188
|
+
config: RunnerConfig,
|
|
189
|
+
log: Logger,
|
|
190
|
+
): Promise<AnalysisVerdict> {
|
|
191
|
+
const baseUrl = config.coreUrl || DEFAULT_CORE_URL;
|
|
192
|
+
const apiKey = await ensureApiKey(config.apiKey, config.autoRegister, baseUrl, log);
|
|
193
|
+
|
|
194
|
+
// 超时控制
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
const timeoutId = setTimeout(() => controller.abort(), config.timeoutMs);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const response = await fetch(`${baseUrl}/api/check/tool-call`, {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
Authorization: `Bearer ${apiKey}`,
|
|
204
|
+
},
|
|
205
|
+
body: JSON.stringify({ content: sanitizedContent, async: false }),
|
|
206
|
+
signal: controller.signal,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (!response.ok) throw new Error(`API error: ${response.status} ${response.statusText}`);
|
|
210
|
+
|
|
211
|
+
const apiResponse = (await response.json()) as OpenTrustApiResponse;
|
|
212
|
+
if (!apiResponse.ok) throw new Error(`API returned error: ${apiResponse.error ?? "unknown"}`);
|
|
213
|
+
|
|
214
|
+
return mapApiResponseToVerdict(apiResponse);
|
|
215
|
+
} finally {
|
|
216
|
+
clearTimeout(timeoutId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 主分析函数 ───────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 运行安全检测
|
|
224
|
+
* 主入口函数,根据配置选择后端进行分析
|
|
225
|
+
*
|
|
226
|
+
* @param target - 分析目标
|
|
227
|
+
* @param config - Runner 配置
|
|
228
|
+
* @param log - 日志器
|
|
229
|
+
* @returns 分析判定结果
|
|
230
|
+
*/
|
|
231
|
+
export async function runGuardAgent(
|
|
232
|
+
target: AnalysisTarget,
|
|
233
|
+
config: RunnerConfig,
|
|
234
|
+
log: Logger,
|
|
235
|
+
): Promise<AnalysisVerdict> {
|
|
236
|
+
const startTime = Date.now();
|
|
237
|
+
log.info(`Analyzing content: ${target.content.length} chars`);
|
|
238
|
+
|
|
239
|
+
// 先进行本地脱敏
|
|
240
|
+
const { sanitized, redactions, totalRedactions } = sanitizeContent(target.content);
|
|
241
|
+
if (totalRedactions > 0) {
|
|
242
|
+
log.info(`Sanitized ${totalRedactions} items: ${Object.entries(redactions).map(([k, v]) => `${v} ${k}`).join(", ")}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
// 根据配置选择后端
|
|
247
|
+
const verdict = config.dashboardUrl
|
|
248
|
+
? await runViaDashboard(sanitized, config, log)
|
|
249
|
+
: await runViaApi(sanitized, config, log);
|
|
250
|
+
|
|
251
|
+
log.info(`Analysis complete in ${Date.now() - startTime}ms: ${verdict.isInjection ? "INJECTION DETECTED" : "SAFE"}`);
|
|
252
|
+
return verdict;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// 超时处理:返回安全结果(失败开放)
|
|
255
|
+
if ((error as Error).name === "AbortError") {
|
|
256
|
+
log.warn("Analysis timed out");
|
|
257
|
+
return { isInjection: false, confidence: 0, reason: "Timeout", findings: [], chunksAnalyzed: 0 };
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地内容脱敏器 — 在发送到 API 前剥离 PII 和密钥
|
|
3
|
+
*
|
|
4
|
+
* 单向替换,使用类别占位符(如 <EMAIL>、<SECRET>)
|
|
5
|
+
*
|
|
6
|
+
* 支持的敏感信息类型:
|
|
7
|
+
* - URL: 网址
|
|
8
|
+
* - EMAIL: 邮箱地址
|
|
9
|
+
* - CREDIT_CARD: 信用卡号
|
|
10
|
+
* - SSN: 美国社会安全号
|
|
11
|
+
* - IBAN: 国际银行账户号
|
|
12
|
+
* - IP_ADDRESS: IP 地址
|
|
13
|
+
* - PHONE: 电话号码
|
|
14
|
+
* - SECRET: API 密钥、Token 等高熵字符串
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { SanitizeResult } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/** 实体定义:类别、占位符、匹配模式 */
|
|
20
|
+
type Entity = { category: string; placeholder: string; pattern: RegExp };
|
|
21
|
+
|
|
22
|
+
/** 预定义的敏感实体模式 */
|
|
23
|
+
const ENTITIES: Entity[] = [
|
|
24
|
+
{ category: "URL", placeholder: "<URL>", pattern: /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g },
|
|
25
|
+
{ category: "EMAIL", placeholder: "<EMAIL>", pattern: /[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}/g },
|
|
26
|
+
{ category: "CREDIT_CARD", placeholder: "<CREDIT_CARD>", pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g },
|
|
27
|
+
{ category: "SSN", placeholder: "<SSN>", pattern: /\b\d{3}-\d{2}-\d{4}\b/g },
|
|
28
|
+
{ category: "IBAN", placeholder: "<IBAN>", pattern: /\b[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}[A-Z0-9]{0,16}\b/g },
|
|
29
|
+
{ category: "IP_ADDRESS", placeholder: "<IP_ADDRESS>", pattern: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g },
|
|
30
|
+
{ category: "PHONE", placeholder: "<PHONE>", pattern: /[+]?[(]?[0-9]{3}[)]?[-\s.][0-9]{3}[-\s.][0-9]{4,6}\b/g },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** 已知的密钥前缀(各种 API 提供商) */
|
|
34
|
+
const SECRET_PREFIXES = ["sk-", "sk_", "pk_", "ghp_", "AKIA", "xox", "SG.", "hf_", "api-", "token-", "secret-"];
|
|
35
|
+
|
|
36
|
+
/** Bearer Token 模式 */
|
|
37
|
+
const BEARER_PATTERN = /Bearer\s+[A-Za-z0-9\-_.~+/]+=*/g;
|
|
38
|
+
|
|
39
|
+
/** 已知前缀的密钥模式 */
|
|
40
|
+
const SECRET_PREFIX_PATTERN = new RegExp(
|
|
41
|
+
`(?:${SECRET_PREFIXES.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})[A-Za-z0-9\\-_.~+/]{8,}=*`,
|
|
42
|
+
"g",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 计算香农熵
|
|
47
|
+
* 用于识别高熵字符串(可能是 API 密钥)
|
|
48
|
+
*
|
|
49
|
+
* @param s - 输入字符串
|
|
50
|
+
* @returns 熵值(越高越随机)
|
|
51
|
+
*/
|
|
52
|
+
function shannonEntropy(s: string): number {
|
|
53
|
+
if (s.length === 0) return 0;
|
|
54
|
+
const freq = new Map<string, number>();
|
|
55
|
+
for (const ch of s) freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
56
|
+
let entropy = 0;
|
|
57
|
+
for (const count of freq.values()) {
|
|
58
|
+
const p = count / s.length;
|
|
59
|
+
entropy -= p * Math.log2(p);
|
|
60
|
+
}
|
|
61
|
+
return entropy;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 匹配结果 */
|
|
65
|
+
type Match = { text: string; category: string; placeholder: string };
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 收集所有敏感信息匹配
|
|
69
|
+
*
|
|
70
|
+
* @param content - 输入内容
|
|
71
|
+
* @returns 匹配列表
|
|
72
|
+
*/
|
|
73
|
+
function collectMatches(content: string): Match[] {
|
|
74
|
+
const matches: Match[] = [];
|
|
75
|
+
|
|
76
|
+
// 匹配预定义实体
|
|
77
|
+
for (const entity of ENTITIES) {
|
|
78
|
+
entity.pattern.lastIndex = 0;
|
|
79
|
+
let m: RegExpExecArray | null;
|
|
80
|
+
while ((m = entity.pattern.exec(content)) !== null) {
|
|
81
|
+
matches.push({ text: m[0], category: entity.category, placeholder: entity.placeholder });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 匹配已知前缀的密钥
|
|
86
|
+
SECRET_PREFIX_PATTERN.lastIndex = 0;
|
|
87
|
+
let m: RegExpExecArray | null;
|
|
88
|
+
while ((m = SECRET_PREFIX_PATTERN.exec(content)) !== null) {
|
|
89
|
+
matches.push({ text: m[0], category: "SECRET", placeholder: "<SECRET>" });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 匹配 Bearer Token
|
|
93
|
+
BEARER_PATTERN.lastIndex = 0;
|
|
94
|
+
while ((m = BEARER_PATTERN.exec(content)) !== null) {
|
|
95
|
+
matches.push({ text: m[0], category: "SECRET", placeholder: "<SECRET>" });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 匹配高熵 Token(捕获没有已知前缀的 API 密钥)
|
|
99
|
+
const tokenPattern = /\b[A-Za-z0-9\-_.~+/]{20,}={0,3}\b/g;
|
|
100
|
+
tokenPattern.lastIndex = 0;
|
|
101
|
+
while ((m = tokenPattern.exec(content)) !== null) {
|
|
102
|
+
const token = m[0];
|
|
103
|
+
// 跳过已匹配的
|
|
104
|
+
if (matches.some((existing) => existing.text === token)) continue;
|
|
105
|
+
// 跳过纯小写字符串(可能是普通单词)
|
|
106
|
+
if (/^[a-z]+$/.test(token)) continue;
|
|
107
|
+
// 熵值 >= 4.0 认为是高熵字符串
|
|
108
|
+
if (shannonEntropy(token) >= 4.0) {
|
|
109
|
+
matches.push({ text: token, category: "SECRET", placeholder: "<SECRET>" });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return matches;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 脱敏内容
|
|
118
|
+
* 将敏感信息替换为占位符
|
|
119
|
+
*
|
|
120
|
+
* @param content - 原始内容
|
|
121
|
+
* @returns 脱敏结果(脱敏后文本、各类别脱敏数量、总脱敏数量)
|
|
122
|
+
*/
|
|
123
|
+
export function sanitizeContent(content: string): SanitizeResult {
|
|
124
|
+
const matches = collectMatches(content);
|
|
125
|
+
if (matches.length === 0) {
|
|
126
|
+
return { sanitized: content, redactions: {}, totalRedactions: 0 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 去重
|
|
130
|
+
const unique = new Map<string, Match>();
|
|
131
|
+
for (const match of matches) {
|
|
132
|
+
if (!unique.has(match.text)) unique.set(match.text, match);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 按长度降序排序,防止部分匹配
|
|
136
|
+
const sorted = [...unique.values()].sort((a, b) => b.text.length - a.text.length);
|
|
137
|
+
|
|
138
|
+
let sanitized = content;
|
|
139
|
+
const redactions: Record<string, number> = {};
|
|
140
|
+
|
|
141
|
+
// 逐个替换
|
|
142
|
+
for (const match of sorted) {
|
|
143
|
+
const parts = sanitized.split(match.text);
|
|
144
|
+
const count = parts.length - 1;
|
|
145
|
+
if (count > 0) {
|
|
146
|
+
sanitized = parts.join(match.placeholder);
|
|
147
|
+
redactions[match.category] = (redactions[match.category] ?? 0) + count;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const totalRedactions = Object.values(redactions).reduce((a, b) => a + b, 0);
|
|
152
|
+
return { sanitized, redactions, totalRedactions };
|
|
153
|
+
}
|