优化安全审计相关代码

This commit is contained in:
dushixiang committed 2025-11-20 20:18:28 +08:00
1 parent f445e718cc
commit c242146802
11 files changed
+2898 -2381

No files matched your search

+2
View File
@@ -209,6 +209,8 @@ type VPSAuditResult struct {
ThreatLevel string `json:"threatLevel"`
// 修复建议
Recommendations []string `json:"recommendations,omitempty"`
// 审计警告(权限不足、检查失败等非安全问题)
AuditWarnings []string `json:"auditWarnings,omitempty"`
}
// SystemInfo 系统信息
+122 -2381
View File
File diff suppressed because it is too large. Load diff
+279
View File
@@ -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())
}
}
+419
View File
@@ -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
}
+270
View File
@@ -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
}
+380
View File
@@ -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"
}
+207
View File
@@ -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
}
+431
View File
@@ -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
}
+298
View File
@@ -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,
},
}
}
+164
View File
@@ -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
}
+326
View File
@@ -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)
}