@pwddd/skills-scanner 1.0.0-beta.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 +255 -0
- package/index.ts +647 -0
- package/openclaw.plugin.json +122 -0
- package/package.json +64 -0
- package/skills/skills-scanner/SKILL.md +281 -0
- package/src/api-client.ts +245 -0
- package/src/before-install-hook.ts +241 -0
- package/src/cache.ts +138 -0
- package/src/commands.ts +289 -0
- package/src/config-validator.ts +110 -0
- package/src/config.ts +230 -0
- package/src/cron-manager.ts +210 -0
- package/src/debug.ts +40 -0
- package/src/error-handler.ts +103 -0
- package/src/high-risk-operation-guard.ts +62 -0
- package/src/metrics.ts +140 -0
- package/src/prompt-guidance.ts +80 -0
- package/src/prompt-injection-guard.ts +56 -0
- package/src/rate-limiter.ts +102 -0
- package/src/report.ts +128 -0
- package/src/scanner.ts +230 -0
- package/src/state.ts +136 -0
- package/src/structured-logger.ts +97 -0
- package/src/types.ts +76 -0
- package/src/watcher.ts +178 -0
package/index.ts
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw Plugin: skills-scanner
|
|
3
|
+
*
|
|
4
|
+
* Security scanner for OpenClaw Skills to detect potential threats.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import type { ScannerConfig } from "./src/types.js";
|
|
12
|
+
import { skillsScannerConfigSchema, generateConfigGuide } from "./src/config.js";
|
|
13
|
+
import {
|
|
14
|
+
loadState,
|
|
15
|
+
saveState,
|
|
16
|
+
expandPath,
|
|
17
|
+
defaultScanDirs,
|
|
18
|
+
isFirstRun,
|
|
19
|
+
markConfigReviewed,
|
|
20
|
+
getStateDir,
|
|
21
|
+
} from "./src/state.js";
|
|
22
|
+
import { runScan } from "./src/scanner.js";
|
|
23
|
+
import { buildDailyReport } from "./src/report.js";
|
|
24
|
+
import { ensureCronJobViaGateway, checkCronJobStatus } from "./src/cron-manager.js";
|
|
25
|
+
import { startWatcher } from "./src/watcher.js";
|
|
26
|
+
import { createCommandHandlers } from "./src/commands.js";
|
|
27
|
+
import { SKILLS_SECURITY_GUIDANCE } from "./src/prompt-guidance.js";
|
|
28
|
+
import { PROMPT_INJECTION_GUARD } from "./src/prompt-injection-guard.js";
|
|
29
|
+
import { HIGH_RISK_OPERATION_GUARD } from "./src/high-risk-operation-guard.js";
|
|
30
|
+
import { handleBeforeInstall } from "./src/before-install-hook.js";
|
|
31
|
+
import type { BeforeInstallEvent } from "./src/before-install-hook.js";
|
|
32
|
+
import { validateConfig } from "./src/config-validator.js";
|
|
33
|
+
import { getMetricsSummary } from "./src/metrics.js";
|
|
34
|
+
import { debugLog, isDebugMode } from "./src/debug.js";
|
|
35
|
+
|
|
36
|
+
// Constants
|
|
37
|
+
const PLUGIN_ROOT = process.env.OPENCLAW_PLUGIN_ROOT || __dirname;
|
|
38
|
+
|
|
39
|
+
export default definePluginEntry({
|
|
40
|
+
id: "skills-scanner",
|
|
41
|
+
name: "Skills Scanner",
|
|
42
|
+
description: "Security scanner for OpenClaw Skills to detect potential threats",
|
|
43
|
+
configSchema: skillsScannerConfigSchema,
|
|
44
|
+
register(api) {
|
|
45
|
+
// Get state directory using official API
|
|
46
|
+
const STATE_DIR = getStateDir(api.runtime);
|
|
47
|
+
const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
|
|
48
|
+
|
|
49
|
+
const cfg: ScannerConfig =
|
|
50
|
+
api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
|
|
51
|
+
|
|
52
|
+
// Validate configuration
|
|
53
|
+
const validation = validateConfig(cfg, api.logger);
|
|
54
|
+
if (!validation.valid) {
|
|
55
|
+
api.logger.error("[skills-scanner] ❌ Invalid configuration, plugin may not work correctly");
|
|
56
|
+
// Continue loading but with warnings
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const apiUrl = cfg.apiUrl ?? "https://110.vemic.com/skills-scanner";
|
|
60
|
+
const scanDirs =
|
|
61
|
+
(cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
|
|
62
|
+
? cfg.scanDirs!.map(expandPath)
|
|
63
|
+
: defaultScanDirs();
|
|
64
|
+
const behavioral = cfg.behavioral ?? false;
|
|
65
|
+
const useLLM = cfg.useLLM ?? false;
|
|
66
|
+
const policy = cfg.policy ?? "balanced";
|
|
67
|
+
const preInstallScan = cfg.preInstallScan ?? "on";
|
|
68
|
+
const onUnsafe = cfg.onUnsafe ?? "warn";
|
|
69
|
+
const injectSecurityGuidance = cfg.injectSecurityGuidance ?? true;
|
|
70
|
+
const enablePromptInjectionGuard = cfg.enablePromptInjectionGuard ?? false;
|
|
71
|
+
const enableHighRiskOperationGuard = cfg.enableHighRiskOperationGuard ?? false;
|
|
72
|
+
const enableBeforeInstallHook = cfg.enableBeforeInstallHook ?? true;
|
|
73
|
+
|
|
74
|
+
api.logger.info("[skills-scanner] ═══════════════════════════════════════");
|
|
75
|
+
api.logger.info("[skills-scanner] Plugin loading...");
|
|
76
|
+
api.logger.info(`[skills-scanner] API URL: ${apiUrl}`);
|
|
77
|
+
api.logger.info(`[skills-scanner] Scan directories: ${scanDirs.join(", ")}`);
|
|
78
|
+
api.logger.info(`[skills-scanner] Before-install hook: ${enableBeforeInstallHook ? "✅ ENABLED" : "❌ DISABLED"}`);
|
|
79
|
+
|
|
80
|
+
if (isDebugMode()) {
|
|
81
|
+
api.logger.info("[skills-scanner] 🐛 DEBUG MODE ENABLED");
|
|
82
|
+
debugLog(api.logger, "Full configuration", cfg);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Inject system prompt guidance (can be disabled via config)
|
|
86
|
+
if (injectSecurityGuidance) {
|
|
87
|
+
// Build combined guidance
|
|
88
|
+
const guidanceParts = [SKILLS_SECURITY_GUIDANCE];
|
|
89
|
+
|
|
90
|
+
if (enablePromptInjectionGuard) {
|
|
91
|
+
guidanceParts.push(PROMPT_INJECTION_GUARD);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (enableHighRiskOperationGuard) {
|
|
95
|
+
guidanceParts.push(HIGH_RISK_OPERATION_GUARD);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const combinedGuidance = guidanceParts.join("\n\n");
|
|
99
|
+
|
|
100
|
+
api.on("before_prompt_build", async () => ({
|
|
101
|
+
prependSystemContext: combinedGuidance,
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
api.logger.info("[skills-scanner] ✅ Security guidance injected into system prompt");
|
|
105
|
+
if (enablePromptInjectionGuard) {
|
|
106
|
+
api.logger.info("[skills-scanner] - Prompt injection guard enabled");
|
|
107
|
+
}
|
|
108
|
+
if (enableHighRiskOperationGuard) {
|
|
109
|
+
api.logger.info("[skills-scanner] - High-risk operation guard enabled");
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
api.logger.info("[skills-scanner] ⏭️ Security guidance injection disabled");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Register before_install hook (CRITICAL SECURITY GATE)
|
|
116
|
+
if (enableBeforeInstallHook) {
|
|
117
|
+
api.on("before_install", async (event: BeforeInstallEvent) => {
|
|
118
|
+
try {
|
|
119
|
+
return await handleBeforeInstall(event, {
|
|
120
|
+
apiUrl,
|
|
121
|
+
behavioral,
|
|
122
|
+
useLLM,
|
|
123
|
+
policy,
|
|
124
|
+
logger: api.logger,
|
|
125
|
+
enabled: enableBeforeInstallHook,
|
|
126
|
+
timeoutMs: cfg.scanTimeoutMs || 30000, // Use configured timeout or 30s default
|
|
127
|
+
});
|
|
128
|
+
} catch (err: any) {
|
|
129
|
+
api.logger.error("[skills-scanner] ❌ before_install hook error", {
|
|
130
|
+
error: err.message,
|
|
131
|
+
stack: err.stack,
|
|
132
|
+
skillPath: event.skillPath,
|
|
133
|
+
});
|
|
134
|
+
// Return safe default on error - allow installation but log the failure
|
|
135
|
+
return { block: false };
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
api.logger.info("[skills-scanner] 🛡️ before_install hook registered (installation interception active)");
|
|
140
|
+
} else {
|
|
141
|
+
api.logger.warn("[skills-scanner] ⚠️ before_install hook DISABLED - installations will NOT be intercepted!");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Register gateway_start hook for automatic cron job registration
|
|
145
|
+
api.on("gateway_start", async () => {
|
|
146
|
+
try {
|
|
147
|
+
api.logger.info("[skills-scanner] 🚀 Gateway started, checking cron job...");
|
|
148
|
+
await ensureCronJobViaGateway({
|
|
149
|
+
logger: api.logger,
|
|
150
|
+
callGateway: async (method: string, params: any) => {
|
|
151
|
+
return await api.callGateway(method, params);
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
api.logger.info("[skills-scanner] ✅ Cron job check completed");
|
|
155
|
+
} catch (err: any) {
|
|
156
|
+
api.logger.error("[skills-scanner] ❌ Cron job registration failed", {
|
|
157
|
+
error: err.message,
|
|
158
|
+
stack: err.stack,
|
|
159
|
+
});
|
|
160
|
+
// Don't throw - avoid blocking gateway startup
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Register plugin_uninstall hook for cleanup
|
|
165
|
+
api.on("plugin_uninstall", async () => {
|
|
166
|
+
api.logger.info("[skills-scanner] 🗑️ Plugin uninstalling, cleaning up...");
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// 1. Stop file watcher
|
|
170
|
+
if (stopWatcher) {
|
|
171
|
+
api.logger.debug("[skills-scanner] Stopping file watcher...");
|
|
172
|
+
stopWatcher();
|
|
173
|
+
stopWatcher = null;
|
|
174
|
+
api.logger.debug("[skills-scanner] ✅ File watcher stopped");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2. Remove cron jobs
|
|
178
|
+
try {
|
|
179
|
+
const listResult = await api.callGateway("cron.list", {});
|
|
180
|
+
const jobs = listResult?.jobs || [];
|
|
181
|
+
const ourJobs = jobs.filter((j: any) => j.name === "skills-weekly-report");
|
|
182
|
+
|
|
183
|
+
for (const job of ourJobs) {
|
|
184
|
+
const jobId = job.jobId || job.id;
|
|
185
|
+
await api.callGateway("cron.remove", { jobId });
|
|
186
|
+
api.logger.info("[skills-scanner] Removed cron job", { jobId });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (ourJobs.length > 0) {
|
|
190
|
+
api.logger.info(`[skills-scanner] ✅ Removed ${ourJobs.length} cron job(s)`);
|
|
191
|
+
}
|
|
192
|
+
} catch (err: any) {
|
|
193
|
+
api.logger.warn("[skills-scanner] Failed to remove cron jobs", {
|
|
194
|
+
error: err.message,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. Save final state
|
|
199
|
+
try {
|
|
200
|
+
const finalState = loadState(api.runtime);
|
|
201
|
+
finalState.lastUninstallAt = new Date().toISOString();
|
|
202
|
+
saveState(finalState, api.runtime);
|
|
203
|
+
api.logger.debug("[skills-scanner] ✅ Final state saved");
|
|
204
|
+
} catch (err: any) {
|
|
205
|
+
api.logger.warn("[skills-scanner] Failed to save final state", {
|
|
206
|
+
error: err.message,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
api.logger.info("[skills-scanner] ✅ Cleanup completed successfully");
|
|
211
|
+
} catch (err: any) {
|
|
212
|
+
api.logger.error("[skills-scanner] ❌ Cleanup failed", {
|
|
213
|
+
error: err.message,
|
|
214
|
+
stack: err.stack,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Register config_changed hook for hot reload
|
|
220
|
+
api.on("config_changed", async (newConfig: any) => {
|
|
221
|
+
api.logger.info("[skills-scanner] 🔄 Configuration changed, reloading...");
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const newCfg: ScannerConfig =
|
|
225
|
+
newConfig?.plugins?.entries?.["skills-scanner"]?.config ?? {};
|
|
226
|
+
|
|
227
|
+
// Validate new configuration
|
|
228
|
+
const validation = validateConfig(newCfg, api.logger);
|
|
229
|
+
if (!validation.valid) {
|
|
230
|
+
api.logger.error("[skills-scanner] ❌ Invalid new configuration, keeping old config");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check what changed
|
|
235
|
+
const apiUrlChanged = newCfg.apiUrl !== cfg.apiUrl;
|
|
236
|
+
const scanDirsChanged = JSON.stringify(newCfg.scanDirs) !== JSON.stringify(cfg.scanDirs);
|
|
237
|
+
const preInstallScanChanged = newCfg.preInstallScan !== cfg.preInstallScan;
|
|
238
|
+
|
|
239
|
+
if (apiUrlChanged) {
|
|
240
|
+
api.logger.info("[skills-scanner] API URL updated", {
|
|
241
|
+
old: cfg.apiUrl,
|
|
242
|
+
new: newCfg.apiUrl,
|
|
243
|
+
});
|
|
244
|
+
// Update global apiUrl variable
|
|
245
|
+
Object.assign(cfg, { apiUrl: newCfg.apiUrl });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (scanDirsChanged || preInstallScanChanged) {
|
|
249
|
+
api.logger.info("[skills-scanner] Scan configuration updated, restarting watcher...");
|
|
250
|
+
|
|
251
|
+
// Stop old watcher
|
|
252
|
+
if (stopWatcher) {
|
|
253
|
+
stopWatcher();
|
|
254
|
+
stopWatcher = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Start new watcher with updated config
|
|
258
|
+
const newScanDirs =
|
|
259
|
+
(newCfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
|
|
260
|
+
? newCfg.scanDirs!.map(expandPath)
|
|
261
|
+
: defaultScanDirs();
|
|
262
|
+
|
|
263
|
+
if (newCfg.preInstallScan === "on" && newScanDirs.length > 0) {
|
|
264
|
+
stopWatcher = startWatcher(
|
|
265
|
+
newScanDirs,
|
|
266
|
+
newCfg.onUnsafe ?? "warn",
|
|
267
|
+
newCfg.behavioral ?? false,
|
|
268
|
+
newCfg.apiUrl ?? apiUrl,
|
|
269
|
+
newCfg.useLLM ?? false,
|
|
270
|
+
newCfg.policy ?? "balanced",
|
|
271
|
+
persistWatcherAlert,
|
|
272
|
+
api.logger,
|
|
273
|
+
QUARANTINE_DIR
|
|
274
|
+
);
|
|
275
|
+
api.logger.info("[skills-scanner] ✅ Watcher restarted with new configuration");
|
|
276
|
+
} else {
|
|
277
|
+
api.logger.info("[skills-scanner] ⏭️ Watcher disabled by new configuration");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Update global config
|
|
281
|
+
Object.assign(cfg, newCfg);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
api.logger.info("[skills-scanner] ✅ Configuration reload completed");
|
|
285
|
+
} catch (err: any) {
|
|
286
|
+
api.logger.error("[skills-scanner] ❌ Configuration reload failed", {
|
|
287
|
+
error: err.message,
|
|
288
|
+
stack: err.stack,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Check if first run
|
|
294
|
+
const firstRun = isFirstRun(cfg, api.runtime);
|
|
295
|
+
if (firstRun) {
|
|
296
|
+
api.logger.info("[skills-scanner] 🎉 First run detected");
|
|
297
|
+
const configGuide = generateConfigGuide(
|
|
298
|
+
cfg,
|
|
299
|
+
apiUrl,
|
|
300
|
+
scanDirs,
|
|
301
|
+
behavioral,
|
|
302
|
+
useLLM,
|
|
303
|
+
policy,
|
|
304
|
+
preInstallScan,
|
|
305
|
+
onUnsafe
|
|
306
|
+
);
|
|
307
|
+
console.log(configGuide);
|
|
308
|
+
markConfigReviewed(api.runtime);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Helper for watcher alerts
|
|
312
|
+
function persistWatcherAlert(msg: string): void {
|
|
313
|
+
const state = loadState(api.runtime);
|
|
314
|
+
const alerts: string[] = (state as any).pendingAlerts ?? [];
|
|
315
|
+
alerts.push(`[${new Date().toLocaleString("en-US")}] ${msg}`);
|
|
316
|
+
saveState({ ...state, pendingAlerts: alerts } as any, api.runtime);
|
|
317
|
+
api.logger.warn(`[skills-scanner] ${msg}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Service: start watcher
|
|
321
|
+
let stopWatcher: (() => void) | null = null;
|
|
322
|
+
|
|
323
|
+
api.registerService({
|
|
324
|
+
id: "skills-scanner-setup",
|
|
325
|
+
start: async () => {
|
|
326
|
+
api.logger.info("[skills-scanner] 🚀 Service starting...");
|
|
327
|
+
|
|
328
|
+
if (preInstallScan === "on" && scanDirs.length > 0) {
|
|
329
|
+
api.logger.info(`[skills-scanner] 📁 Starting file monitoring: ${scanDirs.length} directories`);
|
|
330
|
+
stopWatcher = startWatcher(
|
|
331
|
+
scanDirs,
|
|
332
|
+
onUnsafe,
|
|
333
|
+
behavioral,
|
|
334
|
+
apiUrl,
|
|
335
|
+
useLLM,
|
|
336
|
+
policy,
|
|
337
|
+
persistWatcherAlert,
|
|
338
|
+
api.logger,
|
|
339
|
+
QUARANTINE_DIR
|
|
340
|
+
);
|
|
341
|
+
api.logger.info("[skills-scanner] ✅ File monitoring started");
|
|
342
|
+
} else {
|
|
343
|
+
api.logger.info("[skills-scanner] ⏭️ Pre-install scan disabled");
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
stop: async () => {
|
|
347
|
+
api.logger.info("[skills-scanner] 🛑 Service stopping...");
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
// 1. Stop file watcher
|
|
351
|
+
if (stopWatcher) {
|
|
352
|
+
api.logger.debug("[skills-scanner] Stopping file watcher...");
|
|
353
|
+
stopWatcher();
|
|
354
|
+
stopWatcher = null;
|
|
355
|
+
api.logger.debug("[skills-scanner] ✅ File watcher stopped");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 2. Save final state
|
|
359
|
+
try {
|
|
360
|
+
const finalState = loadState(api.runtime);
|
|
361
|
+
finalState.lastShutdownAt = new Date().toISOString();
|
|
362
|
+
saveState(finalState, api.runtime);
|
|
363
|
+
api.logger.debug("[skills-scanner] ✅ Final state saved");
|
|
364
|
+
} catch (err: any) {
|
|
365
|
+
api.logger.warn("[skills-scanner] Failed to save final state", {
|
|
366
|
+
error: err.message,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 3. Clear pending alerts (optional - keep for next run)
|
|
371
|
+
// This is intentionally commented out to preserve alerts
|
|
372
|
+
// const state = loadState(api.runtime);
|
|
373
|
+
// if ((state as any).pendingAlerts?.length > 0) {
|
|
374
|
+
// saveState({ ...state, pendingAlerts: [] } as any, api.runtime);
|
|
375
|
+
// }
|
|
376
|
+
|
|
377
|
+
api.logger.info("[skills-scanner] ✅ Service stopped cleanly");
|
|
378
|
+
} catch (err: any) {
|
|
379
|
+
api.logger.error("[skills-scanner] ❌ Error during shutdown", {
|
|
380
|
+
error: err.message,
|
|
381
|
+
stack: err.stack,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Health check endpoint
|
|
388
|
+
api.registerHttpRoute({
|
|
389
|
+
method: "GET",
|
|
390
|
+
path: "/health/skills-scanner",
|
|
391
|
+
handler: async (req, res) => {
|
|
392
|
+
try {
|
|
393
|
+
const state = loadState(api.runtime);
|
|
394
|
+
|
|
395
|
+
// Check API availability
|
|
396
|
+
let apiStatus = "unknown";
|
|
397
|
+
try {
|
|
398
|
+
const response = await fetch(`${apiUrl}/health`, {
|
|
399
|
+
signal: AbortSignal.timeout(3000),
|
|
400
|
+
});
|
|
401
|
+
apiStatus = response.ok ? "available" : "unavailable";
|
|
402
|
+
} catch {
|
|
403
|
+
apiStatus = "unavailable";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Get performance metrics
|
|
407
|
+
const metrics = getMetricsSummary(STATE_DIR);
|
|
408
|
+
|
|
409
|
+
const health = {
|
|
410
|
+
status: "healthy",
|
|
411
|
+
plugin: {
|
|
412
|
+
version: api.version || "1.0.0",
|
|
413
|
+
id: api.id,
|
|
414
|
+
name: api.name,
|
|
415
|
+
},
|
|
416
|
+
api: {
|
|
417
|
+
url: apiUrl,
|
|
418
|
+
status: apiStatus,
|
|
419
|
+
},
|
|
420
|
+
watcher: {
|
|
421
|
+
enabled: preInstallScan === "on",
|
|
422
|
+
running: stopWatcher !== null,
|
|
423
|
+
directories: scanDirs.length,
|
|
424
|
+
},
|
|
425
|
+
state: {
|
|
426
|
+
lastScanAt: state.lastScanAt || null,
|
|
427
|
+
lastShutdownAt: state.lastShutdownAt || null,
|
|
428
|
+
pendingAlerts: (state as any).pendingAlerts?.length || 0,
|
|
429
|
+
},
|
|
430
|
+
metrics: {
|
|
431
|
+
totalScans: metrics.totalScans,
|
|
432
|
+
successRate: metrics.successRate.toFixed(2) + "%",
|
|
433
|
+
averageDurationMs: metrics.averageDurationMs,
|
|
434
|
+
lastScanAt: metrics.lastScanAt || null,
|
|
435
|
+
},
|
|
436
|
+
config: {
|
|
437
|
+
policy,
|
|
438
|
+
behavioral,
|
|
439
|
+
useLLM,
|
|
440
|
+
beforeInstallHook: enableBeforeInstallHook,
|
|
441
|
+
},
|
|
442
|
+
timestamp: new Date().toISOString(),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
res.status(200).json(health);
|
|
446
|
+
} catch (err: any) {
|
|
447
|
+
api.logger.error("[skills-scanner] Health check failed", {
|
|
448
|
+
error: err.message,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
res.status(503).json({
|
|
452
|
+
status: "unhealthy",
|
|
453
|
+
error: err.message,
|
|
454
|
+
timestamp: new Date().toISOString(),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// Command handlers
|
|
461
|
+
const handlers = createCommandHandlers(
|
|
462
|
+
cfg,
|
|
463
|
+
apiUrl,
|
|
464
|
+
scanDirs,
|
|
465
|
+
behavioral,
|
|
466
|
+
useLLM,
|
|
467
|
+
policy,
|
|
468
|
+
preInstallScan,
|
|
469
|
+
onUnsafe,
|
|
470
|
+
api.logger,
|
|
471
|
+
async (method: string, params: any) => {
|
|
472
|
+
return await api.callGateway(method, params);
|
|
473
|
+
}
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
// Chat command: /skills-scanner
|
|
477
|
+
api.registerCommand({
|
|
478
|
+
name: "skills-scanner",
|
|
479
|
+
description: "Skills 安全扫描工具。用法:/skills-scanner <子命令> [参数]",
|
|
480
|
+
acceptsArgs: true,
|
|
481
|
+
requireAuth: true,
|
|
482
|
+
handler: async (ctx: any) => {
|
|
483
|
+
const args = (ctx.args ?? "").trim();
|
|
484
|
+
|
|
485
|
+
if (!args) {
|
|
486
|
+
return {
|
|
487
|
+
text: [
|
|
488
|
+
"🔍 *Skills Scanner - 安全扫描工具*",
|
|
489
|
+
"",
|
|
490
|
+
"可用命令:",
|
|
491
|
+
"• `/skills-scanner scan <路径> [选项]` - 扫描 Skill",
|
|
492
|
+
"• `/skills-scanner scan clawhub <URL> [选项]` - 扫描 ClawHub Skill",
|
|
493
|
+
"• `/skills-scanner health` - 健康检查",
|
|
494
|
+
"• `/skills-scanner config [操作]` - 配置管理",
|
|
495
|
+
"• `/skills-scanner cron [操作]` - 定时任务管理",
|
|
496
|
+
"",
|
|
497
|
+
"扫描选项:",
|
|
498
|
+
"• `--detailed` - 显示详细发现",
|
|
499
|
+
"• `--behavioral` - 启用行为分析",
|
|
500
|
+
"• `--recursive` - 递归扫描子目录",
|
|
501
|
+
"• `--report` - 生成日报格式",
|
|
502
|
+
"",
|
|
503
|
+
"示例:",
|
|
504
|
+
"```",
|
|
505
|
+
"/skills-scanner scan ~/my-skill",
|
|
506
|
+
"/skills-scanner scan ~/skills --recursive",
|
|
507
|
+
"/skills-scanner scan clawhub https://clawhub.ai/username/project",
|
|
508
|
+
"/skills-scanner health",
|
|
509
|
+
"```",
|
|
510
|
+
"",
|
|
511
|
+
"💡 使用 `/skills-scanner help` 查看详细帮助",
|
|
512
|
+
].join("\n"),
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const parts = args.split(/\s+/);
|
|
517
|
+
const subCommand = parts[0].toLowerCase();
|
|
518
|
+
const subArgs = parts.slice(1).join(" ");
|
|
519
|
+
|
|
520
|
+
if (subCommand === "scan") {
|
|
521
|
+
return await handlers.handleScanCommand(subArgs);
|
|
522
|
+
} else if (subCommand === "health") {
|
|
523
|
+
return await handlers.handleHealthCommand();
|
|
524
|
+
} else if (subCommand === "config") {
|
|
525
|
+
return await handlers.handleConfigCommand(subArgs);
|
|
526
|
+
} else if (subCommand === "cron") {
|
|
527
|
+
return await handlers.handleCronCommand(subArgs);
|
|
528
|
+
} else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
|
|
529
|
+
return { text: handlers.getHelpText() };
|
|
530
|
+
} else {
|
|
531
|
+
return {
|
|
532
|
+
text: `❌ 未知子命令:${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助`,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Gateway RPC methods
|
|
539
|
+
api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
|
|
540
|
+
const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
|
|
541
|
+
if (!p) return respond(false, { error: "Missing path parameter" });
|
|
542
|
+
|
|
543
|
+
try {
|
|
544
|
+
const res = await runScan(mode === "batch" ? "batch" : "scan", expandPath(p), {
|
|
545
|
+
recursive,
|
|
546
|
+
detailed,
|
|
547
|
+
behavioral,
|
|
548
|
+
apiUrl,
|
|
549
|
+
useLLM,
|
|
550
|
+
policy,
|
|
551
|
+
});
|
|
552
|
+
respond(res.exitCode === 0, {
|
|
553
|
+
output: res.output,
|
|
554
|
+
exitCode: res.exitCode,
|
|
555
|
+
is_safe: res.exitCode === 0,
|
|
556
|
+
data: res.data,
|
|
557
|
+
});
|
|
558
|
+
} catch (err: any) {
|
|
559
|
+
respond(false, { error: err.message });
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
api.registerGatewayMethod("skillsScanner.report", async ({ respond }: any) => {
|
|
564
|
+
if (scanDirs.length === 0) return respond(false, { error: "No scan directories found" });
|
|
565
|
+
try {
|
|
566
|
+
const report = await buildDailyReport(
|
|
567
|
+
scanDirs,
|
|
568
|
+
behavioral,
|
|
569
|
+
apiUrl,
|
|
570
|
+
useLLM,
|
|
571
|
+
policy,
|
|
572
|
+
api.logger
|
|
573
|
+
);
|
|
574
|
+
respond(true, { report, state: loadState() });
|
|
575
|
+
} catch (err: any) {
|
|
576
|
+
respond(false, { error: err.message });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// CLI commands
|
|
581
|
+
api.registerCli(
|
|
582
|
+
({ program }: any) => {
|
|
583
|
+
const cmd = program.command("skills-scanner").description("OpenClaw Skills 安全扫描工具");
|
|
584
|
+
|
|
585
|
+
cmd
|
|
586
|
+
.command("scan <path>")
|
|
587
|
+
.description("扫描单个 Skill")
|
|
588
|
+
.option("--detailed", "显示所有发现")
|
|
589
|
+
.option("--behavioral", "启用行为分析")
|
|
590
|
+
.action(async (p: string, opts: any) => {
|
|
591
|
+
const res = await runScan("scan", expandPath(p), {
|
|
592
|
+
...opts,
|
|
593
|
+
apiUrl,
|
|
594
|
+
useLLM,
|
|
595
|
+
policy,
|
|
596
|
+
});
|
|
597
|
+
console.log(res.output);
|
|
598
|
+
process.exit(res.exitCode);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
cmd
|
|
602
|
+
.command("batch <directory>")
|
|
603
|
+
.description("批量扫描目录")
|
|
604
|
+
.option("--recursive", "递归扫描子目录")
|
|
605
|
+
.option("--detailed", "显示所有发现")
|
|
606
|
+
.option("--behavioral", "启用行为分析")
|
|
607
|
+
.action(async (d: string, opts: any) => {
|
|
608
|
+
const res = await runScan("batch", expandPath(d), {
|
|
609
|
+
...opts,
|
|
610
|
+
apiUrl,
|
|
611
|
+
useLLM,
|
|
612
|
+
policy,
|
|
613
|
+
});
|
|
614
|
+
console.log(res.output);
|
|
615
|
+
process.exit(res.exitCode);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
cmd
|
|
619
|
+
.command("report")
|
|
620
|
+
.description("生成日报")
|
|
621
|
+
.action(async () => {
|
|
622
|
+
const report = await buildDailyReport(
|
|
623
|
+
scanDirs,
|
|
624
|
+
behavioral,
|
|
625
|
+
apiUrl,
|
|
626
|
+
useLLM,
|
|
627
|
+
policy,
|
|
628
|
+
console
|
|
629
|
+
);
|
|
630
|
+
console.log(report);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
cmd
|
|
634
|
+
.command("health")
|
|
635
|
+
.description("检查 API 服务健康状态")
|
|
636
|
+
.action(async () => {
|
|
637
|
+
const res = await runScan("health", "", { apiUrl });
|
|
638
|
+
console.log(res.output);
|
|
639
|
+
process.exit(res.exitCode);
|
|
640
|
+
});
|
|
641
|
+
},
|
|
642
|
+
{ commands: ["skills-scanner"] }
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
api.logger.info("[skills-scanner] ✅ Plugin registered");
|
|
646
|
+
},
|
|
647
|
+
});
|