@pwddd/skills-scanner 3.0.22 → 4.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/index.ts CHANGED
@@ -4,180 +4,243 @@
4
4
  * Security scanner for OpenClaw Skills to detect potential threats.
5
5
  */
6
6
 
7
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
7
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
8
8
  import { join } from "node:path";
9
- import os from "node:os";
10
- import { existsSync } from "node:fs";
11
9
  import type { ScannerConfig } from "./src/types.js";
12
10
  import { skillsScannerConfigSchema, generateConfigGuide } from "./src/config.js";
13
11
  import {
14
12
  loadState,
15
13
  saveState,
16
14
  expandPath,
17
- defaultScanDirs,
18
15
  isFirstRun,
19
16
  markConfigReviewed,
17
+ getStateDir,
20
18
  } from "./src/state.js";
21
- import { ensureDeps, getPythonCommand, isPythonReady } from "./src/deps.js";
22
19
  import { runScan } from "./src/scanner.js";
23
- import { buildDailyReport } from "./src/report.js";
24
- import { ensureCronJob, checkCronJobStatus } from "./src/cron.js";
25
- import { startWatcher } from "./src/watcher.js";
20
+ import { ensureCronJob } from "./src/cron-manager.js";
26
21
  import { createCommandHandlers } from "./src/commands.js";
27
22
  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";
23
+ import { handleBeforeInstall } from "./src/before-install-hook.js";
24
+ import type { BeforeInstallEvent } from "./src/before-install-hook.js";
25
+ import { validateConfig } from "./src/config-validator.js";
26
+ import { getMetricsSummary } from "./src/metrics.js";
27
+ import { debugLog, isDebugMode } from "./src/debug.js";
30
28
 
31
29
  // Constants
32
30
  const PLUGIN_ROOT = process.env.OPENCLAW_PLUGIN_ROOT || __dirname;
33
- const SKILL_DIR = join(PLUGIN_ROOT, "skills", "skills-scanner");
34
- const SCAN_SCRIPT = join(SKILL_DIR, "scan.py");
35
- const STATE_DIR = join(os.homedir(), ".openclaw", "skills-scanner");
36
- const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
37
31
 
