mohuclaw 1.0.1 → 2.0.1

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/README.md CHANGED
@@ -13,6 +13,8 @@ OpenClaw 安全审计工具,支持终端和 Web 双界面交互式安全分析
13
13
  - **自动修复**:一键修复检测到的安全问题
14
14
  - **IOC 威胁情报**:内置恶意 IP/域名检测
15
15
  - **多语言支持**:中文 / 英文界面切换
16
+ - **报告导出**:支持 JSON / HTML / TXT 格式
17
+ - **REST API**:支持第三方系统集成
16
18
 
17
19
  ## 安装
18
20
 
@@ -45,6 +47,95 @@ mohuclaw-tui
45
47
  | `f` | 执行修复 |
46
48
  | `Esc` | 退出选择 |
47
49
 
50
+ ## 报告导出
51
+
52
+ 扫描完成后,点击 **导出报告** 按钮可导出:
53
+
54
+ - **JSON** - 结构化数据,适合程序处理
55
+ - **HTML** - 可打印的报告,适合人工阅读
56
+ - **TXT** - 纯文本格式,适合日志归档
57
+
58
+ ## REST API
59
+
60
+ ### 创建 API Key
61
+
62
+ ```bash
63
+ curl -X POST http://localhost:12340/api/auth/keys \
64
+ -H "Content-Type: application/json" \
65
+ -d '{"name":"my-app"}'
66
+ ```
67
+
68
+ ### 启动扫描
69
+
70
+ ```bash
71
+ curl -X POST http://localhost:12340/api/scan/run \
72
+ -H "X-API-Key: YOUR_KEY"
73
+ ```
74
+
75
+ ### 获取扫描结果
76
+
77
+ ```bash
78
+ curl http://localhost:12340/api/scan/TASK_ID \
79
+ -H "X-API-Key: YOUR_KEY"
80
+ ```
81
+
82
+ ### 导出报告
83
+
84
+ ```bash
85
+ # JSON 格式
86
+ curl http://localhost:12340/api/export/json/TASK_ID \
87
+ -H "X-API-Key: YOUR_KEY" \
88
+ -o report.json
89
+
90
+ # HTML 格式
91
+ curl http://localhost:12340/api/export/html/TASK_ID \
92
+ -H "X-API-Key: YOUR_KEY" \
93
+ -o report.html
94
+ ```
95
+
96
+ ### API 文档
97
+
98
+ 访问 http://localhost:12340/api/docs 查看完整 API 文档。
99
+
100
+ ### 主要端点
101
+
102
+ | 端点 | 方法 | 说明 |
103
+ |------|------|------|
104
+ | `/api/health` | GET | 健康检查 |
105
+ | `/api/auth/keys` | POST | 创建 API Key |
106
+ | `/api/auth/keys` | GET | 列出所有 Key |
107
+ | `/api/auth/keys/:key` | DELETE | 删除 Key |
108
+ | `/api/scan/run` | POST | 启动扫描 |
109
+ | `/api/scan/:taskId` | GET | 获取扫描结果 |
110
+ | `/api/scan/:taskId/cancel` | POST | 取消扫描 |
111
+ | `/api/scan/latest` | GET | 获取最新结果 |
112
+ | `/api/scan/history` | GET | 获取扫描历史 |
113
+ | `/api/export/json/:taskId` | GET | 导出 JSON 报告 |
114
+ | `/api/export/html/:taskId` | GET | 导出 HTML 报告 |
115
+ | `/api/export/txt/:taskId` | GET | 导出文本报告 |
116
+
117
+ ### 第三方集成示例
118
+
119
+ **Python 示例:**
120
+
121
+ ```python
122
+ import requests
123
+
124
+ API_URL = "http://localhost:12340"
125
+ API_KEY = "mohu_xxx_yyy"
126
+
127
+ headers = {"X-API-Key": API_KEY}
128
+
129
+ # 启动扫描
130
+ resp = requests.post(f"{API_URL}/api/scan/run", headers=headers)
131
+ task_id = resp.json()["taskId"]
132
+
133
+ # 获取结果
134
+ result = requests.get(f"{API_URL}/api/scan/{task_id}", headers=headers).json()
135
+ print(f"状态: {result['status']}")
136
+ print(f"严重问题: {result['summary']['critical']}")
137
+ ```
138
+
48
139
  ## 配置
