@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 +11 -2
- package/NOTICE +1 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- 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 +79 -2
- package/scripts/test_yotta_vetter.py +0 -111
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
|
|
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
|
|
53
|
+
- 版权:YottaMeta 纯自有 MIT + NOTICE 品牌声明。
|
package/NOTICE
CHANGED
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).
|
|
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)
|
|
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
|
|
@@ -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)
|