@yottameta/yotta-vetter 0.1.5 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # 更新日志
2
2
 
3
+ ## v0.2.2 (2026-08-30)
4
+
5
+ 安全家族检测能力增强 + 措辞规范(与元信 / 元安 v0.2.2 对齐):
6
+
7
+ - **报告升级(双视角)**:新增安全健康度评分(0-100) + 威胁捕获模型视图(8 检测点逐类 verdict) + 行为项(13 项),文本 / Markdown / JSON 三格式输出。
8
+ - **规则同步**:vetter_rules 同步元安权威源至 61 条(新增 路径穿越 / MCP 工具面命令执行 / MCP 工具面任意文件读写 检测器)。
9
+ - **措辞规范**:正文不再写版本号;统一对外表述(含代码注释与报告输出)。
10
+ - 测试:12 / 12 全绿(Python 3.8 / 3.13)。
11
+
3
12
  ## v0.1.5 (2026-08-29)
4
13
 
5
14
  - 安装方式统一为四方式(对齐发布规范 §3.3.1):方式一 `npx -y @yottameta/yotta-vetter --agent <name>` / `--dir <dir>`(推荐,走 npm 源);方式二 `git clone https://github.com/YottaMeta/yotta-vetter.git`;方式三 GitHub Download ZIP;方式四 `bash install.sh --agent/--dir/--list`。移除 `npx skills` 与 `-g` 推荐;中英双 README 安装节同步。
@@ -32,7 +41,7 @@ README 按标准补全:新增「这是什么 / 核心价值 / 核心优势 /
32
41
 
33
42
  ## v0.1.0 (2026-08-26)
34
43
 
35
- YottaMeta 自有实现首版(重写自第三方技术包 skill-vetter v2.0.0,已完全重写,无上游代码):
44
+ YottaMeta 自有实现首版(零依赖自研 + 中文教学):
36
45
 
37
46
  - check:四阶段初审(来源/代码/权限/风险)+ 危险模式规则扫描 + SKILL VETTING REPORT 报告。
38
47
  - 危险模式规则与 yotta-security-audit(元安)共用(vetter_rules.py 为 audit_rules.py 的同步副本)。