49
140
 
50
141
  ```bash
@@ -100,7 +100,11 @@
100
100
  removeSelected: '移除选中',
101
101
  removing: '正在移除',
102
102
  applyFix: '执行自动修复',
103
- applying: '正在修复...'
103
+ applying: '正在修复...',
104
+ export: '导出报告',
105
+ exportJSON: 'JSON',
106
+ exportHTML: 'HTML',
107
+ exportTXT: 'TXT'
104
108
  },
105
109
  tabs: {
106
110
  overview: '概览',
@@ -183,7 +187,11 @@
183
187
  removeSelected: 'Remove Selected',
184
188
  removing: 'Removing',
185
189
  applyFix: 'Apply Auto-Fix',
186
- applying: 'Applying...'
190
+ applying: 'Applying...',
191
+ export: 'Export',
192
+ exportJSON: 'JSON Report',
193
+ exportHTML: 'HTML Report',
194
+ exportTXT: 'Text Report'
187
195
  },
188
196
  tabs: {
189
197
  overview: 'Overview',
@@ -882,7 +890,7 @@
882
890
  }, [isScanning]);
883
891
 
884
892
  return (
885
- <div className="glass rounded-xl p-4 h-[600px] overflow-y-auto bg-gray-900/50">
893
+ <div className="glass rounded-xl p-4 h-[600px] overflow-y-auto bg-gray-900/50 overflow-visible">
886
894
  {logs.length === 0 ? (
887
895
  <div className="text-center py-12">
888
896
  <DocumentTextIcon />
@@ -987,6 +995,7 @@
987
995
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
988
996
  const [openclawHome, setOpenclawHome] = useState('');
989
997
  const [language, setLanguage] = useState('zh');
998
+ const [showExportMenu, setShowExportMenu] = useState(false);
990
999
  const eventSourceRef = useRef(null);
991
1000
 
992
1001
  const t = translations[language];
@@ -1024,6 +1033,16 @@
1024
1033
  setOpenclawHome(value);
1025
1034
  };
1026
1035
 
1036
+ const exportReport = (format) => {
1037
+ if (!currentTaskId) {
1038
+ alert(language === 'zh' ? '没有可导出的扫描结果' : 'No scan result to export');
1039
+ return;
1040
+ }
1041
+
1042
+ const url = `/api/export/${format}/${currentTaskId}`;
1043
+ window.open(url, '_blank');
1044
+ };
1045
+
1027
1046
  const connectToSSE = useCallback((taskId) => {
1028
1047
  if (eventSourceRef.current) {
1029
1048
  eventSourceRef.current.close();
@@ -1066,7 +1085,8 @@
1066
1085
  setResult(data);
1067
1086
  setStatus(data.status);
1068
1087
  setIsScanning(false);
1069
- setCurrentTaskId(null);
1088
+ // Keep taskId for export functionality
1089
+ // setCurrentTaskId(null);
1070
1090
 
1071
1091
  // Update history
1072
1092
  setHistory(prev => [{
@@ -1229,6 +1249,22 @@
1229
1249
  {t.buttons.settings}
1230
1250
  </button>
1231
1251
 
1252
+ {/* Export Button */}
1253
+ {result && (
1254
+ <button
1255
+ onClick={() => setShowExportMenu(!showExportMenu)}
1256
+ className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded text-sm transition-colors"
1257
+ >
1258
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1259
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
1260
+ </svg>
1261
+ {t.buttons.export}
1262
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1263
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
1264
+ </svg>
1265
+ </button>
1266
+ )}
1267
+
1232
1268
  {isScanning ? (
1233
1269
  <button
1234
1270
  onClick={cancelScan}
@@ -1250,6 +1286,39 @@
1250
1286
  </div>
1251
1287
  </header>
1252
1288
 
1289
+ {/* Export Menu - Fixed Position */}
1290
+ {showExportMenu && (
1291
+ <>
1292
+ <div
1293
+ className="fixed inset-0 z-[998]"
1294
+ onClick={() => setShowExportMenu(false)}
1295
+ />
1296
+ <div className="fixed z-[999] bg-gray-800 rounded-lg shadow-2xl border border-gray-600" style={{ top: '70px', right: '200px' }}>
1297
+ <button
1298
+ onClick={() => { exportReport('json'); setShowExportMenu(false); }}
1299
+ className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-gray-700 rounded-t-lg flex items-center gap-3"
1300
+ >
1301
+ <span className="w-6 h-6 flex items-center justify-center bg-blue-500/20 rounded text-blue-400 text-xs font-mono">JSON</span>
1302
+ {t.buttons.exportJSON}
1303
+ </button>
1304
+ <button
1305
+ onClick={() => { exportReport('html'); setShowExportMenu(false); }}
1306
+ className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-gray-700 flex items-center gap-3"
1307
+ >
1308
+ <span className="w-6 h-6 flex items-center justify-center bg-green-500/20 rounded text-green-400 text-xs font-mono">HTML</span>
1309
+ {t.buttons.exportHTML}
1310
+ </button>
1311
+ <button
1312
+ onClick={() => { exportReport('txt'); setShowExportMenu(false); }}
1313
+ className="w-full px-4 py-2.5 text-left text-sm text-white hover:bg-gray-700 rounded-b-lg flex items-center gap-3"
1314
+ >
1315
+ <span className="w-6 h-6 flex items-center justify-center bg-yellow-500/20 rounded text-yellow-400 text-xs font-mono">TXT</span>
1316
+ {t.buttons.exportTXT}
1317
+ </button>
1318
+ </div>
1319
+ </>
1320
+ )}
1321
+
1253
1322
  {/* Progress Bar */}
1254
1323
  {isScanning && (
1255
1324
  <div className="max-w-7xl mx-auto mb-6">
@@ -46,6 +46,79 @@ const scanTasks = new Map();
46
46
  const scanResults = new Map();
47
47
  let currentScanTaskId = null;
48
48
 
49
+ // API Keys storage (in production, use a database)
50
+ const API_KEYS_FILE = path.join(PACKAGE_DIR, '.api_keys.json');
51
+ let apiKeys = new Map();
52
+
53
+ function loadApiKeys() {
54
+ try {
55
+ if (fs.existsSync(API_KEYS_FILE)) {
56
+ const data = fs.readFileSync(API_KEYS_FILE, 'utf8');
57
+ const keys = JSON.parse(data);
58
+ apiKeys = new Map(Object.entries(keys));
59
+ }
60
+ } catch (error) {
61
+ console.error('Error loading API keys:', error);
62
+ }
63
+ }
64
+
65
+ function saveApiKeys() {
66
+ try {
67
+ const obj = Object.fromEntries(apiKeys);
68
+ fs.writeFileSync(API_KEYS_FILE, JSON.stringify(obj, null, 2), 'utf8');
69
+ } catch (error) {
70
+ console.error('Error saving API keys:', error);
71
+ }
72
+ }
73
+
74
+ function generateApiKey() {
75
+ return `mohu_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 15)}`;
76
+ }
77
+
78
+ // Initialize API keys on startup
79
+ loadApiKeys();
80
+
81
+ // API Key authentication middleware
82
+ function apiKeyAuth(req, res, next) {
83
+ // Skip auth for web UI and public endpoints
84
+ const publicPaths = ['/', '/index.html', '/api/health', '/api/docs', '/api/auth/login'];
85
+ if (publicPaths.includes(req.path) || req.path.startsWith('/api/auth/')) {
86
+ return next();
87
+ }
88
+
89
+ // Check for API key in header or query
90
+ const apiKey = req.headers['x-api-key'] || req.query.apiKey;
91
+
92
+ if (!apiKey) {
93
+ return res.status(401).json({
94
+ error: 'API key required',
95
+ message: 'Provide X-API-Key header or apiKey query parameter'
96
+ });
97
+ }
98
+
99
+ const keyData = apiKeys.get(apiKey);
100
+ if (!keyData) {
101
+ return res.status(401).json({ error: 'Invalid API key' });
102
+ }
103
+
104
+ // Update last used
105
+ keyData.lastUsed = new Date().toISOString();
106
+ saveApiKeys();
107
+
108
+ req.apiKey = keyData;
109
+ next();
110
+ }
111
+
112
+ // Apply API key auth to /api routes (except public ones)
113
+ app.use('/api', (req, res, next) => {
114
+ // Allow web UI to access without API key (localhost only)
115
+ const isLocalhost = req.ip === '::1' || req.ip === '127.0.0.1' || req.hostname === 'localhost';
116
+ if (isLocalhost && !req.headers['x-api-key'] && !req.query.apiKey) {
117
+ return next();
118
+ }
119
+ return apiKeyAuth(req, res, next);
120
+ });
121
+
49
122
  // Settings storage functions
50
123
  function loadSettings() {
51
124
  try {
@@ -87,6 +160,153 @@ app.use((req, res, next) => {
87
160
  next();
88
161
  });
89
162
 
163
+ // ==================== API Documentation ====================
164
+ app.get('/api/docs', (req, res) => {
165
+ res.json({
166
+ name: 'MohuClaw API',
167
+ version: '1.0.1',
168
+ description: 'REST API for MohuClaw Security Scanner',
169
+ authentication: {
170
+ type: 'API Key',
171
+ header: 'X-API-Key',
172
+ query: 'apiKey',
173
+ description: 'Include API key in X-API-Key header or apiKey query parameter'
174
+ },
175
+ endpoints: {
176
+ 'GET /api/health': {
177
+ description: 'Health check',
178
+ auth: false
179
+ },
180
+ 'POST /api/auth/keys': {
181
+ description: 'Create new API key',
182
+ auth: false,
183
+ body: { name: 'string (optional)' }
184
+ },
185
+ 'GET /api/auth/keys': {
186
+ description: 'List all API keys',
187
+ auth: true
188
+ },
189
+ 'DELETE /api/auth/keys/:key': {
190
+ description: 'Delete an API key',
191
+ auth: true
192
+ },
193
+ 'POST /api/scan/run': {
194
+ description: 'Start a security scan',
195
+ auth: true,
196
+ returns: { taskId: 'string' }
197
+ },
198
+ 'GET /api/scan/sse': {
199
+ description: 'SSE stream for scan progress',
200
+ auth: true,
201
+ query: { taskId: 'string' }
202
+ },
203
+ 'GET /api/scan/:taskId': {
204
+ description: 'Get scan result by task ID',
205
+ auth: true
206
+ },
207
+ 'POST /api/scan/:taskId/cancel': {
208
+ description: 'Cancel a running scan',
209
+ auth: true
210
+ },
211
+ 'GET /api/scan/latest': {
212
+ description: 'Get latest scan result',
213
+ auth: true
214
+ },
215
+ 'GET /api/scan/history': {
216
+ description: 'Get scan history',
217
+ auth: true
218
+ },
219
+ 'GET /api/export/json/:taskId': {
220
+ description: 'Export scan result as JSON',
221
+ auth: true
222
+ },
223
+ 'GET /api/export/html/:taskId': {
224
+ description: 'Export scan result as HTML',
225
+ auth: true
226
+ },
227
+ 'GET /api/export/txt/:taskId': {
228
+ description: 'Export scan result as plain text',
229
+ auth: true
230
+ },
231
+ 'GET /api/settings/openclaw_home': {
232
+ description: 'Get OPENCLAW_HOME setting',
233
+ auth: true
234
+ },
235
+ 'POST /api/settings/openclaw_home': {
236
+ description: 'Set OPENCLAW_HOME setting',
237
+ auth: true,
238
+ body: { value: 'string' }
239
+ }
240
+ },
241
+ examples: {
242
+ createApiKey: `curl -X POST http://localhost:12340/api/auth/keys -H "Content-Type: application/json" -d '{"name":"my-app"}'`,
243
+ startScan: `curl -X POST http://localhost:12340/api/scan/run -H "X-API-Key: YOUR_KEY"`,
244
+ getResult: `curl http://localhost:12340/api/scan/TASK_ID -H "X-API-Key: YOUR_KEY"`,
245
+ exportReport: `curl http://localhost:12340/api/export/json/TASK_ID -H "X-API-Key: YOUR_KEY" -o report.json`
246
+ }
247
+ });
248
+ });
249
+
250
+ // Health check endpoint
251
+ app.get('/api/health', (req, res) => {
252
+ res.json({
253
+ status: 'ok',
254
+ timestamp: new Date().toISOString(),
255
+ version: '1.0.1',
256
+ uptime: process.uptime()
257
+ });
258
+ });
259
+
260
+ // ==================== API Key Management ====================
261
+ app.post('/api/auth/keys', (req, res) => {
262
+ const { name = 'default' } = req.body;
263
+ const key = generateApiKey();
264
+
265
+ apiKeys.set(key, {
266
+ name,
267
+ createdAt: new Date().toISOString(),
268
+ lastUsed: null
269
+ });
270
+ saveApiKeys();
271
+
272
+ res.status(201).json({
273
+ key,
274
+ name,
275
+ message: 'API key created. Store it securely - it cannot be retrieved again.'
276
+ });
277
+ });
278
+
279
+ app.get('/api/auth/keys', (req, res) => {
280
+ const keys = Array.from(apiKeys.entries()).map(([key, data]) => ({
281
+ key: key.substring(0, 10) + '...', // Masked
282
+ name: data.name,
283
+ createdAt: data.createdAt,
284
+ lastUsed: data.lastUsed
285
+ }));
286
+ res.json({ keys });
287
+ });
288
+
289
+ app.delete('/api/auth/keys/:key', (req, res) => {
290
+ const { key } = req.params;
291
+
292
+ // Find key by prefix match
293
+ let foundKey = null;
294
+ for (const k of apiKeys.keys()) {
295
+ if (k.startsWith(key)) {
296
+ foundKey = k;
297
+ break;
298
+ }
299
+ }
300
+
301
+ if (!foundKey) {
302
+ return res.status(404).json({ error: 'API key not found' });
303
+ }
304
+
305
+ apiKeys.delete(foundKey);
306
+ saveApiKeys();
307
+ res.json({ message: 'API key deleted' });
308
+ });
309
+
90
310
  // Generate unique task ID
