use std::path::Path;
use std::process::Command;
use crate::domain::audit::{AuditItem, AuditReport, AuditStatus, PackageManager};
use crate::domain::errors::DomainError;
pub fn run_cmd(program: &str, args: &[&str]) -> Result<String, DomainError> {
let output = Command::new(program)
.args(args)
.output()
.map_err(|e| DomainError::SystemCommandFailed(format!("无法执行 {program}: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(DomainError::SystemCommandFailed(format!(
"{program} 返回非零退出码: {stderr}"
)));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub fn detect_is_root() -> bool {
run_cmd("id", &["-u"])
.map(|uid| uid.trim() == "0")
.unwrap_or(false)
}
pub fn detect_package_manager() -> PackageManager {
if which("apt") {
PackageManager::Apt
} else if which("yum") {
PackageManager::Yum
} else if which("dnf") {
PackageManager::Dnf
} else {
PackageManager::Unknown
}
}
pub fn which(cmd: &str) -> bool {
Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn sshd_config_get(key: &str) -> Option<String> {
let path = Path::new("/etc/ssh/sshd_config");
let content = std::fs::read_to_string(path).ok()?;
let mut value = None;
for line in content.lines() {
let line = line.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
if let Some(stripped) = line.strip_prefix(key)
&& (stripped.starts_with(' ') || stripped.starts_with('\t'))
{
value = Some(stripped.trim().to_string());
}
}
value
}
pub fn detect_ssh_port() -> u16 {
sshd_config_get("Port")
.and_then(|v| v.parse().ok())
.unwrap_or(22)
}
pub fn detect_password_auth_disabled() -> bool {
let password = sshd_config_get("PasswordAuthentication")
.map(|v| v.eq_ignore_ascii_case("no"))
.unwrap_or(false);
let challenge = sshd_config_get("ChallengeResponseAuthentication")
.map(|v| v.eq_ignore_ascii_case("no"))
.unwrap_or(false);
password && challenge
}
pub fn detect_root_login_disabled() -> bool {
sshd_config_get("PermitRootLogin")
.map(|v| v.eq_ignore_ascii_case("no") || v.eq_ignore_ascii_case("prohibit-password"))
.unwrap_or(false)
}
pub fn detect_sudo_users() -> Vec<String> {
let output = Command::new("getent")
.args(["group", "sudo"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
let s = String::from_utf8_lossy(&o.stdout).to_string();
Some(s)
} else {
None
}
})
.or_else(|| {
Command::new("getent")
.args(["group", "wheel"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
Some(String::from_utf8_lossy(&o.stdout).to_string())
} else {
None
}
})
});
match output {
Some(line) => {
if let Some(colon_pos) = line.rfind(':') {
let after = &line[colon_pos + 1..].trim();
if after.is_empty() {
return vec![];
}
let users: Vec<String> = after
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
users
.into_iter()
.filter(|u| {
let uid = get_user_uid(u);
uid >= 1000
})
.collect()
} else {
vec![]
}
}
None => vec![],
}
}
fn get_user_uid(username: &str) -> u32 {
Command::new("id")
.args(["-u", username])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
let s = String::from_utf8_lossy(&o.stdout);
s.trim().parse().ok()
} else {
None
}
})
.unwrap_or(0)
}
pub fn detect_fail2ban_installed() -> bool {
which("fail2ban-server")
}
pub fn detect_ufw_enabled() -> bool {
let output = Command::new("ufw")
.arg("status")
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string());
match output {
Some(s) => s.contains("active") || s.contains("Status: active"),
None => false,
}
}
pub fn detect_auto_updates_enabled() -> bool {
let systemd = Command::new("systemctl")
.args(["is-enabled", "unattended-upgrades"])
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if systemd {
return true;
}
let path = Path::new("/etc/apt/apt.conf.d/20auto-upgrades");
if path.exists()
&& let Ok(content) = std::fs::read_to_string(path)
{
return content.contains("APT::Periodic::Update-Package-Lists \"1\"")
|| content.contains("APT::Periodic::Unattended-Upgrade \"1\"");
}
false
}
pub fn detect_system_up_to_date() -> bool {
let cache_paths = ["/var/lib/apt/lists", "/var/cache/apt/pkgcache.bin"];
for path_str in &cache_paths {
let path = Path::new(path_str);
if let Ok(metadata) = path.metadata()
&& let Ok(modified) = metadata.modified()
&& let Ok(elapsed) = modified.elapsed()
{
return elapsed.as_secs() < 604800;
}
}
false
}
pub fn run_full_audit() -> AuditReport {
let is_root = detect_is_root();
let package_manager = detect_package_manager();
let ssh_port = detect_ssh_port();
let password_auth_disabled = detect_password_auth_disabled();
let root_login_disabled = detect_root_login_disabled();
let sudo_users = detect_sudo_users();
let fail2ban_installed = detect_fail2ban_installed();
let ufw_enabled = detect_ufw_enabled();
let auto_updates_enabled = detect_auto_updates_enabled();
let system_up_to_date = detect_system_up_to_date();
let mut items = vec![];
items.push(if is_root {
AuditItem::safe("当前用户权限", "已以 root 运行".into())
} else {
AuditItem::missing("当前用户权限", "非 root 用户,需要 root 权限".into())
});
items.push(AuditItem {
name: "包管理器",
status: AuditStatus::Safe,
detail: format!("检测到 {}", package_manager.name()),
});
items.push(if ssh_port != 22 {
AuditItem::safe("SSH 端口", format!("已自定义为 {ssh_port}"))
} else {
AuditItem::missing("SSH 端口", "默认端口 22".into())
});
items.push(if password_auth_disabled {
AuditItem::safe("密码登录", "已禁用".into())
} else {
AuditItem::missing("密码登录", "密码登录未禁用".into())
});
items.push(if root_login_disabled {
AuditItem::safe("root 登录", "已禁止".into())
} else {
AuditItem::missing("root 登录", "root 登录未禁止".into())
});
items.push(if sudo_users.is_empty() {
AuditItem::missing("sudo 用户", "未检测到非 root 管理用户".into())
} else {
AuditItem::safe("sudo 用户", format!("已存在: {}", sudo_users.join(", ")))
});
items.push(if fail2ban_installed {
AuditItem::safe("Fail2ban", "已安装".into())
} else {
AuditItem::missing("Fail2ban", "未安装".into())
});
items.push(if ufw_enabled {
AuditItem::safe("UFW 防火墙", "已启用".into())
} else {
AuditItem::missing("UFW 防火墙", "未启用".into())
});
items.push(if auto_updates_enabled {
AuditItem::safe("自动安全更新", "已启用".into())
} else {
AuditItem::missing("自动安全更新", "未启用".into())
});
items.push(if system_up_to_date {
AuditItem::safe("系统更新状态", "缓存未过期".into())
} else {
AuditItem::needs_update("系统更新状态", "缓存已过期,建议更新".into())
});
AuditReport {
items,
is_root,
package_manager,
ssh_port,
password_auth_disabled,
root_login_disabled,
sudo_users,
fail2ban_installed,
ufw_enabled,
auto_updates_enabled,
system_up_to_date,
}
}
pub fn create_system_user(username: &str) -> Result<(), DomainError> {
let output = Command::new("useradd")
.args(["-m", "-s", "/bin/bash", username])
.output()
.map_err(|e| DomainError::SystemCommandFailed(format!("useradd 失败: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(DomainError::SystemCommandFailed(format!(
"useradd 失败: {stderr}"
)));
}
let _ = Command::new("usermod")
.args(["-aG", "sudo", username])
.output();
let _ = Command::new("mkdir")
.args(["-p", &format!("/home/{username}/.ssh")])
.output();
let _ = Command::new("chown")
.args([
"-R",
&format!("{username}:{username}"),
&format!("/home/{username}/.ssh"),
])
.output();
let _ = Command::new("chmod")
.args(["700", &format!("/home/{username}/.ssh")])
.output();
Ok(())
}
pub fn lock_user_password(username: &str) -> Result<(), DomainError> {
let output = Command::new("passwd")
.args(["-l", username])
.output()
.map_err(|e| DomainError::SystemCommandFailed(format!("passwd -l 失败: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(DomainError::SystemCommandFailed(format!(
"锁定密码失败: {stderr}"
)));
}
Ok(())
}
pub fn generate_ssh_keypair(username: &str) -> Result<String, DomainError> {
let home = if username == "root" {
"/root".to_string()
} else {
format!("/home/{username}")
};
let key_path = format!("{home}/.ssh/id_ed25519");
let pub_key_path = format!("{key_path}.pub");
let output = Command::new("ssh-keygen")
.args([
"-t", "ed25519", "-f", &key_path, "-N", "", "-q",
])
.output()
.map_err(|e| DomainError::SystemCommandFailed(format!("ssh-keygen 失败: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(DomainError::SystemCommandFailed(format!(
"ssh-keygen 失败: {stderr}"
)));
}
let _ = Command::new("chown")
.args([&format!("{username}:{username}"), &key_path, &pub_key_path])
.output();
Ok(pub_key_path)
}
pub fn add_authorized_key(username: &str, pub_key: &str) -> Result<(), DomainError> {
let home = if username == "root" {
"/root".to_string()
} else {
format!("/home/{username}")
};
let ssh_dir = format!("{home}/.ssh");
let auth_keys = format!("{ssh_dir}/authorized_keys");
let _ = Command::new("mkdir").args(["-p", &ssh_dir]).output();
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&auth_keys)
.map_err(|e| DomainError::SystemCommandFailed(format!("打开 authorized_keys 失败: {e}")))?;
writeln!(file, "{}", pub_key)
.map_err(|e| DomainError::SystemCommandFailed(format!("追加公钥失败: {e}")))?;
let _ = Command::new("chmod").args(["600", &auth_keys]).output();
let _ = Command::new("chown")
.args([&format!("{username}:{username}"), &auth_keys])
.output();
Ok(())
}
pub fn random_suggested_port() -> u16 {
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(42);
let base = 1024 + (seed % 64511) as u16;
let common_ports = [22, 80, 443, 3306, 5432, 6379, 8080, 8443];
if common_ports.contains(&base) {
((base as u32 + 100) % 64511 + 1024) as u16
} else {
base
}
}
pub fn get_key_fingerprint(username: &str) -> Option<String> {
let home = if username == "root" {
"/root".to_string()
} else {
format!("/home/{username}")
};
let auth_keys = format!("{home}/.ssh/authorized_keys");
if !std::path::Path::new(&auth_keys).exists() {
return None;
}
let content = std::fs::read_to_string(&auth_keys).ok()?;
let first_key = content.lines().find(|line| {
let t = line.trim();
!t.is_empty() && !t.starts_with('#')
})?;
let first_key = first_key.trim();
use std::io::Write;
let mut tmp_file = tempfile::Builder::new()
.prefix("u2secure_key_")
.tempfile()
.ok()?;
writeln!(tmp_file, "{first_key}").ok()?;
let output = Command::new("ssh-keygen")
.args(["-l", "-f"])
.arg(tmp_file.path().as_os_str())
.output()
.ok()?;
if !output.status.success() {
let parts: Vec<&str> = first_key.split_whitespace().collect();
let kind = parts.first().unwrap_or(&"unknown");
let truncated = if first_key.len() > 47 {
format!("{}...", &first_key[..47])
} else {
first_key.to_string()
};
return Some(format!("({kind}) {truncated}"));
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if stdout.is_empty() {
None
} else {
Some(stdout)
}
}
pub fn user_exists(username: &str) -> bool {
Command::new("id")
.arg(username)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub fn home_dir(username: &str) -> String {
if username == "root" {
"/root".to_string()
} else {
if let Ok(output) = Command::new("getent").args(["passwd", username]).output()
&& output.status.success()
{
let line = String::from_utf8_lossy(&output.stdout);
if let Some(home) = line.trim().split(':').nth(5)
&& !home.is_empty()
{
return home.to_string();
}
}
format!("/home/{username}")
}
}