@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
package/dist/server.js
CHANGED
|
@@ -55,11 +55,13 @@ __export(config_exports, {
|
|
|
55
55
|
isHeuristicEnabled: () => isHeuristicEnabled,
|
|
56
56
|
isLLMAvailable: () => isLLMAvailable,
|
|
57
57
|
isLogicRulesEnabled: () => isLogicRulesEnabled,
|
|
58
|
+
probeLLMConnection: () => probeLLMConnection,
|
|
58
59
|
updateLLMConfig: () => updateLLMConfig
|
|
59
60
|
});
|
|
60
61
|
import fs from "fs";
|
|
61
62
|
import path2 from "path";
|
|
62
63
|
import os from "os";
|
|
64
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
63
65
|
function loadConfigFile(filePath) {
|
|
64
66
|
try {
|
|
65
67
|
const raw = fs.readFileSync(filePath, "utf-8");
|
|
@@ -122,16 +124,38 @@ function getTimeoutConfig() {
|
|
|
122
124
|
function getRetryConfig() {
|
|
123
125
|
return retryConfig;
|
|
124
126
|
}
|
|
127
|
+
function wildcardToRegExp(pattern) {
|
|
128
|
+
return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, ".*");
|
|
129
|
+
}
|
|
125
130
|
function loadCustomRules() {
|
|
131
|
+
const rules = [];
|
|
126
132
|
try {
|
|
127
133
|
const raw = process.env.AGENT_MOSS_CUSTOM_RULES;
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
if (raw) {
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
if (Array.isArray(parsed)) {
|
|
137
|
+
for (const r of parsed) {
|
|
138
|
+
if (r && r.pattern && r.action) rules.push({ pattern: r.pattern, action: r.action, severity: r.severity || "high" });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
132
142
|
} catch {
|
|
133
|
-
return [];
|
|
134
143
|
}
|
|
144
|
+
for (const rulesPath of USER_RULES_PATHS) {
|
|
145
|
+
try {
|
|
146
|
+
if (!fs.existsSync(rulesPath)) continue;
|
|
147
|
+
const data = JSON.parse(fs.readFileSync(rulesPath, "utf-8"));
|
|
148
|
+
const actions = Array.isArray(data?.sensitive_actions) ? data.sensitive_actions : [];
|
|
149
|
+
for (const r of actions) {
|
|
150
|
+
const p = r?.pattern;
|
|
151
|
+
if (!p) continue;
|
|
152
|
+
rules.push({ pattern: wildcardToRegExp(p), action: "Deny", severity: "high" });
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return rules;
|
|
135
159
|
}
|
|
136
160
|
function getLLMConfig() {
|
|
137
161
|
return llmConfig;
|
|
@@ -146,6 +170,30 @@ function isLLMAvailable() {
|
|
|
146
170
|
if (process.env.AGENT_MOSS_DISABLE_LLM === "1") return false;
|
|
147
171
|
return llmConfig.apiKey.length > 0;
|
|
148
172
|
}
|
|
173
|
+
async function probeLLMConnection(timeoutMs = 8e3) {
|
|
174
|
+
if (!isLLMAvailable()) return false;
|
|
175
|
+
const OpenAI2 = (await import("openai")).default;
|
|
176
|
+
const config = getLLMConfig();
|
|
177
|
+
const provider = detectProvider(config.baseUrl, config.provider);
|
|
178
|
+
const extraHeaders = getProviderHeaders(provider);
|
|
179
|
+
const client = new OpenAI2({
|
|
180
|
+
baseURL: config.baseUrl,
|
|
181
|
+
apiKey: config.apiKey,
|
|
182
|
+
timeout: timeoutMs,
|
|
183
|
+
maxRetries: 0,
|
|
184
|
+
defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
const resp = await client.chat.completions.create({
|
|
188
|
+
model: config.model,
|
|
189
|
+
messages: [{ role: "user", content: "ping" }],
|
|
190
|
+
max_tokens: 1
|
|
191
|
+
});
|
|
192
|
+
return !!(resp && resp.choices && resp.choices[0]);
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
149
197
|
function getLLMFailMode() {
|
|
150
198
|
const val = (process.env.AGENT_MOSS_LLM_FAIL_MODE || "fail_open").toLowerCase();
|
|
151
199
|
return val === "fail_closed" ? "fail_closed" : "fail_open";
|
|
@@ -189,7 +237,7 @@ function isHeuristicEnabled() {
|
|
|
189
237
|
function isLogicRulesEnabled() {
|
|
190
238
|
return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
|
|
191
239
|
}
|
|
192
|
-
var timeoutConfig, retryConfig, llmConfig, customRules, PROVIDER_HEADERS;
|
|
240
|
+
var timeoutConfig, retryConfig, llmConfig, __configModuleDir, USER_RULES_PATHS, customRules, PROVIDER_HEADERS;
|
|
193
241
|
var init_config = __esm({
|
|
194
242
|
"src/config.ts"() {
|
|
195
243
|
"use strict";
|
|
@@ -211,6 +259,21 @@ var init_config = __esm({
|
|
|
211
259
|
timeout: parseInt(process.env.AGENT_MOSS_LLM_TIMEOUT || String(fileConfig.timeout ?? String(tr.timeout.prompt1Timeout)), 10)
|
|
212
260
|
};
|
|
213
261
|
})();
|
|
262
|
+
__configModuleDir = (() => {
|
|
263
|
+
try {
|
|
264
|
+
return path2.dirname(fileURLToPath2(import.meta.url));
|
|
265
|
+
} catch {
|
|
266
|
+
return typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
267
|
+
}
|
|
268
|
+
})();
|
|
269
|
+
USER_RULES_PATHS = [
|
|
270
|
+
path2.join(__configModuleDir, "rules", "user_rules.json"),
|
|
271
|
+
// 源在 ts/src/rules(一般不存在)
|
|
272
|
+
path2.join(__configModuleDir, "..", "rules", "user_rules.json"),
|
|
273
|
+
// dist: dist/engine/../rules = dist/rules ✅
|
|
274
|
+
path2.join(__configModuleDir, "..", "..", "agent_moss", "rules", "user_rules.json")
|
|
275
|
+
// dev src: ts/src/../../agent_moss
|
|
276
|
+
];
|
|
214
277
|
customRules = loadCustomRules();
|
|
215
278
|
PROVIDER_HEADERS = {
|
|
216
279
|
"openrouter": {
|
|
@@ -227,7 +290,17 @@ var init_config = __esm({
|
|
|
227
290
|
// src/engine/inline-analyzer.ts
|
|
228
291
|
function isLiteralTextCommand(command) {
|
|
229
292
|
ECHO_CMD_RE.lastIndex = 0;
|
|
230
|
-
|
|
293
|
+
if (!ECHO_CMD_RE.test(command)) return false;
|
|
294
|
+
if (command.includes(">")) {
|
|
295
|
+
let d = command.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
296
|
+
d = d.replace(/(?:2|&|1)?\s*>\s*\/dev\/null\b/g, "");
|
|
297
|
+
d = d.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
|
|
298
|
+
d = d.replace(/'[^']*'|"[^"]*"/g, "");
|
|
299
|
+
if (/>{1,2}\s*(?!&)\S/.test(d)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return true;
|
|
231
304
|
}
|
|
232
305
|
function extractInlineCode(command) {
|
|
233
306
|
const parts = [];
|
|
@@ -304,7 +377,7 @@ function analyzeInlineScript(command) {
|
|
|
304
377
|
riskType: "file_access"
|
|
305
378
|
};
|
|
306
379
|
}
|
|
307
|
-
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;
|
|
380
|
+
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;
|
|
308
381
|
var init_inline_analyzer = __esm({
|
|
309
382
|
"src/engine/inline-analyzer.ts"() {
|
|
310
383
|
"use strict";
|
|
@@ -374,6 +447,13 @@ var init_inline_analyzer = __esm({
|
|
|
374
447
|
// xargs bash / xargs sh
|
|
375
448
|
/\|\s*xargs\s+(?:bash|sh|zsh)/gi
|
|
376
449
|
];
|
|
450
|
+
SHELL_RC_FRAGMENTS = [
|
|
451
|
+
".bashrc",
|
|
452
|
+
".zshrc",
|
|
453
|
+
".profile",
|
|
454
|
+
".bash_profile",
|
|
455
|
+
".zprofile"
|
|
456
|
+
];
|
|
377
457
|
SENSITIVE_PATH_FRAGMENTS = [
|
|
378
458
|
"/etc/shadow",
|
|
379
459
|
"/etc/gshadow",
|
|
@@ -390,9 +470,10 @@ var init_inline_analyzer = __esm({
|
|
|
390
470
|
"/dev/kmsg",
|
|
391
471
|
".ssh/id_rsa",
|
|
392
472
|
".ssh/id_ed25519",
|
|
393
|
-
".ssh/authorized_keys"
|
|
473
|
+
".ssh/authorized_keys",
|
|
474
|
+
...SHELL_RC_FRAGMENTS
|
|
394
475
|
];
|
|
395
|
-
REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "i");
|
|
476
|
+
REDIRECT_TO_SENSITIVE_RE = new RegExp(`>{1,2}\\s*(?:\\S*/)?(${SENSITIVE_PATH_FRAGMENTS.map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:\\s|$)`, "i");
|
|
396
477
|
DIR_TRAVERSAL_KEYWORDS = [
|
|
397
478
|
// Python 目录遍历
|
|
398
479
|
"os.listdir",
|
|
@@ -415,11 +496,34 @@ function isWriteOperation(actionType, actionDetail, writeKeywords = WRITE_KEYWOR
|
|
|
415
496
|
if (writeKeywords.some((kw) => actionType.includes(kw) || actionDetail.includes(kw))) return true;
|
|
416
497
|
if (WRITE_COMMAND_RES.some((rx) => rx.test(actionDetail))) return true;
|
|
417
498
|
if (/\bsed\s+-[a-zA-Z]*i/i.test(actionDetail)) return true;
|
|
499
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
500
|
+
const trimmed = seg.trim();
|
|
501
|
+
if (!trimmed) continue;
|
|
502
|
+
const parts = trimmed.split(/\s+/);
|
|
503
|
+
const fw = parts[0]?.toLowerCase() || "";
|
|
504
|
+
if (fw === "wget" || fw === "curl") {
|
|
505
|
+
const isStdout = /\s(?:-O|-o)-(?:\s|$)/.test(trimmed) || /\s--output-document=-(?:\s|$)/i.test(trimmed) || /\s--output=-(?:\s|$)/i.test(trimmed);
|
|
506
|
+
if (isStdout) continue;
|
|
507
|
+
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);
|
|
508
|
+
if (isDevnull) continue;
|
|
509
|
+
if (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed)) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
418
517
|
if (actionDetail.includes(">")) {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
518
|
+
const detailWithoutCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
519
|
+
const detailWithoutDevnull = detailWithoutCompare.replace(DEVNULL_REDIRECT_RE, "");
|
|
520
|
+
const detailUnquotedRedirect = detailWithoutDevnull.replace(
|
|
521
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
522
|
+
(_m, _q, p) => ">" + p
|
|
523
|
+
);
|
|
524
|
+
const detailWithoutQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
|
|
525
|
+
if (/>{1,2}\s*(?!&)\S/.test(detailWithoutQuotes)) {
|
|
526
|
+
return true;
|
|
423
527
|
}
|
|
424
528
|
}
|
|
425
529
|
return false;
|
|
@@ -443,27 +547,31 @@ function runLogicCheck(input) {
|
|
|
443
547
|
if (rbwResult) return rbwResult;
|
|
444
548
|
if (input.promptSession && input.promptSession.trim()) {
|
|
445
549
|
if (!isExcludedType) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
550
|
+
if (INLINE_SCRIPT_RE.test(actionDetail)) {
|
|
551
|
+
} else {
|
|
552
|
+
const promptLower = input.promptSession.toLowerCase();
|
|
553
|
+
for (const pattern of INTENT_DEVIATION_PATTERNS) {
|
|
554
|
+
const intentMatched = pattern.intentKeywords.some((kw) => promptLower.includes(kw));
|
|
555
|
+
const dangerousMatched = pattern.dangerousActions.some((kw) => matchDangerousAction(actionDetail, kw));
|
|
556
|
+
if (intentMatched && dangerousMatched) {
|
|
557
|
+
return {
|
|
558
|
+
allowed: false,
|
|
559
|
+
reason: pattern.reason,
|
|
560
|
+
riskLevel: "high",
|
|
561
|
+
riskType: "intent_deviation",
|
|
562
|
+
confidence: 85,
|
|
563
|
+
source: "logic_rule",
|
|
564
|
+
violatedLayers: ["2"]
|
|
565
|
+
};
|
|
566
|
+
}
|
|
460
567
|
}
|
|
461
568
|
}
|
|
462
569
|
}
|
|
463
570
|
}
|
|
464
571
|
const indirectResult = checkIndirectFileAccess(actionType, actionDetail, resolvedCwd);
|
|
465
572
|
if (indirectResult) return indirectResult;
|
|
466
|
-
if (
|
|
573
|
+
if (INLINE_SCRIPT_RE.test(actionDetail)) {
|
|
574
|
+
} else if (containsPrivilegeEscalation(actionDetail)) {
|
|
467
575
|
const detailAfterSudo = actionDetail.replace(/(^|\|\||&&|\s)sudo\s+/g, " ");
|
|
468
576
|
const isAdminCmd = ["apt", "systemctl", "yum", "dnf", "pacman", "zypper", "apk"].some((kw) => detailAfterSudo.includes(kw));
|
|
469
577
|
if (!isAdminCmd) {
|
|
@@ -493,6 +601,9 @@ function extractFilePaths(text) {
|
|
|
493
601
|
const paths = [];
|
|
494
602
|
const unixPaths = text.match(/(?:^|\s)(\/[^\s;|&><'"]+)/g);
|
|
495
603
|
if (unixPaths) paths.push(...unixPaths.map((p) => p.trim()));
|
|
604
|
+
const textNoQuotes = text.replace(/'[^']*'|"[^"]*"/g, "");
|
|
605
|
+
const kvPaths = textNoQuotes.match(/(?:=)(\/[^\s;|&><'"]+)/g);
|
|
606
|
+
if (kvPaths) paths.push(...kvPaths.map((p) => p.replace(/^=/, "")));
|
|
496
607
|
const quotedPaths = text.match(/["']\/([^"']+)["']/g);
|
|
497
608
|
if (quotedPaths) paths.push(...quotedPaths.map((p) => p.replace(/["']/g, "")));
|
|
498
609
|
return [...new Set(paths)];
|
|
@@ -503,8 +614,74 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
|
|
|
503
614
|
const isBashLike = actionType === "bash" || actionType === "executebash" || actionType === "terminal-executebash";
|
|
504
615
|
const isWrite = isBashLike ? isWriteOperation(actionType, actionDetail) : WRITE_KEYWORDS.some((kw) => actionType.includes(kw));
|
|
505
616
|
if (!isWrite) return null;
|
|
506
|
-
const
|
|
507
|
-
if (
|
|
617
|
+
const WRITE_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "tee", "dd", "truncate", "mkfs", "fdisk", "parted"]);
|
|
618
|
+
if (isBashLike) {
|
|
619
|
+
const detailNoCompare = actionDetail.replace(/<=>|>=|<=|<>|=>/g, " ");
|
|
620
|
+
const detailNoDevnull = detailNoCompare.replace(DEVNULL_REDIRECT_RE, "");
|
|
621
|
+
const detailUnquotedRedirect = detailNoDevnull.replace(
|
|
622
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
623
|
+
(_m, _q, p) => ">" + p
|
|
624
|
+
);
|
|
625
|
+
const detailNoQuotes = detailUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, "");
|
|
626
|
+
const hasRedirectWrite = />{1,2}\s*(?!&)\S/.test(detailNoQuotes);
|
|
627
|
+
let hasWriteSeg = hasRedirectWrite;
|
|
628
|
+
if (!hasWriteSeg) {
|
|
629
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
630
|
+
const trimmed = seg.trim();
|
|
631
|
+
if (!trimmed) continue;
|
|
632
|
+
const fw = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
|
|
633
|
+
if (WRITE_CMDS.has(fw) || /\bsed\s+-[a-zA-Z]*i/i.test(trimmed)) {
|
|
634
|
+
hasWriteSeg = true;
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
if ((fw === "wget" || fw === "curl") && (/\s(?:-O|--output-document|--output)(?:=|\s|$)/i.test(trimmed) || /\s-O\S/.test(trimmed))) {
|
|
638
|
+
hasWriteSeg = true;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
if (fw === "rsync" && !/--list-only|--dry-run/i.test(trimmed) && !/(^|[^a-zA-Z])-n([^a-zA-Z]|$)/.test(trimmed)) {
|
|
642
|
+
hasWriteSeg = true;
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!hasWriteSeg) return null;
|
|
648
|
+
}
|
|
649
|
+
const SOURCE_ARG_CMDS = /* @__PURE__ */ new Set(["cp", "mv", "install", "rsync"]);
|
|
650
|
+
const REDIRECT_TARGET_RE = new RegExp(`>{1,2}\\s*(?!&)(?:'(/[^']+)'|"(/[^\\"]+)"|(/[^\\s;|&><'\\"]+))`);
|
|
651
|
+
const targetPaths = [];
|
|
652
|
+
for (const seg of actionDetail.split(/&&|\|\||;|\n/)) {
|
|
653
|
+
const trimmed = seg.trim();
|
|
654
|
+
if (!trimmed) continue;
|
|
655
|
+
const firstWord = trimmed.split(/\s+/)[0]?.toLowerCase() || "";
|
|
656
|
+
if (firstWord === "cd") {
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (READ_ONLY_COMMANDS.has(firstWord)) {
|
|
660
|
+
const segUnquoted = trimmed.replace(new RegExp(`>{1,2}\\s*(['"])([~./][^'"]+)\\1`, "g"), (_m, _q, p) => ">" + p);
|
|
661
|
+
const segNoDevnull = segUnquoted.replace(DEVNULL_REDIRECT_RE, "");
|
|
662
|
+
const segNoQuotes = segNoDevnull.replace(/'[^']*'|"[^"]*"/g, "");
|
|
663
|
+
const m = segNoQuotes.match(REDIRECT_TARGET_RE);
|
|
664
|
+
if (m) targetPaths.push(m[1] || m[2] || m[3]);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const segPaths = extractFilePaths(trimmed);
|
|
668
|
+
const segPathsFiltered = firstWord === "wget" || firstWord === "curl" ? segPaths.filter((p) => p !== "/dev/null") : segPaths;
|
|
669
|
+
if (SOURCE_ARG_CMDS.has(firstWord)) {
|
|
670
|
+
if (segPathsFiltered.length > 1) targetPaths.push(...segPathsFiltered.slice(1));
|
|
671
|
+
} else if (firstWord === "dd") {
|
|
672
|
+
const ifSourceMatches = trimmed.match(/\bif=(\/[^\s;|&><'"]+)/g);
|
|
673
|
+
const ifSourcePaths = new Set(ifSourceMatches ? ifSourceMatches.map((m) => m.replace(/^if=/, "")) : []);
|
|
674
|
+
const devNullMatches = trimmed.match(/\bof=(\/dev\/\S+)/g);
|
|
675
|
+
const devNullTargets = new Set(devNullMatches ? devNullMatches.map((m) => m.replace(/^of=/, "")) : []);
|
|
676
|
+
for (const sp of segPaths) {
|
|
677
|
+
if (!ifSourcePaths.has(sp) && !devNullTargets.has(sp)) targetPaths.push(sp);
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
targetPaths.push(...segPathsFiltered);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const uniqueTargetPaths = [...new Set(targetPaths)];
|
|
684
|
+
if (uniqueTargetPaths.length === 0) return null;
|
|
508
685
|
const historyReadPaths = /* @__PURE__ */ new Set();
|
|
509
686
|
for (const action of actionHistory) {
|
|
510
687
|
const histDetail = action.actionDetail.toLowerCase();
|
|
@@ -517,7 +694,7 @@ function checkReadBeforeWrite(actionType, actionDetail, actionHistory, reason, c
|
|
|
517
694
|
}
|
|
518
695
|
}
|
|
519
696
|
const unreadPaths = [];
|
|
520
|
-
for (const path3 of
|
|
697
|
+
for (const path3 of uniqueTargetPaths) {
|
|
521
698
|
const hasRead = [...historyReadPaths].some(
|
|
522
699
|
(rp) => path3 === rp || path3.startsWith(rp) || rp.startsWith(path3)
|
|
523
700
|
);
|
|
@@ -580,6 +757,18 @@ function checkUserDeletionConsent(actionType, actionDetail, actionHistory) {
|
|
|
580
757
|
violatedLayers: ["2"]
|
|
581
758
|
};
|
|
582
759
|
}
|
|
760
|
+
function containsPrivilegeEscalation(actionDetail) {
|
|
761
|
+
const ad = actionDetail.toLowerCase();
|
|
762
|
+
for (const seg of ad.split(/&&|\|\||;|\n/)) {
|
|
763
|
+
const trimmed = seg.trim();
|
|
764
|
+
if (!trimmed) continue;
|
|
765
|
+
const parts = trimmed.split(/\s+/);
|
|
766
|
+
const fw = parts[0]?.toLowerCase() || "";
|
|
767
|
+
if (fw === "sudo" || fw === "su" || fw === "setuid") return true;
|
|
768
|
+
if (fw === "chmod" && /\bu\s*\+\s*s\b/.test(trimmed)) return true;
|
|
769
|
+
}
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
583
772
|
function checkIndirectFileAccess(actionType, actionDetail, cwd) {
|
|
584
773
|
if (actionType !== "bash" && actionType !== "executebash" && actionType !== "terminal-executebash") {
|
|
585
774
|
return null;
|
|
@@ -684,12 +873,13 @@ var init_logic_rules = __esm({
|
|
|
684
873
|
{ path: "/sys/", riskLevel: "high", desc: "sysfs \u5185\u6838\u63A5\u53E3" },
|
|
685
874
|
// 设备文件(/dev/null、/dev/zero、/dev/urandom 已移除,误报率极高)
|
|
686
875
|
{ path: "/dev/random", riskLevel: "high", desc: "\u963B\u585E\u5F0F\u968F\u673A\u6570\u8BBE\u5907\uFF08\u8BFB\u53D6\u8017\u5C3D\u71B5\u6C60\uFF09" },
|
|
687
|
-
{ path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907" },
|
|
876
|
+
{ path: "/dev/mem", riskLevel: "critical", desc: "\u7269\u7406\u5185\u5B58\u8BBF\u95EE\u8BBE\u5907\uFF08\u8BFB\u5199\u5747\u5371\u9669\uFF09", credential: true },
|
|
688
877
|
{ path: "/dev/kmsg", riskLevel: "high", desc: "\u5185\u6838\u6D88\u606F\u7F13\u51B2\u533A" },
|
|
689
878
|
// SSH 主机私钥
|
|
690
|
-
|
|
691
|
-
{ path: "/etc/ssh/
|
|
692
|
-
{ path: "/etc/ssh/
|
|
879
|
+
// SSH 主机私钥(排除 .pub 公钥变体,对齐 Python .ssh/id_rsa(?!\.pub) 范式)
|
|
880
|
+
{ path: "/etc/ssh/ssh_host_rsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A RSA \u79C1\u94A5", credential: true },
|
|
881
|
+
{ path: "/etc/ssh/ssh_host_ed25519_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A Ed25519 \u79C1\u94A5", credential: true },
|
|
882
|
+
{ path: "/etc/ssh/ssh_host_ecdsa_key(?!\\.pub)", riskLevel: "critical", desc: "SSH \u4E3B\u673A ECDSA \u79C1\u94A5", credential: true }
|
|
693
883
|
];
|
|
694
884
|
HOME_SENSITIVE_PATHS = [
|
|
695
885
|
{ path: ".ssh/id_rsa", riskLevel: "critical", desc: "SSH \u79C1\u94A5", credential: true },
|
|
@@ -730,7 +920,7 @@ var init_logic_rules = __esm({
|
|
|
730
920
|
"cat"
|
|
731
921
|
]);
|
|
732
922
|
NON_WRITE_TOOL_PREFIXES = ["memory-", "skill-manager-", "mailbox-", "app-"];
|
|
733
|
-
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"];
|
|
923
|
+
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"];
|
|
734
924
|
WRITE_COMMAND_RES = [
|
|
735
925
|
/\brm\b/,
|
|
736
926
|
/\bunlink\b/,
|
|
@@ -776,6 +966,12 @@ var init_logic_rules = __esm({
|
|
|
776
966
|
"nl",
|
|
777
967
|
"tac",
|
|
778
968
|
"rev",
|
|
969
|
+
// NEW-FP-I: echo/printf 是文本输出命令,参数是字面量非写目标。加入此集让
|
|
970
|
+
// read_before_write 的 echo 段走只读命令分支(只提取重定向目标,不提取参数路径),
|
|
971
|
+
// 避免 echo "cat /etc/shadow" > /tmp/x.sh 的引号内 /etc/shadow 被当写目标误报。
|
|
972
|
+
// echo "x" > /tmp/x.sh 的重定向目标 /tmp/x.sh 仍被提取(真实写不漏报)。
|
|
973
|
+
"echo",
|
|
974
|
+
"printf",
|
|
779
975
|
"lsblk",
|
|
780
976
|
"blockdev",
|
|
781
977
|
"smartctl",
|
|
@@ -828,7 +1024,7 @@ var init_logic_rules = __esm({
|
|
|
828
1024
|
"modinfo",
|
|
829
1025
|
"sysctl"
|
|
830
1026
|
]);
|
|
831
|
-
DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b
|
|
1027
|
+
DEVNULL_REDIRECT_RE = /(?:2|&|1)?\s*>\s*\/dev\/null\b/g;
|
|
832
1028
|
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"];
|
|
833
1029
|
CREATE_KEYWORDS = ["\u521B\u5EFA", "\u65B0\u5EFA", "create", "new file", "touch"];
|
|
834
1030
|
PASSWORD_MODIFY_PATTERNS = [
|
|
@@ -878,7 +1074,7 @@ var init_logic_rules = __esm({
|
|
|
878
1074
|
// src/engine/skill-loader.ts
|
|
879
1075
|
import { readFileSync, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
|
|
880
1076
|
import { resolve, dirname } from "path";
|
|
881
|
-
import { fileURLToPath as
|
|
1077
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
882
1078
|
function loadAllSkills() {
|
|
883
1079
|
if (cachedSkills) return cachedSkills;
|
|
884
1080
|
cachedSkills = {};
|
|
@@ -949,7 +1145,7 @@ var init_skill_loader = __esm({
|
|
|
949
1145
|
"src/engine/skill-loader.ts"() {
|
|
950
1146
|
"use strict";
|
|
951
1147
|
init_esm_shims();
|
|
952
|
-
__filename2 =
|
|
1148
|
+
__filename2 = fileURLToPath3(import.meta.url);
|
|
953
1149
|
__dirname2 = dirname(__filename2);
|
|
954
1150
|
SKILLS_DIR = resolve(__dirname2, "../skills");
|
|
955
1151
|
SKILLS_DIR_FALLBACK = resolve(__dirname2, "../../../agent_moss/skills");
|
|
@@ -1024,7 +1220,7 @@ __export(runtime_config_exports, {
|
|
|
1024
1220
|
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, unlinkSync, readdirSync as readdirSync3 } from "fs";
|
|
1025
1221
|
import { join } from "path";
|
|
1026
1222
|
import { homedir } from "os";
|
|
1027
|
-
import { fileURLToPath as
|
|
1223
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
1028
1224
|
import { dirname as dirname2 } from "path";
|
|
1029
1225
|
function getRuntimeConfigPath() {
|
|
1030
1226
|
const envPath = process.env[ENV_RUNTIME_CONFIG_PATH];
|
|
@@ -1880,7 +2076,7 @@ var init_runtime_config = __esm({
|
|
|
1880
2076
|
init_heuristic();
|
|
1881
2077
|
init_logic_rules();
|
|
1882
2078
|
init_skill_loader();
|
|
1883
|
-
__dirname3 = dirname2(
|
|
2079
|
+
__dirname3 = dirname2(fileURLToPath4(import.meta.url));
|
|
1884
2080
|
RUNTIME_CONFIG_DIR = join(homedir(), ".config", "agentmoss");
|
|
1885
2081
|
RUNTIME_CONFIG_PATH = join(RUNTIME_CONFIG_DIR, "agent_moss_runtime.json");
|
|
1886
2082
|
ENV_RUNTIME_CONFIG_PATH = "AGENT_MOSS_RUNTIME_CONFIG_PATH";
|
|
@@ -1903,7 +2099,7 @@ var init_runtime_config = __esm({
|
|
|
1903
2099
|
// src/agents/index.ts
|
|
1904
2100
|
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
1905
2101
|
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
1906
|
-
import { fileURLToPath as
|
|
2102
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1907
2103
|
function resolveAgentsDir() {
|
|
1908
2104
|
if (existsSync4(AGENTS_DIR_PROD)) return AGENTS_DIR_PROD;
|
|
1909
2105
|
if (existsSync4(AGENTS_DIR_DEV_FALLBACK)) return AGENTS_DIR_DEV_FALLBACK;
|
|
@@ -1952,7 +2148,7 @@ var init_agents = __esm({
|
|
|
1952
2148
|
"src/agents/index.ts"() {
|
|
1953
2149
|
"use strict";
|
|
1954
2150
|
init_esm_shims();
|
|
1955
|
-
__filename3 =
|
|
2151
|
+
__filename3 = fileURLToPath5(import.meta.url);
|
|
1956
2152
|
__dirname4 = dirname3(__filename3);
|
|
1957
2153
|
AGENTS_DIR_PROD = resolve2(__dirname4, "agents");
|
|
1958
2154
|
AGENTS_DIR_DEV_FALLBACK = resolve2(__dirname4, "../../../agent_moss/agents");
|
|
@@ -1972,19 +2168,25 @@ function runHeuristicCheck(input) {
|
|
|
1972
2168
|
const re = new RegExp(rule.pattern);
|
|
1973
2169
|
if (re.test(command)) {
|
|
1974
2170
|
if (rule.action === "Deny") {
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2171
|
+
return {
|
|
2172
|
+
allowed: false,
|
|
2173
|
+
reason: `\u547D\u4E2D\u7528\u6237\u654F\u611F\u52A8\u4F5C\u89C4\u5219: ${rule.description || rule.pattern}`,
|
|
2174
|
+
riskLevel: rule.severity,
|
|
2175
|
+
riskType: "sensitive_action",
|
|
2176
|
+
confidence: 90,
|
|
2177
|
+
source: "heuristic",
|
|
2178
|
+
violatedLayers: ["1.1"]
|
|
2179
|
+
};
|
|
1980
2180
|
}
|
|
1981
2181
|
}
|
|
1982
2182
|
} catch {
|
|
1983
2183
|
}
|
|
1984
2184
|
}
|
|
2185
|
+
const scanDetail = stripLiteralTextSegments(input.aNext.actionDetail);
|
|
2186
|
+
const scanCommand = `${input.aNext.actionType}: ${scanDetail}`;
|
|
1985
2187
|
let topRiskType = "";
|
|
1986
2188
|
for (const rule of DANGEROUS_PATTERNS) {
|
|
1987
|
-
if (rule.pattern.test(
|
|
2189
|
+
if (rule.pattern.test(scanCommand)) {
|
|
1988
2190
|
matched.push(rule.description);
|
|
1989
2191
|
if (severityRank(rule.severity) > severityRank(highestSeverity)) {
|
|
1990
2192
|
highestSeverity = rule.severity;
|
|
@@ -1998,7 +2200,7 @@ function runHeuristicCheck(input) {
|
|
|
1998
2200
|
for (const rule of agentExt.extra_command_patterns) {
|
|
1999
2201
|
try {
|
|
2000
2202
|
const re = new RegExp(rule.pattern, "i");
|
|
2001
|
-
if (re.test(
|
|
2203
|
+
if (re.test(scanCommand)) {
|
|
2002
2204
|
matched.push(`agent \u4E13\u5C5E\u89C4\u5219: ${rule.reason}`);
|
|
2003
2205
|
if (severityRank(rule.risk_level) > severityRank(highestSeverity)) {
|
|
2004
2206
|
highestSeverity = rule.risk_level;
|
|
@@ -2022,14 +2224,12 @@ function runHeuristicCheck(input) {
|
|
|
2022
2224
|
const actionTypeL = input.aNext.actionType.toLowerCase();
|
|
2023
2225
|
const detailL = input.aNext.actionDetail.toLowerCase();
|
|
2024
2226
|
const isExcludedType = SENSITIVE_EXCLUDED_PREFIXES.some((p) => actionTypeL.startsWith(p));
|
|
2227
|
+
const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
|
|
2228
|
+
if (dangerousResult) return dangerousResult;
|
|
2025
2229
|
if (!isExcludedType) {
|
|
2026
2230
|
const pathResult = checkSensitivePath(detailL, input.cwd || "/", agentExt);
|
|
2027
2231
|
if (pathResult) return pathResult;
|
|
2028
2232
|
}
|
|
2029
|
-
const dangerousResult = checkDangerousPatterns(actionTypeL, detailL);
|
|
2030
|
-
if (dangerousResult) return dangerousResult;
|
|
2031
|
-
const lateralResult = checkLateralMovement(detailL);
|
|
2032
|
-
if (lateralResult) return lateralResult;
|
|
2033
2233
|
if (matched.length > 0) {
|
|
2034
2234
|
return {
|
|
2035
2235
|
allowed: false,
|
|
@@ -2042,6 +2242,8 @@ function runHeuristicCheck(input) {
|
|
|
2042
2242
|
// 对齐 py coordinator.py:165(L1.1 = 命中高危命令模式/注入关键词)
|
|
2043
2243
|
};
|
|
2044
2244
|
}
|
|
2245
|
+
const lateralResult = checkLateralMovement(detailL);
|
|
2246
|
+
if (lateralResult) return lateralResult;
|
|
2045
2247
|
return null;
|
|
2046
2248
|
}
|
|
2047
2249
|
function checkInjectionKeywords(text, extraKeywords) {
|
|
@@ -2064,6 +2266,68 @@ function isInlineScriptCommand(actionDetail) {
|
|
|
2064
2266
|
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;
|
|
2065
2267
|
return re.test(actionDetail);
|
|
2066
2268
|
}
|
|
2269
|
+
function stripLiteralTextSegments(actionDetail) {
|
|
2270
|
+
const writeScriptExec = />\s*[/~]\S*\.(sh|py|pl|rb)\b/.test(actionDetail) && /(?:&&|\|\||;|\n|\|)\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)/.test(actionDetail);
|
|
2271
|
+
const pipeToInterpreter = /\|\s*(?:bash|sh|zsh|dash|python|perl|ruby|node)\b/.test(actionDetail);
|
|
2272
|
+
if (writeScriptExec || pipeToInterpreter) {
|
|
2273
|
+
return actionDetail;
|
|
2274
|
+
}
|
|
2275
|
+
const resultParts = [];
|
|
2276
|
+
for (const seg of splitCommandSegments(actionDetail)) {
|
|
2277
|
+
const fw = (seg.trim().split(/\s+/)[0] || "").toLowerCase();
|
|
2278
|
+
if (fw === "echo" || fw === "printf") {
|
|
2279
|
+
const segUnquotedRedirect = seg.replace(
|
|
2280
|
+
/>{1,2}\s*(['"])([~./][^'"]+)\1/g,
|
|
2281
|
+
(_m, _q, p) => ">" + p
|
|
2282
|
+
);
|
|
2283
|
+
resultParts.push(segUnquotedRedirect.replace(/'[^']*'|"[^"]*"/g, ""));
|
|
2284
|
+
} else {
|
|
2285
|
+
resultParts.push(seg);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
return resultParts.join("");
|
|
2289
|
+
}
|
|
2290
|
+
function splitCommandSegments(actionDetail) {
|
|
2291
|
+
const segments = [];
|
|
2292
|
+
let cur = "";
|
|
2293
|
+
let quote = null;
|
|
2294
|
+
for (let i = 0; i < actionDetail.length; i++) {
|
|
2295
|
+
const ch = actionDetail[i];
|
|
2296
|
+
if (quote) {
|
|
2297
|
+
cur += ch;
|
|
2298
|
+
if (ch === quote) quote = null;
|
|
2299
|
+
continue;
|
|
2300
|
+
}
|
|
2301
|
+
if (ch === "'" || ch === '"') {
|
|
2302
|
+
quote = ch;
|
|
2303
|
+
cur += ch;
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
2306
|
+
if (ch === ";" || ch === "\n") {
|
|
2307
|
+
segments.push(cur);
|
|
2308
|
+
segments.push(ch);
|
|
2309
|
+
cur = "";
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
if (ch === "&" && actionDetail[i + 1] === "&") {
|
|
2313
|
+
segments.push(cur);
|
|
2314
|
+
segments.push("&&");
|
|
2315
|
+
cur = "";
|
|
2316
|
+
i++;
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
if (ch === "|" && actionDetail[i + 1] === "|") {
|
|
2320
|
+
segments.push(cur);
|
|
2321
|
+
segments.push("||");
|
|
2322
|
+
cur = "";
|
|
2323
|
+
i++;
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
2326
|
+
cur += ch;
|
|
2327
|
+
}
|
|
2328
|
+
if (cur) segments.push(cur);
|
|
2329
|
+
return segments;
|
|
2330
|
+
}
|
|
2067
2331
|
function severityRank(s) {
|
|
2068
2332
|
const ranks = { low: 1, medium: 2, high: 3, critical: 4 };
|
|
2069
2333
|
return ranks[s] || 0;
|
|
@@ -2071,13 +2335,18 @@ function severityRank(s) {
|
|
|
2071
2335
|
function checkSensitivePath(command, cwd, agentExt) {
|
|
2072
2336
|
const expandedCwd = cwd.replace(/^~/, process.env.HOME || "/home/user");
|
|
2073
2337
|
const isWriteOpFlag = isWriteOperation("", command);
|
|
2338
|
+
const pathScanText = stripLiteralTextSegments(command);
|
|
2074
2339
|
const pathMatches = (text, path3) => {
|
|
2075
|
-
const
|
|
2340
|
+
const parts = path3.split(/(\(\?!.*?\))/);
|
|
2341
|
+
const escaped = parts.map((part) => {
|
|
2342
|
+
if (part.startsWith("(?!")) return part;
|
|
2343
|
+
return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2344
|
+
}).join("");
|
|
2076
2345
|
const re = path3.startsWith(".") || path3.startsWith("/") ? new RegExp(`(?:^|[\\s/\\\\'"])${escaped}\\b`, "i") : new RegExp(`\\b${escaped}\\b`, "i");
|
|
2077
2346
|
return re.test(text);
|
|
2078
2347
|
};
|
|
2079
2348
|
for (const sp of ROOT_SENSITIVE_PATHS) {
|
|
2080
|
-
if (pathMatches(
|
|
2349
|
+
if (pathMatches(pathScanText, sp.path.toLowerCase())) {
|
|
2081
2350
|
if (sp.path === "/dev/random") {
|
|
2082
2351
|
if (isWriteOpFlag) {
|
|
2083
2352
|
continue;
|
|
@@ -2111,7 +2380,7 @@ function checkSensitivePath(command, cwd, agentExt) {
|
|
|
2111
2380
|
const expanded = `${expandedCwd}/${sp.path}`;
|
|
2112
2381
|
const pathRe = sp.path.endsWith(".pub") ? new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(sp.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
|
|
2113
2382
|
const expandedRe = sp.path.endsWith(".pub") ? new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i") : new RegExp(expanded.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?!\\.pub)", "i");
|
|
2114
|
-
if (pathRe.test(
|
|
2383
|
+
if (pathRe.test(pathScanText) || expandedRe.test(pathScanText)) {
|
|
2115
2384
|
if (!isWriteOpFlag && !sp.credential) {
|
|
2116
2385
|
continue;
|
|
2117
2386
|
}
|
|
@@ -2182,8 +2451,10 @@ function checkSensitivePath(command, cwd, agentExt) {
|
|
|
2182
2451
|
}
|
|
2183
2452
|
function checkDangerousPatterns(actionType, actionDetail) {
|
|
2184
2453
|
if (actionType === "file_write" || actionType === "file_edit") return null;
|
|
2185
|
-
const TEMP_DIRS = ["/tmp
|
|
2186
|
-
const isTempPath = TEMP_DIRS.some(
|
|
2454
|
+
const TEMP_DIRS = ["/tmp", "/var/tmp", "/run", "/dev/shm"];
|
|
2455
|
+
const isTempPath = TEMP_DIRS.some(
|
|
2456
|
+
(td) => new RegExp(`(?:^|[\\s/])${td.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[\\s/]|$)`).test(actionDetail)
|
|
2457
|
+
);
|
|
2187
2458
|
if (!isTempPath && (actionDetail.includes("*") || actionDetail.includes("?")) && ["rm ", "rm ", "del ", "del ", "delete", "remove ", "remove ", "\u5220\u9664", "\u79FB\u9664"].some((kw) => actionDetail.includes(kw))) {
|
|
2188
2459
|
return {
|
|
2189
2460
|
allowed: false,
|
|
@@ -2195,7 +2466,22 @@ function checkDangerousPatterns(actionType, actionDetail) {
|
|
|
2195
2466
|
violatedLayers: ["1.1"]
|
|
2196
2467
|
};
|
|
2197
2468
|
}
|
|
2198
|
-
|
|
2469
|
+
const findWriteMatch = actionDetail.match(
|
|
2470
|
+
/\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
|
|
2471
|
+
);
|
|
2472
|
+
if (findWriteMatch) {
|
|
2473
|
+
const cmdHit = findWriteMatch[1] || "xargs";
|
|
2474
|
+
return {
|
|
2475
|
+
allowed: false,
|
|
2476
|
+
reason: `\u68C0\u6D4B\u5230 find \u6279\u91CF\u6267\u884C\u5199\u64CD\u4F5C (${cmdHit})\uFF0C\u53EF\u80FD\u6279\u91CF\u4FEE\u6539/\u5220\u9664\u6587\u4EF6`,
|
|
2477
|
+
riskLevel: "high",
|
|
2478
|
+
riskType: "script_execution",
|
|
2479
|
+
confidence: 90,
|
|
2480
|
+
source: "heuristic",
|
|
2481
|
+
violatedLayers: ["1.1"]
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
if (/>\s*\/etc\//.test(actionDetail) || />\s*\/boot\//.test(actionDetail)) {
|
|
2199
2485
|
return {
|
|
2200
2486
|
allowed: false,
|
|
2201
2487
|
reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u8986\u76D6\u5199\u5165\u5173\u952E\u7CFB\u7EDF\u76EE\u5F55",
|
|
@@ -2206,17 +2492,95 @@ function checkDangerousPatterns(actionType, actionDetail) {
|
|
|
2206
2492
|
violatedLayers: ["1.1"]
|
|
2207
2493
|
};
|
|
2208
2494
|
}
|
|
2495
|
+
if (/>\s*\/dev\/(sd|hd|nvme|vd|loop|mem|kmsg|port|zero|random|urandom|full|core)/i.test(actionDetail)) {
|
|
2496
|
+
return {
|
|
2497
|
+
allowed: false,
|
|
2498
|
+
reason: "\u68C0\u6D4B\u5230\u91CD\u5B9A\u5411\u5199\u5165\u8BBE\u5907\u6587\u4EF6",
|
|
2499
|
+
riskLevel: "high",
|
|
2500
|
+
riskType: "file_access",
|
|
2501
|
+
confidence: 90,
|
|
2502
|
+
source: "heuristic",
|
|
2503
|
+
violatedLayers: ["1.1"]
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2209
2506
|
return null;
|
|
2210
2507
|
}
|
|
2211
2508
|
function extractRemoteHost(command) {
|
|
2212
|
-
const
|
|
2213
|
-
|
|
2509
|
+
const tokens = command.split(/\s+/).map((t) => t.replace(/^["']|["']$/g, ""));
|
|
2510
|
+
for (const tok of tokens) {
|
|
2511
|
+
const am = tok.match(/^([\w.-]+)@([\w.-]+)(:.*)?$/);
|
|
2512
|
+
if (am && am[2] && !isLocalHost(am[2])) {
|
|
2513
|
+
return `${am[1]}@${am[2]}`;
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
for (const tok of tokens) {
|
|
2517
|
+
const hm = tok.match(/^([\w.-]+):\/(.*)?$/);
|
|
2518
|
+
if (hm && hm[1] && !isLocalHost(hm[1])) {
|
|
2519
|
+
return hm[1];
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const skipValueOpts = /* @__PURE__ */ new Set([
|
|
2523
|
+
"-p",
|
|
2524
|
+
"-l",
|
|
2525
|
+
"-i",
|
|
2526
|
+
"-o",
|
|
2527
|
+
"-b",
|
|
2528
|
+
"-c",
|
|
2529
|
+
"-m",
|
|
2530
|
+
"-F",
|
|
2531
|
+
"-e",
|
|
2532
|
+
"-S",
|
|
2533
|
+
"-W",
|
|
2534
|
+
"-J",
|
|
2535
|
+
"-I",
|
|
2536
|
+
"-T",
|
|
2537
|
+
"-B",
|
|
2538
|
+
"-L",
|
|
2539
|
+
"-R",
|
|
2540
|
+
"-D",
|
|
2541
|
+
"-E",
|
|
2542
|
+
"-M",
|
|
2543
|
+
"-O",
|
|
2544
|
+
"-P",
|
|
2545
|
+
"-q"
|
|
2546
|
+
]);
|
|
2547
|
+
let foundCmd = false;
|
|
2548
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2549
|
+
const t = tokens[i];
|
|
2550
|
+
if (t === "ssh" || t === "scp" || t === "rsync") {
|
|
2551
|
+
foundCmd = true;
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
if (!foundCmd) continue;
|
|
2555
|
+
if (t.startsWith("-")) {
|
|
2556
|
+
if (skipValueOpts.has(t) && i + 1 < tokens.length) {
|
|
2557
|
+
i++;
|
|
2558
|
+
}
|
|
2559
|
+
continue;
|
|
2560
|
+
}
|
|
2561
|
+
if (t.startsWith("/")) continue;
|
|
2562
|
+
return t;
|
|
2563
|
+
}
|
|
2564
|
+
return null;
|
|
2214
2565
|
}
|
|
2215
2566
|
function isLocalHost(host) {
|
|
2216
2567
|
const localNames = ["localhost", "127.0.0.1", "::1", process.env.HOSTNAME || "", "0.0.0.0"];
|
|
2217
2568
|
return localNames.includes(host.replace(/.*@/, ""));
|
|
2218
2569
|
}
|
|
2219
2570
|
function checkLateralMovement(actionDetail) {
|
|
2571
|
+
if (/\b(?:python|python3|perl|ruby|node|php|lua)\b\s+.*-[cer][a-zA-Z]*\s/.test(actionDetail)) return null;
|
|
2572
|
+
const segments = actionDetail.split(/&&|\|\||;|\n/).map((s) => s.trim()).filter(Boolean);
|
|
2573
|
+
let hasRealCall = false;
|
|
2574
|
+
for (const seg of segments) {
|
|
2575
|
+
const parts = seg.split(/\s+/);
|
|
2576
|
+
const fw = (parts[0] || "").toLowerCase();
|
|
2577
|
+
if (fw === "echo" || fw === "printf") continue;
|
|
2578
|
+
if (/(?:ssh|scp|rsync)\s+/.test(seg)) {
|
|
2579
|
+
hasRealCall = true;
|
|
2580
|
+
break;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if (!hasRealCall) return null;
|
|
2220
2584
|
if (/(?:ssh|scp|rsync)\s+/.test(actionDetail)) {
|
|
2221
2585
|
const targetHost = extractRemoteHost(actionDetail);
|
|
2222
2586
|
if (targetHost && !isLocalHost(targetHost)) {
|
|
@@ -2316,10 +2680,14 @@ var init_heuristic = __esm({
|
|
|
2316
2680
|
{ 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" },
|
|
2317
2681
|
{ pattern: /:\(\)\s*\{.*:\|:&.*\};:/i, severity: "critical", description: "Fork Bomb", riskType: "resource_exhaustion" },
|
|
2318
2682
|
// --- Critical: remote code execution ---
|
|
2319
|
-
|
|
2683
|
+
// curl|bash 优先匹配 user_rules(high/sensitive_action),见下方 UserRuleMatcher 段;
|
|
2684
|
+
// 非 bash 的 curl|sh/zsh/dash 走 CommandPatternScanner critical。两条分流由
|
|
2685
|
+
// runHeuristicCheck 的规则顺序保证(bash 在前 high,其余 fall-through 到 critical)。
|
|
2686
|
+
{ pattern: /curl\s+.*\|\s*bash/i, severity: "high", description: "curl pipe bash \u6267\u884C (user_rules)", riskType: "sensitive_action" },
|
|
2687
|
+
{ pattern: /curl\s+.*\|\s*(sh|zsh|dash)/i, severity: "critical", description: "curl pipe shell \u6267\u884C", riskType: "script_execution" },
|
|
2320
2688
|
{ pattern: /wget\s+.*\|\s*(bash|sh)/i, severity: "critical", description: "wget pipe shell \u6267\u884C", riskType: "script_execution" },
|
|
2321
2689
|
{ pattern: /eval\s+\$\(curl|eval\s+\$\(wget/i, severity: "critical", description: "eval \u8FDC\u7A0B\u4EE3\u7801\u6267\u884C", riskType: "script_execution" },
|
|
2322
|
-
{ pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "
|
|
2690
|
+
{ pattern: /base64\s+-d\s+.*\|\s*bash/i, severity: "high", description: "base64 \u89E3\u7801\u6267\u884C (user_rules)", riskType: "sensitive_action" },
|
|
2323
2691
|
// --- Critical: reverse shell ---
|
|
2324
2692
|
{ pattern: /\/dev\/tcp\/|bash\s+-i\s+>&/i, severity: "critical", description: "\u53CD\u5F39 Shell", riskType: "script_execution" },
|
|
2325
2693
|
{ pattern: /nc\s+-[el]\s+.*\d{1,5}/i, severity: "high", description: "Netcat \u53CD\u5F39\u8FDE\u63A5", riskType: "lateral_movement" },
|
|
@@ -2330,13 +2698,25 @@ var init_heuristic = __esm({
|
|
|
2330
2698
|
{ pattern: /\.ssh\/id_rsa(?!\.pub)/i, severity: "critical", description: "\u8BBF\u95EE SSH \u79C1\u94A5\u6587\u4EF6", riskType: "file_access" },
|
|
2331
2699
|
// --- Critical: privilege escalation / container escape ---
|
|
2332
2700
|
{ 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" },
|
|
2333
|
-
// ---
|
|
2334
|
-
|
|
2701
|
+
// --- Critical: dangerous permissions / ownership (提权 / 权限敞口) ---
|
|
2702
|
+
// 修正「只拦 777、xx7 漏放」的缺口:
|
|
2703
|
+
// 1) 八进制末位 7:other 位含写+执行敞口(chmod 777/647/007/4777 等);644/755/1700 不命中
|
|
2704
|
+
// 2) 八进制 setuid/setgid 位:首位 4(setuid)/2(setgid) 的 4 位八进制(chmod 4755/2755)
|
|
2705
|
+
// 3) 符号 setuid/setgid:chmod u+s / g+s / a+s
|
|
2706
|
+
// 普通权限修改(chmod 644)不在此列——L2 read_before_write 已豁免 chmod/chown(不写内容),
|
|
2707
|
+
// 危险形态在此 L1 专门拦,以「提权」正确理由拦截,不再靠 RBW 误打误撞。
|
|
2708
|
+
{ 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" },
|
|
2709
|
+
{ 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" },
|
|
2710
|
+
{ 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" },
|
|
2335
2711
|
{ 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" },
|
|
2336
2712
|
// sudo 提权 — 只拦截 sudo 后跟的危险命令,不一刀切拦截所有 sudo
|
|
2337
2713
|
{ 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" },
|
|
2338
2714
|
// --- High: system config overwrite ---
|
|
2715
|
+
// > /etc/ 重定向覆盖:对齐 Python CommandPatternScanner `>\s*/etc/`(不兼容引号路径),
|
|
2716
|
+
// 引号包裹的重定向目标(cat > '/etc/ssh/sshd_config')走 checkSensitivePath 敏感路径 high。
|
|
2339
2717
|
{ pattern: />\s*\/etc\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u7CFB\u7EDF\u914D\u7F6E\u76EE\u5F55", riskType: "file_access" },
|
|
2718
|
+
{ pattern: />\s*\/boot\//i, severity: "critical", description: "\u8986\u76D6\u5199\u5165\u542F\u52A8\u5F15\u5BFC\u76EE\u5F55", riskType: "file_access" },
|
|
2719
|
+
// 注:/proc/ 重定向覆盖走 checkSensitivePath 的 high(内核参数 /proc/sys/),此处不拦 /proc/ 避免遮蔽(对齐 Python)。
|
|
2340
2720
|
// --- High: download script + execute risk ---
|
|
2341
2721
|
{ 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" },
|
|
2342
2722
|
{ pattern: /(?:curl|wget)\s+.*\s+&&?\s*(?:chmod\s+\+x|bash|sh)/i, severity: "high", description: "\u4E0B\u8F7D\u540E\u6267\u884C", riskType: "script_execution" },
|
|
@@ -2352,7 +2732,11 @@ var init_heuristic = __esm({
|
|
|
2352
2732
|
// --- Medium: credential export ---
|
|
2353
2733
|
{ pattern: /export\s+.*(?:TOKEN|KEY|SECRET|PASSWORD)/i, severity: "medium", description: "\u5BFC\u51FA\u654F\u611F\u51ED\u8BC1", riskType: "data_exfiltration" },
|
|
2354
2734
|
// --- High: shell rc file append (persistence) ---
|
|
2735
|
+
// 两条规则:第一条匹配 ~ 与 /home/<user>/ 形态;第二条按文件名兜底,
|
|
2736
|
+
// 覆盖非标准 home 路径(如鸿蒙 /storage/Users/currentUser/.bashrc、
|
|
2737
|
+
// /root/.bashrc),避免按 home 前缀枚举导致漏检持久化后门。
|
|
2355
2738
|
{ 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" },
|
|
2739
|
+
{ 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" },
|
|
2356
2740
|
// --- High: device file write (只拦截重定向写入设备文件,不拦截读取) ---
|
|
2357
2741
|
{ 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" },
|
|
2358
2742
|
// --- High: non-interactive password modification ---
|
|
@@ -2632,7 +3016,7 @@ var require_bindings = __commonJS({
|
|
|
2632
3016
|
init_esm_shims();
|
|
2633
3017
|
var fs4 = __require("fs");
|
|
2634
3018
|
var path3 = __require("path");
|
|
2635
|
-
var
|
|
3019
|
+
var fileURLToPath8 = require_file_uri_to_path();
|
|
2636
3020
|
var join6 = path3.join;
|
|
2637
3021
|
var dirname7 = path3.dirname;
|
|
2638
3022
|
var exists = fs4.accessSync && function(path4) {
|
|
@@ -2744,7 +3128,7 @@ var require_bindings = __commonJS({
|
|
|
2744
3128
|
Error.stackTraceLimit = origSTL;
|
|
2745
3129
|
var fileSchema = "file://";
|
|
2746
3130
|
if (fileName.indexOf(fileSchema) === 0) {
|
|
2747
|
-
fileName =
|
|
3131
|
+
fileName = fileURLToPath8(fileName);
|
|
2748
3132
|
}
|
|
2749
3133
|
return fileName;
|
|
2750
3134
|
};
|
|
@@ -3411,8 +3795,8 @@ init_skill_loader();
|
|
|
3411
3795
|
init_esm_shims();
|
|
3412
3796
|
import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
|
|
3413
3797
|
import { resolve as resolve3, dirname as dirname4 } from "path";
|
|
3414
|
-
import { fileURLToPath as
|
|
3415
|
-
var __filename4 =
|
|
3798
|
+
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3799
|
+
var __filename4 = fileURLToPath6(import.meta.url);
|
|
3416
3800
|
var __dirname5 = dirname4(__filename4);
|
|
3417
3801
|
var TEMPLATES_DIR = resolve3(__dirname5, "../templates");
|
|
3418
3802
|
var TEMPLATES_DIR_FALLBACK = resolve3(__dirname5, "../../../agent_moss/templates");
|
|
@@ -4327,11 +4711,13 @@ async function analyzeWithLLM(prompt, config) {
|
|
|
4327
4711
|
} catch {
|
|
4328
4712
|
}
|
|
4329
4713
|
clearTimeout(totalTimeoutTimer);
|
|
4714
|
+
const riskLevel = parsed.riskLevel ?? parsed.risk_level;
|
|
4715
|
+
const riskType = parsed.riskType ?? parsed.risk_type;
|
|
4330
4716
|
return {
|
|
4331
4717
|
allowed: parsed.allowed !== false,
|
|
4332
4718
|
reason: parsed.reason || "LLM \u5206\u6790\u5B8C\u6210",
|
|
4333
|
-
riskLevel: ["low", "medium", "high", "critical"].includes(
|
|
4334
|
-
riskType:
|
|
4719
|
+
riskLevel: ["low", "medium", "high", "critical"].includes(riskLevel) ? riskLevel : "low",
|
|
4720
|
+
riskType: riskType || "",
|
|
4335
4721
|
confidence: typeof parsed.confidence === "number" ? Math.min(100, Math.max(0, parsed.confidence)) : 70,
|
|
4336
4722
|
source: "llm",
|
|
4337
4723
|
violatedLayers: ["3"]
|
|
@@ -4449,7 +4835,10 @@ async function runAnalysis(input) {
|
|
|
4449
4835
|
if (heuristicResult && !heuristicResult.allowed) {
|
|
4450
4836
|
const skipInlineFileAccess = (isInlineScriptCmd || isLiteralTextCmd) && heuristicResult.riskType === "file_access";
|
|
4451
4837
|
if (!skipInlineFileAccess) {
|
|
4452
|
-
|
|
4838
|
+
const shouldDenyL1 = heuristicResult.riskLevel === "high" || heuristicResult.riskLevel === "critical" || heuristicResult.riskLevel === "medium" && heuristicResult.riskType === "file_access";
|
|
4839
|
+
if (shouldDenyL1) {
|
|
4840
|
+
return heuristicResult;
|
|
4841
|
+
}
|
|
4453
4842
|
}
|
|
4454
4843
|
}
|
|
4455
4844
|
let scriptChain = null;
|
|
@@ -4527,7 +4916,8 @@ async function runAnalysis(input) {
|
|
|
4527
4916
|
) && DANGEROUS_PATTERNS.some(
|
|
4528
4917
|
(p) => (p.severity === "critical" || p.severity === "high") && p.pattern.test(lowerDetail)
|
|
4529
4918
|
);
|
|
4530
|
-
|
|
4919
|
+
const hasRedirectWrite = isWriteOperation(firstCmd, lowerDetail);
|
|
4920
|
+
if (!fullCmdHit && !hasRedirectWrite) {
|
|
4531
4921
|
return {
|
|
4532
4922
|
allowed: true,
|
|
4533
4923
|
reason: `\u5B89\u5168\u7684\u53EA\u8BFB bash \u547D\u4EE4: ${firstCmd}`,
|
|
@@ -4854,8 +5244,8 @@ init_token_stats();
|
|
|
4854
5244
|
import { Hono } from "hono";
|
|
4855
5245
|
import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
|
|
4856
5246
|
import { join as join5, dirname as dirname6 } from "path";
|
|
4857
|
-
import { fileURLToPath as
|
|
4858
|
-
var __dirname6 = dirname6(
|
|
5247
|
+
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
5248
|
+
var __dirname6 = dirname6(fileURLToPath7(import.meta.url));
|
|
4859
5249
|
var STATIC_DIR = join5(__dirname6, "static");
|
|
4860
5250
|
var CONSOLE_TOKEN_ENV = "AGENT_MOSS_CONSOLE_TOKEN";
|
|
4861
5251
|
function getConsoleToken() {
|