@@ -41,4 +50,4 @@ YottaMeta 自有实现首版(重写自第三方技术包 skill-vetter v2.0.0
41
50
  - 输出:文本 / --json / --report report.md(SKILL VETTING REPORT 模板,含结论/决策记录/时间戳/审查者)。
42
51
  - 零依赖(Python 3.8+ 标准库),Windows + Linux 通用,UTF-8 加固(GBK 控制台不崩)。
43
52
  - exit code 语义与元安一致:0=干净/仅 low,1=medium,2=high,3=critical,4=错误。
44
- - 版权:YottaMeta 纯自有 MIT + NOTICE 品牌声明;README 一行上游致谢。
53
+ - 版权:YottaMeta 纯自有 MIT + NOTICE 品牌声明。
package/NOTICE CHANGED
@@ -8,4 +8,4 @@
8
8
  2. 不得暗示派生作品由 YottaMeta 官方维护、认可或与之存在关联;
9
9
  3. 建议在派生作品中明确声明「与 YottaMeta 官方无关联」。
10
10
 
11
- 上游来源致谢:本技能为 YottaMeta 全新实现,审查协议参考开源社区的 skill-vetter 类技能思路,无上游代码。
11
+ 来源声明:本技能由 YottaMeta 全新实现(零依赖自研 + 中文教学)。
package/README.md CHANGED
@@ -156,4 +156,4 @@ See [CHANGELOG.md](./CHANGELOG.md).
156
156
 
157
157
  ## License
158
158
 
159
- [MIT](./LICENSE) © YottaMeta. "Yuanshen" / "yotta-vetter" and the YottaMeta family names (yotta-* prefix) are YottaMeta brand identifiers; derived works must not reuse them, see [NOTICE](./NOTICE). The review protocol references open-source skill-vetter style skills; the implementation is YottaMeta's own.
159
+ [MIT](./LICENSE) © YottaMeta. "Yuanshen" / "yotta-vetter" and the YottaMeta family names (yotta-* prefix) are YottaMeta brand identifiers; derived works must not reuse them, see [NOTICE](./NOTICE). YottaMeta original implementation (zero-dependency, self-developed).
package/README.zh-CN.md CHANGED
@@ -142,4 +142,4 @@ bash install.sh --list # 列出智能体 -> 默认目录
142
142
 
143
143
  ## 许可证
144
144
 
145
- MIT © YottaMeta —— 详见 [LICENSE](./LICENSE)。品牌声明见 [NOTICE](./NOTICE)。上游来源致谢:审查协议参考开源社区 skill-vetter 类技能思路,实现为 YottaMeta 自有。
145
+ MIT © YottaMeta —— 详见 [LICENSE](./LICENSE)。品牌声明见 [NOTICE](./NOTICE)。来源声明:本技能由 YottaMeta 全新实现(零依赖自研 + 中文教学)。
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-vetter
3
- version: 0.1.5
3
+ version: 0.2.2
4
4
  description: 元审 —— 安装任何技能前的安全审查协议:四阶段 checklist(来源→代码→权限→风险)+ 轻量 checker,与元安联动深度扫描。触发:安装/评估任何技能前、从市场或 GitHub 获取技能、审查他人分享的技能、任何「要装未知代码」的场景;或用户说 审查/审查技能/vetting/技能安全审查/检查技能 等。边界:checker 只做初审与报告,结论需人工确认;绝不代替最终决策。
5
5
  license: MIT
6
6
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-vetter",
3
- "version": "0.1.5",
3
+ "version": "0.2.2",
4
4
  "description": "Yuanshen (元审) — the security review protocol before installing any skill: a four-phase checklist (source → code → permissions → risk) + a lightweight checker, with deep-scan handoff to yotta-security-audit. Triggers before installing/evaluating any skill or reviewing unknown code. Boundaries: initial review & report only; human confirmation required.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -22,7 +22,10 @@
22
22
  "scripts",
23
23
  "assets",
24
24
  "bin",
25
- "README.zh-CN.md"
25
+ "README.zh-CN.md",
26
+ "!**/__pycache__",
27
+ "!**/*.pyc",
28
+ "!scripts/test_*.py"
26
29
  ],
