@pwddd/skills-scanner 1.2.4 → 2.1.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.
Potentially problematic release.
This version of @pwddd/skills-scanner might be problematic. Click here for more details.
- package/README.md +130 -7
- package/index.ts +170 -95
- package/openclaw.plugin.json +22 -3
- package/package.json +3 -3
- package/skills/skills-scanner/scan.py +461 -240
package/README.md
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
# openclaw-skills-scanner
|
|
2
2
|
|
|
3
|
-
OpenClaw Plugin
|
|
3
|
+
OpenClaw Plugin,通过 HTTP API 调用远程 [cisco-ai-skill-scanner](https://github.com/cisco-ai-defense/skill-scanner) 服务实现对 OpenClaw Skills 的安全扫描。
|
|
4
|
+
|
|
5
|
+
## 架构说明
|
|
6
|
+
|
|
7
|
+
本插件采用 **HTTP 客户端模式**:
|
|
8
|
+
- **客户端**:本插件(TypeScript + Python HTTP 客户端)
|
|
9
|
+
- **服务端**:独立运行的 `skill-scanner-api` 服务(需单独部署)
|
|
10
|
+
- **通信方式**:通过 HTTP REST API 调用扫描服务
|
|
11
|
+
|
|
12
|
+
### 为什么使用 HTTP 模式?
|
|
13
|
+
|
|
14
|
+
1. **依赖隔离**:避免复杂的 Python 依赖安装问题(LiteLLM、代理配置等)
|
|
15
|
+
2. **服务复用**:多个客户端可共享同一个扫描服务
|
|
16
|
+
3. **灵活部署**:扫描服务可部署在专用服务器上,支持更强大的硬件配置
|
|
17
|
+
4. **简化维护**:客户端只需 `requests` 库,无需安装完整的 `cisco-ai-skill-scanner`
|
|
4
18
|
|
|
5
19
|
## 功能
|
|
6
20
|
|
|
7
21
|
| 功能 | 实现方式 | 状态 |
|
|
8
22
|
|---|---|---|
|
|
9
|
-
| 启动时自动安装 Python
|
|
23
|
+
| 启动时自动安装 Python 依赖(requests) | `registerService` | ✅ 全自动 |
|
|
10
24
|
| `/scan-skill` `/scan-skills` 按需扫描 | `registerCommand` | ✅ 全自动 |
|
|
11
25
|
| `/scan-report` 立即日报 | `registerCommand` | ✅ 全自动 |
|
|
12
26
|
| `/scan-status` 状态查看 | `registerCommand` | ✅ 全自动 |
|
|
@@ -17,21 +31,47 @@ OpenClaw Plugin,基于 [cisco-ai-skill-scanner](https://github.com/cisco-ai-de
|
|
|
17
31
|
|
|
18
32
|
---
|
|
19
33
|
|
|
20
|
-
##
|
|
34
|
+
## 前置要求
|
|
35
|
+
|
|
36
|
+
### 1. 启动 skill-scanner-api 服务
|
|
37
|
+
|
|
38
|
+
在服务器上安装并启动 API 服务:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 安装 cisco-ai-skill-scanner
|
|
42
|
+
pip install cisco-ai-skill-scanner
|
|
43
|
+
|
|
44
|
+
# 启动 API 服务(默认端口 8000)
|
|
45
|
+
skill-scanner-api
|
|
46
|
+
|
|
47
|
+
# 或指定端口
|
|
48
|
+
skill-scanner-api --port 8080
|
|
49
|
+
|
|
50
|
+
# 开发模式(自动重载)
|
|
51
|
+
skill-scanner-api --reload
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
验证服务是否正常:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
curl http://localhost:8000/health
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. 安装本插件
|
|
21
61
|
|
|
22
62
|
```bash
|
|
23
63
|
# 从本地目录安装
|
|
24
64
|
openclaw plugins install ./openclaw-skills-scanner
|
|
25
65
|
|
|
26
|
-
# 或从 npm
|
|
27
|
-
openclaw plugins install @
|
|
66
|
+
# 或从 npm 安装
|
|
67
|
+
openclaw plugins install @pwddd/skills-scanner
|
|
28
68
|
|
|
29
69
|
# 重启 Gateway(Plugin 变更必须重启)
|
|
30
70
|
openclaw gateway restart
|
|
31
71
|
```
|
|
32
72
|
|
|
33
73
|
重启后 Gateway 会自动:
|
|
34
|
-
1. 创建 Python venv 并安装 `
|
|
74
|
+
1. 创建 Python venv 并安装 `requests` 库
|
|
35
75
|
2. 开始监听 Skills 目录(安装前扫描)
|
|
36
76
|
3. 在启动日志里打印是否需要注册 Cron Job
|
|
37
77
|
|
|
@@ -116,8 +156,11 @@ Plugin 启动后用 `fs.watch` 监听所有 Skills 目录。任何新 Skill 出
|
|
|
116
156
|
"skills-scanner": {
|
|
117
157
|
"enabled": true,
|
|
118
158
|
"config": {
|
|
159
|
+
"apiUrl": "http://localhost:8000", // API 服务地址
|
|
119
160
|
"scanDirs": ["~/.openclaw/skills"], // 留空自动检测
|
|
120
161
|
"behavioral": false, // 行为分析(较慢)
|
|
162
|
+
"useLLM": false, // 启用 LLM 分析
|
|
163
|
+
"policy": "balanced", // strict | balanced | permissive
|
|
121
164
|
"preInstallScan": "on", // fs.watch 安装前扫描
|
|
122
165
|
"onUnsafe": "quarantine" // quarantine | delete | warn
|
|
123
166
|
}
|
|
@@ -127,12 +170,92 @@ Plugin 启动后用 `fs.watch` 监听所有 Skills 目录。任何新 Skill 出
|
|
|
127
170
|
}
|
|
128
171
|
```
|
|
129
172
|
|
|
173
|
+
### 配置说明
|
|
174
|
+
|
|
175
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `apiUrl` | string | `http://localhost:8000` | skill-scanner-api 服务地址 |
|
|
178
|
+
| `scanDirs` | string[] | 自动检测 | 要扫描的 Skills 目录列表 |
|
|
179
|
+
| `behavioral` | boolean | `false` | 启用行为分析(更准确但较慢) |
|
|
180
|
+
| `useLLM` | boolean | `false` | 启用 LLM 分析(需 API 服务配置 LLM) |
|
|
181
|
+
| `policy` | string | `balanced` | 扫描策略:`strict`(严格)/ `balanced`(平衡)/ `permissive`(宽松) |
|
|
182
|
+
| `preInstallScan` | string | `on` | 是否启用 fs.watch 安装前扫描 |
|
|
183
|
+
| `onUnsafe` | string | `quarantine` | 发现不安全 Skill 时的处置方式 |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## HTTP API 端点说明
|
|
188
|
+
|
|
189
|
+
本插件使用以下 API 端点:
|
|
190
|
+
|
|
191
|
+
| 端点 | 方法 | 用途 |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| `/health` | GET | 健康检查 |
|
|
194
|
+
| `/scan-upload` | POST | 上传 ZIP 文件扫描(单个 Skill) |
|
|
195
|
+
| `/scan-batch` | POST | 批量异步扫描(服务器本地目录) |
|
|
196
|
+
| `/scan-batch/{scan_id}` | GET | 查询批量扫描结果 |
|
|
197
|
+
|
|
198
|
+
### 扫描方式
|
|
199
|
+
|
|
200
|
+
1. **单个 Skill 扫描**:使用 `/scan-upload` 端点
|
|
201
|
+
- 客户端将 Skill 目录打包成 ZIP
|
|
202
|
+
- 上传到服务器
|
|
203
|
+
- 服务器解压并扫描
|
|
204
|
+
- 返回扫描结果
|
|
205
|
+
|
|
206
|
+
2. **批量扫描(服务器本地)**:使用 `/scan-batch` 端点
|
|
207
|
+
- 适用于服务器本地目录
|
|
208
|
+
- 异步执行,返回 scan_id
|
|
209
|
+
- 轮询 `/scan-batch/{scan_id}` 获取结果
|
|
210
|
+
|
|
211
|
+
3. **批量扫描(客户端上传)**:客户端循环调用 `/scan-upload`
|
|
212
|
+
- 适用于客户端本地多个 Skills
|
|
213
|
+
- 逐个打包上传扫描
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## 故障排查
|
|
218
|
+
|
|
219
|
+
### 1. 连接失败
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
✗ 无法连接到 API 服务: http://localhost:8000
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**解决方法**:
|
|
226
|
+
- 确认 `skill-scanner-api` 服务正在运行
|
|
227
|
+
- 检查配置中的 `apiUrl` 是否正确
|
|
228
|
+
- 测试连接:`curl http://localhost:8000/health`
|
|
229
|
+
|
|
230
|
+
### 2. 代理问题
|
|
231
|
+
|
|
232
|
+
如果遇到代理相关错误,插件会自动清除代理环境变量。如果仍有问题,手动清除:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 3. Python 依赖问题
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
⏳ Python 依赖尚未就绪
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**解决方法**:
|
|
245
|
+
- 检查 `uv` 是否安装:`uv --version`
|
|
246
|
+
- 手动安装依赖:
|
|
247
|
+
```bash
|
|
248
|
+
cd ~/.openclaw/extensions/skills-scanner/skills/skills-scanner
|
|
249
|
+
uv venv .venv --python 3.10
|
|
250
|
+
uv pip install --python .venv/bin/python requests>=2.31.0
|
|
251
|
+
```
|
|
252
|
+
|
|
130
253
|
---
|
|
131
254
|
|
|
132
255
|
## 发布到 npm
|
|
133
256
|
|
|
134
257
|
```bash
|
|
135
|
-
#
|
|
258
|
+
# 已发布为 @pwddd/skills-scanner
|
|
136
259
|
npm login
|
|
137
260
|
npm publish --access public
|
|
138
261
|
```
|
package/index.ts
CHANGED
|
@@ -42,10 +42,16 @@ const QUARANTINE_DIR = join(STATE_DIR, "quarantine");
|
|
|
42
42
|
// ── 型別 ──────────────────────────────────────────────────────────────────────
|
|
43
43
|
|
|
44
44
|
interface ScannerConfig {
|
|
45
|
+
/** API 服务地址 */
|
|
46
|
+
apiUrl?: string;
|
|
45
47
|
/** 扫描目录列表,默认自动检测 */
|
|
46
48
|
scanDirs?: string[];
|
|
47
49
|
/** 是否启用行为分析(较慢但更准确) */
|
|
48
50
|
behavioral?: boolean;
|
|
51
|
+
/** 是否启用 LLM 分析 */
|
|
52
|
+
useLLM?: boolean;
|
|
53
|
+
/** 扫描策略 */
|
|
54
|
+
policy?: "strict" | "balanced" | "permissive";
|
|
49
55
|
/**
|
|
50
56
|
* 安装前扫描(fs.watch):
|
|
51
57
|
* - "on" 监听所有 scanDirs(默认)
|
|
@@ -75,10 +81,10 @@ function hasUv(): boolean {
|
|
|
75
81
|
|
|
76
82
|
function isVenvReady(): boolean {
|
|
77
83
|
if (!existsSync(VENV_PYTHON)) return false;
|
|
78
|
-
|
|
79
|
-
// 检查
|
|
84
|
+
|
|
85
|
+
// 检查 requests 是否安装
|
|
80
86
|
try {
|
|
81
|
-
execSync(`"${VENV_PYTHON}" -c "import
|
|
87
|
+
execSync(`"${VENV_PYTHON}" -c "import requests"`, { stdio: 'ignore' });
|
|
82
88
|
return true;
|
|
83
89
|
} catch {
|
|
84
90
|
return false;
|
|
@@ -115,74 +121,40 @@ function saveState(s: ScanState) {
|
|
|
115
121
|
|
|
116
122
|
async function ensureDeps(logger: any): Promise<boolean> {
|
|
117
123
|
if (isVenvReady()) {
|
|
118
|
-
logger.info("[skills-scanner] Python 依赖已就绪(
|
|
124
|
+
logger.info("[skills-scanner] Python 依赖已就绪(requests 已安装)");
|
|
119
125
|
return true;
|
|
120
126
|
}
|
|
121
|
-
|
|
127
|
+
|
|
122
128
|
if (!hasUv()) {
|
|
123
129
|
logger.warn("[skills-scanner] uv 未安装:brew install uv 或 curl -LsSf https://astral.sh/uv/install.sh | sh");
|
|
124
130
|
return false;
|
|
125
131
|
}
|
|
126
|
-
|
|
127
|
-
logger.info("[skills-scanner] 正在安装
|
|
128
|
-
|
|
132
|
+
|
|
133
|
+
logger.info("[skills-scanner] 正在安装 Python 依赖...");
|
|
134
|
+
|
|
129
135
|
try {
|
|
130
|
-
// 使用 uv 创建 venv 并安装包(一步完成)
|
|
131
|
-
logger.info("[skills-scanner] 创建虚拟环境并安装依赖...");
|
|
132
136
|
const venvDir = join(SKILL_DIR, ".venv");
|
|
133
|
-
|
|
137
|
+
|
|
134
138
|
// 如果 venv 已存在,先删除
|
|
135
139
|
if (existsSync(venvDir)) {
|
|
136
140
|
logger.info("[skills-scanner] 清理旧的虚拟环境...");
|
|
137
141
|
rmSync(venvDir, { recursive: true, force: true });
|
|
138
142
|
}
|
|
139
|
-
|
|
140
|
-
//
|
|
143
|
+
|
|
144
|
+
// 创建 venv
|
|
141
145
|
await execAsync(`uv venv "${venvDir}" --python 3.10`);
|
|
142
146
|
logger.info("[skills-scanner] 虚拟环境创建完成");
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
logger.info("[skills-scanner] 安装
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const installResult = await execAsync(installCmd);
|
|
150
|
-
logger.info(`[skills-scanner] 安装输出: ${installResult.stdout.trim()}`);
|
|
151
|
-
if (installResult.stderr) {
|
|
152
|
-
logger.warn(`[skills-scanner] 安装警告: ${installResult.stderr.trim()}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
147
|
+
|
|
148
|
+
// 安装 requests
|
|
149
|
+
logger.info("[skills-scanner] 安装 requests...");
|
|
150
|
+
await execAsync(`uv pip install --python "${VENV_PYTHON}" requests>=2.31.0`);
|
|
151
|
+
|
|
155
152
|
// 验证安装
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
logger.info(`[skills-scanner] ✅ 验证成功: ${verifyResult.trim()}`);
|
|
160
|
-
return true;
|
|
161
|
-
} catch (verifyErr: any) {
|
|
162
|
-
logger.error("[skills-scanner] ❌ 验证失败");
|
|
163
|
-
logger.error(`[skills-scanner] 错误: ${verifyErr.message}`);
|
|
164
|
-
|
|
165
|
-
// 尝试手动检查文件
|
|
166
|
-
try {
|
|
167
|
-
const sitePackages = join(venvDir, "lib");
|
|
168
|
-
logger.debug(`[skills-scanner] 检查 site-packages: ${sitePackages}`);
|
|
169
|
-
if (existsSync(sitePackages)) {
|
|
170
|
-
const { stdout } = await execAsync(`find "${sitePackages}" -name "skill_scanner*" -type d`);
|
|
171
|
-
logger.debug(`[skills-scanner] 找到的包: ${stdout.trim() || "无"}`);
|
|
172
|
-
}
|
|
173
|
-
} catch {}
|
|
174
|
-
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
153
|
+
execSync(`"${VENV_PYTHON}" -c "import requests"`, { stdio: 'ignore' });
|
|
154
|
+
logger.info("[skills-scanner] ✅ 依赖安装完成");
|
|
155
|
+
return true;
|
|
177
156
|
} catch (err: any) {
|
|
178
157
|
logger.error(`[skills-scanner] ⚠️ 依赖安装失败: ${err.message}`);
|
|
179
|
-
if (err.stdout) logger.error(`[skills-scanner] stdout: ${err.stdout}`);
|
|
180
|
-
if (err.stderr) logger.error(`[skills-scanner] stderr: ${err.stderr}`);
|
|
181
|
-
logger.error(`[skills-scanner] 请手动运行:`);
|
|
182
|
-
logger.error(`[skills-scanner] cd ~/.openclaw/extensions/skills-scanner/skills/skills-scanner`);
|
|
183
|
-
logger.error(`[skills-scanner] rm -rf .venv`);
|
|
184
|
-
logger.error(`[skills-scanner] uv venv .venv --python 3.10`);
|
|
185
|
-
logger.error(`[skills-scanner] uv pip install --python .venv/bin/python cisco-ai-skill-scanner`);
|
|
186
158
|
return false;
|
|
187
159
|
}
|
|
188
160
|
}
|
|
@@ -190,25 +162,24 @@ async function ensureDeps(logger: any): Promise<boolean> {
|
|
|
190
162
|
async function runScan(
|
|
191
163
|
mode: "scan" | "batch",
|
|
192
164
|
target: string,
|
|
193
|
-
opts: { detailed?: boolean; behavioral?: boolean; recursive?: boolean; jsonOut?: string } = {}
|
|
165
|
+
opts: { detailed?: boolean; behavioral?: boolean; recursive?: boolean; jsonOut?: string; apiUrl?: string; useLLM?: boolean; policy?: string } = {}
|
|
194
166
|
): Promise<{ exitCode: number; output: string }> {
|
|
195
167
|
const args = [mode, target];
|
|
196
168
|
if (opts.detailed) args.push("--detailed");
|
|
197
169
|
if (opts.behavioral) args.push("--behavioral");
|
|
198
170
|
if (opts.recursive) args.push("--recursive");
|
|
171
|
+
if (opts.useLLM) args.push("--llm");
|
|
172
|
+
if (opts.policy) args.push("--policy", opts.policy);
|
|
199
173
|
if (opts.jsonOut) args.push("--json", opts.jsonOut);
|
|
174
|
+
if (opts.apiUrl) args.unshift("--api-url", opts.apiUrl);
|
|
200
175
|
|
|
201
176
|
const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" ${args.map(a => `"${a}"`).join(" ")}`;
|
|
202
|
-
|
|
177
|
+
|
|
203
178
|
// 调试日志
|
|
204
179
|
console.log(`[skills-scanner] 执行命令: ${cmd}`);
|
|
205
|
-
|
|
206
|
-
console.log(`[skills-scanner] SCAN_SCRIPT: ${SCAN_SCRIPT}`);
|
|
207
|
-
console.log(`[skills-scanner] Python 存在: ${existsSync(VENV_PYTHON)}`);
|
|
208
|
-
console.log(`[skills-scanner] Script 存在: ${existsSync(SCAN_SCRIPT)}`);
|
|
209
|
-
|
|
180
|
+
|
|
210
181
|
try {
|
|
211
|
-
//
|
|
182
|
+
// 清除代理环境变量,避免连接问题
|
|
212
183
|
const env = { ...process.env };
|
|
213
184
|
delete env.http_proxy;
|
|
214
185
|
delete env.https_proxy;
|
|
@@ -216,10 +187,10 @@ async function runScan(
|
|
|
216
187
|
delete env.HTTPS_PROXY;
|
|
217
188
|
delete env.all_proxy;
|
|
218
189
|
delete env.ALL_PROXY;
|
|
219
|
-
|
|
220
|
-
const { stdout, stderr } = await execAsync(cmd, {
|
|
190
|
+
|
|
191
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
221
192
|
timeout: 180_000,
|
|
222
|
-
env
|
|
193
|
+
env
|
|
223
194
|
});
|
|
224
195
|
return { exitCode: 0, output: (stdout + stderr).trim() };
|
|
225
196
|
} catch (err: any) {
|
|
@@ -230,7 +201,14 @@ async function runScan(
|
|
|
230
201
|
|
|
231
202
|
// ── 日报 ──────────────────────────────────────────────────────────────────────
|
|
232
203
|
|
|
233
|
-
async function buildDailyReport(
|
|
204
|
+
async function buildDailyReport(
|
|
205
|
+
dirs: string[],
|
|
206
|
+
behavioral: boolean,
|
|
207
|
+
apiUrl: string,
|
|
208
|
+
useLLM: boolean,
|
|
209
|
+
policy: string,
|
|
210
|
+
logger: any
|
|
211
|
+
): Promise<string> {
|
|
234
212
|
const now = new Date();
|
|
235
213
|
const dateStr = now.toLocaleDateString("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
236
214
|
const timeStr = now.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
|
|
@@ -245,7 +223,14 @@ async function buildDailyReport(dirs: string[], behavioral: boolean, logger: any
|
|
|
245
223
|
const expanded = expandPath(dir);
|
|
246
224
|
if (!existsSync(expanded)) continue;
|
|
247
225
|
const tmpJson = join(STATE_DIR, `tmp-${Date.now()}.json`);
|
|
248
|
-
await runScan("batch", expanded, {
|
|
226
|
+
await runScan("batch", expanded, {
|
|
227
|
+
behavioral,
|
|
228
|
+
recursive: true,
|
|
229
|
+
jsonOut: tmpJson,
|
|
230
|
+
apiUrl,
|
|
231
|
+
useLLM,
|
|
232
|
+
policy
|
|
233
|
+
});
|
|
249
234
|
try {
|
|
250
235
|
const rows: any[] = JSON.parse(readFileSync(tmpJson, "utf-8"));
|
|
251
236
|
try { rmSync(tmpJson); } catch {}
|
|
@@ -294,6 +279,9 @@ async function handleNewSkill(
|
|
|
294
279
|
skillPath: string,
|
|
295
280
|
onUnsafe: "quarantine" | "delete" | "warn",
|
|
296
281
|
behavioral: boolean,
|
|
282
|
+
apiUrl: string,
|
|
283
|
+
useLLM: boolean,
|
|
284
|
+
policy: string,
|
|
297
285
|
notifyFn: (msg: string) => void,
|
|
298
286
|
logger: any
|
|
299
287
|
) {
|
|
@@ -302,7 +290,13 @@ async function handleNewSkill(
|
|
|
302
290
|
logger.info(`[skills-scanner] 🔍 检测到新 Skill,开始安装前扫描: ${name}`);
|
|
303
291
|
notifyFn(`🔍 检测到新 Skill \`${name}\`,正在安全扫描...`);
|
|
304
292
|
|
|
305
|
-
const res = await runScan("scan", skillPath, {
|
|
293
|
+
const res = await runScan("scan", skillPath, {
|
|
294
|
+
behavioral,
|
|
295
|
+
detailed: true,
|
|
296
|
+
apiUrl,
|
|
297
|
+
useLLM,
|
|
298
|
+
policy
|
|
299
|
+
});
|
|
306
300
|
|
|
307
301
|
if (res.exitCode === 0) {
|
|
308
302
|
notifyFn(`✅ \`${name}\` 安全检查通过,可以正常使用。`);
|
|
@@ -340,6 +334,9 @@ function startWatcher(
|
|
|
340
334
|
dirs: string[],
|
|
341
335
|
onUnsafe: "quarantine" | "delete" | "warn",
|
|
342
336
|
behavioral: boolean,
|
|
337
|
+
apiUrl: string,
|
|
338
|
+
useLLM: boolean,
|
|
339
|
+
policy: string,
|
|
343
340
|
notifyFn: (msg: string) => void,
|
|
344
341
|
logger: any
|
|
345
342
|
): () => void {
|
|
@@ -360,7 +357,7 @@ function startWatcher(
|
|
|
360
357
|
if (prev) clearTimeout(prev);
|
|
361
358
|
timers.set(skillPath, setTimeout(() => {
|
|
362
359
|
timers.delete(skillPath);
|
|
363
|
-
handleNewSkill(skillPath, onUnsafe, behavioral, notifyFn, logger);
|
|
360
|
+
handleNewSkill(skillPath, onUnsafe, behavioral, apiUrl, useLLM, policy, notifyFn, logger);
|
|
364
361
|
}, 500));
|
|
365
362
|
});
|
|
366
363
|
});
|
|
@@ -377,15 +374,19 @@ function startWatcher(
|
|
|
377
374
|
|
|
378
375
|
export default function register(api: any) {
|
|
379
376
|
const cfg: ScannerConfig = api.config?.plugins?.entries?.["skills-scanner"]?.config ?? {};
|
|
377
|
+
const apiUrl = cfg.apiUrl ?? "http://localhost:8000";
|
|
380
378
|
const scanDirs = (cfg.scanDirs?.map(expandPath) ?? []).filter(existsSync).length > 0
|
|
381
379
|
? (cfg.scanDirs!.map(expandPath))
|
|
382
380
|
: defaultScanDirs();
|
|
383
381
|
const behavioral = cfg.behavioral ?? false;
|
|
382
|
+
const useLLM = cfg.useLLM ?? false;
|
|
383
|
+
const policy = cfg.policy ?? "balanced";
|
|
384
384
|
const preInstallScan = cfg.preInstallScan ?? "on";
|
|
385
385
|
const onUnsafe = cfg.onUnsafe ?? "quarantine";
|
|
386
386
|
|
|
387
387
|
api.logger.info("[skills-scanner] ═══════════════════════════════════════");
|
|
388
388
|
api.logger.info("[skills-scanner] Plugin 正在加载...");
|
|
389
|
+
api.logger.info(`[skills-scanner] API 服务地址: ${apiUrl}`);
|
|
389
390
|
api.logger.info(`[skills-scanner] PLUGIN_ROOT: ${PLUGIN_ROOT}`);
|
|
390
391
|
api.logger.info(`[skills-scanner] SKILL_DIR: ${SKILL_DIR}`);
|
|
391
392
|
api.logger.info(`[skills-scanner] VENV_PYTHON: ${VENV_PYTHON}`);
|
|
@@ -434,7 +435,7 @@ export default function register(api: any) {
|
|
|
434
435
|
|
|
435
436
|
if (preInstallScan === "on" && scanDirs.length > 0) {
|
|
436
437
|
api.logger.info(`[skills-scanner] 📁 启动文件监控: ${scanDirs.length} 个目录`);
|
|
437
|
-
stopWatcher = startWatcher(scanDirs, onUnsafe, behavioral, persistWatcherAlert, api.logger);
|
|
438
|
+
stopWatcher = startWatcher(scanDirs, onUnsafe, behavioral, apiUrl, useLLM, policy, persistWatcherAlert, api.logger);
|
|
438
439
|
api.logger.info("[skills-scanner] ✅ 文件监控已启动");
|
|
439
440
|
} else {
|
|
440
441
|
api.logger.info("[skills-scanner] ⏭️ 安装前扫描已禁用");
|
|
@@ -493,20 +494,19 @@ export default function register(api: any) {
|
|
|
493
494
|
if (!isVenvReady()) {
|
|
494
495
|
return { text: "⏳ Python 依赖尚未就绪,请稍后重试或查看日志" };
|
|
495
496
|
}
|
|
496
|
-
|
|
497
|
-
// 测试 Python 能否导入
|
|
498
|
-
try {
|
|
499
|
-
execSync(`"${VENV_PYTHON}" -c "import skill_scanner; print('Import OK')"`, { encoding: 'utf-8' });
|
|
500
|
-
} catch (err: any) {
|
|
501
|
-
return { text: `❌ Python 导入测试失败:\n\`\`\`\n${err.message}\n\`\`\`` };
|
|
502
|
-
}
|
|
503
497
|
|
|
504
498
|
const parts = raw.split(/\s+/);
|
|
505
499
|
const skillPath = expandPath(parts.find(p => !p.startsWith("--")) ?? "");
|
|
506
500
|
const detailed = parts.includes("--detailed");
|
|
507
501
|
const useBehav = parts.includes("--behavioral") || behavioral;
|
|
508
502
|
|
|
509
|
-
const res = await runScan("scan", skillPath, {
|
|
503
|
+
const res = await runScan("scan", skillPath, {
|
|
504
|
+
detailed,
|
|
505
|
+
behavioral: useBehav,
|
|
506
|
+
apiUrl,
|
|
507
|
+
useLLM,
|
|
508
|
+
policy
|
|
509
|
+
});
|
|
510
510
|
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
511
511
|
return { text: `${icon} 扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
512
512
|
},
|
|
@@ -529,7 +529,14 @@ export default function register(api: any) {
|
|
|
529
529
|
const detailed = parts.includes("--detailed");
|
|
530
530
|
const useBehav = parts.includes("--behavioral") || behavioral;
|
|
531
531
|
|
|
532
|
-
const res = await runScan("batch", dirPath, {
|
|
532
|
+
const res = await runScan("batch", dirPath, {
|
|
533
|
+
recursive,
|
|
534
|
+
detailed,
|
|
535
|
+
behavioral: useBehav,
|
|
536
|
+
apiUrl,
|
|
537
|
+
useLLM,
|
|
538
|
+
policy
|
|
539
|
+
});
|
|
533
540
|
const icon = res.exitCode === 0 ? "✅" : "❌";
|
|
534
541
|
return { text: `${icon} 批量扫描完成\n\`\`\`\n${res.output}\n\`\`\`` };
|
|
535
542
|
},
|
|
@@ -544,7 +551,7 @@ export default function register(api: any) {
|
|
|
544
551
|
handler: async (_ctx: any) => {
|
|
545
552
|
if (!isVenvReady()) return { text: "⏳ Python 依赖尚未就绪,请稍后重试" };
|
|
546
553
|
if (scanDirs.length === 0) return { text: "⚠️ 未找到可扫描目录,请检查配置" };
|
|
547
|
-
const report = await buildDailyReport(scanDirs, behavioral, api.logger);
|
|
554
|
+
const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, api.logger);
|
|
548
555
|
return { text: report };
|
|
549
556
|
},
|
|
550
557
|
});
|
|
@@ -561,27 +568,78 @@ export default function register(api: any) {
|
|
|
561
568
|
|
|
562
569
|
const lines = [
|
|
563
570
|
"📋 *Skills Scanner 状态*",
|
|
571
|
+
`API 服务地址:${apiUrl}`,
|
|
564
572
|
`Python 依赖:${isVenvReady() ? "✅ 就绪" : "❌ 未就绪"}`,
|
|
565
573
|
`Python 路径:${VENV_PYTHON}`,
|
|
566
574
|
`scan.py 路径:${SCAN_SCRIPT}`,
|
|
567
575
|
`安装前扫描:${preInstallScan === "on" ? `✅ 监听中(${onUnsafe})` : "❌ 已禁用"}`,
|
|
576
|
+
`扫描策略:${policy}`,
|
|
577
|
+
`LLM 分析:${useLLM ? "✅ 启用" : "❌ 禁用"}`,
|
|
578
|
+
`行为分析:${behavioral ? "✅ 启用" : "❌ 禁用"}`,
|
|
568
579
|
`上次扫描:${state.lastScanAt ? new Date(state.lastScanAt).toLocaleString("zh-CN") : "从未"}`,
|
|
569
580
|
`上次问题 Skills:${state.lastUnsafeSkills?.length ? state.lastUnsafeSkills.join(", ") : "无"}`,
|
|
570
581
|
`扫描目录:\n${scanDirs.map(d => ` • ${d}`).join("\n")}`,
|
|
571
582
|
];
|
|
572
583
|
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
lines.push(`cisco-ai-skill-scanner 版本:${versionCheck.trim()}`);
|
|
577
|
-
} catch (err: any) {
|
|
578
|
-
lines.push(`❌ 依赖检查失败:${err.message}`);
|
|
579
|
-
|
|
580
|
-
// 尝试列出已安装的包
|
|
584
|
+
// API 健康检查
|
|
585
|
+
if (isVenvReady()) {
|
|
586
|
+
lines.push("", "🔍 *API 服务检查*");
|
|
581
587
|
try {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
588
|
+
// 调用 scan.py health 命令
|
|
589
|
+
const cmd = `"${VENV_PYTHON}" "${SCAN_SCRIPT}" --api-url "${apiUrl}" health`;
|
|
590
|
+
|
|
591
|
+
// 清除代理环境变量
|
|
592
|
+
const env = { ...process.env };
|
|
593
|
+
delete env.http_proxy;
|
|
594
|
+
delete env.https_proxy;
|
|
595
|
+
delete env.HTTP_PROXY;
|
|
596
|
+
delete env.HTTPS_PROXY;
|
|
597
|
+
delete env.all_proxy;
|
|
598
|
+
delete env.ALL_PROXY;
|
|
599
|
+
|
|
600
|
+
const { stdout, stderr } = await execAsync(cmd, { timeout: 5000, env });
|
|
601
|
+
const output = (stdout + stderr).trim();
|
|
602
|
+
|
|
603
|
+
if (output.includes("✓") || output.includes("正常")) {
|
|
604
|
+
lines.push(`API 服务:✅ 正常`);
|
|
605
|
+
|
|
606
|
+
// 尝试解析可用的分析器信息
|
|
607
|
+
if (output.includes("analyzers_available")) {
|
|
608
|
+
try {
|
|
609
|
+
// 从输出中提取 JSON(如果有的话)
|
|
610
|
+
const jsonMatch = output.match(/\{[\s\S]*\}/);
|
|
611
|
+
if (jsonMatch) {
|
|
612
|
+
const healthData = JSON.parse(jsonMatch[0]);
|
|
613
|
+
if (healthData.analyzers_available) {
|
|
614
|
+
lines.push(`可用分析器:${healthData.analyzers_available.join(", ")}`);
|
|
615
|
+
}
|
|
616
|
+
if (healthData.version) {
|
|
617
|
+
lines.push(`API 版本:${healthData.version}`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
} catch {}
|
|
621
|
+
}
|
|
622
|
+
} else {
|
|
623
|
+
lines.push(`API 服务:❌ 不可用`);
|
|
624
|
+
lines.push(`响应:${output}`);
|
|
625
|
+
}
|
|
626
|
+
} catch (err: any) {
|
|
627
|
+
lines.push(`API 服务:❌ 连接失败`);
|
|
628
|
+
const errorMsg = err.message || err.toString();
|
|
629
|
+
if (errorMsg.includes("ECONNREFUSED") || errorMsg.includes("无法连接")) {
|
|
630
|
+
lines.push(`错误:无法连接到 ${apiUrl}`);
|
|
631
|
+
} else {
|
|
632
|
+
lines.push(`错误:${errorMsg}`);
|
|
633
|
+
}
|
|
634
|
+
lines.push("", "💡 请确保 skill-scanner-api 服务正在运行:");
|
|
635
|
+
lines.push("```");
|
|
636
|
+
lines.push("skill-scanner-api");
|
|
637
|
+
lines.push("# 或指定端口");
|
|
638
|
+
lines.push("skill-scanner-api --port 8080");
|
|
639
|
+
lines.push("```");
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
lines.push("", "⚠️ Python 依赖未就绪,无法检查 API 服务");
|
|
585
643
|
}
|
|
586
644
|
|
|
587
645
|
if (alerts.length > 0) {
|
|
@@ -612,14 +670,21 @@ export default function register(api: any) {
|
|
|
612
670
|
const { path: p, mode = "scan", recursive = false, detailed = false } = params ?? {};
|
|
613
671
|
if (!p) return respond(false, { error: "缺少 path 参数" });
|
|
614
672
|
if (!isVenvReady()) return respond(false, { error: "Python 依赖未就绪" });
|
|
615
|
-
const res = await runScan(mode === "batch" ? "batch" : "scan", expandPath(p), {
|
|
673
|
+
const res = await runScan(mode === "batch" ? "batch" : "scan", expandPath(p), {
|
|
674
|
+
recursive,
|
|
675
|
+
detailed,
|
|
676
|
+
behavioral,
|
|
677
|
+
apiUrl,
|
|
678
|
+
useLLM,
|
|
679
|
+
policy
|
|
680
|
+
});
|
|
616
681
|
respond(res.exitCode === 0, { output: res.output, exitCode: res.exitCode, is_safe: res.exitCode === 0 });
|
|
617
682
|
});
|
|
618
683
|
|
|
619
684
|
api.registerGatewayMethod("skillsScanner.report", async ({ respond }: any) => {
|
|
620
685
|
if (!isVenvReady()) return respond(false, { error: "Python 依赖未就绪" });
|
|
621
686
|
if (scanDirs.length === 0) return respond(false, { error: "未找到可扫描目录" });
|
|
622
|
-
const report = await buildDailyReport(scanDirs, behavioral, api.logger);
|
|
687
|
+
const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, api.logger);
|
|
623
688
|
respond(true, { report, state: loadState() });
|
|
624
689
|
});
|
|
625
690
|
|
|
@@ -632,7 +697,12 @@ export default function register(api: any) {
|
|
|
632
697
|
.option("--detailed", "显示所有 findings")
|
|
633
698
|
.option("--behavioral", "启用行为分析")
|
|
634
699
|
.action(async (p: string, opts: any) => {
|
|
635
|
-
const res = await runScan("scan", expandPath(p),
|
|
700
|
+
const res = await runScan("scan", expandPath(p), {
|
|
701
|
+
...opts,
|
|
702
|
+
apiUrl,
|
|
703
|
+
useLLM,
|
|
704
|
+
policy
|
|
705
|
+
});
|
|
636
706
|
console.log(res.output);
|
|
637
707
|
process.exit(res.exitCode);
|
|
638
708
|
});
|
|
@@ -643,7 +713,12 @@ export default function register(api: any) {
|
|
|
643
713
|
.option("--detailed", "显示所有 findings")
|
|
644
714
|
.option("--behavioral", "启用行为分析")
|
|
645
715
|
.action(async (d: string, opts: any) => {
|
|
646
|
-
const res = await runScan("batch", expandPath(d),
|
|
716
|
+
const res = await runScan("batch", expandPath(d), {
|
|
717
|
+
...opts,
|
|
718
|
+
apiUrl,
|
|
719
|
+
useLLM,
|
|
720
|
+
policy
|
|
721
|
+
});
|
|
647
722
|
console.log(res.output);
|
|
648
723
|
process.exit(res.exitCode);
|
|
649
724
|
});
|
|
@@ -651,7 +726,7 @@ export default function register(api: any) {
|
|
|
651
726
|
cmd.command("report")
|
|
652
727
|
.description("立即执行全量扫描并打印日报")
|
|
653
728
|
.action(async () => {
|
|
654
|
-
const report = await buildDailyReport(scanDirs, behavioral, console);
|
|
729
|
+
const report = await buildDailyReport(scanDirs, behavioral, apiUrl, useLLM, policy, console);
|
|
655
730
|
console.log(report);
|
|
656
731
|
});
|
|
657
732
|
}, { commands: ["skills-scan"] });
|