Files
codex-app-server/cloud/tests/test_chv_secfix.py
T
host df8c64bd64 feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示
架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py,
生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读
registry 文件清理,不构造池、不碰 prewarm/acquire/release);
cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律
ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由);
worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从
接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、
R3 永不替换,#215 模块当前不存在由测试钉住)。

可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/
timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集
与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图
与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型
标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍
(删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。

前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案
(执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表
行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行
(已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本
卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个
生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生),
只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
2026-10-31 06:55:26 +00:00

702 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""CHV 安全审计修复轨测试(feat-chv-secfix,堆叠于 #239)。
只用标准库与 mock runner/launcher/api_client,绝不启动真实
VM:当前环境没有可跑真 VM 的宿主。本文件只证明代码层面
的修复(密钥移出 argv、磁盘删除、默认值、校验、审计等)
成立;真机否定测试(tap 防火墙实效、unset 代理后直连、
IP spoof 后 SSH 拒绝等)一律列在就绪清单 §11,不在本
文件声称已验证。测试里只用 FAKE 占位密钥,绝无真实密钥。
"""
import json
import os
import shlex
import socket
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
try:
from cloud.chv.config import build_chv_config
from cloud.chv.disk import (
check_snapshot_limits,
delete_task_disk,
provision_task_disk,
record_snapshot,
)
from cloud.chv.hardening import (
ensure_secure_state_dir,
socket_is_live,
validate_net_entries,
validate_snapshot_url,
)
from cloud.chv.hostkeys import (
build_seed_disk,
dirty_marker_path,
write_seed,
)
from cloud.chv.experimental_pool import WarmPool
from cloud.chv.ssh_transport import (
build_ssh_command,
split_secret_env,
)
from cloud.sandbox import (
ChvFailure,
CloudHypervisorExecutor,
)
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.config import build_chv_config
from cloud.chv.disk import (
check_snapshot_limits,
delete_task_disk,
provision_task_disk,
record_snapshot,
)
from cloud.chv.hardening import (
ensure_secure_state_dir,
socket_is_live,
validate_net_entries,
validate_snapshot_url,
)
from cloud.chv.hostkeys import (
build_seed_disk,
dirty_marker_path,
write_seed,
)
from cloud.chv.experimental_pool import WarmPool
from cloud.chv.ssh_transport import (
build_ssh_command,
split_secret_env,
)
from cloud.sandbox import (
ChvFailure,
CloudHypervisorExecutor,
)
class FakeResult:
def __init__(self, returncode=0, stdout="", stderr=""):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
class FakeProc:
def __init__(self, alive=True):
self._alive = alive
def poll(self):
return None if self._alive else 1
def terminate(self):
self._alive = False
def wait(self, timeout=None):
return 0
def kill(self):
self._alive = False
def _write(path, content=b"x"):
with open(path, "wb") as fh:
fh.write(content)
os.chmod(path, 0o755)
return path
class SecfixCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.binary = _write(os.path.join(self.dir, "cloud-hypervisor"))
self.ch_remote = _write(os.path.join(self.dir, "ch-remote"))
self.kernel = _write(os.path.join(self.dir, "vmlinux"))
self.rootfs = _write(os.path.join(self.dir, "disk.raw"),
b"R" * 100)
self.ssh_key = _write(os.path.join(self.dir, "id_chv"))
self.kvm = _write(os.path.join(self.dir, "kvm"))
self.cpuinfo = os.path.join(self.dir, "cpuinfo")
with open(self.cpuinfo, "w") as fh:
fh.write("flags : fpu vmx sse\n")
self.state_dir = os.path.join(self.dir, "state")
self.calls = []
self.audit = []
def tearDown(self):
self.tmp.cleanup()
def _default_runner(self, cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh":
if cmd[-1] == "true":
return FakeResult(0, "", "")
return FakeResult(0, "", "")
if cmd and cmd[0] == "ssh-keygen":
priv = cmd[cmd.index("-f") + 1]
with open(priv, "w") as fh:
fh.write("-----BEGIN OPENSSH PRIVATE KEY-----\nFAKE\n")
os.chmod(priv, 0o600)
with open(priv + ".pub", "w") as fh:
fh.write("ssh-ed25519 AAAAfake1 vm-key\n")
return FakeResult(0, "", "")
if cmd and cmd[0] == "genisoimage":
out = cmd[cmd.index("-output") + 1]
with open(out, "wb") as fh:
fh.write(b"CIDATA" * 16)
return FakeResult(0, "", "")
return FakeResult(0, "", "")
def _default_api(self, socket_path, method, path, payload=None):
return (200, {})
def make_exec(self, runner=None, env_extra=None, **overrides):
# 生产默认:provisioned(不显式设 keyscan),回环仍
# 是实验室宣称(mock runner 假装转发已通)。
env = {"CHV_EGRESS_PROXY": "http://10.0.0.1:8888",
"CHV_SSH_FORWARD": "1"}
if env_extra:
env.update(env_extra)
kwargs = dict(
binary=self.binary, ch_remote=self.ch_remote,
kernel=self.kernel, rootfs=self.rootfs,
ssh_key=self.ssh_key, kvm_path=self.kvm,
cpuinfo_path=self.cpuinfo, state_dir=self.state_dir,
runner=runner or self._default_runner,
launcher=lambda cmd: FakeProc(alive=True),
api_client=self._default_api,
sleeper=lambda s: None, env=env)
kwargs.update(overrides)
ex = CloudHypervisorExecutor(**kwargs)
ex.audit_callback = lambda e, t, d, k=None: self.audit.append(
(e, t.get("id") if isinstance(t, dict) else t, d, k))
return ex
# -- H1 egress 代码门 + H7 tap 隔离 ------------------------------------
class EgressAndNetTest(SecfixCase):
def test_egress_proxy_non_http_rejected(self):
ex = self.make_exec(
env_extra={"CHV_EGRESS_PROXY": "ftp://10.0.0.1:21"})
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind, "config_invalid")
def test_egress_proxy_without_host_rejected(self):
ex = self.make_exec(
env_extra={"CHV_EGRESS_PROXY": "http:///path"})
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind, "config_invalid")
def test_net_unknown_field_rejected(self):
with self.assertRaises(ChvFailure) as ctx:
validate_net_entries(
[{"tap": "tap0", "mac": "aa:bb:cc:dd:ee:01",
"ip": "10.0.0.2", "gateway": "10.0.0.1"}], [])
self.assertEqual(ctx.exception.kind, "net_config_invalid")
def test_net_intra_config_dup_rejected(self):
with self.assertRaises(ChvFailure) as ctx:
validate_net_entries(
[{"tap": "tap0", "mac": "aa:bb:cc:dd:ee:01",
"ip": "10.0.0.2"},
{"tap": "tap0", "mac": "aa:bb:cc:dd:ee:02",
"ip": "10.0.0.3"}], [])
self.assertEqual(ctx.exception.kind, "net_config_invalid")
def test_net_cross_vm_conflict_refuses_second_wake(self):
net = '[{"tap": "tap0", "mac": "aa:bb:cc:dd:ee:01",' \
'"ip": "10.0.0.2", "mask": "255.255.255.0"}]'
ex = self.make_exec(env_extra={"CHV_NET_JSON": net})
out = ex.wake_vm({"id": "vm1"})
self.assertEqual(out["state"], "running")
# 第一台的 net.json 已登记;第二台同 tap 即拒绝,
# 且未起 VMM(launcher 只被调过一次)。
launches = []
ex.launcher = lambda cmd: launches.append(cmd) or FakeProc(True)
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "vm2"})
self.assertEqual(ctx.exception.kind, "net_tap_conflict")
self.assertEqual(launches, [])
self.assertTrue(any(e == "vm_failed"
for e, _, _, _ in self.audit))
# -- H2 密钥移出 argv ---------------------------------------------------
class SecretDeliveryTest(SecfixCase):
def test_secret_env_split_and_argv_refusal(self):
secrets, plain = split_secret_env(
{"SUB2API_API_KEY": "FAKE-KEY",
"SUB2API_BASE_URL": "http://gw/"})
self.assertEqual(secrets, {"SUB2API_API_KEY": "FAKE-KEY"})
self.assertEqual(plain, {"SUB2API_BASE_URL": "http://gw/"})
with self.assertRaises(ValueError):
build_ssh_command(2222, "/k", "root", None, "/kh",
["true"],
extra_env={"SUB2API_API_KEY": "FAKE-KEY"})
def test_seed_writes_single_write_files_with_guest_env(self):
seed_dir = os.path.join(self.dir, "seed")
write_seed(seed_dir, "chv-t1", "FAKE-PRIV", "ssh-ed25519 AAAAx",
secret_env={"SUB2API_API_KEY": "FAKE-KEY-123"})
text = ""
with open(os.path.join(seed_dir, "user-data")) as fh:
text = fh.read()
# 只能有一个 write_files 键(重复键会覆盖 host key 条目)。
self.assertEqual(text.count("write_files:"), 1)
self.assertIn("/etc/chv/guest-env", text)
self.assertIn("SUB2API_API_KEY=", text)
self.assertEqual(oct(os.stat(
os.path.join(seed_dir, "user-data")).st_mode & 0o777),
"0o600")
def test_keyscan_with_secret_fails_closed_before_launch(self):
ex = self.make_exec(
env_extra={"CHV_HOST_KEY_MODE": "keyscan"})
launches = []
ex.launcher = lambda cmd: launches.append(cmd) or FakeProc(True)
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "k1"},
extra_env={"SUB2API_API_KEY": "FAKE-KEY"})
self.assertEqual(ctx.exception.kind,
"secret_env_requires_provisioned")
self.assertEqual(launches, [])
def test_provisioned_wake_deletes_host_private_key(self):
ex = self.make_exec()
out = ex.wake_vm({"id": "s1"},
extra_env={"SUB2API_API_KEY": "FAKE-KEY"})
self.assertEqual(out["state"], "running")
vm_dir = os.path.join(self.state_dir, "chv-s1")
# 宿主私钥已删(seed 盘建好后即删);seed.iso 在且 0600;
# 密钥投递有审计(只含名,不含值)。
self.assertFalse(os.path.exists(
os.path.join(vm_dir, "ssh_host_ed25519_key")))
seed_iso = os.path.join(vm_dir, "seed.iso")
self.assertTrue(os.path.exists(seed_iso))
self.assertEqual(oct(os.stat(seed_iso).st_mode & 0o777),
"0o600")
self.assertTrue(any(e == "host_key_generated"
for e, _, _, _ in self.audit))
self.assertFalse(any("FAKE-KEY" in (d or "")
for _, _, d, _ in self.audit))
def test_seed_iso_build_sets_0600(self):
seed_dir = os.path.join(self.dir, "seed2")
os.makedirs(seed_dir, exist_ok=True)
out = os.path.join(self.dir, "seed2.iso")
build_seed_disk(seed_dir, out, runner=self._default_runner)
self.assertEqual(oct(os.stat(out).st_mode & 0o777), "0o600")
# -- H3 任务盘删除 + GC --------------------------------------------------
class DiskDeletionTest(SecfixCase):
def test_stop_deletes_task_disk_and_audits(self):
ex = self.make_exec()
disk = provision_task_disk(self.state_dir, "chv-d1",
self.rootfs, "copy")
self.assertTrue(os.path.exists(disk))
ex.stop_vm({"id": "d1"})
self.assertFalse(os.path.exists(disk))
self.assertTrue(any(e == "vm_disk_deleted"
for e, _, _, _ in self.audit))
def test_gc_deletes_orphan_disk_only(self):
ex = self.make_exec()
provision_task_disk(self.state_dir, "chv-orphan",
self.rootfs, "copy")
live_disk = provision_task_disk(self.state_dir, "chv-live",
self.rootfs, "copy")
# live VM 有 socket 文件(mock 占位,不代表活 VMM,
# 但 GC 的 live 判定含 socket 存在——见 gc_stale_disks
# 的 socket 判定,此处只验 orphan 被删、live 不动)。
os.makedirs(self.state_dir, exist_ok=True)
with open(os.path.join(self.state_dir, "chv-live.sock"),
"w") as fh:
fh.write("")
result = ex.gc_stale_disks()
self.assertIn("chv-orphan", result["deleted"])
self.assertTrue(os.path.exists(live_disk))
def test_disk_delete_failure_audited_and_marks_dirty(self):
ex = self.make_exec()
provision_task_disk(self.state_dir, "chv-f1",
self.rootfs, "copy")
from unittest import mock
with mock.patch("cloud.chv.executor.delete_task_disk",
side_effect=OSError("disk busy")):
ex.stop_vm({"id": "f1"})
self.assertTrue(any(e == "vm_disk_delete_failed"
for e, _, _, _ in self.audit))
self.assertTrue(os.path.exists(
dirty_marker_path(self.state_dir, "chv-f1")))
# -- H4 默认值 ------------------------------------------------------------
class DefaultsTest(unittest.TestCase):
def test_production_defaults(self):
cfg = build_chv_config({}, {})
self.assertEqual(cfg.host_key_mode, "provisioned")
self.assertNotEqual(cfg.state_dir, "/tmp/chv-vms")
self.assertTrue(cfg.state_dir.startswith(os.path.expanduser("~")))
self.assertEqual(cfg.disk_gb, 50.0)
self.assertEqual(cfg.snapshot_max_count, 10)
self.assertEqual(cfg.snapshot_max_total_gb, 20.0)
self.assertEqual(cfg.snapshot_retention_days, 30.0)
def test_keyscan_still_explicitly_selectable(self):
cfg = build_chv_config(
{}, {"CHV_HOST_KEY_MODE": "keyscan"})
self.assertEqual(cfg.host_key_mode, "keyscan")
def test_disk_gb_zero_or_negative_rejected(self):
with self.assertRaises(ValueError):
build_chv_config({}, {"CHV_DISK_GB": "0"})
with self.assertRaises(ValueError):
build_chv_config({}, {"CHV_DISK_GB": "-5"})
# -- H5 崩溃残留扫描 -------------------------------------------------------
class ResidueScanTest(SecfixCase):
def test_cleanup_stale_secrets_removes_residue(self):
ex = self.make_exec()
vm_dir = os.path.join(self.state_dir, "chv-old")
os.makedirs(os.path.join(vm_dir, "seed"), exist_ok=True)
with open(os.path.join(vm_dir, "ssh_host_ed25519_key"),
"w") as fh:
fh.write("FAKE-PRIV")
with open(os.path.join(vm_dir, "seed.iso"), "wb") as fh:
fh.write(b"CIDATA")
cleaned = ex.cleanup_stale_secrets()
self.assertIn("chv-old", cleaned)
self.assertFalse(os.path.exists(
os.path.join(vm_dir, "ssh_host_ed25519_key")))
self.assertFalse(os.path.exists(
os.path.join(vm_dir, "seed.iso")))
self.assertTrue(any(e == "host_secret_residue_cleaned"
for e, _, _, _ in self.audit))
# -- H6 state_dir 与 socket 权限 -------------------------------------------
class StateDirTest(SecfixCase):
def test_preflight_creates_state_dir_0700(self):
ex = self.make_exec()
self.assertTrue(ex.preflight())
mode = os.stat(self.state_dir).st_mode & 0o777
self.assertEqual(oct(mode), "0o700")
def test_ensure_secure_state_dir_fixes_loose_dir(self):
os.makedirs(self.state_dir, exist_ok=True)
os.chmod(self.state_dir, 0o755)
ensure_secure_state_dir(self.state_dir)
self.assertEqual(
oct(os.stat(self.state_dir).st_mode & 0o777), "0o700")
def test_group_writable_socket_fails_fast(self):
ex = self.make_exec()
os.makedirs(self.state_dir, exist_ok=True)
sock_path = os.path.join(self.state_dir, "chv-sock.sock")
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(sock_path)
srv.listen(1)
os.chmod(sock_path, 0o666)
try:
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "sock"})
self.assertEqual(ctx.exception.kind,
"socket_permissions_insecure")
finally:
srv.close()
def test_dead_socket_not_counted_live_socket_counted(self):
ex = self.make_exec(env_extra={"CHV_MAX_VMS": "1"})
os.makedirs(self.state_dir, exist_ok=True)
with open(os.path.join(self.state_dir, "chv-dead.sock"),
"w") as fh:
fh.write("")
self.assertEqual(ex._persistent_vm_count(), 0)
self.assertFalse(socket_is_live(
os.path.join(self.state_dir, "chv-dead.sock")))
srv = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
srv.bind(os.path.join(self.state_dir, "chv-live.sock"))
srv.listen(1)
try:
self.assertTrue(socket_is_live(
os.path.join(self.state_dir, "chv-live.sock")))
self.assertEqual(ex._persistent_vm_count(), 1)
finally:
srv.close()
# -- M1 远程转义 ------------------------------------------------------------
class RemoteQuotingTest(unittest.TestCase):
def test_metachars_round_trip_through_remote_shell(self):
inner = ["python3", "/app/host.py", "dispatch",
"--task", "a; rm -rf / $(whoami) 'q' \"d\""]
cmd = build_ssh_command(2222, "/k", "root", None, "/kh",
inner, ssh_host="10.0.0.5")
user_idx = cmd.index("root@10.0.0.5")
remote = cmd[user_idx + 1:]
# ssh 拼接后远程 shell 解析回来的恰好是本地 list。
self.assertEqual(shlex.split(" ".join(remote)), inner)
# 含元字符的 token 在 argv 里是被引用的单个元素,
# 不是裸的 shell 元字符。
task_token = [t for t in cmd if "whoami" in t]
self.assertEqual(len(task_token), 1)
self.assertTrue(task_token[0].startswith("'"))
# -- M2 脏标记拒起 ------------------------------------------------------------
class DirtyMarkerTest(SecfixCase):
def test_dirty_marker_refuses_provision_and_wake(self):
ex = self.make_exec()
vm_dir = os.path.join(self.state_dir, "chv-dirty")
os.makedirs(vm_dir, exist_ok=True)
with open(dirty_marker_path(self.state_dir, "chv-dirty"),
"w") as fh:
fh.write("cleanup failed\n")
with self.assertRaises(ChvFailure) as ctx:
ex.provision_host_key({"id": "dirty"})
self.assertEqual(ctx.exception.kind, "vm_dirty_refused")
with self.assertRaises(ChvFailure) as ctx2:
ex.wake_vm({"id": "dirty"})
self.assertEqual(ctx2.exception.kind, "vm_dirty_refused")
# -- M3 快照上限 ---------------------------------------------------------------
class SnapshotLimitTest(SecfixCase):
def _snap_file(self, name, size=64):
path = os.path.join(self.state_dir, name)
os.makedirs(self.state_dir, exist_ok=True)
with open(path, "wb") as fh:
fh.write(b"S" * size)
return "file://" + path
def test_snapshot_count_limit_refused_before_chremote(self):
ex = self.make_exec(env_extra={"CHV_SNAPSHOT_MAX_COUNT": "1"})
url1 = self._snap_file("s1.snap")
record_snapshot(self.state_dir, "chv-q", url1, 20.0)
calls_before = len(self.calls)
with self.assertRaises(ChvFailure) as ctx:
ex.snapshot_vm({"id": "q"}, self._snap_file("s2.snap"))
self.assertEqual(ctx.exception.kind, "snapshot_quota_exceeded")
# 上限在 ch-remote 之前拒绝,runner 未被调新命令。
self.assertEqual(len(self.calls), calls_before)
def test_snapshot_retention_prunes_expired(self):
import time
ex = self.make_exec()
old_url = self._snap_file("old.snap")
entries = [{"url": old_url,
"path": old_url[len("file://"):],
"size": 64,
"created_at": time.time() - 40 * 86400}]
import json as _json
vm_dir = os.path.join(self.state_dir, "chv-r")
os.makedirs(vm_dir, exist_ok=True)
with open(os.path.join(vm_dir, "snapshots.json"), "w") as fh:
_json.dump(entries, fh)
new_url = self._snap_file("new.snap")
ex.snapshot_vm({"id": "r"}, new_url)
self.assertFalse(os.path.exists(old_url[len("file://"):]))
self.assertTrue(os.path.exists(new_url[len("file://"):]))
def test_snapshot_total_cap_prunes_oldest(self):
ex = self.make_exec(
env_extra={"CHV_SNAPSHOT_MAX_TOTAL_GB": "0.0000001"})
# 总量帽约 107 字节:两块 64 字节快照登记后,旧的被剪。
url1 = self._snap_file("t1.snap")
record_snapshot(self.state_dir, "chv-t", url1, 20.0)
url2 = self._snap_file("t2.snap")
_entries, removed = record_snapshot(
self.state_dir, "chv-t", url2, 0.0000001)
self.assertEqual(len(removed), 1)
self.assertFalse(os.path.exists(url1[len("file://"):]))
# -- M4/M8 构建脚本 --------------------------------------------------------------
class BuildScriptTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = self.tmp.name
self.bin_dir = os.path.join(self.dir, "bin")
os.makedirs(os.path.join(self.bin_dir, "codex-resources"))
for name in ("codex", "codex-code-mode-host"):
p = os.path.join(self.bin_dir, name)
with open(p, "w") as fh:
fh.write("#!/bin/sh\n")
os.chmod(p, 0o755)
self.repo = os.path.join(self.dir, "repo")
os.makedirs(os.path.join(self.repo, "cloud"))
with open(os.path.join(self.repo, "host.py"), "w") as fh:
fh.write("# host\n")
self.script = str(Path(__file__).resolve().parents[2]
/ "chv-guest" / "build.sh")
def tearDown(self):
self.tmp.cleanup()
def _run(self, args, env_extra=None):
env = dict(os.environ)
env.update({"CODEX_BIN_DIR": self.bin_dir,
"UBUNTU_VERSION": "24.04",
"CODEX_VERSION": "0.164.0-alpha.9",
"REPO_COMMIT": "test123",
"REPO_DIR": self.repo})
if env_extra:
env.update(env_extra)
return subprocess.run(["bash", self.script] + args,
capture_output=True, text=True, env=env)
def test_dry_run_requires_sha_and_prints_sshd_hardening(self):
out_dir = os.path.join(self.dir, "out")
r = self._run(["--dry-run", out_dir])
self.assertNotEqual(r.returncode, 0)
self.assertIn("UBUNTU_IMAGE_SHA256", r.stderr)
r2 = self._run(["--dry-run", out_dir],
env_extra={"UBUNTU_IMAGE_SHA256": "0" * 64})
self.assertEqual(r2.returncode, 0, r2.stderr)
self.assertIn("99-chv.conf", r2.stdout)
self.assertIn("PasswordAuthentication no", r2.stdout)
self.assertIn("sshd -t", r2.stdout)
self.assertIn("virt-cat", r2.stdout)
# -- M5 审计补齐 ------------------------------------------------------------------
class AuditCoverageTest(SecfixCase):
def test_host_key_mismatch_audited_and_fail_fast(self):
ex = self.make_exec(
env_extra={"CHV_HOST_KEY_MODE": "keyscan"})
kh = ex.known_hosts_path({"id": "m1"})
os.makedirs(os.path.dirname(kh), exist_ok=True)
with open(kh, "w") as fh:
fh.write("[127.0.0.1]:2222 ssh-ed25519 AAAAfake\n")
def bad_ssh(cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh":
return FakeResult(
255, "",
"Host key verification failed.\n"
"REMOTE HOST IDENTIFICATION HAS CHANGED\n")
return FakeResult(0, "", "")
ex.runner = bad_ssh
with self.assertRaises(ChvFailure) as ctx:
ex.wait_guest_ready({"id": "m1"})
self.assertEqual(ctx.exception.kind, "host_key_mismatch")
self.assertTrue(any(e == "host_key_mismatch"
for e, _, _, _ in self.audit))
def test_preflight_failure_is_audited(self):
ex = self.make_exec(binary="/tmp/no-such-chv-binary")
with self.assertRaises(ChvFailure):
ex.preflight()
self.assertTrue(any(e == "preflight_failed"
for e, _, _, _ in self.audit))
def test_audit_callback_failure_does_not_break_task(self):
ex = self.make_exec()
ex.audit_callback = lambda *args: (_ for _ in ()).throw(
RuntimeError("audit store down"))
# _emit 只记宿主 error 日志,不抛异常。
ex._emit("vm_create", {"id": "t"}, "vm=chv-t")
def test_capture_success_is_audited(self):
ex = self.make_exec(
env_extra={"CHV_HOST_KEY_MODE": "keyscan"})
def keyscan_runner(cmd, timeout=None):
self.calls.append(list(cmd))
if cmd and cmd[0] == "ssh-keyscan":
return FakeResult(
0, "[127.0.0.1]:2222 ssh-ed25519 AAAAfake1\n", "")
return FakeResult(0, "", "")
ex.runner = keyscan_runner
self.assertTrue(ex.capture_host_key({"id": "cap1"}))
self.assertTrue(any(e == "host_key_captured"
for e, _, _, _ in self.audit))
# -- M6 reuse 租户声明 ---------------------------------------------------------------
class ReuseTenantTest(SecfixCase):
def test_reuse_without_tenant_declaration_refused(self):
ex = self.make_exec(env_extra={
"CHV_DISK_MODE": "reuse",
"CHV_REUSE_SINGLE_TENANT_EXPERIMENT": "1"})
with self.assertRaises(ChvFailure) as ctx:
ex.preflight()
self.assertEqual(ctx.exception.kind, "reuse_tenant_undeclared")
def test_reuse_with_declaration_passes_preflight(self):
ex = self.make_exec(env_extra={
"CHV_DISK_MODE": "reuse",
"CHV_REUSE_SINGLE_TENANT_EXPERIMENT": "1",
"CHV_SINGLE_TENANT_PROJECT": "p1"})
self.assertTrue(ex.preflight())
# -- L1/L2/L3/L4 -----------------------------------------------------------------------
class LowItemsTest(SecfixCase):
def test_task_disk_symlink_not_followed(self):
target = os.path.join(self.dir, "victim.bin")
with open(target, "wb") as fh:
fh.write(b"VICTIM")
vm_dir = os.path.join(self.state_dir, "chv-l1")
os.makedirs(vm_dir, exist_ok=True)
os.symlink(target, os.path.join(vm_dir, "disk.raw"))
disk = provision_task_disk(self.state_dir, "chv-l1",
self.rootfs, "copy")
self.assertFalse(os.path.islink(disk))
with open(disk, "rb") as fh:
self.assertEqual(fh.read(), b"R" * 100)
with open(target, "rb") as fh:
self.assertEqual(fh.read(), b"VICTIM")
def test_snapshot_url_outside_allowed_dir_refused(self):
with self.assertRaises(ChvFailure) as ctx:
validate_snapshot_url("file:///etc/passwd",
self.state_dir)
self.assertEqual(ctx.exception.kind,
"snapshot_path_not_allowed")
def test_remote_snapshot_requires_explicit_flag(self):
with self.assertRaises(ChvFailure) as ctx:
validate_snapshot_url("s3://bucket/snap",
self.state_dir)
self.assertEqual(ctx.exception.kind,
"snapshot_remote_not_allowed")
self.assertIsNone(validate_snapshot_url(
"s3://bucket/snap", self.state_dir, allow_remote=True))
def test_capacity_failure_is_audited(self):
ex = self.make_exec(env_extra={"CHV_MAX_VMS": "0"})
with self.assertRaises(ChvFailure) as ctx:
ex.wake_vm({"id": "cap"})
self.assertEqual(ctx.exception.kind, "host_capacity_exceeded")
self.assertTrue(any(e == "vm_failed" and k == "host_capacity_exceeded"
for e, _, _, k in self.audit))
def test_dual_net_vars_warn_but_pass(self):
ex = self.make_exec(env_extra={
"CHV_NET": "tap=tap0",
"CHV_NET_JSON": '[{"tap": "tap0"}]'})
self.assertTrue(ex.preflight())
self.assertTrue(any(e == "net_config_dual"
for e, _, _, _ in self.audit))
def test_warm_pool_acquire_doc_defines_task_disk_policy(self):
# L5 是文档项(接线仍未做):acquire 文档必须写清
# 预热盘与任务盘的对应关系,不假装已接。
self.assertIn("provision_task_disk", WarmPool.acquire.__doc__)
if __name__ == "__main__":
unittest.main()