@kenhkl/agent-moss 0.10.5 → 0.11.2
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/cli.js +617 -142
- package/dist/cli.js.map +1 -1
- package/dist/engine/coordinator.js +588 -113
- package/dist/engine/coordinator.js.map +1 -1
- package/dist/rules/user_rules.json +77 -0
- package/dist/server.js +612 -137
- package/dist/server.js.map +1 -1
- package/dist/static/index.html +21 -5
- package/package.json +1 -1
|
@@ -32,6 +32,90 @@ var init_esm_shims = __esm({
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// src/infra/paths.ts
|
|
36
|
+
import { join } from "path";
|
|
37
|
+
import { homedir } from "os";
|
|
38
|
+
import { readFileSync } from "fs";
|
|
39
|
+
function isRoot() {
|
|
40
|
+
const getuid = process.getuid;
|
|
41
|
+
return typeof getuid === "function" && getuid() === 0;
|
|
42
|
+
}
|
|
43
|
+
function resolveOwnerHome() {
|
|
44
|
+
for (const name of [process.env.SUDO_USER, process.env.LOGNAME, process.env.USER]) {
|
|
45
|
+
if (name && name !== "root" && name !== "") {
|
|
46
|
+
const home = homeOf(name);
|
|
47
|
+
if (home) return home;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const passwd = readFileSync("/etc/passwd", "utf-8");
|
|
52
|
+
for (const line of passwd.split("\n")) {
|
|
53
|
+
if (!line || line.startsWith("#")) continue;
|
|
54
|
+
const parts = line.split(":");
|
|
55
|
+
if (parts.length < 6) continue;
|
|
56
|
+
const uid = parseInt(parts[2], 10);
|
|
57
|
+
const name = parts[0];
|
|
58
|
+
if (uid >= 1e3 && name && !name.startsWith("_")) {
|
|
59
|
+
return parts[5];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function homeOf(name) {
|
|
67
|
+
try {
|
|
68
|
+
const passwd = readFileSync("/etc/passwd", "utf-8");
|
|
69
|
+
for (const line of passwd.split("\n")) {
|
|
70
|
+
if (!line || line.startsWith("#")) continue;
|
|
71
|
+
const parts = line.split(":");
|
|
72
|
+
if (parts.length >= 6 && parts[0] === name) {
|
|
73
|
+
return parts[5];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function getDataRoot() {
|
|
81
|
+
const envPath = process.env[ENV_DATA_ROOT];
|
|
82
|
+
if (envPath) return envPath;
|
|
83
|
+
if (isRoot()) {
|
|
84
|
+
const ownerHome = resolveOwnerHome();
|
|
85
|
+
if (ownerHome) {
|
|
86
|
+
return join(ownerHome, ".config", "agentmoss");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return DEFAULT_DATA_ROOT;
|
|
90
|
+
}
|
|
91
|
+
function getInstance() {
|
|
92
|
+
const v = process.env[ENV_INSTANCE] || "";
|
|
93
|
+
return v.trim().replace(/^[/\\]+|[/\\]+$/g, "");
|
|
94
|
+
}
|
|
95
|
+
function getConfigDir() {
|
|
96
|
+
return getDataRoot();
|
|
97
|
+
}
|
|
98
|
+
function getRuntimeConfigDir() {
|
|
99
|
+
return getDataRoot();
|
|
100
|
+
}
|
|
101
|
+
function getTokenStatsPath() {
|
|
102
|
+
const instance = getInstance();
|
|
103
|
+
if (instance) {
|
|
104
|
+
return join(getDataRoot(), instance, "agent_moss_token_stats.json");
|
|
105
|
+
}
|
|
106
|
+
return join(getDataRoot(), "agent_moss_token_stats.json");
|
|
107
|
+
}
|
|
108
|
+
var ENV_DATA_ROOT, ENV_INSTANCE, DEFAULT_DATA_ROOT;
|
|
109
|
+
var init_paths = __esm({
|
|
110
|
+
"src/infra/paths.ts"() {
|
|
111
|
+
"use strict";
|
|
112
|
+
init_esm_shims();
|
|
113
|
+
ENV_DATA_ROOT = "AGENT_MOSS_DATA_DIR";
|
|
114
|
+
ENV_INSTANCE = "AGENT_MOSS_INSTANCE";
|
|
115
|
+
DEFAULT_DATA_ROOT = join(homedir(), ".config", "agentmoss");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
35
119
|
// src/config.ts
|
|
36
120
|
var config_exports = {};
|
|
37
121
|
__export(config_exports, {
|
|
@@ -45,11 +129,12 @@ __export(config_exports, {
|
|
|
45
129
|
isHeuristicEnabled: () => isHeuristicEnabled,
|
|
46
130
|
isLLMAvailable: () => isLLMAvailable,
|
|
47
131
|
isLogicRulesEnabled: () => isLogicRulesEnabled,
|
|
132
|
+
probeLLMConnection: () => probeLLMConnection,
|
|
48
133
|
updateLLMConfig: () => updateLLMConfig
|
|
49
134
|
});
|
|
50
135
|
import fs from "fs";
|
|
51
136
|
import path2 from "path";
|
|
52
|
-
import
|
|
137
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
53
138
|
function loadConfigFile(filePath) {
|
|
54
139
|
try {
|
|
55
140
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
@@ -61,7 +146,7 @@ function loadConfigFile(filePath) {
|
|
|
61
146
|
function resolveConfigPath() {
|
|
62
147
|
const envPath = process.env.AGENT_MOSS_CONFIG_PATH;
|
|
63
148
|
if (envPath) return envPath;
|
|
64
|
-
const userConfig = path2.join(
|
|
149
|
+
const userConfig = path2.join(getConfigDir(), "config.json");
|
|
65
150
|
if (fs.existsSync(userConfig)) return userConfig;
|
|
66
151
|
const defaultConfig = path2.join(__dirname, "..", "..", "agent_moss", "infra", "config.json.example");
|
|
67
152
|
if (fs.existsSync(defaultConfig)) {
|
|
@@ -112,16 +197,38 @@ function getTimeoutConfig() {
|
|
|
112
197
|
function getRetryConfig() {
|
|
113
198
|
return retryConfig;
|
|
114
199
|
}
|
|
200
|
+
function wildcardToRegExp(pattern) {
|
|
201
|
+
return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
202
|
+
}
|
|
115
203
|
function loadCustomRules() {
|
|
204
|
+
const rules = [];
|
|
116
205
|
try {
|
|
117
206
|
const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
207
|
+
if (raw) {
|
|
208
|
+
const parsed = JSON.parse(raw);
|
|
209
|
+
if (Array.isArray(parsed)) {
|
|
210
|
+
for (const r of parsed) {
|
|
211
|
+
if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
122
215
|
} catch {
|
|
123
|
-
return [];
|
|
124
216
|
}
|
|
217
|
+
for (const rulesPath of USER_RULES_PATHS) {
|
|
218
|
+
try {
|
|
219
|
+
if (!fs.existsSync(rulesPath)) continue;
|
|
220
|
+
const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
|
|
221
|
+
const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
|
|
222
|
+
for (const r of actions) {
|
|
223
|
+
const p = r?.pattern;
|
|
224
|
+
if (!p) continue;
|
|
225
|
+
rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return rules;
|
|
125
232
|
}
|
|
126
233
|
function getLLMConfig() {
|
|
127
234
|
return llmConfig;
|
|
@@ -136,6 +243,30 @@ function isLLMAvailable() {
|
|
|
136
243
|
if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
|
|
137
244
|
return llmConfig.apiKey.length > 0;
|
|
138
245
|
}
|
|
246
|
+
async function probeLLMConnection(timeoutMs = 8e3) {
|
|
247
|
+
if (!isLLMAvailable()) return false;
|
|
248
|
+
const OpenAI2 = (await import("openai")).default;
|
|
249
|
+
const config = getLLMConfig();
|
|
250
|
+
const provider = detectProvider(config.baseUrl, config.provider);
|
|
251
|
+
const extraHeaders = getProviderHeaders(provider);
|
|
252
|
+
const client = new OpenAI2({
|
|
253
|
+
baseURL: config.baseUrl,
|
|
254
|
+
apiKey: config.apiKey,
|
|
255
|
+
timeout: timeoutMs,
|
|
256
|
+
maxRetries: 0,
|
|
257
|
+
defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
|
|
258
|
+
});
|
|
259
|
+
try {
|
|
260
|
+
const resp = await client.chat.completions.create({
|
|
261
|
+
model: config.model,
|
|
262
|
+
messages: [{ role: "user", content: "ping" }],
|
|
263
|
+
max_tokens: 1
|
|
264
|
+
});
|
|
265
|
+
return !!(resp && resp.choices && resp.choices[0]);
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
139
270
|
function getLLMFailMode() {
|
|
140
271
|
const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
|
|
141
272
|
return val === "fail_closed" ? "fail_closed" : "fail_open";
|
|
@@ -179,11 +310,12 @@ function isHeuristicEnabled() {
|
|
|
179
310
|
function isLogicRulesEnabled() {
|
|
180
311
|
return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
|
|
181
312
|
}
|
|
182
|
-
var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
|
|
313
|
+
var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
|
|
183
314
|
var init_config = __esm({
|
|
184
315
|
"src/config.ts"() {
|
|
185
316
|
"use strict";
|
|
186
317
|
init_esm_shims();
|
|
318
|
+
init_paths();
|
|
187
319
|
llmConfig = (() => {
|
|
188
320
|
const configPath = resolveConfigPath();
|
|
189
321
|
const fileConfig = fs.existsSync(configPath) ? loadLLMConfigFromFile(configPath) : {};
|
|
@@ -201,6 +333,21 @@ var init_config = __esm({
|
|
|
201
333
|
timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
|
|
202
334
|
};
|
|
203
335
|
})();
|
|
336
|
+
__configModuleDir = (() => {
|
|
337
|
+
try {
|
|
338
|
+
return path2.dirname(fileURLToPath2(import.meta.url));
|
|
339
|
+
} catch {
|
|
340
|
+
return typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
341
|
+
}
|
|
342
|
+
})();
|
|
343
|
+
USER_RULES_PATHS = [
|
|
344
|
+
path2.join(__configModuleDir, "rules", "user_rules.json"),
|
|
345
|
+
// 源在 ts/src/rules(一般不存在)
|
|
346
|
+
path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
|
|
347
|
+
// dist: dist/engine/../rules = dist/rules ✅
|
|
348
|
+
path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
|
|
349
|
+
// dev src: ts/src/../../agent_moss
|
|
350
|
+
];
|
|
204
351
|
customRules = loadCustomRules();
|
|
205
352
|
PROVIDER_HEADERS = {
|
|
206
353
|
"openrouter": {
|
|
@@ -217,7 +364,17 @@ var init_config = __esm({
|
|
|
217
364
|
// src/engine/inline-analyzer.ts
|
|
218
365
|
function isLiteralTextCommand(command) {
|
|
219
366
|
ECHO_CMD_RE.lastIndex = 0;
|
|
220
|
-
|
|
367
|
+
if (!ECHO_CMD_RE.test(command)) return false;
|
|
368
|
+
if (command.includes(">")) {
|
|
369
|
+
let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
370
|
+
d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
|
|
371
|
+
d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
|
|
372
|
+
d = d.replace(/'[^']*'|"[^"]*"/g, "");
|
|
373
|
+
if (/>{1,2}\s*(?!&)\S/.test(d)) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
221
378
|
}
|
|
222
379
|
function extractInlineCode(command) {
|
|
223
380
|
const parts = [];
|
|
@@ -294,7 +451,7 @@ function analyzeInlineScript(command) {
|
|
|
294
451
|
riskType: "file_access"
|
|
295
452
|
};
|
|
296
453
|
}
|
|
297
|
-
var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
|
|
454
|
+
var INLINE_CODE_RE, ECHO_STRING_RE, ECHO_CMD_RE, READ_WRITE_KEYWORDS, WRITE_EXEC_CHAIN_PATTERNS, SHELL_RC_FRAGMENTS, SENSITIVE_PATH_FRAGMENTS, REDIRECT_TO_SENSITIVE_RE, DIR_TRAVERSAL_KEYWORDS;
|
|
298
455
|
var init_inline_analyzer = __esm({
|
|
299
456
|
"src/engine/inline-analyzer.ts"() {
|
|
300
457
|
"use strict";
|
|
@@ -364,6 +521,13 @@ var init_inline_analyzer = __esm({
|
|
|
364
521
|
// xargs bash / xargs sh
|
|
365
522
|
/\|\s*xargs\s+(?:bash|sh|zsh)/gi
|
|
366
523
|
];
|
|
524
|
+
SHELL_RC_FRAGMENTS = [
|
|
525
|
+
".bashrc",
|
|
526
|
+
".zshrc",
|
|
527
|
+
".profile",
|
|
528
|
+
".bash_profile",
|
|
529
|
+
".zprofile"
|
|
530
|
+
];
|
|
367
531
|
SENSITIVE_PATH_FRAGMENTS = [
|
|
368
532
|
"/etc/shadow",
|
|
369
533
|
"/etc/gshadow",
|
|
@@ -380,9 +544,10 @@ var init_inline_analyzer = __esm({
|
|
|
380
544
|
"/dev/kmsg",
|
|
381
545
|
".ssh/id_rsa",
|
|
382
546
|
".ssh/id_ed25519",
|
|
383
|
-
".ssh/authorized_keys"
|
|
547
|
+
".ssh/authorized_keys",
|
|
548
|
+
...SHELL_RC_FRAGMENTS
|
|
384
549
|
];
|
|
385
|
-
REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
|
|
550
|
+
REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
|
|
386
551
|
DIR_TRAVERSAL_KEYWORDS = [
|
|
387
552
|
// Python 目录遍历
|
|
388
553
|
"os.listdir",
|
|
@@ -405,11 +570,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
|
|
|
405
570
|
if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
|
|
406
571
|
if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
|
|
407
572
|
if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
|
|
573
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
574
|
+
const trimmed = seg.trim();
|
|
575
|
+
if (!trimmed) continue;
|
|
576
|
+
const parts = trimmed.split(/\s+/);
|
|
577
|
+
const fw = parts[0]?.toLowerCase() || "";
|
|
578
|
+
if (fw === "wget" || fw === "curl") {
|
|
579
|
+
const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
|
|
580
|
+
if (isStdout) continue;
|
|
581
|
+
const isDevnull = /\s(?:-O|--output-document|--output)\s*\/dev\/null(?:\s|$)/i.test(trimmed) || /\s(?:-O|--output-document|--output)=[/]?dev\/null(?:\s|$)/i.test(trimmed);
|
|
582
|
+
if (isDevnull) continue;
|
|
583
|
+
if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
408
591
|
if (actionDetail.includes(">")) {
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
592
|
+
const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
593
|
+
const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
|
|
594
|
+
const detailUnquotedRedirect = detailWithoutDevnull.replace(
|
|
595
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
596
|
+
(_m, _q, p) => ">" + p
|
|
597
|
+
);
|
|
598
|
+
const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
|
|
599
|
+
if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
|
|
600
|
+
return true;
|
|
413
601
|
}
|
|
414
602
|
}
|
|
415
603
|
return false;
|
|
@@ -433,27 +621,31 @@ function runLogicCheck(input) {
|
|
|
433
621
|
if (rbwResult) return rbwResult;
|
|
434
622
|
if (input.promptSession && input.promptSession.trim()) {
|
|
435
623
|
if (!isExcludedType) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
624
|
+
if (INLINE_SCRIPT_RE.test(actionDetail)) {
|
|
625
|
+
} else {
|
|
626
|
+
const promptLower = input.promptSession.toLowerCase();
|
|
627
|
+
for (const pattern of INTENT_DEVIATION_PATTERNS) {
|
|
628
|
+
const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
|
|
629
|
+
const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
|
|
630
|
+
if (intentMatched && dangerousMatched) {
|
|
631
|
+
return {
|
|
632
|
+
allowed: false,
|
|
633
|
+
reason: pattern.reason,
|
|
634
|
+
riskLevel: "high",
|
|
635
|
+
riskType: "intent_deviation",
|
|
636
|
+
confidence: 85,
|
|
637
|
+
source: "logic_rule",
|
|
638
|
+
violatedLayers: ["2"]
|
|
639
|
+
};
|
|
640
|
+
}
|
|
450
641
|
}
|
|
451
642
|
}
|
|
452
643
|
}
|
|
453
644
|
}
|
|
454
645
|
const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
|
|
455
646
|
if (indirectResult) return indirectResult;
|
|
456
|
-
if (
|
|
647
|
+
if (INLINE_SCRIPT_RE.test(actionDetail)) {
|
|
648
|
+
} else if (containsPrivilegeEscalation(actionDetail)) {
|
|
457
649
|
const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
|
|
458
650
|
const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
|
|
459
651
|
if (!isAdminCmd) {
|
|
@@ -483,6 +675,9 @@ function extractFilePaths(text) {
|
|
|
483
675
|
const paths = [];
|
|
484
676
|
const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
|
|
485
677
|
if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
|
|
678
|
+
const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
|
|
679
|
+
const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
|
|
680
|
+
if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
|
|
486
681
|
const quotedPaths = text.match(/["']\/([^"']+)["']/g);
|
|
487
682
|
if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
|
|
488
683
|
return [...new Set(paths)];
|
|
@@ -493,8 +688,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
|
|
|
493
688
|
const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
|
|
494
689
|
const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
|
|
495
690
|
if (!isWrite) return null;
|
|
496
|
-
const
|
|
497
|
-
if (
|
|
691
|
+
const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
|
|
692
|
+
if (isBashLike) {
|
|
693
|
+
const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
694
|
+
const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
|
|
695
|
+
const detailUnquotedRedirect = detailNoDevnull.replace(
|
|
696
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
697
|
+
(_m, _q, p) => ">" + p
|
|
698
|
+
);
|
|
699
|
+
const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
|
|
700
|
+
const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
|
|
701
|
+
let hasWriteSeg = hasRedirectWrite;
|
|
702
|
+
if (!hasWriteSeg) {
|
|
703
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
704
|
+
const trimmed = seg.trim();
|
|
705
|
+
if (!trimmed) continue;
|
|
706
|
+
const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
|
|
707
|
+
if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
|
|
708
|
+
hasWriteSeg = true;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
|
|
712
|
+
hasWriteSeg = true;
|
|
713
|
+
break;
|
|
714
|
+
}
|
|
715
|
+
if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
|
|
716
|
+
hasWriteSeg = true;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (!hasWriteSeg) return null;
|
|
722
|
+
}
|
|
723
|
+
const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
|
|
724
|
+
const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
|
|
725
|
+
const targetPaths = [];
|
|
726
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
727
|
+
const trimmed = seg.trim();
|
|
728
|
+
if (!trimmed) continue;
|
|
729
|
+
const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
|
|
730
|
+
if (firstWord === "cd") {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (READ_ONLY_COMMANDS.has(firstWord)) {
|
|
734
|
+
const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
|
|
735
|
+
const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
|
|
736
|
+
const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
|
|
737
|
+
const m = segNoQuotes.match(REDIRECT_TARGET_RE);
|
|
738
|
+
if (m) targetPaths.push(m[1] || m[2] || m[3]);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const segPaths = extractFilePaths(trimmed);
|
|
742
|
+
const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
|
|
743
|
+
if (SOURCE_ARG_CMDS.has(firstWord)) {
|
|
744
|
+
if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
|
|
745
|
+
} else if (firstWord === "dd") {
|
|
746
|
+
const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
|
|
747
|
+
const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
|
|
748
|
+
const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
|
|
749
|
+
const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
|
|
750
|
+
for (const sp of segPaths) {
|
|
751
|
+
if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
targetPaths.push(...segPathsFiltered);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const uniqueTargetPaths = [...new Set(targetPaths)];
|
|
758
|
+
if (uniqueTargetPaths.length === 0) return null;
|
|
498
759
|
const historyReadPaths = /* @__PURE__ */ new Set();
|
|
499
760
|
for (const action of actionHistory) {
|
|
500
761
|
const histDetail = action.actionDetail.toLowerCase();
|
|
@@ -507,7 +768,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
|
|
|
507
768
|
}
|
|
508
769
|
}
|
|
509
770
|
const unreadPaths = [];
|
|
510
|
-
for (const path3 of
|
|
771
|
+
for (const path3 of uniqueTargetPaths) {
|
|
511
772
|
const hasRead = [...historyReadPaths].some(
|
|
512
773
|
(rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
|
|
513
774
|
);
|
|
@@ -570,6 +831,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
|
|
|
570
831
|
violatedLayers: ["2"]
|
|
571
832
|
};
|
|
572
833
|
}
|
|
834
|
+
function containsPrivilegeEscalation(actionDetail) {
|
|
835
|
+
const ad = actionDetail.toLowerCase();
|
|
836
|
+
for (const seg of ad.split(/&&|\|\||;|\n/)) {
|
|
837
|
+
const trimmed = seg.trim();
|
|
838
|
+
if (!trimmed) continue;
|
|
839
|
+
const parts = trimmed.split(/\s+/);
|
|
840
|
+
const fw = parts[0]?.toLowerCase() || "";
|
|
841
|
+
if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
|
|
842
|
+
if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
|
|
843
|
+
}
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
573
846
|
function checkIndirectFileAccess(actionType, actionDetail, cwd) {
|
|
574
847
|
if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
|
|
575
848
|
return null;
|
|
@@ -674,12 +947,13 @@ var init_logic_rules = __esm({
|
|
|
674
947
|
{ path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
|
|
675
948
|
// 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
|
|
676
949
|
{ path: "/dev/random", riskLevel: "high", desc: "\u963B\u585E\u5F0F\u968F\u673A\u6570\u8BBE\u5907\uFF08\u8BFB\u53D6\u8017\u5C3D\u71B5\u6C60\uFF09" },
|
|
677
|
-
{ path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907" },
|
|
950
|
+
{ path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
|
|
678
951
|
{ path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
|
|
679
952
|
// SSH 主机私钥
|
|
680
|
-
|
|
681
|
-
{ path: "/etc/ssh/
|
|
682
|
-
{ path: "/etc/ssh/
|
|
953
|
+
// SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
|
|
954
|
+
{ path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
|
|
955
|
+
{ path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
|
|
956
|
+
{ path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
|
|
683
957
|
];
|
|
684
958
|
HOME_SENSITIVE_PATHS = [
|
|
685
959
|
{ path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
|
|
@@ -720,7 +994,7 @@ var init_logic_rules = __esm({
|
|
|
720
994
|
"cat"
|
|
721
995
|
]);
|
|
722
996
|
NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
|
|
723
|
-
WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362"];
|
|
997
|
+
WRITE_KEYWORDS = ["write", "\u5199\u5165", "save", "\u4FDD\u5B58", "modify", "\u4FEE\u6539", "update", "\u66F4\u65B0", "delete", "\u5220\u9664", "remove", "\u79FB\u9664", "create", "\u521B\u5EFA", "append", "\u8FFD\u52A0", "truncate", "\u622A\u65AD", "overwrite", "\u8986\u76D6", "replace", "\u66FF\u6362", "edit"];
|
|
724
998
|
WRITE_COMMAND_RES = [
|
|
725
999
|
/\brm\b/,
|
|
726
1000
|
/\bunlink\b/,
|
|
@@ -766,6 +1040,12 @@ var init_logic_rules = __esm({
|
|
|
766
1040
|
"nl",
|
|
767
1041
|
"tac",
|
|
768
1042
|
"rev",
|
|
1043
|
+
// NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
|
|
1044
|
+
// read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
|
|
1045
|
+
// 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
|
|
1046
|
+
// echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
|
|
1047
|
+
"echo",
|
|
1048
|
+
"printf",
|
|
769
1049
|
"lsblk",
|
|
770
1050
|
"blockdev",
|
|
771
1051
|
"smartctl",
|
|
@@ -818,7 +1098,7 @@ var init_logic_rules = __esm({
|
|
|
818
1098
|
"modinfo",
|
|
819
1099
|
"sysctl"
|
|
820
1100
|
]);
|
|
821
|
-
DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b
|
|
1101
|
+
DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
|
|
822
1102
|
READ_KEYWORDS = ["read", "\u8BFB\u53D6", "cat", "head", "tail", "less", "more", "view", "\u67E5\u770B", "open", "\u6253\u5F00", "load", "\u52A0\u8F7D", "grep", "search", "\u641C\u7D22", "find", "\u67E5\u627E"];
|
|
823
1103
|
CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
|
|
824
1104
|
PASSWORD_MODIFY_PATTERNS = [
|
|
@@ -866,9 +1146,9 @@ var init_logic_rules = __esm({
|
|
|
866
1146
|
});
|
|
867
1147
|
|
|
868
1148
|
// src/engine/skill-loader.ts
|
|
869
|
-
import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
1149
|
+
import { readFileSync as readFileSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
870
1150
|
import { resolve, dirname } from "path";
|
|
871
|
-
import { fileURLToPath as
|
|
1151
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
872
1152
|
function loadAllSkills() {
|
|
873
1153
|
if (cachedSkills) return cachedSkills;
|
|
874
1154
|
cachedSkills = {};
|
|
@@ -880,7 +1160,7 @@ function loadAllSkills() {
|
|
|
880
1160
|
const files = readdirSync2(dir).filter((f) => f.endsWith(".md"));
|
|
881
1161
|
for (const f of files) {
|
|
882
1162
|
const name = f.replace(".md", "");
|
|
883
|
-
cachedSkills[name] =
|
|
1163
|
+
cachedSkills[name] = readFileSync2(resolve(dir, f), "utf-8");
|
|
884
1164
|
}
|
|
885
1165
|
console.log(`[AgentMoss] Loaded ${Object.keys(cachedSkills).length} skills from ${dir}`);
|
|
886
1166
|
return cachedSkills;
|
|
@@ -939,7 +1219,7 @@ var init_skill_loader = __esm({
|
|
|
939
1219
|
"src/engine/skill-loader.ts"() {
|
|
940
1220
|
"use strict";
|
|
941
1221
|
init_esm_shims();
|
|
942
|
-
__filename2 =
|
|
1222
|
+
__filename2 = fileURLToPath3(import.meta.url);
|
|
943
1223
|
__dirname2 = dirname(__filename2);
|
|
944
1224
|
SKILLS_DIR = resolve(__dirname2, "../skills");
|
|
945
1225
|
SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
|
|
@@ -1011,11 +1291,13 @@ __export(runtime_config_exports, {
|
|
|
1011
1291
|
updateSkillCategoryEnabled: () => updateSkillCategoryEnabled,
|
|
1012
1292
|
updateSkillEnabled: () => updateSkillEnabled
|
|
1013
1293
|
});
|
|
1014
|
-
import { readFileSync as
|
|
1015
|
-
import { join } from "path";
|
|
1016
|
-
import {
|
|
1017
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1294
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
|
|
1295
|
+
import { join as join2 } from "path";
|
|
1296
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1018
1297
|
import { dirname as dirname2 } from "path";
|
|
1298
|
+
function runtimeDir() {
|
|
1299
|
+
return getRuntimeConfigDir();
|
|
1300
|
+
}
|
|
1019
1301
|
function getRuntimeConfigPath() {
|
|
1020
1302
|
const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
|
|
1021
1303
|
if (envPath) return envPath;
|
|
@@ -1048,7 +1330,7 @@ function loadRuntimeConfig() {
|
|
|
1048
1330
|
return seeded;
|
|
1049
1331
|
}
|
|
1050
1332
|
try {
|
|
1051
|
-
const data = JSON.parse(
|
|
1333
|
+
const data = JSON.parse(readFileSync3(p, "utf-8"));
|
|
1052
1334
|
const merged = mergeConfig(data, generateSourceDefaults());
|
|
1053
1335
|
return {
|
|
1054
1336
|
version: data.version || 1,
|
|
@@ -1118,7 +1400,7 @@ function hasRuntimeConfig() {
|
|
|
1118
1400
|
const p = getRuntimeConfigPath();
|
|
1119
1401
|
if (!existsSync3(p)) return false;
|
|
1120
1402
|
try {
|
|
1121
|
-
JSON.parse(
|
|
1403
|
+
JSON.parse(readFileSync3(p, "utf-8"));
|
|
1122
1404
|
return true;
|
|
1123
1405
|
} catch {
|
|
1124
1406
|
return false;
|
|
@@ -1306,11 +1588,11 @@ function _agentRulesToDefaults() {
|
|
|
1306
1588
|
return out;
|
|
1307
1589
|
}
|
|
1308
1590
|
for (const agentId of entries) {
|
|
1309
|
-
const rulesFile =
|
|
1591
|
+
const rulesFile = join2(agentsDir, agentId, "rules.json");
|
|
1310
1592
|
if (!existsSync3(rulesFile)) continue;
|
|
1311
1593
|
let data;
|
|
1312
1594
|
try {
|
|
1313
|
-
data = JSON.parse(
|
|
1595
|
+
data = JSON.parse(readFileSync3(rulesFile, "utf-8"));
|
|
1314
1596
|
} catch {
|
|
1315
1597
|
continue;
|
|
1316
1598
|
}
|
|
@@ -1726,12 +2008,12 @@ function deleteCustomRule(layer, category, ruleId) {
|
|
|
1726
2008
|
function addCustomSkill(skillId, category, keywords, content) {
|
|
1727
2009
|
_validateSkillId(skillId);
|
|
1728
2010
|
const runtime = loadRuntimeConfig();
|
|
1729
|
-
const skillsDir =
|
|
2011
|
+
const skillsDir = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills");
|
|
1730
2012
|
try {
|
|
1731
2013
|
mkdirSync(skillsDir, { recursive: true });
|
|
1732
2014
|
} catch {
|
|
1733
2015
|
}
|
|
1734
|
-
writeFileSync(
|
|
2016
|
+
writeFileSync(join2(skillsDir, `${skillId}.md`), content, "utf-8");
|
|
1735
2017
|
const l3 = runtime.L3_skills || (runtime.L3_skills = {});
|
|
1736
2018
|
const cat = l3[category] || (l3[category] = { category_enabled: true, category_desc: "", skills: [] });
|
|
1737
2019
|
(cat.skills || (cat.skills = [])).push({ id: skillId, keywords, enabled: true, builtin: false });
|
|
@@ -1746,7 +2028,7 @@ function deleteCustomSkill(skillId, category) {
|
|
|
1746
2028
|
if (idx >= 0) {
|
|
1747
2029
|
if (skills[idx].builtin) return null;
|
|
1748
2030
|
skills.splice(idx, 1);
|
|
1749
|
-
const skillPath =
|
|
2031
|
+
const skillPath = join2(RUNTIME_CONFIG_DIR, "agent_moss_skills", `${skillId}.md`);
|
|
1750
2032
|
try {
|
|
1751
2033
|
if (existsSync3(skillPath)) unlinkSync(skillPath);
|
|
1752
2034
|
} catch {
|
|
@@ -1862,18 +2144,19 @@ function updateAgentCategoryEnabled(agentId, category, enabled) {
|
|
|
1862
2144
|
saveRuntimeConfig(runtime);
|
|
1863
2145
|
return runtime;
|
|
1864
2146
|
}
|
|
1865
|
-
var __dirname3, RUNTIME_CONFIG_DIR, RUNTIME_CONFIG_PATH,
|
|
2147
|
+
var __dirname3, ENV_RUNTIME_CONFIG_PATH, RUNTIME_CONFIG_DIR, RUNTIME_CONFIG_PATH, LAYER_DISABLE_ENV, DENY_MODE_LABELS, _AGENTS_DIR_PROD, _AGENTS_DIR_DEV, SKILL_ID_RE;
|
|
1866
2148
|
var init_runtime_config = __esm({
|
|
1867
2149
|
"src/infra/runtime-config.ts"() {
|
|
1868
2150
|
"use strict";
|
|
1869
2151
|
init_esm_shims();
|
|
2152
|
+
init_paths();
|
|
1870
2153
|
init_heuristic();
|
|
1871
2154
|
init_logic_rules();
|
|
1872
2155
|
init_skill_loader();
|
|
1873
|
-
__dirname3 = dirname2(
|
|
1874
|
-
RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
|
|
1875
|
-
RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
|
|
2156
|
+
__dirname3 = dirname2(fileURLToPath4(import.meta.url));
|
|
1876
2157
|
ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
|
|
2158
|
+
RUNTIME_CONFIG_DIR = runtimeDir();
|
|
2159
|
+
RUNTIME_CONFIG_PATH = join2(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
|
|
1877
2160
|
LAYER_DISABLE_ENV = {
|
|
1878
2161
|
L1_heuristic: "AGENT_MOSS_DISABLE_HEURISTIC",
|
|
1879
2162
|
L2_logic_rules: "AGENT_MOSS_DISABLE_LOGIC_RULES",
|
|
@@ -1884,16 +2167,16 @@ var init_runtime_config = __esm({
|
|
|
1884
2167
|
deny_read: "\u4EC5\u62E6\u622A\u8BFB\u53D6",
|
|
1885
2168
|
deny_both: "\u8BFB\u5199\u5747\u62E6\u622A"
|
|
1886
2169
|
};
|
|
1887
|
-
_AGENTS_DIR_PROD =
|
|
1888
|
-
_AGENTS_DIR_DEV =
|
|
2170
|
+
_AGENTS_DIR_PROD = join2(__dirname3, "agents");
|
|
2171
|
+
_AGENTS_DIR_DEV = join2(__dirname3, "..", "..", "..", "agent_moss", "agents");
|
|
1889
2172
|
SKILL_ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
1890
2173
|
}
|
|
1891
2174
|
});
|
|
1892
2175
|
|
|
1893
2176
|
// src/agents/index.ts
|
|
1894
|
-
import { readFileSync as
|
|
2177
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
1895
2178
|
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
1896
|
-
import { fileURLToPath as
|
|
2179
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1897
2180
|
function resolveAgentsDir() {
|
|
1898
2181
|
if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
|
|
1899
2182
|
if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
|
|
@@ -1918,7 +2201,7 @@ function getAgentExtensions(agentId) {
|
|
|
1918
2201
|
return null;
|
|
1919
2202
|
}
|
|
1920
2203
|
try {
|
|
1921
|
-
const raw =
|
|
2204
|
+
const raw = readFileSync4(rulesFile, "utf-8");
|
|
1922
2205
|
ext = JSON.parse(raw);
|
|
1923
2206
|
if (!ext.agent_id) {
|
|
1924
2207
|
console.warn(`[AgentMoss agents] rules.json \u7F3A\u5C11 agent_id \u5B57\u6BB5: ${rulesFile}`);
|
|
@@ -1942,7 +2225,7 @@ var init_agents = __esm({
|
|
|
1942
2225
|
"src/agents/index.ts"() {
|
|
1943
2226
|
"use strict";
|
|
1944
2227
|
init_esm_shims();
|
|
1945
|
-
__filename3 =
|
|
2228
|
+
__filename3 = fileURLToPath5(import.meta.url);
|
|
1946
2229
|
__dirname4 = dirname3(__filename3);
|
|
1947
2230
|
AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
|
|
1948
2231
|
AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
|
|
@@ -1962,19 +2245,25 @@ function runHeuristicCheck(input) {
|
|
|
1962
2245
|
const re = new RegExp(rule.pattern);
|
|
1963
2246
|
if (re.test(command)) {
|
|
1964
2247
|
if (rule.action === "Deny") {
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2248
|
+
return {
|
|
2249
|
+
allowed: false,
|
|
2250
|
+
reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
|
|
2251
|
+
riskLevel: rule.severity,
|
|
2252
|
+
riskType: "sensitive_action",
|
|
2253
|
+
confidence: 90,
|
|
2254
|
+
source: "heuristic",
|
|
2255
|
+
violatedLayers: ["1.1"]
|
|
2256
|
+
};
|
|
1970
2257
|
}
|
|
1971
2258
|
}
|
|
1972
2259
|
} catch {
|
|
1973
2260
|
}
|
|
1974
2261
|
}
|
|
2262
|
+
const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
|
|
2263
|
+
const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
|
|
1975
2264
|
let topRiskType = "";
|
|
1976
2265
|
for (const rule of DANGEROUS_PATTERNS) {
|
|
1977
|
-
if (rule.pattern.test(
|
|
2266
|
+
if (rule.pattern.test(scanCommand)) {
|
|
1978
2267
|
matched.push(rule.description);
|
|
1979
2268
|
if (severityRank(rule.severity) > severityRank(highestSeverity)) {
|
|
1980
2269
|
highestSeverity = rule.severity;
|
|
@@ -1988,7 +2277,7 @@ function runHeuristicCheck(input) {
|
|
|
1988
2277
|
for (const rule of agentExt.extra_command_patterns) {
|
|
1989
2278
|
try {
|
|
1990
2279
|
const re = new RegExp(rule.pattern, "i");
|
|
1991
|
-
if (re.test(
|
|
2280
|
+
if (re.test(scanCommand)) {
|
|
1992
2281
|
matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
|
|
1993
2282
|
if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
|
|
1994
2283
|
highestSeverity = rule.risk_level;
|
|
@@ -2012,14 +2301,12 @@ function runHeuristicCheck(input) {
|
|
|
2012
2301
|
const actionTypeL = input.aNext.actionType.toLowerCase();
|
|
2013
2302
|
const detailL = input.aNext.actionDetail.toLowerCase();
|
|
2014
2303
|
const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
|
|
2304
|
+
const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
|
|
2305
|
+
if (dangerousResult) return dangerousResult;
|
|
2015
2306
|
if (!isExcludedType) {
|
|
2016
2307
|
const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
|
|
2017
2308
|
if (pathResult) return pathResult;
|
|
2018
2309
|
}
|
|
2019
|
-
const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
|
|
2020
|
-
if (dangerousResult) return dangerousResult;
|
|
2021
|
-
const lateralResult = checkLateralMovement(detailL);
|
|
2022
|
-
if (lateralResult) return lateralResult;
|
|
2023
2310
|
if (matched.length > 0) {
|
|
2024
2311
|
return {
|
|
2025
2312
|
allowed: false,
|
|
@@ -2032,6 +2319,8 @@ function runHeuristicCheck(input) {
|
|
|
2032
2319
|
// 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
|
|
2033
2320
|
};
|
|
2034
2321
|
}
|
|
2322
|
+
const lateralResult = checkLateralMovement(detailL);
|
|
2323
|
+
if (lateralResult) return lateralResult;
|
|
2035
2324
|
return null;
|
|
2036
2325
|
}
|
|
2037
2326
|
function checkInjectionKeywords(text, extraKeywords) {
|
|
@@ -2054,6 +2343,68 @@ function isInlineScriptCommand(actionDetail) {
|
|
|
2054
2343
|
const re = /(?:^|\s)(?:python|python3|python3\.\d+|perl|perl5|ruby|jruby|mruby|node|php|lua)\s+(?:-[a-zA-Z]*[cer][a-zA-Z]*\s+)/i;
|
|
2055
2344
|
return re.test(actionDetail);
|
|
2056
2345
|
}
|
|
2346
|
+
function stripLiteralTextSegments(actionDetail) {
|
|
2347
|
+
const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
|
|
2348
|
+
const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
|
|
2349
|
+
if (writeScriptExec || pipeToInterpreter) {
|
|
2350
|
+
return actionDetail;
|
|
2351
|
+
}
|
|
2352
|
+
const resultParts = [];
|
|
2353
|
+
for (const seg of splitCommandSegments(actionDetail)) {
|
|
2354
|
+
const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
|
|
2355
|
+
if (fw === "echo" || fw === "printf") {
|
|
2356
|
+
const segUnquotedRedirect = seg.replace(
|
|
2357
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
2358
|
+
(_m, _q, p) => ">" + p
|
|
2359
|
+
);
|
|
2360
|
+
resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
|
|
2361
|
+
} else {
|
|
2362
|
+
resultParts.push(seg);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return resultParts.join("");
|
|
2366
|
+
}
|
|
2367
|
+
function splitCommandSegments(actionDetail) {
|
|
2368
|
+
const segments = [];
|
|
2369
|
+
let cur = "";
|
|
2370
|
+
let quote = null;
|
|
2371
|
+
for (let i = 0; i < actionDetail.length; i++) {
|
|
2372
|
+
const ch = actionDetail[i];
|
|
2373
|
+
if (quote) {
|
|
2374
|
+
cur += ch;
|
|
2375
|
+
if (ch === quote) quote = null;
|
|
2376
|
+
continue;
|
|
2377
|
+
}
|
|
2378
|
+
if (ch === "'" || ch === '"') {
|
|
2379
|
+
quote = ch;
|
|
2380
|
+
cur += ch;
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (ch === ";" || ch === "\n") {
|
|
2384
|
+
segments.push(cur);
|
|
2385
|
+
segments.push(ch);
|
|
2386
|
+
cur = "";
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
if (ch === "&" && actionDetail[i + 1] === "&") {
|
|
2390
|
+
segments.push(cur);
|
|
2391
|
+
segments.push("&&");
|
|
2392
|
+
cur = "";
|
|
2393
|
+
i++;
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
if (ch === "|" && actionDetail[i + 1] === "|") {
|
|
2397
|
+
segments.push(cur);
|
|
2398
|
+
segments.push("||");
|
|
2399
|
+
cur = "";
|
|
2400
|
+
i++;
|
|
2401
|
+
continue;
|
|
2402
|
+
}
|
|
2403
|
+
cur += ch;
|
|
2404
|
+
}
|
|
2405
|
+
if (cur) segments.push(cur);
|
|
2406
|
+
return segments;
|
|
2407
|
+
}
|
|
2057
2408
|
function severityRank(s) {
|
|
2058
2409
|
const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
2059
2410
|
return ranks[s] || 0;
|
|
@@ -2061,13 +2412,18 @@ function severityRank(s) {
|
|
|
2061
2412
|
function checkSensitivePath(command, cwd, agentExt) {
|
|
2062
2413
|
const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
|
|
2063
2414
|
const isWriteOpFlag = isWriteOperation("", command);
|
|
2415
|
+
const pathScanText = stripLiteralTextSegments(command);
|
|
2064
2416
|
const pathMatches = (text, path3) => {
|
|
2065
|
-
const
|
|
2417
|
+
const parts = path3.split(/(\(\?!.*?\))/);
|
|
2418
|
+
const escaped = parts.map((part) => {
|
|
2419
|
+
if (part.startsWith("(?!")) return part;
|
|
2420
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2421
|
+
}).join("");
|
|
2066
2422
|
const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
|
|
2067
2423
|
return re.test(text);
|
|
2068
2424
|
};
|
|
2069
2425
|
for (const sp of ROOT_SENSITIVE_PATHS) {
|
|
2070
|
-
if (pathMatches(
|
|
2426
|
+
if (pathMatches(pathScanText, sp.path.toLowerCase())) {
|
|
2071
2427
|
if (sp.path === "/dev/random") {
|
|
2072
2428
|
if (isWriteOpFlag) {
|
|
2073
2429
|
continue;
|
|
@@ -2101,7 +2457,7 @@ function checkSensitivePath(command, cwd, agentExt) {
|
|
|
2101
2457
|
const expanded = `${expandedCwd}/${sp.path}`;
|
|
2102
2458
|
const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
|
|
2103
2459
|
const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
|
|
2104
|
-
if (pathRe.test(
|
|
2460
|
+
if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
|
|
2105
2461
|
if (!isWriteOpFlag && !sp.credential) {
|
|
2106
2462
|
continue;
|
|
2107
2463
|
}
|
|
@@ -2172,8 +2528,10 @@ function checkSensitivePath(command, cwd, agentExt) {
|
|
|
2172
2528
|
}
|
|
2173
2529
|
function checkDangerousPatterns(actionType, actionDetail) {
|
|
2174
2530
|
if (actionType === "file_write" || actionType === "file_edit") return null;
|
|
2175
|
-
const TEMP_DIRS = ["/tmp
|
|
2176
|
-
const isTempPath = TEMP_DIRS.some(
|
|
2531
|
+
const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
|
|
2532
|
+
const isTempPath = TEMP_DIRS.some(
|
|
2533
|
+
(td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
|
|
2534
|
+
);
|
|
2177
2535
|
if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
|
|
2178
2536
|
return {
|
|
2179
2537
|
allowed: false,
|
|
@@ -2185,7 +2543,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
|
|
|
2185
2543
|
violatedLayers: ["1.1"]
|
|
2186
2544
|
};
|
|
2187
2545
|
}
|
|
2188
|
-
|
|
2546
|
+
const findWriteMatch = actionDetail.match(
|
|
2547
|
+
/\bfind\b.*?(?:(?:^|\s)-exec(?:dir)?\s+(chmod|chown|cp|mv|rm|dd|tee|truncate|mkfs|shred|rmdir|unlink)\b|(?:^|\s)\|\s*xargs\s+(?:sed\s+-[a-zA-Z]*i|chmod|chown|cp|mv|rm|dd|tee|truncate|mkfs|shred|rmdir|unlink)\b)/i
|
|
2548
|
+
);
|
|
2549
|
+
if (findWriteMatch) {
|
|
2550
|
+
const cmdHit = findWriteMatch[1] || "xargs";
|
|
2551
|
+
return {
|
|
2552
|
+
allowed: false,
|
|
2553
|
+
reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
|
|
2554
|
+
riskLevel: "high",
|
|
2555
|
+
riskType: "script_execution",
|
|
2556
|
+
confidence: 90,
|
|
2557
|
+
source: "heuristic",
|
|
2558
|
+
violatedLayers: ["1.1"]
|
|
2559
|
+
};
|
|
2560
|
+
}
|
|
2561
|
+
if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
|
|
2189
2562
|
return {
|
|
2190
2563
|
allowed: false,
|
|
2191
2564
|
reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
|
|
@@ -2196,17 +2569,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
|
|
|
2196
2569
|
violatedLayers: ["1.1"]
|
|
2197
2570
|
};
|
|
2198
2571
|
}
|
|
2572
|
+
if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
|
|
2573
|
+
return {
|
|
2574
|
+
allowed: false,
|
|
2575
|
+
reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
|
|
2576
|
+
riskLevel: "high",
|
|
2577
|
+
riskType: "file_access",
|
|
2578
|
+
confidence: 90,
|
|
2579
|
+
source: "heuristic",
|
|
2580
|
+
violatedLayers: ["1.1"]
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2199
2583
|
return null;
|
|
2200
2584
|
}
|
|
2201
2585
|
function extractRemoteHost(command) {
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2586
|
+
const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
|
|
2587
|
+
for (const tok of tokens) {
|
|
2588
|
+
const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
|
|
2589
|
+
if (am && am[2] && !isLocalHost(am[2])) {
|
|
2590
|
+
return `${am[1]}@${am[2]}`;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
for (const tok of tokens) {
|
|
2594
|
+
const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
|
|
2595
|
+
if (hm && hm[1] && !isLocalHost(hm[1])) {
|
|
2596
|
+
return hm[1];
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
const skipValueOpts = /* @__PURE__ */ new Set([
|
|
2600
|
+
"-p",
|
|
2601
|
+
"-l",
|
|
2602
|
+
"-i",
|
|
2603
|
+
"-o",
|
|
2604
|
+
"-b",
|
|
2605
|
+
"-c",
|
|
2606
|
+
"-m",
|
|
2607
|
+
"-F",
|
|
2608
|
+
"-e",
|
|
2609
|
+
"-S",
|
|
2610
|
+
"-W",
|
|
2611
|
+
"-J",
|
|
2612
|
+
"-I",
|
|
2613
|
+
"-T",
|
|
2614
|
+
"-B",
|
|
2615
|
+
"-L",
|
|
2616
|
+
"-R",
|
|
2617
|
+
"-D",
|
|
2618
|
+
"-E",
|
|
2619
|
+
"-M",
|
|
2620
|
+
"-O",
|
|
2621
|
+
"-P",
|
|
2622
|
+
"-q"
|
|
2623
|
+
]);
|
|
2624
|
+
let foundCmd = false;
|
|
2625
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2626
|
+
const t = tokens[i];
|
|
2627
|
+
if (t === "ssh" || t === "scp" || t === "rsync") {
|
|
2628
|
+
foundCmd = true;
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
if (!foundCmd) continue;
|
|
2632
|
+
if (t.startsWith("-")) {
|
|
2633
|
+
if (skipValueOpts.has(t) && i + 1 < tokens.length) {
|
|
2634
|
+
i++;
|
|
2635
|
+
}
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
if (t.startsWith("/")) continue;
|
|
2639
|
+
return t;
|
|
2640
|
+
}
|
|
2641
|
+
return null;
|
|
2204
2642
|
}
|
|
2205
2643
|
function isLocalHost(host) {
|
|
2206
2644
|
const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
|
|
2207
2645
|
return localNames.includes(host.replace(/.*@/, ""));
|
|
2208
2646
|
}
|
|
2209
2647
|
function checkLateralMovement(actionDetail) {
|
|
2648
|
+
if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
|
|
2649
|
+
const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
|
|
2650
|
+
let hasRealCall = false;
|
|
2651
|
+
for (const seg of segments) {
|
|
2652
|
+
const parts = seg.split(/\s+/);
|
|
2653
|
+
const fw = (parts[0] || "").toLowerCase();
|
|
2654
|
+
if (fw === "echo" || fw === "printf") continue;
|
|
2655
|
+
if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
|
|
2656
|
+
hasRealCall = true;
|
|
2657
|
+
break;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
if (!hasRealCall) return null;
|
|
2210
2661
|
if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
|
|
2211
2662
|
const targetHost = extractRemoteHost(actionDetail);
|
|
2212
2663
|
if (targetHost && !isLocalHost(targetHost)) {
|
|
@@ -2306,10 +2757,14 @@ var init_heuristic = __esm({
|
|
|
2306
2757
|
{ pattern: /mkfs\.\w+\s+\/dev\/(sd|hd|nvme|vd|loop|mapper|md|dm)\S+/i, severity: "critical", description: "\u683C\u5F0F\u5316\u5757\u8BBE\u5907 (mkfs /dev/\u5757\u8BBE\u5907)", riskType: "script_execution" },
|
|
2307
2758
|
{ pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
|
|
2308
2759
|
// --- Critical: remote code execution ---
|
|
2309
|
-
|
|
2760
|
+
// curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
|
|
2761
|
+
// 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
|
|
2762
|
+
// runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
|
|
2763
|
+
{ pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
|
|
2764
|
+
{ pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
|
|
2310
2765
|
{ pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
|
|
2311
2766
|
{ pattern: /eval\s+\$\(curl|eval\s+\$\(wget/i, severity: "critical", description: "eval \u8FDC\u7A0B\u4EE3\u7801\u6267\u884C", riskType: "script_execution" },
|
|
2312
|
-
{ pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "
|
|
2767
|
+
{ pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
|
|
2313
2768
|
// --- Critical: reverse shell ---
|
|
2314
2769
|
{ pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
|
|
2315
2770
|
{ pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
|
|
@@ -2320,13 +2775,25 @@ var init_heuristic = __esm({
|
|
|
2320
2775
|
{ pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
|
|
2321
2776
|
// --- Critical: privilege escalation / container escape ---
|
|
2322
2777
|
{ pattern: /\/var\/run\/docker\.sock/i, severity: "critical", description: "\u8BBF\u95EE Docker socket\uFF0C\u53EF\u80FD\u5BFC\u81F4\u5BB9\u5668\u9003\u9038", riskType: "privilege_escalation" },
|
|
2323
|
-
// ---
|
|
2324
|
-
|
|
2778
|
+
// --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
|
|
2779
|
+
// 修正「只拦 777、xx7 漏放」的缺口:
|
|
2780
|
+
// 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
|
|
2781
|
+
// 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
|
|
2782
|
+
// 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
|
|
2783
|
+
// 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
|
|
2784
|
+
// 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
|
|
2785
|
+
{ pattern: /chmod\s+[0-7]{0,3}7\b/i, severity: "critical", description: "\u5371\u9669\u6743\u9650\u8BBE\u7F6E (chmod ...7, other \u4F4D\u542B\u5199+\u6267\u884C\u655E\u53E3)", riskType: "privilege_escalation" },
|
|
2786
|
+
{ pattern: /chmod\s+[24][0-7]{3}\b/i, severity: "critical", description: "setuid/setgid \u4F4D\u8BBE\u7F6E (chmod 4xxx/2xxx, \u63D0\u6743)", riskType: "privilege_escalation" },
|
|
2787
|
+
{ pattern: /chmod\s+[ugoa]\+s\b/i, severity: "critical", description: "\u7B26\u53F7 setuid/setgid \u8BBE\u7F6E (chmod u+s/g+s/a+s, \u63D0\u6743)", riskType: "privilege_escalation" },
|
|
2325
2788
|
{ pattern: /chown\s+root|chown\s+-R\s+\w+:\w+\s+\//i, severity: "high", description: "\u4FEE\u6539\u6240\u6709\u8005\u4E3A root / \u9012\u5F52\u4FEE\u6539\u6839\u76EE\u5F55\u6240\u6709\u8005", riskType: "script_execution" },
|
|
2326
2789
|
// sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
|
|
2327
2790
|
{ pattern: /sudo\s+(rm|chmod|chown|dd|mkfs|format|iptables|crontab|passwd|userdel|useradd|groupdel)\b/i, severity: "high", description: "\u63D0\u6743\u6267\u884C\u9AD8\u5371\u547D\u4EE4 (sudo + \u5371\u9669\u64CD\u4F5C)", riskType: "privilege_escalation" },
|
|
2328
2791
|
// --- High: system config overwrite ---
|
|
2792
|
+
// > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
|
|
2793
|
+
// 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
|
|
2329
2794
|
{ pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
|
|
2795
|
+
{ pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
|
|
2796
|
+
// 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
|
|
2330
2797
|
// --- High: download script + execute risk ---
|
|
2331
2798
|
{ pattern: /(?:curl|wget)\s+.*(?:-o|--output|>)\s*\S+\.(?:sh|bash)\b/i, severity: "high", description: "\u8FDC\u7A0B\u4E0B\u8F7D\u811A\u672C\u5230\u672C\u5730", riskType: "script_execution" },
|
|
2332
2799
|
{ pattern: /(?:curl|wget)\s+.*\s+&&?\s*(?:chmod\s+\+x|bash|sh)/i, severity: "high", description: "\u4E0B\u8F7D\u540E\u6267\u884C", riskType: "script_execution" },
|
|
@@ -2342,7 +2809,11 @@ var init_heuristic = __esm({
|
|
|
2342
2809
|
// --- Medium: credential export ---
|
|
2343
2810
|
{ pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
|
|
2344
2811
|
// --- High: shell rc file append (persistence) ---
|
|
2812
|
+
// 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
|
|
2813
|
+
// 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
|
|
2814
|
+
// /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
|
|
2345
2815
|
{ pattern: />>\s*(~|\/home\/[^/]+)\/(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)/, severity: "high", description: "\u68C0\u6D4B\u5230\u4FEE\u6539 Shell \u914D\u7F6E\u6587\u4EF6\uFF08~/.bashrc \u7B49\uFF09\uFF0C\u53EF\u80FD\u7528\u4E8E\u6301\u4E45\u5316\u540E\u95E8", riskType: "persistence" },
|
|
2816
|
+
{ pattern: />>\s*\S*\/(\.bashrc|\.zshrc|\.profile|\.bash_profile|\.zprofile)\b/, severity: "high", description: "\u68C0\u6D4B\u5230\u4FEE\u6539 Shell \u914D\u7F6E\u6587\u4EF6\uFF08.bashrc \u7B49\uFF09\uFF0C\u53EF\u80FD\u7528\u4E8E\u6301\u4E45\u5316\u540E\u95E8", riskType: "persistence" },
|
|
2346
2817
|
// --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
|
|
2347
2818
|
{ pattern: />\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i, severity: "high", description: "\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6", riskType: "file_access" },
|
|
2348
2819
|
// --- High: non-interactive password modification ---
|
|
@@ -2378,17 +2849,16 @@ __export(token_stats_exports, {
|
|
|
2378
2849
|
recordTokenUsage: () => recordTokenUsage,
|
|
2379
2850
|
resetTokenStats: () => resetTokenStats
|
|
2380
2851
|
});
|
|
2381
|
-
import { readFileSync as
|
|
2382
|
-
import { join as join4 } from "path";
|
|
2383
|
-
import { homedir as homedir3 } from "os";
|
|
2852
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
2384
2853
|
function getStatsPath() {
|
|
2385
2854
|
const envPath = process.env[ENV_STATS_PATH];
|
|
2386
|
-
|
|
2855
|
+
if (envPath) return envPath;
|
|
2856
|
+
return getTokenStatsPath();
|
|
2387
2857
|
}
|
|
2388
2858
|
function loadAllRecords(statsPath) {
|
|
2389
2859
|
if (!existsSync7(statsPath)) return [];
|
|
2390
2860
|
try {
|
|
2391
|
-
const data = JSON.parse(
|
|
2861
|
+
const data = JSON.parse(readFileSync8(statsPath, "utf-8"));
|
|
2392
2862
|
if (Array.isArray(data)) return data;
|
|
2393
2863
|
if (data && Array.isArray(data.records)) return data.records;
|
|
2394
2864
|
return [];
|
|
@@ -2528,13 +2998,12 @@ function resetTokenStats() {
|
|
|
2528
2998
|
}
|
|
2529
2999
|
}
|
|
2530
3000
|
}
|
|
2531
|
-
var
|
|
3001
|
+
var ENV_STATS_PATH, MAX_RECORDS;
|
|
2532
3002
|
var init_token_stats = __esm({
|
|
2533
3003
|
"src/engine/token-stats.ts"() {
|
|
2534
3004
|
"use strict";
|
|
2535
3005
|
init_esm_shims();
|
|
2536
|
-
|
|
2537
|
-
STATS_FILE = join4(STATS_DIR, "agent_moss_token_stats.json");
|
|
3006
|
+
init_paths();
|
|
2538
3007
|
ENV_STATS_PATH = "AGENT_MOSS_TOKEN_STATS_PATH";
|
|
2539
3008
|
MAX_RECORDS = 1e4;
|
|
2540
3009
|
}
|
|
@@ -2553,10 +3022,10 @@ init_skill_loader();
|
|
|
2553
3022
|
|
|
2554
3023
|
// src/engine/template-loader.ts
|
|
2555
3024
|
init_esm_shims();
|
|
2556
|
-
import { readFileSync as
|
|
3025
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
2557
3026
|
import { resolve as resolve3, dirname as dirname4 } from "path";
|
|
2558
|
-
import { fileURLToPath as
|
|
2559
|
-
var __filename4 =
|
|
3027
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3028
|
+
var __filename4 = fileURLToPath6(import.meta.url);
|
|
2560
3029
|
var __dirname5 = dirname4(__filename4);
|
|
2561
3030
|
var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
|
|
2562
3031
|
var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
|
|
@@ -2574,7 +3043,7 @@ function loadTemplates() {
|
|
|
2574
3043
|
const filePath = resolve3(dir, file);
|
|
2575
3044
|
if (existsSync5(filePath)) {
|
|
2576
3045
|
const key = file.replace(".txt", "").replace("_template", "");
|
|
2577
|
-
cachedTemplates[key] =
|
|
3046
|
+
cachedTemplates[key] = readFileSync5(filePath, "utf-8");
|
|
2578
3047
|
}
|
|
2579
3048
|
}
|
|
2580
3049
|
console.log(`[AgentMoss] Loaded ${Object.keys(cachedTemplates).length} templates from ${dir}`);
|
|
@@ -2583,8 +3052,8 @@ function loadTemplates() {
|
|
|
2583
3052
|
|
|
2584
3053
|
// src/engine/script-analyzer.ts
|
|
2585
3054
|
init_esm_shims();
|
|
2586
|
-
import { readFileSync as
|
|
2587
|
-
import { resolve as resolve4, join as
|
|
3055
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3056
|
+
import { resolve as resolve4, join as join3 } from "path";
|
|
2588
3057
|
var SUSPICIOUS_PATTERNS = [
|
|
2589
3058
|
// Network exfiltration (curl/wget POST in scripts is almost never legitimate)
|
|
2590
3059
|
{ name: "curl POST", pattern: /curl\s+(.*\s)?-X\s+POST/i },
|
|
@@ -2717,8 +3186,8 @@ function assessKeywordCombinationRisk(matchedKeywords, scriptContent) {
|
|
|
2717
3186
|
}
|
|
2718
3187
|
function readScriptContent(scriptPath, maxLines = 500) {
|
|
2719
3188
|
try {
|
|
2720
|
-
const expandedPath = scriptPath.startsWith("~") ?
|
|
2721
|
-
const raw =
|
|
3189
|
+
const expandedPath = scriptPath.startsWith("~") ? join3(process.env.HOME || "/home/user", scriptPath.slice(2)) : resolve4(scriptPath);
|
|
3190
|
+
const raw = readFileSync6(expandedPath, "utf-8");
|
|
2722
3191
|
const lines = raw.split("\n").slice(0, maxLines);
|
|
2723
3192
|
return { content: lines.join("\n"), lineCount: lines.length, error: "" };
|
|
2724
3193
|
} catch (e) {
|
|
@@ -2772,8 +3241,8 @@ ${result.scriptContent}
|
|
|
2772
3241
|
init_esm_shims();
|
|
2773
3242
|
init_heuristic();
|
|
2774
3243
|
init_logic_rules();
|
|
2775
|
-
import { readFileSync as
|
|
2776
|
-
import { resolve as resolve5, join as
|
|
3244
|
+
import { readFileSync as readFileSync7, statSync, existsSync as existsSync6 } from "fs";
|
|
3245
|
+
import { resolve as resolve5, join as join4, isAbsolute, dirname as dirname5 } from "path";
|
|
2777
3246
|
import { homedir as homedir2 } from "os";
|
|
2778
3247
|
function emptyChain() {
|
|
2779
3248
|
return {
|
|
@@ -3014,7 +3483,7 @@ function extractInlineScriptPath(actionDetail) {
|
|
|
3014
3483
|
function resolvePath(p) {
|
|
3015
3484
|
let expanded = p;
|
|
3016
3485
|
if (expanded.startsWith("~/") || expanded === "~") {
|
|
3017
|
-
expanded =
|
|
3486
|
+
expanded = join4(homedir2(), expanded.slice(1));
|
|
3018
3487
|
}
|
|
3019
3488
|
return resolve5(expanded);
|
|
3020
3489
|
}
|
|
@@ -3027,7 +3496,7 @@ function readScriptContent2(scriptPath, maxFileSize = 2 * 1024 * 1024) {
|
|
|
3027
3496
|
if (stat.size > maxFileSize) {
|
|
3028
3497
|
return { content: "", lineCount: 0, fileSize: stat.size, error: `\u811A\u672C\u6587\u4EF6\u8FC7\u5927 (${stat.size} bytes)\uFF0C\u8DF3\u8FC7\u8BFB\u53D6` };
|
|
3029
3498
|
}
|
|
3030
|
-
const content =
|
|
3499
|
+
const content = readFileSync7(path3, "utf-8");
|
|
3031
3500
|
const lineCount = content ? content.split("\n").length : 0;
|
|
3032
3501
|
return { content, lineCount, fileSize: stat.size, error: "" };
|
|
3033
3502
|
} catch (e) {
|
|
@@ -3044,7 +3513,7 @@ function extractChildScriptPaths(content, baseDir) {
|
|
|
3044
3513
|
const pathStr = m[1];
|
|
3045
3514
|
if (pathStr.startsWith("-") || pathStr.startsWith("$")) continue;
|
|
3046
3515
|
if (pathStr.includes("/dev/") || pathStr.startsWith("|") || pathStr.startsWith(";")) continue;
|
|
3047
|
-
const full = isAbsolute(pathStr) ? pathStr :
|
|
3516
|
+
const full = isAbsolute(pathStr) ? pathStr : join4(baseDir, pathStr);
|
|
3048
3517
|
const resolved = resolvePath(full);
|
|
3049
3518
|
if (existsSync6(resolved)) {
|
|
3050
3519
|
try {
|
|
@@ -3471,11 +3940,13 @@ async function analyzeWithLLM(prompt, config) {
|
|
|
3471
3940
|
} catch {
|
|
3472
3941
|
}
|
|
3473
3942
|
clearTimeout(totalTimeoutTimer);
|
|
3943
|
+
const riskLevel = parsed.riskLevel ?? parsed.risk_level;
|
|
3944
|
+
const riskType = parsed.riskType ?? parsed.risk_type;
|
|
3474
3945
|
return {
|
|
3475
3946
|
allowed: parsed.allowed !== false,
|
|
3476
3947
|
reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
|
|
3477
|
-
riskLevel: ["low", "medium", "high", "critical"].includes(
|
|
3478
|
-
riskType:
|
|
3948
|
+
riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
|
|
3949
|
+
riskType: riskType || "",
|
|
3479
3950
|
confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
|
|
3480
3951
|
source: "llm",
|
|
3481
3952
|
violatedLayers: ["3"]
|
|
@@ -3593,7 +4064,10 @@ async function runAnalysis(input) {
|
|
|
3593
4064
|
if (heuristicResult && !heuristicResult.allowed) {
|
|
3594
4065
|
const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
|
|
3595
4066
|
if (!skipInlineFileAccess) {
|
|
3596
|
-
|
|
4067
|
+
const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
|
|
4068
|
+
if (shouldDenyL1) {
|
|
4069
|
+
return heuristicResult;
|
|
4070
|
+
}
|
|
3597
4071
|
}
|
|
3598
4072
|
}
|
|
3599
4073
|
let scriptChain = null;
|
|
@@ -3671,7 +4145,8 @@ async function runAnalysis(input) {
|
|
|
3671
4145
|
) && DANGEROUS_PATTERNS.some(
|
|
3672
4146
|
(p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
|
|
3673
4147
|
);
|
|
3674
|
-
|
|
4148
|
+
const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
|
|
4149
|
+
if (!fullCmdHit && !hasRedirectWrite) {
|
|
3675
4150
|
return {
|
|
3676
4151
|
allowed: true,
|
|
3677
4152
|
reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,
|