@yottameta/yotta-guardian 0.1.4 → 0.1.5

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,20 @@
1
1
  # 更新日志
2
2
 
3
+ ## v0.1.5 (2026-09-19)
4
+
5
+ 安全修复:包装命令解包,堵住 argv 级分析的绕过路径。
6
+
7
+ - 背景:此前 argv 级规则只看命令第一个词(`argv[0]`)。`bash -c "…"`、`cmd /c "…"`、
8
+ `powershell -Command "…"`、`sudo …`、`env …`、`nohup …`、`timeout 5 …` 这类包装会把真正的
9
+ 动词藏在内层,导致包装后的危险命令被判为放行(例:裸 `rm -rf /` 拒绝,`bash -c "rm -rf /"` 却放行)。
10
+ - 修复:新增受控解包层(`_unwrap_argv` / `_shell_inner` / `_strip_wrapper`)——
11
+ 识别 shell(sh/bash/zsh/dash/ksh/ash/busybox/cmd/powershell/pwsh)、提权与环境包装
12
+ (sudo/doas/env/nohup/setsid/stdbuf/nice/ionice/time/xargs/timeout/command/builtin/exec/chroot),
13
+ 剥离包装自身的选项与参数后,对内层命令跑同一套 argv 规则;解包深度上限 3 层,可嵌套(如 `sudo sh -c "…"`)。
14
+ - 边界:解包只影响 argv 级动词分析,文本级规则与放行模式行为不变;解包失败时不产生新命中,
15
+ 保持既有判定(不引入误报放大)。
16
+ - 验证:测试 72/72(新增 `WrapperBypassTest` 12 项:9 项阻断回归 + 3 项安全侧不误报)。
17
+
3
18
  ## v0.1.4 (2026-09-19)
4
19
 
5
20
  版本对齐维护:修复历史发布元数据与包内版本不一致;无功能变更。
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yotta-guardian
3
- version: 0.1.4
3
+ version: 0.1.5
4
4
  description: 元盾 —— 跨智能体的危险调用拦截护栏:确定性规则引擎 + 可插拔意图验证(不绑模型),拦截危险 exec / write / edit / read / run / shell 工具调用,提供审计日志。触发:代理要执行高风险命令(递归删除、磁盘格式化、提权、防火墙改动、反向 shell、下载即执行等)、要写入系统敏感路径或修改系统配置、要在执行危险操作前做安全检查、或用户说 护栏/拦截/危险操作/安全检查 等。边界:默认只读评估,不自动执行也不放行危险操作;不替代用户决策;不隐藏审计记录;规则可配置。
5
5
  license: MIT
6
6
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-guardian",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Yuandun (元盾) — cross-agent dangerous tool-call guardrail: a deterministic rule engine + pluggable intent verifier (model-agnostic) that evaluates exec/write/edit/read/run/shell calls and provides audit logs. Triggers when an agent is about to run a high-risk command, write sensitive paths, or change system config. Boundaries: read-only evaluation by default, configurable allowances.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -8,7 +8,7 @@
8
8
  | 层 | 作用于 | 说明 |
9
9
  |---|---|---|
10
10
  | 文本模式 | exec 命令 / write 内容 | 正则匹配危险形态(下载即执行、编码执行、反向 shell、向系统文件追加内容等) |
11
- | argv 级 | exec / run / shell | 命令拆词后按「动词 + 标志 + 目标」分析(递归删除、格式化设备、提权、防火墙、服务、持久化等) |
11
+ | argv 级 | exec / run / shell | 先做包装解包(shell `-c` / `cmd /c` / `powershell -Command` / sudo / env / nohup / timeout 等,深度上限 3),再按「动词 + 标志 + 目标」分析(递归删除、格式化设备、提权、防火墙、服务、持久化等) |
12
12
  | 敏感路径 | write / edit / read | 目标路径规范化后按系统核心 / 用户敏感 / 越界分级 |
13
13
  | 放行规则 | 命令 / 路径 | 命中则高危可降级为警告(critical 不受影响) |
14
14
 
@@ -51,7 +51,7 @@ _HERE = Path(__file__).resolve().parent
51
51
  sys.path.insert(0, str(_HERE))
52
52
  import guardian_rules as GR # noqa: E402
53
53
 
54
- VERSION = "0.1.4"
54
+ VERSION = "0.1.5"
55
55
  TOOL_NAME = "yotta-guardian"
56
56
  TOOL_CN = "元盾"
57
57
 
@@ -282,6 +282,14 @@ class RuleEngine:
282
282
  argv = _tokenize(call.cmd)
283
283
  if not argv:
284
284
  return []
285
+ findings = []
286
+ # v0.1.5:先做包装解包(sh -c / cmd /c / powershell -Command / sudo / env …),
287
+ # 否则只看 argv[0] 会漏掉内层真正的危险动词。
288
+ for sub in _unwrap_argv(argv):
289
+ findings += self._argv_findings_one(sub)
290
+ return findings
291
+
292
+ def _argv_findings_one(self, argv):
285
293
  verb = os.path.basename(argv[0]).lower()
286
294
  findings = []
287
295
  if verb in _RM_VERBS:
@@ -444,6 +452,100 @@ def _tokenize(cmd):
444
452
  return cmd.split()
445
453
 
446
454
 
