test_shell_lifecycle.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import os
  2. import pathlib
  3. import shutil
  4. import subprocess
  5. import tempfile
  6. import unittest
  7. PROJECT_ROOT = pathlib.Path(__file__).resolve().parent.parent
  8. class TestShellLifecycleP0P1Bugs(unittest.TestCase):
  9. """测试并锁定 Shell 部署与发布脚本中的高危安全及逻辑漏洞"""
  10. def setUp(self):
  11. self.tmp_dir = tempfile.TemporaryDirectory()
  12. self.work_dir = pathlib.Path(self.tmp_dir.name)
  13. def tearDown(self):
  14. self.tmp_dir.cleanup()
  15. def test_p0_uninstall_debian_empty_service_name_deletes_var_lib(self):
  16. """[P0/P2 修复验证] 验证 uninstall_debian.sh 传空服务名立即报错退出,绝不执行 rm -rf /var/lib/"""
  17. mock_bin = self.work_dir / "bin"
  18. mock_bin.mkdir()
  19. # Mock id 绕过 root 检测
  20. (mock_bin / "id").write_text("#!/bin/sh\necho 0\n")
  21. # Mock systemctl
  22. (mock_bin / "systemctl").write_text("#!/bin/sh\nexit 0\n")
  23. # Mock rm 记录所有删除操作
  24. rm_log = self.work_dir / "rm_calls.log"
  25. (mock_bin / "rm").write_text(f"#!/bin/sh\necho \"rm $*\" >> {rm_log}\n")
  26. for p in mock_bin.iterdir():
  27. p.chmod(0o755)
  28. script_path = PROJECT_ROOT / "scripts" / "uninstall_debian.sh"
  29. env = os.environ.copy()
  30. env["PATH"] = f"{mock_bin}:{env.get('PATH', '')}"
  31. # 传入非法空参数 --service-name ""
  32. proc = subprocess.run(
  33. ["bash", str(script_path), "--service-name", ""],
  34. env=env,
  35. capture_output=True,
  36. text=True,
  37. cwd=str(self.work_dir),
  38. )
  39. self.assertNotEqual(proc.returncode, 0, "空服务名必须报错退出")
  40. rm_content = rm_log.read_text() if rm_log.exists() else ""
  41. self.assertNotIn("rm -rf /var/lib/", rm_content, "修复验证确认:绝不执行 rm -rf /var/lib/")
  42. def test_p0_build_router_package_trap_prematurely_deletes_tmp_dir(self):
  43. """[P0/P2 修复验证] 验证 build_router_package.sh 延迟注册 trap,遇到前置报错绝不误删已有目录"""
  44. victim_dir = self.work_dir / "victim_data_dir"
  45. victim_dir.mkdir()
  46. canary_file = victim_dir / "data.txt"
  47. canary_file.write_text("critical data")
  48. script_path = PROJECT_ROOT / "build_router_package.sh"
  49. env = os.environ.copy()
  50. env["TMP_DIR"] = str(victim_dir)
  51. env["ROUTER_CONF"] = "/nonexistent/path/config_router.conf"
  52. # 执行打包脚本,因缺少配置文件而必然报错退出
  53. proc = subprocess.run(
  54. ["sh", str(script_path)],
  55. env=env,
  56. capture_output=True,
  57. text=True,
  58. cwd=str(self.work_dir),
  59. )
  60. self.assertNotEqual(proc.returncode, 0)
  61. # 验证修复成功:victim_dir 完好无损,没有被提前的 trap cleanup 误删
  62. self.assertTrue(victim_dir.exists(), "修复验证确认:build_router_package.sh 未误删 victim_dir")
  63. def test_p1_run_update_and_commit_passes_token_in_env_not_argv(self):
  64. """[P1 修复验证] 验证 run_update_and_commit.sh 采用 GIT_CONFIG_* 环境变量传递 Token,argv 零泄露"""
  65. script_path = PROJECT_ROOT / "scripts" / "run_update_and_commit.sh"
  66. script_content = script_path.read_text()
  67. # 确认彻底移除了命令行 -c "http.extraHeader=..."
  68. self.assertNotIn('git_auth_opts=(-c "http.extraHeader', script_content, "Token 严禁出现在命令行 argv 参数中")
  69. self.assertIn("GIT_CONFIG_COUNT=1", script_content, "使用 Git 原生配置环境变量安全传递")
  70. self.assertIn("GIT_CONFIG_KEY_0=http.extraHeader", script_content)
  71. self.assertIn("GIT_CONFIG_VALUE_0=Authorization: Basic", script_content)
  72. def test_p1_run_update_and_commit_ls_remote_distinguishes_error_code(self):
  73. """[P1 修复验证] 验证 run_update_and_commit.sh 明确区分 ls-remote 退出码 2 与 128"""
  74. script_path = PROJECT_ROOT / "scripts" / "run_update_and_commit.sh"
  75. script_content = script_path.read_text()
  76. self.assertIn("ls_remote_rc", script_content, "已捕获并校验 ls-remote 退出码")
  77. self.assertIn("$ls_remote_rc -eq 2", script_content, "显式匹配 2 作为分支不存在")
  78. self.assertIn("fatal: git ls-remote failed", script_content, "非 0/2 致命错误直接报错退出,决不误建孤儿分支")
  79. def test_p1_run_update_and_commit_fallback_silence(self):
  80. """[P1 修复验证] 验证 run_update_and_commit.sh 在回退状态且值未变时执行回退静默"""
  81. script_path = PROJECT_ROOT / "scripts" / "run_update_and_commit.sh"
  82. script_content = script_path.read_text()
  83. self.assertIn('is_fallback="0"', script_content)
  84. self.assertIn('"error_use_last_good"', script_content)
  85. self.assertIn('is_fallback="1"', script_content)
  86. self.assertIn('"$is_fallback" == "1"', script_content)
  87. if __name__ == "__main__":
  88. unittest.main()