@pwddd/skills-scanner 1.0.3

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 ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * OpenClaw Plugin: skills-scanner
3
+ *
4
+ * Security scanner for OpenClaw Skills to detect potential threats.
5
+ */
6
+
7
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
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
+ } from "./src/state.js";
21
+ import { ensureDeps, isVenvReady } from "./src/deps.js";
22
+ import { runScan } from "./src/scanner.js";
23
+ import { buildDailyReport } from "./src/report.js";
24
+ import { ensureCronJob } from "./src/cron.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
+
29
+ // Constants
30
+ const PLUGIN_ROOT = process.env.OPENCLAW_PLUGIN_ROOT || __dirname;
31
+ const SKILL_DIR = join(PLUGIN_ROOT, "skills", "skills-scanner");
32
+ const VENV_PYTHON = join(SKILL_DIR, ".venv", "bin", "python");
33
+ const SCAN_SCRIPT = join(SKILL_DIR, "scan.py");
34
+ const STATE_DIR = join(os.homedir(), ".openclaw", "skills-scanner");
35
+ const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
36
+
37
+ export default function register(api: OpenClawPluginApi) {
38
+ const cfg: ScannerConfig =
39
+ api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
40
+ const apiUrl = cfg.apiUrl ?? "http://localhost:8000";
41
+ const scanDirs =
42
+ (cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
43
+ ? cfg.scanDirs!.map(expandPath)
44
+ : defaultScanDirs();
45
+ const behavioral = cfg.behavioral ?? false;
46
+ const useLLM = cfg.useLLM ?? false;
47
+ const policy = cfg.policy ?? "balanced";
48
+ const preInstallScan = cfg.preInstallScan ?? "on";
49
+ const onUnsafe = cfg.onUnsafe ?? "quarantine";
50
+ const injectSecurityGuidance = cfg.injectSecurityGuidance ?? true;
51
+
52
+ api.logger.info("[skills-scanner] ═══════════════════════════════════════");
53
+ api.logger.info("[skills-scanner] Plugin loading...");
54
+ api.logger.info(`[skills-scanner] API URL: ${apiUrl}`);
55
+ api.logger.info(`[skills-scanner] Scan directories: ${scanDirs.join(", ")}`);
56
+ api.logger.info(
57
+ `[skills-scanner] Python dependencies: ${isVenvReady(VENV_PYTHON) ? "✅ Ready" : "❌ Not installed"}`
58
+ );
59
+
60
+ // Inject system prompt guidance (can be disabled via config)
61
+ if (injectSecurityGuidance) {
62
+ api.on("before_prompt_build", async () => ({
63
+ prependSystemContext: SKILLS_SECURITY_GUIDANCE,
64
+ }));
65
+ api.logger.info("[skills-scanner] ✅ Security guidance injected into system prompt");
66
+ } else {
67
+ api.logger.info("[skills-scanner] ⏭️ Security guidance injection disabled");
68
+ }
69
+
70
+ // Check if first run
71
+ const firstRun = isFirstRun(cfg);
72
+ if (firstRun) {
73
+ api.logger.info("[skills-scanner] 🎉 First run detected");
74
+ const configGuide = generateConfigGuide(
75
+ cfg,
76
+ apiUrl,
77
+ scanDirs,
78
+ behavioral,
79
+ useLLM,
80
+ policy,
81
+ preInstallScan,
82
+ onUnsafe
83
+ );
84
+ console.log(configGuide);
85
+ markConfigReviewed();
86
+ }
87
+
88
+ // Install dependencies immediately
89
+ if (!isVenvReady(VENV_PYTHON)) {
90
+ api.logger.info("[skills-scanner] Installing Python dependencies...");
91
+ ensureDeps(SKILL_DIR, VENV_PYTHON, api.logger)
92
+ .then((success) => {
93
+ if (success) {
94
+ api.logger.info("[skills-scanner] ✅ Dependencies installed");
95
+ }
96
+ })
97
+ .catch((err) => {
98
+ api.logger.error(`[skills-scanner] Dependency installation failed: ${err.message}`);
99
+ });
100
+ }
101
+
102
+ // Helper for watcher alerts
103
+ function persistWatcherAlert(msg: string): void {
104
+ const state = loadState();
105
+ const alerts: string[] = (state as any).pendingAlerts ?? [];
106
+ alerts.push(`[${new Date().toLocaleString("en-US")}] ${msg}`);
107
+ saveState({ ...state, pendingAlerts: alerts } as any);
108
+ api.logger.warn(`[skills-scanner] ${msg}`);
109
+ }
110
+
111
+ // Service: Install deps + start watcher
112
+ let stopWatcher: (() => void) | null = null;
113
+
114
+ api.registerService({
115
+ id: "skills-scanner-setup",
116
+ start: async () => {
117
+ api.logger.info("[skills-scanner] 🚀 Service starting...");
118
+
119
+ const depsReady = await ensureDeps(SKILL_DIR, VENV_PYTHON, api.logger);
120
+
121
+ if (!depsReady) {
122
+ api.logger.error("[skills-scanner] ❌ Dependencies installation failed");
123
+ return;
124
+ }
125
+
126
+ if (preInstallScan === "on" && scanDirs.length > 0) {
127
+ api.logger.info(`[skills-scanner] 📁 Starting file monitoring: ${scanDirs.length} directories`);
128
+ stopWatcher = startWatcher(
129
+ scanDirs,
130
+ onUnsafe,
131
+ behavioral,
132
+ apiUrl,
133
+ useLLM,
134
+ policy,
135
+ persistWatcherAlert,
136
+ api.logger,
137
+ VENV_PYTHON,
138
+ SCAN_SCRIPT,
139
+ QUARANTINE_DIR
140
+ );
141
+ api.logger.info("[skills-scanner] ✅ File monitoring started");
142
+ } else {
143
+ api.logger.info("[skills-scanner] ⏭️ Pre-install scan disabled");
144
+ }
145
+
146
+ // Register cron job (only in Gateway mode)
147
+ const isGatewayMode = !!(api as any).runtime;
148
+ if (isGatewayMode) {
149
+ await ensureCronJob(api.logger);
150
+ }
151
+
152
+ api.logger.info("[skills-scanner] ─────────────────────────────────────");
153
+ },
154
+ stop: () => {
155
+ api.logger.info("[skills-scanner] 🛑 Service stopping...");
156
+ stopWatcher?.();
157
+ stopWatcher = null;
158
+ },
159
+ });
160
+
161
+ // Command handlers
162
+ const handlers = createCommandHandlers(
163
+ cfg,
164
+ apiUrl,
165
+ scanDirs,
166
+ behavioral,
167
+ useLLM,
168
+ policy,
169
+ preInstallScan,
170
+ onUnsafe,
171
+ VENV_PYTHON,
172
+ SCAN_SCRIPT,
173
+ api.logger
174
+ );
175
+
176
+ // Chat command: /skills-scanner
177
+ api.registerCommand({
178
+ name: "skills-scanner",
179
+ description: "Skills 安全扫描工具。用法: /skills-scanner <子命令> [参数]",
180
+ acceptsArgs: true,
181
+ requireAuth: true,
182
+ handler: async (ctx: any) => {
183
+ const args = (ctx.args ?? "").trim();
184
+
185
+ if (!args) {
186
+ return {
187
+ text: [
188
+ "🔍 *Skills Scanner - 安全扫描工具*",
189
+ "",
190
+ "可用命令:",
191
+ "• `/skills-scanner scan <路径> [选项]` - 扫描 Skill",
192
+ "• `/skills-scanner status` - 查看状态",
193
+ "• `/skills-scanner config [操作]` - 配置管理",
194
+ "• `/skills-scanner cron [操作]` - 定时任务管理",
195
+ "",
196
+ "扫描选项:",
197
+ "• `--detailed` - 显示详细发现",
198
+ "• `--behavioral` - 启用行为分析",
199
+ "• `--recursive` - 递归扫描子目录",
200
+ "• `--report` - 生成日报格式",
201
+ "",
202
+ "示例:",
203
+ "```",
204
+ "/skills-scanner scan ~/my-skill",
205
+ "/skills-scanner scan ~/skills --recursive",
206
+ "/skills-scanner status",
207
+ "```",
208
+ "",
209
+ "💡 使用 `/skills-scanner help` 查看详细帮助",
210
+ ].join("\n"),
211
+ };
212
+ }
213
+
214
+ const parts = args.split(/\s+/);
215
+ const subCommand = parts[0].toLowerCase();
216
+ const subArgs = parts.slice(1).join(" ");
217
+
218
+ if (subCommand === "scan") {
219
+ return await handlers.handleScanCommand(subArgs);
220
+ } else if (subCommand === "status") {
221
+ return await handlers.handleStatusCommand();
222
+ } else if (subCommand === "config") {
223
+ return await handlers.handleConfigCommand(subArgs);
224
+ } else if (subCommand === "cron") {
225
+ return await handlers.handleCronCommand(subArgs);
226
+ } else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
227
+ return { text: handlers.getHelpText() };
228
+ } else {
229
+ return {
230
+ text: `❌ 未知子命令: ${subCommand}\n\n使用 \`/skills-scanner help\` 查看帮助`,
231
+ };
232
+ }
233
+ },
234
+ });
235
+
236
+ // Gateway RPC methods
237
+ api.registerGatewayMethod("skillsScanner.scan", async ({ respond, params }: any) => {
238
+ const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
239
+ if (!p) return respond(false, { error: "Missing path parameter" });
240
+ if (!isVenvReady(VENV_PYTHON))
241
+ return respond(false, { error: "Python dependencies not ready" });
242
+ const res = await runScan(VENV_PYTHON, SCAN_SCRIPT, mode === "batch" ? "batch" : "scan", expandPath(p), {
243
+ recursive,
244
+ detailed,
245
+ behavioral,
246
+ apiUrl,
247
+ useLLM,
248
+ policy,
249
+ });
250
+ respond(res.exitCode === 0, {
251
+ output: res.output,
252
+ exitCode: res.exitCode,
253
+ is_safe: res.exitCode === 0,
254
+ });
255
+ });
256
+
257
+ api.registerGatewayMethod("skillsScanner.report", async ({ respond }: any) => {
258
+ if (!isVenvReady(VENV_PYTHON))
259
+ return respond(false, { error: "Python dependencies not ready" });
260
+ if (scanDirs.length === 0) return respond(false, { error: "No scan directories found" });
261
+ const report = await buildDailyReport(
262
+ scanDirs,
263
+ behavioral,
264
+ apiUrl,
265
+ useLLM,
266
+ policy,
267
+ api.logger,
268
+ VENV_PYTHON,
269
+ SCAN_SCRIPT
270
+ );
271
+ respond(true, { report, state: loadState() });
272
+ });
273
+
274
+ // CLI commands
275
+ api.registerCli(
276
+ ({ program }: any) => {
277
+ const cmd = program.command("skills-scanner").description("OpenClaw Skills 安全扫描工具");
278
+
279
+ cmd
280
+ .command("scan <path>")
281
+ .description("扫描单个 Skill")
282
+ .option("--detailed", "显示所有发现")
283
+ .option("--behavioral", "启用行为分析")
284
+ .action(async (p: string, opts: any) => {
285
+ const res = await runScan(VENV_PYTHON, SCAN_SCRIPT, "scan", expandPath(p), {
286
+ ...opts,
287
+ apiUrl,
288
+ useLLM,
289
+ policy,
290
+ });
291
+ console.log(res.output);
292
+ process.exit(res.exitCode);
293
+ });
294
+
295
+ cmd
296
+ .command("batch <directory>")
297
+ .description("批量扫描目录")
298
+ .option("--recursive", "递归扫描子目录")
299
+ .option("--detailed", "显示所有发现")
300
+ .option("--behavioral", "启用行为分析")
301
+ .action(async (d: string, opts: any) => {
302
+ const res = await runScan(VENV_PYTHON, SCAN_SCRIPT, "batch", expandPath(d), {
303
+ ...opts,
304
+ apiUrl,
305
+ useLLM,
306
+ policy,
307
+ });
308
+ console.log(res.output);
309
+ process.exit(res.exitCode);
310
+ });
311
+
312
+ cmd
313
+ .command("report")
314
+ .description("生成日报")
315
+ .action(async () => {
316
+ const report = await buildDailyReport(
317
+ scanDirs,
318
+ behavioral,
319
+ apiUrl,
320
+ useLLM,
321
+ policy,
322
+ console,
323
+ VENV_PYTHON,
324
+ SCAN_SCRIPT
325
+ );
326
+ console.log(report);
327
+ });
328
+
329
+ cmd
330
+ .command("health")
331
+ .description("检查 API 服务健康状态")
332
+ .action(async () => {
333
+ if (!isVenvReady(VENV_PYTHON)) {
334
+ console.error("❌ Python 依赖未就绪");
335
+ process.exit(1);
336
+ }
337
+
338
+ try {
339
+ const { exec } = await import("node:child_process");
340
+ const { promisify } = await import("node:util");
341
+ const execAsync = promisify(exec);
342
+
343
+ const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
344
+ const env = { ...process.env };
345
+ delete env.http_proxy;
346
+ delete env.https_proxy;
347
+ delete env.HTTP_PROXY;
348
+ delete env.HTTPS_PROXY;
349
+ delete env.all_proxy;
350
+ delete env.ALL_PROXY;
351
+
352
+ const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
353
+ const output = (stdout + stderr).trim();
354
+ console.log(output);
355
+
356
+ if (output.includes("✓") || output.includes("OK")) {
357
+ process.exit(0);
358
+ } else {
359
+ process.exit(1);
360
+ }
361
+ } catch (err: any) {
362
+ console.error(`❌ 连接失败: ${err.message}`);
363
+ console.error(`\n💡 请确保 skill-scanner-api 服务正在运行:`);
364
+ console.error(` skill-scanner-api`);
365
+ process.exit(1);
366
+ }
367
+ });
368
+ },
369
+ { commands: ["skills-scanner"] }
370
+ );
371
+
372
+ api.logger.info("[skills-scanner] ✅ Plugin registered");
373
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "id": "skills-scanner",
3
+ "name": "Skills Scanner",
4
+ "description": "Security scanner for OpenClaw Skills to detect potential threats",
5
+ "version": "1.0.1",
6
+ "author": "pwddd",
7
+ "skills": ["./skills"],
8
+ "configSchema": {
9
+ "type": "object",
10
+ "additionalProperties": false,
11
+ "properties": {
12
+ "apiUrl": {
13
+ "type": "string",
14
+ "description": "Scanner API service URL",
15
+ "default": "http://localhost:8000"
16
+ },
17
+ "scanDirs": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "string"
21
+ },
22
+ "description": "List of directories to scan for Skills",
23
+ "default": []
24
+ },
25
+ "behavioral": {
26
+ "type": "boolean",
27
+ "description": "Enable behavioral analysis (slower but more accurate)",
28
+ "default": false
29
+ },
30
+ "useLLM": {
31
+ "type": "boolean",
32
+ "description": "Enable LLM-based semantic analysis",
33
+ "default": false
34
+ },
35
+ "policy": {
36
+ "type": "string",
37
+ "enum": ["strict", "balanced", "permissive"],
38
+ "description": "Scanning policy: strict (more false positives) / balanced (recommended) / permissive (may miss threats)",
39
+ "default": "balanced"
40
+ },
41
+ "preInstallScan": {
42
+ "type": "string",
43
+ "enum": ["on", "off"],
44
+ "description": "Enable pre-installation scanning (monitors directories for new Skills)",
45
+ "default": "on"
46
+ },
47
+ "onUnsafe": {
48
+ "type": "string",
49
+ "enum": ["quarantine", "delete", "warn"],
50
+ "description": "Action to take when unsafe Skill is detected: quarantine (recommended) / delete / warn",
51
+ "default": "quarantine"
52
+ },
53
+ "injectSecurityGuidance": {
54
+ "type": "boolean",
55
+ "description": "Inject Skills security guidance into system prompt (requires AI to scan before installing Skills)",
56
+ "default": true
57
+ }
58
+ }
59
+ }
60
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@pwddd/skills-scanner",
3
+ "version": "1.0.3",
4
+ "description": "OpenClaw Skills security scanner plugin - detect malicious code, data exfiltration, and prompt injection",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "author": "pwddd",
8
+ "license": "MIT",
9
+ "keywords": [
10
+ "openclaw",
11
+ "openclaw-plugin",
12
+ "security",
13
+ "scanner",
14
+ "skills",
15
+ "malware-detection"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/openclaw/openclaw.git",
20
+ "directory": "extensions/skills-scanner"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/openclaw/openclaw/issues"
24
+ },
25
+ "homepage": "https://github.com/openclaw/openclaw/tree/main/extensions/skills-scanner#readme",
26
+ "files": [
27
+ "index.ts",
28
+ "openclaw.plugin.json",
29
+ "README.md",
30
+ "src/**/*.ts",
31
+ "skills/**/*"
32
+ ],
33
+ "dependencies": {
34
+ "@sinclair/typebox": "0.34.48"
35
+ },
36
+ "peerDependencies": {
37
+ "openclaw": ">=2026.3.9"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "openclaw": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "openclaw": {
45
+ "extensions": [
46
+ "./index.ts"
47
+ ]
48
+ }
49
+ }