@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.

@@ -1,11 +1,11 @@
1
1
  # /// script
2
2
  # dependencies = [
3
- # "cisco-ai-skill-scanner>=0.1.0",
3
+ # "requests>=2.31.0",
4
4
  # ]
5
5
  # ///
6
6
  """
7
- OpenClaw Skills Security Scanner
8
- 基于 cisco-ai-skill-scanner,支持单个/批量扫描
7
+ OpenClaw Skills Security Scanner (HTTP Client)
8
+ 通过 HTTP API 调用远程 cisco-ai-skill-scanner 服务
9
9
 
10
10
  注意:此脚本必须使用 venv 中的 Python 运行
11
11
  """
@@ -14,36 +14,27 @@ import sys
14
14
  import os
15
15
  import json
16
16
  import argparse
17
+ import tempfile
18
+ import zipfile
19
+ import time
17
20
  from pathlib import Path
21
+ from typing import Optional, Dict, Any, List
18
22
 
19
23
  # ── 依赖检查 ──────────────────────────────────────────────────────────────────
20
24
  try:
21
- from skill_scanner import SkillScanner
22
- from skill_scanner.core.analyzers import (
23
- StaticAnalyzer,
24
- BehavioralAnalyzer,
25
- PipelineAnalyzer,
26
- )
25
+ import requests
27
26
  except ImportError as e:
28
- print("❌ cisco-ai-skill-scanner 未安装。")
27
+ print("❌ requests 未安装。")
29
28
  print(f" 导入错误: {e}")
30
- print(" 请运行: uv pip install cisco-ai-skill-scanner")
31
- print(f" Python 路径: {sys.executable}")
32
- print(f" Python 版本: {sys.version}")
33
-
34
- # 尝试显示 sys.path
35
- print(" sys.path:")
36
- for p in sys.path:
37
- print(f" - {p}")
38
-
39
- sys.exit(1)
40
- except Exception as e:
41
- print(f"❌ 导入时发生未知错误: {type(e).__name__}: {e}")
42
- import traceback
43
- traceback.print_exc()
29
+ print(" 请运行: uv pip install requests")
44
30
  sys.exit(1)
45
31
 
46
32
 
33
+ # ── 配置 ──────────────────────────────────────────────────────────────────────
34
+ DEFAULT_API_URL = os.getenv("SKILL_SCANNER_API_URL", "http://localhost:8000")
35
+ REQUEST_TIMEOUT = 180 # 3 分钟超时
36
+
37
+
47
38
  # ── 颜色输出 ──────────────────────────────────────────────────────────────────
48
39
  USE_COLOR = sys.stdout.isatty()
49
40
 
@@ -58,231 +49,461 @@ BOLD = lambda t: c(t, "1")
58
49
  DIM = lambda t: c(t, "2")
59
50
 
60
51
 
