8852e6d0ca
之前禁止所有 shell 操作符,导致 crictl ps -a|tail / df -h | grep vda 这类常用诊断管道无法使用。 改动: 1. FORBIDDEN_TOKENS 移除了 | > >> $,保留 ; || && < << ` 2. 新增 _has_shell_operators() 检测命令是否需要 shell 执行 3. 新增 _pipe_to_dangerous_target() 阻止管道后执行 sh/bash/rm 等危险命令 4. classify_shell_command 对含管道命令: 只检查管道前 binary 白名单, 子命令不做限制(管道后工具多样,精确分类无意义) 5. execute_command 对 needs_shell=True 的命令用 create_subprocess_shell 执行 6. AI Prompt 第1条安全规则改为允许管道和重定向 7. 分号仍然禁止;cat 不安全路径检查对管道场景同样生效
202 lines
8.0 KiB
Python
202 lines
8.0 KiB
Python
import unittest
|
|
|
|
from backend.agent_tools import (
|
|
classify_agent_command,
|
|
classify_kubectl_command,
|
|
classify_shell_command,
|
|
parse_kubectl_command,
|
|
parse_shell_command,
|
|
)
|
|
|
|
|
|
class KubectlCommandPolicyTest(unittest.TestCase):
|
|
def test_allows_read_only_kubectl_command_for_autonomous_execution(self):
|
|
decision = classify_kubectl_command("kubectl get pods -n production -o wide")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_requires_approval_for_mutating_kubectl_command(self):
|
|
decision = classify_kubectl_command("kubectl rollout restart deployment/api -n production")
|
|
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_rejects_shell_operators_and_non_kubectl_commands(self):
|
|
# 分号 ; 仍被拒绝
|
|
with self.assertRaises(ValueError):
|
|
parse_kubectl_command("kubectl get pods; rm -rf /")
|
|
# 管道后的 sh/bash 等危险命令仍被拒绝
|
|
with self.assertRaises(ValueError):
|
|
parse_kubectl_command("kubectl get pods | sh")
|
|
# 非 kubectl 命令仍被拒绝
|
|
for command in (
|
|
"bash -c kubectl get pods",
|
|
"sudo kubectl get pods",
|
|
):
|
|
with self.subTest(command=command):
|
|
with self.assertRaises(ValueError):
|
|
parse_kubectl_command(command)
|
|
|
|
def test_rejects_exec_even_when_command_looks_read_only(self):
|
|
decision = classify_kubectl_command("kubectl exec api-0 -- cat /etc/hosts")
|
|
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
|
|
class ShellCommandPolicyTest(unittest.TestCase):
|
|
def test_allows_read_only_systemctl_status_for_autonomous_execution(self):
|
|
decision = classify_shell_command("systemctl status kubelet")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_allows_journalctl_with_flags(self):
|
|
decision = classify_shell_command("journalctl -u kubelet -n 100 --no-pager")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_allows_crictl_ps(self):
|
|
decision = classify_shell_command("crictl ps -a")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_allows_df_and_free(self):
|
|
for command in ("df -h", "free -m", "uptime"):
|
|
with self.subTest(command=command):
|
|
decision = classify_shell_command(command)
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_allows_ip_addr(self):
|
|
decision = classify_shell_command("ip addr show")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_requires_approval_for_systemctl_restart(self):
|
|
decision = classify_shell_command("systemctl restart kubelet")
|
|
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_rejects_unknown_binary(self):
|
|
with self.assertRaises(ValueError):
|
|
classify_shell_command("rm -rf /")
|
|
|
|
def test_rejects_shell_operators_in_shell_command(self):
|
|
# 分号 ; 仍被拒绝
|
|
with self.assertRaises(ValueError):
|
|
parse_shell_command("systemctl status kubelet; rm -rf /")
|
|
# 管道 | 和重定向 > 现在允许通过分类器(server 端会用 shell=True 执行)
|
|
decision = classify_shell_command("df -h | grep vda")
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
self.assertTrue(decision.needs_shell)
|
|
decision = classify_shell_command("journalctl -u kubelet > /tmp/log")
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
self.assertTrue(decision.needs_shell)
|
|
|
|
def test_cat_only_allows_safe_paths(self):
|
|
decision = classify_shell_command("cat /etc/os-release")
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_cat_rejects_unsafe_path(self):
|
|
with self.assertRaises(ValueError):
|
|
classify_shell_command("cat /etc/shadow")
|
|
|
|
|
|
class EtcdCommandPolicyTest(unittest.TestCase):
|
|
def test_etcdctl_endpoint_status_is_read_only(self):
|
|
decision = classify_shell_command("etcdctl endpoint status")
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_etcdctl_with_global_flags_still_classifies(self):
|
|
decision = classify_shell_command(
|
|
"etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/ssl/ca.pem endpoint health"
|
|
)
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_etcdctl_member_list_is_read_only(self):
|
|
decision = classify_shell_command("etcdctl member list")
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_etcdctl_get_and_version_are_read_only(self):
|
|
for command in ("etcdctl get /kubernetes", "etcdctl version", "etcdctl alarm list"):
|
|
with self.subTest(command=command):
|
|
decision = classify_shell_command(command)
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_etcdctl_alarm_list_is_read_only(self):
|
|
decision = classify_shell_command("etcdctl alarm list")
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertFalse(decision.requires_approval)
|
|
|
|
def test_etcdctl_put_requires_approval(self):
|
|
decision = classify_shell_command("etcdctl put key value")
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_etcdctl_member_remove_requires_approval(self):
|
|
decision = classify_shell_command("etcdctl member remove abc123")
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_etcdctl_snapshot_save_requires_approval(self):
|
|
decision = classify_shell_command("etcdctl snapshot save /tmp/etcd.db")
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_etcdctl_compact_requires_approval(self):
|
|
decision = classify_shell_command("etcdctl compact 12345")
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_etcdctl_auth_enable_requires_approval(self):
|
|
decision = classify_shell_command("etcdctl auth enable")
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_etcdctl_rejects_unknown_subcommand(self):
|
|
with self.assertRaises(ValueError):
|
|
classify_shell_command("etcdctl bogus command")
|
|
|
|
|
|
class AgentCommandDispatchTest(unittest.TestCase):
|
|
def test_dispatches_kubectl_command_to_kubectl_classifier(self):
|
|
decision = classify_agent_command("kubectl get pods -A")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertEqual(decision.verb, "get")
|
|
|
|
def test_dispatches_shell_command_to_shell_classifier(self):
|
|
decision = classify_agent_command("systemctl status kubelet")
|
|
|
|
self.assertEqual(decision.mode, "read")
|
|
self.assertEqual(decision.verb, "systemctl")
|
|
|
|
def test_dispatches_write_shell_command_to_shell_classifier(self):
|
|
decision = classify_agent_command("systemctl restart kubelet")
|
|
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
def test_dispatches_write_kubectl_command(self):
|
|
decision = classify_agent_command("kubectl delete pod api-0 -n prod")
|
|
|
|
self.assertEqual(decision.mode, "write")
|
|
self.assertTrue(decision.requires_approval)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|