"""VPS Agent 采集模块 采集本机资源、服务器信息与基础安全状态。 依赖 psutil + 少量系统命令(ufw/iptables/sshd_config)。 """ import platform import socket import subprocess import time import psutil def collect_metrics() -> dict: """资源使用率(CPU/内存/磁盘/网络/负载/运行时长)""" cpu = psutil.cpu_percent(interval=1) mem = psutil.virtual_memory() disk = psutil.disk_usage("/") net = psutil.net_io_counters() try: load_1m = psutil.getloadavg()[0] except (AttributeError, OSError): load_1m = None return { "cpu_pct": round(cpu, 1), "mem_pct": round(mem.percent, 1), "disk_pct": round(disk.percent, 1), "net_in_mb": round(net.bytes_recv / 1024 / 1024, 2), "net_out_mb": round(net.bytes_sent / 1024 / 1024, 2), "load_1m": round(load_1m, 2) if load_1m is not None else None, "uptime_sec": int(time.time() - psutil.boot_time()), } def _get_ip() -> str: """获取本机出口 IP(不实际发包)""" try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception: return "" def collect_server_info() -> dict: """服务器基础信息""" os_name = platform.system() try: with open("/etc/os-release") as f: for line in f: if line.startswith("PRETTY_NAME="): os_name = line.split("=", 1)[1].strip().strip('"') break except Exception: pass return { "hostname": socket.gethostname(), "os": os_name, "kernel": platform.release(), "cpu_cores": psutil.cpu_count(), "mem_total_gb": round(psutil.virtual_memory().total / 1024**3, 1), "disk_total_gb": round(psutil.disk_usage("/").total / 1024**3, 1), "public_ip": _get_ip(), "status": "online", } def _check_ssh_config() -> dict: """检查 SSH 配置(root 登录 / 密码登录)""" path = "/etc/ssh/sshd_config" permit_root = "unknown" password_auth = "unknown" try: with open(path) as f: for line in f: low = line.strip().lower() if low.startswith("permitrootlogin"): parts = low.split() permit_root = parts[1] if len(parts) > 1 else "unknown" elif low.startswith("passwordauthentication"): parts = low.split() password_auth = parts[1] if len(parts) > 1 else "unknown" except Exception: return {"check_item": "ssh_config", "status": "unknown", "detail": "无法读取 sshd_config", "suggestion": None} status = "pass" suggestions = [] if permit_root == "yes": status = "warn" suggestions.append("设置 PermitRootLogin no 或 prohibit-password") if password_auth == "yes": status = "warn" suggestions.append("关闭密码登录 PasswordAuthentication no,改用密钥") return { "check_item": "ssh_config", "status": status, "detail": f"PermitRootLogin={permit_root}, PasswordAuthentication={password_auth}", "suggestion": ";".join(suggestions) if suggestions else None, } def _check_firewall() -> dict: """检查防火墙状态""" try: out = subprocess.run(["ufw", "status"], capture_output=True, text=True, timeout=5) if "Status: active" in out.stdout: return {"check_item": "firewall", "status": "pass", "detail": "ufw 已启用", "suggestion": None} return {"check_item": "firewall", "status": "warn", "detail": "ufw 未启用", "suggestion": "启用防火墙 ufw enable"} except Exception: try: out = subprocess.run(["iptables", "-L", "-n"], capture_output=True, text=True, timeout=5) if out.stdout.strip(): return {"check_item": "firewall", "status": "pass", "detail": "iptables 有规则", "suggestion": None} except Exception: pass return {"check_item": "firewall", "status": "unknown", "detail": "未检测到 ufw/iptables", "suggestion": None} def _check_listening_ports() -> dict: """统计监听端口""" try: conns = psutil.net_connections(kind="inet") listening = sorted({c.laddr.port for c in conns if c.status == "LISTEN"}) detail = f"开放端口 {len(listening)} 个: {','.join(map(str, listening[:15]))}" status = "pass" if len(listening) < 10 else "warn" suggestion = "端口偏多,建议关闭非必要端口" if status == "warn" else None return {"check_item": "listening_ports", "status": status, "detail": detail, "suggestion": suggestion} except Exception as e: # noqa: BLE001 return {"check_item": "listening_ports", "status": "unknown", "detail": str(e), "suggestion": None} def collect_security() -> list: """基础安全检查项""" return [_check_ssh_config(), _check_firewall(), _check_listening_ports()]