@yan162/changewayguard 6.8.25

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.
Files changed (285) hide show
  1. package/LICENSE +21 -0
  2. package/OpenClaw-linux_Mac-Guide-zh.md +89 -0
  3. package/dashboard-dist/api/122.index.js +95 -0
  4. package/dashboard-dist/api/122.index.js.map +1 -0
  5. package/dashboard-dist/api/143.index.js +2734 -0
  6. package/dashboard-dist/api/143.index.js.map +1 -0
  7. package/dashboard-dist/api/154.index.js +4151 -0
  8. package/dashboard-dist/api/154.index.js.map +1 -0
  9. package/dashboard-dist/api/173.index.js +24112 -0
  10. package/dashboard-dist/api/173.index.js.map +1 -0
  11. package/dashboard-dist/api/217.index.js +44 -0
  12. package/dashboard-dist/api/217.index.js.map +1 -0
  13. package/dashboard-dist/api/222.index.js +90 -0
  14. package/dashboard-dist/api/222.index.js.map +1 -0
  15. package/dashboard-dist/api/280.index.js +213 -0
  16. package/dashboard-dist/api/280.index.js.map +1 -0
  17. package/dashboard-dist/api/369.index.js +115 -0
  18. package/dashboard-dist/api/369.index.js.map +1 -0
  19. package/dashboard-dist/api/374.index.js +1896 -0
  20. package/dashboard-dist/api/374.index.js.map +1 -0
  21. package/dashboard-dist/api/424.index.js +135 -0
  22. package/dashboard-dist/api/424.index.js.map +1 -0
  23. package/dashboard-dist/api/445.index.js +3562 -0
  24. package/dashboard-dist/api/445.index.js.map +1 -0
  25. package/dashboard-dist/api/555.index.js +496 -0
  26. package/dashboard-dist/api/555.index.js.map +1 -0
  27. package/dashboard-dist/api/573.index.js +806 -0
  28. package/dashboard-dist/api/573.index.js.map +1 -0
  29. package/dashboard-dist/api/580.index.js +1420 -0
  30. package/dashboard-dist/api/580.index.js.map +1 -0
  31. package/dashboard-dist/api/581.index.js +67 -0
  32. package/dashboard-dist/api/581.index.js.map +1 -0
  33. package/dashboard-dist/api/598.index.js +328 -0
  34. package/dashboard-dist/api/598.index.js.map +1 -0
  35. package/dashboard-dist/api/720.index.js +105 -0
  36. package/dashboard-dist/api/720.index.js.map +1 -0
  37. package/dashboard-dist/api/744.index.js +333 -0
  38. package/dashboard-dist/api/744.index.js.map +1 -0
  39. package/dashboard-dist/api/818.index.js +374 -0
  40. package/dashboard-dist/api/818.index.js.map +1 -0
  41. package/dashboard-dist/api/831.index.js +99 -0
  42. package/dashboard-dist/api/831.index.js.map +1 -0
  43. package/dashboard-dist/api/84.index.js +64 -0
  44. package/dashboard-dist/api/84.index.js.map +1 -0
  45. package/dashboard-dist/api/900.index.js +81 -0
  46. package/dashboard-dist/api/900.index.js.map +1 -0
  47. package/dashboard-dist/api/917.index.js +88 -0
  48. package/dashboard-dist/api/917.index.js.map +1 -0
  49. package/dashboard-dist/api/927.index.js +4250 -0
  50. package/dashboard-dist/api/927.index.js.map +1 -0
  51. package/dashboard-dist/api/948.index.js +64 -0
  52. package/dashboard-dist/api/948.index.js.map +1 -0
  53. package/dashboard-dist/api/982.index.js +67 -0
  54. package/dashboard-dist/api/982.index.js.map +1 -0
  55. package/dashboard-dist/api/99.index.js +1176 -0
  56. package/dashboard-dist/api/99.index.js.map +1 -0
  57. package/dashboard-dist/api/drizzle/sqlite/0000_short_captain_stacy.sql +70 -0
  58. package/dashboard-dist/api/drizzle/sqlite/0001_closed_magus.sql +10 -0
  59. package/dashboard-dist/api/drizzle/sqlite/0002_agent_capability_observation.sql +38 -0
  60. package/dashboard-dist/api/drizzle/sqlite/0003_auth_magic_link.sql +28 -0
  61. package/dashboard-dist/api/drizzle/sqlite/0004_static_scan_fields.sql +8 -0
  62. package/dashboard-dist/api/drizzle/sqlite/0005_gateway_activity.sql +24 -0
  63. package/dashboard-dist/api/drizzle/sqlite/0006_sour_marauders.sql +41 -0
  64. package/dashboard-dist/api/drizzle/sqlite/meta/0000_snapshot.json +460 -0
  65. package/dashboard-dist/api/drizzle/sqlite/meta/0001_snapshot.json +536 -0
  66. package/dashboard-dist/api/drizzle/sqlite/meta/0006_snapshot.json +1249 -0
  67. package/dashboard-dist/api/drizzle/sqlite/meta/_journal.json +55 -0
  68. package/dashboard-dist/api/index.js +28482 -0
  69. package/dashboard-dist/api/index.js.map +1 -0
  70. package/dashboard-dist/api/package.json +16 -0
  71. package/dashboard-dist/api/sourcemap-register.cjs +1 -0
  72. package/dashboard-dist/web/assets/index-BKUfzbIg.js +148 -0
  73. package/dashboard-dist/web/assets/index-rHRH99IQ.css +1 -0
  74. package/dashboard-dist/web/changeway-logo.png +0 -0
  75. package/dashboard-dist/web/favicon.svg +29 -0
  76. package/dashboard-dist/web/index.html +15 -0
  77. package/dashboard-dist/web/logo.svg +16 -0
  78. package/dist/agent/activation.d.ts +21 -0
  79. package/dist/agent/activation.d.ts.map +1 -0
  80. package/dist/agent/activation.js +94 -0
  81. package/dist/agent/activation.js.map +1 -0
  82. package/dist/agent/auth.d.ts +73 -0
  83. package/dist/agent/auth.d.ts.map +1 -0
  84. package/dist/agent/auth.js +363 -0
  85. package/dist/agent/auth.js.map +1 -0
  86. package/dist/agent/behavior-detector.d.ts +150 -0
  87. package/dist/agent/behavior-detector.d.ts.map +1 -0
  88. package/dist/agent/behavior-detector.js +559 -0
  89. package/dist/agent/behavior-detector.js.map +1 -0
  90. package/dist/agent/business-reporter.d.ts +114 -0
  91. package/dist/agent/business-reporter.d.ts.map +1 -0
  92. package/dist/agent/business-reporter.js +359 -0
  93. package/dist/agent/business-reporter.js.map +1 -0
  94. package/dist/agent/config-sync.d.ts +70 -0
  95. package/dist/agent/config-sync.d.ts.map +1 -0
  96. package/dist/agent/config-sync.js +133 -0
  97. package/dist/agent/config-sync.js.map +1 -0
  98. package/dist/agent/config.d.ts +98 -0
  99. package/dist/agent/config.d.ts.map +1 -0
  100. package/dist/agent/config.js +348 -0
  101. package/dist/agent/config.js.map +1 -0
  102. package/dist/agent/content-injection-scanner.d.ts +35 -0
  103. package/dist/agent/content-injection-scanner.d.ts.map +1 -0
  104. package/dist/agent/content-injection-scanner.js +270 -0
  105. package/dist/agent/content-injection-scanner.js.map +1 -0
  106. package/dist/agent/engine-log-writer.d.ts +6 -0
  107. package/dist/agent/engine-log-writer.d.ts.map +1 -0
  108. package/dist/agent/engine-log-writer.js +18 -0
  109. package/dist/agent/engine-log-writer.js.map +1 -0
  110. package/dist/agent/env.d.ts +19 -0
  111. package/dist/agent/env.d.ts.map +1 -0
  112. package/dist/agent/env.js +44 -0
  113. package/dist/agent/env.js.map +1 -0
  114. package/dist/agent/event-reporter.d.ts +87 -0
  115. package/dist/agent/event-reporter.d.ts.map +1 -0
  116. package/dist/agent/event-reporter.js +306 -0
  117. package/dist/agent/event-reporter.js.map +1 -0
  118. package/dist/agent/file-watcher.d.ts +50 -0
  119. package/dist/agent/file-watcher.d.ts.map +1 -0
  120. package/dist/agent/file-watcher.js +135 -0
  121. package/dist/agent/file-watcher.js.map +1 -0
  122. package/dist/agent/fs-utils.d.ts +22 -0
  123. package/dist/agent/fs-utils.d.ts.map +1 -0
  124. package/dist/agent/fs-utils.js +41 -0
  125. package/dist/agent/fs-utils.js.map +1 -0
  126. package/dist/agent/gateway-manager.d.ts +59 -0
  127. package/dist/agent/gateway-manager.d.ts.map +1 -0
  128. package/dist/agent/gateway-manager.js +583 -0
  129. package/dist/agent/gateway-manager.js.map +1 -0
  130. package/dist/agent/hook-types.d.ts +276 -0
  131. package/dist/agent/hook-types.d.ts.map +1 -0
  132. package/dist/agent/hook-types.js +51 -0
  133. package/dist/agent/hook-types.js.map +1 -0
  134. package/dist/agent/http-client.d.ts +19 -0
  135. package/dist/agent/http-client.d.ts.map +1 -0
  136. package/dist/agent/http-client.js +37 -0
  137. package/dist/agent/http-client.js.map +1 -0
  138. package/dist/agent/index.d.ts +8 -0
  139. package/dist/agent/index.d.ts.map +1 -0
  140. package/dist/agent/index.js +8 -0
  141. package/dist/agent/index.js.map +1 -0
  142. package/dist/agent/openclaw-hybrid-audit-changeway.js +1447 -0
  143. package/dist/agent/prompt-gate.d.ts +16 -0
  144. package/dist/agent/prompt-gate.d.ts.map +1 -0
  145. package/dist/agent/prompt-gate.js +58 -0
  146. package/dist/agent/prompt-gate.js.map +1 -0
  147. package/dist/agent/prompt-input.d.ts +9 -0
  148. package/dist/agent/prompt-input.d.ts.map +1 -0
  149. package/dist/agent/prompt-input.js +173 -0
  150. package/dist/agent/prompt-input.js.map +1 -0
  151. package/dist/agent/prompt-output.d.ts +4 -0
  152. package/dist/agent/prompt-output.d.ts.map +1 -0
  153. package/dist/agent/prompt-output.js +19 -0
  154. package/dist/agent/prompt-output.js.map +1 -0
  155. package/dist/agent/runner.d.ts +23 -0
  156. package/dist/agent/runner.d.ts.map +1 -0
  157. package/dist/agent/runner.js +165 -0
  158. package/dist/agent/runner.js.map +1 -0
  159. package/dist/agent/runtime-mode.d.ts +10 -0
  160. package/dist/agent/runtime-mode.d.ts.map +1 -0
  161. package/dist/agent/runtime-mode.js +19 -0
  162. package/dist/agent/runtime-mode.js.map +1 -0
  163. package/dist/agent/sanitizer.d.ts +10 -0
  164. package/dist/agent/sanitizer.d.ts.map +1 -0
  165. package/dist/agent/sanitizer.js +175 -0
  166. package/dist/agent/sanitizer.js.map +1 -0
  167. package/dist/agent/scan-activity.d.ts +19 -0
  168. package/dist/agent/scan-activity.d.ts.map +1 -0
  169. package/dist/agent/scan-activity.js +34 -0
  170. package/dist/agent/scan-activity.js.map +1 -0
  171. package/dist/agent/types.d.ts +177 -0
  172. package/dist/agent/types.d.ts.map +1 -0
  173. package/dist/agent/types.js +5 -0
  174. package/dist/agent/types.js.map +1 -0
  175. package/dist/agent/workspace-scanner.d.ts +35 -0
  176. package/dist/agent/workspace-scanner.d.ts.map +1 -0
  177. package/dist/agent/workspace-scanner.js +137 -0
  178. package/dist/agent/workspace-scanner.js.map +1 -0
  179. package/dist/dashboard-launcher.d.ts +52 -0
  180. package/dist/dashboard-launcher.d.ts.map +1 -0
  181. package/dist/dashboard-launcher.js +363 -0
  182. package/dist/dashboard-launcher.js.map +1 -0
  183. package/dist/gateway/activity.d.ts +52 -0
  184. package/dist/gateway/activity.d.ts.map +1 -0
  185. package/dist/gateway/activity.js +111 -0
  186. package/dist/gateway/activity.js.map +1 -0
  187. package/dist/gateway/config.d.ts +50 -0
  188. package/dist/gateway/config.d.ts.map +1 -0
  189. package/dist/gateway/config.js +200 -0
  190. package/dist/gateway/config.js.map +1 -0
  191. package/dist/gateway/handlers/anthropic.d.ts +12 -0
  192. package/dist/gateway/handlers/anthropic.d.ts.map +1 -0
  193. package/dist/gateway/handlers/anthropic.js +254 -0
  194. package/dist/gateway/handlers/anthropic.js.map +1 -0
  195. package/dist/gateway/handlers/gemini.d.ts +12 -0
  196. package/dist/gateway/handlers/gemini.d.ts.map +1 -0
  197. package/dist/gateway/handlers/gemini.js +101 -0
  198. package/dist/gateway/handlers/gemini.js.map +1 -0
  199. package/dist/gateway/handlers/models.d.ts +4 -0
  200. package/dist/gateway/handlers/models.d.ts.map +1 -0
  201. package/dist/gateway/handlers/models.js +36 -0
  202. package/dist/gateway/handlers/models.js.map +1 -0
  203. package/dist/gateway/handlers/openai.d.ts +16 -0
  204. package/dist/gateway/handlers/openai.d.ts.map +1 -0
  205. package/dist/gateway/handlers/openai.js +254 -0
  206. package/dist/gateway/handlers/openai.js.map +1 -0
  207. package/dist/gateway/index.d.ts +27 -0
  208. package/dist/gateway/index.d.ts.map +1 -0
  209. package/dist/gateway/index.js +290 -0
  210. package/dist/gateway/index.js.map +1 -0
  211. package/dist/gateway/mapping-store.d.ts +38 -0
  212. package/dist/gateway/mapping-store.d.ts.map +1 -0
  213. package/dist/gateway/mapping-store.js +74 -0
  214. package/dist/gateway/mapping-store.js.map +1 -0
  215. package/dist/gateway/restorer.d.ts +63 -0
  216. package/dist/gateway/restorer.d.ts.map +1 -0
  217. package/dist/gateway/restorer.js +284 -0
  218. package/dist/gateway/restorer.js.map +1 -0
  219. package/dist/gateway/sanitizer.d.ts +17 -0
  220. package/dist/gateway/sanitizer.d.ts.map +1 -0
  221. package/dist/gateway/sanitizer.js +228 -0
  222. package/dist/gateway/sanitizer.js.map +1 -0
  223. package/dist/gateway/types.d.ts +53 -0
  224. package/dist/gateway/types.d.ts.map +1 -0
  225. package/dist/gateway/types.js +5 -0
  226. package/dist/gateway/types.js.map +1 -0
  227. package/dist/index.d.ts +19 -0
  228. package/dist/index.d.ts.map +1 -0
  229. package/dist/index.js +2990 -0
  230. package/dist/index.js.map +1 -0
  231. package/dist/memory/index.d.ts +5 -0
  232. package/dist/memory/index.d.ts.map +1 -0
  233. package/dist/memory/index.js +5 -0
  234. package/dist/memory/index.js.map +1 -0
  235. package/dist/memory/store.d.ts +82 -0
  236. package/dist/memory/store.d.ts.map +1 -0
  237. package/dist/memory/store.js +194 -0
  238. package/dist/memory/store.js.map +1 -0
  239. package/dist/platform-client/index.d.ts +63 -0
  240. package/dist/platform-client/index.d.ts.map +1 -0
  241. package/dist/platform-client/index.js +294 -0
  242. package/dist/platform-client/index.js.map +1 -0
  243. package/dist/platform-client/types.d.ts +109 -0
  244. package/dist/platform-client/types.d.ts.map +1 -0
  245. package/dist/platform-client/types.js +3 -0
  246. package/dist/platform-client/types.js.map +1 -0
  247. package/dist/workspace-agents-guide.d.ts +22 -0
  248. package/dist/workspace-agents-guide.d.ts.map +1 -0
  249. package/dist/workspace-agents-guide.js +92 -0
  250. package/dist/workspace-agents-guide.js.map +1 -0
  251. package/dist/workspace-agents-sync.d.ts +24 -0
  252. package/dist/workspace-agents-sync.d.ts.map +1 -0
  253. package/dist/workspace-agents-sync.js +41 -0
  254. package/dist/workspace-agents-sync.js.map +1 -0
  255. package/dist/workspace-agents-watcher.d.ts +23 -0
  256. package/dist/workspace-agents-watcher.d.ts.map +1 -0
  257. package/dist/workspace-agents-watcher.js +152 -0
  258. package/dist/workspace-agents-watcher.js.map +1 -0
  259. package/dist/workspace-discovery.d.ts +11 -0
  260. package/dist/workspace-discovery.d.ts.map +1 -0
  261. package/dist/workspace-discovery.js +116 -0
  262. package/dist/workspace-discovery.js.map +1 -0
  263. package/gateway/package-lock.json +597 -0
  264. package/gateway/package.json +57 -0
  265. package/gateway/pnpm-lock.yaml +342 -0
  266. package/gateway/src/activity.ts +142 -0
  267. package/gateway/src/config.ts +246 -0
  268. package/gateway/src/handlers/anthropic.ts +328 -0
  269. package/gateway/src/handlers/gemini.ts +122 -0
  270. package/gateway/src/handlers/models.ts +45 -0
  271. package/gateway/src/handlers/openai.ts +333 -0
  272. package/gateway/src/index.ts +344 -0
  273. package/gateway/src/mapping-store.ts +88 -0
  274. package/gateway/src/restorer.ts +322 -0
  275. package/gateway/src/sanitizer.ts +298 -0
  276. package/gateway/src/types.ts +73 -0
  277. package/gateway/tsconfig.json +20 -0
  278. package/openclaw.plugin.json +86 -0
  279. package/package.json +74 -0
  280. package/samples/Untitled +1 -0
  281. package/samples/clean-email.txt +20 -0
  282. package/samples/test-document.md +53 -0
  283. package/samples/test-email-popup.txt +44 -0
  284. package/samples/test-email.txt +32 -0
  285. package/samples/test-webpage.html +51 -0
