whitzard-claw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -0
- package/bin/whitzard-tui.js +73 -0
- package/bin/whitzard-webui.js +67 -0
- package/dist/tui/tui.js +38733 -0
- package/dist/webui/index.html +1235 -0
- package/dist/webui/server.js +876 -0
- package/ioc/c2-ips.txt +25 -0
- package/ioc/file-hashes.txt +13 -0
- package/ioc/malicious-domains.txt +46 -0
- package/ioc/malicious-hashes.txt +5 -0
- package/ioc/malicious-publishers.txt +34 -0
- package/ioc/malicious-skill-patterns.txt +87 -0
- package/package.json +50 -0
- package/scripts/check/access_control.sh +183 -0
- package/scripts/check/credential_storage.sh +222 -0
- package/scripts/check/execution_sandbox.sh +502 -0
- package/scripts/check/memory_poisoning.sh +334 -0
- package/scripts/check/network_exposure.sh +479 -0
- package/scripts/check/resource_cost.sh +182 -0
- package/scripts/check/supply_chain.sh +553 -0
- package/scripts/repair/access_control/_common.sh +249 -0
- package/scripts/repair/access_control/check_1.sh +28 -0
- package/scripts/repair/access_control/check_2.sh +27 -0
- package/scripts/repair/access_control/check_3.sh +23 -0
- package/scripts/repair/access_control/check_4.sh +23 -0
- package/scripts/repair/access_control/check_5.sh +20 -0
- package/scripts/repair/credential_storage/_common.sh +277 -0
- package/scripts/repair/credential_storage/check_1.sh +47 -0
- package/scripts/repair/credential_storage/check_2.sh +35 -0
- package/scripts/repair/credential_storage/check_3.sh +53 -0
- package/scripts/repair/credential_storage/logs/security-scan.log +15 -0
- package/scripts/repair/execution_sandbox/_common.sh +302 -0
- package/scripts/repair/execution_sandbox/check_1.sh +67 -0
- package/scripts/repair/execution_sandbox/check_10.sh +23 -0
- package/scripts/repair/execution_sandbox/check_11.sh +34 -0
- package/scripts/repair/execution_sandbox/check_12.sh +38 -0
- package/scripts/repair/execution_sandbox/check_13.sh +29 -0
- package/scripts/repair/execution_sandbox/check_2.sh +46 -0
- package/scripts/repair/execution_sandbox/check_3.sh +37 -0
- package/scripts/repair/execution_sandbox/check_4.sh +23 -0
- package/scripts/repair/execution_sandbox/check_5.sh +28 -0
- package/scripts/repair/execution_sandbox/check_6.sh +17 -0
- package/scripts/repair/execution_sandbox/check_7.sh +17 -0
- package/scripts/repair/execution_sandbox/check_8.sh +17 -0
- package/scripts/repair/execution_sandbox/check_9.sh +17 -0
- package/scripts/repair/execution_sandbox/logs/security-scan.log +10 -0
- package/scripts/repair/memory_poisoning/_common.sh +336 -0
- package/scripts/repair/memory_poisoning/check_1.sh +51 -0
- package/scripts/repair/memory_poisoning/check_2.sh +26 -0
- package/scripts/repair/memory_poisoning/check_3.sh +24 -0
- package/scripts/repair/memory_poisoning/check_4.sh +27 -0
- package/scripts/repair/memory_poisoning/check_5.sh +20 -0
- package/scripts/repair/network_exposure/_common.sh +330 -0
- package/scripts/repair/network_exposure/check_1.sh +86 -0
- package/scripts/repair/network_exposure/check_10.sh +16 -0
- package/scripts/repair/network_exposure/check_11.sh +31 -0
- package/scripts/repair/network_exposure/check_12.sh +24 -0
- package/scripts/repair/network_exposure/check_2.sh +26 -0
- package/scripts/repair/network_exposure/check_3.sh +43 -0
- package/scripts/repair/network_exposure/check_4.sh +23 -0
- package/scripts/repair/network_exposure/check_5.sh +16 -0
- package/scripts/repair/network_exposure/check_6.sh +98 -0
- package/scripts/repair/network_exposure/check_7.sh +35 -0
- package/scripts/repair/network_exposure/check_8.sh +19 -0
- package/scripts/repair/network_exposure/check_9.sh +19 -0
- package/scripts/repair/resource_cost/_common.sh +303 -0
- package/scripts/repair/resource_cost/check_1.sh +16 -0
- package/scripts/repair/resource_cost/check_2.sh +16 -0
- package/scripts/repair/resource_cost/check_3.sh +23 -0
- package/scripts/repair/supply_chain/_common.sh +222 -0
- package/scripts/repair/supply_chain/check_1.sh +95 -0
- package/scripts/repair/supply_chain/check_10.sh +60 -0
- package/scripts/repair/supply_chain/check_11.sh +63 -0
- package/scripts/repair/supply_chain/check_12.sh +36 -0
- package/scripts/repair/supply_chain/check_13.sh +44 -0
- package/scripts/repair/supply_chain/check_14.sh +33 -0
- package/scripts/repair/supply_chain/check_15.sh +33 -0
- package/scripts/repair/supply_chain/check_16.sh +34 -0
- package/scripts/repair/supply_chain/check_17.sh +61 -0
- package/scripts/repair/supply_chain/check_18.sh +62 -0
- package/scripts/repair/supply_chain/check_2.sh +93 -0
- package/scripts/repair/supply_chain/check_3.sh +78 -0
- package/scripts/repair/supply_chain/check_4.sh +72 -0
- package/scripts/repair/supply_chain/check_5.sh +73 -0
- package/scripts/repair/supply_chain/check_6.sh +81 -0
- package/scripts/repair/supply_chain/check_7.sh +52 -0
- package/scripts/repair/supply_chain/check_8.sh +71 -0
- package/scripts/repair/supply_chain/check_9.sh +78 -0
- package/scripts/repair/supply_chain/logs/security-scan.log +77 -0
- package/scripts/scan.sh +228 -0
- package/webui/index.html +1235 -0
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const HOME_DIR = os.homedir();
|
|
11
|
+
|
|
12
|
+
// Get dynamic paths based on installation location
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Determine package directory (works for both development and installed scenarios)
|
|
17
|
+
function getPackageDir() {
|
|
18
|
+
// Check if we're in dist/webui folder (installed package)
|
|
19
|
+
const distWebuiIndex = __dirname.lastIndexOf('dist' + path.sep + 'webui');
|
|
20
|
+
if (distWebuiIndex !== -1) {
|
|
21
|
+
return __dirname.substring(0, distWebuiIndex);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check if we're in a node_modules package (npm install scenario)
|
|
25
|
+
const nodeModulesIndex = __dirname.lastIndexOf(path.sep + 'node_modules' + path.sep + 'whitzard-claw');
|
|
26
|
+
if (nodeModulesIndex !== -1) {
|
|
27
|
+
return __dirname.substring(0, nodeModulesIndex + path.sep + 'node_modules' + path.sep + 'whitzard-claw');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Development mode: parent is project root
|
|
31
|
+
return path.resolve(__dirname, '..');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PORT = process.env.PORT || 12340;
|
|
35
|
+
const PACKAGE_DIR = getPackageDir();
|
|
36
|
+
const SCRIPTS_DIR = path.join(PACKAGE_DIR, 'scripts');
|
|
37
|
+
const IOC_DIR = path.join(PACKAGE_DIR, 'ioc');
|
|
38
|
+
const LOGS_DIR = path.join(PACKAGE_DIR, 'logs');
|
|
39
|
+
const SCAN_SCRIPT = path.join(SCRIPTS_DIR, 'scan.sh');
|
|
40
|
+
const SETTINGS_FILE = path.join(PACKAGE_DIR, '.openclaw_settings.json');
|
|
41
|
+
|
|
42
|
+
const app = express();
|
|
43
|
+
|
|
44
|
+
// In-memory storage for scan results
|
|
45
|
+
const scanTasks = new Map();
|
|
46
|
+
const scanResults = new Map();
|
|
47
|
+
let currentScanTaskId = null;
|
|
48
|
+
|
|
49
|
+
// Settings storage functions
|
|
50
|
+
function loadSettings() {
|
|
51
|
+
try {
|
|
52
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
53
|
+
const data = fs.readFileSync(SETTINGS_FILE, 'utf8');
|
|
54
|
+
return JSON.parse(data);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error loading settings:', error);
|
|
58
|
+
}
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function saveSettings(settings) {
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Error saving settings:', error);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getOpenclawHome() {
|
|
73
|
+
const settings = loadSettings();
|
|
74
|
+
// Default to ~/.openclaw in package root
|
|
75
|
+
return settings.openclawHome || path.join(HOME_DIR, '.openclaw');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Middleware
|
|
79
|
+
app.use(express.json());
|
|
80
|
+
app.use(express.static(path.join(PACKAGE_DIR, 'webui')));
|
|
81
|
+
|
|
82
|
+
// Security headers
|
|
83
|
+
app.use((req, res, next) => {
|
|
84
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
85
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
86
|
+
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
87
|
+
next();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Generate unique task ID
|
|
91
|
+
function generateTaskId() {
|
|
92
|
+
return `scan_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse scan output line by line
|
|
96
|
+
function parseScanLine(line, currentCategory) {
|
|
97
|
+
const result = { type: 'unknown', data: {} };
|
|
98
|
+
|
|
99
|
+
// Check header: [1/12] Auditing gateway...
|
|
100
|
+
const headerMatch = line.match(/^\[(\d+)\/(\d+)\]\s+(.+)/);
|
|
101
|
+
if (headerMatch) {
|
|
102
|
+
return {
|
|
103
|
+
type: 'check-start',
|
|
104
|
+
data: {
|
|
105
|
+
num: parseInt(headerMatch[1]),
|
|
106
|
+
total: parseInt(headerMatch[2]),
|
|
107
|
+
name: headerMatch[3].replace(/\.\.\.$/, '').trim(),
|
|
108
|
+
category: currentCategory
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check result lines
|
|
114
|
+
if (line.startsWith('CLEAN:')) {
|
|
115
|
+
return { type: 'check-result', data: { status: 'CLEAN', details: line.substring(6).trim() } };
|
|
116
|
+
}
|
|
117
|
+
if (line.startsWith('WARNING:')) {
|
|
118
|
+
return { type: 'check-result', data: { status: 'WARNING', details: line.substring(8).trim() } };
|
|
119
|
+
}
|
|
120
|
+
if (line.startsWith('CRITICAL:')) {
|
|
121
|
+
return { type: 'check-result', data: { status: 'CRITICAL', details: line.substring(9).trim() } };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Check category start
|
|
125
|
+
const categoryStartMatch = line.match(/^CATEGORY START:\s*(\w+)\s*\((\d+)\s+checks?\)/i);
|
|
126
|
+
if (categoryStartMatch) {
|
|
127
|
+
return {
|
|
128
|
+
type: 'category-start',
|
|
129
|
+
data: {
|
|
130
|
+
name: categoryStartMatch[1],
|
|
131
|
+
totalChecks: parseInt(categoryStartMatch[2])
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check category end
|
|
137
|
+
const categoryEndMatch = line.match(/^CATEGORY SUMMARY:\s*(\w+)/i);
|
|
138
|
+
if (categoryEndMatch) {
|
|
139
|
+
return { type: 'category-end', data: { name: categoryEndMatch[1] } };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check final summary
|
|
143
|
+
const summaryMatch = line.match(/SCAN COMPLETE:\s*(\d+)\s*critical,\s*(\d+)\s*warnings?,\s*(\d+)\s*clean/i);
|
|
144
|
+
if (summaryMatch) {
|
|
145
|
+
return {
|
|
146
|
+
type: 'scan-summary',
|
|
147
|
+
data: {
|
|
148
|
+
critical: parseInt(summaryMatch[1]),
|
|
149
|
+
warnings: parseInt(summaryMatch[2]),
|
|
150
|
+
clean: parseInt(summaryMatch[3])
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check status
|
|
156
|
+
const statusMatch = line.match(/STATUS:\s*(\w+)/i);
|
|
157
|
+
if (statusMatch) {
|
|
158
|
+
return { type: 'scan-status', data: { status: statusMatch[1] } };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Run scan script
|
|
165
|
+
function runScan(taskId) {
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
const openclawHome = getOpenclawHome();
|
|
168
|
+
const env = {
|
|
169
|
+
...process.env,
|
|
170
|
+
PATH: `${process.env.HOME}/.local/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}`,
|
|
171
|
+
HOME: process.env.HOME,
|
|
172
|
+
OPENCLAW_HOME: openclawHome
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
console.log(`[SCAN] Using OPENCLAW_HOME: ${openclawHome}`);
|
|
176
|
+
|
|
177
|
+
const child = spawn('/bin/bash', [SCAN_SCRIPT], {
|
|
178
|
+
env,
|
|
179
|
+
cwd: PACKAGE_DIR,
|
|
180
|
+
shell: false
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
let outputBuffer = '';
|
|
184
|
+
let currentCategory = null;
|
|
185
|
+
let currentCheck = null;
|
|
186
|
+
const categories = new Map();
|
|
187
|
+
const checks = [];
|
|
188
|
+
|
|
189
|
+
const task = scanTasks.get(taskId);
|
|
190
|
+
if (!task) {
|
|
191
|
+
resolve({ success: false, error: 'Task not found' });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
task.process = child;
|
|
196
|
+
task.startTime = Date.now();
|
|
197
|
+
|
|
198
|
+
child.stdout.on('data', (data) => {
|
|
199
|
+
const lines = data.toString().split('\n');
|
|
200
|
+
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
if (!line.trim()) continue;
|
|
203
|
+
|
|
204
|
+
outputBuffer += line + '\n';
|
|
205
|
+
const parsed = parseScanLine(line, currentCategory);
|
|
206
|
+
|
|
207
|
+
switch (parsed.type) {
|
|
208
|
+
case 'category-start':
|
|
209
|
+
currentCategory = parsed.data.name;
|
|
210
|
+
categories.set(currentCategory, {
|
|
211
|
+
name: parsed.data.name,
|
|
212
|
+
totalChecks: parsed.data.totalChecks,
|
|
213
|
+
completedChecks: 0,
|
|
214
|
+
critical: 0,
|
|
215
|
+
warnings: 0,
|
|
216
|
+
clean: 0,
|
|
217
|
+
checks: []
|
|
218
|
+
});
|
|
219
|
+
task.completedChecks = 0;
|
|
220
|
+
task.currentCategory = currentCategory;
|
|
221
|
+
broadcastSSE(taskId, 'category-start', parsed.data);
|
|
222
|
+
break;
|
|
223
|
+
|
|
224
|
+
case 'check-start':
|
|
225
|
+
currentCheck = {
|
|
226
|
+
num: parsed.data.num,
|
|
227
|
+
name: parsed.data.name,
|
|
228
|
+
category: currentCategory,
|
|
229
|
+
status: 'RUNNING',
|
|
230
|
+
details: ''
|
|
231
|
+
};
|
|
232
|
+
break;
|
|
233
|
+
|
|
234
|
+
case 'check-result':
|
|
235
|
+
if (currentCheck) {
|
|
236
|
+
// If this is the first result for this check, set status and details
|
|
237
|
+
if (!currentCheck.status || currentCheck.status === 'RUNNING') {
|
|
238
|
+
currentCheck.status = parsed.data.status;
|
|
239
|
+
currentCheck.details = parsed.data.details;
|
|
240
|
+
currentCheck.allDetails = [parsed.data.details]; // Start collecting all details
|
|
241
|
+
} else {
|
|
242
|
+
// Same check has multiple results - merge them
|
|
243
|
+
// Keep the most severe status (CRITICAL > WARNING > CLEAN)
|
|
244
|
+
const severityOrder = { 'CRITICAL': 3, 'WARNING': 2, 'CLEAN': 1 };
|
|
245
|
+
if (severityOrder[parsed.data.status] > severityOrder[currentCheck.status]) {
|
|
246
|
+
currentCheck.status = parsed.data.status;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Add unique details only
|
|
250
|
+
if (!currentCheck.allDetails.includes(parsed.data.details)) {
|
|
251
|
+
currentCheck.allDetails.push(parsed.data.details);
|
|
252
|
+
}
|
|
253
|
+
// Update details to show all (joined)
|
|
254
|
+
currentCheck.details = currentCheck.allDetails.join('\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const category = categories.get(currentCategory);
|
|
258
|
+
if (category) {
|
|
259
|
+
// Only add to checks array once (on first result)
|
|
260
|
+
if (!category.checks.find(c => c.num === currentCheck.num)) {
|
|
261
|
+
category.checks.push({ ...currentCheck });
|
|
262
|
+
} else {
|
|
263
|
+
// Update existing check with merged details
|
|
264
|
+
const existingCheck = category.checks.find(c => c.num === currentCheck.num);
|
|
265
|
+
existingCheck.status = currentCheck.status;
|
|
266
|
+
existingCheck.details = currentCheck.details;
|
|
267
|
+
existingCheck.allDetails = currentCheck.allDetails;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
category.completedChecks++;
|
|
271
|
+
|
|
272
|
+
// Update category counters based on final status
|
|
273
|
+
if (currentCheck.status === 'CLEAN') {
|
|
274
|
+
category.clean++;
|
|
275
|
+
} else if (currentCheck.status === 'WARNING') {
|
|
276
|
+
category.warnings++;
|
|
277
|
+
} else if (currentCheck.status === 'CRITICAL') {
|
|
278
|
+
category.critical++;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
task.completedChecks++;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Broadcast update
|
|
285
|
+
broadcastSSE(taskId, 'check-result', {
|
|
286
|
+
...currentCheck,
|
|
287
|
+
categoryStats: categories.get(currentCategory)
|
|
288
|
+
});
|
|
289
|
+
currentCheck = null;
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
|
|
293
|
+
case 'category-end':
|
|
294
|
+
broadcastSSE(taskId, 'category-end', categories.get(parsed.data.name));
|
|
295
|
+
break;
|
|
296
|
+
|
|
297
|
+
case 'scan-summary':
|
|
298
|
+
task.summary = parsed.data;
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case 'scan-status':
|
|
302
|
+
task.status = parsed.data.status;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
child.stderr.on('data', (data) => {
|
|
309
|
+
console.error(`[${taskId}] stderr:`, data.toString());
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
child.on('close', (code) => {
|
|
313
|
+
task.endTime = Date.now();
|
|
314
|
+
task.exitCode = code;
|
|
315
|
+
|
|
316
|
+
const result = {
|
|
317
|
+
taskId,
|
|
318
|
+
timestamp: new Date(task.startTime).toISOString(),
|
|
319
|
+
duration: task.endTime - task.startTime,
|
|
320
|
+
exitCode: code,
|
|
321
|
+
summary: task.summary || { critical: 0, warnings: 0, clean: 0 },
|
|
322
|
+
status: task.status || (code === 0 ? 'SECURE' : code === 1 ? 'WARNING' : 'COMPROMISED'),
|
|
323
|
+
categories: Array.from(categories.values()),
|
|
324
|
+
checks: checks,
|
|
325
|
+
rawOutput: outputBuffer
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
scanResults.set(taskId, result);
|
|
329
|
+
scanTasks.delete(taskId);
|
|
330
|
+
currentScanTaskId = null;
|
|
331
|
+
|
|
332
|
+
broadcastSSE(taskId, 'scan-complete', result);
|
|
333
|
+
resolve({ success: true, result });
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
child.on('error', (err) => {
|
|
337
|
+
task.endTime = Date.now();
|
|
338
|
+
task.status = 'FAILED';
|
|
339
|
+
task.error = err.message;
|
|
340
|
+
|
|
341
|
+
scanTasks.delete(taskId);
|
|
342
|
+
currentScanTaskId = null;
|
|
343
|
+
|
|
344
|
+
broadcastSSE(taskId, 'scan-error', { error: err.message });
|
|
345
|
+
resolve({ success: false, error: err.message });
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// SSE clients
|
|
351
|
+
const sseClients = new Map();
|
|
352
|
+
|
|
353
|
+
function broadcastSSE(taskId, event, data) {
|
|
354
|
+
const clients = sseClients.get(taskId) || [];
|
|
355
|
+
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
356
|
+
|
|
357
|
+
clients.forEach(client => {
|
|
358
|
+
try {
|
|
359
|
+
client.res.write(message);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
// Client disconnected, will be cleaned up
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Routes
|
|
367
|
+
|
|
368
|
+
// Serve index.html
|
|
369
|
+
app.get('/', (req, res) => {
|
|
370
|
+
res.sendFile(path.join(__dirname, 'index.html'));
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Start scan
|
|
374
|
+
app.post('/api/scan/run', async (req, res) => {
|
|
375
|
+
if (currentScanTaskId) {
|
|
376
|
+
return res.status(409).json({
|
|
377
|
+
error: 'Scan already in progress',
|
|
378
|
+
taskId: currentScanTaskId
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const taskId = generateTaskId();
|
|
383
|
+
const task = {
|
|
384
|
+
id: taskId,
|
|
385
|
+
status: 'STARTING',
|
|
386
|
+
startTime: null,
|
|
387
|
+
endTime: null,
|
|
388
|
+
currentCategory: null,
|
|
389
|
+
completedChecks: 0,
|
|
390
|
+
totalChecks: 59, // 12+5+13+3+5+18+3
|
|
391
|
+
summary: null,
|
|
392
|
+
process: null
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
scanTasks.set(taskId, task);
|
|
396
|
+
currentScanTaskId = taskId;
|
|
397
|
+
sseClients.set(taskId, []);
|
|
398
|
+
|
|
399
|
+
res.json({ taskId, status: 'started' });
|
|
400
|
+
|
|
401
|
+
// Run scan asynchronously
|
|
402
|
+
runScan(taskId);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// SSE endpoint
|
|
406
|
+
app.get('/api/scan/sse', (req, res) => {
|
|
407
|
+
const { taskId } = req.query;
|
|
408
|
+
|
|
409
|
+
if (!taskId || !scanTasks.has(taskId)) {
|
|
410
|
+
// Allow connecting to completed scans for result retrieval
|
|
411
|
+
if (!taskId || !scanResults.has(taskId)) {
|
|
412
|
+
return res.status(404).send('Task not found');
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
417
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
418
|
+
res.setHeader('Connection', 'keep-alive');
|
|
419
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
420
|
+
|
|
421
|
+
// Send initial connection confirmation
|
|
422
|
+
res.write(`event: connected\ndata: {"taskId":"${taskId || 'latest'}"}\n\n`);
|
|
423
|
+
|
|
424
|
+
const client = { res, connectedAt: Date.now() };
|
|
425
|
+
|
|
426
|
+
if (taskId) {
|
|
427
|
+
if (!sseClients.has(taskId)) {
|
|
428
|
+
sseClients.set(taskId, []);
|
|
429
|
+
}
|
|
430
|
+
sseClients.get(taskId).push(client);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// If task is completed, send the result immediately
|
|
434
|
+
if (taskId && scanResults.has(taskId)) {
|
|
435
|
+
const result = scanResults.get(taskId);
|
|
436
|
+
res.write(`event: scan-complete\ndata: ${JSON.stringify(result)}\n\n`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Cleanup on disconnect
|
|
440
|
+
req.on('close', () => {
|
|
441
|
+
if (taskId) {
|
|
442
|
+
const clients = sseClients.get(taskId);
|
|
443
|
+
if (clients) {
|
|
444
|
+
const index = clients.indexOf(client);
|
|
445
|
+
if (index > -1) clients.splice(index, 1);
|
|
446
|
+
if (clients.length === 0) {
|
|
447
|
+
sseClients.delete(taskId);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Get scan result
|
|
455
|
+
app.get('/api/scan/:taskId/result', (req, res) => {
|
|
456
|
+
const { taskId } = req.params;
|
|
457
|
+
|
|
458
|
+
if (scanResults.has(taskId)) {
|
|
459
|
+
res.json(scanResults.get(taskId));
|
|
460
|
+
} else if (scanTasks.has(taskId)) {
|
|
461
|
+
const task = scanTasks.get(taskId);
|
|
462
|
+
res.json({
|
|
463
|
+
taskId,
|
|
464
|
+
status: task.status,
|
|
465
|
+
currentCategory: task.currentCategory,
|
|
466
|
+
completedChecks: task.completedChecks,
|
|
467
|
+
totalChecks: task.totalChecks,
|
|
468
|
+
progress: Math.round((task.completedChecks / task.totalChecks) * 100)
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
res.status(404).json({ error: 'Task not found' });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Get latest scan result
|
|
476
|
+
app.get('/api/scan/latest', (req, res) => {
|
|
477
|
+
const latestTaskId = Array.from(scanResults.keys()).pop();
|
|
478
|
+
|
|
479
|
+
if (latestTaskId) {
|
|
480
|
+
res.json(scanResults.get(latestTaskId));
|
|
481
|
+
} else {
|
|
482
|
+
res.json({ error: 'No scans run yet' });
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Get scan history (last 10 results)
|
|
487
|
+
app.get('/api/scan/history', (req, res) => {
|
|
488
|
+
const history = Array.from(scanResults.entries())
|
|
489
|
+
.map(([id, result]) => ({
|
|
490
|
+
taskId: id,
|
|
491
|
+
timestamp: result.timestamp,
|
|
492
|
+
status: result.status,
|
|
493
|
+
summary: result.summary,
|
|
494
|
+
duration: result.duration
|
|
495
|
+
}))
|
|
496
|
+
.slice(-10)
|
|
497
|
+
.reverse();
|
|
498
|
+
|
|
499
|
+
res.json(history);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Cancel running scan
|
|
503
|
+
app.post('/api/scan/:taskId/cancel', (req, res) => {
|
|
504
|
+
const { taskId } = req.params;
|
|
505
|
+
|
|
506
|
+
if (!scanTasks.has(taskId)) {
|
|
507
|
+
return res.status(404).json({ error: 'Task not found or already completed' });
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const task = scanTasks.get(taskId);
|
|
511
|
+
if (task.process) {
|
|
512
|
+
task.process.kill('SIGTERM');
|
|
513
|
+
res.json({ taskId, status: 'cancelled' });
|
|
514
|
+
} else {
|
|
515
|
+
res.status(500).json({ error: 'Cannot cancel scan' });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
// Health check
|
|
520
|
+
app.get('/api/health', (req, res) => {
|
|
521
|
+
res.json({
|
|
522
|
+
status: 'ok',
|
|
523
|
+
uptime: process.uptime(),
|
|
524
|
+
activeScans: scanTasks.size,
|
|
525
|
+
completedScans: scanResults.size
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Get scan logs (real-time)
|
|
530
|
+
app.get('/api/scan/logs', (req, res) => {
|
|
531
|
+
const logFilePath = path.join(LOGS_DIR, 'security-scan.log');
|
|
532
|
+
|
|
533
|
+
if (!fs.existsSync(logFilePath)) {
|
|
534
|
+
return res.json({ logs: [], exists: false });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
const content = fs.readFileSync(logFilePath, 'utf8');
|
|
539
|
+
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
540
|
+
res.json({
|
|
541
|
+
logs: lines,
|
|
542
|
+
exists: true,
|
|
543
|
+
lineCount: lines.length
|
|
544
|
+
});
|
|
545
|
+
} catch (error) {
|
|
546
|
+
res.status(500).json({ error: error.message });
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// SSE endpoint for real-time log streaming
|
|
551
|
+
app.get('/api/scan/logs/sse', (req, res) => {
|
|
552
|
+
const { taskId } = req.query;
|
|
553
|
+
const logFilePath = path.join(LOGS_DIR, 'security-scan.log');
|
|
554
|
+
|
|
555
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
556
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
557
|
+
res.setHeader('Connection', 'keep-alive');
|
|
558
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
559
|
+
|
|
560
|
+
// Send initial connection confirmation
|
|
561
|
+
res.write(`event: connected\ndata: {"status":"connected"}\n\n`);
|
|
562
|
+
|
|
563
|
+
// Send existing logs if file exists
|
|
564
|
+
if (fs.existsSync(logFilePath)) {
|
|
565
|
+
try {
|
|
566
|
+
const content = fs.readFileSync(logFilePath, 'utf8');
|
|
567
|
+
const lines = content.split('\n').filter(line => line.trim() !== '');
|
|
568
|
+
res.write(`event: logs\ndata: ${JSON.stringify({ logs: lines })}\n\n`);
|
|
569
|
+
} catch (e) {
|
|
570
|
+
console.error('Error reading log file:', e);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Watch file for changes if there's an active scan
|
|
575
|
+
let fsWatcher = null;
|
|
576
|
+
let lastSize = 0;
|
|
577
|
+
|
|
578
|
+
if (fs.existsSync(logFilePath)) {
|
|
579
|
+
const stats = fs.statSync(logFilePath);
|
|
580
|
+
lastSize = stats.size;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const pollInterval = setInterval(() => {
|
|
584
|
+
if (!fs.existsSync(logFilePath)) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
try {
|
|
589
|
+
const stats = fs.statSync(logFilePath);
|
|
590
|
+
|
|
591
|
+
if (stats.size > lastSize) {
|
|
592
|
+
// File has grown, read new content
|
|
593
|
+
const buffer = Buffer.alloc(stats.size - lastSize);
|
|
594
|
+
const fd = fs.openSync(logFilePath, 'r');
|
|
595
|
+
fs.readSync(fd, buffer, 0, buffer.length, lastSize);
|
|
596
|
+
fs.closeSync(fd);
|
|
597
|
+
|
|
598
|
+
const newContent = buffer.toString('utf8');
|
|
599
|
+
const newLines = newContent.split('\n').filter(line => line.trim() !== '');
|
|
600
|
+
|
|
601
|
+
if (newLines.length > 0) {
|
|
602
|
+
res.write(`event: new-logs\ndata: ${JSON.stringify({ logs: newLines })}\n\n`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
lastSize = stats.size;
|
|
606
|
+
}
|
|
607
|
+
} catch (e) {
|
|
608
|
+
console.error('Error polling log file:', e);
|
|
609
|
+
}
|
|
610
|
+
}, 500); // Poll every 500ms
|
|
611
|
+
|
|
612
|
+
// Cleanup on disconnect
|
|
613
|
+
req.on('close', () => {
|
|
614
|
+
clearInterval(pollInterval);
|
|
615
|
+
if (fsWatcher) {
|
|
616
|
+
fsWatcher.close();
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Clear logs endpoint (called before starting a new scan)
|
|
622
|
+
app.post('/api/scan/logs/clear', (req, res) => {
|
|
623
|
+
const logFilePath = path.join(LOGS_DIR, 'security-scan.log');
|
|
624
|
+
|
|
625
|
+
try {
|
|
626
|
+
// Ensure logs directory exists
|
|
627
|
+
if (!fs.existsSync(LOGS_DIR)) {
|
|
628
|
+
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Clear or create the log file
|
|
632
|
+
fs.writeFileSync(logFilePath, '');
|
|
633
|
+
res.json({ success: true, message: 'Logs cleared' });
|
|
634
|
+
} catch (error) {
|
|
635
|
+
res.status(500).json({ error: error.message });
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// ============ SETTINGS APIs ============
|
|
640
|
+
|
|
641
|
+
// Get OPENCLAW_HOME setting
|
|
642
|
+
app.get('/api/settings/openclaw_home', (req, res) => {
|
|
643
|
+
const value = getOpenclawHome();
|
|
644
|
+
res.json({ value });
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Save OPENCLAW_HOME setting
|
|
648
|
+
app.post('/api/settings/openclaw_home', (req, res) => {
|
|
649
|
+
const { value } = req.body;
|
|
650
|
+
|
|
651
|
+
if (typeof value !== 'string') {
|
|
652
|
+
return res.status(400).json({ error: 'Value must be a string' });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const settings = loadSettings();
|
|
656
|
+
settings.openclawHome = value;
|
|
657
|
+
|
|
658
|
+
if (saveSettings(settings)) {
|
|
659
|
+
res.json({ success: true, value });
|
|
660
|
+
} else {
|
|
661
|
+
res.status(500).json({ error: 'Failed to save settings' });
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// ============ REMEDIATION APIs ============
|
|
666
|
+
|
|
667
|
+
// Find repair script path
|
|
668
|
+
function findRepairScript(category, checkNum) {
|
|
669
|
+
const categoryDir = path.join(SCRIPTS_DIR, 'repair', category.toLowerCase());
|
|
670
|
+
const scriptPath = path.join(categoryDir, `check_${checkNum}.sh`);
|
|
671
|
+
|
|
672
|
+
if (fs.existsSync(scriptPath)) {
|
|
673
|
+
return scriptPath;
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Parse repair script output
|
|
679
|
+
function parseRepairOutput(output) {
|
|
680
|
+
const result = {
|
|
681
|
+
guidance: '',
|
|
682
|
+
autoFixAvailable: false,
|
|
683
|
+
skills: []
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Extract guidance (everything before auto-fix marker)
|
|
687
|
+
const autoFixIndex = output.indexOf('auto-fix\n');
|
|
688
|
+
if (autoFixIndex !== -1) {
|
|
689
|
+
result.guidance = output.substring(0, autoFixIndex).trim();
|
|
690
|
+
result.autoFixAvailable = true;
|
|
691
|
+
|
|
692
|
+
// Extract skill names (lines after auto-fix marker)
|
|
693
|
+
const skillsSection = output.substring(autoFixIndex + 9).trim(); // 'auto-fix\n' is 9 characters
|
|
694
|
+
if (skillsSection.length > 0) {
|
|
695
|
+
result.skills = skillsSection.split('\n')
|
|
696
|
+
.map(s => s.trim())
|
|
697
|
+
.filter(s => s.length > 0 && !s.startsWith('auto-fix')); // Filter out empty lines and duplicate markers
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
result.guidance = output.trim();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Get remediation guidance for a specific check
|
|
707
|
+
app.post('/api/remediate/guidance', async (req, res) => {
|
|
708
|
+
const { category, checkNum } = req.body;
|
|
709
|
+
|
|
710
|
+
if (!category || !checkNum) {
|
|
711
|
+
return res.status(400).json({ error: 'Missing category or checkNum' });
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const scriptPath = findRepairScript(category, checkNum);
|
|
715
|
+
if (!scriptPath) {
|
|
716
|
+
return res.status(404).json({ error: 'Repair script not found' });
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
try {
|
|
720
|
+
const openclawHome = getOpenclawHome();
|
|
721
|
+
const env = {
|
|
722
|
+
...process.env,
|
|
723
|
+
PATH: `${process.env.HOME}/.local/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}`,
|
|
724
|
+
HOME: process.env.HOME,
|
|
725
|
+
OPENCLAW_HOME: openclawHome
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
console.log(`[REMEDIATE] Using OPENCLAW_HOME: ${openclawHome}`);
|
|
729
|
+
|
|
730
|
+
const scriptResult = await new Promise((resolve, reject) => {
|
|
731
|
+
let stdout = '';
|
|
732
|
+
let stderr = '';
|
|
733
|
+
|
|
734
|
+
const child = spawn('/bin/bash', [scriptPath], {
|
|
735
|
+
env,
|
|
736
|
+
cwd: PACKAGE_DIR,
|
|
737
|
+
shell: false
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
child.stdout.on('data', (data) => {
|
|
741
|
+
stdout += data.toString();
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
child.stderr.on('data', (data) => {
|
|
745
|
+
stderr += data.toString();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
child.on('close', (code) => {
|
|
749
|
+
resolve({ exitCode: code, stdout, stderr });
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
child.on('error', reject);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const parsed = parseRepairOutput(scriptResult.stdout);
|
|
756
|
+
|
|
757
|
+
// Determine fix type:
|
|
758
|
+
// - hasAutoFix: true if auto-fix available AND has skills to select (user needs to choose)
|
|
759
|
+
// - directFix: true if auto-fix available but NO skills (can fix directly)
|
|
760
|
+
res.json({
|
|
761
|
+
category,
|
|
762
|
+
checkNum,
|
|
763
|
+
...parsed,
|
|
764
|
+
hasAutoFix: parsed.autoFixAvailable && parsed.skills.length > 0,
|
|
765
|
+
directFix: parsed.autoFixAvailable && parsed.skills.length === 0
|
|
766
|
+
});
|
|
767
|
+
} catch (error) {
|
|
768
|
+
res.status(500).json({ error: error.message });
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Execute auto-fix for a specific skill
|
|
773
|
+
app.post('/api/remediate/execute', async (req, res) => {
|
|
774
|
+
const { category, checkNum, skillName } = req.body;
|
|
775
|
+
|
|
776
|
+
if (!category || !checkNum) {
|
|
777
|
+
return res.status(400).json({ error: 'Missing required parameters' });
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const scriptPath = findRepairScript(category, checkNum);
|
|
781
|
+
if (!scriptPath) {
|
|
782
|
+
return res.status(404).json({ error: 'Repair script not found' });
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
try {
|
|
786
|
+
const openclawHome = getOpenclawHome();
|
|
787
|
+
console.log(`[AUTO-FIX] Executing repair script: ${scriptPath}`);
|
|
788
|
+
console.log(`[AUTO-FIX] Parameters: category=${category}, checkNum=${checkNum}, skillName=${skillName}`);
|
|
789
|
+
console.log(`[AUTO-FIX] Using OPENCLAW_HOME: ${openclawHome}`);
|
|
790
|
+
|
|
791
|
+
const env = {
|
|
792
|
+
...process.env,
|
|
793
|
+
PATH: `${process.env.HOME}/.local/bin:/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:${process.env.PATH}`,
|
|
794
|
+
HOME: process.env.HOME,
|
|
795
|
+
OPENCLAW_HOME: openclawHome,
|
|
796
|
+
AUTO_FIX: '1'
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// Set SKILL_NAME only if provided (not null/undefined/empty)
|
|
800
|
+
// skillName=null means direct fix without specific skill name
|
|
801
|
+
if (skillName && skillName !== 'auto' && skillName !== 'null') {
|
|
802
|
+
env.SKILL_NAME = skillName;
|
|
803
|
+
console.log(`[AUTO-FIX] Setting SKILL_NAME=${skillName}`);
|
|
804
|
+
} else {
|
|
805
|
+
console.log(`[AUTO-FIX] No SKILL_NAME set (direct fix mode)`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const result = await new Promise((resolve, reject) => {
|
|
809
|
+
let stdout = '';
|
|
810
|
+
let stderr = '';
|
|
811
|
+
|
|
812
|
+
const child = spawn('/bin/bash', [scriptPath], {
|
|
813
|
+
env,
|
|
814
|
+
cwd: PACKAGE_DIR,
|
|
815
|
+
shell: false
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
child.stdout.on('data', (data) => {
|
|
819
|
+
const text = data.toString();
|
|
820
|
+
stdout += text;
|
|
821
|
+
console.log(`[AUTO-FIX STDOUT] ${text.trim()}`);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
child.stderr.on('data', (data) => {
|
|
825
|
+
const text = data.toString();
|
|
826
|
+
stderr += text;
|
|
827
|
+
console.error(`[AUTO-FIX STDERR] ${text.trim()}`);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
child.on('close', (code) => {
|
|
831
|
+
console.log(`[AUTO-FIX] Script closed with exit code: ${code}`);
|
|
832
|
+
resolve({ exitCode: code, stdout, stderr });
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
child.on('error', (err) => {
|
|
836
|
+
console.error(`[AUTO-FIX ERROR] Spawn error: ${err.message}`);
|
|
837
|
+
reject(err);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
// Check for errors in stderr or non-zero exit code
|
|
842
|
+
if (result.exitCode !== 0) {
|
|
843
|
+
console.error(`[AUTO-FIX] Failed with exit code ${result.exitCode}`);
|
|
844
|
+
console.error(`[AUTO-FIX] stderr: ${result.stderr}`);
|
|
845
|
+
return res.status(500).json({
|
|
846
|
+
success: false,
|
|
847
|
+
error: `Script exited with code ${result.exitCode}: ${result.stderr}`
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
console.log(`[AUTO-FIX] Success! stdout: ${result.stdout}`);
|
|
852
|
+
|
|
853
|
+
res.json({
|
|
854
|
+
success: true,
|
|
855
|
+
skillName: skillName || 'direct-fix',
|
|
856
|
+
output: result.stdout,
|
|
857
|
+
stderr: result.stderr,
|
|
858
|
+
exitCode: result.exitCode,
|
|
859
|
+
message: skillName
|
|
860
|
+
? `Skill '${skillName}' has been removed successfully`
|
|
861
|
+
: 'Fix applied successfully'
|
|
862
|
+
});
|
|
863
|
+
} catch (error) {
|
|
864
|
+
console.error(`[AUTO-FIX ERROR] ${error.message}`);
|
|
865
|
+
res.status(500).json({
|
|
866
|
+
success: false,
|
|
867
|
+
error: error.message
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
// Start server
|
|
873
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
874
|
+
console.log(`🛡️ Whitzard-Claw Web UI running at http://localhost:${PORT}`);
|
|
875
|
+
console.log(` Press Ctrl+C to stop`);
|
|
876
|
+
});
|