@yottameta/yotta-vetter 0.1.4 → 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 +18 -2
- package/NOTICE +1 -1
- package/README.md +29 -40
- package/README.zh-CN.md +29 -40
- package/SKILL.md +1 -1
- package/package.json +5 -2
- package/references/checklist.md +1 -0
- package/references/vetting-report-template.md +18 -0
- package/scripts/vetter_rules.py +90 -1
- package/scripts/yotta_vetter.py +80 -3
- package/scripts/__pycache__/vetter_rules.cpython-38.pyc +0 -0
- package/scripts/test_yotta_vetter.py +0 -111
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
12
|
+
## v0.1.5 (2026-08-29)
|
|
13
|
+
|
|
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 安装节同步。
|
|
15
|
+
- 版本对齐:package.json / SKILL.md / CHANGELOG / 引擎 VERSION / 测试断言 / README 锚点 = 0.1.5。
|
|
16
|
+
- 维护修复:签名数据文件跳过列表加入 hardening_rules.py。
|
|
17
|
+
- 无功能变更(仅文档与版本同步)。
|
|
18
|
+
|
|
3
19
|
## v0.1.4 (2026-08-29)
|
|
4
20
|
|
|
5
21
|
维护性修复(签名数据豁免):
|
|
@@ -25,7 +41,7 @@ README 按标准补全:新增「这是什么 / 核心价值 / 核心优势 /
|
|
|
25
41
|
|
|
26
42
|
## v0.1.0 (2026-08-26)
|
|
27
43
|
|
|
28
|
-
YottaMeta
|
|
44
|
+
YottaMeta 自有实现首版(零依赖自研 + 中文教学):
|
|
29
45
|
|
|
30
46
|
- check:四阶段初审(来源/代码/权限/风险)+ 危险模式规则扫描 + SKILL VETTING REPORT 报告。
|
|
31
47
|
- 危险模式规则与 yotta-security-audit(元安)共用(vetter_rules.py 为 audit_rules.py 的同步副本)。
|
|
@@ -34,4 +50,4 @@ YottaMeta 自有实现首版(重写自第三方技术包 skill-vetter v2.0.0
|
|
|
34
50
|
- 输出:文本 / --json / --report report.md(SKILL VETTING REPORT 模板,含结论/决策记录/时间戳/审查者)。
|
|
35
51
|
- 零依赖(Python 3.8+ 标准库),Windows + Linux 通用,UTF-8 加固(GBK 控制台不崩)。
|
|
36
52
|
- exit code 语义与元安一致:0=干净/仅 low,1=medium,2=high,3=critical,4=错误。
|
|
37
|
-
- 版权:YottaMeta 纯自有 MIT + NOTICE
|
|
53
|
+
- 版权:YottaMeta 纯自有 MIT + NOTICE 品牌声明。
|
package/NOTICE
CHANGED
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| **Semi-automated source** | source checks stars / last-updated / license with a local cache; degrades gracefully offline |
|
|
42
42
|
| **Deep-scan handoff** | high-and-above automatically guides you into yotta-security-audit |
|
|
43
43
|
| **Zero dependency** | Python 3.8+ standard library; no daemon / database; Windows + Linux |
|
|
44
|
-
| **Ecosystem distribution** | GitHub + npm synced; install
|
|
44
|
+
| **Ecosystem distribution** | GitHub + npm synced; four install methods (npx / git clone / Download ZIP / install.sh) |
|
|
45
45
|
|
|
46
46
|
## Commands / four-phase review protocol
|
|
47
47
|
|
|
@@ -87,53 +87,42 @@ python3 scripts/yotta_vetter.py source github:YottaMeta/yotta-memory
|
|
|
87
87
|
|
|
88
88
|
## Install
|
|
89
89
|
|
|
90
|
-
Pick any
|
|
90
|
+
Pick any of the four methods below; the order is the recommended priority. Skill files always come from **npm** (GitHub can be slow without a proxy; npm supports mirrors).
|
|
91
91
|
|
|
92
|
-
### Method 1: npm
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
npx -y @yottameta/yotta-vetter --
|
|
92
|
+
### Method 1: npm one-liner (recommended)
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
# Optional China mirror: npm config set registry https://registry.npmmirror.com
|
|
96
|
+
npx -y @yottameta/yotta-vetter --agent <agent-name> # install to the agent's default user-level skills dir
|
|
97
|
+
npx -y @yottameta/yotta-vetter --dir <your-skills-dir> # point to the skills dir itself (e.g. ~/.codex/skills)
|
|
97
98
|
```
|
|
98
|
-
> Not in the preset list? Use --dir to point at the agent's skills directory, or manual copy (method 3). --list shows each agent's default directory. You can also npm pack @yottameta/yotta-vetter and unpack it to install via method 2 / 3.
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
- `--agent <name>` installs to that agent's default user-level directory; `--list` shows each agent's default directory.
|
|
101
|
+
- `--dir <path>` installs to the given directory; for agents not in the preset list, point `--dir` at their skills directory.
|
|
102
|
+
- If the mirror has not synced the new package (404): add `--registry=https://registry.npmjs.org/` (a proxy may be needed in China), or wait for the mirror cache.
|
|
103
|
+
|
|
104
|
+
### Method 2: git clone (developers / git available)
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
git clone https://github.com/YottaMeta/yotta-vetter.git <your-skills-dir>/yotta-vetter
|
|
106
108
|
```
|
|
107
|
-
> Covers 17 agent families including Trae / Qwen / Comate / CodeBuddy / Kimi. Windows users: works with Git Bash; otherwise use method 3.
|
|
108
109
|
|
|
109
|
-
### Method 3: manual
|
|
110
|
-
Copy the whole `yotta-vetter` folder into the target agent's skills directory. Common locations (user level; Windows uses %USERPROFILE%, Linux/macOS uses ~):
|
|
110
|
+
### Method 3: GitHub Download ZIP (manual / no git)
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
| Amp | %USERPROFILE%\.config\agents\skills\yotta-vetter\ | .agents\skills\ |
|
|
122
|
-
| Kiro | %USERPROFILE%\.kiro\skills\yotta-vetter\ | .kiro\skills\ |
|
|
123
|
-
| WorkBuddy | %USERPROFILE%\.workbuddy\skills\yotta-vetter\ | .workbuddy\skills\ |
|
|
124
|
-
| Trae Code CLI | %USERPROFILE%\.traecli\skills\yotta-vetter\ | .traecli\skills\ |
|
|
125
|
-
| Trae IDE (CN) | %USERPROFILE%\.trae-cn\skills\yotta-vetter\ | .trae\skills\ |
|
|
126
|
-
| Qwen Code | %USERPROFILE%\.qwen\skills\yotta-vetter\ | .qwen\skills\ |
|
|
127
|
-
| Comate | %USERPROFILE%\.comate\skills\yotta-vetter\ | .comate\skills\ |
|
|
128
|
-
| CodeBuddy | %USERPROFILE%\.codebuddy\skills\yotta-vetter\ | .codebuddy\skills\ |
|
|
129
|
-
| Kimi | %USERPROFILE%\.kimi\skills\yotta-vetter\ | .kimi\skills\ |
|
|
130
|
-
| Generic AGENTS.md | %USERPROFILE%\.agents\skills\yotta-vetter\ | .agents\skills\ |
|
|
131
|
-
|
|
132
|
-
> If Codex's CODEX_HOME is set, it overrides the default; the same applies to opencode's XDG_CONFIG_HOME. .agents\skills is not a universal directory — only OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot etc. read it; **Claude Code and Codex do not read it by default**. When unsure, use --dir or let the agent install it.
|
|
112
|
+
On the GitHub repository `YottaMeta/yotta-vetter`, click **Code → Download ZIP**, unzip it and put the `yotta-vetter` folder into the agent's skills directory.
|
|
113
|
+
|
|
114
|
+
### Method 4: install.sh (multi-agent one-liner script)
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
bash install.sh --agent <name> # install to the agent's default user-level directory
|
|
118
|
+
bash install.sh --dir <path> # install to the given directory
|
|
119
|
+
bash install.sh --list # list agents -> default directories
|
|
120
|
+
```
|
|
133
121
|
|
|
122
|
+
> Method 1 uses the npm registry (npmmirror / npmjs) and does not depend on GitHub; Methods 2/3 use GitHub and may fail without a proxy in China.
|
|
134
123
|
## Upgrade / uninstall
|
|
135
124
|
|
|
136
|
-
- **Upgrade**: reinstall the latest version to overwrite — npx -y @yottameta/yotta-vetter
|
|
125
|
+
- **Upgrade**: reinstall the latest version to overwrite — rerun the install command you used (e.g. `npx -y @yottameta/yotta-vetter --agent <name>` or `bash install.sh --agent <name>`). Old files in the skill directory are replaced; other project files are untouched.
|
|
137
126
|
- **Uninstall**: delete the yotta-vetter folder under the target agent's skills directory (see the table above). The skill stops taking effect after removal.
|
|
138
127
|
|
|
139
128
|
## FAQ
|
|
@@ -167,4 +156,4 @@ See [CHANGELOG.md](./CHANGELOG.md).
|
|
|
167
156
|
|
|
168
157
|
## License
|
|
169
158
|
|
|
170
|
-
[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).
|
|
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
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| **来源半自动** | source 命令查 stars/更新时间/许可证,本地缓存、无网络自动降级 |
|
|
42
42
|
| **联动深度扫描** | high 及以上自动引导跑 yotta-security-audit |
|
|
43
43
|
| **零依赖** | Python 3.8+ 标准库,无 daemon / 无数据库;Windows + Linux 通用 |
|
|
44
|
-
| **生态分发** | GitHub + npm 双源同步发布;npx / install.sh
|
|
44
|
+
| **生态分发** | GitHub + npm 双源同步发布;npx / git clone / Download ZIP / install.sh 四种安装方式 |
|
|
45
45
|
|
|
46
46
|
## 功能体系 / 四阶段审查协议
|
|
47
47
|
|
|
@@ -87,53 +87,42 @@ python3 scripts/yotta_vetter.py source github:YottaMeta/yotta-memory
|
|
|
87
87
|
|
|
88
88
|
## 安装
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
以下四种方式任选,顺序即推荐优先级;技能文件一律从 **npm** 获取(GitHub 无代理较慢,npm 支持镜像)。
|
|
91
91
|
|
|
92
|
-
### 方式一:npm
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
npx -y @yottameta/yotta-vetter --
|
|
92
|
+
### 方式一:npm 一行装(推荐)
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
# 可选国内加速:npm config set registry https://registry.npmmirror.com
|
|
96
|
+
npx -y @yottameta/yotta-vetter --agent <智能体名称> # 装到指定智能体默认用户级技能目录
|
|
97
|
+
npx -y @yottameta/yotta-vetter --dir <智能体的技能目录> # 指到技能目录本身(如 ~/.codex/skills)
|
|
97
98
|
```
|
|
98
|
-
> 智能体不在预置列表里?用 `--dir` 指定它的 skills 目录,或手动复制(方式三)。`--list` 可查看各智能体对应的默认目录。想手动拿文件也可 `npm pack @yottameta/yotta-vetter` 解包后按方式二/三安装。
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
- `--agent <name>` 自动装到该智能体默认用户级目录;`--list` 可查看各智能体默认目录。
|
|
101
|
+
- `--dir <路径>` 装到指定的技能目录;未收录的智能体用 `--dir` 指到它的技能目录。
|
|
102
|
+
- npmmirror 未同步新包(404):加 `--registry=https://registry.npmjs.org/`(国内需代理),或稍等镜像缓存。
|
|
103
|
+
|
|
104
|
+
### 方式二:git clone(开发者 / 有 git 环境)
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
git clone https://github.com/YottaMeta/yotta-vetter.git <智能体的技能目录>/yotta-vetter
|
|
106
108
|
```
|
|
107
|
-
> 覆盖 17 类智能体,含国内 Trae / Qwen / Comate / CodeBuddy / Kimi。Windows 用户:装有 Git Bash 即可用;否则用方式三手动复制。
|
|
108
109
|
|
|
109
|
-
###
|
|
110
|
-
把整个 `yotta-vetter` 文件夹复制到目标智能体的 skills 目录。常见位置(用户级;Windows 用 `%USERPROFILE%`,Linux/macOS 用 `~`):
|
|
110
|
+
### 方式三:GitHub 下载压缩包(手动 / 无 git 环境)
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
| Amp | `%USERPROFILE%\.config\agents\skills\yotta-vetter\` | `.agents\skills\` |
|
|
122
|
-
| Kiro | `%USERPROFILE%\.kiro\skills\yotta-vetter\` | `.kiro\skills\` |
|
|
123
|
-
| WorkBuddy | `%USERPROFILE%\.workbuddy\skills\yotta-vetter\` | `.workbuddy\skills\` |
|
|
124
|
-
| Trae Code CLI | `%USERPROFILE%\.traecli\skills\yotta-vetter\` | `.traecli\skills\` |
|
|
125
|
-
| Trae IDE(国内) | `%USERPROFILE%\.trae-cn\skills\yotta-vetter\` | `.trae\skills\` |
|
|
126
|
-
| Qwen Code | `%USERPROFILE%\.qwen\skills\yotta-vetter\` | `.qwen\skills\` |
|
|
127
|
-
| Comate | `%USERPROFILE%\.comate\skills\yotta-vetter\` | `.comate\skills\` |
|
|
128
|
-
| CodeBuddy | `%USERPROFILE%\.codebuddy\skills\yotta-vetter\` | `.codebuddy\skills\` |
|
|
129
|
-
| Kimi | `%USERPROFILE%\.kimi\skills\yotta-vetter\` | `.kimi\skills\` |
|
|
130
|
-
| 通用 AGENTS.md | `%USERPROFILE%\.agents\skills\yotta-vetter\` | `.agents\skills\` |
|
|
131
|
-
|
|
132
|
-
> Codex 默认目录若设置了环境变量 `CODEX_HOME`,以该变量为准;opencode 若设置 `XDG_CONFIG_HOME` 同理。`.agents\skills` 并非通用目录,仅 OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 等会读取,**Claude Code 与 Codex 默认不读**。不确定时用 `--dir` 指定,或让该智能体自行安装。
|
|
112
|
+
在 GitHub 仓库 `YottaMeta/yotta-vetter` 点 **Code → Download ZIP**,解压后把 `yotta-vetter` 文件夹放进智能体技能目录。
|
|
113
|
+
|
|
114
|
+
### 方式四:install.sh(多智能体一键脚本)
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
bash install.sh --agent <name> # 装到指定智能体默认用户级目录
|
|
118
|
+
bash install.sh --dir <path> # 装到指定目录
|
|
119
|
+
bash install.sh --list # 列出智能体 -> 默认目录
|
|
120
|
+
```
|
|
133
121
|
|
|
122
|
+
> 方式一走 npm 源(npmmirror / npmjs),不依赖 GitHub;方式二 / 三走 GitHub,国内无代理可能失败。
|
|
134
123
|
## 升级 / 卸载
|
|
135
124
|
|
|
136
|
-
-
|
|
125
|
+
- **升级**:重新安装最新版覆盖即可——重跑你用的安装命令(如 `npx -y @yottameta/yotta-vetter --agent <name>` 或 `bash install.sh --agent <name>`)。技能目录内旧文件会被替换;不影响项目中其他文件。
|
|
137
126
|
- **卸载**:删除目标智能体 skills 目录下的 `yotta-vetter` 文件夹(各智能体目录见上表)即可。卸载后本技能不再生效。
|
|
138
127
|
|
|
139
128
|
## 常见问题
|
|
@@ -153,4 +142,4 @@ bash install.sh --dir /path/to/skills
|
|
|
153
142
|
|
|
154
143
|
## 许可证
|
|
155
144
|
|
|
156
|
-
MIT © YottaMeta —— 详见 [LICENSE](./LICENSE)。品牌声明见 [NOTICE](./NOTICE)
|
|
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.
|
|
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.
|
|
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",
|
package/references/checklist.md
CHANGED
|
@@ -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
|
|
package/scripts/vetter_rules.py
CHANGED
|
@@ -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
|
|
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%)"
|
package/scripts/yotta_vetter.py
CHANGED
|
@@ -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.
|
|
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
|
|
@@ -125,7 +125,7 @@ def collect_files(root):
|
|
|
125
125
|
try:
|
|
126
126
|
if p.suffix.lower() not in TEXT_EXTENSIONS and p.name.lower() not in DOTFILE_NAMES:
|
|
127
127
|
continue
|
|
128
|
-
if p.name in ("audit_rules.py", "vetter_rules.py", "verify_rules.py"):
|
|
128
|
+
if p.name in ("audit_rules.py", "vetter_rules.py", "verify_rules.py", "hardening_rules.py"):
|
|
129
129
|
continue # 签名数据文件(规则表)
|
|
130
130
|
if p.stat().st_size > MAX_FILE_SIZE:
|
|
131
131
|
continue
|
|
@@ -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))
|
|
Binary file
|
|
@@ -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)
|