@pwddd/skills-scanner 1.0.0-beta.2 → 1.0.0-beta.21
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 +3 -45
- package/index.ts +13 -266
- package/openclaw.plugin.json +1 -11
- package/package.json +1 -1
- package/skills/skills-scanner/SKILL.md +15 -51
- package/src/api-client.ts +32 -2
- package/src/before-install-hook.ts +42 -9
- package/src/commands.ts +21 -98
- package/src/config-validator.ts +0 -16
- package/src/config.ts +8 -50
- package/src/cron-manager.ts +117 -169
- package/src/prompt-guidance.ts +8 -18
- package/src/state.ts +0 -17
- package/src/types.ts +0 -4
- package/src/high-risk-operation-guard.ts +0 -62
- package/src/prompt-injection-guard.ts +0 -56
- package/src/report.ts +0 -128
- package/src/watcher.ts +0 -178
package/README.md
CHANGED
|
@@ -4,10 +4,9 @@ OpenClaw Skills 安全扫描插件,用于检测 Skills 中的潜在安全威
|
|
|
4
4
|
|
|
5
5
|
## 功能特性
|
|
6
6
|
|
|
7
|
-
- 🔍
|
|
7
|
+
- 🔍 **手动扫描**: 支持扫描指定路径的 Skill
|
|
8
8
|
- 🛡️ **安装前拦截**: 使用 before_install hook 在安装前强制拦截不安全的 Skills
|
|
9
9
|
- 🌐 **ClawHub 扫描**: 直接扫描 ClawHub 上的 Skill,无需手动下载
|
|
10
|
-
- 📊 **定时周报**: 每周一自动生成安全扫描报告
|
|
11
10
|
- 🛡️ **多种策略**: 支持 strict/balanced/permissive 三种扫描策略
|
|
12
11
|
- 🤖 **LLM 分析**: 可选的 LLM 语义分析
|
|
13
12
|
- 🔒 **自动隔离**: 检测到不安全的 Skill 自动隔离或删除
|
|
@@ -34,11 +33,9 @@ openclaw plugins install @pwddd/skills-scanner
|
|
|
34
33
|
"enabled": true,
|
|
35
34
|
"config": {
|
|
36
35
|
"apiUrl": "https://110.vemic.com/skills-scanner",
|
|
37
|
-
"scanDirs": ["~/.openclaw/skills", "~/.openclaw/workspace/skills"],
|
|
38
36
|
"behavioral": false,
|
|
39
37
|
"useLLM": false,
|
|
40
38
|
"policy": "balanced",
|
|
41
|
-
"preInstallScan": "on",
|
|
42
39
|
"onUnsafe": "warn",
|
|
43
40
|
"enableBeforeInstallHook": true
|
|
44
41
|
}
|
|
@@ -51,16 +48,12 @@ openclaw plugins install @pwddd/skills-scanner
|
|
|
51
48
|
### 配置说明
|
|
52
49
|
|
|
53
50
|
- `apiUrl`: 扫描 API 服务地址
|
|
54
|
-
- `scanDirs`: 要监控的 Skills 目录列表
|
|
55
51
|
- `behavioral`: 是否启用行为分析(深度扫描,较慢)
|
|
56
52
|
- `useLLM`: 是否使用 LLM 进行语义分析
|
|
57
53
|
- `policy`: 扫描策略
|
|
58
54
|
- `strict`: 严格模式,发现任何可疑行为都标记为不安全
|
|
59
55
|
- `balanced`: 平衡模式(推荐)
|
|
60
56
|
- `permissive`: 宽松模式,只标记明确的威胁
|
|
61
|
-
- `preInstallScan`: 是否启用文件监控(安装后扫描)
|
|
62
|
-
- `on`: 启用
|
|
63
|
-
- `off`: 禁用
|
|
64
57
|
- `onUnsafe`: 发现不安全 Skill 的处理方式
|
|
65
58
|
- `warn`: 仅警告,不处理(推荐)
|
|
66
59
|
- `quarantine`: 移入隔离目录
|
|
@@ -88,7 +81,6 @@ openclaw config reload
|
|
|
88
81
|
|
|
89
82
|
配置变更会自动:
|
|
90
83
|
- 更新 API URL
|
|
91
|
-
- 重启文件监控器(如果扫描目录变更)
|
|
92
84
|
- 应用新的扫描策略
|
|
93
85
|
|
|
94
86
|
#### 调试模式
|
|
@@ -108,7 +100,6 @@ SKILLS_SCANNER_DEBUG=1 openclaw gateway start
|
|
|
108
100
|
- 完整的配置信息
|
|
109
101
|
- 详细的扫描过程
|
|
110
102
|
- API 请求和响应
|
|
111
|
-
- 文件监控事件
|
|
112
103
|
|
|
113
104
|
#### 健康检查端点
|
|
114
105
|
|
|
@@ -130,11 +121,6 @@ curl http://localhost:3000/health/skills-scanner
|
|
|
130
121
|
"url": "https://110.vemic.com/skills-scanner",
|
|
131
122
|
"status": "available"
|
|
132
123
|
},
|
|
133
|
-
"watcher": {
|
|
134
|
-
"enabled": true,
|
|
135
|
-
"running": true,
|
|
136
|
-
"directories": 2
|
|
137
|
-
},
|
|
138
124
|
"metrics": {
|
|
139
125
|
"totalScans": 42,
|
|
140
126
|
"successRate": "95.24%",
|
|
@@ -152,7 +138,6 @@ curl http://localhost:3000/health/skills-scanner
|
|
|
152
138
|
/skills-scanner scan clawhub <URL> [选项] # 扫描 ClawHub Skill
|
|
153
139
|
/skills-scanner health # 健康检查
|
|
154
140
|
/skills-scanner config [操作] # 配置管理
|
|
155
|
-
/skills-scanner cron [操作] # 定时任务管理
|
|
156
141
|
/skills-scanner help # 帮助信息
|
|
157
142
|
```
|
|
158
143
|
|
|
@@ -161,14 +146,12 @@ curl http://localhost:3000/health/skills-scanner
|
|
|
161
146
|
- `--detailed`: 显示详细的安全发现
|
|
162
147
|
- `--behavioral`: 启用行为分析
|
|
163
148
|
- `--recursive`: 递归扫描子目录
|
|
164
|
-
- `--report`: 生成日报格式
|
|
165
149
|
|
|
166
150
|
#### 示例
|
|
167
151
|
|
|
168
152
|
```
|
|
169
153
|
/skills-scanner scan ~/.openclaw/skills/my-skill
|
|
170
154
|
/skills-scanner scan ~/.openclaw/skills --recursive
|
|
171
|
-
/skills-scanner scan ~/.openclaw/skills --report
|
|
172
155
|
/skills-scanner scan clawhub https://clawhub.ai/username/project
|
|
173
156
|
/skills-scanner scan clawhub https://clawhub.ai/Asleep123/caldav-calendar --detailed
|
|
174
157
|
/skills-scanner health
|
|
@@ -186,9 +169,6 @@ openclaw skills-scanner clawhub <url> [--detailed] [--behavioral]
|
|
|
186
169
|
# 批量扫描目录
|
|
187
170
|
openclaw skills-scanner batch <directory> [--recursive] [--detailed]
|
|
188
171
|
|
|
189
|
-
# 生成日报
|
|
190
|
-
openclaw skills-scanner report
|
|
191
|
-
|
|
192
172
|
# 检查 API 服务健康状态
|
|
193
173
|
openclaw skills-scanner health
|
|
194
174
|
```
|
|
@@ -196,10 +176,8 @@ openclaw skills-scanner health
|
|
|
196
176
|
## 工作流程
|
|
197
177
|
|
|
198
178
|
1. **插件启动**: 自动初始化并连接 API 服务
|
|
199
|
-
2.
|
|
200
|
-
3.
|
|
201
|
-
4. **结果处理**: 根据配置隔离/删除/警告不安全的 Skill
|
|
202
|
-
5. **定时周报**: 每周一 12:05 自动生成安全报告
|
|
179
|
+
2. **手动扫描**: 使用命令扫描指定的 Skill 或目录
|
|
180
|
+
3. **结果处理**: 根据配置隔离/删除/警告不安全的 Skill
|
|
203
181
|
|
|
204
182
|
## 故障排除
|
|
205
183
|
|
|
@@ -209,24 +187,6 @@ openclaw skills-scanner health
|
|
|
209
187
|
2. 运行健康检查:`/skills-scanner health`
|
|
210
188
|
3. 检查网络连接
|
|
211
189
|
|
|
212
|
-
### 定时任务未注册
|
|
213
|
-
|
|
214
|
-
定时任务会在插件启动时自动注册。如果需要手动注册:
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
# 手动注册定时任务
|
|
218
|
-
/skills-scanner cron setup
|
|
219
|
-
|
|
220
|
-
# 或使用 CLI
|
|
221
|
-
openclaw cron add \
|
|
222
|
-
--name "skills-weekly-report" \
|
|
223
|
-
--cron "5 12 * * 1" \
|
|
224
|
-
--tz "Asia/Shanghai" \
|
|
225
|
-
--session isolated \
|
|
226
|
-
--message "请执行 /skills-scanner scan --report 并把结果发送到此渠道" \
|
|
227
|
-
--announce
|
|
228
|
-
```
|
|
229
|
-
|
|
230
190
|
## 开发
|
|
231
191
|
|
|
232
192
|
### 目录结构
|
|
@@ -241,8 +201,6 @@ extensions/skills-scanner/
|
|
|
241
201
|
│ ├── api-client.ts # HTTP API 客户端
|
|
242
202
|
│ ├── config.ts # 配置管理
|
|
243
203
|
│ ├── scanner.ts # 扫描逻辑
|
|
244
|
-
│ ├── watcher.ts # 文件监控
|
|
245
|
-
│ ├── cron.ts # 定时任务
|
|
246
204
|
│ ├── commands.ts # 命令处理
|
|
247
205
|
│ └── types.ts # 类型定义
|
|
248
206
|
└── skills/
|
package/index.ts
CHANGED
|
@@ -6,27 +6,20 @@
|
|
|
6
6
|
|
|
7
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,
|
|
20
17
|
getStateDir,
|
|
21
18
|
} from "./src/state.js";
|
|
22
19
|
import { runScan } from "./src/scanner.js";
|
|
23
|
-
import {
|
|
24
|
-
import { ensureCronJobViaGateway, checkCronJobStatus } from "./src/cron-manager.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";
|
|
30
23
|
import { handleBeforeInstall } from "./src/before-install-hook.js";
|
|
31
24
|
import type { BeforeInstallEvent } from "./src/before-install-hook.js";
|
|
32
25
|
import { validateConfig } from "./src/config-validator.js";
|
|
@@ -57,24 +50,16 @@ export default definePluginEntry({
|
|
|
57
50
|
}
|
|
58
51
|
|
|
59
52
|
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
53
|
const behavioral = cfg.behavioral ?? false;
|
|
65
54
|
const useLLM = cfg.useLLM ?? false;
|
|
66
55
|
const policy = cfg.policy ?? "balanced";
|
|
67
|
-
const preInstallScan = cfg.preInstallScan ?? "on";
|
|
68
56
|
const onUnsafe = cfg.onUnsafe ?? "warn";
|
|
69
57
|
const injectSecurityGuidance = cfg.injectSecurityGuidance ?? true;
|
|
70
|
-
const enablePromptInjectionGuard = cfg.enablePromptInjectionGuard ?? false;
|
|
71
|
-
const enableHighRiskOperationGuard = cfg.enableHighRiskOperationGuard ?? false;
|
|
72
58
|
const enableBeforeInstallHook = cfg.enableBeforeInstallHook ?? true;
|
|
73
59
|
|
|
74
60
|
api.logger.info("[skills-scanner] ═══════════════════════════════════════");
|
|
75
61
|
api.logger.info("[skills-scanner] Plugin loading...");
|
|
76
62
|
api.logger.info(`[skills-scanner] API URL: ${apiUrl}`);
|
|
77
|
-
api.logger.info(`[skills-scanner] Scan directories: ${scanDirs.join(", ")}`);
|
|
78
63
|
api.logger.info(`[skills-scanner] Before-install hook: ${enableBeforeInstallHook ? "✅ ENABLED" : "❌ DISABLED"}`);
|
|
79
64
|
|
|
80
65
|
if (isDebugMode()) {
|
|
@@ -84,30 +69,11 @@ export default definePluginEntry({
|
|
|
84
69
|
|
|
85
70
|
// Inject system prompt guidance (can be disabled via config)
|
|
86
71
|
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
72
|
api.on("before_prompt_build", async () => ({
|
|
101
|
-
prependSystemContext:
|
|
73
|
+
prependSystemContext: SKILLS_SECURITY_GUIDANCE,
|
|
102
74
|
}));
|
|
103
75
|
|
|
104
76
|
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
77
|
} else {
|
|
112
78
|
api.logger.info("[skills-scanner] ⏭️ Security guidance injection disabled");
|
|
113
79
|
}
|
|
@@ -129,7 +95,9 @@ export default definePluginEntry({
|
|
|
129
95
|
api.logger.error("[skills-scanner] ❌ before_install hook error", {
|
|
130
96
|
error: err.message,
|
|
131
97
|
stack: err.stack,
|
|
132
|
-
|
|
98
|
+
sourcePath: event.sourcePath,
|
|
99
|
+
targetType: event.targetType,
|
|
100
|
+
targetName: event.targetName,
|
|
133
101
|
});
|
|
134
102
|
// Return safe default on error - allow installation but log the failure
|
|
135
103
|
return { block: false };
|
|
@@ -141,80 +109,20 @@ export default definePluginEntry({
|
|
|
141
109
|
api.logger.warn("[skills-scanner] ⚠️ before_install hook DISABLED - installations will NOT be intercepted!");
|
|
142
110
|
}
|
|
143
111
|
|
|
144
|
-
//
|
|
145
|
-
|
|
112
|
+
// 在插件启动时设置系统 crontab 清理任务
|
|
113
|
+
(async () => {
|
|
146
114
|
try {
|
|
147
|
-
|
|
148
|
-
await ensureCronJobViaGateway({
|
|
115
|
+
await ensureCronJob({
|
|
149
116
|
logger: api.logger,
|
|
150
|
-
|
|
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,
|
|
117
|
+
config: api.config,
|
|
159
118
|
});
|
|
160
|
-
|
|
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");
|
|
119
|
+
api.logger.info("[skills-scanner] ✅ System crontab cleanup task ensured");
|
|
211
120
|
} catch (err: any) {
|
|
212
|
-
api.logger.
|
|
121
|
+
api.logger.warn("[skills-scanner] ⚠️ Failed to setup system crontab", {
|
|
213
122
|
error: err.message,
|
|
214
|
-
stack: err.stack,
|
|
215
123
|
});
|
|
216
124
|
}
|
|
217
|
-
});
|
|
125
|
+
})();
|
|
218
126
|
|
|
219
127
|
// Register config_changed hook for hot reload
|
|
220
128
|
api.on("config_changed", async (newConfig: any) => {
|
|
@@ -233,8 +141,6 @@ export default definePluginEntry({
|
|
|
233
141
|
|
|
234
142
|
// Check what changed
|
|
235
143
|
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
144
|
|
|
239
145
|
if (apiUrlChanged) {
|
|
240
146
|
api.logger.info("[skills-scanner] API URL updated", {
|
|
@@ -245,42 +151,6 @@ export default definePluginEntry({
|
|
|
245
151
|
Object.assign(cfg, { apiUrl: newCfg.apiUrl });
|
|
246
152
|
}
|
|
247
153
|
|
|
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
154
|
api.logger.info("[skills-scanner] ✅ Configuration reload completed");
|
|
285
155
|
} catch (err: any) {
|
|
286
156
|
api.logger.error("[skills-scanner] ❌ Configuration reload failed", {
|
|
@@ -297,93 +167,15 @@ export default definePluginEntry({
|
|
|
297
167
|
const configGuide = generateConfigGuide(
|
|
298
168
|
cfg,
|
|
299
169
|
apiUrl,
|
|
300
|
-
scanDirs,
|
|
301
170
|
behavioral,
|
|
302
171
|
useLLM,
|
|
303
172
|
policy,
|
|
304
|
-
preInstallScan,
|
|
305
173
|
onUnsafe
|
|
306
174
|
);
|
|
307
175
|
console.log(configGuide);
|
|
308
176
|
markConfigReviewed(api.runtime);
|
|
309
177
|
}
|
|
310
178
|
|
|
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
179
|
// Health check endpoint
|
|
388
180
|
api.registerHttpRoute({
|
|
389
181
|
method: "GET",
|
|
@@ -417,11 +209,6 @@ export default definePluginEntry({
|
|
|
417
209
|
url: apiUrl,
|
|
418
210
|
status: apiStatus,
|
|
419
211
|
},
|
|
420
|
-
watcher: {
|
|
421
|
-
enabled: preInstallScan === "on",
|
|
422
|
-
running: stopWatcher !== null,
|
|
423
|
-
directories: scanDirs.length,
|
|
424
|
-
},
|
|
425
212
|
state: {
|
|
426
213
|
lastScanAt: state.lastScanAt || null,
|
|
427
214
|
lastShutdownAt: state.lastShutdownAt || null,
|
|
@@ -461,16 +248,12 @@ export default definePluginEntry({
|
|
|
461
248
|
const handlers = createCommandHandlers(
|
|
462
249
|
cfg,
|
|
463
250
|
apiUrl,
|
|
464
|
-
scanDirs,
|
|
465
251
|
behavioral,
|
|
466
252
|
useLLM,
|
|
467
253
|
policy,
|
|
468
|
-
preInstallScan,
|
|
469
254
|
onUnsafe,
|
|
470
255
|
api.logger,
|
|
471
|
-
|
|
472
|
-
return await api.callGateway(method, params);
|
|
473
|
-
}
|
|
256
|
+
api.config
|
|
474
257
|
);
|
|
475
258
|
|
|
476
259
|
// Chat command: /skills-scanner
|
|
@@ -492,13 +275,11 @@ export default definePluginEntry({
|
|
|
492
275
|
"• `/skills-scanner scan clawhub <URL> [选项]` - 扫描 ClawHub Skill",
|
|
493
276
|
"• `/skills-scanner health` - 健康检查",
|
|
494
277
|
"• `/skills-scanner config [操作]` - 配置管理",
|
|
495
|
-
"• `/skills-scanner cron [操作]` - 定时任务管理",
|
|
496
278
|
"",
|
|
497
279
|
"扫描选项:",
|
|
498
280
|
"• `--detailed` - 显示详细发现",
|
|
499
281
|
"• `--behavioral` - 启用行为分析",
|
|
500
282
|
"• `--recursive` - 递归扫描子目录",
|
|
501
|
-
"• `--report` - 生成日报格式",
|
|
502
283
|
"",
|
|
503
284
|
"示例:",
|
|
504
285
|
"```",
|
|
@@ -523,8 +304,6 @@ export default definePluginEntry({
|
|
|
523
304
|
return await handlers.handleHealthCommand();
|
|
524
305
|
} else if (subCommand === "config") {
|
|
525
306
|
return await handlers.handleConfigCommand(subArgs);
|
|
526
|
-
} else if (subCommand === "cron") {
|
|
527
|
-
return await handlers.handleCronCommand(subArgs);
|
|
528
307
|
} else if (subCommand === "help" || subCommand === "--help" || subCommand === "-h") {
|
|
529
308
|
return { text: handlers.getHelpText() };
|
|
530
309
|
} else {
|
|
@@ -560,23 +339,6 @@ export default definePluginEntry({
|
|
|
560
339
|
}
|
|
561
340
|
});
|
|
562
341
|
|
|
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
342
|
// CLI commands
|
|
581
343
|
api.registerCli(
|
|
582
344
|
({ program }: any) => {
|
|
@@ -615,21 +377,6 @@ export default definePluginEntry({
|
|
|
615
377
|
process.exit(res.exitCode);
|
|
616
378
|
});
|
|
617
379
|
|
|
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
380
|
cmd
|
|
634
381
|
.command("health")
|
|
635
382
|
.description("检查 API 服务健康状态")
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "skills-scanner",
|
|
3
3
|
"name": "Skills Scanner",
|
|
4
4
|
"description": "Security scanner for OpenClaw Skills to detect potential threats",
|
|
5
|
-
"version": "1.0.0-beta.
|
|
5
|
+
"version": "1.0.0-beta.21",
|
|
6
6
|
"author": "pwddd",
|
|
7
7
|
"skills": ["./skills"],
|
|
8
8
|
"uiHints": {
|
|
@@ -27,10 +27,6 @@
|
|
|
27
27
|
"label": "扫描策略",
|
|
28
28
|
"help": "strict=严格 / balanced=平衡(推荐)/ permissive=宽松"
|
|
29
29
|
},
|
|
30
|
-
"preInstallScan": {
|
|
31
|
-
"label": "安装前扫描",
|
|
32
|
-
"help": "监听新 Skill 并自动扫描"
|
|
33
|
-
},
|
|
34
30
|
"onUnsafe": {
|
|
35
31
|
"label": "不安全处理",
|
|
36
32
|
"help": "warn=仅警告(推荐)/ quarantine=隔离 / delete=删除"
|
|
@@ -85,12 +81,6 @@
|
|
|
85
81
|
"description": "Scanning policy: strict (more false positives) / balanced (recommended) / permissive (may miss threats)",
|
|
86
82
|
"default": "balanced"
|
|
87
83
|
},
|
|
88
|
-
"preInstallScan": {
|
|
89
|
-
"type": "string",
|
|
90
|
-
"enum": ["on", "off"],
|
|
91
|
-
"description": "Enable pre-installation scanning (monitors directories for new Skills)",
|
|
92
|
-
"default": "on"
|
|
93
|
-
},
|
|
94
84
|
"onUnsafe": {
|
|
95
85
|
"type": "string",
|
|
96
86
|
"enum": ["quarantine", "delete", "warn"],
|
package/package.json
CHANGED