优化安全审计相关代码
This commit is contained in:
1 parent
f445e718cc
commit
c242146802
11 files changed
+2898
-2381
No files matched your search
@@ -209,6 +209,8 @@ type VPSAuditResult struct {
|
||||
ThreatLevel string `json:"threatLevel"`
|
||||
// 修复建议
|
||||
Recommendations []string `json:"recommendations,omitempty"`
|
||||
// 审计警告(权限不足、检查失败等非安全问题)
|
||||
AuditWarnings []string `json:"auditWarnings,omitempty"`
|
||||
}
|
||||
|
||||
// SystemInfo 系统信息
|
||||
|
||||
+122
-2381
File diff suppressed because it is too large.
Load diff
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user