@yottameta/yotta-verify-mcp-plugin 0.4.3 → 0.4.4
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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-verify-mcp/SKILL.md +1 -1
- package/skills/yotta-verify-mcp/scripts/yotta_verify.py +86 -3
- package/skills/yotta-verify-mcp/scripts/yotta_verify_mcp.py +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Security",
|
|
15
|
-
"version": "0.4.
|
|
15
|
+
"version": "0.4.4",
|
|
16
16
|
"description": "YuanXinMCP (元信MCP) - the pre-install security scanner for Agent skills, exposed as a stdio MCP server: scan_skill (dir/package -> verdict + findings), generate_badge (audited badge), gate_check (CI gate), get_report (JSON/Markdown). Local offline static scan; no upload of scanned content, no execution of scanned code; human confirmation required.",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元信MCP yotta-verify-mcp",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Security",
|
|
15
|
-
"version": "0.4.
|
|
15
|
+
"version": "0.4.4",
|
|
16
16
|
"description": "YuanXinMCP (元信MCP) - the pre-install security scanner for Agent skills, exposed as a stdio MCP server: scan_skill (dir/package -> verdict + findings), generate_badge (audited badge), gate_check (CI gate), get_report (JSON/Markdown). Local offline static scan; no upload of scanned content, no execution of scanned code; human confirmation required.",
|
|
17
17
|
"interface": {
|
|
18
18
|
"displayName": "元信MCP yotta-verify-mcp",
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "yotta-verify-mcp",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"description": "YuanXinMCP (元信MCP) - the pre-install security scanner for Agent skills, exposed as a stdio MCP server: scan_skill (dir/package -> verdict + findings), generate_badge (audited badge), gate_check (CI gate), get_report (JSON/Markdown). Local offline static scan; no upload of scanned content, no execution of scanned code; human confirmation required.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "YottaMeta",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: yotta-verify-mcp
|
|
3
3
|
description: 元信MCP(yotta-verify-mcp)—— 把元信(yotta-verify)装前安全扫描暴露为 stdio MCP server,提供 scan_skill(目录/包 → verdict+发现)、generate_badge(audited 徽章)、gate_check(CI 闸门)、get_report(JSON/Markdown 双视角综合报告)四个 MCP 工具。触发:给 MCP 客户端配置元信MCP、把装前信任扫描接入智能体/工作流、调用上列 MCP 工具做扫描;或用户说 MCP元信/元信MCP/trust-mcp/scan-mcp 等。边界:本地离线静态扫描,不上传被测内容、不执行被测代码、不联网;结论需人工确认。MCP 配置与全局/永久记忆写入均为可选,必须先获得用户明确同意;未获同意前不写任何文件,可直接降级 CLI。
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.4
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -53,7 +53,7 @@ sys.path.insert(0, str(_HERE))
|
|
|
53
53
|
import verify_rules # noqa: E402
|
|
54
54
|
import threat_engine # noqa: E402
|
|
55
55
|
|
|
56
|
-
VERSION = "0.4.
|
|
56
|
+
VERSION = "0.4.4"
|
|
57
57
|
TOOL_NAME = "yotta-verify"
|
|
58
58
|
CN_NAME = "元信"
|
|
59
59
|
|
|
@@ -75,6 +75,20 @@ DOTFILE_NAMES = {
|
|
|
75
75
|
MAX_FILE_SIZE = 1_000_000
|
|
76
76
|
MAX_LINE_LEN = verify_rules.MAX_LINE_LEN
|
|
77
77
|
MAX_FILES = 2000
|
|
78
|
+
# v0.4.4:不可静态分析的可执行 / 二进制载荷不再静默跳过(fail-closed)。
|
|
79
|
+
# 旧行为只对 TEXT_EXTENSIONS 做内容分析,`.exe` / `.dll` / `.jar` / 无扩展名二进制
|
|
80
|
+
# 会被直接忽略——攻击方可以把载荷放进这些文件逃避扫描。现在按扩展名 + 魔数识别,
|
|
81
|
+
# 命中即出 STR-009(medium),扫描结论至少为「需复核」。
|
|
82
|
+
OPAQUE_EXTENSIONS = {
|
|
83
|
+
".exe", ".dll", ".sys", ".com", ".msi", ".scr", ".cab", ".cpl",
|
|
84
|
+
".so", ".dylib", ".bin", ".o", ".a",
|
|
85
|
+
".jar", ".class", ".apk", ".dex", ".wasm", ".node",
|
|
86
|
+
}
|
|
87
|
+
OPAQUE_MAGIC = (b"\x7fELF", b"MZ", b"\xca\xfe\xba\xbe", b"\xcf\xfa\xed\xfe")
|
|
88
|
+
# tarball 解包上限(防资源耗尽)
|
|
89
|
+
MAX_TARBALL_MEMBERS = 2000
|
|
90
|
+
MAX_TARBALL_MEMBER_BYTES = 50 * 1024 * 1024
|
|
91
|
+
MAX_TARBALL_TOTAL_BYTES = 200 * 1024 * 1024
|
|
78
92
|
# 签名数据文件:规则表是扫描器自身与家族检测技能的签名数据库,不是被测技能行为。
|
|
79
93
|
# v0.3.2 收紧(旧行为 = 按文件名全局豁免,`scripts/<同名>.py` 可被随意伪造以逃避扫描):
|
|
80
94
|
# ① 相对路径必须恰为 scripts/<规则表名>.py;
|
|
@@ -225,6 +239,57 @@ def walk_files(root, base="", trusted_tests=None, scan_root=None):
|
|
|
225
239
|
return out
|
|
226
240
|
|
|
227
241
|
|
|
242
|
+
def _read_magic(path, size=4):
|
|
243
|
+
try:
|
|
244
|
+
with open(str(path), "rb") as fh:
|
|
245
|
+
return fh.read(size)
|
|
246
|
+
except OSError:
|
|
247
|
+
return b""
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def find_opaque_files(root):
|
|
251
|
+
"""列出无法静态分析、但带可执行 / 二进制特征的文件(v0.4.4,fail-closed)。"""
|
|
252
|
+
out = []
|
|
253
|
+
if root.is_file():
|
|
254
|
+
candidates = [(root, root.name)]
|
|
255
|
+
else:
|
|
256
|
+
candidates = []
|
|
257
|
+
stack = [root]
|
|
258
|
+
while stack:
|
|
259
|
+
cur = stack.pop()
|
|
260
|
+
try:
|
|
261
|
+
entries = sorted(cur.iterdir())
|
|
262
|
+
except OSError:
|
|
263
|
+
continue
|
|
264
|
+
for entry in entries:
|
|
265
|
+
if entry.name in SKIP_DIRS:
|
|
266
|
+
continue
|
|
267
|
+
if entry.is_dir():
|
|
268
|
+
stack.append(entry)
|
|
269
|
+
elif entry.is_file():
|
|
270
|
+
rel = str(entry.relative_to(root)).replace("\\", "/")
|
|
271
|
+
candidates.append((entry, rel))
|
|
272
|
+
for path, rel in candidates:
|
|
273
|
+
name = path.name.lower()
|
|
274
|
+
if name in DOTFILE_NAMES or Path(name).suffix in TEXT_EXTENSIONS:
|
|
275
|
+
continue
|
|
276
|
+
suffix = Path(name).suffix
|
|
277
|
+
opaque = suffix in OPAQUE_EXTENSIONS
|
|
278
|
+
if not opaque:
|
|
279
|
+
magic = _read_magic(path)
|
|
280
|
+
opaque = any(magic.startswith(sig) for sig in OPAQUE_MAGIC)
|
|
281
|
+
if not opaque:
|
|
282
|
+
continue
|
|
283
|
+
try:
|
|
284
|
+
size = path.stat().st_size
|
|
285
|
+
except OSError:
|
|
286
|
+
size = 0
|
|
287
|
+
out.append((rel, size))
|
|
288
|
+
if len(out) >= 50:
|
|
289
|
+
break
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
|
|
228
293
|
def read_lines(path):
|
|
229
294
|
"""读取文本文件行列表(容错编码;超长行截断)。"""
|
|
230
295
|
try:
|
|
@@ -547,10 +612,15 @@ def exit_code_of(verdict):
|
|
|
547
612
|
# ── 扫描主流程 ─────────────────────────────────────────────────────────────
|
|
548
613
|
|
|
549
614
|
def _safe_extract(tf, dest):
|
|
550
|
-
"""提取 tarball(Python 3.8
|
|
615
|
+
"""提取 tarball(Python 3.8 兼容;拒绝路径穿越、链接与特殊文件;v0.4.4 加解包上限)。"""
|
|
551
616
|
safe_members = []
|
|
552
617
|
root = Path(dest).resolve()
|
|
553
|
-
|
|
618
|
+
members = tf.getmembers()
|
|
619
|
+
if len(members) > MAX_TARBALL_MEMBERS:
|
|
620
|
+
raise ValueError("tarball 成员数超过上限: %d > %d"
|
|
621
|
+
% (len(members), MAX_TARBALL_MEMBERS))
|
|
622
|
+
total_bytes = 0
|
|
623
|
+
for member in members:
|
|
554
624
|
name = member.name.replace("\\", "/")
|
|
555
625
|
drive_path = len(name) >= 2 and name[1] == ":" and name[0].isalpha()
|
|
556
626
|
if (not name or name.startswith("/") or drive_path
|
|
@@ -558,6 +628,12 @@ def _safe_extract(tf, dest):
|
|
|
558
628
|
raise ValueError("tarball 含危险路径: %s" % name)
|
|
559
629
|
if member.issym() or member.islnk() or member.isdev() or member.isfifo():
|
|
560
630
|
raise ValueError("tarball 含链接或特殊文件成员: %s" % member.name)
|
|
631
|
+
size = int(member.size or 0)
|
|
632
|
+
if size > MAX_TARBALL_MEMBER_BYTES:
|
|
633
|
+
raise ValueError("tarball 单成员过大: %s(%d 字节)" % (member.name, size))
|
|
634
|
+
total_bytes += size
|
|
635
|
+
if total_bytes > MAX_TARBALL_TOTAL_BYTES:
|
|
636
|
+
raise ValueError("tarball 解包总量超过上限: %d 字节" % total_bytes)
|
|
561
637
|
parts = [part for part in name.split("/") if part not in ("", ".")]
|
|
562
638
|
target = root.joinpath(*parts).resolve()
|
|
563
639
|
try:
|
|
@@ -625,6 +701,13 @@ def scan_core(target, name_hint=None, auto_name_hint=False):
|
|
|
625
701
|
name_hint = resolve_name_hint(target, root, from_archive=from_archive)
|
|
626
702
|
files = walk_files(root)
|
|
627
703
|
findings = scan_patterns(files)
|
|
704
|
+
# v0.4.4:不可静态分析的可执行 / 二进制载荷按 fail-closed 处理(不再静默跳过)
|
|
705
|
+
for rel, size in find_opaque_files(root):
|
|
706
|
+
findings.append(Finding(
|
|
707
|
+
"Structure", "medium", "不可扫描内容",
|
|
708
|
+
rel, 0,
|
|
709
|
+
"包内含无法静态分析的可执行 / 二进制文件(%d 字节),需人工确认来源与用途"
|
|
710
|
+
% size, 70, "STR-009"))
|
|
628
711
|
check_skill_integrity(root, findings, name_hint)
|
|
629
712
|
permission_summary(files, findings)
|
|
630
713
|
# 检测技能文档降级(2026-08-30):目标含检测器签名文件(audit_rules 等)→
|