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 +91 -0
- package/dist/webui/index.html +73 -4
- package/dist/webui/server.js +418 -0
- package/dist/webui/server.js.bak +1074 -0
- package/package.json +1 -1
- package/webui/index.html +73 -4
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
|
package/dist/webui/index.html
CHANGED
|
@@ -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
|
-
|
|
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">
|
package/dist/webui/server.js
CHANGED
|
@@ -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}`);
|