@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/src/types.ts CHANGED
@@ -1,50 +1,72 @@
1
- /**
2
- * Skills Scanner 类型定义
3
- */
4
-
5
- export interface ScannerConfig {
6
- apiUrl?: string;
7
- scanDirs?: string[];
8
- behavioral?: boolean;
9
- useLLM?: boolean;
10
- policy?: "strict" | "balanced" | "permissive";
11
- preInstallScan?: "on" | "off";
12
- onUnsafe?: "quarantine" | "delete" | "warn";
13
- injectSecurityGuidance?: boolean;
14
- enablePromptInjectionGuard?: boolean;
15
- enableHighRiskOperationGuard?: boolean;
16
- }
17
-
18
- export interface ScanState {
19
- lastScanAt?: string;
20
- lastUnsafeSkills?: string[];
21
- configReviewed?: boolean;
22
- cronJobId?: string;
23
- pendingAlerts?: string[];
24
- }
25
-
26
- export interface ScanOptions {
27
- detailed?: boolean;
28
- behavioral?: boolean;
29
- recursive?: boolean;
30
- jsonOut?: string;
31
- apiUrl?: string;
32
- useLLM?: boolean;
33
- policy?: string;
34
- }
35
-
36
- export interface ScanResult {
37
- exitCode: number;
38
- output: string;
39
- }
40
-
41
- export interface ScanRecord {
42
- name?: string;
43
- path?: string;
44
- is_safe?: boolean;
45
- error?: string;
46
- max_severity?: string;
47
- findings?: number;
48
- }
49
-
50
- export type OnUnsafeAction = "quarantine" | "delete" | "warn";
1
+ /**
2
+ * Skills Scanner 类型定义
3
+ */
4
+
5
+ export interface ScannerConfig {
6
+ apiUrl?: string;
7
+ behavioral?: boolean;
8
+ useLLM?: boolean;
9
+ policy?: "strict" | "balanced" | "permissive";
10
+ onUnsafe?: "quarantine" | "delete" | "warn";
11
+ injectSecurityGuidance?: boolean;
12
+ enableBeforeInstallHook?: boolean;
13
+ scanTimeoutMs?: number; // Scan timeout in milliseconds (default: 180000)
14
+ reportDir?: string; // Custom report directory
15
+ quarantineDir?: string; // Custom quarantine directory
16
+ }
17
+
18
+ export interface ScanState {
19
+ version?: number; // State schema version
20
+ lastScanAt?: string;
21
+ lastShutdownAt?: string;
22
+ lastUninstallAt?: string; // Track when plugin was uninstalled
23
+ lastUnsafeSkills?: string[];
24
+ configReviewed?: boolean;
25
+ cronJobId?: string;
26
+ pendingAlerts?: string[];
27
+ }
28
+
29
+ export interface ScanOptions {
30
+ detailed?: boolean;
31
+ behavioral?: boolean;
32
+ recursive?: boolean;
33
+ jsonOut?: string;
34
+ apiUrl?: string;
35
+ useLLM?: boolean;
36
+ policy?: string;
37
+ }
38
+
39
+ export interface ScanResult {
40
+ exitCode: number;
41
+ output: string;
42
+ data?: ScanResultData;
43
+ }
44
+
45
+ export interface ScanResultData {
46
+ is_safe?: boolean;
47
+ max_severity?: string;
48
+ findings?: number;
49
+ error?: string;
50
+ }
51
+
52
+ export interface ScanRecord {
53
+ name?: string;
54
+ path?: string;
55
+ is_safe?: boolean;
56
+ error?: string;
57
+ max_severity?: string;
58
+ findings?: number;
59
+ }
60
+
61
+ export type OnUnsafeAction = "quarantine" | "delete" | "warn";
62
+
63
+ export interface CommandResponse {
64
+ text: string;
65
+ }
66
+
67
+ export interface PluginLogger {
68
+ debug(message: string, data?: any): void;
69
+ info(message: string, data?: any): void;
70
+ warn(message: string, data?: any): void;
71
+ error(message: string, data?: any): void;
72
+ }
@@ -1,446 +0,0 @@
1
- # /// script
2
- # dependencies = [
3
- # "requests>=2.31.0",
4
- # ]
5
- # ///
6
- """
7
- OpenClaw Skills 安全扫描器 (HTTP 客户端)
8
- 通过 HTTP API 调用远程 skill-scanner-api 服务
9
-
10
- 注意:此脚本使用系统 Python 运行,需确保已安装 requests 依赖
11
- """
12
-
13
- import sys
14
- import os
15
- import json
16
- import argparse
17
- import tempfile
18
- import zipfile
19
- import time
20
- from pathlib import Path
21
- from typing import Optional, Dict, Any, List
22
-
23
- # 依赖检查
24
- try:
25
- import requests
26
- except ImportError as e:
27
- print("❌ requests 未安装。")
28
- print(f" 导入错误: {e}")
29
- print(" 请运行: pip install requests")
30
- sys.exit(1)
31
-
32
-
33
- # 配置
34
- DEFAULT_API_URL = "http://10.110.3.133"
35
- REQUEST_TIMEOUT = 180 # 3 分钟
36
-
37
-
38
- # 颜色输出
39
- USE_COLOR = sys.stdout.isatty()
40
-
41
- def c(text, code):
42
- return f"\033[{code}m{text}\033[0m" if USE_COLOR else text
43
-
44
- RED = lambda t: c(t, "31")
45
- YELLOW = lambda t: c(t, "33")
46
- GREEN = lambda t: c(t, "32")
47
- CYAN = lambda t: c(t, "36")
48
- BOLD = lambda t: c(t, "1")
49
- DIM = lambda t: c(t, "2")
50
-
51
-
52
- # HTTP 客户端
53
- class SkillScannerClient:
54
- """skill-scanner HTTP API 客户端"""
55
-
56
- def __init__(self, base_url: str = DEFAULT_API_URL):
57
- self.base_url = base_url.rstrip('/')
58
- self.session = requests.Session()
59
-
60
- def health_check(self) -> Dict[str, Any]:
61
- """健康检查,返回详细信息"""
62
- try:
63
- response = self.session.get(f"{self.base_url}/health", timeout=5)
64
- response.raise_for_status()
65
- return {
66
- 'status': 'healthy',
67
- 'data': response.json()
68
- }
69
- except requests.exceptions.ConnectionError:
70
- return {
71
- 'status': 'unreachable',
72
- 'error': f'无法连接到 {self.base_url}'
73
- }
74
- except requests.exceptions.Timeout:
75
- return {
76
- 'status': 'timeout',
77
- 'error': '请求超时'
78
- }
79
- except Exception as e:
80
- return {
81
- 'status': 'error',
82
- 'error': str(e)
83
- }
84
-
85
- def scan_upload(
86
- self,
87
- skill_path: str,
88
- policy: str = "balanced",
89
- use_llm: bool = False,
90
- use_behavioral: bool = False
91
- ) -> Dict[str, Any]:
92
- """上传 ZIP 文件扫描(单个 Skill)
93
-
94
- API: POST /scan-upload
95
- - 上传 ZIP 文件
96
- - 服务器解压并查找 SKILL.md
97
- - 返回扫描结果
98
- """
99
- with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_zip:
100
- zip_path = tmp_zip.name
101
-
102
- try:
103
- self._create_zip(skill_path, zip_path)
104
-
105
- with open(zip_path, 'rb') as f:
106
- files = {'file': (os.path.basename(skill_path) + '.zip', f, 'application/zip')}
107
- data = {
108
- 'policy': policy,
109
- 'use_llm': str(use_llm).lower(),
110
- 'use_behavioral': str(use_behavioral).lower(),
111
- # Enable in-depth safety checks by default
112
- 'use_zip_virus': 'true',
113
- 'enable_meta': 'true',
114
- }
115
-
116
- response = self.session.post(
117
- f"{self.base_url}/scan-upload",
118
- files=files,
119
- data=data,
120
- timeout=REQUEST_TIMEOUT
121
- )
122
- response.raise_for_status()
123
- return response.json()
124
- finally:
125
- if os.path.exists(zip_path):
126
- os.unlink(zip_path)
127
-
128
- def scan_batch_upload(
129
- self,
130
- skill_paths: List[str],
131
- policy: str = "balanced",
132
- use_llm: bool = False,
133
- use_behavioral: bool = False
134
- ) -> List[Dict[str, Any]]:
135
- """批量上传多个 Skill(客户端循环)"""
136
- results = []
137
-
138
- for i, skill_path in enumerate(skill_paths, 1):
139
- print(f"[{i}/{len(skill_paths)}] 正在扫描: {skill_path}")
140
-
141
- try:
142
- result = self.scan_upload(
143
- skill_path,
144
- policy=policy,
145
- use_llm=use_llm,
146
- use_behavioral=use_behavioral
147
- )
148
- results.append({
149
- 'path': skill_path,
150
- 'success': True,
151
- 'result': result
152
- })
153
- status = "✓" if result.get('is_safe', False) else "✗"
154
- print(f" {status} {result.get('skill_name', 'Unknown')}: {result.get('findings_count', 0)} 个发现")
155
- except Exception as e:
156
- results.append({
157
- 'path': skill_path,
158
- 'success': False,
159
- 'error': str(e)
160
- })
161
- print(f" ✗ 失败: {e}")
162
-
163
- return results
164
-
165
- def scan_clawhub(
166
- self,
167
- clawhub_url: str,
168
- policy: str = "balanced",
169
- use_llm: bool = False,
170
- use_behavioral: bool = True
171
- ) -> Dict[str, Any]:
172
- """扫描 ClawHub 上的 Skill
173
-
174
- API: POST /scan-clawhub
175
- - 提供 ClawHub URL
176
- - 服务器自动下载并扫描
177
- - 返回扫描结果
178
-
179
- Args:
180
- clawhub_url: ClawHub 项目 URL (例如: https://clawhub.ai/username/project)
181
- policy: 扫描策略
182
- use_llm: 是否启用 LLM 分析
183
- use_behavioral: 是否启用行为分析
184
- """
185
- data = {
186
- 'clawhub_url': clawhub_url,
187
- 'policy': policy,
188
- 'use_llm': use_llm,
189
- 'use_behavioral': use_behavioral,
190
- 'llm_provider': 'anthropic',
191
- 'use_virustotal': False,
192
- 'use_aidefense': False,
193
- 'use_trigger': False,
194
- 'use_zip_virus': True,
195
- 'enable_meta': True
196
- }
197
-
198
- response = self.session.post(
199
- f"{self.base_url}/scan-clawhub",
200
- json=data,
201
- timeout=REQUEST_TIMEOUT
202
- )
203
- response.raise_for_status()
204
- return response.json()
205
-
206
- @staticmethod
207
- def _create_zip(source_dir: str, zip_path: str):
208
- """创建 ZIP 文件"""
209
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
210
- source_path = Path(source_dir)
211
- for file_path in source_path.rglob('*'):
212
- if file_path.is_file():
213
- arcname = file_path.relative_to(source_path.parent)
214
- zipf.write(file_path, arcname)
215
-
216
-
217
- # 格式化输出
218
- def format_scan_result(result: Dict[str, Any], detailed: bool = False) -> str:
219
- """格式化扫描结果"""
220
- lines = []
221
-
222
- skill_name = result.get('skill_name', 'Unknown')
223
- is_safe = result.get('is_safe', False)
224
- max_severity = result.get('max_severity', 'NONE')
225
- findings_count = result.get('findings_count', 0)
226
-
227
- status_icon = GREEN("✓") if is_safe else RED("✗")
228
- lines.append(f"{status_icon} {BOLD(skill_name)}")
229
- lines.append(f" 严重性: {_severity_color(max_severity)}")
230
- lines.append(f" 发现数: {findings_count}")
231
-
232
- if detailed and findings_count > 0:
233
- findings = result.get('findings', [])
234
- lines.append("")
235
- lines.append(BOLD("发现详情:"))
236
- for i, finding in enumerate(findings[:10], 1):
237
- severity = finding.get('severity', 'UNKNOWN')
238
- category = finding.get('category', 'Unknown')
239
- description = finding.get('description', 'No description')
240
- lines.append(f" {i}. [{_severity_color(severity)}] {category}")
241
- lines.append(f" {description}")
242
-
243
- if len(findings) > 10:
244
- lines.append(f" ... 还有 {len(findings) - 10} 条发现")
245
-
246
- return "\n".join(lines)
247
-
248
-
249
- def format_batch_result(result: Dict[str, Any]) -> str:
250
- """格式化批量扫描结果"""
251
- lines = []
252
-
253
- total = result.get('total_skills_scanned', 0)
254
- safe = result.get('safe_skills', 0)
255
- unsafe = result.get('unsafe_skills', 0)
256
-
257
- lines.append(BOLD("批量扫描结果"))
258
- lines.append(f" 总计: {total} 个 Skills")
259
- lines.append(f" 安全: {GREEN(str(safe))}")
260
- lines.append(f" 问题: {RED(str(unsafe))}")
261
-
262
- if unsafe > 0:
263
- skills = result.get('skills', [])
264
- unsafe_skills = [s for s in skills if not s.get('is_safe', True)]
265
- lines.append("")
266
- lines.append(BOLD("问题 Skills:"))
267
- for skill in unsafe_skills[:10]:
268
- name = skill.get('skill_name', 'Unknown')
269
- severity = skill.get('max_severity', 'UNKNOWN')
270
- count = skill.get('findings_count', 0)
271
- lines.append(f" • {name} [{_severity_color(severity)}] - {count} 条发现")
272
-
273
- return "\n".join(lines)
274
-
275
-
276
- def _severity_color(severity: str) -> str:
277
- """严重性着色"""
278
- severity_upper = severity.upper()
279
- if severity_upper in ('CRITICAL', 'HIGH'):
280
- return RED(severity_upper)
281
- elif severity_upper == 'MEDIUM':
282
- return YELLOW(severity_upper)
283
- elif severity_upper == 'LOW':
284
- return CYAN(severity_upper)
285
- else:
286
- return DIM(severity_upper)
287
-
288
-
289
- # 命令行接口
290
- def main():
291
- parser = argparse.ArgumentParser(
292
- description="OpenClaw Skills 安全扫描器 (HTTP 客户端)",
293
- formatter_class=argparse.RawDescriptionHelpFormatter
294
- )
295
-
296
- parser.add_argument(
297
- '--api-url',
298
- default=DEFAULT_API_URL,
299
- help=f'API 服务地址 (默认: {DEFAULT_API_URL})'
300
- )
301
-
302
- subparsers = parser.add_subparsers(dest='command', help='命令')
303
-
304
- # scan 命令
305
- scan_parser = subparsers.add_parser('scan', help='扫描单个 Skill(上传 ZIP)')
306
- scan_parser.add_argument('path', help='Skill 目录路径')
307
- scan_parser.add_argument('--detailed', action='store_true', help='显示详细发现')
308
- scan_parser.add_argument('--behavioral', action='store_true', help='启用行为分析')
309
- scan_parser.add_argument('--llm', action='store_true', help='启用 LLM 分析')
310
- scan_parser.add_argument('--policy', default='balanced', choices=['strict', 'balanced', 'permissive'], help='扫描策略')
311
- scan_parser.add_argument('--json', metavar='FILE', help='输出 JSON 到文件')
312
-
313
- # batch 命令(客户端批量上传)
314
- batch_parser = subparsers.add_parser('batch', help='批量扫描多个 Skills(客户端循环)')
315
- batch_parser.add_argument('paths', nargs='+', help='多个 Skill 目录路径')
316
- batch_parser.add_argument('--behavioral', action='store_true', help='启用行为分析')
317
- batch_parser.add_argument('--llm', action='store_true', help='启用 LLM 分析')
318
- batch_parser.add_argument('--policy', default='balanced', choices=['strict', 'balanced', 'permissive'], help='扫描策略')
319
- batch_parser.add_argument('--json', metavar='FILE', help='输出 JSON 到文件')
320
-
321
- # clawhub 命令
322
- clawhub_parser = subparsers.add_parser('clawhub', help='扫描 ClawHub 上的 Skill')
323
- clawhub_parser.add_argument('url', help='ClawHub 项目 URL (例如: https://clawhub.ai/username/project)')
324
- clawhub_parser.add_argument('--detailed', action='store_true', help='显示详细发现')
325
- clawhub_parser.add_argument('--behavioral', dest='behavioral', action='store_true', help='启用行为分析')
326
- clawhub_parser.add_argument('--no-behavioral', dest='behavioral', action='store_false', help='关闭行为分析')
327
- clawhub_parser.set_defaults(behavioral=True)
328
- clawhub_parser.add_argument('--llm', action='store_true', help='启用 LLM 分析')
329
- clawhub_parser.add_argument('--policy', default='balanced', choices=['strict', 'balanced', 'permissive'], help='扫描策略')
330
- clawhub_parser.add_argument('--json', metavar='FILE', help='输出 JSON 到文件')
331
-
332
- # health 命令
333
- subparsers.add_parser('health', help='健康检查')
334
-
335
- args = parser.parse_args()
336
-
337
- if not args.command:
338
- parser.print_help()
339
- sys.exit(1)
340
-
341
- client = SkillScannerClient(args.api_url)
342
-
343
- try:
344
- if args.command == 'health':
345
- health_result = client.health_check()
346
-
347
- if health_result['status'] == 'healthy':
348
- print(GREEN("✓") + " API 服务正常")
349
-
350
- data = health_result.get('data', {})
351
- if data:
352
- print(f" 版本: {data.get('version', 'Unknown')}")
353
- analyzers = data.get('analyzers_available', [])
354
- if analyzers:
355
- print(f" 可用分析器: {', '.join(analyzers)}")
356
- print(json.dumps(data))
357
-
358
- sys.exit(0)
359
- else:
360
- print(RED("✗") + f" API 服务不可用: {args.api_url}")
361
- error = health_result.get('error', '未知错误')
362
- print(f" 错误: {error}")
363
- sys.exit(1)
364
-
365
- elif args.command == 'scan':
366
- print(f"正在扫描: {args.path}")
367
- result = client.scan_upload(
368
- args.path,
369
- policy=args.policy,
370
- use_llm=args.llm,
371
- use_behavioral=args.behavioral
372
- )
373
-
374
- if args.json:
375
- with open(args.json, 'w') as f:
376
- json.dump(result, f, indent=2)
377
- print(f"结果已保存到: {args.json}")
378
- else:
379
- print(format_scan_result(result, args.detailed))
380
-
381
- sys.exit(0 if result.get('is_safe', False) else 1)
382
-
383
- elif args.command == 'batch':
384
- print(f"正在批量扫描 {len(args.paths)} 个 Skills...")
385
- results = client.scan_batch_upload(
386
- args.paths,
387
- policy=args.policy,
388
- use_llm=args.llm,
389
- use_behavioral=args.behavioral
390
- )
391
-
392
- total = len(results)
393
- success = sum(1 for r in results if r['success'])
394
- failed = total - success
395
-
396
- if args.json:
397
- with open(args.json, 'w') as f:
398
- json.dump(results, f, indent=2)
399
- print(f"\n结果已保存到: {args.json}")
400
-
401
- print(f"\n批量扫描完成: {success}/{total} 成功, {failed} 失败")
402
- sys.exit(0 if failed == 0 else 1)
403
-
404
- elif args.command == 'clawhub':
405
- print(f"正在扫描 ClawHub Skill: {args.url}")
406
- result = client.scan_clawhub(
407
- args.url,
408
- policy=args.policy,
409
- use_llm=args.llm,
410
- use_behavioral=args.behavioral
411
- )
412
-
413
- if args.json:
414
- with open(args.json, 'w') as f:
415
- json.dump(result, f, indent=2)
416
- print(f"结果已保存到: {args.json}")
417
- else:
418
- print(format_scan_result(result, args.detailed))
419
-
420
- sys.exit(0 if result.get('is_safe', False) else 1)
421
-
422
- except requests.exceptions.ConnectionError:
423
- print(RED("✗") + f" 无法连接到 API 服务: {args.api_url}")
424
- print("请确保 skill-scanner-api 服务正在运行")
425
- sys.exit(1)
426
- except requests.exceptions.Timeout:
427
- print(RED("✗") + " 请求超时")
428
- sys.exit(1)
429
- except requests.exceptions.HTTPError as e:
430
- print(RED("✗") + f" HTTP 错误: {e}")
431
- if e.response is not None:
432
- try:
433
- error_detail = e.response.json()
434
- print(f"详情: {error_detail}")
435
- except:
436
- print(f"响应: {e.response.text}")
437
- sys.exit(1)
438
- except Exception as e:
439
- print(RED("✗") + f" 错误: {e}")
440
- import traceback
441
- traceback.print_exc()
442
- sys.exit(1)
443
-
444
-
445
- if __name__ == '__main__':
446
- main()