@yottameta/yotta-verify-mcp-plugin 0.4.1 → 0.4.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/.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 +66 -14
- package/skills/yotta-verify-mcp/scripts/yotta_verify_mcp.py +2 -6
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"authentication": "ON_INSTALL"
|
|
13
13
|
},
|
|
14
14
|
"category": "Security",
|
|
15
|
-
"version": "0.4.
|
|
15
|
+
"version": "0.4.2",
|
|
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.2",
|
|
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.2",
|
|
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.2
|
|
5
5
|
license: MIT
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -52,7 +52,7 @@ sys.path.insert(0, str(_HERE))
|
|
|
52
52
|
import verify_rules # noqa: E402
|
|
53
53
|
import threat_engine # noqa: E402
|
|
54
54
|
|
|
55
|
-
VERSION = "0.4.
|
|
55
|
+
VERSION = "0.4.2"
|
|
56
56
|
TOOL_NAME = "yotta-verify"
|
|
57
57
|
CN_NAME = "元信"
|
|
58
58
|
|
|
@@ -436,6 +436,8 @@ def downgrade_detector_docs(findings, root):
|
|
|
436
436
|
if not is_detector_skill(root):
|
|
437
437
|
return
|
|
438
438
|
for f in findings:
|
|
439
|
+
if f.detector == "Structure":
|
|
440
|
+
continue
|
|
439
441
|
if f.severity in ("critical", "high", "medium"):
|
|
440
442
|
if Path(f.file_path).suffix.lower() in _DOC_EXT:
|
|
441
443
|
f.severity = "info"
|
|
@@ -492,19 +494,71 @@ def exit_code_of(verdict):
|
|
|
492
494
|
# ── 扫描主流程 ─────────────────────────────────────────────────────────────
|
|
493
495
|
|
|
494
496
|
def _safe_extract(tf, dest):
|
|
495
|
-
"""提取 tarball(Python 3.8
|
|
497
|
+
"""提取 tarball(Python 3.8 兼容;拒绝路径穿越、链接与特殊文件)。"""
|
|
498
|
+
safe_members = []
|
|
499
|
+
root = Path(dest).resolve()
|
|
496
500
|
for member in tf.getmembers():
|
|
497
|
-
name = member.name
|
|
498
|
-
|
|
501
|
+
name = member.name.replace("\\", "/")
|
|
502
|
+
drive_path = len(name) >= 2 and name[1] == ":" and name[0].isalpha()
|
|
503
|
+
if (not name or name.startswith("/") or drive_path
|
|
504
|
+
or any(part == ".." for part in name.split("/"))):
|
|
499
505
|
raise ValueError("tarball 含危险路径: %s" % name)
|
|
500
|
-
|
|
506
|
+
if member.issym() or member.islnk() or member.isdev() or member.isfifo():
|
|
507
|
+
raise ValueError("tarball 含链接或特殊文件成员: %s" % member.name)
|
|
508
|
+
parts = [part for part in name.split("/") if part not in ("", ".")]
|
|
509
|
+
target = root.joinpath(*parts).resolve()
|
|
510
|
+
try:
|
|
511
|
+
target.relative_to(root)
|
|
512
|
+
except ValueError:
|
|
513
|
+
raise ValueError("tarball 成员越出临时目录: %s" % member.name)
|
|
514
|
+
member.mode = member.mode & 0o777
|
|
515
|
+
safe_members.append(member)
|
|
516
|
+
if sys.version_info >= (3, 12):
|
|
517
|
+
tf.extractall(dest, members=safe_members, filter="data")
|
|
518
|
+
else:
|
|
519
|
+
tf.extractall(dest, members=safe_members)
|
|
501
520
|
|
|
502
521
|
|
|
503
|
-
def
|
|
522
|
+
def _package_slug_from_root(root):
|
|
523
|
+
"""从包根 package.json 读取 npm 包名并归一为技能 slug。"""
|
|
524
|
+
try:
|
|
525
|
+
data = json.loads((root / "package.json").read_text(encoding="utf-8"))
|
|
526
|
+
except (OSError, ValueError):
|
|
527
|
+
return None
|
|
528
|
+
name = data.get("name") if isinstance(data, dict) else None
|
|
529
|
+
if not isinstance(name, str):
|
|
530
|
+
return None
|
|
531
|
+
slug = name.strip().rsplit("/", 1)[-1]
|
|
532
|
+
if not slug or slug in (".", "..") or "/" in slug or "\\" in slug:
|
|
533
|
+
return None
|
|
534
|
+
return slug
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def resolve_name_hint(target, root, from_archive=False):
|
|
538
|
+
"""解析 STR-004 的期望名称:安装目录用目录名,npm 包输入用包名。"""
|
|
539
|
+
if from_archive:
|
|
540
|
+
package_root = root / "package"
|
|
541
|
+
if package_root.is_dir():
|
|
542
|
+
root = package_root
|
|
543
|
+
slug = _package_slug_from_root(root)
|
|
544
|
+
if slug:
|
|
545
|
+
return slug
|
|
546
|
+
return Path(target).name
|
|
547
|
+
if root.name == "package":
|
|
548
|
+
slug = _package_slug_from_root(root)
|
|
549
|
+
if slug:
|
|
550
|
+
return slug
|
|
551
|
+
return root.name
|
|
552
|
+
return root.name
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def scan_core(target, name_hint=None, auto_name_hint=False):
|
|
504
556
|
"""扫描目录/tarball,返回 (findings, counts, verdict, scan_meta)。"""
|
|
505
557
|
tmpdir = None
|
|
506
558
|
root = Path(target)
|
|
559
|
+
from_archive = False
|
|
507
560
|
if root.is_file() and str(root).lower().endswith((".tgz", ".tar.gz")):
|
|
561
|
+
from_archive = True
|
|
508
562
|
tmpdir = tempfile.mkdtemp(prefix="yotta-verify-")
|
|
509
563
|
with tarfile.open(str(root), "r:gz") as tf:
|
|
510
564
|
_safe_extract(tf, tmpdir)
|
|
@@ -514,6 +568,8 @@ def scan_core(target, name_hint=None):
|
|
|
514
568
|
import shutil
|
|
515
569
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
|
516
570
|
raise SystemExit("目标不存在或不是目录: %s" % target)
|
|
571
|
+
if auto_name_hint and name_hint is None:
|
|
572
|
+
name_hint = resolve_name_hint(target, root, from_archive=from_archive)
|
|
517
573
|
files = walk_files(root)
|
|
518
574
|
findings = scan_patterns(files)
|
|
519
575
|
check_skill_integrity(root, findings, name_hint)
|
|
@@ -748,12 +804,8 @@ def shields_url(verdict):
|
|
|
748
804
|
|
|
749
805
|
# ── CLI ─────────────────────────────────────────────────────────────────────
|
|
750
806
|
|
|
751
|
-
def _name_hint(target):
|
|
752
|
-
return Path(target).name
|
|
753
|
-
|
|
754
|
-
|
|
755
807
|
def cmd_scan(args):
|
|
756
|
-
findings, counts, verdict, meta = scan_core(args.path,
|
|
808
|
+
findings, counts, verdict, meta = scan_core(args.path, auto_name_hint=True)
|
|
757
809
|
code = exit_code_of(verdict)
|
|
758
810
|
# gate 模式
|
|
759
811
|
if args.max_severity:
|
|
@@ -801,7 +853,7 @@ def cmd_badge(args):
|
|
|
801
853
|
}
|
|
802
854
|
# 若给目录:先扫描拿 verdict;否则默认 SAFE
|
|
803
855
|
if args.path and Path(args.path).exists():
|
|
804
|
-
findings, counts, verdict, meta = scan_core(args.path,
|
|
856
|
+
findings, counts, verdict, meta = scan_core(args.path, auto_name_hint=True)
|
|
805
857
|
else:
|
|
806
858
|
verdict = VERDICT_SAFE
|
|
807
859
|
counts = {s: 0 for s in _SEVERITY_ORDER}
|
|
@@ -815,7 +867,7 @@ def cmd_badge(args):
|
|
|
815
867
|
|
|
816
868
|
|
|
817
869
|
def cmd_report(args):
|
|
818
|
-
findings, counts, verdict, meta = scan_core(args.path,
|
|
870
|
+
findings, counts, verdict, meta = scan_core(args.path, auto_name_hint=True)
|
|
819
871
|
if args.json:
|
|
820
872
|
print(render_json(findings, counts, verdict, meta))
|
|
821
873
|
else:
|
|
@@ -830,7 +882,7 @@ def cmd_report(args):
|
|
|
830
882
|
|
|
831
883
|
|
|
832
884
|
def cmd_gate(args):
|
|
833
|
-
findings, counts, verdict, meta = scan_core(args.path,
|
|
885
|
+
findings, counts, verdict, meta = scan_core(args.path, auto_name_hint=True)
|
|
834
886
|
code = exit_code_of(verdict)
|
|
835
887
|
limit = _SEVERITY_VALUE.get((args.max_severity or "medium").lower(), 1)
|
|
836
888
|
worst = _SEVERITY_VALUE.get(verdict_worst(findings), 0)
|
|
@@ -27,7 +27,7 @@ sys.path.insert(0, str(_HERE))
|
|
|
27
27
|
|
|
28
28
|
import yotta_verify as yv # noqa: E402
|
|
29
29
|
|
|
30
|
-
VERSION = "0.4.
|
|
30
|
+
VERSION = "0.4.2"
|
|
31
31
|
TOOL_NAME = "yotta-verify-mcp"
|
|
32
32
|
CN_NAME = "元信"
|
|
33
33
|
MCP_PROTOCOL_MODERN = "2026-07-28"
|
|
@@ -45,10 +45,6 @@ def _tool_error(message, extra=None):
|
|
|
45
45
|
"isError": True}
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def _name_hint(path):
|
|
49
|
-
return Path(path).name
|
|
50
|
-
|
|
51
|
-
|
|
52
48
|
def _resolve_target(target, tmp_dirs):
|
|
53
49
|
"""把 target 解析为本地路径/压缩包路径;npm 包名则 npm pack 到临时目录。"""
|
|
54
50
|
p = Path(target)
|
|
@@ -77,7 +73,7 @@ def _scan(target):
|
|
|
77
73
|
tmp_dirs = []
|
|
78
74
|
try:
|
|
79
75
|
path = _resolve_target(target, tmp_dirs)
|
|
80
|
-
findings, counts, verdict, meta = yv.scan_core(path,
|
|
76
|
+
findings, counts, verdict, meta = yv.scan_core(path, auto_name_hint=True)
|
|
81
77
|
return findings, counts, verdict, meta, None
|
|
82
78
|
except SystemExit as e:
|
|
83
79
|
return None, None, None, None, str(e)
|