38
- const PYTHON_CMD = getPythonCommand();
32
+ export default definePluginEntry({
33
+ id: "skills-scanner",
34
+ name: "Skills Scanner",
35
+ description: "Security scanner for OpenClaw Skills to detect potential threats",
36
+ configSchema: skillsScannerConfigSchema,
37
+ register(api) {
38
+ // Get state directory using official API
39
+ const STATE_DIR = getStateDir(api.runtime);
40
+ const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
39
41
 
40
- export default function register(api: OpenClawPluginApi) {
41
42
  const cfg: ScannerConfig =
42
43
  api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
43
- const apiUrl = cfg.apiUrl ?? "http://10.110.3.133";
44
- const scanDirs =
45
- (cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
46
- ? cfg.scanDirs!.map(expandPath)
47
- : defaultScanDirs();
44
+
45
+ // Validate configuration
46
+ const validation = validateConfig(cfg, api.logger);
47
+ if (!validation.valid) {
48
+ api.logger.error("[skills-scanner] ❌ Invalid configuration, plugin may not work correctly");
49
+ // Continue loading but with warnings
50
+ }
51
+
52
+ const apiUrl = cfg.apiUrl ?? "https://110.vemic.com/skills-scanner";
48
53
  const behavioral = cfg.behavioral ?? false;
49
54
  const useLLM = cfg.useLLM ?? false;
50
55
  const policy = cfg.policy ?? "balanced";
51
- const preInstallScan = cfg.preInstallScan ?? "on";
52
56
  const onUnsafe = cfg.onUnsafe ?? "warn";
53
57
  const injectSecurityGuidance = cfg.injectSecurityGuidance ?? true;
54
- const enablePromptInjectionGuard = cfg.enablePromptInjectionGuard ?? false;
55
- const enableHighRiskOperationGuard = cfg.enableHighRiskOperationGuard ?? false;
58
+ const enableBeforeInstallHook = cfg.enableBeforeInstallHook ?? true;
56
59
 
57
60
  api.logger.info("[skills-scanner] ═══════════════════════════════════════");
58
61
  api.logger.info("[skills-scanner] Plugin loading...");
59
62
  api.logger.info(`[skills-scanner] API URL: ${apiUrl}`);
60
- api.logger.info(`[skills-scanner] Scan directories: ${scanDirs.join(", ")}`);
61
- api.logger.info(
62
- `[skills-scanner] Python dependencies: ${isPythonReady(PYTHON_CMD) ? "✅ Ready" : "❌ Not installed"}`
63
- );
63
+ api.logger.info(`[skills-scanner] Before-install hook: ${enableBeforeInstallHook ? " ENABLED" : "❌ DISABLED"}`);
64
+
65
+ if (isDebugMode()) {
66
+ api.logger.info("[skills-scanner] 🐛 DEBUG MODE ENABLED");
67
+ debugLog(api.logger, "Full configuration", cfg);
68
+ }
64
69
 
65
70
  // Inject system prompt guidance (can be disabled via config)
66
71
  if (injectSecurityGuidance) {
67
- // Build combined guidance
68
- const guidanceParts = [SKILLS_SECURITY_GUIDANCE];
69
-
70
- if (enablePromptInjectionGuard) {
71
- guidanceParts.push(PROMPT_INJECTION_GUARD);
72
- }
73
-
74
- if (enableHighRiskOperationGuard) {
75
- guidanceParts.push(HIGH_RISK_OPERATION_GUARD);
76
- }
77
-
78
- const combinedGuidance = guidanceParts.join("\n\n");
79
-
80
72
  api.on("before_prompt_build", async () => ({
81
- prependSystemContext: combinedGuidance,
73
+ prependSystemContext: SKILLS_SECURITY_GUIDANCE,
82
74
  }));
83
-
75
+
84
76
  api.logger.info("[skills-scanner] ✅ Security guidance injected into system prompt");
85
- if (enablePromptInjectionGuard) {
86
- api.logger.info("[skills-scanner] - Prompt injection guard enabled");
87
- }
88
- if (enableHighRiskOperationGuard) {
89
- api.logger.info("[skills-scanner] - High-risk operation guard enabled");
90
- }
91
77
  } else {
92
78
  api.logger.info("[skills-scanner] ⏭️ Security guidance injection disabled");
93
79
  }
94
80
 
81
+ // Register before_install hook (CRITICAL SECURITY GATE)
82
+ if (enableBeforeInstallHook) {
83
+ api.on("before_install", async (event: BeforeInstallEvent) => {
84
+ try {
85
+ return await handleBeforeInstall(event, {
86
+ apiUrl,
87
+ behavioral,
88
+ useLLM,
89
+ policy,
90
+ logger: api.logger,
91
+ enabled: enableBeforeInstallHook,
92
+ timeoutMs: cfg.scanTimeoutMs || 30000, // Use configured timeout or 30s default
93
+ });
94
+ } catch (err: any) {
95
+ api.logger.error("[skills-scanner] ❌ before_install hook error", {
96
+ error: err.message,
97
+ stack: err.stack,
98
+ sourcePath: event.sourcePath,
99
+ targetType: event.targetType,
100
+ targetName: event.targetName,
101
+ });
102
+ // Return safe default on error - allow installation but log the failure
103
+ return { block: false };
104
+ }
105
+ });
106
+
107
+ api.logger.info("[skills-scanner] 🛡️ before_install hook registered (installation interception active)");
108
+ } else {
109
+ api.logger.warn("[skills-scanner] ⚠️ before_install hook DISABLED - installations will NOT be intercepted!");
110
+ }
111
+
112
+ // 在插件启动时设置系统 crontab 清理任务
113
+ (async () => {
114
+ try {
115
+ await ensureCronJob({
116
+ logger: api.logger,
117
+ config: api.config,
118
+ });
119
+ api.logger.info("[skills-scanner] ✅ System crontab cleanup task ensured");
120
+ } catch (err: any) {
121
+ api.logger.warn("[skills-scanner] ⚠️ Failed to setup system crontab", {
122
+ error: err.message,
123
+ });
124
+ }
125
+ })();
126
+
127
+ // Register config_changed hook for hot reload
128
+ api.on("config_changed", async (newConfig: any) => {
129
+ api.logger.info("[skills-scanner] 🔄 Configuration changed, reloading...");
130
+
131
+ try {
132
+ const newCfg: ScannerConfig =
133
+ newConfig?.plugins?.entries?.["skills-scanner"]?.config ?? {};
134
+
135
+ // Validate new configuration
136
+ const validation = validateConfig(newCfg, api.logger);
137
+ if (!validation.valid) {
138
+ api.logger.error("[skills-scanner] ❌ Invalid new configuration, keeping old config");
139
+ return;
140
+ }
141
+
142
+ // Check what changed
143
+ const apiUrlChanged = newCfg.apiUrl !== cfg.apiUrl;
144
+
145
+ if (apiUrlChanged) {
146
+ api.logger.info("[skills-scanner] API URL updated", {
147
+ old: cfg.apiUrl,
148
+ new: newCfg.apiUrl,
149
+ });
150
+ // Update global apiUrl variable
151
+ Object.assign(cfg, { apiUrl: newCfg.apiUrl });
152
+ }
153
+
154
+ api.logger.info("[skills-scanner] ✅ Configuration reload completed");
155
+ } catch (err: any) {
156
+ api.logger.error("[skills-scanner] ❌ Configuration reload failed", {
157
+ error: err.message,
158
+ stack: err.stack,
159
+ });
160
+ }
161
+ });
162
+
95
163
  // Check if first run
96
- const firstRun = isFirstRun(cfg);
164
+ const firstRun = isFirstRun(cfg, api.runtime);
97
165
  if (firstRun) {
98
166
  api.logger.info("[skills-scanner] 🎉 First run detected");
99
167
  const configGuide = generateConfigGuide(
100
168
  cfg,
101
169
  apiUrl,
102
- scanDirs,
103
170
  behavioral,
104
171
  useLLM,
105
172
  policy,
106
- preInstallScan,
107
173
  onUnsafe
108
174
  );
109
175
  console.log(configGuide);
110
- markConfigReviewed();
176
+ markConfigReviewed(api.runtime);
111
177
  }
112
178
 
113
- // Install dependencies immediately
114
- if (!isPythonReady(PYTHON_CMD)) {
115
- api.logger.info("[skills-scanner] Installing Python dependencies...");
116
- ensureDeps(PYTHON_CMD, api.logger)
117
- .then((success) => {
118
- if (success) {
119
- api.logger.info("[skills-scanner] Dependencies installed");
179
+ // Health check endpoint
180
+ api.registerHttpRoute({
181
+ method: "GET",
182
+ path: "/health/skills-scanner",
183
+ handler: async (req, res) => {
184
+ try {
185
+ const state = loadState(api.runtime);
186
+
187
+ // Check API availability
188
+ let apiStatus = "unknown";
189
+ try {
190
+ const response = await fetch(`${apiUrl}/health`, {
191
+ signal: AbortSignal.timeout(3000),
192
+ });
193
+ apiStatus = response.ok ? "available" : "unavailable";
194
+ } catch {
195
+ apiStatus = "unavailable";
120
196
  }
121
- })
122
- .catch((err) => {
123
- api.logger.error(`[skills-scanner] Dependency installation failed: ${err.message}`);
124
- });
125
- }
126
-
127
- // Helper for watcher alerts
128
- function persistWatcherAlert(msg: string): void {
129
- const state = loadState();
130
- const alerts: string[] = (state as any).pendingAlerts ?? [];
131
- alerts.push(`[${new Date().toLocaleString("en-US")}] ${msg}`);
132
- saveState({ ...state, pendingAlerts: alerts } as any);
133
- api.logger.warn(`[skills-scanner] ${msg}`);
134
- }
135
-
136
- // Service: Install deps + start watcher
137
- let stopWatcher: (() => void) | null = null;
138
-
139
- api.registerService({
140
- id: "skills-scanner-setup",
141
- start: async () => {
142
- api.logger.info("[skills-scanner] 🚀 Service starting...");
143
197
 
144
- const depsReady = await ensureDeps(PYTHON_CMD, api.logger);
145
-
146
- if (!depsReady) {
147
- api.logger.error("[skills-scanner] Dependencies installation failed");
148
- return;
149
- }
198
+ // Get performance metrics
199
+ const metrics = getMetricsSummary(STATE_DIR);
200
+
201
+ const health = {
202
+ status: "healthy",
203
+ plugin: {
204
+ version: api.version || "1.0.0",
205
+ id: api.id,
206
+ name: api.name,
207
+ },
208
+ api: {
209
+ url: apiUrl,
210
+ status: apiStatus,
211
+ },
212
+ state: {
213
+ lastScanAt: state.lastScanAt || null,
214
+ lastShutdownAt: state.lastShutdownAt || null,
215
+ pendingAlerts: (state as any).pendingAlerts?.length || 0,
216
+ },
217
+ metrics: {
218
+ totalScans: metrics.totalScans,
219
+ successRate: metrics.successRate.toFixed(2) + "%",
220
+ averageDurationMs: metrics.averageDurationMs,
221
+ lastScanAt: metrics.lastScanAt || null,
222
+ },
223
+ config: {
224
+ policy,
225
+ behavioral,
226
+ useLLM,
227
+ beforeInstallHook: enableBeforeInstallHook,
228
+ },
229
+ timestamp: new Date().toISOString(),
230
+ };
150
231
 
151
- api.logger.info("[skills-scanner] Python dependencies ready (requests installed)");
232
+ res.status(200).json(health);
233
+ } catch (err: any) {
234
+ api.logger.error("[skills-scanner] Health check failed", {
235
+ error: err.message,
236
+ });
152
237
 
153
- if (preInstallScan === "on" && scanDirs.length > 0) {
154
- api.logger.info(`[skills-scanner] 📁 Starting file monitoring: ${scanDirs.length} directories`);
155
- stopWatcher = startWatcher(
156
- scanDirs,
157
- onUnsafe,
158
- behavioral,
159
- apiUrl,
160
- useLLM,
161
- policy,
162
- persistWatcherAlert,
163
- api.logger,
164
- PYTHON_CMD,
165
- SCAN_SCRIPT,
166
- QUARANTINE_DIR
167
- );
168
- api.logger.info("[skills-scanner] ✅ File monitoring started");
169
- } else {
170
- api.logger.info("[skills-scanner] ⏭️ Pre-install scan disabled");
238
+ res.status(503).json({
239
+ status: "unhealthy",
240
+ error: err.message,
241
+ timestamp: new Date().toISOString(),
242
+ });
171
243
  }
172
-
173
- // Auto-register cron job
174
- api.logger.info("[skills-scanner] 🕐 Setting up weekly report cron job...");
175
- await ensureCronJob(api.logger);
176
- },
177
- stop: () => {
178
- api.logger.info("[skills-scanner] 🛑 Service stopping...");
179
- stopWatcher?.();
180
- stopWatcher = null;
181
244
  },
182
245
  });
183
246
 
@@ -185,21 +248,18 @@ export default function register(api: OpenClawPluginApi) {
185
248
  const handlers = createCommandHandlers(
186
249
  cfg,
187
250
  apiUrl,
188
- scanDirs,
189
251
  behavioral,
190
252
  useLLM,
191
253
  policy,
192
- preInstallScan,
193
254
  onUnsafe,
194
- PYTHON_CMD,
195
- SCAN_SCRIPT,
196
- api.logger
255
+ api.logger,
256
+ api.config
197
257
  );
198
258
 
199
259
  // Chat command: /skills-scanner
200
260
  api.registerCommand({
201
261
  name: "skills-scanner",
202
- description: "Skills 安全扫描工具。用法: /skills-scanner <子命令> [参数]",
262
+ description: "Skills 安全扫描工具。用法:/skills-scanner <子命令> [参数]",
203
263
  acceptsArgs: true,
204
264
  requireAuth: true,
205
265
  handler: async (ctx: any) => {
@@ -212,20 +272,20 @@ export default function register(api: OpenClawPluginApi) {
212
272
  "",
213
273
  "可用命令:",
214
274
  "• `/skills-scanner scan <路径> [选项]` - 扫描 Skill",
275
+ "• `/skills-scanner scan clawhub <URL> [选项]` - 扫描 ClawHub Skill",
215
276
  "• `/skills-scanner health` - 健康检查",
216
277
  "• `/skills-scanner config [操作]` - 配置管理",
217
- "• `/skills-scanner cron [操作]` - 定时任务管理",
218
278
  "",
219
279
  "扫描选项:",
220
280
  "• `--detailed` - 显示详细发现",
221
281
  "• `--behavioral` - 启用行为分析",
222
282
  "• `--recursive` - 递归扫描子目录",
223
- "• `--report` - 生成日报格式",
224
283
  "",
225
284
  "示例:",
226
285
  "```",
227
286
  "/skills-scanner scan ~/my-skill",
228
287
  "/skills-scanner scan ~/skills --recursive",
288
+ "/skills-scanner scan clawhub https://clawhub.ai/username/project",
229
289
  "/skills-scanner health",
230
290
  "```",
231
291
  "",
@@ -244,13 +304,11 @@ export default function register(api: OpenClawPluginApi) {
244
304
  return await handlers.handleHealthCommand();
245
305
  } else if (subCommand === "config") {
246
306
  return await handlers.handleConfigCommand(subArgs);
247
- } else if (subCommand === "cron") {
248
- return await handlers.handleCronCommand(subArgs);
249
307
  } else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
250
308
  return { text: handlers.getHelpText() };
251
309
  } else {
252
310
  return {
253
- text: `❌ 未知子命令: ${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助`,
311
+ text: `❌ 未知子命令:${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助`,
254
312
  };
255
313
  }
256
314
  },
@@ -260,38 +318,25 @@ export default function register(api: OpenClawPluginApi) {
260
318
  api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
261
319
  const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
262
320
  if (!p) return respond(false, { error: "Missing path parameter" });
263
- if (!isPythonReady(PYTHON_CMD))
264
- return respond(false, { error: "Python dependencies not ready" });
265
- const res = await runScan(PYTHON_CMD, SCAN_SCRIPT, mode === "batch" ? "batch" : "scan", expandPath(p), {
266
- recursive,
267
- detailed,
268
- behavioral,
269
- apiUrl,
270
- useLLM,
271
- policy,
272
- });
273
- respond(res.exitCode === 0, {
274
- output: res.output,
275
- exitCode: res.exitCode,
276
- is_safe: res.exitCode === 0,
277
- });
278
- });
279
321
 
280
- api.registerGatewayMethod("skillsScanner.report", async ({ respond }: any) => {
281
- if (!isPythonReady(PYTHON_CMD))
282
- return respond(false, { error: "Python dependencies not ready" });
283
- if (scanDirs.length === 0) return respond(false, { error: "No scan directories found" });
284
- const report = await buildDailyReport(
285
- scanDirs,
286
- behavioral,
287
- apiUrl,
288
- useLLM,
289
- policy,
290
- api.logger,
291
- PYTHON_CMD,
292
- SCAN_SCRIPT
293
- );
294
- respond(true, { report, state: loadState() });
322
+ try {
323
+ const res = await runScan(mode === "batch" ? "batch" : "scan", expandPath(p), {
324
+ recursive,
325
+ detailed,
326
+ behavioral,
327
+ apiUrl,
328
+ useLLM,
329
+ policy,
330
+ });
331
+ respond(res.exitCode === 0, {
332
+ output: res.output,
333
+ exitCode: res.exitCode,
334
+ is_safe: res.exitCode === 0,
335
+ data: res.data,
336
+ });
337
+ } catch (err: any) {
338
+ respond(false, { error: err.message });
339
+ }
295
340
  });
296
341
 
297
342
  // CLI commands
@@ -305,7 +350,7 @@ export default function register(api: OpenClawPluginApi) {
305
350
  .option("--detailed", "显示所有发现")
306
351
  .option("--behavioral", "启用行为分析")
307
352
  .action(async (p: string, opts: any) => {
308
- const res = await runScan(PYTHON_CMD, SCAN_SCRIPT, "scan", expandPath(p), {
353
+ const res = await runScan("scan", expandPath(p), {
309
354
  ...opts,
310
355
  apiUrl,
311
356
  useLLM,
@@ -322,7 +367,7 @@ export default function register(api: OpenClawPluginApi) {
322
367
  .option("--detailed", "显示所有发现")
323
368
  .option("--behavioral", "启用行为分析")
324
369
  .action(async (d: string, opts: any) => {
325
- const res = await runScan(PYTHON_CMD, SCAN_SCRIPT, "batch", expandPath(d), {
370
+ const res = await runScan("batch", expandPath(d), {
326
371
  ...opts,
327
372
  apiUrl,
328
373
  useLLM,
@@ -332,65 +377,18 @@ export default function register(api: OpenClawPluginApi) {
332
377
  process.exit(res.exitCode);
333
378
  });
334
379
 
335
- cmd
336
- .command("report")
337
- .description("生成日报")
338
- .action(async () => {
339
- const report = await buildDailyReport(
340
- scanDirs,
341
- behavioral,
342
- apiUrl,
343
- useLLM,
344
- policy,
345
- console,
346
- PYTHON_CMD,
347
- SCAN_SCRIPT
348
- );
349
- console.log(report);
350
- });
351
-
352
380
  cmd
353
381
  .command("health")
354
382
  .description("检查 API 服务健康状态")
355
383
  .action(async () => {
356
- if (!isPythonReady(PYTHON_CMD)) {
357
- console.error("❌ Python 依赖未就绪");
358
- process.exit(1);
359
- }
360
-
361
- try {
362
- const { exec } = await import("node:child_process");
363
- const { promisify } = await import("node:util");
364
- const execAsync = promisify(exec);
365
-
366
- const cmd = `"${PYTHON_CMD}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
367
- const env = { ...process.env };
368
- delete env.http_proxy;
369
- delete env.https_proxy;
370
- delete env.HTTP_PROXY;
371
- delete env.HTTPS_PROXY;
372
- delete env.all_proxy;
373
- delete env.ALL_PROXY;
374
-
375
- const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
376
- const output = (stdout + stderr).trim();
377
- console.log(output);
378
-
379
- if (output.includes("✓") || output.includes("OK")) {
380
- process.exit(0);
381
- } else {
382
- process.exit(1);
383
- }
384
- } catch (err: any) {
385
- console.error(`❌ 连接失败: ${err.message}`);
386
- console.error(`\n💡 请确保 skill-scanner-api 服务正在运行:`);
387
- console.error(` skill-scanner-api`);
388
- process.exit(1);
389
- }
384
+ const res = await runScan("health", "", { apiUrl });
385
+ console.log(res.output);
386
+ process.exit(res.exitCode);
390
387
  });
391
388
  },
392
389
  { commands: ["skills-scanner"] }
393
390
  );
394
391
 
395
392
  api.logger.info("[skills-scanner] ✅ Plugin registered");
396
- }
393
+ },
394
+ });