91
311
  function generateTaskId() {
92
312
  return `scan_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
@@ -869,6 +1089,204 @@ app.post('/api/remediate/execute', async (req, res) => {
869
1089
  }
870
1090
  });
871
1091
 
1092
+ // Export scan report as JSON
1093
+ app.get('/api/export/json/:taskId', (req, res) => {
1094
+ const { taskId } = req.params;
1095
+ const result = scanResults.get(taskId);
1096
+
1097
+ if (!result) {
1098
+ return res.status(404).json({ error: 'Scan result not found' });
1099
+ }
1100
+
1101
+ const report = {
1102
+ meta: {
1103
+ tool: 'MohuClaw',
1104
+ version: '1.0.1',
1105
+ generatedAt: new Date().toISOString(),
1106
+ taskId: taskId
1107
+ },
1108
+ summary: result.summary,
1109
+ status: result.status,
1110
+ categories: result.categories,
1111
+ items: result.items || []
1112
+ };
1113
+
1114
+ res.setHeader('Content-Type', 'application/json');
1115
+ res.setHeader('Content-Disposition', `attachment; filename="mohuclaw-report-${taskId}.json"`);
1116
+ res.json(report);
1117
+ });
1118
+
1119
+ // Export scan report as HTML
1120
+ app.get('/api/export/html/:taskId', (req, res) => {
1121
+ const { taskId } = req.params;
1122
+ const result = scanResults.get(taskId);
1123
+
1124
+ if (!result) {
1125
+ return res.status(404).json({ error: 'Scan result not found' });
1126
+ }
1127
+
1128
+ const statusColors = {
1129
+ SECURE: '#22c55e',
1130
+ WARNING: '#eab308',
1131
+ COMPROMISED: '#ef4444'
1132
+ };
1133
+
1134
+ const categoryNames = {
1135
+ NETWORK_EXPOSURE: '网络暴露',
1136
+ ACCESS_CONTROL: '访问控制',
1137
+ EXECUTION_SANDBOX: '执行沙箱',
1138
+ CREDENTIAL_STORAGE: '凭证存储',
1139
+ MEMORY_POISONING: '内存投毒',
1140
+ SUPPLY_CHAIN: '供应链安全',
1141
+ RESOURCE_COST: '资源成本'
1142
+ };
1143
+
1144
+ const html = `<!DOCTYPE html>
1145
+ <html lang="zh-CN">
1146
+ <head>
1147
+ <meta charset="UTF-8">
1148
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1149
+ <title>MohuClaw 安全报告 - ${taskId}</title>
1150
+ <style>
1151
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1152
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0b0f19; color: #e5e7eb; padding: 2rem; }
1153
+ .container { max-width: 1200px; margin: 0 auto; }
1154
+ .header { text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, #1e3a5f 0%, #1f2937 100%); border-radius: 1rem; }
1155
+ .header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
1156
+ .header .meta { color: #9ca3af; font-size: 0.875rem; }
1157
+ .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
1158
+ .summary-card { padding: 1.5rem; border-radius: 0.5rem; text-align: center; }
1159
+ .summary-card.critical { background: rgba(239, 68, 68, 0.2); border: 1px solid #ef4444; }
1160
+ .summary-card.warning { background: rgba(234, 179, 8, 0.2); border: 1px solid #eab308; }
1161
+ .summary-card.clean { background: rgba(34, 197, 94, 0.2); border: 1px solid #22c55e; }
1162
+ .summary-card .value { font-size: 2.5rem; font-weight: bold; }
1163
+ .summary-card .label { font-size: 0.875rem; color: #9ca3af; margin-top: 0.25rem; }
1164
+ .status-badge { display: inline-block; padding: 0.5rem 1rem; border-radius: 0.5rem; font-weight: bold; text-transform: uppercase; }
1165
+ .categories { margin-bottom: 2rem; }
1166
+ .category { background: #1f2937; border-radius: 0.5rem; margin-bottom: 1rem; overflow: hidden; }
1167
+ .category-header { padding: 1rem; background: #374151; display: flex; justify-content: space-between; align-items: center; }
1168
+ .category-header h3 { font-size: 1rem; }
1169
+ .category-stats { display: flex; gap: 1rem; font-size: 0.875rem; }
1170
+ .category-stats span { padding: 0.25rem 0.5rem; border-radius: 0.25rem; }
1171
+ .items { padding: 1rem; }
1172
+ .item { padding: 1rem; border-left: 3px solid #4b5563; margin-bottom: 0.5rem; background: #111827; border-radius: 0 0.5rem 0.5rem 0; }
1173
+ .item.critical { border-left-color: #ef4444; }
1174
+ .item.warning { border-left-color: #eab308; }
1175
+ .item .title { font-weight: 600; margin-bottom: 0.25rem; }
1176
+ .item .description { font-size: 0.875rem; color: #9ca3af; }
1177
+ .footer { text-align: center; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid #374151; color: #6b7280; font-size: 0.875rem; }
1178
+ </style>
1179
+ </head>
1180
+ <body>
1181
+ <div class="container">
1182
+ <div class="header">
1183
+ <h1>🛡️ MohuClaw 安全报告</h1>
1184
+ <div class="meta">
1185
+ <span>任务ID: ${taskId}</span> |
1186
+ <span>生成时间: ${new Date().toLocaleString('zh-CN')}</span> |
1187
+ <span class="status-badge" style="background: ${statusColors[result.status] || '#6b7280'}">${result.status}</span>
1188
+ </div>
1189
+ </div>
1190
+
1191
+ <div class="summary">
1192
+ <div class="summary-card critical">
1193
+ <div class="value" style="color: #ef4444">${result.summary.critical}</div>
1194
+ <div class="label">严重问题</div>
1195
+ </div>
1196
+ <div class="summary-card warning">
1197
+ <div class="value" style="color: #eab308">${result.summary.warnings}</div>
1198
+ <div class="label">警告</div>
1199
+ </div>
1200
+ <div class="summary-card clean">
1201
+ <div class="value" style="color: #22c55e">${result.summary.clean}</div>
1202
+ <div class="label">通过检查</div>
1203
+ </div>
1204
+ </div>
1205
+
1206
+ <div class="categories">
1207
+ ${(result.categories || []).map(cat => `
1208
+ <div class="category">
1209
+ <div class="category-header">
1210
+ <h3>${categoryNames[cat.name] || cat.name}</h3>
1211
+ <div class="category-stats">
1212
+ ${cat.critical > 0 ? `<span style="background: rgba(239,68,68,0.2); color: #ef4444">严重: ${cat.critical}</span>` : ''}
1213
+ ${cat.warnings > 0 ? `<span style="background: rgba(234,179,8,0.2); color: #eab308">警告: ${cat.warnings}</span>` : ''}
1214
+ ${cat.clean > 0 ? `<span style="background: rgba(34,197,94,0.2); color: #22c55e">通过: ${cat.clean}</span>` : ''}
1215
+ </div>
1216
+ </div>
1217
+ </div>
1218
+ `).join('')}
1219
+ </div>
1220
+
1221
+ ${(result.items || []).length > 0 ? `
1222
+ <div class="items-section">
1223
+ <h2 style="margin-bottom: 1rem;">问题详情</h2>
1224
+ ${result.items.map(item => `
1225
+ <div class="item ${item.severity}">
1226
+ <div class="title">${item.title || item.checkName || '未知问题'}</div>
1227
+ <div class="description">${item.description || item.message || ''}</div>
1228
+ </div>
1229
+ `).join('')}
1230
+ </div>
1231
+ ` : ''}
1232
+
1233
+ <div class="footer">
1234
+ <p>Generated by MohuClaw Security Assistant</p>
1235
+ <p>© 2024 北京模湖智能科技有限公司</p>
1236
+ </div>
1237
+ </div>
1238
+ </body>
1239
+ </html>`;
1240
+
1241
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
1242
+ res.setHeader('Content-Disposition', `attachment; filename="mohuclaw-report-${taskId}.html"`);
1243
+ res.send(html);
1244
+ });
1245
+
1246
+ // Export scan report as plain text
1247
+ app.get('/api/export/txt/:taskId', (req, res) => {
1248
+ const { taskId } = req.params;
1249
+ const result = scanResults.get(taskId);
1250
+
1251
+ if (!result) {
1252
+ return res.status(404).json({ error: 'Scan result not found' });
1253
+ }
1254
+
1255
+ const lines = [
1256
+ '========================================',
1257
+ ' MohuClaw 安全扫描报告',
1258
+ '========================================',
1259
+ '',
1260
+ `任务ID: ${taskId}`,
1261
+ `生成时间: ${new Date().toLocaleString('zh-CN')}`,
1262
+ `状态: ${result.status}`,
1263
+ '',
1264
+ '--- 扫描摘要 ---',
1265
+ `严重问题: ${result.summary.critical}`,
1266
+ `警告: ${result.summary.warnings}`,
1267
+ `通过检查: ${result.summary.clean}`,
1268
+ '',
1269
+ '--- 分类详情 ---',
1270
+ ...(result.categories || []).map(cat =>
1271
+ `${cat.name}: ${cat.critical} 严重, ${cat.warnings} 警告, ${cat.clean} 通过`
1272
+ ),
1273
+ '',
1274
+ '--- 问题详情 ---',
1275
+ ...(result.items || []).map(item =>
1276
+ `[${item.severity?.toUpperCase() || 'INFO'}] ${item.title || item.checkName}: ${item.description || item.message || ''}`
1277
+ ),
1278
+ '',
1279
+ '========================================',
1280
+ 'Generated by MohuClaw Security Assistant',
1281
+ '© 2024 北京模湖智能科技有限公司',
1282
+ '========================================'
1283
+ ];
1284
+
1285
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
1286
+ res.setHeader('Content-Disposition', `attachment; filename="mohuclaw-report-${taskId}.txt"`);
1287
+ res.send(lines.join('\n'));
1288
+ });
1289
+
872
1290
  // Start server
873
1291
  app.listen(PORT, '0.0.0.0', () => {
874
1292
  console.log(`🛡️ MohuClaw Web UI running at http://localhost:${PORT}`);