27
30
  "repository": {
28
31
  "type": "git",
@@ -14,6 +14,7 @@
14
14
  - [ ] SKILL.md 存在,frontmatter name 与目录名一致
15
15
  - [ ] 描述清晰、版本号有、目录结构合理
16
16
  - [ ] 危险模式扫描:下载后执行 / 动态求值 / 数据外传 / 凭据访问 / 持久化 / 提权 / 混淆
17
+ - [ ] 威胁捕获 8 检测点:供应链 / 命令执行 / 网络请求与数据外传 / 文件操作与敏感路径访问 / Prompt 注入 / 远程脚本下载执行 / 可疑编码·混淆 / 其他安全风险
17
18
  - [ ] 安装钩子:npm 生命周期脚本、安装自定义命令类
18
19
  - [ ] 隐藏字符:零宽字符、双向覆盖字符
19
20
  - [ ] 依赖声明与必要性
@@ -17,6 +17,24 @@ check --report 生成的报告遵循本结构;人工审查也可直接套用
17
17
  - 风险等级: <LOW / MEDIUM / HIGH / CRITICAL>
18
18
  - 结论: <SAFE TO INSTALL / REVIEW REQUIRED / INSTALL WITH CAUTION / DO NOT INSTALL>
19
19
  - 决策记录: <一句决策理由>
20
+ - 安全健康度评分: <0-100>
21
+
22
+ ## 威胁捕获模型视图(8 类)
23
+
24
+ | 检测点 | verdict | 命中 |
25
+ |---|---|---|
26
+ | 供应链风险 | <safe / suspicious / danger / n/a> | N |
27
+ | 命令执行风险 | <safe / suspicious / danger / n/a> | N |
28
+ | 网络请求与数据外传 | <safe / suspicious / danger / n/a> | N |
29
+ | 文件操作与敏感路径访问 | <safe / suspicious / danger / n/a> | N |
30
+ | Prompt 注入风险 | <safe / suspicious / danger / n/a> | N |
31
+ | 远程脚本下载执行 | <safe / suspicious / danger / n/a> | N |
32
+ | 可疑编码·混淆 | <safe / suspicious / danger / n/a> | N |
33
+ | 其他安全风险 | <safe / suspicious / danger / n/a> | N |
34
+
35
+ ## 行为项(13 项)
36
+
37
+ 观察到:<行为项清单 / 未观察到明显系统行为>
20
38
 
21
39
  ## 汇总
22
40
 
@@ -47,7 +47,7 @@ PATTERN_RULES = [
47
47
  # ── Obfuscation 混淆执行 ──────────────────────────────────────────────
48
48
  Rule("OBF-001", "Obfuscation", "high",
49
49
  r"\beval\s*\(\s*[^\"'\x600-9]",
50
- "eval 传入非字面量参数(可能执行外部输入)", 80),
50
+ "eval 传入非字面量参数(可能执行不可信输入)", 80),
51
51
  Rule("OBF-002", "Obfuscation", "high",
52
52
  r"(?<!\.)\bexec\s*\(\s*[^\"'\x600-9]",
53
53
  "exec 传入非字面量参数", 80),
@@ -198,6 +198,32 @@ PATTERN_RULES = [
198
198
  Rule("SOC-002", "SocialEngineering", "medium",
199
199
  r"(?i)(?:metamask|wallet|private\s+key\s+backup|助记词|钱包)",
200
200
  "加密货币钱包相关命名", 55),
201
+ # ── PathTraversal 路径穿越(2026-08-30 增强:文件操作与敏感路径访问)────
202
+ Rule("PTV-001", "PathTraversal", "high",
203
+ r"(?i)(?:open|read|write|unlink|remove|rename|shutil\.copy|Path|path\.join|os\.path\.join)\s*\([^)]*\.\.(?:/|\\)",
204
+ "文件操作路径含父目录逃逸段,可能越权访问任意路径", 85),
205
+ Rule("PTV-002", "PathTraversal", "high",
206
+ r"(?i)(?:join|resolve|abspath|realpath)\s*\([^)]*\.\.(?:/|\\)",
207
+ "路径 join/解析未归一化父目录段,存在穿越风险", 80),
208
+
209
+ # ── MCPCommandExec MCP 工具面命令执行(2026-08-30 增强:命令执行风险)──
210
+ Rule("MCE-001", "MCPCommandExec", "critical",
211
+ r"(?i)(?:child_process\.)?(?:spawn|exec|execFile|fork)\s*\(\s*(?:params|tool_params|arguments|args|argv|input|command|user_input|req\.params|query|model)",
212
+ "MCP/工具参数流入子进程执行(spawn/exec/fork)", 92),
213
+ Rule("MCE-002", "MCPCommandExec", "critical",
214
+ r"(?i)(?:spawnSync|execSync)\s*\(\s*(?:params\.get|params\[|arguments\[|req\.params|user_input)",
215
+ "MCP 工具参数对象直接流入同步子进程执行(spawnSync/execSync)", 95),
216
+ Rule("MCE-003", "MCPCommandExec", "critical",
217
+ r"(?i)(?:subprocess\.)?(?:Popen|call|run)\s*\([^)]*shell\s*=\s*True[^)]*(?:params|args|argv|input|command|user_input|cmd)",
218
+ "subprocess 以 shell=True 执行不可信输入", 92),
219
+
220
+ # ── MCPFileAccess MCP 工具面任意文件读写(2026-08-30 增强:文件操作)───
221
+ Rule("MFA-001", "MCPFileAccess", "high",
222
+ r"(?i)(?:fs\.)?(?:writeFile|writeFileSync|appendFile|createWriteStream)\s*\(\s*(?:params\.get|params\[|arguments\[|req\.params|user_input)",
223
+ "MCP 工具参数对象直接流入文件写操作(任意路径写入风险)", 88),
224
+ Rule("MFA-002", "MCPFileAccess", "high",
225
+ r"(?i)(?:fs\.)?(?:readFile|readFileSync|createReadStream)\s*\(\s*(?:params\.get|params\[|arguments\[|req\.params|user_input)",
226
+ "MCP 工具参数对象直接流入文件读操作(任意路径读取风险)", 88),
201
227
  ]
202
228
 
203
229
  # 敏感文件名(文件名级匹配,CredentialTheft 辅助)
@@ -212,6 +238,69 @@ SENSITIVE_FILENAMES = [
212
238
  (".env", "环境变量文件", "medium", 45),
213
239
  ]
214
240
 
241
+ # ══════════════════════════════════════════════════════════════════════════
242
+ # 威胁捕获模型(2026-08-30 增强:8 检测点 + 13 行为项口径)
243
+ # ══════════════════════════════════════════════════════════════════════════
244
+ # 官方 8 检测点(2026-08-30)
245
+ THREAT_TAXONOMY = {
246
+ "supply_chain": "供应链风险",
247
+ "command_execution": "命令执行风险",
248
+ "network_exfil": "网络请求与数据外传",
249
+ "file_access": "文件操作与敏感路径访问",
250
+ "prompt_injection": "Prompt 注入风险",
251
+ "remote_download": "远程脚本下载执行",
252
+ "obfuscation": "可疑编码/混淆",
253
+ "other": "其他安全风险",
254
+ }
255
+ TAXONOMY_ORDER = ("supply_chain", "command_execution", "network_exfil", "file_access",
256
+ "prompt_injection", "remote_download", "obfuscation", "other")
257
+
258
+ # detector → 官方 8 检测点
259
+ DETECTOR_TO_TAXONOMY = {
260
+ "DownloadExec": "remote_download",
261
+ "Obfuscation": "obfuscation",
262
+ "Persistence": "other",
263
+ "Exfiltration": "network_exfil",
264
+ "CredentialTheft": "file_access",
265
+ "NetworkCall": "network_exfil",
266
+ "PrivilegeEscalation": "other",
267
+ "SocialEngineering": "other",
268
+ "PromptInjection": "prompt_injection",
269
+ "PathTraversal": "file_access",
270
+ "MCPCommandExec": "command_execution",
271
+ "MCPFileAccess": "file_access",
272
+ "Structure": "other",
273
+ "Permission": "other",
274
+ "MCPToolSurface": "command_execution",
275
+ }
276
+
277
+ # 13 行为项(2026-08-30)
278
+ BEHAVIORS = (
279
+ "安装依赖包", "收集系统信息", "收集用户信息", "创建定时任务", "DNS 查询", "写入文件",
280
+ "HTTP 请求", "读取环境变量", "收集网络配置信息", "写入配置文件", "调用远端 API",
281
+ "读取文件", "修改 AI 配置",
282
+ )
283
+
284
+ # detector → 行为项(一/多个;攻击模式类如注入/混淆不映射具体行为)
285
+ DETECTOR_TO_BEHAVIORS = {
286
+ "DownloadExec": ("安装依赖包", "HTTP 请求"),
287
+ "Obfuscation": (),
288
+ "Persistence": ("创建定时任务", "写入配置文件"),
289
+ "Exfiltration": ("HTTP 请求", "调用远端 API"),
290
+ "CredentialTheft": ("读取文件", "收集用户信息"),
291
+ "NetworkCall": ("DNS 查询", "HTTP 请求", "调用远端 API"),
292
+ "PrivilegeEscalation": ("修改 AI 配置",),
293
+ "SocialEngineering": (),
294
+ "PromptInjection": (),
295
+ "PathTraversal": ("读取文件", "写入文件"),
296
+ "MCPCommandExec": (),
297
+ "MCPFileAccess": ("读取文件", "写入文件"),
298
+ "Structure": (),
299
+ "Permission": (),
300
+ "MCPToolSurface": (),
301
+ }
302
+
303
+
215
304
  # 外传敏感文件组合(EXF 规则之外的补充,按文件名 + 行内网络调用)
216
305
  # 安装钩子可疑关键字(yotta_audit.py PostInstallHookDetector 使用;本文件为签名数据,自扫豁免)
217
306
  POSTINSTALL_SUSPICIOUS = r"(?i)(curl|wget|bash|sh\s|python|node\s+-e|eval|powershell|/tmp|%temp%)"
@@ -48,7 +48,7 @@ _HERE = Path(__file__).resolve().parent
48
48
  sys.path.insert(0, str(_HERE))
49
49
  import vetter_rules # noqa: E402
50
50
 
51
- VERSION = "0.1.5"
51
+ VERSION = "0.2.2"
52
52
  TOOL_NAME = "yotta-vetter"
53
53
  MAX_FILE_SIZE = 1_000_000
54
54
  MAX_LINE_LEN = vetter_rules.MAX_LINE_LEN
@@ -198,7 +198,7 @@ def inventory_checks(root, files):
198
198
  "Inventory", "low", "permissions", str(p),
199
199
  description="非脚本文件带可执行位", confidence=40,
200
200
  rule_id="PERM-002"))
