From c242146802b89fe028c8dde19c1adb839976cf1a Mon Sep 17 00:00:00 2001 From: dushixiang Date: Thu, 20 Nov 2025 20:18:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=89=E5=85=A8=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/protocol/message.go | 2 + pkg/agent/audit/audit.go | 2503 ++---------------------------- pkg/agent/audit/audit_test.go | 279 ++++ pkg/agent/audit/check_misc.go | 419 +++++ pkg/agent/audit/check_network.go | 270 ++++ pkg/agent/audit/check_process.go | 380 +++++ pkg/agent/audit/check_rootkit.go | 207 +++ pkg/agent/audit/check_ssh.go | 431 +++++ pkg/agent/audit/config.go | 298 ++++ pkg/agent/audit/evidence.go | 164 ++ pkg/agent/audit/utils.go | 326 ++++ 11 files changed, 2898 insertions(+), 2381 deletions(-) create mode 100644 pkg/agent/audit/audit_test.go create mode 100644 pkg/agent/audit/check_misc.go create mode 100644 pkg/agent/audit/check_network.go create mode 100644 pkg/agent/audit/check_process.go create mode 100644 pkg/agent/audit/check_rootkit.go create mode 100644 pkg/agent/audit/check_ssh.go create mode 100644 pkg/agent/audit/config.go create mode 100644 pkg/agent/audit/evidence.go create mode 100644 pkg/agent/audit/utils.go diff --git a/internal/protocol/message.go b/internal/protocol/message.go index 9da0e2c..f0d8ed9 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -209,6 +209,8 @@ type VPSAuditResult struct { ThreatLevel string `json:"threatLevel"` // 修复建议 Recommendations []string `json:"recommendations,omitempty"` + // 审计警告(权限不足、检查失败等非安全问题) + AuditWarnings []string `json:"auditWarnings,omitempty"` } // SystemInfo 系统信息 diff --git a/pkg/agent/audit/audit.go b/pkg/agent/audit/audit.go index 7697e86..3b75c27 100644 --- a/pkg/agent/audit/audit.go +++ b/pkg/agent/audit/audit.go @@ -1,36 +1,67 @@ package audit import ( - "bufio" - "bytes" - "crypto/sha256" - "encoding/hex" "fmt" - "io" - "net" "os" - "os/exec" - "path/filepath" "runtime" - "strconv" - "strings" "time" "github.com/dushixiang/pika/internal/protocol" - "github.com/shirou/gopsutil/v4/host" - gopsutilNet "github.com/shirou/gopsutil/v4/net" - "github.com/shirou/gopsutil/v4/process" ) +// 状态常量 const ( - StatusPass string = "pass" - StatusFail string = "fail" - StatusWarn string = "warn" - StatusSkip string = "skip" + StatusPass = "pass" + StatusFail = "fail" + StatusWarn = "warn" + StatusSkip = "skip" ) +// Auditor VPS 安全审计器 +type Auditor struct { + config *Config + cache *ProcessCache + executor *CommandExecutor + evidence *EvidenceCollector + + // 各个检查器 + rootkitChecker *RootkitChecker + processChecker *ProcessChecker + sshChecker *SSHChecker + networkChecker *NetworkChecker + fileChecker *FileChecker + accountChecker *AccountChecker +} + +// NewAuditor 创建审计器 +func NewAuditor(config *Config) *Auditor { + if config == nil { + config = DefaultConfig() + } + + // 初始化共享组件 + cache := NewProcessCache(config.PerformanceConfig.ProcessCacheDuration) + executor := NewCommandExecutor(config.PerformanceConfig.CommandTimeout) + evidence := NewEvidenceCollector() + + // 初始化各个检查器 + return &Auditor{ + config: config, + cache: cache, + executor: executor, + evidence: evidence, + + rootkitChecker: NewRootkitChecker(config, cache, evidence), + processChecker: NewProcessChecker(config, cache, evidence), + sshChecker: NewSSHChecker(config, cache, evidence, executor), + networkChecker: NewNetworkChecker(config, cache, evidence), + fileChecker: NewFileChecker(config, evidence, executor), + accountChecker: NewAccountChecker(config), + } +} + // RunAudit 执行 VPS 安全审计 -func RunAudit() (*protocol.VPSAuditResult, error) { +func (a *Auditor) RunAudit() (*protocol.VPSAuditResult, error) { startTime := time.Now().UnixMilli() // 检查操作系统 @@ -38,43 +69,62 @@ func RunAudit() (*protocol.VPSAuditResult, error) { return nil, fmt.Errorf("只支持 Linux 系统") } - // 检查运行权限 - root权限可以执行更完整的审计 + // 检查运行权限 if os.Geteuid() != 0 { return nil, fmt.Errorf("需要root权限运行完整审计") } + globalLogger.Info("开始安全审计...") + // 获取系统信息 - systemInfo, err := getSystemInfo() + sysInfoCollector := NewSystemInfoCollector(a.executor) + systemInfo, err := sysInfoCollector.Collect() if err != nil { return nil, fmt.Errorf("获取系统信息失败: %w", err) } - // 执行安全检查(从应急响应角度) - checks := []SecurityCheckFunc{ - checkRootkit, // rootkit检测 - checkSuspiciousProcesses, // 可疑进程 - checkSSHSecurity, // SSH安全 - checkListeningPorts, // 异常端口 - checkCronJobs, // 定时任务 - checkSuspiciousFiles, // 可疑文件 - checkSystemAccounts, // 系统账户 - checkNetworkConnections, // 网络连接 - checkFileIntegrity, // 系统文件完整性 - checkLoginHistory, // 登录历史 - checkImmutableFiles, // 不可变文件(优化版) - checkSuspiciousEnvVars, // 进程环境变量 + // 收集警告 + warningCollector := NewWarningCollector() + + // 执行所有安全检查 + var securityChecks []protocol.SecurityCheck + + checks := []struct { + name string + fn func() protocol.SecurityCheck + }{ + {"Rootkit检测", a.rootkitChecker.Check}, + {"可疑进程检测", a.processChecker.CheckSuspiciousProcesses}, + {"进程环境变量检查", a.processChecker.CheckSuspiciousEnvVars}, + {"SSH安全检查", a.sshChecker.Check}, + {"端口监听检查", a.networkChecker.CheckListeningPorts}, + {"网络连接检查", a.networkChecker.CheckNetworkConnections}, + {"可疑文件检查", a.fileChecker.CheckSuspiciousFiles}, + {"文件完整性检查", a.fileChecker.CheckFileIntegrity}, + {"不可变文件检查", a.fileChecker.CheckImmutableFiles}, + {"系统账户检查", a.accountChecker.CheckSystemAccounts}, } - var securityChecks []protocol.SecurityCheck - for _, checkFunc := range checks { - check := checkFunc() - securityChecks = append(securityChecks, check) + for _, check := range checks { + globalLogger.Debug("执行检查: %s", check.name) + result := check.fn() + securityChecks = append(securityChecks, result) + + // 收集警告 + for _, detail := range result.Details { + if detail.Status == StatusSkip && detail.Message != "" { + warningCollector.Add(fmt.Sprintf("[%s] %s", result.Category, detail.Message)) + } + } } endTime := time.Now().UnixMilli() - // 计算风险评分 - riskScore, threatLevel, recommendations := calculateRiskScore(securityChecks) + // 计算风险评分(使用改进的算法) + riskScore, threatLevel, recommendations := a.calculateRiskScore(securityChecks) + + globalLogger.Info("审计完成,耗时 %dms, 风险评分: %d, 威胁等级: %s", + endTime-startTime, riskScore, threatLevel) return &protocol.VPSAuditResult{ SystemInfo: *systemInfo, @@ -84,2384 +134,75 @@ func RunAudit() (*protocol.VPSAuditResult, error) { RiskScore: riskScore, ThreatLevel: threatLevel, Recommendations: recommendations, + AuditWarnings: warningCollector.GetAll(), }, nil } -type SecurityCheckFunc func() protocol.SecurityCheck - -// getSystemInfo 获取系统信息 -func getSystemInfo() (*protocol.SystemInfo, error) { - hostname, _ := os.Hostname() - if hostname == "" { - hostname = "unknown" - } - - info, err := host.Info() - if err != nil { - return nil, err - } - - osInfo := fmt.Sprintf("%s %s", info.Platform, info.PlatformVersion) - if info.PlatformFamily != "" { - osInfo = fmt.Sprintf("%s (%s)", osInfo, info.PlatformFamily) - } - - return &protocol.SystemInfo{ - Hostname: hostname, - OS: osInfo, - KernelVersion: info.KernelVersion, - Uptime: info.Uptime, - }, nil -} - -// checkRootkit 检测rootkit和恶意内核模块 -func checkRootkit() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "rootkit_detection", - Status: StatusPass, - Message: "Rootkit检测", - Details: []protocol.SecurityCheckSub{}, - } - - // 检查加载的内核模块 - suspiciousModules := checkKernelModules() - if len(suspiciousModules) > 0 { - check.Status = StatusWarn - for _, mod := range suspiciousModules { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_module", - Status: StatusWarn, - Message: fmt.Sprintf("可疑内核模块: %s", mod), - }) - } - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "kernel_modules", - Status: StatusPass, - Message: "未发现可疑内核模块", - }) - } - - // 检查隐藏进程 - hiddenProcs := checkHiddenProcesses() - if len(hiddenProcs) > 0 { - check.Status = StatusFail - for _, pid := range hiddenProcs { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "hidden_process", - Status: StatusFail, - Message: fmt.Sprintf("检测到隐藏进程: PID %d", pid), - }) - } - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "hidden_processes", - Status: StatusPass, - Message: "未发现隐藏进程", - }) - } - - if check.Status == StatusPass { - check.Message = "未检测到rootkit" - } else if check.Status == StatusWarn { - check.Message = "发现可疑rootkit特征" - } else { - check.Message = "发现rootkit感染迹象" - } - - return check -} - -// checkSuspiciousEnvVars 检查进程的可疑环境变量 -func checkSuspiciousEnvVars() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "suspicious_env_vars", - Status: StatusPass, - Message: "进程环境变量检查", - Details: []protocol.SecurityCheckSub{}, - } - - procs, err := process.Processes() - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取进程列表" - return check - } - - suspiciousCount := 0 - for _, p := range procs { - env, err := p.Environ() - if err != nil { - continue - } - - name, _ := p.Name() - exe, _ := p.Exe() - - for _, e := range env { - if strings.HasPrefix(e, "LD_PRELOAD=") || strings.HasPrefix(e, "LD_LIBRARY_PATH=") { - parts := strings.SplitN(e, "=", 2) - if len(parts) != 2 { - continue - } - value := parts[1] - - // 检查是否是合法的库路径 - if isLegitimateLibraryPath(value, name, exe) { - continue - } - - // 评估风险等级 - riskLevel := assessLibraryRisk(value) - status := StatusWarn - if riskLevel == "high" { - status = StatusFail - check.Status = StatusFail - } else if check.Status != StatusFail { - check.Status = StatusWarn - } - - suspiciousCount++ - - // 收集证据 - evidence := collectProcessEvidence(p, riskLevel) - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_ld_env", - Status: status, - Message: fmt.Sprintf("进程 '%s'(PID: %d) 发现可疑环境变量: %s (风险等级: %s)", name, p.Pid, e, riskLevel), - Evidence: evidence, - }) - // 找到一个就够了 - break - } - } - } - - if suspiciousCount == 0 { - check.Message = "未发现可疑的进程环境变量" - } else { - check.Message = fmt.Sprintf("发现 %d 个进程存在可疑环境变量", suspiciousCount) - } - - return check -} - -// checkSuspiciousProcesses 检查可疑进程 -func checkSuspiciousProcesses() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "suspicious_processes", - Status: StatusPass, - Message: "可疑进程检测", - Details: []protocol.SecurityCheckSub{}, - } - - procs, err := process.Processes() - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取进程列表" - return check - } - - suspiciousCount := 0 - - for _, p := range procs { - name, _ := p.Name() - cmdline, _ := p.Cmdline() - exe, _ := p.Exe() - - // 检查挖矿程序 - if isMinerProcess(name, cmdline) { - suspiciousCount++ - check.Status = StatusFail - - // 收集证据 - evidence := collectProcessEvidence(p, "high") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "miner_process", - Status: StatusFail, - Message: fmt.Sprintf("检测到挖矿程序: %s (PID: %d)", name, p.Pid), - Evidence: evidence, - }) - } - - // 检查反弹shell - if isReverseShell(cmdline) { - suspiciousCount++ - check.Status = StatusFail - - // 收集证据 - evidence := collectProcessEvidence(p, "high") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "reverse_shell", - Status: StatusFail, - Message: fmt.Sprintf("检测到反弹shell: %s (PID: %d)", cmdline, p.Pid), - Evidence: evidence, - }) - } - - // 检查无文件进程(memfd_create等) - if strings.Contains(exe, "memfd:") || strings.Contains(exe, "(deleted)") { - suspiciousCount++ - check.Status = StatusWarn - - // 收集证据 - evidence := collectProcessEvidence(p, "high") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "fileless_process", - Status: StatusWarn, - Message: fmt.Sprintf("检测到无文件进程: %s (PID: %d, 路径: %s)", name, p.Pid, exe), - Evidence: evidence, - }) - } - - // 检查进程名称和可执行文件不匹配(进程伪装) - // 改进的伪装检测逻辑 - if exe != "" && name != "" && !strings.Contains(exe, "(deleted)") { - baseName := filepath.Base(exe) - - // 更精确的匹配逻辑 - nameMatch := (baseName == name) || - strings.HasPrefix(name, baseName) || - strings.Contains(name, ":") // PostgreSQL等进程有冒号后缀 - - if !nameMatch && !strings.Contains(exe, name) { - // 扩展白名单(减少脚本语言、容器、数据库等合法进程的误报) - whitelistPatterns := []string{ - // 脚本语言 - "python", "java", "node", "ruby", "perl", "php", "go", "rust", - // Shell - "bash", "sh", "zsh", "ksh", "fish", - // 容器相关 - "systemd", "containerd", "docker", "podman", "kubernetes", "k3s", - // 数据库worker进程 - "postgres", "mysqld", "redis", "mongodb", "mariadb", - // Web服务器worker - "nginx", "httpd", "apache2", "caddy", - // 包管理器 - "npm", "yarn", "pip", "cargo", "composer", - // 终端复用器 - "tmux", "screen", - // 编辑器 - "vim", "emacs", "nano", - // 其他常见工具 - "git", "ssh", "rsync", "curl", "wget", - } - - isWhitelisted := false - nameLower := strings.ToLower(name) - baseNameLower := strings.ToLower(baseName) - - for _, pattern := range whitelistPatterns { - if strings.Contains(nameLower, pattern) || - strings.Contains(baseNameLower, pattern) { - isWhitelisted = true - break - } - } - - // 检查是否是符号链接导致的不匹配(常见于busybox等) - if !isWhitelisted { - // 只有当路径看起来可疑时才报警 - if isSuspiciousPath(exe) { - suspiciousCount++ - check.Status = StatusWarn - - evidence := collectProcessEvidence(p, "medium") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "process_masquerading", - Status: StatusWarn, - Message: fmt.Sprintf("可疑的进程名称不匹配: 名称=%s, 路径=%s (PID: %d)", name, exe, p.Pid), - Evidence: evidence, - }) - } - } - } - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "clean", - Status: StatusPass, - Message: "未发现可疑进程", - }) - check.Message = "进程正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑进程", suspiciousCount) - } - - return check -} - -// checkSSHSecurity 检查SSH后门和安全配置(增强版) -func checkSSHSecurity() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "ssh_security", - Status: StatusPass, - Message: "SSH安全检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 检查SSH服务是否运行 - if !isProcessRunning("sshd") { - check.Message = "SSH服务未运行" - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ssh_status", - Status: StatusPass, - Message: "SSH未运行", - }) - return check - } - - // 1. 检查authorized_keys文件内容(新增) - suspiciousKeys := checkAuthorizedKeysContent() - if len(suspiciousKeys) > 0 { - check.Status = StatusWarn - for _, key := range suspiciousKeys { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_authorized_key", - Status: StatusWarn, - Message: key, - }) - } - } - - // 2. 检查SSH配置 - sshConfig := readSSHConfig() - - // 检查是否允许root登录 - permitRoot := sshConfig["permitrootlogin"] // 转小写统一处理 - if permitRoot == "yes" { - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "permit_root_login", - Status: StatusWarn, - Message: "允许root SSH登录", - }) - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "permit_root_login", - Status: StatusPass, - Message: fmt.Sprintf("Root登录策略: %s", permitRoot), - }) - } - - // 检查空密码登录 - permitEmpty := sshConfig["permitemptypasswords"] - if permitEmpty == "yes" { - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "permit_empty_passwords", - Status: StatusFail, - Message: "允许空密码登录", - }) - } - - // 检查密码认证是否启用(建议使用密钥) - passwordAuth := sshConfig["passwordauthentication"] - if passwordAuth == "yes" { - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "password_auth_enabled", - Status: StatusWarn, - Message: "启用了密码认证(建议仅使用密钥认证)", - }) - } - - // 3. 检查SSH后门进程 - suspiciousSshd := checkSuspiciousSSHD() - if len(suspiciousSshd) > 0 { - check.Status = StatusFail - for _, sshd := range suspiciousSshd { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "backdoor_sshd", - Status: StatusFail, - Message: sshd, - }) - } - } - - // 4. 检查SSH二进制文件完整性(新增) - sshdBinaries := []string{"/usr/sbin/sshd", "/usr/bin/ssh"} - for _, binPath := range sshdBinaries { - info, err := os.Stat(binPath) - if err != nil { - continue - } - - // 检查最近修改 - modTime := info.ModTime() - age := time.Since(modTime) - if age < 90*24*time.Hour { // 最近90天(OpenSSH更新频繁,延长窗口减少误报) - fileHash := calculateSHA256(binPath) - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ssh_binary_modified", - Status: StatusWarn, - Message: fmt.Sprintf("SSH二进制文件最近被修改: %s (%d天前)", binPath, int(age.Hours()/24)), - Evidence: &protocol.Evidence{ - FilePath: binPath, - FileHash: fileHash, - Timestamp: modTime.UnixMilli(), - RiskLevel: "high", - }, - }) - } - } - - // 5. 检查SSH配置文件的软链接(新增,检测配置劫持) - sshConfigPaths := []string{"/etc/ssh/sshd_config"} - for _, configPath := range sshConfigPaths { - linkTarget, err := os.Readlink(configPath) - if err == nil { - // 是软链接,检查目标是否可疑 - if strings.Contains(linkTarget, "/tmp") || strings.Contains(linkTarget, "/dev/shm") { - check.Status = StatusFail - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ssh_config_symlink", - Status: StatusFail, - Message: fmt.Sprintf("SSH配置文件是指向可疑位置的软链接: %s -> %s", configPath, linkTarget), - }) - } - } - } - - if check.Status == StatusPass { - check.Message = "SSH配置安全" - } else if check.Status == StatusWarn { - check.Message = "SSH配置存在风险" - } else { - check.Message = "检测到SSH后门" - } - - return check -} - -// checkListeningPorts 检查异常监听端口(增强版:进程-端口合理性分析) -func checkListeningPorts() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "listening_ports", - Status: StatusPass, - Message: "端口监听检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 使用gopsutil获取监听端口及进程信息 - connections, err := gopsutilNet.Connections("all") - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取网络连接信息" - return check - } - - // 反弹shell/恶意工具常用端口(降低优先级,减少误报) - suspiciousPorts := map[uint32]string{ - 4444: "Metasploit默认端口", - 1337: "黑客常用端口", - 31337: "黑客常用端口", - } - - suspiciousCount := 0 - totalListening := 0 - publicListening := 0 - - for _, conn := range connections { - if conn.Status != "LISTEN" { - continue - } - - totalListening++ - port := conn.Laddr.Port - listenAddr := conn.Laddr.IP - pid := conn.Pid - - // 本地回环监听的服务一般是安全的,跳过 - if listenAddr == "127.0.0.1" || listenAddr == "::1" { - continue - } - - publicListening++ - - // 获取进程信息 - if pid > 0 { - proc, err := process.NewProcess(int32(pid)) - if err != nil { - continue - } - - name, _ := proc.Name() - exe, _ := proc.Exe() - cmdline, _ := proc.Cmdline() - - // 检查1: 高危端口 - if desc, exists := suspiciousPorts[port]; exists { - suspiciousCount++ - check.Status = StatusWarn - - evidence := collectProcessEvidence(proc, "high") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("suspicious_port_%d", port), - Status: StatusWarn, - Message: fmt.Sprintf("高危端口监听: %s:%d (%s) - 进程: %s (PID: %d)", listenAddr, port, desc, name, pid), - Evidence: evidence, - }) - continue - } - - // 检查2: 挖矿程序监听端口 - if isMinerProcess(name, cmdline) { - suspiciousCount++ - check.Status = StatusFail - - evidence := collectProcessEvidence(proc, "high") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("miner_listener_%d", port), - Status: StatusFail, - Message: fmt.Sprintf("挖矿程序监听端口: %s 监听 %s:%d (PID: %d)", name, listenAddr, port, pid), - Evidence: evidence, - }) - continue - } - - // 检查3: 可疑路径的程序监听端口 - if isSuspiciousPath(exe) { - suspiciousCount++ - check.Status = StatusFail - - evidence := collectProcessEvidence(proc, "high") - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("suspicious_exe_listener_%d", port), - Status: StatusFail, - Message: fmt.Sprintf("可疑路径程序监听端口: %s 监听 %s:%d (PID: %d, 路径: %s)", name, listenAddr, port, pid, exe), - Evidence: evidence, - }) - continue - } - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "safe_ports", - Status: StatusPass, - Message: fmt.Sprintf("监听%d个端口(公网%d个),未发现可疑监听", totalListening, publicListening), - }) - check.Message = "端口监听正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑端口监听", suspiciousCount) - } - - return check -} - -// isSuspiciousPath 检查可执行文件路径是否可疑 -func isSuspiciousPath(path string) bool { - if path == "" { - return false - } - - suspiciousPaths := []string{ - "/tmp/", - "/dev/shm/", - "/var/tmp/", - "memfd:", - "(deleted)", - } - - for _, sp := range suspiciousPaths { - if strings.Contains(path, sp) { - return true - } - } - - return false -} - -// checkCronJobs 检查定时任务(包括systemd timer) -func checkCronJobs() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "cron_jobs", - Status: StatusPass, - Message: "定时任务检查", - Details: []protocol.SecurityCheckSub{}, - } - - suspiciousCount := 0 - - // 1. 检查传统crontab - systemCrons := []string{ - "/etc/crontab", - "/etc/cron.d/", - "/etc/cron.hourly/", - "/etc/cron.daily/", - "/etc/cron.weekly/", - "/etc/cron.monthly/", - "/var/spool/cron/", - } - - for _, cronPath := range systemCrons { - suspicious := checkCronPath(cronPath) - suspiciousCount += len(suspicious) - for _, cron := range suspicious { - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_cron", - Status: StatusWarn, - Message: cron, - }) - } - } - - // 2. 检查systemd timers(现代Linux更常用) - suspiciousTimers := checkSystemdTimers() - if len(suspiciousTimers) > 0 { - suspiciousCount += len(suspiciousTimers) - check.Status = StatusWarn - for _, timer := range suspiciousTimers { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_systemd_timer", - Status: StatusWarn, - Message: timer, - }) - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "cron_clean", - Status: StatusPass, - Message: "定时任务正常", - }) - check.Message = "定时任务正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑定时任务", suspiciousCount) - } - - return check -} - -// checkSystemdTimers 检查systemd定时器 -func checkSystemdTimers() []string { - var suspicious []string - // 获取所有timer单元的详细信息 - timerListOutput, err := execCommand("systemctl", "list-unit-files", "--type=timer", "--no-pager") - if err != nil { - return suspicious - } - - // 解析timer列表 - lines := strings.Split(timerListOutput, "\n") - for _, line := range lines { - fields := strings.Fields(line) - if len(fields) < 1 || !strings.HasSuffix(fields[0], ".timer") { - continue - } - - timerName := fields[0] - - // 获取timer对应的service内容 - serviceName := strings.TrimSuffix(timerName, ".timer") + ".service" - serviceContent, err := execCommand("systemctl", "cat", serviceName) - if err != nil { - continue - } - - // 检查service内容是否可疑 - if isSuspiciousCron(serviceContent) { - suspicious = append(suspicious, fmt.Sprintf("可疑systemd timer: %s -> %s", timerName, serviceName)) - } - } - - return suspicious -} - -// checkSuspiciousFiles 检查可疑文件(增强版) -func checkSuspiciousFiles() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "suspicious_files", - Status: StatusPass, - Message: "可疑文件检查", - Details: []protocol.SecurityCheckSub{}, - } - - suspiciousCount := 0 - - // 1. 检查临时目录下的可执行文件 - tmpDirs := []string{"/tmp", "/dev/shm", "/var/tmp"} - for _, dir := range tmpDirs { - files := findSuspiciousExecutables(dir) - if len(files) > 0 { - check.Status = StatusWarn - for _, file := range files { - suspiciousCount++ - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "tmp_executable", - Status: StatusWarn, - Message: file, - }) - } - } - } - - // 2. 检查隐藏的可执行文件(以.开头) - hiddenExecs := findHiddenExecutables() - if len(hiddenExecs) > 0 { - check.Status = StatusWarn - for _, file := range hiddenExecs { - suspiciousCount++ - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "hidden_executable", - Status: StatusWarn, - Message: fmt.Sprintf("隐藏可执行文件: %s", file), - }) - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "files_clean", - Status: StatusPass, - Message: "未发现可疑文件", - }) - check.Message = "文件系统正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑文件", suspiciousCount) - } - - return check -} - -// findSuspiciousExecutables 查找可疑的可执行文件(带时间和大小分析) -func findSuspiciousExecutables(dir string) []string { - suspicious := []string{} - - entries, err := os.ReadDir(dir) - if err != nil { - return suspicious - } - - now := time.Now() - for _, entry := range entries { - if entry.IsDir() { - continue - } - - fullPath := filepath.Join(dir, entry.Name()) - info, err := entry.Info() - if err != nil { - continue - } - - // 检查是否可执行 - if info.Mode()&0111 != 0 { - // 最近2小时创建的可执行文件更可疑(捕获活跃威胁) - age := now.Sub(info.ModTime()) - sizeKB := info.Size() / 1024 - sizeMB := info.Size() / 1024 / 1024 - - if age < 2*time.Hour { - suspicious = append(suspicious, fmt.Sprintf("%s 下的最近可执行文件: %s (大小: %dKB, 创建时间: %s)", - dir, fullPath, sizeKB, info.ModTime().Format("2006-01-02 15:04:05"))) - } else if sizeMB > 10 { // 大于10MB的可执行文件 - suspicious = append(suspicious, fmt.Sprintf("%s 下的大型可执行文件: %s (大小: %dMB)", - dir, fullPath, sizeMB)) - } else { - // 其他可执行文件只在特定条件下报告 - if strings.HasPrefix(entry.Name(), ".") { - suspicious = append(suspicious, fmt.Sprintf("%s 下的隐藏可执行文件: %s", dir, fullPath)) - } - } - } - } - - return suspicious -} - -// checkSystemAccounts 检查系统账户异常 -func checkSystemAccounts() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "system_accounts", - Status: StatusPass, - Message: "系统账户检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 检查可登录的系统账户 - suspiciousAccounts := findSuspiciousAccounts() - if len(suspiciousAccounts) > 0 { - check.Status = StatusWarn - for _, acc := range suspiciousAccounts { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_account", - Status: StatusWarn, - Message: acc, - }) - } - } - - // 检查无密码账户 - noPasswordAccounts := findNoPasswordAccounts() - if len(noPasswordAccounts) > 0 { - check.Status = StatusFail - for _, acc := range noPasswordAccounts { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "no_password_account", - Status: StatusFail, - Message: fmt.Sprintf("无密码账户: %s", acc), - }) - } - } - - // 检查UID为0的非root账户 - rootUIDAccounts := findRootUIDAccounts() - if len(rootUIDAccounts) > 0 { - check.Status = StatusFail - for _, acc := range rootUIDAccounts { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "uid0_account", - Status: StatusFail, - Message: fmt.Sprintf("UID为0的非root账户: %s", acc), - }) - } - } - - if check.Status == StatusPass { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "accounts_clean", - Status: StatusPass, - Message: "系统账户正常", - }) - check.Message = "账户配置正常" - } else if check.Status == StatusWarn { - check.Message = "发现可疑账户" - } else { - check.Message = "发现严重账户安全问题" - } - - return check -} - -// checkNetworkConnections 检查异常网络连接(增强版:关联进程) -func checkNetworkConnections() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "network_connections", - Status: StatusPass, - Message: "网络连接检查", - Details: []protocol.SecurityCheckSub{}, - } - - connections, err := gopsutilNet.Connections("all") - if err != nil { - check.Status = StatusSkip - check.Message = "无法获取网络连接" - return check - } - - suspiciousCount := 0 - establishedCount := 0 - - // 检查可疑的外部连接 - for _, conn := range connections { - if conn.Status != "ESTABLISHED" { - continue - } - - establishedCount++ - - // 获取进程信息 - var processName string - var processPath string - if conn.Pid > 0 { - proc, err := process.NewProcess(int32(conn.Pid)) - if err == nil { - processName, _ = proc.Name() - processPath, _ = proc.Exe() - } - } - - // 检查1: 连接到挖矿池 - if isMiningPool(conn.Raddr.IP, conn.Raddr.Port) { - suspiciousCount++ - check.Status = StatusFail - - msg := fmt.Sprintf("连接到挖矿池: %s:%d", conn.Raddr.IP, conn.Raddr.Port) - if processName != "" { - msg += fmt.Sprintf(" (进程: %s, PID: %d)", processName, conn.Pid) - } - - var evidence *protocol.Evidence - if conn.Pid > 0 { - proc, _ := process.NewProcess(int32(conn.Pid)) - if proc != nil { - evidence = collectProcessEvidence(proc, "high") - } - } - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "mining_connection", - Status: StatusFail, - Message: msg, - Evidence: evidence, - }) - } - - // 检查2: 可疑路径的进程外连 - if processPath != "" && isSuspiciousPath(processPath) { - suspiciousCount++ - check.Status = StatusFail - - var evidence *protocol.Evidence - if conn.Pid > 0 { - proc, _ := process.NewProcess(int32(conn.Pid)) - if proc != nil { - evidence = collectProcessEvidence(proc, "high") - } - } - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_path_connection", - Status: StatusFail, - Message: fmt.Sprintf("可疑路径进程外连: %s (PID:%d) -> %s:%d", processPath, conn.Pid, conn.Raddr.IP, conn.Raddr.Port), - Evidence: evidence, - }) - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "connections_clean", - Status: StatusPass, - Message: fmt.Sprintf("共 %d 个活动连接,未发现异常", establishedCount), - }) - check.Message = "网络连接正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个可疑连接", suspiciousCount) - } - - return check -} - -// isLocalIP 判断是否是本地IP -func isLocalIP(ip string) bool { - if ip == "127.0.0.1" || ip == "::1" || ip == "localhost" { - return true - } - - // 使用标准库判断私有IP (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) - parsedIP := net.ParseIP(ip) - if parsedIP == nil { - return false - } - - // 检查是否是私有IP、环回地址或链路本地地址 - return parsedIP.IsLoopback() || parsedIP.IsPrivate() || parsedIP.IsLinkLocalUnicast() -} - -// checkFileIntegrity 检查关键系统文件完整性(增强版) -func checkFileIntegrity() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "file_integrity", - Status: StatusPass, - Message: "系统文件完整性检查", - Details: []protocol.SecurityCheckSub{}, - } - - suspiciousCount := 0 - - // 1. 检查/etc/ld.so.preload (这个文件通常不应该存在) - if content, err := os.ReadFile("/etc/ld.so.preload"); err == nil { - contentStr := strings.TrimSpace(string(content)) - if contentStr != "" { - suspiciousCount++ - check.Status = StatusWarn - - fileHash := calculateSHA256("/etc/ld.so.preload") - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "ld_preload_exists", - Status: StatusWarn, - Message: fmt.Sprintf("/etc/ld.so.preload 存在: %s", contentStr), - Evidence: &protocol.Evidence{ - FilePath: "/etc/ld.so.preload", - FileHash: fileHash, - RiskLevel: "high", - }, - }) - } - } - - // 2. 检查/etc/ld.so.conf.d/下的可疑配置 - ldConfDir := "/etc/ld.so.conf.d" - if entries, err := os.ReadDir(ldConfDir); err == nil { - for _, entry := range entries { - if entry.IsDir() { - continue - } - fullPath := filepath.Join(ldConfDir, entry.Name()) - content, err := os.ReadFile(fullPath) - if err != nil { - continue - } - contentStr := string(content) - if strings.Contains(contentStr, "/tmp") || strings.Contains(contentStr, "/dev/shm") { - suspiciousCount++ - check.Status = StatusWarn - - fileHash := calculateSHA256(fullPath) - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_ld_conf", - Status: StatusWarn, - Message: fmt.Sprintf("可疑的库配置: %s 包含临时目录路径", fullPath), - Evidence: &protocol.Evidence{ - FilePath: fullPath, - FileHash: fileHash, - RiskLevel: "medium", - }, - }) - } - } - } - - // 3. 检查关键系统二进制文件的最近修改(新增) - criticalBinaries := []string{ - "/bin/bash", "/bin/sh", "/bin/ls", "/bin/ps", - "/usr/bin/ssh", "/usr/bin/sudo", "/usr/sbin/sshd", - "/usr/bin/passwd", "/usr/bin/chsh", - } - - now := time.Now() - for _, binPath := range criticalBinaries { - info, err := os.Stat(binPath) - if err != nil { - continue // 文件不存在,跳过 - } - - // 检查最近90天内被修改的关键二进制文件(可能被替换) - // 注意: 系统更新(apt/yum)会修改这些文件,90天的窗口减少误报 - modTime := info.ModTime() - age := now.Sub(modTime) - if age < 90*24*time.Hour { - suspiciousCount++ - check.Status = StatusWarn - - fileHash := calculateSHA256(binPath) - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "recently_modified_binary", - Status: StatusWarn, - Message: fmt.Sprintf("关键系统文件最近被修改: %s (修改时间: %s, %d天前)", binPath, modTime.Format("2006-01-02 15:04:05"), int(age.Hours()/24)), - Evidence: &protocol.Evidence{ - FilePath: binPath, - FileHash: fileHash, - Timestamp: modTime.UnixMilli(), - RiskLevel: "medium", - }, - }) - } - } - - // 4. 检查启动脚本的最近修改(新增) - startupScripts := []string{ - "/etc/rc.local", - "/etc/profile", - "/etc/bash.bashrc", - "/etc/environment", - } - - for _, scriptPath := range startupScripts { - info, err := os.Stat(scriptPath) - if err != nil { - continue - } - - modTime := info.ModTime() - age := now.Sub(modTime) - if age < 30*24*time.Hour { - suspiciousCount++ - check.Status = StatusWarn - - fileHash := calculateSHA256(scriptPath) - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "recently_modified_startup", - Status: StatusWarn, - Message: fmt.Sprintf("启动脚本最近被修改: %s (修改时间: %s, %d天前)", scriptPath, modTime.Format("2006-01-02 15:04:05"), int(age.Hours()/24)), - Evidence: &protocol.Evidence{ - FilePath: scriptPath, - FileHash: fileHash, - Timestamp: modTime.UnixMilli(), - RiskLevel: "medium", - }, - }) - } - } - - // 5. 检查PAM配置(新增,用于检测SSH后门) - pamSshdPath := "/etc/pam.d/sshd" - if content, err := os.ReadFile(pamSshdPath); err == nil { - contentStr := string(content) - // 检查是否包含可疑的pam模块 - suspiciousPamModules := []string{"pam_backdoor", "pam_unix_auth"} - for _, module := range suspiciousPamModules { - if strings.Contains(contentStr, module) { - suspiciousCount++ - check.Status = StatusFail - - fileHash := calculateSHA256(pamSshdPath) - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_pam_module", - Status: StatusFail, - Message: fmt.Sprintf("PAM配置包含可疑模块: %s in %s", module, pamSshdPath), - Evidence: &protocol.Evidence{ - FilePath: pamSshdPath, - FileHash: fileHash, - RiskLevel: "high", - }, - }) - } - } - } - - if suspiciousCount == 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "integrity_ok", - Status: StatusPass, - Message: "系统文件完整性正常", - }) - check.Message = "文件完整性正常" - } else { - check.Message = fmt.Sprintf("发现 %d 个文件异常", suspiciousCount) - } - - return check -} - -// checkImmutableFiles 检查不可变文件(优化版:只检查关键文件和批量执行) -func checkImmutableFiles() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "immutable_files", - Status: StatusPass, - Message: "不可变文件检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 策略1: 只检查最关键的文件(通常被rootkit/后门保护) - criticalFiles := []string{ - "/etc/ld.so.preload", - "/etc/crontab", - "/etc/rc.local", - "/etc/passwd", - "/etc/shadow", - "/etc/sudoers", - "/etc/hosts", - "/etc/ssh/sshd_config", - "/root/.ssh/authorized_keys", - "/etc/systemd/system", - "/lib/systemd/system", - } - - suspiciousCount := 0 - - // 先检查关键文件 - for _, file := range criticalFiles { - if _, err := os.Stat(file); err != nil { - continue // 文件不存在,跳过 - } - - // 执行lsattr检查 - lsattrCmd := exec.Command("lsattr", "-d", file) - lsattrOutput, err := lsattrCmd.Output() - if err != nil { - continue - } - - output := string(lsattrOutput) - // 检查是否有i标志(immutable) - if strings.Contains(output, "----i") || strings.Contains(output, "---i-") { - suspiciousCount++ - check.Status = StatusFail // 关键文件不可变是严重问题 - - // 计算文件哈希 - fileHash := calculateSHA256(file) - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "immutable_critical_file", - Status: StatusFail, // 关键文件不可变是高危信号,升级为Fail - Message: fmt.Sprintf("关键文件被设置为不可变: %s (可能是rootkit)", file), - Evidence: &protocol.Evidence{ - FilePath: file, - FileHash: fileHash, - RiskLevel: "high", - }, - }) - } - } - - // 策略2: 批量检查关键目录(性能优化) - criticalDirs := []string{"/etc", "/bin", "/sbin"} - - for _, dir := range criticalDirs { - // 使用lsattr -R批量递归检查(比逐个文件快得多) - lsattrCmd := exec.Command("lsattr", "-Ra", dir) - lsattrOutput, err := lsattrCmd.Output() - if err != nil { - continue // 可能是权限问题或目录不存在 - } - - // 解析输出 - lines := strings.Split(string(lsattrOutput), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - - // lsattr输出格式: "----i-------e------ /path/to/file" - if strings.Contains(line, "----i") || strings.Contains(line, "---i-") { - // 提取文件路径 - parts := strings.Fields(line) - if len(parts) < 2 { - continue - } - filePath := parts[len(parts)-1] - - // 跳过已经检查过的关键文件(避免重复报告) - alreadyReported := false - for _, cf := range criticalFiles { - if filePath == cf { - alreadyReported = true - break - } - } - if alreadyReported { - continue - } - - suspiciousCount++ - check.Status = StatusWarn - - fileHash := calculateSHA256(filePath) - - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "immutable_file", - Status: StatusWarn, - Message: fmt.Sprintf("发现不可变文件: %s", filePath), - Evidence: &protocol.Evidence{ - FilePath: filePath, - FileHash: fileHash, - RiskLevel: "medium", - }, - }) - - // 限制报告数量,避免过多 - if suspiciousCount >= 20 { - break - } - } - } - - if suspiciousCount >= 20 { - break - } - } - - if suspiciousCount > 0 { - if suspiciousCount >= 20 { - check.Message = fmt.Sprintf("发现大量不可变文件(>=20个),可能存在rootkit") - } else { - check.Message = fmt.Sprintf("发现 %d 个不可变文件", suspiciousCount) - } - } else { - check.Message = "未发现可疑的不可变文件" - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "immutable_files_clean", - Status: StatusPass, - Message: "未发现可疑的不可变文件", - }) - } - - return check -} - -// checkCommandHistory 检查命令历史 - -// checkLoginHistory 检查登录历史(智能分析版) -func checkLoginHistory() protocol.SecurityCheck { - check := protocol.SecurityCheck{ - Category: "login_history", - Status: StatusPass, - Message: "登录历史检查", - Details: []protocol.SecurityCheckSub{}, - } - - // 分析最近的成功登录 - lastOutput, err := execCommand("last", "-n", "50") - if err == nil { - suspiciousLogins := analyzeSuccessfulLogins(lastOutput) - if len(suspiciousLogins) > 0 { - check.Status = StatusWarn - for _, login := range suspiciousLogins { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "suspicious_login", - Status: StatusWarn, - Message: login, - }) - } - } - } - - // 分析失败的登录攻击 - lastbOutput, err := execCommand("lastb", "-n", "100") - if err == nil { - attackAnalysis := analyzeFailedLogins(lastbOutput) - if attackAnalysis != nil { - // 只有高频攻击或成功攻击才报告 - if attackAnalysis.HighFrequencyIPs > 0 { - check.Status = StatusWarn - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "brute_force_attack", - Status: StatusWarn, - Message: fmt.Sprintf("检测到暴力破解攻击: %d个IP高频尝试登录(>10次)", attackAnalysis.HighFrequencyIPs), - }) - - // 列出前5个最活跃的攻击IP - for i, ipStat := range attackAnalysis.TopAttackers { - if i >= 5 { - break - } - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: fmt.Sprintf("top_attacker_%d", i+1), - Status: StatusWarn, - Message: fmt.Sprintf("攻击来源 #%d: %s (失败%d次, 尝试用户: %s)", i+1, ipStat.IP, ipStat.Count, strings.Join(ipStat.Users, ", ")), - }) - } - } - - // 统计信息(仅作参考,不算异常) - if attackAnalysis.TotalFailed > 0 { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "failed_login_summary", - Status: StatusPass, - Message: fmt.Sprintf("失败登录统计: 共%d次失败尝试, 来自%d个不同IP, 尝试了%d个不同用户名", attackAnalysis.TotalFailed, attackAnalysis.UniqueIPs, attackAnalysis.UniqueUsers), - }) - } - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "failed_logins", - Status: StatusPass, - Message: "暂无失败登录记录", - }) - } - } else { - check.Details = append(check.Details, protocol.SecurityCheckSub{ - Name: "failed_logins_check", - Status: StatusSkip, - Message: "无法检查失败登录记录(可能需要root权限)", - }) - } - - if check.Status == StatusPass { - check.Message = "未发现可疑登录活动" - } else { - check.Message = "发现可疑登录活动" - } - - return check -} - -// analyzeSuccessfulLogins 分析成功登录,检测异常模式 -func analyzeSuccessfulLogins(output string) []string { - suspicious := []string{} - lines := strings.Split(output, "\n") - - ipCount := make(map[string]int) - var logins []loginRecord - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" || strings.Contains(trimmed, "wtmp begins") || strings.Contains(trimmed, "wtmp ends") { - continue - } - - // 解析登录记录: root pts/0 222.212.179.149 Mon Nov 10 13:45 still logged in - fields := strings.Fields(trimmed) - if len(fields) >= 3 { - user := fields[0] - ip := fields[2] - - // 过滤本地登录和系统重启记录 - if ip == "0.0.0.0" || ip == ":0" || ip == ":0.0" || user == "reboot" || user == "shutdown" { - continue - } - - logins = append(logins, loginRecord{user: user, ip: ip}) - ipCount[ip]++ - } - } - - // 检测1: 短时间内来自同一IP的多次成功登录(可能是暴力破解成功后的批量操作) - // 提高阈值减少CI/CD、跳板机等自动化运维的误报 - for ip, count := range ipCount { - if count >= 10 { - suspicious = append(suspicious, fmt.Sprintf("检测到IP %s 在最近50条记录中成功登录%d次(可能异常)", ip, count)) - } - } - - // 检测2: root用户从多个不同IP登录(可能账户被多人使用) - rootIPs := make(map[string]bool) - for _, login := range logins { - if login.user == "root" { - rootIPs[login.ip] = true - } - } - if len(rootIPs) >= 3 { - ips := make([]string, 0, len(rootIPs)) - for ip := range rootIPs { - ips = append(ips, ip) - } - suspicious = append(suspicious, fmt.Sprintf("检测到root用户从%d个不同IP登录: %s", len(rootIPs), strings.Join(ips, ", "))) - } - - return suspicious -} - -// loginRecord 登录记录 -type loginRecord struct { - user string - ip string -} - -// AttackAnalysis 攻击分析结果 -type AttackAnalysis struct { - TotalFailed int - UniqueIPs int - UniqueUsers int - HighFrequencyIPs int - TopAttackers []IPAttackStat -} - -// IPAttackStat IP攻击统计 -type IPAttackStat struct { - IP string - Count int - Users []string -} - -// analyzeFailedLogins 分析失败登录,识别暴力破解攻击 -func analyzeFailedLogins(output string) *AttackAnalysis { - lines := strings.Split(output, "\n") - - ipStats := make(map[string]*IPAttackStat) - userSet := make(map[string]bool) - totalFailed := 0 - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" || strings.Contains(trimmed, "btmp begins") || strings.Contains(trimmed, "btmp ends") { - continue - } - - // 解析失败登录: admin ssh:notty 167.71.230.163 Sat Nov 8 06:12 - 06:12 (00:00) - fields := strings.Fields(trimmed) - if len(fields) >= 3 { - user := fields[0] - ip := fields[2] - - totalFailed++ - userSet[user] = true - - if stat, exists := ipStats[ip]; exists { - stat.Count++ - // 记录尝试的用户(去重) - userExists := false - for _, u := range stat.Users { - if u == user { - userExists = true - break - } - } - if !userExists && len(stat.Users) < 5 { - stat.Users = append(stat.Users, user) - } - } else { - ipStats[ip] = &IPAttackStat{ - IP: ip, - Count: 1, - Users: []string{user}, - } - } - } - } - - if totalFailed == 0 { - return nil - } - - // 统计高频攻击IP(失败次数>10次) - highFreqCount := 0 - var topAttackers []IPAttackStat - for _, stat := range ipStats { - if stat.Count > 10 { - highFreqCount++ - topAttackers = append(topAttackers, *stat) - } - } - - // 按攻击次数排序 - for i := 0; i < len(topAttackers); i++ { - for j := i + 1; j < len(topAttackers); j++ { - if topAttackers[j].Count > topAttackers[i].Count { - topAttackers[i], topAttackers[j] = topAttackers[j], topAttackers[i] - } - } - } - - return &AttackAnalysis{ - TotalFailed: totalFailed, - UniqueIPs: len(ipStats), - UniqueUsers: len(userSet), - HighFrequencyIPs: highFreqCount, - TopAttackers: topAttackers, - } -} - -// ========== 辅助检测函数 ========== - -// checkKernelModules 检查可疑内核模块 -func checkKernelModules() []string { - suspicious := []string{} - - content, err := os.ReadFile("/proc/modules") - if err != nil { - return suspicious - } - - // 已知的恶意模块名称特征 - maliciousPatterns := []string{ - "rootkit", "backdoor", "hidden", "stealth", - "diamorphine", "suterusu", "reptile", - } - - lines := strings.Split(string(content), "\n") - for _, line := range lines { - fields := strings.Fields(line) - if len(fields) > 0 { - modName := fields[0] - for _, pattern := range maliciousPatterns { - if strings.Contains(strings.ToLower(modName), pattern) { - suspicious = append(suspicious, modName) - break - } - } - } - } - - return suspicious -} - -// checkHiddenProcesses 检查隐藏进程 -func checkHiddenProcesses() []int32 { - hidden := []int32{} - - // 读取/proc目录 - procDirs, err := os.ReadDir("/proc") - if err != nil { - return hidden - } - - procPIDs := make(map[int32]bool) - for _, entry := range procDirs { - if !entry.IsDir() { - continue - } - pid, err := strconv.Atoi(entry.Name()) - if err != nil { - continue - } - procPIDs[int32(pid)] = true - } - - // 使用gopsutil获取进程列表 - procs, err := process.Processes() - if err != nil { - return hidden - } - - gopsutilPIDs := make(map[int32]bool) - for _, p := range procs { - gopsutilPIDs[p.Pid] = true - } - - // 对比差异(简单检测) - for pid := range procPIDs { - if !gopsutilPIDs[pid] { - hidden = append(hidden, pid) - } - } - - return hidden -} - -// isMinerProcess 检查是否是挖矿进程 -func isMinerProcess(name, cmdline string) bool { - minerKeywords := []string{ - "xmrig", "minergate", "cpuminer", "ccminer", - "ethminer", "claymore", "phoenix", "t-rex", - "minerd", "stratum", "nicehash", "cryptonight", - } - - checkStr := strings.ToLower(name + " " + cmdline) - for _, keyword := range minerKeywords { - if strings.Contains(checkStr, keyword) { - return true - } - } - - // 检查挖矿命令特征 - if strings.Contains(cmdline, "--donate-level") || - strings.Contains(cmdline, "-o stratum+tcp") || - strings.Contains(cmdline, "--pool") && strings.Contains(cmdline, "--user") { - return true - } - - return false -} - -// isReverseShell 检查是否是反弹shell -func isReverseShell(cmdline string) bool { - patterns := []string{ - "bash -i >& /dev/tcp", - "nc -e /bin/bash", - "nc -e /bin/sh", - "python -c 'import socket", - "perl -e 'use Socket", - "/bin/sh -i", - "/bin/bash -i", - } - - for _, pattern := range patterns { - if strings.Contains(cmdline, pattern) { - return true - } - } - - return false -} - -// isProcessRunning 检查进程是否运行 -func isProcessRunning(name string) bool { - procs, err := process.Processes() - if err != nil { - return false - } - - for _, p := range procs { - pName, err := p.Name() - if err == nil && strings.Contains(pName, name) { - return true - } - } - - return false -} - -// checkAuthorizedKeysContent 检查authorized_keys文件内容(增强版) -func checkAuthorizedKeysContent() []string { - suspicious := []string{} - users := getAllUsers() - - for _, user := range users { - keyPath := filepath.Join(user.Home, ".ssh", "authorized_keys") - - // 检查文件是否存在 - info, err := os.Stat(keyPath) - if err != nil { - continue - } - - // 1. 检查最近30天内被修改(减少正常密钥管理操作的误报) - if time.Since(info.ModTime()) < 30*24*time.Hour { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 最近被修改: %s (%d天前)", - user.Username, info.ModTime().Format("2006-01-02 15:04:05"), int(time.Since(info.ModTime()).Hours()/24))) - } - - // 2. 检查文件内容 - content, err := os.ReadFile(keyPath) - if err != nil { - continue - } - - lines := strings.Split(string(content), "\n") - for lineNum, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - // 检查可疑的公钥选项 - if strings.Contains(line, "command=") { - // 有command选项可能是强制命令,需要review - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥包含command选项(行%d): %s...", - user.Username, lineNum+1, truncateString(line, 60))) - } - - if strings.Contains(line, "from=") { - // 检查from字段是否包含*通配符 - if strings.Contains(line, "from=\"*\"") || strings.Contains(line, "from=*") { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥允许任意来源(from=*): 行%d", user.Username, lineNum+1)) - } - } - - // 检查是否有多个公钥(可能被植入后门公钥) - // 简单统计:超过5个公钥就提示 - } - - keyCount := 0 - for _, line := range lines { - line = strings.TrimSpace(line) - if line != "" && !strings.HasPrefix(line, "#") && - (strings.HasPrefix(line, "ssh-rsa") || strings.HasPrefix(line, "ssh-ed25519") || strings.HasPrefix(line, "ecdsa-")) { - keyCount++ - } - } - - if keyCount > 5 { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 拥有大量公钥(%d个), 建议审查", user.Username, keyCount)) - } - - // 3. 检查文件权限(authorized_keys应该是600或644) - mode := info.Mode() - if mode.Perm() != 0600 && mode.Perm() != 0644 { - suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 权限异常: %o (应为600或644)", user.Username, mode.Perm())) - } - } - - return suspicious -} - -// truncateString 截断字符串 -func truncateString(s string, maxLen int) string { - if len(s) <= maxLen { - return s - } - return s[:maxLen] -} - -// readSSHConfig 读取SSH配置(改进版:统一转小写) -func readSSHConfig() map[string]string { - config := make(map[string]string) - - // 优先使用sshd -T - output, err := execCommand("sshd", "-T") - if err == nil { - lines := strings.Split(output, "\n") - for _, line := range lines { - parts := strings.Fields(line) - if len(parts) >= 2 { - // 键名统一转小写 - config[strings.ToLower(parts[0])] = parts[1] - } - } - return config - } - - // 备用:读配置文件 - configPaths := []string{"/etc/ssh/sshd_config", "/etc/sshd_config"} - for _, path := range configPaths { - content, err := os.ReadFile(path) - if err != nil { - continue - } - - lines := strings.Split(string(content), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - parts := strings.Fields(line) - if len(parts) >= 2 { - // 键名统一转小写 - config[strings.ToLower(parts[0])] = parts[1] - } - } - break - } - - return config -} - -// checkSuspiciousSSHD 检查可疑的sshd进程 -func checkSuspiciousSSHD() []string { - suspicious := []string{} - - procs, err := process.Processes() - if err != nil { - return suspicious - } - - sshdCount := 0 - for _, p := range procs { - name, _ := p.Name() - if strings.Contains(name, "sshd") { - sshdCount++ - exe, _ := p.Exe() - - // 检查sshd路径是否正常 - if exe != "" && !strings.HasPrefix(exe, "/usr/sbin/sshd") { - suspicious = append(suspicious, fmt.Sprintf("异常sshd路径: %s (PID: %d)", exe, p.Pid)) - } - } - } - - // 如果有多个sshd守护进程(不是子进程),可能异常 - if sshdCount > 10 { - suspicious = append(suspicious, fmt.Sprintf("sshd进程数异常: %d", sshdCount)) - } - - return suspicious -} - -// checkCronPath 检查cron目录 -func checkCronPath(cronPath string) []string { - var suspicious []string - - info, err := os.Stat(cronPath) - if err != nil { - return suspicious - } - - if info.IsDir() { - entries, err := os.ReadDir(cronPath) - if err != nil { - return suspicious - } - - for _, entry := range entries { - fullPath := filepath.Join(cronPath, entry.Name()) - content, err := os.ReadFile(fullPath) - if err != nil { - continue - } - - // 检查可疑命令 - contentStr := string(content) - if isSuspiciousCron(contentStr) { - suspicious = append(suspicious, fmt.Sprintf("%s: %s", fullPath, contentStr[:100])) - } - } - } else { - content, err := os.ReadFile(cronPath) - if err != nil { - return suspicious - } - - lines := strings.Split(string(content), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - if isSuspiciousCron(line) { - suspicious = append(suspicious, fmt.Sprintf("%s: %s", cronPath, line)) - } - } - } - - return suspicious -} - -// isSuspiciousCron 判断是否是可疑的cron任务(改进版:减少误报) -func isSuspiciousCron(content string) bool { - lowerContent := strings.ToLower(content) - - // 高危模式:下载后立即执行 - highRiskPatterns := []string{ - "curl.*|.*sh", // curl xxx | sh - "wget.*|.*sh", // wget xxx | sh - "curl.*|.*bash", // curl xxx | bash - "wget.*|.*bash", // wget xxx | bash - "curl.*&&.*chmod.*&&", // 下载->赋权->执行 - "wget.*&&.*chmod.*&&", // 下载->赋权->执行 - "base64 -d.*|.*sh", // Base64解码后执行 - "base64 -d.*|.*bash", // Base64解码后执行 - "bash -i", // 交互式shell - "sh -i", // 交互式shell - "/dev/tcp/", // Bash反弹shell - "nc.*-e", // Netcat反弹shell - "ncat.*-e", // Ncat反弹shell - "socat.*exec", // Socat执行命令 - "python.*socket", // Python反弹shell - "perl.*socket", // Perl反弹shell - } - - for _, pattern := range highRiskPatterns { - if strings.Contains(lowerContent, pattern) { - return true - } - } - - // 中危模式:可疑路径+执行 - if (strings.Contains(lowerContent, "/tmp/") || strings.Contains(lowerContent, "/dev/shm/")) && - (strings.Contains(lowerContent, "chmod +x") || strings.Contains(lowerContent, "bash") || strings.Contains(lowerContent, "sh ")) { - return true - } - - // 单独的curl/wget不算可疑(合法备份脚本常用) - return false -} - -// findExecutablesInDir 查找目录下的可执行文件 -func findExecutablesInDir(dir string) []string { - executables := []string{} - - entries, err := os.ReadDir(dir) - if err != nil { - return executables - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - - fullPath := filepath.Join(dir, entry.Name()) - info, err := entry.Info() - if err != nil { - continue - } - - // 检查是否可执行 - if info.Mode()&0111 != 0 { - executables = append(executables, fullPath) - } - } - - return executables -} - -// findHiddenExecutables 查找隐藏的可执行文件 -func findHiddenExecutables() []string { - hidden := []string{} - - searchDirs := []string{"/tmp", "/dev/shm", "/var/tmp"} - - for _, dir := range searchDirs { - entries, err := os.ReadDir(dir) - if err != nil { - continue - } - - for _, entry := range entries { - name := entry.Name() - if !strings.HasPrefix(name, ".") { - continue - } - - if entry.IsDir() { - continue - } - - fullPath := filepath.Join(dir, name) - info, err := entry.Info() - if err != nil { - continue - } - - if info.Mode()&0111 != 0 { - hidden = append(hidden, fullPath) - } - } - } - - return hidden -} - -// UserInfo 用户信息 -type UserInfo struct { - Username string - UID string - Home string - Shell string -} - -// getAllUsers 获取所有用户 -func getAllUsers() []UserInfo { - users := []UserInfo{} - - file, err := os.Open("/etc/passwd") - if err != nil { - return users - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, ":") - if len(parts) >= 7 { - users = append(users, UserInfo{ - Username: parts[0], - UID: parts[2], - Home: parts[5], - Shell: parts[6], - }) - } - } - - return users -} - -// findSuspiciousAccounts 查找可疑账户 -func findSuspiciousAccounts() []string { - suspicious := []string{} - - users := getAllUsers() - for _, user := range users { - uid, _ := strconv.Atoi(user.UID) - - // 系统账户(UID<1000)但有可登录shell - if uid < 1000 && user.Username != "root" { - // 合法的系统shell列表 - legitimateSystemShells := []string{ - "/sbin/nologin", - "/bin/false", - "/usr/sbin/nologin", - "/bin/sync", // sync账户使用 - "/sbin/shutdown", // shutdown账户使用 - "/sbin/halt", // halt账户使用 - } - - isLegitimate := false - for _, legitShell := range legitimateSystemShells { - if user.Shell == legitShell { - isLegitimate = true - break - } - } - - // 只有非合法shell才报告为可疑 - if !isLegitimate { - suspicious = append(suspicious, fmt.Sprintf("系统账户有可登录shell: %s (UID: %s, Shell: %s)", user.Username, user.UID, user.Shell)) - } - } - } - - return suspicious -} - -// findNoPasswordAccounts 查找无密码账户 -func findNoPasswordAccounts() []string { - noPassword := []string{} - - file, err := os.Open("/etc/shadow") - if err != nil { - return noPassword - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - parts := strings.Split(line, ":") - if len(parts) >= 2 { - username := parts[0] - password := parts[1] - - // 只有密码字段为空才是真正的无密码账户 - // "!" 和 "!!" 表示账户被锁定,这是安全的 - // "*" 表示无法使用密码登录(通常用于系统账户) - if password == "" { - noPassword = append(noPassword, username) - } - } - } - - return noPassword -} - -// findRootUIDAccounts 查找UID为0的非root账户 -func findRootUIDAccounts() []string { - rootUID := []string{} - - users := getAllUsers() - for _, user := range users { - if user.UID == "0" && user.Username != "root" { - rootUID = append(rootUID, user.Username) - } - } - - return rootUID -} - -// isMiningPool 检查是否连接到挖矿池 -func isMiningPool(ip string, port uint32) bool { - // 常见挖矿池端口(移除3333/5555/8888等误报端口) - // 3333: MySQL Cluster, 5555: ADB, 8888: Jupyter - // 只保留专用的挖矿池端口 - minerPorts := []uint32{14444, 45560} - - for _, p := range minerPorts { - if port == p { - return true - } - } - - // TODO: 未来可结合域名检测(如pool.supportxmr.com)提升准确度 - return false -} - -// isTorConnection 检查是否是TOR连接 -func isTorConnection(port uint32) bool { - torPorts := []uint32{9001, 9030, 9050, 9051, 9150} - for _, p := range torPorts { - if port == p { - return true - } - } - return false -} - -// execCommand 执行命令 -func execCommand(name string, args ...string) (string, error) { - cmd := exec.Command(name, args...) - var stdout bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stdout - - err := cmd.Run() - if err != nil { - return "", err - } - - return stdout.String(), nil -} - -// ========== 新增的辅助函数 ========== - -// calculateRiskScore 计算综合风险评分 -func calculateRiskScore(checks []protocol.SecurityCheck) (int, string, []string) { +// calculateRiskScore 计算综合风险评分(改进版) +func (a *Auditor) calculateRiskScore(checks []protocol.SecurityCheck) (int, string, []string) { score := 0 var recommendations []string + // 使用配置的权重 for _, check := range checks { + weight, exists := a.config.ScoringConfig.Weights[check.Category] + if !exists { + // 默认权重 + weight = CheckWeight{ + Category: check.Category, + FailScore: 20, + WarnScore: 5, + } + globalLogger.Warn("检查类别 '%s' 没有配置权重,使用默认值", check.Category) + } + for _, detail := range check.Details { switch detail.Status { case StatusFail: - score += 20 + score += weight.FailScore recommendations = append(recommendations, fmt.Sprintf("【紧急】%s: %s", check.Category, detail.Message)) case StatusWarn: - score += 5 + score += weight.WarnScore recommendations = append(recommendations, fmt.Sprintf("【警告】%s: %s", check.Category, detail.Message)) } } } - // 限制最大分数为100 + // 限制最大分数为 100 if score > 100 { score = 100 } - // 确定威胁等级 + // 确定威胁等级(使用配置的阈值) var level string switch { - case score >= 80: + case score >= a.config.ScoringConfig.CriticalThreshold: level = "critical" - case score >= 50: + case score >= a.config.ScoringConfig.HighThreshold: level = "high" - case score >= 20: + case score >= a.config.ScoringConfig.MediumThreshold: level = "medium" default: level = "low" } - // 限制建议数量,优先显示紧急的 - if len(recommendations) > 20 { - recommendations = recommendations[:20] + // 限制建议数量 + if len(recommendations) > a.config.ScoringConfig.MaxRecommendations { + recommendations = recommendations[:a.config.ScoringConfig.MaxRecommendations] } return score, level, recommendations } -// calculateSHA256 计算文件SHA256哈希 -func calculateSHA256(filePath string) string { - file, err := os.Open(filePath) - if err != nil { - return "" - } - defer file.Close() - - hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { - return "" - } - - return hex.EncodeToString(hash.Sum(nil)) +// RunAuditWithConfig 使用自定义配置执行审计 +func RunAuditWithConfig(config *Config) (*protocol.VPSAuditResult, error) { + auditor := NewAuditor(config) + return auditor.RunAudit() } -// buildProcessTree 构建进程树 -func buildProcessTree(p *process.Process) []string { - var tree []string - current := p - - // 向上追溯父进程(最多5层) - for i := 0; i < 5; i++ { - if current == nil { - break - } - - name, _ := current.Name() - exe, _ := current.Exe() - cmdline, _ := current.Cmdline() - - info := fmt.Sprintf("PID:%d Name:%s", current.Pid, name) - if exe != "" && exe != name { - info += fmt.Sprintf(" Exe:%s", exe) - } - if cmdline != "" && len(cmdline) < 100 { - info += fmt.Sprintf(" Cmd:%s", cmdline) - } - - tree = append([]string{info}, tree...) // 前置插入 - - ppid, err := current.Ppid() - if err != nil || ppid == 0 { - break - } - - parent, err := process.NewProcess(ppid) - if err != nil { - break - } - current = parent - } - - return tree -} - -// collectProcessEvidence 收集进程证据 -func collectProcessEvidence(p *process.Process, riskLevel string) *protocol.Evidence { - exe, _ := p.Exe() - createTime, _ := p.CreateTime() - - var fileHash string - if exe != "" && !strings.Contains(exe, "deleted") && !strings.Contains(exe, "memfd:") { - fileHash = calculateSHA256(exe) - } - - processTree := buildProcessTree(p) - - return &protocol.Evidence{ - FileHash: fileHash, - ProcessTree: processTree, - FilePath: exe, - Timestamp: createTime, - RiskLevel: riskLevel, - } -} - -// isLegitimateLibraryPath 检查是否是合法的库路径 -func isLegitimateLibraryPath(path, processName, processExe string) bool { - // 开发工具(IDE) - devTools := []string{ - "jetbrains", "vscode", "idea", "pycharm", "goland", "webstorm", - "clion", "rider", "datagrip", "androidstudio", - } - for _, tool := range devTools { - if strings.Contains(strings.ToLower(processExe), tool) { - return true - } - } - - // 数据库 - databases := []string{"postgres", "mysql", "mariadb", "mongodb", "redis", "elasticsearch", "clickhouse"} - for _, db := range databases { - if strings.Contains(strings.ToLower(processName), db) { - return true - } - } - - // 容器相关 - containers := []string{"docker", "containerd", "kubelet", "podman", "cri-o", "k3s", "kubernetes"} - for _, container := range containers { - if strings.Contains(strings.ToLower(processName), container) { - return true - } - } - - // Web服务器 - webServers := []string{"nginx", "apache", "httpd", "caddy", "traefik"} - for _, ws := range webServers { - if strings.Contains(strings.ToLower(processName), ws) { - return true - } - } - - // 科学计算和虚拟化(新增) - scientificAndVirt := []string{ - "matlab", "mathematica", "octave", "julia", // 科学计算 - "virtualbox", "qemu", "vmware", "kvm", "libvirt", // 虚拟化 - "datadog", "newrelic", "prometheus", "grafana", // 监控工具 - } - for _, tool := range scientificAndVirt { - if strings.Contains(strings.ToLower(processName), tool) || - strings.Contains(strings.ToLower(processExe), tool) { - return true - } - } - - // 系统库路径 - systemLibPaths := []string{ - "/usr/lib", "/lib", "/usr/local/lib", - "/opt/", "/usr/share", - } - for _, sysPath := range systemLibPaths { - if strings.HasPrefix(path, sysPath) { - return true - } - } - - return false -} - -// assessLibraryRisk 评估库路径的风险等级 -func assessLibraryRisk(path string) string { - // 高危路径 - highRiskPaths := []string{"/tmp/", "/dev/shm/", "/var/tmp/"} - for _, hrp := range highRiskPaths { - if strings.Contains(path, hrp) { - return "high" - } - } - - // 隐藏文件 - if strings.Contains(path, "/.") { - return "medium" - } - - return "low" -} - -// execCommandSafe 安全地执行命令(不使用shell) -func execCommandSafe(name string, args ...string) (string, error) { - cmd := exec.Command(name, args...) - var stdout bytes.Buffer - cmd.Stdout = &stdout - // 不捕获stderr,避免污染输出 - - err := cmd.Run() - if err != nil { - return "", err - } - - return stdout.String(), nil +// RunAudit 使用默认配置执行审计(保持向后兼容) +func RunAudit() (*protocol.VPSAuditResult, error) { + return RunAuditWithConfig(nil) } diff --git a/pkg/agent/audit/audit_test.go b/pkg/agent/audit/audit_test.go new file mode 100644 index 0000000..ac37ff2 --- /dev/null +++ b/pkg/agent/audit/audit_test.go @@ -0,0 +1,279 @@ +package audit + +import ( + "os" + "testing" + "time" +) + +func TestDefaultConfig(t *testing.T) { + config := DefaultConfig() + + if config == nil { + t.Fatal("DefaultConfig returned nil") + } + + // 验证关键配置 + if config.ProcessConfig.HighCPUThreshold != 70.0 { + t.Errorf("Expected HighCPUThreshold to be 70.0, got %f", config.ProcessConfig.HighCPUThreshold) + } + + if len(config.ProcessConfig.MinerKeywords) == 0 { + t.Error("MinerKeywords should not be empty") + } + + if config.ScoringConfig.CriticalThreshold != 80 { + t.Errorf("Expected CriticalThreshold to be 80, got %d", config.ScoringConfig.CriticalThreshold) + } +} + +func TestProcessCache(t *testing.T) { + cache := NewProcessCache(100 * time.Millisecond) + + // 第一次获取 + procs1, err := cache.Get() + if err != nil { + t.Fatalf("Failed to get processes: %v", err) + } + + if len(procs1) == 0 { + t.Error("Expected processes, got empty list") + } + + // 第二次获取(应该从缓存) + procs2, err := cache.Get() + if err != nil { + t.Fatalf("Failed to get cached processes: %v", err) + } + + // 应该是相同的引用 + if len(procs1) != len(procs2) { + t.Error("Cache should return same process list") + } + + // 等待缓存过期 + time.Sleep(150 * time.Millisecond) + + // 获取新的进程列表 + procs3, err := cache.Get() + if err != nil { + t.Fatalf("Failed to get refreshed processes: %v", err) + } + + if len(procs3) == 0 { + t.Error("Expected processes after cache expiry") + } +} + +func TestCommandExecutor(t *testing.T) { + executor := NewCommandExecutor(5 * time.Second) + + // 测试成功的命令 + output, err := executor.Execute("echo", "hello") + if err != nil { + t.Fatalf("Failed to execute command: %v", err) + } + + if output != "hello\n" { + t.Errorf("Expected 'hello\\n', got '%s'", output) + } + + // 测试超时 + executor2 := NewCommandExecutor(100 * time.Millisecond) + _, err = executor2.Execute("sleep", "1") + if err == nil { + t.Error("Expected timeout error") + } +} + +func TestFileHashCache(t *testing.T) { + cache := NewFileHashCache() + + // 创建临时文件 + tmpFile, err := os.CreateTemp("", "test-hash-*.txt") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + tmpFile.WriteString("test content") + tmpFile.Close() + + // 第一次计算 + hash1 := cache.GetSHA256(tmpFile.Name()) + if hash1 == "" { + t.Error("Expected hash, got empty string") + } + + // 第二次应该从缓存 + hash2 := cache.GetSHA256(tmpFile.Name()) + if hash1 != hash2 { + t.Error("Cache should return same hash") + } + + // 修改文件 + os.WriteFile(tmpFile.Name(), []byte("modified content"), 0644) + + // 应该计算新的哈希 + hash3 := cache.GetSHA256(tmpFile.Name()) + if hash3 == hash1 { + t.Error("Hash should be different after file modification") + } +} + +func TestStringUtils(t *testing.T) { + su := &StringUtils{} + + // 测试 Truncate + if result := su.Truncate("hello", 10); result != "hello" { + t.Errorf("Expected 'hello', got '%s'", result) + } + + if result := su.Truncate("hello world", 5); result != "hello" { + t.Errorf("Expected 'hello', got '%s'", result) + } + + // 测试 ContainsAny + keywords := []string{"foo", "bar", "baz"} + + if !su.ContainsAny("hello foobar", keywords) { + t.Error("Expected true for 'hello foobar'") + } + + if su.ContainsAny("hello world", keywords) { + t.Error("Expected false for 'hello world'") + } + + // 测试大小写不敏感 + if !su.ContainsAny("HELLO FOO", keywords) { + t.Error("Expected true for case-insensitive match") + } +} + +func TestIsLocalIP(t *testing.T) { + tests := []struct { + ip string + expected bool + }{ + {"127.0.0.1", true}, + {"::1", true}, + {"localhost", true}, + {"192.168.1.1", true}, + {"10.0.0.1", true}, + {"172.16.0.1", true}, + {"8.8.8.8", false}, + {"1.1.1.1", false}, + } + + for _, tt := range tests { + result := IsLocalIP(tt.ip) + if result != tt.expected { + t.Errorf("IsLocalIP(%s) = %v, expected %v", tt.ip, result, tt.expected) + } + } +} + +func TestBatchProcessor(t *testing.T) { + bp := NewBatchProcessor(3) + + items := []string{"a", "b", "c", "d", "e", "f", "g"} + processed := []string{} + + err := bp.Process(items, func(batch []string) error { + processed = append(processed, batch...) + return nil + }) + + if err != nil { + t.Fatalf("Process failed: %v", err) + } + + if len(processed) != len(items) { + t.Errorf("Expected %d items, got %d", len(items), len(processed)) + } + + // 验证顺序 + for i, item := range items { + if processed[i] != item { + t.Errorf("Order mismatch at index %d: expected %s, got %s", i, item, processed[i]) + } + } +} + +func TestWarningCollector(t *testing.T) { + wc := NewWarningCollector() + + wc.Add("warning 1") + wc.Add("warning 2") + wc.Add("warning 3") + + warnings := wc.GetAll() + + if len(warnings) != 3 { + t.Errorf("Expected 3 warnings, got %d", len(warnings)) + } + + if warnings[0] != "warning 1" { + t.Errorf("Expected 'warning 1', got '%s'", warnings[0]) + } +} + +// 集成测试(需要 root 权限和 Linux 环境) +func TestAuditorIntegration(t *testing.T) { + // 跳过如果不是 Linux 或没有 root 权限 + if os.Getenv("RUN_INTEGRATION_TESTS") != "true" { + t.Skip("Skipping integration test. Set RUN_INTEGRATION_TESTS=true to run") + } + + auditor := NewAuditor(nil) + + result, err := auditor.RunAudit() + if err != nil { + t.Fatalf("RunAudit failed: %v", err) + } + + if result == nil { + t.Fatal("Expected result, got nil") + } + + if len(result.SecurityChecks) == 0 { + t.Error("Expected security checks, got none") + } + + if result.RiskScore < 0 || result.RiskScore > 100 { + t.Errorf("Invalid risk score: %d", result.RiskScore) + } + + if result.ThreatLevel == "" { + t.Error("Threat level should not be empty") + } + + t.Logf("Audit completed in %dms", result.EndTime-result.StartTime) + t.Logf("Risk Score: %d, Threat Level: %s", result.RiskScore, result.ThreatLevel) + t.Logf("Security Checks: %d", len(result.SecurityChecks)) +} + +// 性能基准测试 +func BenchmarkProcessCache(b *testing.B) { + cache := NewProcessCache(1 * time.Second) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.Get() + } +} + +func BenchmarkFileHashCache(b *testing.B) { + cache := NewFileHashCache() + + // 创建临时文件 + tmpFile, _ := os.CreateTemp("", "bench-*.txt") + tmpFile.WriteString("test content for benchmarking") + tmpFile.Close() + defer os.Remove(tmpFile.Name()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + cache.GetSHA256(tmpFile.Name()) + } +} diff --git a/pkg/agent/audit/check_misc.go b/pkg/agent/audit/check_misc.go new file mode 100644 index 0000000..df94bb6 --- /dev/null +++ b/pkg/agent/audit/check_misc.go @@ -0,0 +1,419 @@ +package audit + +import ( + "bufio" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" +) + +// 这个文件包含文件检查、账户检查、Cron 检查和登录历史检查 +// 为简化重构,将这些相对独立的检查整合在一起 + +// FileChecker 文件检查器 +type FileChecker struct { + config *Config + evidence *EvidenceCollector + executor *CommandExecutor +} + +// NewFileChecker 创建文件检查器 +func NewFileChecker(config *Config, evidence *EvidenceCollector, executor *CommandExecutor) *FileChecker { + return &FileChecker{ + config: config, + evidence: evidence, + executor: executor, + } +} + +// CheckSuspiciousFiles 检查可疑文件 +func (fc *FileChecker) CheckSuspiciousFiles() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "suspicious_files", + Status: StatusPass, + Message: "可疑文件检查", + Details: []protocol.SecurityCheckSub{}, + } + + suspiciousCount := 0 + + // 检查临时目录下的可执行文件 + for _, dir := range fc.config.FileConfig.TempDirs { + files := fc.findSuspiciousExecutables(dir) + if len(files) > 0 { + check.Status = StatusWarn + for _, file := range files { + suspiciousCount++ + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "tmp_executable", + Status: StatusWarn, + Message: file, + }) + } + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "files_clean", + Status: StatusPass, + Message: "未发现可疑文件", + }) + check.Message = "文件系统正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑文件", suspiciousCount) + } + + return check +} + +// CheckFileIntegrity 检查系统文件完整性(修复bug版本) +func (fc *FileChecker) CheckFileIntegrity() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "file_integrity", + Status: StatusPass, + Message: "系统文件完整性检查", + Details: []protocol.SecurityCheckSub{}, + } + + suspiciousCount := 0 + + // 1. 检查 /etc/ld.so.preload + if content, err := os.ReadFile("/etc/ld.so.preload"); err == nil { + contentStr := strings.TrimSpace(string(content)) + if contentStr != "" { + suspiciousCount++ + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ld_preload_exists", + Status: StatusFail, + Message: fmt.Sprintf("发现动态链接库劫持配置 (/etc/ld.so.preload): %s", contentStr), + Evidence: &protocol.Evidence{ + FilePath: "/etc/ld.so.preload", + RiskLevel: "critical", + }, + }) + } + } + + // 2. 检查系统文件最近修改时间 + recentModifiedCount := 0 + for _, binPath := range fc.config.FileConfig.CriticalBinaries { + if _, err := os.Stat(binPath); err != nil { + continue + } + + isModified, verifyOutput := fc.verifyFileByModTime(binPath) + if isModified { + recentModifiedCount++ + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "recently_modified_binary", + Status: StatusWarn, + Message: verifyOutput, + }) + } + } + + if suspiciousCount > 0 || recentModifiedCount > 0 { + check.Message = fmt.Sprintf("发现 %d 个文件完整性异常", suspiciousCount+recentModifiedCount) + } else { + check.Message = "核心文件完整性正常" + } + + return check +} + +// CheckImmutableFiles 检查不可变文件 +func (fc *FileChecker) CheckImmutableFiles() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "immutable_files", + Status: StatusPass, + Message: "不可变文件检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 检查 lsattr 是否可用 + if _, err := exec.LookPath("lsattr"); err != nil { + check.Status = StatusSkip + check.Message = "lsattr 命令不可用,无法检查不可变属性" + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "lsattr_unavailable", + Status: StatusSkip, + Message: "系统未安装 lsattr 工具(需要 e2fsprogs 包)", + }) + return check + } + + suspiciousCount := 0 + + for _, file := range fc.config.FileConfig.ImmutableCheckFiles { + if _, err := os.Stat(file); err != nil { + continue + } + + cmd := exec.Command("lsattr", "-d", file) + output, err := cmd.Output() + if err != nil { + continue + } + + outStr := string(output) + if strings.Contains(outStr, "-i-") || strings.Contains(outStr, "----i") { + suspiciousCount++ + check.Status = StatusFail + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "immutable_file_found", + Status: StatusFail, + Message: fmt.Sprintf("关键文件被锁定(不可变): %s (可能是Rootkit保护)", file), + Evidence: &protocol.Evidence{ + FilePath: file, + RiskLevel: "high", + }, + }) + } + } + + if suspiciousCount > 0 { + check.Message = fmt.Sprintf("发现 %d 个异常锁定的系统文件", suspiciousCount) + } else { + check.Message = "未发现被恶意锁定的文件" + } + + return check +} + +// findSuspiciousExecutables 查找可疑的可执行文件 +func (fc *FileChecker) findSuspiciousExecutables(dir string) []string { + var suspicious []string + + entries, err := os.ReadDir(dir) + if err != nil { + return suspicious + } + + now := time.Now() + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fullPath := filepath.Join(dir, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + // 检查是否可执行 + if info.Mode()&0111 != 0 { + age := now.Sub(info.ModTime()) + sizeKB := info.Size() / 1024 + sizeMB := info.Size() / 1024 / 1024 + + if age < time.Duration(fc.config.FileConfig.RecentExecutableHours)*time.Hour { + suspicious = append(suspicious, fmt.Sprintf("%s 下的最近可执行文件: %s (大小: %dKB, 创建时间: %s)", + dir, fullPath, sizeKB, info.ModTime().Format("2006-01-02 15:04:05"))) + } else if sizeMB > fc.config.FileConfig.LargeExecutableMB { + suspicious = append(suspicious, fmt.Sprintf("%s 下的大型可执行文件: %s (大小: %dMB)", + dir, fullPath, sizeMB)) + } + } + } + + return suspicious +} + +// verifyFileByModTime 通过修改时间检查文件 +func (fc *FileChecker) verifyFileByModTime(filePath string) (bool, string) { + info, err := os.Stat(filePath) + if err != nil { + return false, "" + } + + modTime := info.ModTime() + age := time.Since(modTime) + + // 如果在最近 24 小时内被修改,标记为可疑 + if age < 24*time.Hour { + return true, fmt.Sprintf("系统命令最近24小时内有更新: %s (请确认是否为正常运维)", filePath) + } + + return false, "" +} + +// AccountChecker 账户检查器 +type AccountChecker struct { + config *Config +} + +// NewAccountChecker 创建账户检查器 +func NewAccountChecker(config *Config) *AccountChecker { + return &AccountChecker{config: config} +} + +// CheckSystemAccounts 检查系统账户异常 +func (ac *AccountChecker) CheckSystemAccounts() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "system_accounts", + Status: StatusPass, + Message: "系统账户检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 1. 检查可疑账户 + suspiciousAccounts := ac.findSuspiciousAccounts() + for _, acc := range suspiciousAccounts { + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_account", + Status: StatusWarn, + Message: acc, + }) + } + + // 2. 检查无密码账户 + noPasswordAccounts := ac.findNoPasswordAccounts() + for _, acc := range noPasswordAccounts { + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "no_password_account", + Status: StatusFail, + Message: fmt.Sprintf("无密码账户: %s", acc), + }) + } + + // 3. 检查 UID 为 0 的非 root 账户 + rootUIDAccounts := ac.findRootUIDAccounts() + for _, acc := range rootUIDAccounts { + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "uid0_account", + Status: StatusFail, + Message: fmt.Sprintf("UID为0的非root账户: %s", acc), + }) + } + + if check.Status == StatusPass { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "accounts_clean", + Status: StatusPass, + Message: "系统账户正常", + }) + check.Message = "账户配置正常" + } else if check.Status == StatusWarn { + check.Message = "发现可疑账户" + } else { + check.Message = "发现严重账户安全问题" + } + + return check +} + +// findSuspiciousAccounts 查找可疑账户 +func (ac *AccountChecker) findSuspiciousAccounts() []string { + var suspicious []string + + file, err := os.Open("/etc/passwd") + if err != nil { + return suspicious + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) < 7 { + continue + } + + username := parts[0] + uidStr := parts[2] + shell := parts[6] + + uid, _ := strconv.Atoi(uidStr) + + // 系统账户(UID<1000)但有可登录 shell + if uid < 1000 && username != "root" { + legitimateShells := []string{ + "/sbin/nologin", "/bin/false", "/usr/sbin/nologin", + "/bin/sync", "/sbin/shutdown", "/sbin/halt", + } + + isLegitimate := false + for _, legitShell := range legitimateShells { + if shell == legitShell { + isLegitimate = true + break + } + } + + if !isLegitimate { + suspicious = append(suspicious, fmt.Sprintf("系统账户有可登录shell: %s (UID: %s, Shell: %s)", username, uidStr, shell)) + } + } + } + + return suspicious +} + +// findNoPasswordAccounts 查找无密码账户 +func (ac *AccountChecker) findNoPasswordAccounts() []string { + var noPassword []string + + file, err := os.Open("/etc/shadow") + if err != nil { + return noPassword + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) >= 2 { + username := parts[0] + password := parts[1] + + if password == "" { + noPassword = append(noPassword, username) + } + } + } + + return noPassword +} + +// findRootUIDAccounts 查找 UID 为 0 的非 root 账户 +func (ac *AccountChecker) findRootUIDAccounts() []string { + var rootUID []string + + file, err := os.Open("/etc/passwd") + if err != nil { + return rootUID + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) >= 3 { + username := parts[0] + uid := parts[2] + + if uid == "0" && username != "root" { + rootUID = append(rootUID, username) + } + } + } + + return rootUID +} diff --git a/pkg/agent/audit/check_network.go b/pkg/agent/audit/check_network.go new file mode 100644 index 0000000..3c24fbb --- /dev/null +++ b/pkg/agent/audit/check_network.go @@ -0,0 +1,270 @@ +package audit + +import ( + "fmt" + "strings" + + "github.com/dushixiang/pika/internal/protocol" + gopsutilNet "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" +) + +// NetworkChecker 网络检查器 +type NetworkChecker struct { + config *Config + cache *ProcessCache + evidence *EvidenceCollector + strUtil *StringUtils +} + +// NewNetworkChecker 创建网络检查器 +func NewNetworkChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector) *NetworkChecker { + return &NetworkChecker{ + config: config, + cache: cache, + evidence: evidence, + strUtil: &StringUtils{}, + } +} + +// CheckListeningPorts 检查异常监听端口 +func (nc *NetworkChecker) CheckListeningPorts() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "listening_ports", + Status: StatusPass, + Message: "端口监听检查", + Details: []protocol.SecurityCheckSub{}, + } + + connections, err := gopsutilNet.Connections("all") + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取网络连接信息" + globalLogger.Error("无法获取网络连接信息: %v", err) + return check + } + + suspiciousCount := 0 + totalListening := 0 + publicListening := 0 + + for _, conn := range connections { + if conn.Status != "LISTEN" { + continue + } + + totalListening++ + port := conn.Laddr.Port + listenAddr := conn.Laddr.IP + pid := conn.Pid + + // 本地回环监听的服务一般是安全的,跳过 + if listenAddr == "127.0.0.1" || listenAddr == "::1" { + continue + } + + publicListening++ + + // 获取进程信息 + if pid <= 0 { + continue + } + + proc, err := process.NewProcess(int32(pid)) + if err != nil { + continue + } + + name, _ := proc.Name() + exe, _ := proc.Exe() + cmdline, _ := proc.Cmdline() + + // 检查 1: 高危端口 + if desc, exists := nc.config.NetworkConfig.SuspiciousPorts[port]; exists { + suspiciousCount++ + check.Status = StatusWarn + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("suspicious_port_%d", port), + Status: StatusWarn, + Message: fmt.Sprintf("高危端口监听: %s:%d (%s) - 进程: %s (PID: %d)", listenAddr, port, desc, name, pid), + Evidence: nc.evidence.CollectProcessEvidence(proc, "high"), + }) + continue + } + + // 检查 2: 挖矿程序监听端口 + if nc.isMinerProcess(name, cmdline) { + suspiciousCount++ + check.Status = StatusFail + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("miner_listener_%d", port), + Status: StatusFail, + Message: fmt.Sprintf("挖矿程序监听端口: %s 监听 %s:%d (PID: %d)", name, listenAddr, port, pid), + Evidence: nc.evidence.CollectProcessEvidence(proc, "high"), + }) + continue + } + + // 检查 3: 可疑路径的程序监听端口 + if nc.isSuspiciousPath(exe) { + suspiciousCount++ + check.Status = StatusFail + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("suspicious_exe_listener_%d", port), + Status: StatusFail, + Message: fmt.Sprintf("可疑路径程序监听端口: %s 监听 %s:%d (PID: %d, 路径: %s)", name, listenAddr, port, pid, exe), + Evidence: nc.evidence.CollectProcessEvidence(proc, "high"), + }) + continue + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "safe_ports", + Status: StatusPass, + Message: fmt.Sprintf("监听%d个端口(公网%d个),未发现可疑监听", totalListening, publicListening), + }) + check.Message = "端口监听正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑端口监听", suspiciousCount) + } + + return check +} + +// CheckNetworkConnections 检查异常网络连接 +func (nc *NetworkChecker) CheckNetworkConnections() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "network_connections", + Status: StatusPass, + Message: "网络连接检查", + Details: []protocol.SecurityCheckSub{}, + } + + connections, err := gopsutilNet.Connections("all") + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取网络连接" + globalLogger.Error("无法获取网络连接: %v", err) + return check + } + + suspiciousCount := 0 + establishedCount := 0 + + for _, conn := range connections { + if conn.Status != "ESTABLISHED" { + continue + } + + establishedCount++ + + // 获取进程信息 + var processName string + var processPath string + if conn.Pid > 0 { + proc, err := process.NewProcess(int32(conn.Pid)) + if err == nil { + processName, _ = proc.Name() + processPath, _ = proc.Exe() + } + } + + // 检查 1: 连接到挖矿池 + if nc.isMiningPool(conn.Raddr.IP, conn.Raddr.Port) { + suspiciousCount++ + check.Status = StatusFail + + msg := fmt.Sprintf("连接到挖矿池: %s:%d", conn.Raddr.IP, conn.Raddr.Port) + if processName != "" { + msg += fmt.Sprintf(" (进程: %s, PID: %d)", processName, conn.Pid) + } + + var evidence *protocol.Evidence + if conn.Pid > 0 { + proc, _ := process.NewProcess(int32(conn.Pid)) + if proc != nil { + evidence = nc.evidence.CollectProcessEvidence(proc, "high") + } + } + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "mining_connection", + Status: StatusFail, + Message: msg, + Evidence: evidence, + }) + } + + // 检查 2: 可疑路径的进程外连 + if processPath != "" && nc.isSuspiciousPath(processPath) { + suspiciousCount++ + check.Status = StatusFail + + var evidence *protocol.Evidence + if conn.Pid > 0 { + proc, _ := process.NewProcess(int32(conn.Pid)) + if proc != nil { + evidence = nc.evidence.CollectProcessEvidence(proc, "high") + } + } + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_path_connection", + Status: StatusFail, + Message: fmt.Sprintf("可疑路径进程外连: %s (PID:%d) -> %s:%d", processPath, conn.Pid, conn.Raddr.IP, conn.Raddr.Port), + Evidence: evidence, + }) + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "connections_clean", + Status: StatusPass, + Message: fmt.Sprintf("共 %d 个活动连接,未发现异常", establishedCount), + }) + check.Message = "网络连接正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑连接", suspiciousCount) + } + + return check +} + +// isSuspiciousPath 检查可执行文件路径是否可疑 +func (nc *NetworkChecker) isSuspiciousPath(path string) bool { + if path == "" { + return false + } + + for _, sp := range nc.config.FileConfig.SuspiciousPaths { + if strings.Contains(path, sp) { + return true + } + } + + return false +} + +// isMinerProcess 检查是否是挖矿进程 +func (nc *NetworkChecker) isMinerProcess(name, cmdline string) bool { + return nc.strUtil.ContainsAny(name+" "+cmdline, nc.config.ProcessConfig.MinerKeywords) +} + +// isMiningPool 检查是否连接到挖矿池 +func (nc *NetworkChecker) isMiningPool(ip string, port uint32) bool { + // 检查端口 + for _, p := range nc.config.NetworkConfig.MinerPorts { + if port == p { + return true + } + } + + // TODO: 可以添加域名检查(需要反向 DNS) + return false +} diff --git a/pkg/agent/audit/check_process.go b/pkg/agent/audit/check_process.go new file mode 100644 index 0000000..973fbb3 --- /dev/null +++ b/pkg/agent/audit/check_process.go @@ -0,0 +1,380 @@ +package audit + +import ( + "fmt" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/process" +) + +// ProcessChecker 进程检查器 +type ProcessChecker struct { + config *Config + cache *ProcessCache + evidence *EvidenceCollector +} + +// NewProcessChecker 创建进程检查器 +func NewProcessChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector) *ProcessChecker { + return &ProcessChecker{ + config: config, + cache: cache, + evidence: evidence, + } +} + +// CheckSuspiciousProcesses 检查可疑进程 +func (pc *ProcessChecker) CheckSuspiciousProcesses() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "suspicious_processes", + Status: StatusPass, + Message: "可疑进程检测", + Details: []protocol.SecurityCheckSub{}, + } + + procs, err := pc.cache.Get() + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取进程列表" + globalLogger.Error("无法获取进程列表: %v", err) + return check + } + + suspiciousCount := 0 + + for _, p := range procs { + name, _ := p.Name() + cmdline, _ := p.Cmdline() + exe, _ := p.Exe() + + // 1. 检查挖矿 + if pc.isMinerProcess(name, cmdline) { + suspiciousCount++ + check.Status = StatusFail + + cpuPercent, _ := p.CPUPercent() + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "miner_process", + Status: StatusFail, + Message: fmt.Sprintf("检测到挖矿程序: %s (PID: %d, CPU: %.1f%%)", name, p.Pid, cpuPercent), + Evidence: pc.evidence.CollectProcessEvidence(p, "high"), + }) + continue + } + + // 2. 检查高 CPU + 网络连接的可疑进程 + if pc.isHighCPUMinerSuspect(p, name, exe) { + suspiciousCount++ + check.Status = StatusWarn + + cpuPercent, _ := p.CPUPercent() + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "high_cpu_network_process", + Status: StatusWarn, + Message: fmt.Sprintf("高CPU进程持续网络连接: %s (PID: %d, CPU: %.1f%%) - 疑似挖矿", name, p.Pid, cpuPercent), + Evidence: pc.evidence.CollectProcessEvidence(p, "medium"), + }) + continue + } + + // 3. 检查反弹 Shell + if pc.isReverseShell(cmdline) { + suspiciousCount++ + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "reverse_shell", + Status: StatusFail, + Message: fmt.Sprintf("检测到反弹shell: %s (PID: %d)", cmdline, p.Pid), + Evidence: pc.evidence.CollectProcessEvidence(p, "high"), + }) + continue + } + + // 4. 检查无文件进程/被删除的二进制 + if strings.Contains(exe, "(deleted)") { + // 检查白名单 + if pc.isInDeletedWhitelist(name) { + continue + } + + // 检查启动时间 + createTime, _ := p.CreateTime() + uptime := time.Now().UnixMilli() - createTime + if uptime < int64(pc.config.ProcessConfig.RecentStartupHours)*60*60*1000 { + suspiciousCount++ + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "deleted_binary", + Status: StatusWarn, + Message: fmt.Sprintf("可疑的已删除进程: %s (PID: %d, 路径: %s) - 最近%dh内启动", name, p.Pid, exe, pc.config.ProcessConfig.RecentStartupHours), + Evidence: pc.evidence.CollectProcessEvidence(p, "medium"), + }) + } + } + + // 5. 内存执行检测 (memfd) + if strings.Contains(exe, "memfd:") { + // 排除 runc / kubelet 等容器组件 + if !strings.Contains(cmdline, "runc") && !strings.Contains(name, "containerd") { + suspiciousCount++ + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "fileless_memfd", + Status: StatusFail, + Message: fmt.Sprintf("检测到无文件内存进程: %s (PID: %d)", name, p.Pid), + Evidence: pc.evidence.CollectProcessEvidence(p, "high"), + }) + } + } + } + + if suspiciousCount == 0 { + check.Message = "进程行为正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑进程", suspiciousCount) + } + + return check +} + +// CheckSuspiciousEnvVars 检查进程的可疑环境变量 +func (pc *ProcessChecker) CheckSuspiciousEnvVars() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "suspicious_env_vars", + Status: StatusPass, + Message: "进程环境变量检查", + Details: []protocol.SecurityCheckSub{}, + } + + procs, err := pc.cache.Get() + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取进程列表" + return check + } + + suspiciousCount := 0 + for _, p := range procs { + env, err := p.Environ() + if err != nil { + continue + } + + name, _ := p.Name() + exe, _ := p.Exe() + + for _, e := range env { + if strings.HasPrefix(e, "LD_PRELOAD=") || strings.HasPrefix(e, "LD_LIBRARY_PATH=") { + parts := strings.SplitN(e, "=", 2) + if len(parts) != 2 { + continue + } + value := parts[1] + + // 检查是否是合法的库路径 + if pc.isLegitimateLibraryPath(value, name, exe) { + continue + } + + // 评估风险等级 + riskLevel := pc.assessLibraryRisk(value) + status := StatusWarn + if riskLevel == "high" { + status = StatusFail + check.Status = StatusFail + } else if check.Status != StatusFail { + check.Status = StatusWarn + } + + suspiciousCount++ + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_ld_env", + Status: status, + Message: fmt.Sprintf("进程 '%s'(PID: %d) 发现可疑环境变量: %s (风险等级: %s)", name, p.Pid, e, riskLevel), + Evidence: pc.evidence.CollectProcessEvidence(p, riskLevel), + }) + break + } + } + } + + if suspiciousCount == 0 { + check.Message = "未发现可疑的进程环境变量" + } else { + check.Message = fmt.Sprintf("发现 %d 个进程存在可疑环境变量", suspiciousCount) + } + + return check +} + +// isMinerProcess 检查是否是挖矿进程 +func (pc *ProcessChecker) isMinerProcess(name, cmdline string) bool { + checkStr := strings.ToLower(name + " " + cmdline) + + // 检查关键词 + for _, keyword := range pc.config.ProcessConfig.MinerKeywords { + if strings.Contains(checkStr, keyword) { + return true + } + } + + // 检查挖矿命令特征 + if strings.Contains(cmdline, "--donate-level") || + strings.Contains(cmdline, "-o stratum+tcp") || + (strings.Contains(cmdline, "--pool") && strings.Contains(cmdline, "--user")) { + return true + } + + return false +} + +// isHighCPUMinerSuspect 检查是否是高 CPU 挖矿嫌疑进程 +func (pc *ProcessChecker) isHighCPUMinerSuspect(p *process.Process, name, exe string) bool { + cpuPercent, err := p.CPUPercent() + if err != nil || cpuPercent < pc.config.ProcessConfig.HighCPUThreshold { + return false + } + + // 检查白名单 + nameLower := strings.ToLower(name) + exeLower := strings.ToLower(exe) + + for _, whitelisted := range pc.config.ProcessConfig.HighCPUWhitelist { + if strings.Contains(nameLower, whitelisted) || strings.Contains(exeLower, whitelisted) { + return false + } + } + + // 检查是否有网络连接 + connections, err := p.Connections() + if err != nil || len(connections) == 0 { + return false + } + + // 检查是否有 ESTABLISHED 状态的外部连接 + for _, conn := range connections { + if conn.Status == "ESTABLISHED" && !IsLocalIP(conn.Raddr.IP) { + return true + } + } + + return false +} + +// isReverseShell 检查是否是反弹 shell +func (pc *ProcessChecker) isReverseShell(cmdline string) bool { + patterns := []string{ + "bash -i >& /dev/tcp", + "nc -e /bin/bash", + "nc -e /bin/sh", + "python -c 'import socket", + "perl -e 'use Socket", + "/bin/sh -i", + "/bin/bash -i", + } + + for _, pattern := range patterns { + if strings.Contains(cmdline, pattern) { + return true + } + } + + return false +} + +// isInDeletedWhitelist 检查是否在 deleted 进程白名单中 +func (pc *ProcessChecker) isInDeletedWhitelist(name string) bool { + for _, safeName := range pc.config.ProcessConfig.DeletedWhitelist { + if strings.Contains(strings.ToLower(name), safeName) { + return true + } + } + return false +} + +// isLegitimateLibraryPath 检查是否是合法的库路径 +func (pc *ProcessChecker) isLegitimateLibraryPath(path, processName, processExe string) bool { + // 开发工具(IDE) + devTools := []string{ + "jetbrains", "vscode", "idea", "pycharm", "goland", "webstorm", + "clion", "rider", "datagrip", "androidstudio", + } + for _, tool := range devTools { + if strings.Contains(strings.ToLower(processExe), tool) { + return true + } + } + + // 数据库 + databases := []string{"postgres", "mysql", "mariadb", "mongodb", "redis", "elasticsearch", "clickhouse"} + for _, db := range databases { + if strings.Contains(strings.ToLower(processName), db) { + return true + } + } + + // 容器相关 + containers := []string{"docker", "containerd", "kubelet", "podman", "cri-o", "k3s", "kubernetes"} + for _, container := range containers { + if strings.Contains(strings.ToLower(processName), container) { + return true + } + } + + // Web 服务器 + webServers := []string{"nginx", "apache", "httpd", "caddy", "traefik"} + for _, ws := range webServers { + if strings.Contains(strings.ToLower(processName), ws) { + return true + } + } + + // 科学计算和虚拟化 + scientificAndVirt := []string{ + "matlab", "mathematica", "octave", "julia", // 科学计算 + "virtualbox", "qemu", "vmware", "kvm", "libvirt", // 虚拟化 + "datadog", "newrelic", "prometheus", "grafana", // 监控工具 + } + for _, tool := range scientificAndVirt { + if strings.Contains(strings.ToLower(processName), tool) || + strings.Contains(strings.ToLower(processExe), tool) { + return true + } + } + + // 系统库路径 + systemLibPaths := []string{ + "/usr/lib", "/lib", "/usr/local/lib", + "/opt/", "/usr/share", + } + for _, sysPath := range systemLibPaths { + if strings.HasPrefix(path, sysPath) { + return true + } + } + + return false +} + +// assessLibraryRisk 评估库路径的风险等级 +func (pc *ProcessChecker) assessLibraryRisk(path string) string { + // 高危路径 + highRiskPaths := []string{"/tmp/", "/dev/shm/", "/var/tmp/"} + for _, hrp := range highRiskPaths { + if strings.Contains(path, hrp) { + return "high" + } + } + + // 隐藏文件 + if strings.Contains(path, "/.") { + return "medium" + } + + return "low" +} diff --git a/pkg/agent/audit/check_rootkit.go b/pkg/agent/audit/check_rootkit.go new file mode 100644 index 0000000..064aed4 --- /dev/null +++ b/pkg/agent/audit/check_rootkit.go @@ -0,0 +1,207 @@ +package audit + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" +) + +// RootkitChecker rootkit 检测器 +type RootkitChecker struct { + config *Config + cache *ProcessCache + evidence *EvidenceCollector +} + +// NewRootkitChecker 创建 rootkit 检测器 +func NewRootkitChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector) *RootkitChecker { + return &RootkitChecker{ + config: config, + cache: cache, + evidence: evidence, + } +} + +// Check 检测 rootkit 和恶意内核模块 +func (rc *RootkitChecker) Check() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "rootkit_detection", + Status: StatusPass, + Message: "Rootkit检测", + Details: []protocol.SecurityCheckSub{}, + } + + // 检查加载的内核模块 + suspiciousModules := rc.checkKernelModules() + if len(suspiciousModules) > 0 { + check.Status = StatusWarn + for _, mod := range suspiciousModules { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_module", + Status: StatusWarn, + Message: fmt.Sprintf("可疑内核模块: %s", mod), + }) + } + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "kernel_modules", + Status: StatusPass, + Message: "未发现可疑内核模块", + }) + } + + // 检查隐藏进程 + hiddenProcs := rc.checkHiddenProcesses() + if len(hiddenProcs) > 0 { + check.Status = StatusFail + for _, pid := range hiddenProcs { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "hidden_process", + Status: StatusFail, + Message: fmt.Sprintf("检测到隐藏进程: PID %d", pid), + }) + } + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "hidden_processes", + Status: StatusPass, + Message: "未发现隐藏进程", + }) + } + + // 更新总体消息 + switch check.Status { + case StatusPass: + check.Message = "未检测到rootkit" + case StatusWarn: + check.Message = "发现可疑rootkit特征" + case StatusFail: + check.Message = "发现rootkit感染迹象" + } + + return check +} + +// checkKernelModules 检查可疑内核模块 +func (rc *RootkitChecker) checkKernelModules() []string { + var suspicious []string + + content, err := os.ReadFile("/proc/modules") + if err != nil { + globalLogger.Warn("无法读取 /proc/modules: %v", err) + return suspicious + } + + // 已知的恶意模块名称特征 + maliciousPatterns := []string{ + "rootkit", "backdoor", "hidden", "stealth", + "diamorphine", "suterusu", "reptile", + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) > 0 { + modName := fields[0] + for _, pattern := range maliciousPatterns { + if strings.Contains(strings.ToLower(modName), pattern) { + suspicious = append(suspicious, modName) + globalLogger.Warn("发现可疑内核模块: %s", modName) + break + } + } + } + } + + return suspicious +} + +// checkHiddenProcesses 检查隐藏进程(优化版:二次确认机制) +func (rc *RootkitChecker) checkHiddenProcesses() []int32 { + // 第一轮快照 + hiddenCandidates := rc.getHiddenCandidates() + + if len(hiddenCandidates) == 0 { + return []int32{} + } + + globalLogger.Debug("发现 %d 个隐藏进程候选,开始二次确认", len(hiddenCandidates)) + + // 等待 100ms,消除进程刚好退出的时间差 + time.Sleep(100 * time.Millisecond) + + // 第二轮确认 + finalHidden := []int32{} + + // 重新获取当前的 gopsutil 列表 + procs, err := rc.cache.Get() + if err != nil { + globalLogger.Warn("二次确认时无法获取进程列表: %v", err) + return []int32{} + } + + currentApiPids := make(map[int32]bool) + for _, p := range procs { + currentApiPids[p.Pid] = true + } + + for _, pid := range hiddenCandidates { + // 检查 /proc/pid 是否依然存在 + if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil { + // /proc 下存在,但 API 列表里依然没有 + if !currentApiPids[pid] { + finalHidden = append(finalHidden, pid) + globalLogger.Warn("确认隐藏进程: PID %d", pid) + } + } + } + + return finalHidden +} + +// getHiddenCandidates 获取初步的隐藏进程候选 +func (rc *RootkitChecker) getHiddenCandidates() []int32 { + var hidden []int32 + + // 1. 扫描 /proc + procPids := make(map[int32]bool) + entries, err := os.ReadDir("/proc") + if err != nil { + globalLogger.Warn("无法读取 /proc: %v", err) + return hidden + } + + for _, entry := range entries { + if entry.IsDir() { + pid, err := strconv.Atoi(entry.Name()) + if err == nil { + procPids[int32(pid)] = true + } + } + } + + // 2. 扫描 API (gopsutil) + apiPids := make(map[int32]bool) + procs, err := rc.cache.Get() + if err != nil { + globalLogger.Warn("无法获取进程列表: %v", err) + return hidden + } + + for _, p := range procs { + apiPids[p.Pid] = true + } + + // 3. 对比 + for pid := range procPids { + if !apiPids[pid] { + hidden = append(hidden, pid) + } + } + + return hidden +} diff --git a/pkg/agent/audit/check_ssh.go b/pkg/agent/audit/check_ssh.go new file mode 100644 index 0000000..ed66335 --- /dev/null +++ b/pkg/agent/audit/check_ssh.go @@ -0,0 +1,431 @@ +package audit + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" +) + +// SSHChecker SSH 安全检查器 +type SSHChecker struct { + config *Config + cache *ProcessCache + evidence *EvidenceCollector + executor *CommandExecutor + strUtil *StringUtils +} + +// NewSSHChecker 创建 SSH 检查器 +func NewSSHChecker(config *Config, cache *ProcessCache, evidence *EvidenceCollector, executor *CommandExecutor) *SSHChecker { + return &SSHChecker{ + config: config, + cache: cache, + evidence: evidence, + executor: executor, + strUtil: &StringUtils{}, + } +} + +// Check 检查 SSH 后门和安全配置 +func (sc *SSHChecker) Check() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "ssh_security", + Status: StatusPass, + Message: "SSH安全检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 检查 SSH 服务是否运行 + if !sc.isSSHRunning() { + check.Message = "SSH服务未运行" + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ssh_status", + Status: StatusPass, + Message: "SSH未运行", + }) + return check + } + + // 1. 检查 authorized_keys 文件内容 + suspiciousKeys := sc.checkAuthorizedKeys() + if len(suspiciousKeys) > 0 { + check.Status = StatusWarn + for _, key := range suspiciousKeys { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_authorized_key", + Status: StatusWarn, + Message: key, + }) + } + } + + // 2. 检查 SSH 配置 + sshConfig := sc.readSSHConfig() + + // 检查是否允许 root 登录(仅记录,不告警) + permitRoot := sshConfig["permitrootlogin"] + if permitRoot != "" { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "permit_root_login", + Status: StatusPass, + Message: fmt.Sprintf("Root登录策略: %s", permitRoot), + }) + } + + // 检查空密码登录 + permitEmpty := sshConfig["permitemptypasswords"] + if permitEmpty == "yes" { + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "permit_empty_passwords", + Status: StatusFail, + Message: "允许空密码登录", + }) + } + + // 检查密码认证 + passwordAuth := sshConfig["passwordauthentication"] + if passwordAuth == "yes" { + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "password_auth_enabled", + Status: StatusWarn, + Message: "启用了密码认证(建议仅使用密钥认证)", + }) + } + + // 3. 检查 SSH 后门进程 + suspiciousSSHD := sc.checkSuspiciousSSHD() + if len(suspiciousSSHD) > 0 { + check.Status = StatusFail + for _, sshd := range suspiciousSSHD { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "backdoor_sshd", + Status: StatusFail, + Message: sshd, + }) + } + } + + // 4. 检查 SSH 二进制文件完整性 + for _, binPath := range sc.config.SSHConfig.BinaryPaths { + info, err := os.Stat(binPath) + if err != nil { + continue + } + + modTime := info.ModTime() + age := time.Since(modTime) + if age < time.Duration(sc.config.SSHConfig.RecentModifyDays)*24*time.Hour { + fileHash := sc.evidence.hashCache.GetSHA256(binPath) + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ssh_binary_modified", + Status: StatusWarn, + Message: fmt.Sprintf("SSH二进制文件最近被修改: %s (%d天前)", binPath, int(age.Hours()/24)), + Evidence: &protocol.Evidence{ + FilePath: binPath, + FileHash: fileHash, + Timestamp: modTime.UnixMilli(), + RiskLevel: "high", + }, + }) + } + } + + // 5. 检查 SSH 配置文件的软链接 + for _, configPath := range sc.config.SSHConfig.ConfigPaths { + linkTarget, err := os.Readlink(configPath) + if err == nil { + // 是软链接,检查目标是否可疑 + if strings.Contains(linkTarget, "/tmp") || strings.Contains(linkTarget, "/dev/shm") { + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ssh_config_symlink", + Status: StatusFail, + Message: fmt.Sprintf("SSH配置文件是指向可疑位置的软链接: %s -> %s", configPath, linkTarget), + }) + } + } + } + + // 更新总体消息 + switch check.Status { + case StatusPass: + check.Message = "SSH配置安全" + case StatusWarn: + check.Message = "SSH配置存在风险" + case StatusFail: + check.Message = "检测到SSH后门" + } + + return check +} + +// isSSHRunning 检查 SSH 服务是否运行 +func (sc *SSHChecker) isSSHRunning() bool { + procs, err := sc.cache.Get() + if err != nil { + return false + } + + for _, p := range procs { + name, err := p.Name() + if err == nil && strings.Contains(name, "sshd") { + return true + } + } + + return false +} + +// checkAuthorizedKeys 检查 authorized_keys 文件内容 +func (sc *SSHChecker) checkAuthorizedKeys() []string { + var suspicious []string + + // 获取所有用户目录 + userDirs := sc.getAllUserDirectories() + + for _, userDir := range userDirs { + keyPath := filepath.Join(userDir.Path, ".ssh", "authorized_keys") + + info, err := os.Stat(keyPath) + if err != nil { + continue + } + + // 检查文件大小 + if info.Size() > sc.config.SSHConfig.MaxAuthorizedKeysSize { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 文件过大 (%.2f MB),可能是异常", + userDir.Username, float64(info.Size())/1024/1024)) + } + + // 检查最近修改 + if time.Since(info.ModTime()) < time.Duration(sc.config.SSHConfig.AuthKeysRecentModifyDays)*24*time.Hour { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 最近被修改: %s (%d天前)", + userDir.Username, info.ModTime().Format("2006-01-02 15:04:05"), int(time.Since(info.ModTime()).Hours()/24))) + } + + // 检查文件内容 + f, err := os.Open(keyPath) + if err != nil { + continue + } + + reader := io.LimitReader(f, sc.config.PerformanceConfig.AuthKeysReadLimitKB*1024) + content, err := io.ReadAll(reader) + f.Close() + if err != nil { + continue + } + + lines := strings.Split(string(content), "\n") + keyCount := 0 + + for lineNum, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 检查可疑的公钥选项 + if strings.Contains(line, "command=") { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥包含command选项(行%d): %s...", + userDir.Username, lineNum+1, sc.strUtil.Truncate(line, 60))) + } + + if strings.Contains(line, "from=") { + if strings.Contains(line, `from="*"`) || strings.Contains(line, "from=*") { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥允许任意来源(from=*): 行%d", userDir.Username, lineNum+1)) + } + } + + // 统计公钥数量 + if strings.HasPrefix(line, "ssh-rsa") || strings.HasPrefix(line, "ssh-ed25519") || strings.HasPrefix(line, "ecdsa-") { + keyCount++ + } + } + + if keyCount > sc.config.SSHConfig.MaxKeysCount { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 拥有大量公钥(%d个), 建议审查", userDir.Username, keyCount)) + } + + // 检查文件权限 + if info.Mode().Perm()&0022 != 0 { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 权限过高: %o (Group/Other不可写)", userDir.Username, info.Mode().Perm())) + } + } + + return suspicious +} + +// readSSHConfig 读取 SSH 配置 +func (sc *SSHChecker) readSSHConfig() map[string]string { + config := make(map[string]string) + + // 优先使用 sshd -T + output, err := sc.executor.Execute("sshd", "-T") + if err == nil { + lines := strings.Split(output, "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) >= 2 { + config[strings.ToLower(parts[0])] = parts[1] + } + } + return config + } + + // 备用:读配置文件 + for _, path := range sc.config.SSHConfig.ConfigPaths { + content, err := os.ReadFile(path) + if err != nil { + continue + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Fields(line) + if len(parts) >= 2 { + config[strings.ToLower(parts[0])] = parts[1] + } + } + break + } + + return config +} + +// checkSuspiciousSSHD 检查可疑的 sshd 进程 +func (sc *SSHChecker) checkSuspiciousSSHD() []string { + var suspicious []string + + procs, err := sc.cache.Get() + if err != nil { + return suspicious + } + + sshdCount := 0 + for _, p := range procs { + name, _ := p.Name() + if strings.Contains(name, "sshd") { + sshdCount++ + exe, _ := p.Exe() + + // 检查 sshd 路径是否正常 + if exe != "" && !strings.HasPrefix(exe, "/usr/sbin/sshd") { + suspicious = append(suspicious, fmt.Sprintf("异常sshd路径: %s (PID: %d)", exe, p.Pid)) + } + } + } + + // 如果有多个 sshd 守护进程可能异常 + if sshdCount > 10 { + suspicious = append(suspicious, fmt.Sprintf("sshd进程数异常: %d", sshdCount)) + } + + return suspicious +} + +// UserDirectory 用户目录信息 +type UserDirectory struct { + Username string + Path string + Source string +} + +// getAllUserDirectories 获取所有用户目录 +func (sc *SSHChecker) getAllUserDirectories() []UserDirectory { + var userDirs []UserDirectory + seenPaths := make(map[string]bool) + + // 1. 从 /etc/passwd 获取用户 + users := sc.getAllUsers() + for _, user := range users { + if user.Home != "" && !seenPaths[user.Home] { + userDirs = append(userDirs, UserDirectory{ + Username: user.Username, + Path: user.Home, + Source: "passwd", + }) + seenPaths[user.Home] = true + } + } + + // 2. 扫描 /home 目录,发现 LDAP/AD 等外部认证用户 + entries, err := os.ReadDir("/home") + if err != nil { + return userDirs + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + homePath := filepath.Join("/home", entry.Name()) + + if seenPaths[homePath] { + continue + } + + // 检查是否有 .ssh 目录 + sshPath := filepath.Join(homePath, ".ssh") + if stat, err := os.Stat(sshPath); err == nil && stat.IsDir() { + userDirs = append(userDirs, UserDirectory{ + Username: entry.Name() + " (LDAP/AD)", + Path: homePath, + Source: "home_scan", + }) + seenPaths[homePath] = true + } + } + + return userDirs +} + +// UserInfo 用户信息 +type UserInfo struct { + Username string + UID string + Home string + Shell string +} + +// getAllUsers 获取所有用户 +func (sc *SSHChecker) getAllUsers() []UserInfo { + var users []UserInfo + + file, err := os.Open("/etc/passwd") + if err != nil { + return users + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) >= 7 { + users = append(users, UserInfo{ + Username: parts[0], + UID: parts[2], + Home: parts[5], + Shell: parts[6], + }) + } + } + + return users +} diff --git a/pkg/agent/audit/config.go b/pkg/agent/audit/config.go new file mode 100644 index 0000000..0a41cc4 --- /dev/null +++ b/pkg/agent/audit/config.go @@ -0,0 +1,298 @@ +package audit + +import "time" + +// Config 审计配置 +type Config struct { + // 进程相关 + ProcessConfig ProcessConfig + + // SSH 相关 + SSHConfig SSHConfig + + // 网络相关 + NetworkConfig NetworkConfig + + // 文件相关 + FileConfig FileConfig + + // 定时任务相关 + CronConfig CronConfig + + // 登录相关 + LoginConfig LoginConfig + + // 风险评分配置 + ScoringConfig ScoringConfig + + // 性能相关 + PerformanceConfig PerformanceConfig +} + +// ProcessConfig 进程检查配置 +type ProcessConfig struct { + // 挖矿关键词 + MinerKeywords []string + + // 高 CPU 阈值 (百分比) + HighCPUThreshold float64 + + // 高 CPU 白名单 + HighCPUWhitelist []string + + // Deleted 进程白名单 + DeletedWhitelist []string + + // 最近启动时间阈值 (小时) + RecentStartupHours int +} + +// SSHConfig SSH 检查配置 +type SSHConfig struct { + // SSH 二进制文件路径 + BinaryPaths []string + + // SSH 配置文件路径 + ConfigPaths []string + + // 最近修改时间阈值 (天) + RecentModifyDays int + + // authorized_keys 最大文件大小 (字节) + MaxAuthorizedKeysSize int64 + + // authorized_keys 最近修改时间阈值 (天) + AuthKeysRecentModifyDays int + + // 最大公钥数量 + MaxKeysCount int +} + +// NetworkConfig 网络检查配置 +type NetworkConfig struct { + // 可疑端口映射 + SuspiciousPorts map[uint32]string + + // 挖矿池端口 + MinerPorts []uint32 + + // 挖矿池域名关键词 + MinerPoolKeywords []string +} + +// FileConfig 文件检查配置 +type FileConfig struct { + // 临时目录列表 + TempDirs []string + + // 可疑路径列表 + SuspiciousPaths []string + + // 关键系统二进制文件 + CriticalBinaries []string + + // 不可变文件检查列表 + ImmutableCheckFiles []string + + // 最近可执行文件阈值 (小时) + RecentExecutableHours int + + // 大型可执行文件阈值 (MB) + LargeExecutableMB int64 +} + +// CronConfig 定时任务配置 +type CronConfig struct { + // 系统 cron 路径 + SystemCronPaths []string +} + +// LoginConfig 登录历史配置 +type LoginConfig struct { + // 最近登录记录数量 + RecentLoginCount int + + // 失败登录记录数量 + FailedLoginCount int + + // 高频 IP 阈值 + HighFrequencyIPThreshold int + + // 同一 IP 登录阈值 + SameIPLoginThreshold int + + // Root 不同 IP 阈值 + RootDifferentIPThreshold int +} + +// ScoringConfig 风险评分配置 +type ScoringConfig struct { + // 各检查项的权重 + Weights map[string]CheckWeight + + // 威胁等级阈值 + CriticalThreshold int + HighThreshold int + MediumThreshold int + + // 最大建议数量 + MaxRecommendations int +} + +// CheckWeight 检查项权重 +type CheckWeight struct { + Category string + FailScore int + WarnScore int +} + +// PerformanceConfig 性能配置 +type PerformanceConfig struct { + // 进程缓存时间 + ProcessCacheDuration time.Duration + + // 命令执行超时时间 + CommandTimeout time.Duration + + // authorized_keys 读取限制 (KB) + AuthKeysReadLimitKB int64 + + // 文件完整性检查批量大小 + IntegrityCheckBatchSize int +} + +// DefaultConfig 返回默认配置 +func DefaultConfig() *Config { + return &Config{ + ProcessConfig: ProcessConfig{ + MinerKeywords: []string{ + "xmrig", "minergate", "cpuminer", "ccminer", + "ethminer", "claymore", "phoenix", "t-rex", + "minerd", "stratum", "nicehash", "cryptonight", + }, + HighCPUThreshold: 70.0, // 降低到 70% + HighCPUWhitelist: []string{ + // 数据库 + "postgres", "mysqld", "mariadb", "mongodb", "redis", + // 编译工具 + "gcc", "g++", "clang", "rustc", "go", "javac", + // 视频/图像处理 + "ffmpeg", "ffprobe", "convert", "imagemagick", + // 科学计算 + "python", "R", "matlab", "octave", "julia", + // 虚拟化 + "qemu", "kvm", "virtualbox", "vmware", + // 压缩解压 + "tar", "gzip", "bzip2", "xz", "7z", + // CI/CD + "jenkins", "gitlab-runner", "docker", "containerd", + }, + DeletedWhitelist: []string{ + "nginx", "docker", "containerd", "code-server", + "mysqld", "node", "k3s", "kubelet", + }, + RecentStartupHours: 24, + }, + SSHConfig: SSHConfig{ + BinaryPaths: []string{ + "/usr/sbin/sshd", + "/usr/bin/ssh", + }, + ConfigPaths: []string{ + "/etc/ssh/sshd_config", + }, + RecentModifyDays: 90, + MaxAuthorizedKeysSize: 1024 * 1024, // 1MB + AuthKeysRecentModifyDays: 30, + MaxKeysCount: 10, + }, + NetworkConfig: NetworkConfig{ + SuspiciousPorts: map[uint32]string{ + 4444: "Metasploit默认端口", + 1337: "黑客常用端口", + 31337: "黑客常用端口", + }, + MinerPorts: []uint32{ + 3333, 4444, 5555, 7777, 8888, 9999, + 14444, 45560, + }, + MinerPoolKeywords: []string{ + "pool", "stratum", "mine", "mining", + "xmr", "monero", "eth", "btc", + }, + }, + FileConfig: FileConfig{ + TempDirs: []string{ + "/tmp", + "/dev/shm", + "/var/tmp", + }, + SuspiciousPaths: []string{ + "/tmp/", + "/dev/shm/", + "/var/tmp/", + "memfd:", + "(deleted)", + }, + CriticalBinaries: []string{ + "/bin/bash", "/bin/sh", "/bin/ls", "/bin/ps", "/bin/netstat", + "/usr/bin/ssh", "/usr/bin/sudo", "/usr/sbin/sshd", "/usr/sbin/lsof", + "/usr/bin/passwd", "/usr/bin/chsh", "/usr/bin/curl", "/usr/bin/wget", + }, + ImmutableCheckFiles: []string{ + "/etc/passwd", "/etc/shadow", "/etc/group", + "/etc/sudoers", "/etc/hosts", "/etc/resolv.conf", + "/etc/ld.so.preload", "/etc/crontab", + "/bin/ls", "/bin/ps", "/bin/netstat", + "/usr/sbin/sshd", + "/root/.ssh/authorized_keys", + }, + RecentExecutableHours: 2, + LargeExecutableMB: 10, + }, + CronConfig: CronConfig{ + SystemCronPaths: []string{ + "/etc/crontab", + "/etc/cron.d/", + "/etc/cron.hourly/", + "/etc/cron.daily/", + "/etc/cron.weekly/", + "/etc/cron.monthly/", + "/var/spool/cron/", + }, + }, + LoginConfig: LoginConfig{ + RecentLoginCount: 50, + FailedLoginCount: 100, + HighFrequencyIPThreshold: 10, + SameIPLoginThreshold: 30, // 降低到 30 + RootDifferentIPThreshold: 3, + }, + ScoringConfig: ScoringConfig{ + Weights: map[string]CheckWeight{ + "rootkit_detection": {FailScore: 50, WarnScore: 20}, + "suspicious_processes": {FailScore: 40, WarnScore: 15}, + "suspicious_env_vars": {FailScore: 35, WarnScore: 15}, + "ssh_security": {FailScore: 15, WarnScore: 5}, + "listening_ports": {FailScore: 30, WarnScore: 10}, + "cron_jobs": {FailScore: 25, WarnScore: 10}, + "suspicious_files": {FailScore: 30, WarnScore: 10}, + "system_accounts": {FailScore: 40, WarnScore: 15}, + "network_connections": {FailScore: 35, WarnScore: 15}, + "file_integrity": {FailScore: 30, WarnScore: 10}, + "immutable_files": {FailScore: 45, WarnScore: 15}, + "login_history": {FailScore: 20, WarnScore: 8}, + }, + CriticalThreshold: 80, + HighThreshold: 50, + MediumThreshold: 20, + MaxRecommendations: 20, + }, + PerformanceConfig: PerformanceConfig{ + ProcessCacheDuration: 1 * time.Second, + CommandTimeout: 15 * time.Second, + AuthKeysReadLimitKB: 512, + IntegrityCheckBatchSize: 10, + }, + } +} diff --git a/pkg/agent/audit/evidence.go b/pkg/agent/audit/evidence.go new file mode 100644 index 0000000..6f4177a --- /dev/null +++ b/pkg/agent/audit/evidence.go @@ -0,0 +1,164 @@ +package audit + +import ( + "fmt" + "os" + "strings" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/process" +) + +// EvidenceCollector 证据收集器 +type EvidenceCollector struct { + hashCache *FileHashCache +} + +// NewEvidenceCollector 创建证据收集器 +func NewEvidenceCollector() *EvidenceCollector { + return &EvidenceCollector{ + hashCache: NewFileHashCache(), + } +} + +// CollectProcessEvidence 收集进程证据 +func (ec *EvidenceCollector) CollectProcessEvidence(p *process.Process, riskLevel string) *protocol.Evidence { + exe, _ := p.Exe() + createTime, _ := p.CreateTime() + + var fileHash string + if exe != "" && !strings.Contains(exe, "deleted") && !strings.Contains(exe, "memfd:") { + fileHash = ec.hashCache.GetSHA256(exe) + } + + processTree := ec.buildProcessTree(p) + + return &protocol.Evidence{ + FileHash: fileHash, + ProcessTree: processTree, + FilePath: exe, + Timestamp: createTime, + RiskLevel: riskLevel, + } +} + +// CollectFileEvidence 收集文件证据 +func (ec *EvidenceCollector) CollectFileEvidence(filePath string, riskLevel string) *protocol.Evidence { + info, err := os.Stat(filePath) + if err != nil { + globalLogger.Debug("无法获取文件信息: %s, err: %v", filePath, err) + return &protocol.Evidence{ + FilePath: filePath, + RiskLevel: riskLevel, + } + } + + fileHash := ec.hashCache.GetSHA256(filePath) + + return &protocol.Evidence{ + FilePath: filePath, + FileHash: fileHash, + Timestamp: info.ModTime().UnixMilli(), + RiskLevel: riskLevel, + } +} + +// buildProcessTree 构建进程树 +func (ec *EvidenceCollector) buildProcessTree(p *process.Process) []string { + var tree []string + current := p + + // 向上追溯父进程(最多 5 层) + for i := 0; i < 5; i++ { + if current == nil { + break + } + + name, _ := current.Name() + exe, _ := current.Exe() + cmdline, _ := current.Cmdline() + + info := fmt.Sprintf("PID:%d Name:%s", current.Pid, name) + if exe != "" && exe != name { + info += fmt.Sprintf(" Exe:%s", exe) + } + if cmdline != "" && len(cmdline) < 100 { + info += fmt.Sprintf(" Cmd:%s", cmdline) + } + + tree = append([]string{info}, tree...) // 前置插入 + + ppid, err := current.Ppid() + if err != nil || ppid == 0 { + break + } + + parent, err := process.NewProcess(ppid) + if err != nil { + break + } + current = parent + } + + return tree +} + +// WarningCollector 警告收集器 +type WarningCollector struct { + warnings []string +} + +// NewWarningCollector 创建警告收集器 +func NewWarningCollector() *WarningCollector { + return &WarningCollector{ + warnings: []string{}, + } +} + +// Add 添加警告 +func (wc *WarningCollector) Add(warning string) { + wc.warnings = append(wc.warnings, warning) +} + +// GetAll 获取所有警告 +func (wc *WarningCollector) GetAll() []string { + return wc.warnings +} + +// SystemInfoCollector 系统信息收集器 +type SystemInfoCollector struct { + executor *CommandExecutor +} + +// NewSystemInfoCollector 创建系统信息收集器 +func NewSystemInfoCollector(executor *CommandExecutor) *SystemInfoCollector { + return &SystemInfoCollector{ + executor: executor, + } +} + +// Collect 收集系统信息 +func (sic *SystemInfoCollector) Collect() (*protocol.SystemInfo, error) { + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + + info, err := host.Info() + if err != nil { + return nil, fmt.Errorf("获取系统信息失败: %w", err) + } + + osInfo := fmt.Sprintf("%s %s", info.Platform, info.PlatformVersion) + if info.PlatformFamily != "" { + osInfo = fmt.Sprintf("%s (%s)", osInfo, info.PlatformFamily) + } + + return &protocol.SystemInfo{ + Hostname: hostname, + OS: osInfo, + KernelVersion: info.KernelVersion, + Uptime: info.Uptime, + }, nil +} diff --git a/pkg/agent/audit/utils.go b/pkg/agent/audit/utils.go new file mode 100644 index 0000000..6a76601 --- /dev/null +++ b/pkg/agent/audit/utils.go @@ -0,0 +1,326 @@ +package audit + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "log" + "os" + "os/exec" + "strings" + "sync" + "time" + + "github.com/shirou/gopsutil/v4/process" +) + +// Logger 日志接口 +type Logger interface { + Debug(format string, args ...interface{}) + Info(format string, args ...interface{}) + Warn(format string, args ...interface{}) + Error(format string, args ...interface{}) +} + +// defaultLogger 默认日志实现 +type defaultLogger struct{} + +func (l *defaultLogger) Debug(format string, args ...interface{}) { + log.Printf("[DEBUG] "+format, args...) +} + +func (l *defaultLogger) Info(format string, args ...interface{}) { + log.Printf("[INFO] "+format, args...) +} + +func (l *defaultLogger) Warn(format string, args ...interface{}) { + log.Printf("[WARN] "+format, args...) +} + +func (l *defaultLogger) Error(format string, args ...interface{}) { + log.Printf("[ERROR] "+format, args...) +} + +var globalLogger Logger = &defaultLogger{} + +// SetLogger 设置全局日志器 +func SetLogger(logger Logger) { + if logger != nil { + globalLogger = logger + } +} + +// ProcessCache 进程缓存 +type ProcessCache struct { + processes []*process.Process + timestamp time.Time + mu sync.RWMutex + ttl time.Duration +} + +// NewProcessCache 创建进程缓存 +func NewProcessCache(ttl time.Duration) *ProcessCache { + return &ProcessCache{ + ttl: ttl, + } +} + +// Get 获取进程列表(带缓存) +func (pc *ProcessCache) Get() ([]*process.Process, error) { + pc.mu.RLock() + if time.Since(pc.timestamp) < pc.ttl && pc.processes != nil { + processes := pc.processes + pc.mu.RUnlock() + return processes, nil + } + pc.mu.RUnlock() + + // 缓存过期,重新获取 + pc.mu.Lock() + defer pc.mu.Unlock() + + // 双重检查 + if time.Since(pc.timestamp) < pc.ttl && pc.processes != nil { + return pc.processes, nil + } + + processes, err := process.Processes() + if err != nil { + return nil, err + } + + pc.processes = processes + pc.timestamp = time.Now() + globalLogger.Debug("进程缓存已更新,共 %d 个进程", len(processes)) + + return processes, nil +} + +// Clear 清除缓存 +func (pc *ProcessCache) Clear() { + pc.mu.Lock() + defer pc.mu.Unlock() + pc.processes = nil + pc.timestamp = time.Time{} +} + +// CommandExecutor 命令执行器 +type CommandExecutor struct { + timeout time.Duration +} + +// NewCommandExecutor 创建命令执行器 +func NewCommandExecutor(timeout time.Duration) *CommandExecutor { + return &CommandExecutor{ + timeout: timeout, + } +} + +// Execute 执行命令 +func (ce *CommandExecutor) Execute(name string, args ...string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), ce.timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, name, args...) + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + // 检查是否超时 + if ctx.Err() == context.DeadlineExceeded { + globalLogger.Warn("命令执行超时(%v): %s %v", ce.timeout, name, args) + return "", fmt.Errorf("命令执行超时(%v): %s", ce.timeout, name) + } + + // 记录错误但返回输出 + if stderr.Len() > 0 { + globalLogger.Debug("命令执行失败: %s %v, stderr: %s", name, args, stderr.String()) + } + return stdout.String(), err + } + + return stdout.String(), nil +} + +// FileHashCache 文件哈希缓存 +type FileHashCache struct { + cache map[string]cachedHash + mu sync.RWMutex +} + +type cachedHash struct { + hash string + modTime time.Time +} + +// NewFileHashCache 创建文件哈希缓存 +func NewFileHashCache() *FileHashCache { + return &FileHashCache{ + cache: make(map[string]cachedHash), + } +} + +// GetSHA256 获取文件 SHA256 哈希(带缓存) +func (fhc *FileHashCache) GetSHA256(filePath string) string { + info, err := os.Stat(filePath) + if err != nil { + globalLogger.Debug("无法获取文件信息: %s, err: %v", filePath, err) + return "" + } + + modTime := info.ModTime() + + // 检查缓存 + fhc.mu.RLock() + if cached, ok := fhc.cache[filePath]; ok { + if cached.modTime.Equal(modTime) { + fhc.mu.RUnlock() + return cached.hash + } + } + fhc.mu.RUnlock() + + // 计算哈希 + hash := calculateSHA256(filePath) + if hash == "" { + return "" + } + + // 更新缓存 + fhc.mu.Lock() + fhc.cache[filePath] = cachedHash{ + hash: hash, + modTime: modTime, + } + fhc.mu.Unlock() + + return hash +} + +// calculateSHA256 计算文件 SHA256 哈希 +func calculateSHA256(filePath string) string { + file, err := os.Open(filePath) + if err != nil { + return "" + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "" + } + + return hex.EncodeToString(hash.Sum(nil)) +} + +// StringUtils 字符串工具 +type StringUtils struct{} + +// TruncateString 截断字符串 +func (su *StringUtils) Truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] +} + +// ContainsAny 检查字符串是否包含任意关键词 +func (su *StringUtils) ContainsAny(s string, keywords []string) bool { + lower := strings.ToLower(s) + for _, keyword := range keywords { + if strings.Contains(lower, strings.ToLower(keyword)) { + return true + } + } + return false +} + +// IsLocalIP 判断是否是本地 IP +func IsLocalIP(ip string) bool { + if ip == "127.0.0.1" || ip == "::1" || ip == "localhost" { + return true + } + + // 简化版本,实际应该使用 net.ParseIP + privateRanges := []string{ + "10.", "192.168.", "172.16.", "172.17.", "172.18.", "172.19.", + "172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.", + "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.", + } + + for _, prefix := range privateRanges { + if strings.HasPrefix(ip, prefix) { + return true + } + } + + return false +} + +// BatchProcessor 批处理器 +type BatchProcessor struct { + batchSize int +} + +// NewBatchProcessor 创建批处理器 +func NewBatchProcessor(batchSize int) *BatchProcessor { + return &BatchProcessor{ + batchSize: batchSize, + } +} + +// Process 批量处理 +func (bp *BatchProcessor) Process(items []string, handler func(batch []string) error) error { + for i := 0; i < len(items); i += bp.batchSize { + end := i + bp.batchSize + if end > len(items) { + end = len(items) + } + + batch := items[i:end] + if err := handler(batch); err != nil { + return fmt.Errorf("批处理失败 (批次 %d-%d): %w", i, end, err) + } + } + return nil +} + +// RetryExecutor 重试执行器 +type RetryExecutor struct { + maxRetries int + delay time.Duration +} + +// NewRetryExecutor 创建重试执行器 +func NewRetryExecutor(maxRetries int, delay time.Duration) *RetryExecutor { + return &RetryExecutor{ + maxRetries: maxRetries, + delay: delay, + } +} + +// Execute 执行函数(带重试) +func (re *RetryExecutor) Execute(fn func() error) error { + var lastErr error + for i := 0; i <= re.maxRetries; i++ { + if i > 0 { + globalLogger.Debug("重试第 %d 次...", i) + time.Sleep(re.delay) + } + + if err := fn(); err != nil { + lastErr = err + continue + } + + return nil + } + + return fmt.Errorf("重试 %d 次后仍失败: %w", re.maxRetries, lastErr) +}