@kenhkl/agent-moss 0.10.4 → 0.11.0
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 +472 -82
- package/dist/cli.js.map +1 -1
- package/dist/engine/coordinator.js +463 -73
- package/dist/engine/coordinator.js.map +1 -1
- package/dist/rules/user_rules.json +77 -0
- package/dist/server.js +467 -77
- package/dist/server.js.map +1 -1
- package/dist/static/index.html +104 -1
- package/package.json +1 -1
|
@@ -45,11 +45,13 @@ __export(config_exports, {
|
|
|
45
45
|
isHeuristicEnabled: () => isHeuristicEnabled,
|
|
46
46
|
isLLMAvailable: () => isLLMAvailable,
|
|
47
47
|
isLogicRulesEnabled: () => isLogicRulesEnabled,
|
|
48
|
+
probeLLMConnection: () => probeLLMConnection,
|
|
48
49
|
updateLLMConfig: () => updateLLMConfig
|
|
49
50
|
});
|
|
50
51
|
import fs from "fs";
|
|
51
52
|
import path2 from "path";
|
|
52
53
|
import os from "os";
|
|
54
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
53
55
|
function loadConfigFile(filePath) {
|
|
54
56
|
try {
|
|
55
57
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
@@ -112,16 +114,38 @@ function getTimeoutConfig() {
|
|
|
112
114
|
function getRetryConfig() {
|
|
113
115
|
return retryConfig;
|
|
114
116
|
}
|
|
117
|
+
function wildcardToRegExp(pattern) {
|
|
118
|
+
return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
119
|
+
}
|
|
115
120
|
function loadCustomRules() {
|
|
121
|
+
const rules = [];
|
|
116
122
|
try {
|
|
117
123
|
const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
if (raw) {
|
|
125
|
+
const parsed = JSON.parse(raw);
|
|
126
|
+
if (Array.isArray(parsed)) {
|
|
127
|
+
for (const r of parsed) {
|
|
128
|
+
if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
122
132
|
} catch {
|
|
123
|
-
return [];
|
|
124
133
|
}
|
|
134
|
+
for (const rulesPath of USER_RULES_PATHS) {
|
|
135
|
+
try {
|
|
136
|
+
if (!fs.existsSync(rulesPath)) continue;
|
|
137
|
+
const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
|
|
138
|
+
const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
|
|
139
|
+
for (const r of actions) {
|
|
140
|
+
const p = r?.pattern;
|
|
141
|
+
if (!p) continue;
|
|
142
|
+
rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return rules;
|
|
125
149
|
}
|
|
126
150
|
function getLLMConfig() {
|
|
127
151
|
return llmConfig;
|
|
@@ -136,6 +160,30 @@ function isLLMAvailable() {
|
|
|
136
160
|
if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
|
|
137
161
|
return llmConfig.apiKey.length > 0;
|
|
138
162
|
}
|
|
163
|
+
async function probeLLMConnection(timeoutMs = 8e3) {
|
|
164
|
+
if (!isLLMAvailable()) return false;
|
|
165
|
+
const OpenAI2 = (await import("openai")).default;
|
|
166
|
+
const config = getLLMConfig();
|
|
167
|
+
const provider = detectProvider(config.baseUrl, config.provider);
|
|
168
|
+
const extraHeaders = getProviderHeaders(provider);
|
|
169
|
+
const client = new OpenAI2({
|
|
170
|
+
baseURL: config.baseUrl,
|
|
171
|
+
apiKey: config.apiKey,
|
|
172
|
+
timeout: timeoutMs,
|
|
173
|
+
maxRetries: 0,
|
|
174
|
+
defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
|
|
175
|
+
});
|
|
176
|
+
try {
|
|
177
|
+
const resp = await client.chat.completions.create({
|
|
178
|
+
model: config.model,
|
|
179
|
+
messages: [{ role: "user", content: "ping" }],
|
|
180
|
+
max_tokens: 1
|
|
181
|
+
});
|
|
182
|
+
return !!(resp && resp.choices && resp.choices[0]);
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
139
187
|
function getLLMFailMode() {
|
|
140
188
|
const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
|
|
141
189
|
return val === "fail_closed" ? "fail_closed" : "fail_open";
|
|
@@ -179,7 +227,7 @@ function isHeuristicEnabled() {
|
|
|
179
227
|
function isLogicRulesEnabled() {
|
|
180
228
|
return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
|
|
181
229
|
}
|
|
182
|
-
var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
|
|
230
|
+
var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
|
|
183
231
|
var init_config = __esm({
|
|
184
232
|
"src/config.ts"() {
|
|
185
233
|
"use strict";
|
|
@@ -201,6 +249,21 @@ var init_config = __esm({
|
|
|
201
249
|
timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
|
|
202
250
|
};
|
|
203
251
|
})();
|
|
252
|
+
__configModuleDir = (() => {
|
|
253
|
+
try {
|
|
254
|
+
return path2.dirname(fileURLToPath2(import.meta.url));
|
|
255
|
+
} catch {
|
|
256
|
+
return typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
257
|
+
}
|
|
258
|
+
})();
|
|
259
|
+
USER_RULES_PATHS = [
|
|
260
|
+
path2.join(__configModuleDir, "rules", "user_rules.json"),
|
|
261
|
+
// 源在 ts/src/rules(一般不存在)
|
|
262
|
+
path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
|
|
263
|
+
// dist: dist/engine/../rules = dist/rules ✅
|
|
264
|
+
path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
|
|
265
|
+
// dev src: ts/src/../../agent_moss
|
|
266
|
+
];
|
|
204
267
|
customRules = loadCustomRules();
|
|
205
268
|
PROVIDER_HEADERS = {
|
|
206
269
|
"openrouter": {
|
|
@@ -217,7 +280,17 @@ var init_config = __esm({
|
|
|
217
280
|
// src/engine/inline-analyzer.ts
|
|
218
281
|
function isLiteralTextCommand(command) {
|
|
219
282
|
ECHO_CMD_RE.lastIndex = 0;
|
|
220
|
-
|
|
283
|
+
if (!ECHO_CMD_RE.test(command)) return false;
|
|
284
|
+
if (command.includes(">")) {
|
|
285
|
+
let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
286
|
+
d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
|
|
287
|
+
d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
|
|
288
|
+
d = d.replace(/'[^']*'|"[^"]*"/g, "");
|
|
289
|
+
if (/>{1,2}\s*(?!&)\S/.test(d)) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
221
294
|
}
|
|
222
295
|
function extractInlineCode(command) {
|
|
223
296
|
const parts = [];
|
|
@@ -294,7 +367,7 @@ function analyzeInlineScript(command) {
|
|
|
294
367
|
riskType: "file_access"
|
|
295
368
|
};
|
|
296
369
|
}
|
|
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;
|
|
370
|
+
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
371
|
var init_inline_analyzer = __esm({
|
|
299
372
|
"src/engine/inline-analyzer.ts"() {
|
|
300
373
|
"use strict";
|
|
@@ -364,6 +437,13 @@ var init_inline_analyzer = __esm({
|
|
|
364
437
|
// xargs bash / xargs sh
|
|
365
438
|
/\|\s*xargs\s+(?:bash|sh|zsh)/gi
|
|
366
439
|
];
|
|
440
|
+
SHELL_RC_FRAGMENTS = [
|
|
441
|
+
".bashrc",
|
|
442
|
+
".zshrc",
|
|
443
|
+
".profile",
|
|
444
|
+
".bash_profile",
|
|
445
|
+
".zprofile"
|
|
446
|
+
];
|
|
367
447
|
SENSITIVE_PATH_FRAGMENTS = [
|
|
368
448
|
"/etc/shadow",
|
|
369
449
|
"/etc/gshadow",
|
|
@@ -380,9 +460,10 @@ var init_inline_analyzer = __esm({
|
|
|
380
460
|
"/dev/kmsg",
|
|
381
461
|
".ssh/id_rsa",
|
|
382
462
|
".ssh/id_ed25519",
|
|
383
|
-
".ssh/authorized_keys"
|
|
463
|
+
".ssh/authorized_keys",
|
|
464
|
+
...SHELL_RC_FRAGMENTS
|
|
384
465
|
];
|
|
385
|
-
REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
|
|
466
|
+
REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
|
|
386
467
|
DIR_TRAVERSAL_KEYWORDS = [
|
|
387
468
|
// Python 目录遍历
|
|
388
469
|
"os.listdir",
|
|
@@ -405,11 +486,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
|
|
|
405
486
|
if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
|
|
406
487
|
if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
|
|
407
488
|
if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
|
|
489
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
490
|
+
const trimmed = seg.trim();
|
|
491
|
+
if (!trimmed) continue;
|
|
492
|
+
const parts = trimmed.split(/\s+/);
|
|
493
|
+
const fw = parts[0]?.toLowerCase() || "";
|
|
494
|
+
if (fw === "wget" || fw === "curl") {
|
|
495
|
+
const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
|
|
496
|
+
if (isStdout) continue;
|
|
497
|
+
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);
|
|
498
|
+
if (isDevnull) continue;
|
|
499
|
+
if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
408
507
|
if (actionDetail.includes(">")) {
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
508
|
+
const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
509
|
+
const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
|
|
510
|
+
const detailUnquotedRedirect = detailWithoutDevnull.replace(
|
|
511
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
512
|
+
(_m, _q, p) => ">" + p
|
|
513
|
+
);
|
|
514
|
+
const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
|
|
515
|
+
if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
|
|
516
|
+
return true;
|
|
413
517
|
}
|
|
414
518
|
}
|
|
415
519
|
return false;
|
|
@@ -433,27 +537,31 @@ function runLogicCheck(input) {
|
|
|
433
537
|
if (rbwResult) return rbwResult;
|
|
434
538
|
if (input.promptSession && input.promptSession.trim()) {
|
|
435
539
|
if (!isExcludedType) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
540
|
+
if (INLINE_SCRIPT_RE.test(actionDetail)) {
|
|
541
|
+
} else {
|
|
542
|
+
const promptLower = input.promptSession.toLowerCase();
|
|
543
|
+
for (const pattern of INTENT_DEVIATION_PATTERNS) {
|
|
544
|
+
const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
|
|
545
|
+
const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
|
|
546
|
+
if (intentMatched && dangerousMatched) {
|
|
547
|
+
return {
|
|
548
|
+
allowed: false,
|
|
549
|
+
reason: pattern.reason,
|
|
550
|
+
riskLevel: "high",
|
|
551
|
+
riskType: "intent_deviation",
|
|
552
|
+
confidence: 85,
|
|
553
|
+
source: "logic_rule",
|
|
554
|
+
violatedLayers: ["2"]
|
|
555
|
+
};
|
|
556
|
+
}
|
|
450
557
|
}
|
|
451
558
|
}
|
|
452
559
|
}
|
|
453
560
|
}
|
|
454
561
|
const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
|
|
455
562
|
if (indirectResult) return indirectResult;
|
|
456
|
-
if (
|
|
563
|
+
if (INLINE_SCRIPT_RE.test(actionDetail)) {
|
|
564
|
+
} else if (containsPrivilegeEscalation(actionDetail)) {
|
|
457
565
|
const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
|
|
458
566
|
const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
|
|
459
567
|
if (!isAdminCmd) {
|
|
@@ -483,6 +591,9 @@ function extractFilePaths(text) {
|
|
|
483
591
|
const paths = [];
|
|
484
592
|
const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
|
|
485
593
|
if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
|
|
594
|
+
const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
|
|
595
|
+
const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
|
|
596
|
+
if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
|
|
486
597
|
const quotedPaths = text.match(/["']\/([^"']+)["']/g);
|
|
487
598
|
if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
|
|
488
599
|
return [...new Set(paths)];
|
|
@@ -493,8 +604,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
|
|
|
493
604
|
const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
|
|
494
605
|
const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
|
|
495
606
|
if (!isWrite) return null;
|
|
496
|
-
const
|
|
497
|
-
if (
|
|
607
|
+
const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
|
|
608
|
+
if (isBashLike) {
|
|
609
|
+
const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
610
|
+
const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
|
|
611
|
+
const detailUnquotedRedirect = detailNoDevnull.replace(
|
|
612
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
613
|
+
(_m, _q, p) => ">" + p
|
|
614
|
+
);
|
|
615
|
+
const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
|
|
616
|
+
const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
|
|
617
|
+
let hasWriteSeg = hasRedirectWrite;
|
|
618
|
+
if (!hasWriteSeg) {
|
|
619
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
620
|
+
const trimmed = seg.trim();
|
|
621
|
+
if (!trimmed) continue;
|
|
622
|
+
const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
|
|
623
|
+
if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
|
|
624
|
+
hasWriteSeg = true;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
|
|
628
|
+
hasWriteSeg = true;
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
|
|
632
|
+
hasWriteSeg = true;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!hasWriteSeg) return null;
|
|
638
|
+
}
|
|
639
|
+
const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
|
|
640
|
+
const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
|
|
641
|
+
const targetPaths = [];
|
|
642
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
643
|
+
const trimmed = seg.trim();
|
|
644
|
+
if (!trimmed) continue;
|
|
645
|
+
const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
|
|
646
|
+
if (firstWord === "cd") {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
if (READ_ONLY_COMMANDS.has(firstWord)) {
|
|
650
|
+
const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
|
|
651
|
+
const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
|
|
652
|
+
const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
|
|
653
|
+
const m = segNoQuotes.match(REDIRECT_TARGET_RE);
|
|
654
|
+
if (m) targetPaths.push(m[1] || m[2] || m[3]);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
const segPaths = extractFilePaths(trimmed);
|
|
658
|
+
const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
|
|
659
|
+
if (SOURCE_ARG_CMDS.has(firstWord)) {
|
|
660
|
+
if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
|
|
661
|
+
} else if (firstWord === "dd") {
|
|
662
|
+
const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
|
|
663
|
+
const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
|
|
664
|
+
const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
|
|
665
|
+
const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
|
|
666
|
+
for (const sp of segPaths) {
|
|
667
|
+
if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
targetPaths.push(...segPathsFiltered);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const uniqueTargetPaths = [...new Set(targetPaths)];
|
|
674
|
+
if (uniqueTargetPaths.length === 0) return null;
|
|
498
675
|
const historyReadPaths = /* @__PURE__ */ new Set();
|
|
499
676
|
for (const action of actionHistory) {
|
|
500
677
|
const histDetail = action.actionDetail.toLowerCase();
|
|
@@ -507,7 +684,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
|
|
|
507
684
|
}
|
|
508
685
|
}
|
|
509
686
|
const unreadPaths = [];
|
|
510
|
-
for (const path3 of
|
|
687
|
+
for (const path3 of uniqueTargetPaths) {
|
|
511
688
|
const hasRead = [...historyReadPaths].some(
|
|
512
689
|
(rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
|
|
513
690
|
);
|
|
@@ -570,6 +747,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
|
|
|
570
747
|
violatedLayers: ["2"]
|
|
571
748
|
};
|
|
572
749
|
}
|
|
750
|
+
function containsPrivilegeEscalation(actionDetail) {
|
|
751
|
+
const ad = actionDetail.toLowerCase();
|
|
752
|
+
for (const seg of ad.split(/&&|\|\||;|\n/)) {
|
|
753
|
+
const trimmed = seg.trim();
|
|
754
|
+
if (!trimmed) continue;
|
|
755
|
+
const parts = trimmed.split(/\s+/);
|
|
756
|
+
const fw = parts[0]?.toLowerCase() || "";
|
|
757
|
+
if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
|
|
758
|
+
if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
|
|
759
|
+
}
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
573
762
|
function checkIndirectFileAccess(actionType, actionDetail, cwd) {
|
|
574
763
|
if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
|
|
575
764
|
return null;
|
|
@@ -674,12 +863,13 @@ var init_logic_rules = __esm({
|
|
|
674
863
|
{ path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
|
|
675
864
|
// 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
|
|
676
865
|
{ 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" },
|
|
866
|
+
{ path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
|
|
678
867
|
{ path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
|
|
679
868
|
// SSH 主机私钥
|
|
680
|
-
|
|
681
|
-
{ path: "/etc/ssh/
|
|
682
|
-
{ path: "/etc/ssh/
|
|
869
|
+
// SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
|
|
870
|
+
{ path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
|
|
871
|
+
{ path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
|
|
872
|
+
{ path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
|
|
683
873
|
];
|
|
684
874
|
HOME_SENSITIVE_PATHS = [
|
|
685
875
|
{ path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
|
|
@@ -720,7 +910,7 @@ var init_logic_rules = __esm({
|
|
|
720
910
|
"cat"
|
|
721
911
|
]);
|
|
722
912
|
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"];
|
|
913
|
+
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
914
|
WRITE_COMMAND_RES = [
|
|
725
915
|
/\brm\b/,
|
|
726
916
|
/\bunlink\b/,
|
|
@@ -766,6 +956,12 @@ var init_logic_rules = __esm({
|
|
|
766
956
|
"nl",
|
|
767
957
|
"tac",
|
|
768
958
|
"rev",
|
|
959
|
+
// NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
|
|
960
|
+
// read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
|
|
961
|
+
// 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
|
|
962
|
+
// echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
|
|
963
|
+
"echo",
|
|
964
|
+
"printf",
|
|
769
965
|
"lsblk",
|
|
770
966
|
"blockdev",
|
|
771
967
|
"smartctl",
|
|
@@ -818,7 +1014,7 @@ var init_logic_rules = __esm({
|
|
|
818
1014
|
"modinfo",
|
|
819
1015
|
"sysctl"
|
|
820
1016
|
]);
|
|
821
|
-
DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b
|
|
1017
|
+
DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
|
|
822
1018
|
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
1019
|
CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
|
|
824
1020
|
PASSWORD_MODIFY_PATTERNS = [
|
|
@@ -868,7 +1064,7 @@ var init_logic_rules = __esm({
|
|
|
868
1064
|
// src/engine/skill-loader.ts
|
|
869
1065
|
import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
870
1066
|
import { resolve, dirname } from "path";
|
|
871
|
-
import { fileURLToPath as
|
|
1067
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
872
1068
|
function loadAllSkills() {
|
|
873
1069
|
if (cachedSkills) return cachedSkills;
|
|
874
1070
|
cachedSkills = {};
|
|
@@ -939,7 +1135,7 @@ var init_skill_loader = __esm({
|
|
|
939
1135
|
"src/engine/skill-loader.ts"() {
|
|
940
1136
|
"use strict";
|
|
941
1137
|
init_esm_shims();
|
|
942
|
-
__filename2 =
|
|
1138
|
+
__filename2 = fileURLToPath3(import.meta.url);
|
|
943
1139
|
__dirname2 = dirname(__filename2);
|
|
944
1140
|
SKILLS_DIR = resolve(__dirname2, "../skills");
|
|
945
1141
|
SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
|
|
@@ -1014,7 +1210,7 @@ __export(runtime_config_exports, {
|
|
|
1014
1210
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
|
|
1015
1211
|
import { join } from "path";
|
|
1016
1212
|
import { homedir } from "os";
|
|
1017
|
-
import { fileURLToPath as
|
|
1213
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1018
1214
|
import { dirname as dirname2 } from "path";
|
|
1019
1215
|
function getRuntimeConfigPath() {
|
|
1020
1216
|
const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
|
|
@@ -1870,7 +2066,7 @@ var init_runtime_config = __esm({
|
|
|
1870
2066
|
init_heuristic();
|
|
1871
2067
|
init_logic_rules();
|
|
1872
2068
|
init_skill_loader();
|
|
1873
|
-
__dirname3 = dirname2(
|
|
2069
|
+
__dirname3 = dirname2(fileURLToPath4(import.meta.url));
|
|
1874
2070
|
RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
|
|
1875
2071
|
RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
|
|
1876
2072
|
ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
|
|
@@ -1893,7 +2089,7 @@ var init_runtime_config = __esm({
|
|
|
1893
2089
|
// src/agents/index.ts
|
|
1894
2090
|
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
1895
2091
|
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
1896
|
-
import { fileURLToPath as
|
|
2092
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1897
2093
|
function resolveAgentsDir() {
|
|
1898
2094
|
if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
|
|
1899
2095
|
if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
|
|
@@ -1942,7 +2138,7 @@ var init_agents = __esm({
|
|
|
1942
2138
|
"src/agents/index.ts"() {
|
|
1943
2139
|
"use strict";
|
|
1944
2140
|
init_esm_shims();
|
|
1945
|
-
__filename3 =
|
|
2141
|
+
__filename3 = fileURLToPath5(import.meta.url);
|
|
1946
2142
|
__dirname4 = dirname3(__filename3);
|
|
1947
2143
|
AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
|
|
1948
2144
|
AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
|
|
@@ -1962,19 +2158,25 @@ function runHeuristicCheck(input) {
|
|
|
1962
2158
|
const re = new RegExp(rule.pattern);
|
|
1963
2159
|
if (re.test(command)) {
|
|
1964
2160
|
if (rule.action === "Deny") {
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2161
|
+
return {
|
|
2162
|
+
allowed: false,
|
|
2163
|
+
reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
|
|
2164
|
+
riskLevel: rule.severity,
|
|
2165
|
+
riskType: "sensitive_action",
|
|
2166
|
+
confidence: 90,
|
|
2167
|
+
source: "heuristic",
|
|
2168
|
+
violatedLayers: ["1.1"]
|
|
2169
|
+
};
|
|
1970
2170
|
}
|
|
1971
2171
|
}
|
|
1972
2172
|
} catch {
|
|
1973
2173
|
}
|
|
1974
2174
|
}
|
|
2175
|
+
const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
|
|
2176
|
+
const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
|
|
1975
2177
|
let topRiskType = "";
|
|
1976
2178
|
for (const rule of DANGEROUS_PATTERNS) {
|
|
1977
|
-
if (rule.pattern.test(
|
|
2179
|
+
if (rule.pattern.test(scanCommand)) {
|
|
1978
2180
|
matched.push(rule.description);
|
|
1979
2181
|
if (severityRank(rule.severity) > severityRank(highestSeverity)) {
|
|
1980
2182
|
highestSeverity = rule.severity;
|
|
@@ -1988,7 +2190,7 @@ function runHeuristicCheck(input) {
|
|
|
1988
2190
|
for (const rule of agentExt.extra_command_patterns) {
|
|
1989
2191
|
try {
|
|
1990
2192
|
const re = new RegExp(rule.pattern, "i");
|
|
1991
|
-
if (re.test(
|
|
2193
|
+
if (re.test(scanCommand)) {
|
|
1992
2194
|
matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
|
|
1993
2195
|
if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
|
|
1994
2196
|
highestSeverity = rule.risk_level;
|
|
@@ -2012,14 +2214,12 @@ function runHeuristicCheck(input) {
|
|
|
2012
2214
|
const actionTypeL = input.aNext.actionType.toLowerCase();
|
|
2013
2215
|
const detailL = input.aNext.actionDetail.toLowerCase();
|
|
2014
2216
|
const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
|
|
2217
|
+
const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
|
|
2218
|
+
if (dangerousResult) return dangerousResult;
|
|
2015
2219
|
if (!isExcludedType) {
|
|
2016
2220
|
const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
|
|
2017
2221
|
if (pathResult) return pathResult;
|
|
2018
2222
|
}
|
|
2019
|
-
const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
|
|
2020
|
-
if (dangerousResult) return dangerousResult;
|
|
2021
|
-
const lateralResult = checkLateralMovement(detailL);
|
|
2022
|
-
if (lateralResult) return lateralResult;
|
|
2023
2223
|
if (matched.length > 0) {
|
|
2024
2224
|
return {
|
|
2025
2225
|
allowed: false,
|
|
@@ -2032,6 +2232,8 @@ function runHeuristicCheck(input) {
|
|
|
2032
2232
|
// 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
|
|
2033
2233
|
};
|
|
2034
2234
|
}
|
|
2235
|
+
const lateralResult = checkLateralMovement(detailL);
|
|
2236
|
+
if (lateralResult) return lateralResult;
|
|
2035
2237
|
return null;
|
|
2036
2238
|
}
|
|
2037
2239
|
function checkInjectionKeywords(text, extraKeywords) {
|
|
@@ -2054,6 +2256,68 @@ function isInlineScriptCommand(actionDetail) {
|
|
|
2054
2256
|
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
2257
|
return re.test(actionDetail);
|
|
2056
2258
|
}
|
|
2259
|
+
function stripLiteralTextSegments(actionDetail) {
|
|
2260
|
+
const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
|
|
2261
|
+
const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
|
|
2262
|
+
if (writeScriptExec || pipeToInterpreter) {
|
|
2263
|
+
return actionDetail;
|
|
2264
|
+
}
|
|
2265
|
+
const resultParts = [];
|
|
2266
|
+
for (const seg of splitCommandSegments(actionDetail)) {
|
|
2267
|
+
const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
|
|
2268
|
+
if (fw === "echo" || fw === "printf") {
|
|
2269
|
+
const segUnquotedRedirect = seg.replace(
|
|
2270
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
2271
|
+
(_m, _q, p) => ">" + p
|
|
2272
|
+
);
|
|
2273
|
+
resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
|
|
2274
|
+
} else {
|
|
2275
|
+
resultParts.push(seg);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return resultParts.join("");
|
|
2279
|
+
}
|
|
2280
|
+
function splitCommandSegments(actionDetail) {
|
|
2281
|
+
const segments = [];
|
|
2282
|
+
let cur = "";
|
|
2283
|
+
let quote = null;
|
|
2284
|
+
for (let i = 0; i < actionDetail.length; i++) {
|
|
2285
|
+
const ch = actionDetail[i];
|
|
2286
|
+
if (quote) {
|
|
2287
|
+
cur += ch;
|
|
2288
|
+
if (ch === quote) quote = null;
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
if (ch === "'" || ch === '"') {
|
|
2292
|
+
quote = ch;
|
|
2293
|
+
cur += ch;
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
if (ch === ";" || ch === "\n") {
|
|
2297
|
+
segments.push(cur);
|
|
2298
|
+
segments.push(ch);
|
|
2299
|
+
cur = "";
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (ch === "&" && actionDetail[i + 1] === "&") {
|
|
2303
|
+
segments.push(cur);
|
|
2304
|
+
segments.push("&&");
|
|
2305
|
+
cur = "";
|
|
2306
|
+
i++;
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
if (ch === "|" && actionDetail[i + 1] === "|") {
|
|
2310
|
+
segments.push(cur);
|
|
2311
|
+
segments.push("||");
|
|
2312
|
+
cur = "";
|
|
2313
|
+
i++;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
2316
|
+
cur += ch;
|
|
2317
|
+
}
|
|
2318
|
+
if (cur) segments.push(cur);
|
|
2319
|
+
return segments;
|
|
2320
|
+
}
|
|
2057
2321
|
function severityRank(s) {
|
|
2058
2322
|
const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
2059
2323
|
return ranks[s] || 0;
|
|
@@ -2061,13 +2325,18 @@ function severityRank(s) {
|
|
|
2061
2325
|
function checkSensitivePath(command, cwd, agentExt) {
|
|
2062
2326
|
const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
|
|
2063
2327
|
const isWriteOpFlag = isWriteOperation("", command);
|
|
2328
|
+
const pathScanText = stripLiteralTextSegments(command);
|
|
2064
2329
|
const pathMatches = (text, path3) => {
|
|
2065
|
-
const
|
|
2330
|
+
const parts = path3.split(/(\(\?!.*?\))/);
|
|
2331
|
+
const escaped = parts.map((part) => {
|
|
2332
|
+
if (part.startsWith("(?!")) return part;
|
|
2333
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2334
|
+
}).join("");
|
|
2066
2335
|
const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
|
|
2067
2336
|
return re.test(text);
|
|
2068
2337
|
};
|
|
2069
2338
|
for (const sp of ROOT_SENSITIVE_PATHS) {
|
|
2070
|
-
if (pathMatches(
|
|
2339
|
+
if (pathMatches(pathScanText, sp.path.toLowerCase())) {
|
|
2071
2340
|
if (sp.path === "/dev/random") {
|
|
2072
2341
|
if (isWriteOpFlag) {
|
|
2073
2342
|
continue;
|
|
@@ -2101,7 +2370,7 @@ function checkSensitivePath(command, cwd, agentExt) {
|
|
|
2101
2370
|
const expanded = `${expandedCwd}/${sp.path}`;
|
|
2102
2371
|
const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
|
|
2103
2372
|
const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
|
|
2104
|
-
if (pathRe.test(
|
|
2373
|
+
if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
|
|
2105
2374
|
if (!isWriteOpFlag && !sp.credential) {
|
|
2106
2375
|
continue;
|
|
2107
2376
|
}
|
|
@@ -2172,8 +2441,10 @@ function checkSensitivePath(command, cwd, agentExt) {
|
|
|
2172
2441
|
}
|
|
2173
2442
|
function checkDangerousPatterns(actionType, actionDetail) {
|
|
2174
2443
|
if (actionType === "file_write" || actionType === "file_edit") return null;
|
|
2175
|
-
const TEMP_DIRS = ["/tmp
|
|
2176
|
-
const isTempPath = TEMP_DIRS.some(
|
|
2444
|
+
const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
|
|
2445
|
+
const isTempPath = TEMP_DIRS.some(
|
|
2446
|
+
(td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
|
|
2447
|
+
);
|
|
2177
2448
|
if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
|
|
2178
2449
|
return {
|
|
2179
2450
|
allowed: false,
|
|
@@ -2185,7 +2456,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
|
|
|
2185
2456
|
violatedLayers: ["1.1"]
|
|
2186
2457
|
};
|
|
2187
2458
|
}
|
|
2188
|
-
|
|
2459
|
+
const findWriteMatch = actionDetail.match(
|
|
2460
|
+
/\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
|
|
2461
|
+
);
|
|
2462
|
+
if (findWriteMatch) {
|
|
2463
|
+
const cmdHit = findWriteMatch[1] || "xargs";
|
|
2464
|
+
return {
|
|
2465
|
+
allowed: false,
|
|
2466
|
+
reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
|
|
2467
|
+
riskLevel: "high",
|
|
2468
|
+
riskType: "script_execution",
|
|
2469
|
+
confidence: 90,
|
|
2470
|
+
source: "heuristic",
|
|
2471
|
+
violatedLayers: ["1.1"]
|
|
2472
|
+
};
|
|
2473
|
+
}
|
|
2474
|
+
if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
|
|
2189
2475
|
return {
|
|
2190
2476
|
allowed: false,
|
|
2191
2477
|
reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
|
|
@@ -2196,17 +2482,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
|
|
|
2196
2482
|
violatedLayers: ["1.1"]
|
|
2197
2483
|
};
|
|
2198
2484
|
}
|
|
2485
|
+
if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
|
|
2486
|
+
return {
|
|
2487
|
+
allowed: false,
|
|
2488
|
+
reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
|
|
2489
|
+
riskLevel: "high",
|
|
2490
|
+
riskType: "file_access",
|
|
2491
|
+
confidence: 90,
|
|
2492
|
+
source: "heuristic",
|
|
2493
|
+
violatedLayers: ["1.1"]
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2199
2496
|
return null;
|
|
2200
2497
|
}
|
|
2201
2498
|
function extractRemoteHost(command) {
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2499
|
+
const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
|
|
2500
|
+
for (const tok of tokens) {
|
|
2501
|
+
const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
|
|
2502
|
+
if (am && am[2] && !isLocalHost(am[2])) {
|
|
2503
|
+
return `${am[1]}@${am[2]}`;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
for (const tok of tokens) {
|
|
2507
|
+
const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
|
|
2508
|
+
if (hm && hm[1] && !isLocalHost(hm[1])) {
|
|
2509
|
+
return hm[1];
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const skipValueOpts = /* @__PURE__ */ new Set([
|
|
2513
|
+
"-p",
|
|
2514
|
+
"-l",
|
|
2515
|
+
"-i",
|
|
2516
|
+
"-o",
|
|
2517
|
+
"-b",
|
|
2518
|
+
"-c",
|
|
2519
|
+
"-m",
|
|
2520
|
+
"-F",
|
|
2521
|
+
"-e",
|
|
2522
|
+
"-S",
|
|
2523
|
+
"-W",
|
|
2524
|
+
"-J",
|
|
2525
|
+
"-I",
|
|
2526
|
+
"-T",
|
|
2527
|
+
"-B",
|
|
2528
|
+
"-L",
|
|
2529
|
+
"-R",
|
|
2530
|
+
"-D",
|
|
2531
|
+
"-E",
|
|
2532
|
+
"-M",
|
|
2533
|
+
"-O",
|
|
2534
|
+
"-P",
|
|
2535
|
+
"-q"
|
|
2536
|
+
]);
|
|
2537
|
+
let foundCmd = false;
|
|
2538
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2539
|
+
const t = tokens[i];
|
|
2540
|
+
if (t === "ssh" || t === "scp" || t === "rsync") {
|
|
2541
|
+
foundCmd = true;
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
if (!foundCmd) continue;
|
|
2545
|
+
if (t.startsWith("-")) {
|
|
2546
|
+
if (skipValueOpts.has(t) && i + 1 < tokens.length) {
|
|
2547
|
+
i++;
|
|
2548
|
+
}
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
if (t.startsWith("/")) continue;
|
|
2552
|
+
return t;
|
|
2553
|
+
}
|
|
2554
|
+
return null;
|
|
2204
2555
|
}
|
|
2205
2556
|
function isLocalHost(host) {
|
|
2206
2557
|
const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
|
|
2207
2558
|
return localNames.includes(host.replace(/.*@/, ""));
|
|
2208
2559
|
}
|
|
2209
2560
|
function checkLateralMovement(actionDetail) {
|
|
2561
|
+
if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
|
|
2562
|
+
const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
|
|
2563
|
+
let hasRealCall = false;
|
|
2564
|
+
for (const seg of segments) {
|
|
2565
|
+
const parts = seg.split(/\s+/);
|
|
2566
|
+
const fw = (parts[0] || "").toLowerCase();
|
|
2567
|
+
if (fw === "echo" || fw === "printf") continue;
|
|
2568
|
+
if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
|
|
2569
|
+
hasRealCall = true;
|
|
2570
|
+
break;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
if (!hasRealCall) return null;
|
|
2210
2574
|
if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
|
|
2211
2575
|
const targetHost = extractRemoteHost(actionDetail);
|
|
2212
2576
|
if (targetHost && !isLocalHost(targetHost)) {
|
|
@@ -2306,10 +2670,14 @@ var init_heuristic = __esm({
|
|
|
2306
2670
|
{ 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
2671
|
{ pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
|
|
2308
2672
|
// --- Critical: remote code execution ---
|
|
2309
|
-
|
|
2673
|
+
// curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
|
|
2674
|
+
// 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
|
|
2675
|
+
// runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
|
|
2676
|
+
{ pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
|
|
2677
|
+
{ pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
|
|
2310
2678
|
{ pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
|
|
2311
2679
|
{ 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: "
|
|
2680
|
+
{ pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
|
|
2313
2681
|
// --- Critical: reverse shell ---
|
|
2314
2682
|
{ pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
|
|
2315
2683
|
{ pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
|
|
@@ -2320,13 +2688,25 @@ var init_heuristic = __esm({
|
|
|
2320
2688
|
{ pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
|
|
2321
2689
|
// --- Critical: privilege escalation / container escape ---
|
|
2322
2690
|
{ 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
|
-
|
|
2691
|
+
// --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
|
|
2692
|
+
// 修正「只拦 777、xx7 漏放」的缺口:
|
|
2693
|
+
// 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
|
|
2694
|
+
// 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
|
|
2695
|
+
// 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
|
|
2696
|
+
// 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
|
|
2697
|
+
// 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
|
|
2698
|
+
{ 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" },
|
|
2699
|
+
{ 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" },
|
|
2700
|
+
{ 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
2701
|
{ 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
2702
|
// sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
|
|
2327
2703
|
{ 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
2704
|
// --- High: system config overwrite ---
|
|
2705
|
+
// > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
|
|
2706
|
+
// 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
|
|
2329
2707
|
{ pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
|
|
2708
|
+
{ pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
|
|
2709
|
+
// 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
|
|
2330
2710
|
// --- High: download script + execute risk ---
|
|
2331
2711
|
{ 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
2712
|
{ 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 +2722,11 @@ var init_heuristic = __esm({
|
|
|
2342
2722
|
// --- Medium: credential export ---
|
|
2343
2723
|
{ pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
|
|
2344
2724
|
// --- High: shell rc file append (persistence) ---
|
|
2725
|
+
// 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
|
|
2726
|
+
// 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
|
|
2727
|
+
// /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
|
|
2345
2728
|
{ 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" },
|
|
2729
|
+
{ 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
2730
|
// --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
|
|
2347
2731
|
{ 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
2732
|
// --- High: non-interactive password modification ---
|
|
@@ -2555,8 +2939,8 @@ init_skill_loader();
|
|
|
2555
2939
|
init_esm_shims();
|
|
2556
2940
|
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
2557
2941
|
import { resolve as resolve3, dirname as dirname4 } from "path";
|
|
2558
|
-
import { fileURLToPath as
|
|
2559
|
-
var __filename4 =
|
|
2942
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
2943
|
+
var __filename4 = fileURLToPath6(import.meta.url);
|
|
2560
2944
|
var __dirname5 = dirname4(__filename4);
|
|
2561
2945
|
var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
|
|
2562
2946
|
var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
|
|
@@ -3471,11 +3855,13 @@ async function analyzeWithLLM(prompt, config) {
|
|
|
3471
3855
|
} catch {
|
|
3472
3856
|
}
|
|
3473
3857
|
clearTimeout(totalTimeoutTimer);
|
|
3858
|
+
const riskLevel = parsed.riskLevel ?? parsed.risk_level;
|
|
3859
|
+
const riskType = parsed.riskType ?? parsed.risk_type;
|
|
3474
3860
|
return {
|
|
3475
3861
|
allowed: parsed.allowed !== false,
|
|
3476
3862
|
reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
|
|
3477
|
-
riskLevel: ["low", "medium", "high", "critical"].includes(
|
|
3478
|
-
riskType:
|
|
3863
|
+
riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
|
|
3864
|
+
riskType: riskType || "",
|
|
3479
3865
|
confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
|
|
3480
3866
|
source: "llm",
|
|
3481
3867
|
violatedLayers: ["3"]
|
|
@@ -3593,7 +3979,10 @@ async function runAnalysis(input) {
|
|
|
3593
3979
|
if (heuristicResult && !heuristicResult.allowed) {
|
|
3594
3980
|
const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
|
|
3595
3981
|
if (!skipInlineFileAccess) {
|
|
3596
|
-
|
|
3982
|
+
const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
|
|
3983
|
+
if (shouldDenyL1) {
|
|
3984
|
+
return heuristicResult;
|
|
3985
|
+
}
|
|
3597
3986
|
}
|
|
3598
3987
|
}
|
|
3599
3988
|
let scriptChain = null;
|
|
@@ -3671,7 +4060,8 @@ async function runAnalysis(input) {
|
|
|
3671
4060
|
) && DANGEROUS_PATTERNS.some(
|
|
3672
4061
|
(p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
|
|
3673
4062
|
);
|
|
3674
|
-
|
|
4063
|
+
const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
|
|
4064
|
+
if (!fullCmdHit && !hasRedirectWrite) {
|
|
3675
4065
|
return {
|
|
3676
4066
|
allowed: true,
|
|
3677
4067
|
reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,
|