201
- # 符号链接(可能指向外部)
201
+ # 符号链接(可能指向目录外)
202
202
  for p in files:
203
203
  if p.is_symlink():
204
204
  findings.append(Finding("Inventory", "medium", "permissions", str(p),
@@ -265,6 +265,55 @@ def verdict_for(worst):
265
265
  return "REVIEW REQUIRED", "发现 medium 级风险,建议复核后安装"
266
266
  return "SAFE TO INSTALL", "未发现明显风险(仍建议按协议完整审查)"
267
267
 
268
+ # ── 威胁捕获模型视图(2026-08-30 增强:8 检测点 + 13 行为项)────────
269
+ _SEV_WEIGHT = {"critical": 40, "high": 20, "medium": 8, "low": 1, "info": 0}
270
+ _SEV_CAPS = {"critical": 2, "high": 4, "medium": 6, "low": 10, "info": 0}
271
+
272
+
273
+ def _health_score(findings):
274
+ counts = {}
275
+ for f in findings:
276
+ counts[f.severity] = counts.get(f.severity, 0) + 1
277
+ score = 100
278
+ for sev, w in _SEV_WEIGHT.items():
279
+ score -= w * min(counts.get(sev, 0), _SEV_CAPS[sev])
280
+ return max(0, int(round(score)))
281
+
282
+
283
+ def _taxonomy_view(findings):
284
+ hits = {}
285
+ for f in findings:
286
+ key = vetter_rules.DETECTOR_TO_TAXONOMY.get(f.detector, "other")
287
+ hits.setdefault(key, []).append(f)
288
+ out = []
289
+ for key in vetter_rules.TAXONOMY_ORDER:
290
+ items = hits.get(key, [])
291
+ name = vetter_rules.THREAT_TAXONOMY.get(key, key)
292
+ sev = "info"
293
+ for f in items:
294
+ if _SEV_WEIGHT.get(f.severity, 0) > _SEV_WEIGHT.get(sev, 0):
295
+ sev = f.severity
296
+ if not items:
297
+ verdict = "n/a"
298
+ elif sev in ("critical", "high"):
299
+ verdict = "danger"
300
+ elif sev == "medium":
301
+ verdict = "suspicious"
302
+ else:
303
+ verdict = "safe"
304
+ out.append({"name": name, "verdict": verdict, "count": len(items)})
305
+ return out
306
+
307
+
308
+ def _behavior_view(findings):
309
+ observed = {}
310
+ for f in findings:
311
+ for b in vetter_rules.DETECTOR_TO_BEHAVIORS.get(f.detector, ()):
312
+ observed[b] = observed.get(b, 0) + 1
313
+ return [{"behavior": b, "observed": observed.get(b, 0)}
314
+ for b in vetter_rules.BEHAVIORS]
315
+
316
+
268
317
  # ── 输出(文本 / JSON / 报告)──────────────────────────────────────────────
269
318
 
270
319
  def fmt_report(findings, scope):
@@ -289,6 +338,15 @@ def fmt_report(findings, scope):
289
338
  lines.append("风险等级: %s" % _worst(findings).upper())
290
339
  lines.append("结论: %s" % verdict)
291
340
  lines.append("决策记录: %s" % note)
341
+ lines.append("安全健康度评分: %d/100" % _health_score(findings))
342
+ lines.append("")
343
+ lines.append("威胁捕获模型(8 类):")
344
+ for v in _taxonomy_view(findings):
345
+ lines.append(" %-16s %-11s %d" % (v["name"], v["verdict"], v["count"]))
346
+ lines.append("")
347
+ observed = [b["behavior"] for b in _behavior_view(findings) if b["observed"]]
348
+ lines.append("行为项(13 项):%s" % (
349
+ "、".join(observed) if observed else "未观察到明显系统行为"))
292
350
  lines.append("")
293
351
  if findings:
294
352
  lines.append("发现:")
@@ -333,6 +391,20 @@ def write_report_md(path, findings, scope):
333
391
  for sev in _SEVERITY_ORDER:
334
392
  lines.append("| %s | %d |" % (sev.upper(), counts[sev]))
335
393
  lines.append("")
394
+ lines.append("**安全健康度评分:%d/100**" % _health_score(findings))
395
+ lines.append("")
396
+ lines.append("## 威胁捕获模型视图(8 类)")
397
+ lines.append("")
398
+ lines.append("| 检测点 | verdict | 命中 |")
399
+ lines.append("|---|---|---|")
400
+ for v in _taxonomy_view(findings):
401
+ lines.append("| %s | %s | %d |" % (v["name"], v["verdict"], v["count"]))
402
+ lines.append("")
403
+ lines.append("## 行为项(13 项)")
404
+ lines.append("")
405
+ observed = [b["behavior"] for b in _behavior_view(findings) if b["observed"]]
406
+ lines.append("观察到:%s" % ("、".join(observed) if observed else "未观察到明显系统行为"))
407
+ lines.append("")
336
408
  if findings:
337
409
  lines.append("## 发现")
338
410
  lines.append("")
@@ -388,6 +460,11 @@ def cmd_check(args):
388
460
  "tool": TOOL_NAME, "version": VERSION, "scope": scope,
389
461
  "summary": _summary(findings),
390
462
  "findings": [f.to_dict() for f in findings],
463
+ "threat": {
464
+ "health_score": _health_score(findings),
465
+ "taxonomy": _taxonomy_view(findings),
466
+ "behaviors": _behavior_view(findings),
467
+ },
391
468
  }, indent=2, ensure_ascii=False))