61
- # ── 严重级别 ──────────────────────────────────────────────────────────────────
62
- SEVERITY_COLORS = {
63
- "CRITICAL": RED,
64
- "HIGH": RED,
65
- "MEDIUM": YELLOW,
66
- "LOW": GREEN,
67
- "INFO": CYAN,
68
- }
69
-
70
- def severity_label(sev: str) -> str:
71
- sev = (sev or "INFO").upper()
72
- color = SEVERITY_COLORS.get(sev, CYAN)
73
- return color(f"[{sev}]")
74
-
75
-
76
- # ── 构建 Scanner ──────────────────────────────────────────────────────────────
77
- def build_scanner(use_behavioral: bool = False, use_llm: bool = False) -> SkillScanner:
78
- analyzers = [StaticAnalyzer(), PipelineAnalyzer()]
79
- if use_behavioral:
80
- analyzers.append(BehavioralAnalyzer())
81
- if use_llm:
52
+ # ── HTTP 客户端 ───────────────────────────────────────────────────────────────
53
+ class SkillScannerClient:
54
+ """cisco-ai-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
+ """健康检查,返回详细信息"""
82
62
  try:
83
- from skill_scanner.core.analyzers import LLMAnalyzer
84
- api_key = os.environ.get("SKILL_SCANNER_LLM_API_KEY")
85
- if not api_key:
86
- print(YELLOW("⚠️ 未设置 SKILL_SCANNER_LLM_API_KEY,跳过 LLM 分析器"))
87
- else:
88
- analyzers.append(LLMAnalyzer())
89
- except ImportError:
90
- print(YELLOW("⚠️ LLM 分析器不可用,请安装: pip install cisco-ai-skill-scanner[llm]"))
91
- return SkillScanner(analyzers=analyzers)
92
-
93
-
94
- # ── 单个 Skill 扫描 ───────────────────────────────────────────────────────────
95
- def scan_single(path: str, args) -> dict:
96
- skill_path = Path(path).expanduser().resolve()
97
-
98
- if not skill_path.exists():
99
- return {"path": str(skill_path), "error": "路径不存在", "is_safe": False}
100
-
101
- if not skill_path.is_dir():
102
- return {"path": str(skill_path), "error": "必须是目录(Skill 文件夹)", "is_safe": False}
103
-
104
- skill_md = skill_path / "SKILL.md"
105
- if not skill_md.exists():
106
- return {"path": str(skill_path), "error": "未找到 SKILL.md,不是合法的 Skill", "is_safe": False}
107
-
108
- print(f"\n{BOLD('🔍 扫描:')} {CYAN(str(skill_path))}")
109
-
110
- scanner = build_scanner(
111
- use_behavioral=getattr(args, "behavioral", False),
112
- use_llm=getattr(args, "llm", False),
113
- )
114
-
115
- result = scanner.scan_skill(str(skill_path))
116
- findings = result.findings if hasattr(result, "findings") else []
117
- max_sev = str(result.max_severity) if hasattr(result, "max_severity") else "UNKNOWN"
118
- is_safe = bool(result.is_safe) if hasattr(result, "is_safe") else len(findings) == 0
119
-
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
+ use_virustotal: bool = False,
92
+ use_aidefense: bool = False,
93
+ use_trigger: bool = False,
94
+ enable_meta: bool = False,
95
+ vt_api_key: Optional[str] = None,
96
+ aidefense_api_key: Optional[str] = None
97
+ ) -> Dict[str, Any]:
98
+ """上传 ZIP 文件扫描(单个 Skill)
99
+
100
+ 对应 API: POST /scan-upload
101
+ - 上传 ZIP 文件
102
+ - 服务器解压并查找 SKILL.md
103
+ - 返回扫描结果
104
+ """
105
+ # 创建临时 ZIP 文件
106
+ with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_zip:
107
+ zip_path = tmp_zip.name
108
+
109
+ try:
110
+ # 打包 Skill 目录
111
+ self._create_zip(skill_path, zip_path)
112
+
113
+ # 准备请求
114
+ with open(zip_path, 'rb') as f:
115
+ files = {'file': (os.path.basename(skill_path) + '.zip', f, 'application/zip')}
116
+ data = {
117
+ 'policy': policy,
118
+ 'use_llm': str(use_llm).lower(),
119
+ 'use_behavioral': str(use_behavioral).lower(),
120
+ 'use_virustotal': str(use_virustotal).lower(),
121
+ 'use_aidefense': str(use_aidefense).lower(),
122
+ 'use_trigger': str(use_trigger).lower(),
123
+ 'enable_meta': str(enable_meta).lower()
124
+ }
125
+
126
+ # 添加认证头
127
+ headers = {}
128
+ if vt_api_key:
129
+ headers['X-VirusTotal-Key'] = vt_api_key
130
+ if aidefense_api_key:
131
+ headers['X-AIDefense-Key'] = aidefense_api_key
132
+
133
+ response = self.session.post(
134
+ f"{self.base_url}/scan-upload",
135
+ files=files,
136
+ data=data,
137
+ headers=headers,
138
+ timeout=REQUEST_TIMEOUT
139
+ )
140
+ response.raise_for_status()
141
+ return response.json()
142
+ finally:
143
+ # 清理临时文件
144
+ if os.path.exists(zip_path):
145
+ os.unlink(zip_path)
146
+
147
+ def scan_batch_upload(
148
+ self,
149
+ skill_paths: List[str],
150
+ policy: str = "balanced",
151
+ use_llm: bool = False,
152
+ use_behavioral: bool = False,
153
+ max_concurrent: int = 3
154
+ ) -> List[Dict[str, Any]]:
155
+ """批量上传多个 Skill(客户端循环上传)
156
+
157
+ 注意:API 不支持批量上传,这里是客户端实现
158
+ """
159
+ results = []
160
+
161
+ for i, skill_path in enumerate(skill_paths, 1):
162
+ print(f"[{i}/{len(skill_paths)}] 正在扫描: {skill_path}")
163
+
164
+ try:
165
+ result = self.scan_upload(
166
+ skill_path,
167
+ policy=policy,
168
+ use_llm=use_llm,
169
+ use_behavioral=use_behavioral
170
+ )
171
+ results.append({
172
+ 'path': skill_path,
173
+ 'success': True,
174
+ 'result': result
175
+ })
176
+ status = "✓" if result.get('is_safe', False) else "✗"
177
+ print(f" {status} {result.get('skill_name', 'Unknown')}: {result.get('findings_count', 0)} 个发现")
178
+ except Exception as e:
179
+ results.append({
180
+ 'path': skill_path,
181
+ 'success': False,
182
+ 'error': str(e)
183
+ })
184
+ print(f" ✗ 失败: {e}")
185
+
186
+ return results
187
+
188
+ def scan_batch(
189
+ self,
190
+ skills_dir: str,
191
+ recursive: bool = False,
192
+ check_overlap: bool = False,
193
+ policy: str = "balanced",
194
+ use_llm: bool = False,
195
+ use_behavioral: bool = False
196
+ ) -> str:
197
+ """批量异步扫描(服务器本地目录),返回 scan_id
198
+
199
+ 对应 API: POST /scan-batch
200
+ - 扫描服务器本地目录中的多个技能
201
+ - 异步执行,返回 scan_id
202
+ - 需要轮询 GET /scan-batch/{scan_id} 获取结果
203
+ """
204
+ data = {
205
+ 'skills_directory': skills_dir,
206
+ 'recursive': recursive,
207
+ 'check_overlap': check_overlap,
208
+ 'policy': policy,
209
+ 'use_llm': use_llm,
210
+ 'use_behavioral': use_behavioral
211
+ }
212
+
213
+ response = self.session.post(
214
+ f"{self.base_url}/scan-batch",
215
+ json=data,
216
+ timeout=30
217
+ )
218
+ response.raise_for_status()
219
+ return response.json()['scan_id']
220
+
221
+ def get_batch_result(self, scan_id: str) -> Dict[str, Any]:
222
+ """获取批量扫描结果"""
223
+ response = self.session.get(
224
+ f"{self.base_url}/scan-batch/{scan_id}",
225
+ timeout=10
226
+ )
227
+ response.raise_for_status()
228
+ return response.json()
229
+
230
+ def wait_for_batch(self, scan_id: str, poll_interval: int = 5) -> Dict[str, Any]:
231
+ """等待批量扫描完成"""
232
+ while True:
233
+ result = self.get_batch_result(scan_id)
234
+ status = result.get('status')
235
+
236
+ if status == 'completed':
237
+ return result['result']
238
+ elif status == 'failed':
239
+ raise Exception(f"批量扫描失败: {result.get('error')}")
240
+
241
+ time.sleep(poll_interval)
242
+
243
+ @staticmethod
244
+ def _create_zip(source_dir: str, zip_path: str):
245
+ """创建 ZIP 文件"""
246
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
247
+ source_path = Path(source_dir)
248
+ for file_path in source_path.rglob('*'):
249
+ if file_path.is_file():
250
+ arcname = file_path.relative_to(source_path.parent)
251
+ zipf.write(file_path, arcname)
252
+
253
+
254
+ # ── 格式化输出 ────────────────────────────────────────────────────────────────
255
+ def format_scan_result(result: Dict[str, Any], detailed: bool = False) -> str:
256
+ """格式化扫描结果"""
257
+ lines = []
258
+
259
+ # 基本信息
260
+ skill_name = result.get('skill_name', 'Unknown')
261
+ is_safe = result.get('is_safe', False)
262
+ max_severity = result.get('max_severity', 'NONE')
263
+ findings_count = result.get('findings_count', 0)
264
+
120
265
  # 状态行
121
- if is_safe:
122
- status = GREEN(" 安全")
123
- else:
124
- status = RED(" 发现问题") if max_sev in ("CRITICAL", "HIGH") else YELLOW("⚠️ 需关注")
125
-
126
- print(f" 状态: {status} | 最高严重级别: {severity_label(max_sev)} | 发现: {len(findings)} 条")
127
-
128
- # 详细 findings
129
- if findings and getattr(args, "detailed", False):
130
- print(f"\n {BOLD('发现详情:')}")
131
- for i, f in enumerate(findings, 1):
132
- sev = str(getattr(f, "severity", "INFO")).upper()
133
- name = getattr(f, "rule_id", getattr(f, "name", "unknown"))
134
- desc = getattr(f, "description", getattr(f, "message", ""))
135
- loc = getattr(f, "location", getattr(f, "file", ""))
136
- print(f" {DIM(str(i)+'.')} {severity_label(sev)} {BOLD(name)}")
137
- if desc:
138
- print(f" {desc}")
139
- if loc:
140
- print(f" {DIM('位置: ' + str(loc))}")
141
- elif findings and not getattr(args, "detailed", False):
142
- # 非 detailed 模式只显示 HIGH/CRITICAL
143
- serious = [f for f in findings if str(getattr(f, "severity", "")).upper() in ("CRITICAL", "HIGH")]
144
- if serious:
145
- print(f"\n {BOLD('HIGH/CRITICAL 问题:')}")
146
- for f in serious:
147
- sev = str(getattr(f, "severity", "HIGH")).upper()
148
- name = getattr(f, "rule_id", getattr(f, "name", "unknown"))
149
- desc = getattr(f, "description", getattr(f, "message", ""))
150
- print(f" {severity_label(sev)} {BOLD(name)}: {desc}")
151
-
152
- return {
153
- "path": str(skill_path),
154
- "name": skill_path.name,
155
- "is_safe": is_safe,
156
- "max_severity": max_sev,
157
- "findings": len(findings),
158
- }
159
-
160
-
161
- # ── 批量扫描 ──────────────────────────────────────────────────────────────────
162
- def scan_batch(directory: str, args) -> list[dict]:
163
- base = Path(directory).expanduser().resolve()
164
-
165
- if not base.exists() or not base.is_dir():
166
- print(RED(f"❌ 目录不存在: {base}"))
167
- return []
266
+ status_icon = GREEN("✓") if is_safe else RED("✗")
267
+ lines.append(f"{status_icon} {BOLD(skill_name)}")
268
+ lines.append(f" 严重性: {_severity_color(max_severity)}")
269
+ lines.append(f" 发现数: {findings_count}")
270
+
271
+ # 详细发现
272
+ if detailed and findings_count > 0:
273
+ findings = result.get('findings', [])
274
+ lines.append("")
275
+ lines.append(BOLD("发现详情:"))
276
+ for i, finding in enumerate(findings[:10], 1): # 最多显示 10 条
277
+ severity = finding.get('severity', 'UNKNOWN')
278
+ category = finding.get('category', 'Unknown')
279
+ description = finding.get('description', 'No description')
280
+ lines.append(f" {i}. [{_severity_color(severity)}] {category}")
281
+ lines.append(f" {description}")
282
+
283
+ if len(findings) > 10:
284
+ lines.append(f" ... 还有 {len(findings) - 10} 条发现")
285
+
286
+ return "\n".join(lines)
168
287
 
169
- # 查找所有含 SKILL.md 的子目录
170
- skills: list[Path] = []
171
288
 
172
- if getattr(args, "recursive", False):
173
- for skill_md in sorted(base.rglob("SKILL.md")):
174
- skills.append(skill_md.parent)
289
+ def format_batch_result(result: Dict[str, Any]) -> str:
290
+ """格式化批量扫描结果"""
291
+ lines = []
292
+
293
+ total = result.get('total_skills_scanned', 0)
294
+ safe = result.get('safe_skills', 0)
295
+ unsafe = result.get('unsafe_skills', 0)
296
+
297
+ lines.append(BOLD("批量扫描结果"))
298
+ lines.append(f" 总计: {total} 个 Skills")
299
+ lines.append(f" 安全: {GREEN(str(safe))}")
300
+ lines.append(f" 问题: {RED(str(unsafe))}")
301
+
302
+ # 问题 Skills 列表
303
+ if unsafe > 0:
304
+ skills = result.get('skills', [])
305
+ unsafe_skills = [s for s in skills if not s.get('is_safe', True)]
306
+ lines.append("")
307
+ lines.append(BOLD("问题 Skills:"))
308
+ for skill in unsafe_skills[:10]:
309
+ name = skill.get('skill_name', 'Unknown')
310
+ severity = skill.get('max_severity', 'UNKNOWN')
311
+ count = skill.get('findings_count', 0)
312
+ lines.append(f" • {name} [{_severity_color(severity)}] - {count} 条发现")
313
+
314
+ return "\n".join(lines)
315
+
316
+
317
+ def _severity_color(severity: str) -> str:
318
+ """严重性着色"""
319
+ severity_upper = severity.upper()
320
+ if severity_upper in ('CRITICAL', 'HIGH'):
321
+ return RED(severity_upper)
322
+ elif severity_upper == 'MEDIUM':
323
+ return YELLOW(severity_upper)
324
+ elif severity_upper == 'LOW':
325
+ return CYAN(severity_upper)
175
326
  else:
176
- for entry in sorted(base.iterdir()):
177
- if entry.is_dir() and (entry / "SKILL.md").exists():
178
- skills.append(entry)
179
-
180
- if not skills:
181
- print(YELLOW(f"⚠️ 在 {base} 中未找到任何 Skill(含 SKILL.md 的目录)"))
182
- return []
183
-
184
- print(f"\n{BOLD('📂 批量扫描目录:')} {CYAN(str(base))}")
185
- print(f" 发现 {BOLD(str(len(skills)))} 个 Skill\n")
186
- print("─" * 60)
187
-
188
- results = []
189
- safe_count = 0
190
- unsafe_count = 0
191
- error_count = 0
192
-
193
- for skill_path in skills:
194
- r = scan_single(str(skill_path), args)
195
- results.append(r)
196
- if r.get("error"):
197
- error_count += 1
198
- elif r.get("is_safe"):
199
- safe_count += 1
200
- else:
201
- unsafe_count += 1
202
-
203
- # 汇总
204
- print("\n" + "═" * 60)
205
- print(BOLD("📊 批量扫描汇总"))
206
- print("═" * 60)
207
- print(f" 总计: {len(results)} 个 Skill")
208
- print(f" {GREEN('✅ 安全:')} {safe_count} 个")
209
- print(f" {RED('❌ 问题:')} {unsafe_count} 个")
210
- if error_count:
211
- print(f" {YELLOW('⚠️ 错误:')} {error_count} 个")
327
+ return DIM(severity_upper)
212
328
 
213
- # 问题 Skill 列表
214
- unsafe = [r for r in results if not r.get("is_safe") and not r.get("error")]
215
- if unsafe:
216
- print(f"\n {BOLD(RED('需要关注的 Skills:'))}")
217
- for r in unsafe:
218
- sev = r.get("max_severity", "UNKNOWN")
219
- print(f" {severity_label(sev)} {r['name']} ({r.get('findings', 0)} 条发现)")
220
- print(f" {DIM(r['path'])}")
221
329
 
222
- return results
223
-
224
-
225
- # ── JSON 输出 ─────────────────────────────────────────────────────────────────
226
- def save_json(data, output_path: str):
227
- with open(output_path, "w", encoding="utf-8") as f:
228
- json.dump(data, f, ensure_ascii=False, indent=2)
229
- print(f"\n{GREEN('💾 结果已保存:')} {output_path}")
230
-
231
-
232
- # ── CLI 入口 ──────────────────────────────────────────────────────────────────
330
+ # ── 命令行接口 ────────────────────────────────────────────────────────────────
233
331
  def main():
234
332
  parser = argparse.ArgumentParser(
235
- prog="scan.py",
236
- description="OpenClaw Skills 安全扫描器(基于 cisco-ai-skill-scanner)",
237
- formatter_class=argparse.RawDescriptionHelpFormatter,
238
- epilog="""
239
- 示例:
240
- 单个扫描:
241
- python scan.py scan ~/.openclaw/skills/my-skill
242
- python scan.py scan ./my-skill --detailed --behavioral
243
-
244
- 批量扫描:
245
- python scan.py batch ~/.openclaw/skills
246
- python scan.py batch ~/.openclaw/skills --recursive --json results.json
247
- python scan.py batch ./skills --detailed --llm
248
- """,
333
+ description="OpenClaw Skills Security Scanner (HTTP Client)",
334
+ formatter_class=argparse.RawDescriptionHelpFormatter
249
335
  )
250
-
251
- sub = parser.add_subparsers(dest="command", required=True)
252
-
253
- # -- scan 子命令
254
- p_scan = sub.add_parser("scan", help="扫描单个 Skill 目录")
255
- p_scan.add_argument("path", help="Skill 目录路径(含 SKILL.md 的文件夹)")
256
- p_scan.add_argument("--detailed", action="store_true", help="显示所有 findings 详情")
257
- p_scan.add_argument("--behavioral", action="store_true", help="启用行为分析器(AST dataflow)")
258
- p_scan.add_argument("--llm", action="store_true", help="启用 LLM 语义分析器(需要 API Key)")
259
- p_scan.add_argument("--json", metavar="FILE", help="将结果保存为 JSON 文件")
260
-
261
- # -- batch 子命令
262
- p_batch = sub.add_parser("batch", help="批量扫描目录下所有 Skills")
263
- p_batch.add_argument("directory", help="包含多个 Skill 的目录")
264
- p_batch.add_argument("--recursive", action="store_true", help="递归扫描子目录")
265
- p_batch.add_argument("--detailed", action="store_true", help="显示所有 findings 详情")
266
- p_batch.add_argument("--behavioral", action="store_true", help="启用行为分析器")
267
- p_batch.add_argument("--llm", action="store_true", help="启用 LLM 语义分析器")
268
- p_batch.add_argument("--json", metavar="FILE", help="将结果保存为 JSON 文件")
269
-
336
+
337
+ parser.add_argument(
338
+ '--api-url',
339
+ default=DEFAULT_API_URL,
340
+ help=f'API 服务地址 (默认: {DEFAULT_API_URL})'
341
+ )
342
+
343
+ subparsers = parser.add_subparsers(dest='command', help='命令')
344
+
345
+ # scan 命令
346
+ scan_parser = subparsers.add_parser('scan', help='扫描单个 Skill(上传 ZIP)')
347
+ scan_parser.add_argument('path', help='Skill 目录路径')
348
+ scan_parser.add_argument('--detailed', action='store_true', help='显示详细发现')
349
+ scan_parser.add_argument('--behavioral', action='store_true', help='启用行为分析')
350
+ scan_parser.add_argument('--llm', action='store_true', help='启用 LLM 分析')
351
+ scan_parser.add_argument('--virustotal', action='store_true', help='启用 VirusTotal')
352
+ scan_parser.add_argument('--aidefense', action='store_true', help='启用 AI Defense')
353
+ scan_parser.add_argument('--trigger', action='store_true', help='启用触发器分析')
354
+ scan_parser.add_argument('--meta', action='store_true', help='启用元分析(误报过滤)')
355
+ scan_parser.add_argument('--policy', default='balanced', help='扫描策略')
356
+ scan_parser.add_argument('--json', metavar='FILE', help='输出 JSON 到文件')
357
+
358
+ # batch 命令(服务器本地路径批量扫描)
359
+ batch_parser = subparsers.add_parser('batch', help='批量扫描(服务器本地目录)')
360
+ batch_parser.add_argument('path', help='Skills 目录路径(服务器本地)')
361
+ batch_parser.add_argument('--recursive', action='store_true', help='递归扫描')
362
+ batch_parser.add_argument('--check-overlap', action='store_true', help='检查技能描述重叠')
363
+ batch_parser.add_argument('--policy', default='balanced', help='扫描策略')
364
+ batch_parser.add_argument('--json', metavar='FILE', help='输出 JSON 到文件')
365
+
366
+ # batch-upload 命令(客户端批量上传)
367
+ batch_upload_parser = subparsers.add_parser('batch-upload', help='批量上传多个 Skills(客户端循环)')
368
+ batch_upload_parser.add_argument('paths', nargs='+', help='多个 Skill 目录路径')
369
+ batch_upload_parser.add_argument('--behavioral', action='store_true', help='启用行为分析')
370
+ batch_upload_parser.add_argument('--llm', action='store_true', help='启用 LLM 分析')
371
+ batch_upload_parser.add_argument('--policy', default='balanced', help='扫描策略')
372
+ batch_upload_parser.add_argument('--json', metavar='FILE', help='输出 JSON 到文件')
373
+
374
+ # health 命令
375
+ subparsers.add_parser('health', help='健康检查')
376
+
270
377
  args = parser.parse_args()
271
-
272
- if args.command == "scan":
273
- result = scan_single(args.path, args)
274
- if args.json:
275
- save_json(result, args.json)
276
- # 退出码:不安全返回 1
277
- sys.exit(0 if result.get("is_safe") else 1)
278
-
279
- elif args.command == "batch":
280
- results = scan_batch(args.directory, args)
281
- if args.json:
282
- save_json(results, args.json)
283
- unsafe = [r for r in results if not r.get("is_safe")]
284
- sys.exit(0 if not unsafe else 1)
285
-
286
-
287
- if __name__ == "__main__":
378
+
379
+ if not args.command:
380
+ parser.print_help()
381
+ sys.exit(1)
382
+
383
+ # 创建客户端
384
+ client = SkillScannerClient(args.api_url)
385
+
386
+ try:
387
+ if args.command == 'health':
388
+ health_result = client.health_check()
389
+
390
+ if health_result['status'] == 'healthy':
391
+ print(GREEN("✓") + " API 服务正常")
392
+
393
+ # 显示详细信息
394
+ data = health_result.get('data', {})
395
+ if data:
396
+ print(f" 版本: {data.get('version', 'Unknown')}")
397
+ analyzers = data.get('analyzers_available', [])
398
+ if analyzers:
399
+ print(f" 可用分析器: {', '.join(analyzers)}")
400
+
401
+ # 输出 JSON 供程序解析
402
+ print(json.dumps(data))
403
+
404
+ sys.exit(0)
405
+ else:
406
+ print(RED("✗") + f" API 服务不可用: {args.api_url}")
407
+ error = health_result.get('error', '未知错误')
408
+ print(f" 错误: {error}")
409
+ sys.exit(1)
410
+
411
+ elif args.command == 'scan':
412
+ print(f"正在扫描: {args.path}")
413
+ result = client.scan_upload(
414
+ args.path,
415
+ policy=args.policy,
416
+ use_llm=args.llm,
417
+ use_behavioral=args.behavioral,
418
+ use_virustotal=args.virustotal if hasattr(args, 'virustotal') else False,
419
+ use_aidefense=args.aidefense if hasattr(args, 'aidefense') else False,
420
+ use_trigger=args.trigger if hasattr(args, 'trigger') else False,
421
+ enable_meta=args.meta if hasattr(args, 'meta') else False
422
+ )
423
+
424
+ # 输出结果
425
+ if args.json:
426
+ with open(args.json, 'w') as f:
427
+ json.dump(result, f, indent=2)
428
+ print(f"结果已保存到: {args.json}")
429
+ else:
430
+ print(format_scan_result(result, args.detailed))
431
+
432
+ # 退出码
433
+ sys.exit(0 if result.get('is_safe', False) else 1)
434
+
435
+ elif args.command == 'batch':
436
+ print(f"正在批量扫描: {args.path}")
437
+ scan_id = client.scan_batch(
438
+ args.path,
439
+ recursive=args.recursive,
440
+ check_overlap=args.check_overlap if hasattr(args, 'check_overlap') else False,
441
+ policy=args.policy
442
+ )
443
+ print(f"扫描 ID: {scan_id}")
444
+ print("等待扫描完成...")
445
+
446
+ result = client.wait_for_batch(scan_id)
447
+
448
+ # 输出结果
449
+ if args.json:
450
+ with open(args.json, 'w') as f:
451
+ json.dump(result, f, indent=2)
452
+ print(f"结果已保存到: {args.json}")
453
+ else:
454
+ print(format_batch_result(result))
455
+
456
+ # 退出码
457
+ unsafe = result.get('unsafe_skills', 0)
458
+ sys.exit(0 if unsafe == 0 else 1)
459
+
460
+ elif args.command == 'batch-upload':
461
+ print(f"正在批量上传 {len(args.paths)} 个 Skills...")
462
+ results = client.scan_batch_upload(
463
+ args.paths,
464
+ policy=args.policy,
465
+ use_llm=args.llm,
466
+ use_behavioral=args.behavioral
467
+ )
468
+
469
+ # 统计结果
470
+ total = len(results)
471
+ success = sum(1 for r in results if r['success'])
472
+ failed = total - success
473
+
474
+ # 输出结果
475
+ if args.json:
476
+ with open(args.json, 'w') as f:
477
+ json.dump(results, f, indent=2)
478
+ print(f"\n结果已保存到: {args.json}")
479
+
480
+ print(f"\n批量上传完成: {success}/{total} 成功, {failed} 失败")
481
+
482
+ # 退出码
483
+ sys.exit(0 if failed == 0 else 1)
484
+
485
+ except requests.exceptions.ConnectionError:
486
+ print(RED("✗") + f" 无法连接到 API 服务: {args.api_url}")
487
+ print("请确保 skill-scanner-api 服务正在运行")
488
+ sys.exit(1)
489
+ except requests.exceptions.Timeout:
490
+ print(RED("✗") + " 请求超时")
491
+ sys.exit(1)
492
+ except requests.exceptions.HTTPError as e:
493
+ print(RED("✗") + f" HTTP 错误: {e}")
494
+ if e.response is not None:
495
+ try:
496
+ error_detail = e.response.json()
497
+ print(f"详情: {error_detail}")
498
+ except:
499
+ print(f"响应: {e.response.text}")
500
+ sys.exit(1)
501
+ except Exception as e:
502
+ print(RED("✗") + f" 错误: {e}")
503
+ import traceback
504
+ traceback.print_exc()
505
+ sys.exit(1)
506
+
507
+
508
+ if __name__ == '__main__':
288
509
  main()