455
+ # ── 包装命令解包(v0.1.5)─────────────────────────────────────────────────
456
+ # 危险动作常被包装:sh -c "…"、bash -lc "…"、cmd /c "…"、powershell -Command "…"、
457
+ # sudo …、env …、nohup …、timeout 5 …。只分析 argv[0] 会漏掉内层动词,
458
+ # 因此先按已知包装表做受控解包(深度上限),再对内层命令跑同一套 argv 规则。
459
+ _UNWRAP_MAX_DEPTH = 3
460
+
461
+ _SHELL_WRAPPERS = {
462
+ "sh", "bash", "zsh", "dash", "ksh", "ash", "busybox",
463
+ "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe",
464
+ }
465
+ # shell 的「执行字符串」开关:-c / -lc / -ic / /c / /k / -Command
466
+ _SHELL_EXEC_FLAGS = {"/c", "/k", "-command", "-cmd"}
467
+ _SHELL_EXEC_FLAG_RE = re.compile(r"^-[a-z]*c$")
468
+
469
+ _WRAPPER_SPECS = {
470
+ "sudo": {"value_opts": {"-u", "-g", "-p", "-C", "-h", "-r", "-t", "-U",
471
+ "--user", "--group", "--prompt", "--chdir", "--host",
472
+ "--role", "--type", "--other-user", "--close-from"}},
473
+ "doas": {"value_opts": {"-u", "-C"}},
474
+ "env": {"value_opts": {"-u", "-S", "-C", "--unset", "--split-string", "--chdir"},
475
+ "assignments": True},
476
+ "nohup": {"value_opts": {}},
477
+ "setsid": {"value_opts": {}},
478
+ "stdbuf": {"value_opts": {"-i", "-o", "-e", "--input", "--output", "--error"}},
479
+ "nice": {"value_opts": {"-n", "--adjustment"}},
480
+ "ionice": {"value_opts": {"-c", "-n", "-p", "--class", "--classdata", "--pid"}},
481
+ "time": {"value_opts": {"-o", "-f", "--output", "--format"}},
482
+ "xargs": {"value_opts": {"-I", "-n", "-P", "-d", "-a", "-E", "-s", "-L",
483
+ "--max-args", "--max-procs", "--delimiter", "--arg-file",
484
+ "--eof", "--max-chars", "--max-lines"}},
485
+ "timeout": {"value_opts": {"-s", "-k", "--signal", "--kill-after"},
486
+ "skip_positional": 1},
487
+ "command": {"value_opts": {}},
488
+ "builtin": {"value_opts": {}},
489
+ "exec": {"value_opts": {}},
490
+ "chroot": {"value_opts": {"--userspec", "--groups"}},
491
+ "busybox": {"value_opts": {}},
492
+ }
493
+
494
+
495
+ def _shell_inner(argv):
496
+ """从 shell 包装 argv 中取出执行开关后的内层命令文本(无则返回 None)。"""
497
+ for i, tok in enumerate(argv[1:], start=1):
498
+ low = tok.lower()
499
+ if low in _SHELL_EXEC_FLAGS or _SHELL_EXEC_FLAG_RE.match(low):
500
+ rest = argv[i + 1:]
501
+ if not rest:
502
+ return None
503
+ parts = []
504
+ for item in rest:
505
+ # Windows 下 shlex(posix=False) 会保留外层引号,剥掉后再拼内层命令
506
+ if len(item) >= 2 and item[0] == item[-1] and item[0] in ("'", '"'):
507
+ item = item[1:-1]
508
+ parts.append(item)
509
+ return " ".join(parts)
510
+ return None
511
+
512
+
513
+ def _strip_wrapper(argv, spec):
514
+ """跳过包装命令自身的选项/参数,返回内层命令 argv(找不到则空列表)。"""
515
+ value_opts = spec.get("value_opts", set())
516
+ i = 1
517
+ n = len(argv)
518
+ while i < n:
519
+ tok = argv[i]
520
+ if spec.get("assignments") and "=" in tok and not tok.startswith("-"):
521
+ i += 1
522
+ continue
523
+ if tok == "--":
524
+ i += 1
525
+ break
526
+ if tok.startswith("-") and tok != "-":
527
+ i += 2 if tok in value_opts else 1
528
+ continue
529
+ break
530
+ i += int(spec.get("skip_positional", 0) or 0)
531
+ return argv[i:] if i < n else []
532
+
533
+
534
+ def _unwrap_argv(argv, depth=0):
535
+ """返回需要做 argv 级分析的命令列表(含解包出的内层命令)。"""
536
+ if not argv or depth > _UNWRAP_MAX_DEPTH:
537
+ return []
538
+ verb = os.path.basename(argv[0]).lower()
539
+ if verb in _SHELL_WRAPPERS:
540
+ inner = _shell_inner(argv)
541
+ return _unwrap_argv(_tokenize(inner), depth + 1) if inner else []
542
+ spec = _WRAPPER_SPECS.get(verb)
543
+ if spec:
544
+ rest = _strip_wrapper(argv, spec)
545
+ return _unwrap_argv(rest, depth + 1) if rest else []
546
+ return [argv]
547
+
548
+
447
549
  # ── argv 级规则(按动词分组)───────────────────────────────────────────────
448
550
 
449
551
  _RM_VERBS = {"rm", "rmdir", "del", "erase", "unlink", "shred", "remove-item"}
@@ -3,7 +3,7 @@
3
3
  "slug": "yotta-guardian",
4
4
  "name": "元盾",
5
5
  "package": "@yottameta/yotta-guardian",
6
- "version": "0.1.4",
6
+ "version": "0.1.5",
7
7
  "trust": "yottameta",
8
8
  "install": {
9
9
  "idempotent": true