392
469
  else:
393
470
  print(fmt_report(findings, scope))
@@ -1,111 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """yotta-vetter(元审)测试套件。
3
-
4
- 用法:
5
- python3 scripts/test_yotta_vetter.py
6
- 覆盖:check 干净/恶意/自扫、四阶段清单、JSON 与报告、严重级过滤、
7
- V3 元安联动、source 来源检查(格式/404 降级)、GBK 控制台加固。
8
- """
9
- import json
10
- import os
11
- import subprocess
12
- import sys
13
- import tempfile
14
- import unittest
15
- from pathlib import Path
16
-
17
- HERE = Path(__file__).resolve().parent
18
- SCRIPT = HERE / "yotta_vetter.py"
19
- FIX = HERE.parent.parent.parent / ".tmp" / "audit-fixtures"
20
-
21
-
22
- def run_cli(args, env=None):
23
- full = dict(os.environ)
24
- if env:
25
- full.update(env)
26
- return subprocess.run(
27
- [sys.executable, str(SCRIPT)] + args,
28
- capture_output=True, text=True, encoding="utf-8", errors="replace",
29
- env=full, timeout=60,
30
- )
31
-
32
-
33
- class VetterCliTest(unittest.TestCase):
34
- def test_check_clean(self):
35
- r = run_cli(["check", str(FIX / "clean-skill"), "--no-color"])
36
- self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
37
- self.assertIn("SAFE TO INSTALL", r.stdout)
38
-
39
- def test_check_evil_verdict_and_linkage(self):
40
- r = run_cli(["check", str(FIX / "evil-skill"), "--no-color"])
41
- self.assertEqual(r.returncode, 3, r.stdout + r.stderr)
42
- self.assertIn("DO NOT INSTALL", r.stdout)
43
- self.assertIn("yotta-security-audit --target skill", r.stderr) # V3 联动
44
-
45
- def test_check_self_no_high_critical(self):
46
- """自扫:无 high/critical(urllib 来源检查为预期 medium,规则表已豁免)。"""
47
- r = run_cli(["check", str(SCRIPT.parent), "--no-color"])
48
- self.assertIn(r.returncode, (0, 1), r.stdout + r.stderr)
49
- self.assertNotIn("[HIGH]", r.stdout)
50
- self.assertNotIn("[CRITICAL]", r.stdout)
51
-
52
- def test_check_missing_path_exit4(self):
53
- r = run_cli(["check", "C:/definitely/not/exists"])
54
- self.assertEqual(r.returncode, 4, r.stdout + r.stderr)
55
-
56
- def test_check_json(self):
57
- r = run_cli(["check", str(FIX / "evil-skill"), "--json"])
58
- self.assertEqual(r.returncode, 3, r.stderr)
59
- data = json.loads(r.stdout)
60
- self.assertEqual(data["tool"], "yotta-vetter")
61
- self.assertGreaterEqual(data["summary"]["critical"], 1)
62
- self.assertTrue(all("rule_id" in f for f in data["findings"]))
63
-
64
- def test_check_report_md(self):
65
- with tempfile.TemporaryDirectory() as td:
66
- rep = Path(td) / "vetting-report.md"
67
- r = run_cli(["check", str(FIX / "evil-skill"), "--report", str(rep)])
68
- self.assertEqual(r.returncode, 3, r.stderr)
69
- self.assertTrue(rep.exists())
70
- txt = rep.read_text(encoding="utf-8")
71
- self.assertIn("SKILL VETTING REPORT", txt)
72
- self.assertIn("决策记录", txt)
73
-
74
- def test_check_severity_filter(self):
75
- r = run_cli(["check", str(FIX / "evil-skill"),
76
- "--severity", "critical", "--no-color"])
77
- self.assertEqual(r.returncode, 3, r.stderr)
78
- self.assertNotIn("[MEDIUM]", r.stdout)
79
-
80
- def test_inventory_missing_skill_md(self):
81
- with tempfile.TemporaryDirectory() as td:
82
- Path(td, "README.md").write_text("no skill", encoding="utf-8")
83
- r = run_cli(["check", td, "--no-color"])
84
- self.assertIn("缺少 SKILL.md", r.stdout)
85
-
86
- def test_source_bad_format_exit4(self):
87
- r = run_cli(["source", "not-a-slash-spec"])
88
- self.assertEqual(r.returncode, 4, r.stdout + r.stderr)
89
-
90
- def test_source_missing_repo_exit1(self):
91
- r = run_cli(["source", "github:YottaMeta/__no_such_repo_xyz__"])
92
- self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
93
-
94
- def test_bad_command_exit4(self):
95
- r = run_cli(["frobnicate"])
96
- self.assertEqual(r.returncode, 4, r.stdout + r.stderr)
97
-
98
- def test_gbk_console_no_crash(self):
99
- env = dict(os.environ)
100
- env["PYTHONIOENCODING"] = "gbk"
101
- r = subprocess.run(
102
- [sys.executable, str(SCRIPT), "check", str(FIX / "clean-skill"), "--no-color"],
103
- capture_output=True, env=env, timeout=60)
104
- self.assertEqual(r.returncode, 0, r.stderr.decode("gbk", errors="replace"))
105
- self.assertNotIn(b"UnicodeEncodeError", r.stderr)
106
-
107
-
108
- if __name__ == "__main__":
109
- suite = unittest.defaultTestLoader.loadTestsFromModule(sys.modules[__name__])
110
- ok = unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()
111
- sys.exit(0 if ok else 1)