@@ -0,0 +1,1447 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenClaw 混合安全巡检脚本 (Node.js 跨平台版)
4
+ * 兼容性: macOS (darwin), Ubuntu (linux), CentOS (linux), Windows (win32)
5
+ * 聚焦:基础设施安全、SSH 防护、MCP 权限越界与记忆认知安全
6
+ *
7
+ * SECURITY NOTE: All commands and arguments are from a strictly hardcoded whitelist.
8
+ * No user-controlled inputs are passed to child processes.
9
+ * - Unix/Linux: shell is strictly disabled (shell: false).
10
+ * - Windows: shell is locally enabled ONLY to invoke .cmd executable wrappers
11
+ * (a Windows Node.js limitation), but remains protected against injection
12
+ * by utilizing strictly hardcoded argument arrays.
13
+ *
14
+ * @integrity sha256:565556ef7ed65dc93f8822bf59a9df3591648585467a094deedd99a11208325d
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+ const os = require('os');
20
+ const crypto = require('crypto');
21
+ const { spawnSync } = require('child_process');
22
+ const http = require('http');
23
+ const https = require('https');
24
+
25
+ // ──────────────────────────────────────────
26
+ // 命令行参数解析
27
+ // ──────────────────────────────────────────
28
+ const PUSH_ENABLED = process.argv.includes('--push');
29
+ const GENERATE_CONFIG_BASELINE = process.argv.includes('--generate-config-baseline');
30
+ const UPDATE_SKILL_BASELINE = process.argv.includes('--update-skill-baseline');
31
+
32
+ // 环境预设
33
+ const platform = os.platform();
34
+ const HOME = os.homedir();
35
+ const OC = process.env.OPENCLAW_STATE_DIR || path.join(HOME, '.openclaw');
36
+
37
+ // 日期时间处理
38
+ const now = new Date();
39
+ const yest = new Date(now);
40
+ yest.setDate(yest.getDate() - 1);
41
+
42
+ function getLocalDateStr(dateObj) {
43
+ const y = dateObj.getFullYear();
44
+ const m = String(dateObj.getMonth() + 1).padStart(2, '0');
45
+ const d = String(dateObj.getDate()).padStart(2, '0');
46
+ return `${y}-${m}-${d}`;
47
+ }
48
+
49
+ const DATE_STR = getLocalDateStr(now);
50
+ const REPORT_TIME = `${DATE_STR} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
51
+
52
+
53
+ // ──────────────────────────────────────────
54
+ // 目录与文件设置(写入用户目录,日志默认在系统临时目录)
55
+ // ──────────────────────────────────────────
56
+ const REPORT_DIR = path.join(OC, 'security-reports');
57
+ const TMP_ROOT = typeof os.tmpdir === 'function' ? os.tmpdir() : '/tmp';
58
+ // 支持最近24小时的日志(今天 + 昨天)
59
+ const YEST_DATE_STR = getLocalDateStr(yest);
60
+ let LOG_CANDIDATES = [
61
+ path.join(TMP_ROOT, 'clawdbot', `clawdbot-${DATE_STR}.log`),
62
+ path.join(TMP_ROOT, 'openclaw', `openclaw-${DATE_STR}.log`),
63
+ path.join(TMP_ROOT, 'clawdbot', `clawdbot-${YEST_DATE_STR}.log`),
64
+ path.join(TMP_ROOT, 'openclaw', `openclaw-${YEST_DATE_STR}.log`)
65
+ ];
66
+ // Windows 额外兼容 C:\tmp\* 目录(如果存在)
67
+ if (platform === 'win32') {
68
+ LOG_CANDIDATES = LOG_CANDIDATES.concat([
69
+ path.join('C:\\tmp', 'clawdbot', `clawdbot-${DATE_STR}.log`),
70
+ path.join('C:\\tmp', 'openclaw', `openclaw-${DATE_STR}.log`),
71
+ path.join('C:\\tmp', 'clawdbot', `clawdbot-${YEST_DATE_STR}.log`),
72
+ path.join('C:\\tmp', 'openclaw', `openclaw-${YEST_DATE_STR}.log`)
73
+ ]);
74
+ }
75
+ fs.mkdirSync(REPORT_DIR, { recursive: true, mode: 0o700 });
76
+ const REPORT_FILE = path.join(REPORT_DIR, `report-${DATE_STR}.txt`);
77
+ const JSON_OUT_FILE = path.join(REPORT_DIR, 'report.json');
78
+
79
+ const COLORS = {
80
+ reset: "\x1b[0m",
81
+ bright: "\x1b[1m",
82
+ dim: "\x1b[2m",
83
+ green: "\x1b[32m",
84
+ red: "\x1b[31m",
85
+ yellow: "\x1b[33m",
86
+ cyan: "\x1b[36m",
87
+ blue: "\x1b[34m",
88
+ magenta: "\x1b[35m"
89
+ };
90
+
91
+ let SUMMARY = `\n${COLORS.cyan}${COLORS.bright}OPENCLAW SECURITY AUDIT${COLORS.reset} ${COLORS.dim}[${DATE_STR}]${COLORS.reset}\n`;
92
+ SUMMARY += `${COLORS.dim}────────────────────────────────────────────────────────────────────────${COLORS.reset}\n`;
93
+ let RED_COUNT = 0;
94
+ let SKIP_COUNT = 0;
95
+ let JSON_DATA = [];
96
+ let ITEM_SEQ = 0;
97
+
98
+ fs.writeFileSync(REPORT_FILE, `=== OpenClaw Hybrid Security Audit (${DATE_STR}) ===\n`, { mode: 0o600 });
99
+
100
+ // ──────────────────────────────────────────
101
+ // 核心工具函数(全部基于 spawnSync,不使用 shell)
102
+ // ──────────────────────────────────────────
103
+ function appendWarn(item, brief, detail) {
104
+ const icon = `${COLORS.red}[FAIL]${COLORS.reset}`;
105
+ const title = `${COLORS.bright}${item}${COLORS.reset}`;
106
+ const desc = `${COLORS.yellow}${brief.replace(/⚠️ /g, '')}${COLORS.reset}`;
107
+ SUMMARY += ` ${icon} ${title.padEnd(45, ' ')} :: ${desc}\n`;
108
+ RED_COUNT++;
109
+ JSON_DATA.push({ item, brief, detail });
110
+ fs.appendFileSync(REPORT_FILE, `\n[FAIL] ${item}\n${detail}\n`);
111
+ ITEM_SEQ++;
112
+ }
113
+
114
+ function appendInfo(item, brief, detail) {
115
+ const icon = `${COLORS.green}[PASS]${COLORS.reset}`;
116
+ const title = `${COLORS.bright}${item}${COLORS.reset}`;
117
+ const desc = `${COLORS.dim}${brief.replace(/✅ /g, '')}${COLORS.reset}`;
118
+ SUMMARY += ` ${icon} ${title.padEnd(45, ' ')} :: ${desc}\n`;
119
+ JSON_DATA.push({ item, brief, detail });
120
+ fs.appendFileSync(REPORT_FILE, `\n[PASS] ${item}\n${detail}\n`);
121
+ ITEM_SEQ++;
122
+ }
123
+
124
+ function appendSkip(item, brief, detail) {
125
+ const icon = `${COLORS.magenta}[SKIP]${COLORS.reset}`;
126
+ const title = `${COLORS.bright}${item}${COLORS.reset}`;
127
+ const desc = `${COLORS.magenta}${brief}${COLORS.reset}`;
128
+ SUMMARY += ` ${icon} ${title.padEnd(45, ' ')} :: ${desc}\n`;
129
+ SKIP_COUNT++;
130
+ JSON_DATA.push({ item, brief, detail });
131
+ fs.appendFileSync(REPORT_FILE, `\n[SKIP] ${item}\n${detail}\n`);
132
+ ITEM_SEQ++;
133
+ }
134
+
135
+ function buildSafeChildPath(basePath, entryName) {
136
+ const safeName = path.basename(String(entryName || ''));
137
+ if (!safeName || safeName === '.' || safeName === '..') {
138
+ return null;
139
+ }
140
+ return `${basePath}${path.sep}${safeName}`;
141
+ }
142
+
143
+ function buildSafeRelativePath(basePath, relativePath) {
144
+ const raw = String(relativePath || '').replace(/\\/g, '/');
145
+ const normalized = raw.replace(/^\/+/, '');
146
+ if (!normalized || normalized.includes('\0')) {
147
+ return null;
148
+ }
149
+ const segments = normalized.split('/').filter(Boolean);
150
+ if (segments.length === 0 || segments.some(seg => seg === '.' || seg === '..')) {
151
+ return null;
152
+ }
153
+ return `${basePath}${path.sep}${segments.join(path.sep)}`;
154
+ }
155
+
156
+ function runSafeCommand(commandKey, args, strictMode) {
157
+ const safeArgs = Array.isArray(args) ? args.map(arg => String(arg)) : [];
158
+ try {
159
+ let result;
160
+ switch (commandKey) {
161
+ case 'openclaw':
162
+ if (platform === 'win32') {
163
+ // Windows 底层限制:必须依赖 shell 才能解析 .cmd 文件。
164
+ // 安全断言:此处的 safeArgs 为内部硬编码,无用户输入,局部开启 shell 无注入风险。
165
+ result = spawnSync('openclaw.cmd', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000, shell: true });
166
+ } else {
167
+ result = spawnSync('openclaw', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
168
+ }
169
+ break;
170
+ case 'openclaw-cn':
171
+ if (platform === 'win32') {
172
+ result = spawnSync('openclaw-cn.cmd', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000, shell: true });
173
+ } else {
174
+ result = spawnSync('openclaw-cn', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
175
+ }
176
+ break;
177
+ case 'find':
178
+ result = spawnSync('find', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
179
+ break;
180
+ case 'pgrep':
181
+ result = spawnSync('pgrep', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
182
+ break;
183
+ case 'journalctl':
184
+ result = spawnSync('journalctl', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
185
+ break;
186
+ case 'log':
187
+ result = spawnSync('log', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
188
+ break;
189
+ case 'ss':
190
+ result = spawnSync('ss', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
191
+ break;
192
+ case 'ps':
193
+ result = spawnSync('ps', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
194
+ break;
195
+ case 'lsof':
196
+ result = spawnSync('lsof', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
197
+ break;
198
+ case 'diff':
199
+ result = spawnSync('diff', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
200
+ break;
201
+ case 'wevtutil':
202
+ result = spawnSync('wevtutil', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
203
+ break;
204
+ case 'netstat':
205
+ result = spawnSync('netstat', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
206
+ break;
207
+ case 'tasklist':
208
+ result = spawnSync('tasklist', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
209
+ break;
210
+ case 'powershell':
211
+ result = spawnSync('powershell', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
212
+ break;
213
+ case 'icacls':
214
+ result = spawnSync('icacls', safeArgs, { stdio: 'pipe', encoding: 'utf-8', timeout: 30000 });
215
+ break;
216
+ default:
217
+ return strictMode
218
+ ? { success: false, output: `unsupported command: ${commandKey}` }
219
+ : '';
220
+ }
221
+ if (result.error) {
222
+ return strictMode ? { success: false, output: result.error.message } : '';
223
+ }
224
+ if (!strictMode) {
225
+ return (result.stdout || '').trim();
226
+ }
227
+ if (result.status === 0) {
228
+ return { success: true, output: (result.stdout || '').trim() };
229
+ }
230
+ return { success: false, output: ((result.stderr || '') + (result.stdout || '')).trim() };
231
+ } catch (e) {
232
+ return strictMode ? { success: false, output: e.message || '' } : '';
233
+ }
234
+ }
235
+
236
+ function spawnCmd(commandKey, args) {
237
+ return runSafeCommand(commandKey, args, false);
238
+ }
239
+
240
+ function spawnCmdStrict(commandKey, args) {
241
+ return runSafeCommand(commandKey, args, true);
242
+ }
243
+
244
+ function getFileHash(filePath) {
245
+ try { return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex'); }
246
+ catch(e) { return null; }
247
+ }
248
+
249
+ function getFilePerms(filePath) {
250
+ try { return (fs.statSync(filePath).mode & 0o777).toString(8); }
251
+ catch(e) { return "MISSING"; }
252
+ }
253
+
254
+ /**
255
+ * 生成核心配置文件哈希基线
256
+ * 输出到 .config-baseline.sha256 文件,用于 [4/14] 防篡改校验
257
+ */
258
+ function generateConfigBaseline() {
259
+ const baselinePath = path.join(OC, '.config-baseline.sha256');
260
+ const filesToHash = [];
261
+
262
+ // 确定需要监控的配置文件
263
+ // 注意: devices/paired.json 被排除,因为巡检脚本运行时会更新该文件
264
+ const configFiles = [
265
+ path.join(OC, 'openclaw.json'),
266
+ path.join(OC, 'config.json'),
267
+ path.join(OC, 'settings.json')
268
+ ];
269
+
270
+ // 平台特定的配置文件
271
+ if (platform === 'win32') {
272
+ configFiles.push(
273
+ path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'ssh', 'sshd_config'),
274
+ path.join(HOME, '.ssh/authorized_keys'),
275
+ path.join(HOME, '.ssh/config')
276
+ );
277
+ } else {
278
+ configFiles.push(
279
+ '/etc/ssh/sshd_config',
280
+ path.join(HOME, '.ssh/authorized_keys'),
281
+ path.join(HOME, '.ssh/config'),
282
+ '/etc/passwd',
283
+ '/etc/shadow'
284
+ );
285
+ }
286
+
287
+ console.log(`${COLORS.cyan}${COLORS.bright}正在生成配置文件哈希基线...${COLORS.reset}`);
288
+ console.log(`${COLORS.dim}目标文件: ${baselinePath}${COLORS.reset}\n`);
289
+
290
+ let baselineContent = '';
291
+ let successCount = 0;
292
+ let skipCount = 0;
293
+
294
+ for (const filePath of configFiles) {
295
+ const hash = getFileHash(filePath);
296
+ if (hash) {
297
+ // 使用相对路径存储,提高可移植性
298
+ let storedPath = filePath;
299
+ if (filePath.startsWith(OC)) {
300
+ storedPath = path.relative(OC, filePath);
301
+ }
302
+ baselineContent += `${hash} ${storedPath}\n`;
303
+ console.log(` ${COLORS.green}✓${COLORS.reset} ${storedPath}`);
304
+ successCount++;
305
+ } else {
306
+ console.log(` ${COLORS.yellow}○${COLORS.reset} ${filePath} ${COLORS.dim}(跳过: 文件不存在或无权限)${COLORS.reset}`);
307
+ skipCount++;
308
+ }
309
+ }
310
+
311
+ // 写入基线文件
312
+ try {
313
+ fs.writeFileSync(baselinePath, baselineContent, { mode: 0o600 });
314
+ console.log(`\n${COLORS.green}${COLORS.bright}基线生成完成!${COLORS.reset}`);
315
+ console.log(` 已记录: ${successCount} 个文件`);
316
+ if (skipCount > 0) {
317
+ console.log(` 跳过: ${skipCount} 个文件`);
318
+ }
319
+ console.log(`\n${COLORS.dim}提示: 基线文件权限已设置为 600,请妥善保管。${COLORS.reset}`);
320
+ console.log(`${COLORS.dim}如需更新基线,请重新运行此命令。${COLORS.reset}`);
321
+ return true;
322
+ } catch (e) {
323
+ console.error(`\n${COLORS.red}错误: 无法写入基线文件: ${e.message}${COLORS.reset}`);
324
+ return false;
325
+ }
326
+ }
327
+
328
+ let FILTER_SKILLS_KEYWORDS = ["changeway","ctct-security-patrol"];
329
+
330
+ function countMatchesInFile(filePath, regex) {
331
+ try {
332
+ if (!(regex instanceof RegExp)) {
333
+ return 0;
334
+ }
335
+ const content = fs.readFileSync(filePath, 'utf-8');
336
+ const matches = content.match(regex);
337
+ return matches ? matches.length : 0;
338
+ } catch (e) {
339
+ return 0;
340
+ }
341
+ }
342
+
343
+ function grepFile(filePath, regex) {
344
+ try {
345
+ if (!(regex instanceof RegExp)) {
346
+ return [];
347
+ }
348
+ const content = fs.readFileSync(filePath, 'utf-8');
349
+ return content.split('\n').filter(line => {
350
+ regex.lastIndex = 0;
351
+ return regex.test(line);
352
+ });
353
+ } catch (e) {
354
+ return [];
355
+ }
356
+ }
357
+
358
+ // ──────────────────────────────────────────
359
+ // 基线生成模式(若指定 --generate-config-baseline 参数)
360
+ // ──────────────────────────────────────────
361
+ if (GENERATE_CONFIG_BASELINE) {
362
+ const ok = generateConfigBaseline();
363
+ process.exit(ok ? 0 : 1);
364
+ }
365
+
366
+
367
+ function filterAuditOutput(output, keywords) {
368
+ if (!keywords || keywords.length === 0) return output;
369
+
370
+ const lines = output.split('\n');
371
+ const result = [];
372
+ let skipBlock = false;
373
+
374
+ for (const line of lines) {
375
+ const isNewEntry = /^[a-z_]+\.[a-z_]+\s+/i.test(line);
376
+
377
+ if (isNewEntry) {
378
+ const lowerLine = line.toLowerCase();
379
+ skipBlock = keywords.some(kw => lowerLine.includes(kw.toLowerCase()));
380
+ }
381
+
382
+ if (!skipBlock) {
383
+ result.push(line);
384
+ }
385
+ }
386
+
387
+ return result.join('\n');
388
+ }
389
+
390
+
391
+ // ==========================================
392
+ // 板块一:基础设施与系统安全
393
+ // ==========================================
394
+ fs.appendFileSync(REPORT_FILE, `\n--- [板块一] 基础设施与系统安全 ---`);
395
+
396
+ // [1/14] OpenClaw 基础审计
397
+ let itemName = "核心运行环境健康度";
398
+ fs.appendFileSync(REPORT_FILE, `\n\n[1/14] OpenClaw 基础审计 (--deep)`);
399
+ let res1 = spawnCmdStrict("openclaw", ["security", "audit", "--deep"]);
400
+ if (!res1.success) {
401
+ // 如果 openclaw 执行失败,尝试使用 openclaw-cn 再执行一次
402
+ let res1Fallback = spawnCmdStrict("openclaw-cn", ["security", "audit", "--deep"]);
403
+ if (res1Fallback.success) {
404
+ res1 = res1Fallback;
405
+ } else {
406
+ // 如果 fallback 也失败,则将两次失败信息合并
407
+ res1.output += `\n\n[Fallback openclaw-cn 也执行失败]\n${res1Fallback.output || ''}`;
408
+ }
409
+ }
410
+ // 过滤指定的 skills 条目
411
+ res1.output = filterAuditOutput(res1.output, FILTER_SKILLS_KEYWORDS);
412
+ if (res1.success) {
413
+ appendInfo(itemName, "运行环境各项指标正常", res1.output);
414
+ } else {
415
+ appendWarn(itemName, "核心环境存在异常(详见报告)", res1.output);
416
+ }
417
+
418
+ // [2/14] 敏感目录变更
419
+ itemName = "系统敏感目录防篡改监控";
420
+ fs.appendFileSync(REPORT_FILE, `\n[2/14] 敏感目录变更`);
421
+ let SENSITIVE_ROOTS;
422
+ if (platform === 'win32') {
423
+ SENSITIVE_ROOTS = [
424
+ OC,
425
+ path.join(HOME, '.ssh'),
426
+ path.join(HOME, '.gnupg'),
427
+ path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'ssh')
428
+ ];
429
+ } else {
430
+ SENSITIVE_ROOTS = [OC, '/etc', path.join(HOME, '.ssh'), path.join(HOME, '.gnupg'), '/usr/local/bin'];
431
+ }
432
+ const PRUNE_PATTERNS = [
433
+ 'node_modules', '.cache', '.npm', '__pycache__', '.git', 'dist', 'build', '.next', '.nuxt',
434
+ '.pnpm-store', '.yarn', '.venv', 'venv', '.tox'
435
+ ];
436
+ const MAX_FILES_PER_GROUP = 15;
437
+
438
+ /**
439
+ * Pure Node.js recursive file scanner that finds files modified within maxAgeMs.
440
+ * Works cross-platform, replacing the Unix `find` dependency.
441
+ */
442
+ function findRecentFiles(roots, prunePatterns, maxAgeMs) {
443
+ const cutoff = Date.now() - maxAgeMs;
444
+ const pruneSet = new Set(prunePatterns);
445
+ const results = [];
446
+
447
+ function walk(dir) {
448
+ let entries;
449
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
450
+ catch (e) { return; }
451
+ for (const entry of entries) {
452
+ if (pruneSet.has(entry.name)) continue;
453
+ const fullPath = buildSafeChildPath(dir, entry.name);
454
+ if (!fullPath) continue;
455
+ try {
456
+ if (entry.isDirectory()) {
457
+ walk(fullPath);
458
+ } else if (entry.isFile()) {
459
+ const stat = fs.statSync(fullPath);
460
+ if (stat.mtimeMs >= cutoff) {
461
+ results.push(fullPath);
462
+ }
463
+ }
464
+ } catch (e) { /* permission denied or similar */ }
465
+ }
466
+ }
467
+
468
+ for (const root of roots) {
469
+ try {
470
+ const stat = fs.statSync(root);
471
+ if (stat.isDirectory()) {
472
+ walk(root);
473
+ } else if (stat.isFile() && stat.mtimeMs >= cutoff) {
474
+ results.push(root);
475
+ }
476
+ } catch (e) { /* root doesn't exist */ }
477
+ }
478
+ return results;
479
+ }
480
+
481
+ const MS_24H = 24 * 60 * 60 * 1000;
482
+ let lines = findRecentFiles(SENSITIVE_ROOTS, PRUNE_PATTERNS, MS_24H);
483
+ lines = lines.filter(f => !f.endsWith('.sha256'));
484
+ let totalCount = lines.length;
485
+
486
+ function normalizePath(p) {
487
+ return p.replace(HOME, '~');
488
+ }
489
+ function getGroupKey(filePath) {
490
+ for (const root of SENSITIVE_ROOTS) {
491
+ if (filePath === root || filePath.startsWith(root + path.sep)) {
492
+ const rest = filePath.slice(root.length + 1);
493
+ const first = rest.split(path.sep)[0];
494
+ return first ? `${normalizePath(root)}/${first}` : normalizePath(root);
495
+ }
496
+ }
497
+ return path.dirname(filePath);
498
+ }
499
+ let byGroup = {};
500
+ lines.forEach(f => {
501
+ const key = getGroupKey(f);
502
+ if (!byGroup[key]) byGroup[key] = [];
503
+ byGroup[key].push(f);
504
+ });
505
+ let detailLines = ['>>> 近24小时变更(已排除 node_modules/.cache 等噪音目录)'];
506
+ detailLines.push(`>>> 总变更文件数: ${totalCount}`);
507
+ detailLines.push('');
508
+ Object.keys(byGroup).sort().forEach(grp => {
509
+ const list = byGroup[grp];
510
+ if (list.length <= MAX_FILES_PER_GROUP) {
511
+ list.forEach(f => detailLines.push(' ' + normalizePath(f)));
512
+ } else {
513
+ detailLines.push(` [${grp}/...]: ${list.length} 个文件(已折叠)`);
514
+ }
515
+ });
516
+ if (totalCount === 0) {
517
+ appendInfo(itemName, `近24小时变更 0 个配置文件`, `无文件变更`);
518
+ } else {
519
+ appendInfo(itemName, `近24小时变更 ${totalCount} 个文件(核心/折叠显示)`, detailLines.join('\n'));
520
+ }
521
+
522
+ // [3/14] Gateway 环境变量泄露扫描(仅检查变量名是否存在,不读取/记录值)
523
+ itemName = "网关进程内存凭证隔离检查";
524
+ fs.appendFileSync(REPORT_FILE, `\n[3/14] Gateway 环境变量泄露扫描`);
525
+ {
526
+ const gwPidRaw = spawnCmd('pgrep', ['-f', 'openclaw-gateway']);
527
+ const gwPid = gwPidRaw.split('\n')[0];
528
+
529
+ if (gwPid && /^\d+$/.test(gwPid)) {
530
+ let sensitiveVarCount = 0;
531
+
532
+ if (platform === 'linux') {
533
+ const environPath = `/proc/${gwPid}/environ`;
534
+ try {
535
+ const environData = fs.readFileSync(environPath, 'utf-8');
536
+ const envEntries = environData.split('\0').filter(Boolean);
537
+ const sensitivePattern = /^(.*?(SECRET|TOKEN|PASSWORD|KEY|PRIVATE).*?)=/i;
538
+ const hitNames = [];
539
+ envEntries.forEach(entry => {
540
+ const m = entry.match(sensitivePattern);
541
+ if (m) hitNames.push(m[1] + '=(REDACTED)');
542
+ });
543
+ sensitiveVarCount = hitNames.length;
544
+ if (sensitiveVarCount > 0) {
545
+ appendInfo(itemName, `进程环境中存在 ${sensitiveVarCount} 个敏感变量名(仅记录名称,值已脱敏)`, hitNames.join('\n'));
546
+ } else {
547
+ appendInfo(itemName, "未命中敏感环境变量名", "✅ 未命中 SECRET/TOKEN/PASSWORD/KEY 等环境变量名");
548
+ }
549
+ } catch (e) {
550
+ appendInfo(
551
+ itemName,
552
+ "无法读取网关进程环境(权限不足或进程受保护)",
553
+ "⚠️ 读取 /proc/" + gwPid + "/environ 失败: " + e.code + "\n建议以与网关相同用户运行,或通过网关侧诊断接口获取白名单变量名。"
554
+ );
555
+ }
556
+ } else if (platform === 'darwin') {
557
+ appendSkip(
558
+ itemName,
559
+ "macOS 受 SIP 限制,跳过进程环境变量扫描",
560
+ "macOS 下读取其他进程环境变量受 SIP/权限限制。\n建议通过网关侧诊断接口或配置审计来验证敏感变量的使用情况。"
561
+ );
562
+ } else if (platform === 'win32') {
563
+ appendSkip(
564
+ itemName,
565
+ "Windows 下无法直接读取其他进程环境变量,跳过扫描",
566
+ "Windows 不支持 /proc 文件系统,且读取其他进程环境变量需要特殊权限。\n建议通过网关侧诊断接口或配置审计来验证敏感变量的使用情况。"
567
+ );
568
+ }
569
+ } else {
570
+ appendInfo(itemName, "未发现运行中的网关进程", "未发现运行中的网关进程。");
571
+ }
572
+ }
573
+
574
+ // [4/14] 关键配置文件权限与哈希
575
+ itemName = "核心配置防篡改与权限基线";
576
+ fs.appendFileSync(REPORT_FILE, `\n[4/14] ${itemName}`);
577
+
578
+ const baselinePath = path.join(OC, '.config-baseline.sha256');
579
+
580
+ let hashStatus = "MISSING_BASELINE";
581
+ let checksOutput = [];
582
+
583
+ if (fs.existsSync(baselinePath)) {
584
+ hashStatus = "PASSED";
585
+ const baselineData = fs.readFileSync(baselinePath, 'utf-8');
586
+
587
+ baselineData.split('\n').forEach(line => {
588
+ const parts = line.trim().split(/\s+/);
589
+ if (parts.length >= 2) {
590
+ const expectedHash = parts[0];
591
+ const fileRef = parts.slice(1).join(' ');
592
+ const targetPath = path.isAbsolute(fileRef) ? fileRef : buildSafeRelativePath(OC, fileRef);
593
+ if (!targetPath) {
594
+ checksOutput.push(`${fileRef}: SKIPPED (unsafe relative path)`);
595
+ return;
596
+ }
597
+
598
+ if (platform === 'win32') {
599
+ if (!targetPath.startsWith(OC)) {
600
+ checksOutput.push(`${fileRef}: SKIPPED (path outside allowed scope)`);
601
+ return;
602
+ }
603
+ } else {
604
+ // 允许 OC 目录、/etc/ 目录以及用户 SSH 相关文件
605
+ const isInHomeSsh = targetPath.startsWith(path.join(HOME, '.ssh'));
606
+ if (!targetPath.startsWith(OC) && !targetPath.startsWith('/etc/') && !isInHomeSsh) {
607
+ checksOutput.push(`${fileRef}: SKIPPED (path outside allowed scope)`);
608
+ return;
609
+ }
610
+ }
611
+
612
+ const actualHash = getFileHash(targetPath);
613
+
614
+ if (actualHash === expectedHash) {
615
+ checksOutput.push(`${fileRef}: OK`);
616
+ } else if (actualHash === null) {
617
+ checksOutput.push(`${fileRef}: FAILED (文件不存在或无法读取)`);
618
+ hashStatus = "FAILED";
619
+ } else {
620
+ checksOutput.push(`${fileRef}: FAILED (哈希不匹配: 预期 ${expectedHash.substring(0, 16)}..., 实际 ${actualHash.substring(0, 16)}...)`);
621
+ hashStatus = "FAILED";
622
+ }
623
+ }
624
+ });
625
+ }
626
+
627
+ let detail4 = `>>> 哈希基线校验状态: ${hashStatus}\n`;
628
+ if (hashStatus === "MISSING_BASELINE") {
629
+ detail4 += `\n未找到哈希基线文件 (${baselinePath})。\n\n📋 基线生成方式:\n node ${path.basename(__filename)} --generate-config-baseline\n\n💡 提示: 首次运行缺失基线属正常现象,执行上述命令可一键生成配置文件哈希基线,用于后续防篡改监控。`;
630
+ } else {
631
+ detail4 += checksOutput.join('\n') + '\n';
632
+ }
633
+
634
+ /**
635
+ * Check Windows file permission using icacls.
636
+ * Returns true if file has overly permissive ACLs (Everyone or BUILTIN\Users with write/full control).
637
+ */
638
+ function checkWindowsFilePermission(filePath) {
639
+ try {
640
+ if (!fs.existsSync(filePath)) return "MISSING";
641
+ const result = spawnCmd('icacls', [filePath]);
642
+ if (!result) return "UNKNOWN";
643
+ // Check for overly permissive ACLs
644
+ const overlyPermissive = /(Everyone|BUILTIN\\Users).*(F\)|M\)|W\))/i;
645
+ if (overlyPermissive.test(result)) return "PERMISSIVE";
646
+ return "OK";
647
+ } catch (e) {
648
+ return "UNKNOWN";
649
+ }
650
+ }
651
+
652
+ let permOk = true;
653
+
654
+ if (platform === 'win32') {
655
+ const permOCWin = checkWindowsFilePermission(path.join(OC, 'openclaw.json'));
656
+ const sshdConfigPath = path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'ssh', 'sshd_config');
657
+ const permSshdWin = checkWindowsFilePermission(sshdConfigPath);
658
+ const permAuthKeysWin = checkWindowsFilePermission(path.join(HOME, '.ssh/authorized_keys'));
659
+
660
+ detail4 += `\n\n>>> 关键文件权限状态 (Windows ACL):
661
+ openclaw.json : ${permOCWin} (预期: 无 Everyone/Users 写权限)
662
+ sshd_config : ${permSshdWin} (预期: 无 Everyone/Users 写权限) [${sshdConfigPath}]
663
+ authorized_keys : ${permAuthKeysWin} (预期: 无 Everyone/Users 写权限)\n`;
664
+
665
+ if (
666
+ permOCWin === "PERMISSIVE" ||
667
+ permSshdWin === "PERMISSIVE" ||
668
+ permAuthKeysWin === "PERMISSIVE"
669
+ ) {
670
+ permOk = false;
671
+ }
672
+ } else {
673
+ const permOC = getFilePerms(path.join(OC, 'openclaw.json'));
674
+ const permSshd = getFilePerms('/etc/ssh/sshd_config');
675
+ const permAuthKeys = getFilePerms(path.join(HOME, '.ssh/authorized_keys'));
676
+
677
+ detail4 += `\n\n>>> 关键文件权限状态:
678
+ openclaw.json : ${permOC} (预期: 600)
679
+ sshd_config : ${permSshd} (预期: 644 或 600)
680
+ authorized_keys : ${permAuthKeys} (预期: 600 或 644)\n`;
681
+
682
+ if (
683
+ (permOC !== "600" && permOC !== "MISSING") ||
684
+ (permSshd !== "600" && permSshd !== "644" && permSshd !== "MISSING") ||
685
+ (permAuthKeys !== "600" && permAuthKeys !== "644" && permAuthKeys !== "MISSING")
686
+ ) {
687
+ permOk = false;
688
+ }
689
+ }
690
+
691
+ if (hashStatus === "FAILED") {
692
+ appendWarn(itemName, "🚨 危险!哈希校验失败,核心配置可能已被非法篡改!", detail4);
693
+ } else if (hashStatus === "MISSING_BASELINE") {
694
+ if (permOk) {
695
+ appendWarn(itemName, "⚠️ 首次运行缺失哈希参照物,但核心文件权限严密合规", detail4);
696
+ } else {
697
+ appendWarn(itemName, "❌ 缺失哈希参照物,且检测到核心文件权限存在越界风险!", detail4);
698
+ }
699
+ } else if (hashStatus === "PASSED") {
700
+ if (permOk) {
701
+ appendInfo(itemName, "✅ 哈希防篡改校验通过,且核心权限完全合规", detail4);
702
+ } else {
703
+ appendWarn(itemName, "⚠️ 文件未被篡改,但权限设置过于宽松,请排查详细报告", detail4);
704
+ }
705
+ }
706
+
707
+ // [5/14] MCP/Skill 基线完整性
708
+ itemName = "组件与插件供应链完整性";
709
+ fs.appendFileSync(REPORT_FILE, `\n[5/14] MCP/Skill 基线完整性`);
710
+ let SKILL_SCAN_DIRS;
711
+ if (platform === 'win32') {
712
+ SKILL_SCAN_DIRS = [
713
+ path.join(process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'), 'npm', 'node_modules', 'openclaw-cn', 'skills'),
714
+ path.join(process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'), 'npm', 'node_modules', 'openclaw', 'skills'),
715
+ path.join(HOME, '.openclaw', 'workspace', 'skills'),
716
+ path.join(HOME, '.openclaw', 'skills')
717
+ ];
718
+ } else {
719
+ SKILL_SCAN_DIRS = [
720
+ '/opt/homebrew/lib/node_modules/openclaw/skills',
721
+ path.join(HOME, '.openclaw/workspace/skills'),
722
+ path.join(HOME, '.openclaw/skills')
723
+ ];
724
+ }
725
+ let skillDir = SKILL_SCAN_DIRS[0];
726
+ let mcpDir = path.join(OC, 'workspace/mcp');
727
+ let hashDir = path.join(OC, 'security-baselines');
728
+ fs.mkdirSync(hashDir, { recursive: true });
729
+ let curHashPath = path.join(hashDir, 'skill-mcp-current.sha256');
730
+ let baseHashPath = path.join(hashDir, 'skill-mcp-baseline.sha256');
731
+
732
+ function getAllFiles(dirPath, arrayOfFiles = []) {
733
+ try {
734
+ let files = fs.readdirSync(dirPath);
735
+ files.forEach(file => {
736
+ let fullPath = buildSafeChildPath(dirPath, file);
737
+ if (!fullPath) return;
738
+ if (fs.statSync(fullPath).isDirectory()) arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
739
+ else arrayOfFiles.push(fullPath);
740
+ });
741
+ } catch(e) {}
742
+ return arrayOfFiles;
743
+ }
744
+ let allMcpFiles = SKILL_SCAN_DIRS.flatMap(d => getAllFiles(d)).concat(getAllFiles(mcpDir)).sort();
745
+ let curHashes = allMcpFiles.map(f => `${getFileHash(f)} ${f}`).join('\n') + '\n';
746
+ fs.writeFileSync(curHashPath, curHashes);
747
+
748
+ // 如果指定了 --update-skill-baseline,则更新基线并退出
749
+ if (UPDATE_SKILL_BASELINE) {
750
+ fs.writeFileSync(baseHashPath, curHashes);
751
+ console.log(`${COLORS.green}✅ Skill/MCP 基线已更新: ${baseHashPath}${COLORS.reset}`);
752
+ console.log(`${COLORS.dim} 已记录 ${allMcpFiles.length} 个文件的哈希${COLORS.reset}`);
753
+ process.exit(0);
754
+ }
755
+
756
+ if (fs.existsSync(baseHashPath)) {
757
+ let baseData = fs.readFileSync(baseHashPath, 'utf-8');
758
+ if (baseData !== curHashes) {
759
+ // 智能 diff:分类统计新增、修改、删除
760
+ let baseMap = new Map();
761
+ let curMap = new Map();
762
+ baseData.split('\n').filter(l => l.trim()).forEach(line => {
763
+ const parts = line.split(' ');
764
+ if (parts.length >= 2) baseMap.set(parts[1], parts[0]);
765
+ });
766
+ curHashes.split('\n').filter(l => l.trim()).forEach(line => {
767
+ const parts = line.split(' ');
768
+ if (parts.length >= 2) curMap.set(parts[1], parts[0]);
769
+ });
770
+
771
+ let added = []; // 新增文件
772
+ let modified = []; // 修改的文件(哈希变了)
773
+ let removed = []; // 删除的文件
774
+
775
+ // 检查新增和修改
776
+ for (let [filePath, hash] of curMap) {
777
+ if (!baseMap.has(filePath)) {
778
+ added.push(filePath);
779
+ } else if (baseMap.get(filePath) !== hash) {
780
+ modified.push(filePath);
781
+ }
782
+ }
783
+ // 检查删除
784
+ for (let filePath of baseMap.keys()) {
785
+ if (!curMap.has(filePath)) removed.push(filePath);
786
+ }
787
+
788
+ // 提取受影响的 Skill 名称
789
+ function extractSkillNames(filePaths) {
790
+ let names = new Set();
791
+ filePaths.forEach(fp => {
792
+ const match = fp.match(/skills[/\\]([^/\\]+)/);
793
+ if (match) names.add(match[1]);
794
+ });
795
+ return Array.from(names).slice(0, 10); // 最多显示10个
796
+ }
797
+
798
+ let summary = [];
799
+ if (added.length > 0) summary.push(`新增 ${added.length} 个文件`);
800
+ if (modified.length > 0) summary.push(`修改 ${modified.length} 个文件`);
801
+ if (removed.length > 0) summary.push(`删除 ${removed.length} 个文件`);
802
+
803
+ let detail = `>>> 变更摘要: ${summary.join(',')}\n\n`;
804
+
805
+ let affectedSkills = extractSkillNames([...added, ...modified, ...removed]);
806
+ if (affectedSkills.length > 0) {
807
+ detail += `>>> 受影响的 Skill:\n${affectedSkills.map(s => ` - ${s}`).join('\n')}\n`;
808
+ }
809
+
810
+ // 显示部分变更详情(限制数量)
811
+ detail += '\n>>> 变更详情(前 20 个):\n';
812
+ let allChanges = [
813
+ ...added.slice(0, 10).map(f => `+ [新增] ${path.basename(f)}`),
814
+ ...modified.slice(0, 10).map(f => `~ [修改] ${path.basename(f)}`),
815
+ ...removed.slice(0, 10).map(f => `- [删除] ${path.basename(f)}`)
816
+ ].slice(0, 20);
817
+ detail += allChanges.join('\n') || ' (无详细变更信息)';
818
+
819
+ detail += '\n\n💡 提示: 如确认是正常安装/更新 Skill,请执行:\n';
820
+ detail += ` node ${path.basename(__filename)} --update-skill-baseline\n`;
821
+
822
+ // 判断是否为新增 Skill(只有新增,没有修改已有文件)
823
+ if (modified.length === 0 && removed.length === 0 && added.length > 0) {
824
+ appendInfo(itemName, `检测到 ${added.length} 个新增文件(可能是新安装 Skill)`, detail);
825
+ } else {
826
+ appendWarn(itemName, "⚠️ 检测到 Skill/MCP 文件变更", detail);
827
+ }
828
+ } else {
829
+ appendInfo(itemName, "✅ 工具包哈希基线校验通过", `已校验 ${allMcpFiles.length} 个文件,无异常`);
830
+ }
831
+ } else {
832
+ fs.writeFileSync(baseHashPath, curHashes);
833
+ let detail = `首次运行,已建立基线\n\n已记录 ${allMcpFiles.length} 个文件的哈希\n\n`;
834
+ detail += `提示: 后续安装或更新 Skill 后,如需更新基线,请执行:\n`;
835
+ detail += ` node ${path.basename(__filename)} --update-skill-baseline`;
836
+ appendInfo(itemName, "首次运行,已建立基线", detail);
837
+ }
838
+
839
+ // [6/14] 登录与 SSH 审计
840
+ itemName = "远程访问与爆破攻击监控";
841
+ fs.appendFileSync(REPORT_FILE, `\n[6/14] 登录与 SSH 审计`);
842
+ let failedSsh = 0;
843
+ if (platform === 'linux') {
844
+ let journalOut = spawnCmd('journalctl', ['-u', 'sshd', '-u', 'ssh', '--since', '24 hours ago', '--no-pager']);
845
+ if (journalOut) {
846
+ failedSsh = (journalOut.match(/Failed|Invalid/gi) || []).length;
847
+ }
848
+ if (failedSsh === 0) {
849
+ ['/var/log/auth.log', '/var/log/secure', '/var/log/messages'].forEach(logPath => {
850
+ failedSsh += countMatchesInFile(logPath, /sshd.*(Failed|Invalid)/gim);
851
+ });
852
+ }
853
+ } else if (platform === 'darwin') {
854
+ let logOut = spawnCmd('log', ['show', '--predicate', 'process == "sshd"', '--last', '24h']);
855
+ if (logOut) {
856
+ failedSsh = (logOut.match(/Failed|Invalid/gi) || []).length;
857
+ }
858
+ } else if (platform === 'win32') {
859
+ // Event ID 4625 = Failed logon
860
+ let psOut = spawnCmd('powershell', ['-NoProfile', '-Command',
861
+ `(Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4625; StartTime=(Get-Date).AddDays(-1)} -ErrorAction SilentlyContinue | Measure-Object).Count`
862
+ ]);
863
+ failedSsh = parseInt(psOut, 10) || 0;
864
+ }
865
+ let detail6 = `--- SSH 爆破尝试 ---\nFailed SSH attempts (24h): ${failedSsh}`;
866
+ if (failedSsh > 3) appendWarn(itemName, `⚠️ 近24h SSH 失败高达 ${failedSsh} 次,疑似遭遇爆破`, detail6);
867
+ else appendInfo(itemName, `近24h SSH 失败尝试 ${failedSsh} 次`, detail6);
868
+
869
+
870
+ // [7/14] 监听端口与高资源进程
871
+ itemName = "网络暴露面与异常进程排查";
872
+ fs.appendFileSync(REPORT_FILE, `\n[7/14] 监听端口与高资源进程`);
873
+ let portsRaw = "", psRaw = "";
874
+ if (platform === 'linux') {
875
+ let ssOut = spawnCmd('ss', ['-tunlp']);
876
+ portsRaw = ssOut.split('\n').filter(l => /LISTEN/.test(l) && !/127\.0\.0\.1|::1/.test(l)).join('\n');
877
+ psRaw = spawnCmd('ps', ['-eo', 'pid,user,%cpu,%mem,comm', '--sort=-%cpu']);
878
+ psRaw = psRaw.split('\n').slice(0, 6).join('\n');
879
+ } else if (platform === 'darwin') {
880
+ let lsofOut = spawnCmd('lsof', ['-iTCP', '-sTCP:LISTEN', '-P', '-n']);
881
+ portsRaw = lsofOut.split('\n').filter(l => !/127\.0\.0\.1|::1/.test(l)).join('\n');
882
+ psRaw = spawnCmd('ps', ['-eo', 'pid,user,%cpu,%mem,comm', '-r']);
883
+ psRaw = psRaw.split('\n').slice(0, 6).join('\n');
884
+ } else if (platform === 'win32') {
885
+ portsRaw = spawnCmd('netstat', ['-ano']);
886
+ portsRaw = portsRaw.split('\n').filter(l => /LISTENING/i.test(l) && !/127\.0\.0\.1|\[::1\]/.test(l)).join('\n');
887
+ psRaw = spawnCmd('tasklist', ['/FO', 'CSV', '/NH']);
888
+ psRaw = psRaw.split('\n').slice(0, 6).join('\n');
889
+ }
890
+ let portCount = portsRaw ? portsRaw.split('\n').filter(Boolean).length : 0;
891
+ let detail7 = `--- 监听端口与高资源进程 ---\n>>> 全局网络监听状态:\n${portsRaw || '无数据'}\n\n>>> 资源占用 Top 5 进程快照:\n${psRaw || '无数据'}`;
892
+ if (portCount > 0) appendInfo(itemName, `发现 ${portCount} 条疑似对外监听记录,已记录状态快照`, detail7);
893
+ else appendInfo(itemName, "当前无对外开放的监听端口;资源进程快照已记录", detail7);
894
+
895
+ // [8/14] OpenClaw 定时任务
896
+ itemName = "自动化任务与后门驻留排查";
897
+ fs.appendFileSync(REPORT_FILE, `\n[8/14] OpenClaw Cron Jobs`);
898
+ let res8 = spawnCmdStrict("openclaw", ["cron", "list"]);
899
+ if (!res8.success) {
900
+ // 如果 openclaw 执行失败,尝试使用 openclaw-cn 再执行一次
901
+ let res8Fallback = spawnCmdStrict("openclaw-cn", ["cron", "list"]);
902
+ if (res8Fallback.success) {
903
+ res8 = res8Fallback;
904
+ } else {
905
+ // 如果 fallback 也失败,则将两次失败信息合并
906
+ res8.output += `\n\n[Fallback openclaw-cn 也执行失败]\n${res8Fallback.output || ''}`;
907
+ }
908
+ }
909
+ if (res8.success) appendInfo(itemName, "已拉取内部任务列表", res8.output);
910
+ else appendWarn(itemName, "⚠️ 拉取失败(可能是 token/权限问题)", res8.output);
911
+
912
+
913
+ // ==========================================
914
+ // 板块二:Agent 行为审计
915
+ // ==========================================
916
+ fs.appendFileSync(REPORT_FILE, `\n\n--- [板块二] Agent 行为审计 ---`);
917
+
918
+ // [9/14] 危险命令越权调用审计
919
+ itemName = "高危命令与越权行为审计";
920
+ fs.appendFileSync(REPORT_FILE, `\n\n[9/14] 危险命令越权调用审计`);
921
+ const EXISTING_LOG_FILES = LOG_CANDIDATES.filter(p => {
922
+ try { return fs.existsSync(p); } catch (e) { return false; }
923
+ });
924
+ if (EXISTING_LOG_FILES.length > 0) {
925
+ const dangerPattern = /bash\s+-c|rm\s+-rf|chmod\s+777|wget\s|curl\s.*\|.*bash|nc\s+-e|nmap\s/i;
926
+ let dangerLinesAll = [];
927
+ EXISTING_LOG_FILES.forEach(logPath => {
928
+ dangerLinesAll = dangerLinesAll.concat(grepFile(logPath, dangerPattern));
929
+ });
930
+ if (dangerLinesAll.length > 0) {
931
+ appendWarn(
932
+ itemName,
933
+ `发现 ${dangerLinesAll.length} 次高危 Shell 命令调用,请人工核查是否为授权操作`,
934
+ dangerLinesAll.slice(-10).join('\n')
935
+ );
936
+ } else {
937
+ appendInfo(itemName, "未检测到高危系统命令越权执行", "✅ 无高危系统命令越权执行记录");
938
+ }
939
+ } else {
940
+ appendSkip(
941
+ itemName,
942
+ "未找到日志文件,跳过检查",
943
+ `未在以下路径找到日志文件:\n${LOG_CANDIDATES.join('\n')}`
944
+ );
945
+ }
946
+
947
+ // [10/14] 出站网络流量白名单审计
948
+ itemName = "异常外联与数据外泄监控";
949
+ fs.appendFileSync(REPORT_FILE, `\n[10/14] 出站网络流量白名单审计`);
950
+ if (EXISTING_LOG_FILES.length > 0) {
951
+ try {
952
+ let allUrls = [];
953
+ const urlPattern = /https?:\/\/[^\s"]+/gi;
954
+ EXISTING_LOG_FILES.forEach(logPath => {
955
+ const logContent = fs.readFileSync(logPath, 'utf-8');
956
+ const matched = logContent.match(urlPattern) || [];
957
+ allUrls = allUrls.concat(matched);
958
+ });
959
+ const whitelist = /api\.openai\.com|api\.anthropic\.com|github\.com|huggingface\.co|auth\.ctct\.cn:10020\/changeway-open|127\.0\.0\.1|localhost/i;
960
+ const unknownUrls = [...new Set(allUrls.filter(u => !whitelist.test(u)))];
961
+ if (unknownUrls.length > 0) {
962
+ appendWarn(itemName, `发现 ${unknownUrls.length} 个未知外部网络请求,请人工确认是否为预期访问`, unknownUrls.join('\n'));
963
+ } else {
964
+ appendInfo(itemName, "外部网络请求均在白名单内", "✅ 外部网络请求均在白名单内");
965
+ }
966
+ } catch (e) {
967
+ appendInfo(itemName, "日志读取失败", "⚠️ 读取日志文件失败: " + e.message);
968
+ }
969
+ } else {
970
+ appendSkip(
971
+ itemName,
972
+ "无日志文件,跳过网络流量扫描",
973
+ "未找到日志文件以扫描网络请求。"
974
+ );
975
+ }
976
+
977
+
978
+ // ==========================================
979
+ // 板块三:敏感数据与行为审计
980
+ // ==========================================
981
+ fs.appendFileSync(REPORT_FILE, `\n\n--- [板块三] 敏感数据与行为审计 ---`);
982
+
983
+ // [11/14] 敏感系统文件违规读取
984
+ itemName = "系统凭证与敏感文件访问审计";
985
+ fs.appendFileSync(REPORT_FILE, `\n\n[11/14] 敏感系统文件违规读取`);
986
+ if (EXISTING_LOG_FILES.length > 0) {
987
+ const snoopPattern = /cat\s+\/etc\/shadow|cat\s+\/etc\/passwd|read\s+.*\.env|read\s+.*\.ssh\/id_/i;
988
+ let snoopLinesAll = [];
989
+ EXISTING_LOG_FILES.forEach(logPath => {
990
+ snoopLinesAll = snoopLinesAll.concat(grepFile(logPath, snoopPattern));
991
+ });
992
+ if (snoopLinesAll.length > 0) {
993
+ appendWarn(
994
+ itemName,
995
+ `发现 ${snoopLinesAll.length} 次尝试读取系统级敏感凭证的行为`,
996
+ snoopLinesAll.join('\n')
997
+ );
998
+ } else {
999
+ appendInfo(itemName, "基于规则未发现明显违规读取痕迹", "✅ 基于规则未发现明显违规读取痕迹");
1000
+ }
1001
+ } else {
1002
+ appendSkip(
1003
+ itemName,
1004
+ "无日志文件,跳过敏感文件访问扫描",
1005
+ "未找到日志文件以扫描文件读取行为。"
1006
+ );
1007
+ }
1008
+
1009
+ // [12/14] 敏感信息启发式扫描
1010
+ itemName = "硬编码密钥与助记词防泄漏扫描";
1011
+ fs.appendFileSync(REPORT_FILE, `\n[12/14] 敏感信息启发式扫描`);
1012
+ let scanRoot = path.join(OC, 'workspace');
1013
+ let dlpHits = 0;
1014
+ if (fs.existsSync(scanRoot)) {
1015
+ const skipExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.eot']);
1016
+ const hexPattern = /\b0x[a-fA-F0-9]{64}\b/;
1017
+ const mnemonicPattern = /\b([a-z]{3,12}\s+){11}([a-z]{3,12})\b|\b([a-z]{3,12}\s+){23}([a-z]{3,12})\b/;
1018
+
1019
+ function scanDir(dir) {
1020
+ try {
1021
+ const entries = fs.readdirSync(dir);
1022
+ for (const entry of entries) {
1023
+ if (entry === '.git' || entry === 'node_modules') continue;
1024
+ const fullPath = buildSafeChildPath(dir, entry);
1025
+ if (!fullPath) continue;
1026
+ const stat = fs.statSync(fullPath);
1027
+ if (stat.isDirectory()) {
1028
+ scanDir(fullPath);
1029
+ } else if (stat.isFile() && !skipExts.has(path.extname(fullPath).toLowerCase())) {
1030
+ try {
1031
+ const content = fs.readFileSync(fullPath, 'utf-8');
1032
+ if (hexPattern.test(content)) dlpHits++;
1033
+ if (mnemonicPattern.test(content)) dlpHits++;
1034
+ } catch (e) {}
1035
+ }
1036
+ }
1037
+ } catch (e) {}
1038
+ }
1039
+ scanDir(scanRoot);
1040
+ }
1041
+ let detail12 = `敏感信息启发式扫描 hits : ${dlpHits}`;
1042
+ if (dlpHits > 0) appendWarn(itemName, `⚠️ 检测到疑似明文敏感信息(${dlpHits}),请人工复核`, detail12);
1043
+ else appendInfo(itemName, "未发现明显私钥/助记词模式", detail12 + "\n✅ 未发现明显私钥/助记词模式");
1044
+
1045
+ // [13/14] 黄线操作交叉验证 (Sudo/特权提取 vs Memory)
1046
+ itemName = platform === 'win32' ? "特权提取操作对账审计" : "特权提权(Sudo)操作对账审计";
1047
+ fs.appendFileSync(REPORT_FILE, platform === 'win32'
1048
+ ? `\n[13/14] 黄线操作交叉验证 (特权提取 vs Memory)`
1049
+ : `\n[13/14] 黄线操作交叉验证 (Sudo vs Memory)`);
1050
+ let sudoCount = 0;
1051
+ if (platform === 'linux') {
1052
+ ['/var/log/auth.log', '/var/log/secure'].forEach(logPath => {
1053
+ sudoCount += countMatchesInFile(logPath, /sudo.*COMMAND/gim);
1054
+ });
1055
+ } else if (platform === 'darwin') {
1056
+ sudoCount = countMatchesInFile('/var/log/system.log', /sudo.*COMMAND/gim);
1057
+ } else if (platform === 'win32') {
1058
+ // Event ID 4672 = 特权提升
1059
+ let psOut = spawnCmd('powershell', ['-NoProfile', '-Command',
1060
+ `(Get-WinEvent -FilterHashtable @{LogName='Security'; ID=4672; StartTime=(Get-Date).AddDays(-1)} -ErrorAction SilentlyContinue | Measure-Object).Count`
1061
+ ]);
1062
+ sudoCount = parseInt(psOut, 10) || 0;
1063
+ }
1064
+
1065
+ // 获取最近24小时的 memory 文件(今天 + 昨天)
1066
+ function getMemoryFilesForLast24h(memoryDir) {
1067
+ try {
1068
+ const todayStr = getLocalDateStr(now);
1069
+ const yestStr = getLocalDateStr(yest);
1070
+ return fs.readdirSync(memoryDir)
1071
+ .filter(f => {
1072
+ if (!f.toLowerCase().endsWith('.md')) return false;
1073
+ // 匹配今天或昨天的日期前缀
1074
+ return f.startsWith(todayStr) || f.startsWith(yestStr);
1075
+ })
1076
+ .map(f => buildSafeChildPath(memoryDir, f))
1077
+ .filter(Boolean)
1078
+ .sort();
1079
+ } catch (e) {
1080
+ return [];
1081
+ }
1082
+ }
1083
+
1084
+ const memoryDir = path.join(OC, 'workspace/memory');
1085
+ const memFiles = getMemoryFilesForLast24h(memoryDir);
1086
+ let memCount = 0;
1087
+ for (const f of memFiles) {
1088
+ memCount += countMatchesInFile(f, platform === 'win32' ? /特权/gim : /sudo/gim);
1089
+ }
1090
+
1091
+ const privLabel = platform === 'win32' ? '特权提取' : 'Sudo';
1092
+ let detail13 = `${privLabel} Count (Today): ${sudoCount}\nMemory Count (Today): ${memCount}\nMemory Files (Matched): ${memFiles.length}\n${memFiles.length ? memFiles.join('\n') : '(none)'}`;
1093
+ if (sudoCount > 0 && memCount === 0) {
1094
+ appendWarn(itemName, `系统当日发生了 ${sudoCount} 次${privLabel},但 Agent 记忆中无记录`, detail13 + "\n⚠️ 怀疑 Agent 执行了未登记的特权越界操作");
1095
+ } else {
1096
+ appendInfo(itemName, `${privLabel}执行记录(${sudoCount}) 与 大脑记忆(${memCount}) 基本匹配`, detail13 + `\n✅ ${privLabel}执行记录与大脑记忆对账正常`);
1097
+ }
1098
+
1099
+
1100
+ // ==========================================
1101
+ // 板块四:生态与供应链安全
1102
+ // ==========================================
1103
+ fs.appendFileSync(REPORT_FILE, `\n\n--- [板块四] 生态与供应链安全 ---`);
1104
+
1105
+ // [14/14] Skill 恶意威胁情报扫描
1106
+ let itemNameSkill = "生态组件恶意威胁情报扫描";
1107
+ fs.appendFileSync(REPORT_FILE, `\n\n[14/14] skill扫描`);
1108
+
1109
+ let skillMetaList = [];
1110
+
1111
+ SKILL_SCAN_DIRS.forEach(skillRoot => {
1112
+ if (!fs.existsSync(skillRoot)) return;
1113
+ try {
1114
+ let skillDirs = fs.readdirSync(skillRoot);
1115
+ skillDirs.forEach(dir => {
1116
+ let baseDirPath = buildSafeChildPath(skillRoot, dir);
1117
+ if (!baseDirPath) return;
1118
+ let metaPath = `${baseDirPath}${path.sep}_meta.json`;
1119
+ let skillJsonPath = `${baseDirPath}${path.sep}skill.json`;
1120
+
1121
+ let meta = {};
1122
+ let skillJson = {};
1123
+
1124
+ if (fs.existsSync(metaPath)) {
1125
+ try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch (e) {}
1126
+ }
1127
+
1128
+ if (fs.existsSync(skillJsonPath)) {
1129
+ try { skillJson = JSON.parse(fs.readFileSync(skillJsonPath, 'utf-8')); } catch (e) {}
1130
+ }
1131
+
1132
+ const slug = meta.slug || skillJson.slug || dir;
1133
+ const ownerId = meta.ownerId || "";
1134
+ const version = meta.version || skillJson.version || "";
1135
+ const author = skillJson.author || meta.author || "";
1136
+
1137
+ skillMetaList.push({ slug, author, version, ownerId });
1138
+ });
1139
+ } catch (e) {}
1140
+ });
1141
+
1142
+ // ──────────────────────────────────────────
1143
+ // 网络请求封装
1144
+ // ──────────────────────────────────────────
1145
+
1146
+ function generateAgentId() {
1147
+ const idPath = path.join(OC, '.agent-id');
1148
+ if (fs.existsSync(idPath)) {
1149
+ return fs.readFileSync(idPath, 'utf-8').trim();
1150
+ }
1151
+ const id = crypto.randomUUID();
1152
+ try { fs.writeFileSync(idPath, id, { mode: 0o600 }); } catch (e) {}
1153
+ return id;
1154
+ }
1155
+
1156
+ function getActiveMac() {
1157
+ const interfaces = os.networkInterfaces();
1158
+ for (let name in interfaces) {
1159
+ for (let iface of interfaces[name]) {
1160
+ if (!iface.internal && iface.mac && iface.mac !== "00:00:00:00:00:00") {
1161
+ // 与 Java formatMac 一致:小写十六进制
1162
+ return iface.mac.toLowerCase();
1163
+ }
1164
+ }
1165
+ }
1166
+ return "UNKNOWN_MAC";
1167
+ }
1168
+
1169
+ function doSignedPost(apiUrl, apiPath, bodyObj, callback) {
1170
+ const mac = getActiveMac();
1171
+ const hostname = os.hostname();
1172
+ const timestamp = Math.floor(Date.now() / 1000).toString();
1173
+ const nonce = Math.random().toString(36).substring(2, 10);
1174
+ const method = "POST";
1175
+ const bodyStr = JSON.stringify(bodyObj);
1176
+
1177
+ // 计算签名(与 Java 一致:仅 mac + hostname + timestamp + nonce,UTF-8)
1178
+ const signContent = mac + "\n" + hostname + "\n" + timestamp + "\n" + nonce;
1179
+ const sign = crypto.createHash("sha256").update(signContent, 'utf8').digest("hex");
1180
+
1181
+ const urlObj = new URL(apiUrl);
1182
+ const isHttps = urlObj.protocol === 'https:';
1183
+ const defaultPort = isHttps ? 443 : 80;
1184
+ const port = urlObj.port || defaultPort;
1185
+ const options = {
1186
+ hostname: urlObj.hostname,
1187
+ port: port,
1188
+ path: apiPath,
1189
+ method: method,
1190
+ timeout: 10000, // 10秒超时
1191
+ headers: {
1192
+ 'Content-Type': 'application/json',
1193
+ 'Content-Length': Buffer.byteLength(bodyStr),
1194
+ 'X-MAC': mac,
1195
+ 'X-HOSTNAME': hostname,
1196
+ 'X-TIMESTAMP': timestamp,
1197
+ 'X-NONCE': nonce,
1198
+ 'X-SIGN': sign
1199
+ }
1200
+ };
1201
+
1202
+ // 排查用:请求前输出(调试完成后已注释)
1203
+ // console.log('[请求] ' + method + ' ' + apiUrl);
1204
+ // console.log('[请求] host=' + options.hostname + ' port=' + port + ' path=' + apiPath);
1205
+ // console.log('[请求] 签名头 X-MAC=' + mac + ' X-HOSTNAME=' + hostname + ' X-TIMESTAMP=' + timestamp + ' X-NONCE=' + nonce);
1206
+ // console.log('[请求] X-SIGN=' + sign.substring(0, 16) + '... body长度=' + bodyStr.length);
1207
+
1208
+ const requestModule = isHttps ? https : http;
1209
+ const req = requestModule.request(options, (res) => {
1210
+ let data = '';
1211
+ res.on('data', (chunk) => { data += chunk; });
1212
+ res.on('end', () => {
1213
+ const resInfo = {
1214
+ statusCode: res.statusCode,
1215
+ statusMessage: res.statusMessage,
1216
+ headers: res.headers,
1217
+ body: data
1218
+ };
1219
+ // 排查用:响应输出(调试完成后已注释)
1220
+ // console.log('[响应] HTTP ' + res.statusCode + ' ' + (res.statusMessage || ''));
1221
+ // if (res.headers && (res.headers['content-type'] || res.headers['Content-Type'])) {
1222
+ // console.log('[响应] Content-Type: ' + (res.headers['content-type'] || res.headers['Content-Type']));
1223
+ // }
1224
+ // console.log('[响应] body长度=' + data.length);
1225
+ // if (data.length > 0) {
1226
+ // const preview = data.length > 600 ? data.substring(0, 600) + '...' : data;
1227
+ // console.log('[响应] body预览:\n' + preview);
1228
+ // }
1229
+ if (res.statusCode >= 200 && res.statusCode < 300) {
1230
+ callback(null, resInfo);
1231
+ } else {
1232
+ callback(`HTTP ${res.statusCode} ${res.statusMessage || ''}`.trim(), resInfo);
1233
+ }
1234
+ });
1235
+ });
1236
+
1237
+ req.on('error', (e) => {
1238
+ // console.log('[错误] 请求异常: ' + e.message);
1239
+ callback(e.message, null);
1240
+ });
1241
+ req.on('timeout', () => {
1242
+ // console.log('[错误] 请求超时');
1243
+ req.destroy();
1244
+ callback("请求超时", null);
1245
+ });
1246
+
1247
+ req.write(bodyStr);
1248
+ req.end();
1249
+ }
1250
+
1251
+ // ──────────────────────────────────────────
1252
+ // 最终收尾流程
1253
+ // ──────────────────────────────────────────
1254
+ function finalizeAndPushData() {
1255
+ const agentId = generateAgentId();
1256
+ const status = RED_COUNT > 0 ? "warning" : "success";
1257
+
1258
+ const checkedCount = ITEM_SEQ - SKIP_COUNT;
1259
+ const passCount = checkedCount - RED_COUNT;
1260
+
1261
+ let outputObj = {
1262
+ report_time: REPORT_TIME,
1263
+ status,
1264
+ red_item: RED_COUNT,
1265
+ checkedCount: checkedCount,
1266
+ passCount: passCount,
1267
+ agent_id: agentId,
1268
+ data: JSON_DATA
1269
+ };
1270
+
1271
+ const pushObj = {
1272
+ report_time: REPORT_TIME,
1273
+ status,
1274
+ red_item: RED_COUNT,
1275
+ red_count: RED_COUNT,
1276
+ checkedCount: checkedCount,
1277
+ passCount: passCount,
1278
+ agent_id: agentId,
1279
+ data: JSON_DATA.map(({ item, brief }) => ({ item, brief }))
1280
+ };
1281
+
1282
+ fs.writeFileSync(JSON_OUT_FILE, JSON.stringify(outputObj, null, 2), { encoding: 'utf-8', mode: 0o600 });
1283
+
1284
+ SUMMARY += `${COLORS.dim}────────────────────────────────────────────────────────────────────────${COLORS.reset}\n`;
1285
+ SUMMARY += ` ${COLORS.bright}检测统计:${COLORS.reset} ${COLORS.green}PASS ${passCount}${COLORS.reset} ${COLORS.red}FAIL ${RED_COUNT}${COLORS.reset} ${COLORS.magenta}SKIP ${SKIP_COUNT}${COLORS.reset}\n`;
1286
+
1287
+ if (!PUSH_ENABLED) {
1288
+ // 本地离线模式:不展示安全得分,只展示检测统计
1289
+ SUMMARY += `${COLORS.dim}────────────────────────────────────────────────────────────────────────${COLORS.reset}\n`;
1290
+ console.log(SUMMARY);
1291
+ console.log(`${COLORS.dim}详细审计报告已保存至: \`${REPORT_FILE}\`${COLORS.reset}`);
1292
+ console.log(`\n${COLORS.dim}当前为本地离线模式。如需参与全网威胁态势感知,请附加 --push 参数运行。${COLORS.reset}`);
1293
+ process.exit(0);
1294
+ return;
1295
+ }
1296
+
1297
+ // console.log(`\n${COLORS.cyan}正在向云端安全中心同步态势感知数据...${COLORS.reset}`);
1298
+
1299
+ const pushApiUrl = "https://auth.ctct.cn:10020/changeway-open/api/pushAuditData";
1300
+ const pushApiPath = "/changeway-open/api/pushAuditData";
1301
+
1302
+ doSignedPost(pushApiUrl, pushApiPath, pushObj, (err, resData) => {
1303
+ let score = null;
1304
+
1305
+ // if (err) {
1306
+ // console.log(`${COLORS.red}✖ Telemetry upload failed: ${err}${COLORS.reset}`);
1307
+ // } else {
1308
+ // console.log(`${COLORS.green}✔ Telemetry upload successful${COLORS.reset}`);
1309
+ // }
1310
+
1311
+ // 仅在 PUSH 模式下,根据云端返回结果展示系统安全得分
1312
+ if (!err && resData) {
1313
+ try {
1314
+ // doSignedPost 返回的是 { statusCode, headers, body } 结构,这里只解析 body
1315
+ let rawBody = null;
1316
+ if (typeof resData === 'string') {
1317
+ rawBody = resData;
1318
+ } else if (resData && typeof resData.body === 'string') {
1319
+ rawBody = resData.body;
1320
+ }
1321
+
1322
+ if (rawBody && rawBody.trim() !== '') {
1323
+ const apiRes = JSON.parse(rawBody);
1324
+ const dataField = apiRes && apiRes.data;
1325
+
1326
+ // 优先使用顶层 data 字段作为分数(支持 number 或可解析为 number 的字符串)
1327
+ if (typeof dataField === 'number') {
1328
+ score = dataField;
1329
+ } else if (typeof dataField === 'string' && dataField.trim() !== '' && !Number.isNaN(Number(dataField))) {
1330
+ score = Number(dataField);
1331
+ } else if (dataField && typeof dataField.score === 'number') {
1332
+ // 兼容旧版 data.score 结构
1333
+ score = dataField.score;
1334
+ }
1335
+ }
1336
+ } catch (e) {
1337
+ // 如果解析失败则不展示分数
1338
+ }
1339
+ }
1340
+
1341
+ if (typeof score === 'number') {
1342
+ const safeScore = Math.max(0, Math.min(100, Math.round(score)));
1343
+ let scoreColor = safeScore >= 90 ? COLORS.green : (safeScore >= 70 ? COLORS.yellow : COLORS.red);
1344
+ const scoreLine = ` ${COLORS.bright}系统安全得分:${COLORS.reset} ${scoreColor}${safeScore} / 100${COLORS.reset}\n`;
1345
+ // 将系统安全得分行放在报告最前面
1346
+ // SUMMARY = scoreLine + SUMMARY;
1347
+ SUMMARY += scoreLine
1348
+ }
1349
+
1350
+ SUMMARY += `${COLORS.dim}────────────────────────────────────────────────────────────────────────${COLORS.reset}\n`;
1351
+ console.log(SUMMARY);
1352
+ console.log(`${COLORS.dim}详细审计报告已保存至: \`${REPORT_FILE}\`${COLORS.reset}`);
1353
+
1354
+ process.exit(0);
1355
+ });
1356
+ }
1357
+
1358
+ // ──────────────────────────────────────────
1359
+ // 执行逻辑
1360
+ // --push 模式:查威胁情报 → 上报数据
1361
+ // 默认模式:仅本地扫描 + 本地落盘,不发起任何网络请求
1362
+ // ──────────────────────────────────────────
1363
+ if (skillMetaList.length === 0) {
1364
+ appendInfo(itemNameSkill, "未发现已安装的 Skill", "当前环境暂无已安装的 Skill 组件,跳过检测。");
1365
+ finalizeAndPushData();
1366
+ } else {
1367
+ let scannedSummary = `>>> 本机已安装的 Skill 组件清单 (共 ${skillMetaList.length} 个):\n` +
1368
+ skillMetaList.map(s => ` - ${s.slug} (v${s.version}) [Owner: ${s.ownerId}]`).join('\n');
1369
+
1370
+ if (!PUSH_ENABLED) {
1371
+ appendInfo(
1372
+ itemNameSkill,
1373
+ `已列出本机已安装的 ${skillMetaList.length} 个 Skill 组件(威胁情报查询需加 --push)`,
1374
+ scannedSummary
1375
+ );
1376
+ finalizeAndPushData();
1377
+ } else {
1378
+ const assessApiUrl = "https://auth.ctct.cn:10020/changeway-open/api/skills/assessment";
1379
+ const assessApiPath = "/changeway-open/api/skills/assessment";
1380
+
1381
+ doSignedPost(assessApiUrl, assessApiPath, { data: skillMetaList }, (err, apiResRaw) => {
1382
+ let intelHits = 0;
1383
+ let hitDetails = [];
1384
+
1385
+ if (!err && apiResRaw) {
1386
+ try {
1387
+ let apiRes = JSON.parse(apiResRaw.body || apiResRaw);
1388
+ if (apiRes.data && Array.isArray(apiRes.data)) {
1389
+ apiRes.data.forEach(item => {
1390
+ if (item.matched_intel && Array.isArray(item.matched_intel) && item.matched_intel.length > 0) {
1391
+ item.matched_intel.forEach(intel => {
1392
+ intelHits++;
1393
+ const maliciousDesc =
1394
+ intel.is_malicious === 1 || intel.is_malicious === '1'
1395
+ ? '存在恶意'
1396
+ : (intel.is_malicious === 0 || intel.is_malicious === '0'
1397
+ ? '不存在恶意'
1398
+ : '无标记');
1399
+
1400
+ // 风险等级映射:英文 -> 中文 + emoji
1401
+ const severityMap = {
1402
+ 'Benign': '✅ 良性',
1403
+ 'Suspicious': '⚠️ 可疑',
1404
+ 'Malicious': '🚨 恶意'
1405
+ };
1406
+ const severityDisplay = severityMap[intel.severity] || (intel.severity || 'UNKNOWN');
1407
+
1408
+ hitDetails.push(
1409
+ `- 命中威胁情报: [${item.slug} ${item.version}] (Owner: ${item.author})\n` +
1410
+ ` 风险等级 (severity): ${severityDisplay}\n` +
1411
+ ` 情报详情: ${JSON.stringify(intel.info || {})}`
1412
+ );
1413
+ });
1414
+ }
1415
+ });
1416
+ }
1417
+ } catch (parseErr) {
1418
+ hitDetails.push(`⚠️ API 响应解析失败: ${parseErr.message}`);
1419
+ }
1420
+ } else {
1421
+ hitDetails.push(`⚠️ 威胁情报 API 请求异常: ${err}`);
1422
+ }
1423
+
1424
+ let finalDetailText = `${scannedSummary}\n\n>>> 威胁情报扫描结果:\n`;
1425
+
1426
+ if (intelHits > 0) {
1427
+ finalDetailText += hitDetails.join('\n\n');
1428
+ appendWarn(itemNameSkill, `⚠️ 注意!发现 ${intelHits} 个命中情报的组件!`, finalDetailText);
1429
+ }
1430
+ // else if (hitDetails.length > 0 && intelHits === 0) {
1431
+ // console.log(hitDetails.join('\n'))
1432
+ // finalDetailText += hitDetails.join('\n');
1433
+ // appendWarn(itemNameSkill, `⚠️ API 服务异常或鉴权失败`, finalDetailText);
1434
+ // }
1435
+ else {
1436
+ finalDetailText = scannedSummary;
1437
+ appendInfo(
1438
+ itemNameSkill,
1439
+ `已列出本机已安装的 ${skillMetaList.length} 个 Skill 组件`,
1440
+ finalDetailText
1441
+ );
1442
+ }
1443
+
1444
+ finalizeAndPushData();
1445
+ });
1446
+ }
1447
+ }