diff --git a/docs/MessageProtocol.md b/docs/MessageProtocol.md index 40bfd0c..96cd1c9 100644 --- a/docs/MessageProtocol.md +++ b/docs/MessageProtocol.md @@ -1,277 +1,277 @@ -# 消息交互 - -## 数据包格式 - -### 通用数据包结构 -```json -{ - "type": "消息类型", - "timestamp": 1612345678901, - "payload": { - // 根据消息类型的具体数据结构 - } -} -``` - -### 数据类型定义 -```go -type Packet struct { - Type string `json:"type"` // 消息类型 - Timestamp int64 `json:"timestamp"` // Unix时间戳 - Payload interface{} `json:"payload"` // 消息载荷 -} -``` - -## 消息类型及数据结构 - -### 1. 系统状态更新 (`STATUS_UPDATE`) - -**描述**: 定期发送的系统性能指标 - -**推送频率**: 每30秒一次 - -**Payload 结构**: `ServerMetrics` -```json -{ - "timestamp": "2024-01-15T10:30:00Z", - "cpu": { - "model": "Intel(R) Xeon(R) CPU E5-2680 v4", - "cores": 14, - "logical_cores": 28, - "usage_percent": 45.67, - "per_core_percent": [23.4, 45.6, 12.3, ...], - "mhz": 2400.5, - "cache_size": 35840 - }, - "memory": { - "total_gb": 128.0, - "used_gb": 64.5, - "available_gb": 63.5, - "used_percent": 50.4, - "swap_total_gb": 16.0, - "swap_used_gb": 2.3 - }, - "disk": [ - { - "mountpoint": "/", - "device": "/dev/sda1", - "fstype": "ext4", - "total_gb": 500.0, - "used_gb": 250.0, - "free_gb": 250.0, - "used_percent": 50.0, - "inodes_percent": 12.3 - } - ], - "network": { - "interfaces": [ - { - "name": "eth0", - "hardware_addr": "00:11:22:33:44:55", - "ip_addresses": ["192.168.1.100", "fe80::211:22ff:fe33:4455"] - } - ], - "total_recv_mb": 1234.56, - "total_sent_mb": 987.65, - "tcp_connections": 245, - "established_conn": 128 - }, - "load": { - "load_1": 2.34, - "load_5": 2.12, - "load_15": 1.89, - "relative_load_1": 0.83, - "relative_load_5": 0.76, - "relative_load_15": 0.68, - "procs_running": 132, - "procs_total": 456 - }, - "processes": [ - { - "pid": 1234, - "name": "nginx", - "cmdline": "nginx: master process", - "memory_mb": 125.6, - "cpu_percent": 12.3 - } - ], - "host": { - "hostname": "server01", - "os": "linux", - "platform": "ubuntu", - "platform_version": "20.04", - "kernel_version": "5.4.0-42-generic", - "boot_time": "2024-01-15T08:00:00Z", - "uptime": "2小时30分钟45秒", - "cpu_count": 28, - "architecture": "x86_64", - "host_id": "abcdef12-3456-7890-abcd-ef1234567890" - }, - "runtime": { - "go_version": "go1.21.0", - "goos": "linux", - "goarch": "amd64", - "goroot": "/usr/local/go", - "gomaxprocs": 28, - "num_cpu": 28, - "num_goroutine": 42 - }, - "quick_metrics": { - "cpu_percent": 45.67, - "memory_percent": 50.4, - "root_disk_percent": 50.0, - "available_memory_gb": 63.5 - } -} -``` - -### 2. SSH登录告警 (`SSH_ALERT`) - -**描述**: SSH登录安全告警(特别是root登录) - -**触发条件**: SSH登录事件,当检测到root登录时触发HIGH级别告警 - -**Payload 结构**: `Alert` -```json -{ - "type": "SSH_ROOT_LOGIN", - "level": "HIGH", - "message": "检测到来自192.168.1.50的root登录", - "timestamp": "2024-01-15T10:31:15Z", - "data": { - "timestamp": "2024-01-15T10:31:15Z", - "hostname": "server01", - "username": "root", - "method": "publickey", - "source_ip": "192.168.1.50", - "port": "22", - "service": "sshd", - "pid": "12345", - "message": "Accepted publickey for root from 192.168.1.50 port 22" - } -} -``` - -### 3. 文件完整性告警 - -#### 3.1 非白名单文件告警 (`NON_WHITELISTED_FILE`) - -**描述**: 扫描发现不在白名单中的文件 - -**触发条件**: 定期扫描中发现未在白名单中注册的文件 - -**Payload 结构**: -```json -{ - "type": "NON_WHITELISTED_FILE", - "timestamp": 1612345678901, - "payload": { - "filepath": "/tmp/suspicious_file.bin", - "status": "detected" - } -} -``` - -#### 3.2 文件Hash不匹配告警 (`FILE_HASH_MISMATCH`) - -**描述**: 白名单文件被篡改(Hash值不匹配) - -**触发条件**: 文件hash与白名单记录不符 - -**Payload 结构**: -```json -{ - "type": "FILE_HASH_MISMATCH", - "timestamp": 1612345678901, - "payload": { - "filepath": "/usr/bin/ls", - "status": "detected" - } -} -``` - -### 4. 实时文件监控告警 - -#### 4.1 实时文件变动告警 (`REALTIME_FILE_ALERT`) - -**描述**: 监控目录中检测到非白名单文件的创建或修改 - -**触发条件**: 使用fsnotify监控到文件系统事件 - -**Payload 结构**: -```json -{ - "type": "REALTIME_FILE_ALERT", - "timestamp": 1612345678901, - "payload": { - "filepath": "/tmp/new_suspicious_file", - "operation": "CREATE", - "time": "2024-01-15T10:32:00Z" - } -} -``` - -#### 4.2 实时Hash不匹配告警 (`REALTIME_HASH_MISMATCH`) - -**描述**: 监控到白名单文件被实时篡改 - -**Payload 结构**: -```json -{ - "type": "REALTIME_HASH_MISMATCH", - "timestamp": 1612345678901, - "payload": { - "filepath": "/etc/passwd", - "operation": "WRITE", - "time": "2024-01-15T10:33:00Z" - } -} -``` - -## 配置接口 - -### 1. 配置下载接口 - -Agent 启动时会通过 HTTP 下载两份配置: - -#### 官方配置 (GET) -- **URL**: `http://localhost:8090/api/v1/configs/official.json` -- **响应格式**: 符合 `OfficialConfig` 结构 - -#### 用户配置 (GET) -- **URL**: `http://localhost:8090/api/v1/configs/user.json` -- **响应格式**: 符合 `UserConfig` 结构 - -### 2. 配置数据结构 - -#### OfficialConfig -```json -{ - "whitelist_files": { - "/usr/bin/ls": ["hash1", "hash2"], - "/bin/bash": ["hash3"] - }, - "whitelist_processes": ["sshd", "nginx", "docker"], - "ignored_paths": ["/proc", "/sys", "/dev"] -} -``` - -#### UserConfig -```json -{ - "audit_server_url": "ws://audit.example.com:8090/api/v1/ws", - "supplement_files": { - "/opt/myapp/bin/app": ["user_hash1"] - }, - "supplement_processes": { - "myapp": "/opt/myapp/bin/app start", - "custom_service": "" - }, - "ignored_paths": ["/mnt/temp"], - "check_perm_paths": ["/etc/sudoers", "/etc/shadow"], - "email_config": { - "imap_server": "imap.example.com", - "emergency_mail": ["admin@example.com", "security@example.com"] - } -} -``` +# 消息交互 + +## 数据包格式 + +### 通用数据包结构 +```json +{ + "type": "消息类型", + "timestamp": 1612345678901, + "payload": { + // 根据消息类型的具体数据结构 + } +} +``` + +### 数据类型定义 +```go +type Packet struct { + Type string `json:"type"` // 消息类型 + Timestamp int64 `json:"timestamp"` // Unix时间戳 + Payload interface{} `json:"payload"` // 消息载荷 +} +``` + +## 消息类型及数据结构 + +### 1. 系统状态更新 (`STATUS_UPDATE`) + +**描述**: 定期发送的系统性能指标 + +**推送频率**: 每30秒一次 + +**Payload 结构**: `ServerMetrics` +```json +{ + "timestamp": "2024-01-15T10:30:00Z", + "cpu": { + "model": "Intel(R) Xeon(R) CPU E5-2680 v4", + "cores": 14, + "logical_cores": 28, + "usage_percent": 45.67, + "per_core_percent": [23.4, 45.6, 12.3, ...], + "mhz": 2400.5, + "cache_size": 35840 + }, + "memory": { + "total_gb": 128.0, + "used_gb": 64.5, + "available_gb": 63.5, + "used_percent": 50.4, + "swap_total_gb": 16.0, + "swap_used_gb": 2.3 + }, + "disk": [ + { + "mountpoint": "/", + "device": "/dev/sda1", + "fstype": "ext4", + "total_gb": 500.0, + "used_gb": 250.0, + "free_gb": 250.0, + "used_percent": 50.0, + "inodes_percent": 12.3 + } + ], + "network": { + "interfaces": [ + { + "name": "eth0", + "hardware_addr": "00:11:22:33:44:55", + "ip_addresses": ["192.168.1.100", "fe80::211:22ff:fe33:4455"] + } + ], + "total_recv_mb": 1234.56, + "total_sent_mb": 987.65, + "tcp_connections": 245, + "established_conn": 128 + }, + "load": { + "load_1": 2.34, + "load_5": 2.12, + "load_15": 1.89, + "relative_load_1": 0.83, + "relative_load_5": 0.76, + "relative_load_15": 0.68, + "procs_running": 132, + "procs_total": 456 + }, + "processes": [ + { + "pid": 1234, + "name": "nginx", + "cmdline": "nginx: master process", + "memory_mb": 125.6, + "cpu_percent": 12.3 + } + ], + "host": { + "hostname": "server01", + "os": "linux", + "platform": "ubuntu", + "platform_version": "20.04", + "kernel_version": "5.4.0-42-generic", + "boot_time": "2024-01-15T08:00:00Z", + "uptime": "2小时30分钟45秒", + "cpu_count": 28, + "architecture": "x86_64", + "host_id": "abcdef12-3456-7890-abcd-ef1234567890" + }, + "runtime": { + "go_version": "go1.21.0", + "goos": "linux", + "goarch": "amd64", + "goroot": "/usr/local/go", + "gomaxprocs": 28, + "num_cpu": 28, + "num_goroutine": 42 + }, + "quick_metrics": { + "cpu_percent": 45.67, + "memory_percent": 50.4, + "root_disk_percent": 50.0, + "available_memory_gb": 63.5 + } +} +``` + +### 2. SSH登录告警 (`SSH_ALERT`) + +**描述**: SSH登录安全告警(特别是root登录) + +**触发条件**: SSH登录事件,当检测到root登录时触发HIGH级别告警 + +**Payload 结构**: `Alert` +```json +{ + "type": "SSH_ROOT_LOGIN", + "level": "HIGH", + "message": "检测到来自192.168.1.50的root登录", + "timestamp": "2024-01-15T10:31:15Z", + "data": { + "timestamp": "2024-01-15T10:31:15Z", + "hostname": "server01", + "username": "root", + "method": "publickey", + "source_ip": "192.168.1.50", + "port": "22", + "service": "sshd", + "pid": "12345", + "message": "Accepted publickey for root from 192.168.1.50 port 22" + } +} +``` + +### 3. 文件完整性告警 + +#### 3.1 非白名单文件告警 (`NON_WHITELISTED_FILE`) + +**描述**: 扫描发现不在白名单中的文件 + +**触发条件**: 定期扫描中发现未在白名单中注册的文件 + +**Payload 结构**: +```json +{ + "type": "NON_WHITELISTED_FILE", + "timestamp": 1612345678901, + "payload": { + "filepath": "/tmp/suspicious_file.bin", + "status": "detected" + } +} +``` + +#### 3.2 文件Hash不匹配告警 (`FILE_HASH_MISMATCH`) + +**描述**: 白名单文件被篡改(Hash值不匹配) + +**触发条件**: 文件hash与白名单记录不符 + +**Payload 结构**: +```json +{ + "type": "FILE_HASH_MISMATCH", + "timestamp": 1612345678901, + "payload": { + "filepath": "/usr/bin/ls", + "status": "detected" + } +} +``` + +### 4. 实时文件监控告警 + +#### 4.1 实时文件变动告警 (`REALTIME_FILE_ALERT`) + +**描述**: 监控目录中检测到非白名单文件的创建或修改 + +**触发条件**: 使用fsnotify监控到文件系统事件 + +**Payload 结构**: +```json +{ + "type": "REALTIME_FILE_ALERT", + "timestamp": 1612345678901, + "payload": { + "filepath": "/tmp/new_suspicious_file", + "operation": "CREATE", + "time": "2024-01-15T10:32:00Z" + } +} +``` + +#### 4.2 实时Hash不匹配告警 (`REALTIME_HASH_MISMATCH`) + +**描述**: 监控到白名单文件被实时篡改 + +**Payload 结构**: +```json +{ + "type": "REALTIME_HASH_MISMATCH", + "timestamp": 1612345678901, + "payload": { + "filepath": "/etc/passwd", + "operation": "WRITE", + "time": "2024-01-15T10:33:00Z" + } +} +``` + +## 配置接口 + +### 1. 配置下载接口 + +Agent 启动时会通过 HTTP 下载两份配置: + +#### 官方配置 (GET) +- **URL**: `http://localhost:8090/api/v1/configs/official.json` +- **响应格式**: 符合 `OfficialConfig` 结构 + +#### 用户配置 (GET) +- **URL**: `http://localhost:8090/api/v1/configs/user.json` +- **响应格式**: 符合 `UserConfig` 结构 + +### 2. 配置数据结构 + +#### OfficialConfig +```json +{ + "whitelist_files": { + "/usr/bin/ls": ["hash1", "hash2"], + "/bin/bash": ["hash3"] + }, + "whitelist_processes": ["sshd", "nginx", "docker"], + "ignored_paths": ["/proc", "/sys", "/dev"] +} +``` + +#### UserConfig +```json +{ + "audit_server_url": "ws://audit.example.com:8090/api/v1/ws", + "supplement_files": { + "/opt/myapp/bin/app": ["user_hash1"] + }, + "supplement_processes": { + "myapp": "/opt/myapp/bin/app start", + "custom_service": "" + }, + "ignored_paths": ["/mnt/temp"], + "check_perm_paths": ["/etc/sudoers", "/etc/shadow"], + "email_config": { + "imap_server": "imap.example.com", + "emergency_mail": ["admin@example.com", "security@example.com"] + } +} +``` diff --git a/internal/config/config.go b/internal/config/config.go index 8acd233..b8e5015 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,46 +1,72 @@ package config -import "time" - -type Configuration struct { - Local Localconfig // 本地配置 - Offical OfficialConfig // 官方配置 - User UserConfig // 用户自定义配置 +type ModelSwitches struct { + FileScanner bool `json:"file_scanner"` + FileWatcher bool `json:"file_watcher"` + SSHMonitor bool `json:"ssh_monitor"` + SystemMonitor bool `json:"system_monitor"` } -type Localconfig struct { - LogPath string `yaml:"log_path"` - CheckInterval time.Duration `yaml:"check_interval"` - ServerUrl string `yaml:"server_url"` +type SSHMonitorConfig struct { + Enabled bool `json:"enabled"` + AlertOnRootLogin bool `json:"alert_on_root_login"` +} + +type SystemMonitorConfig struct { + CollectInterval string `json:"collect_interval"` + CollectNetwork bool `json:"collect_network"` + CollectProcess bool `json:"collect_process"` + ProcessLimit int `json:"process_limit"` +} + +type MonitorConfig struct { + SSHMonitorConfig SSHMonitorConfig `json:"ssh_monitor"` + SystemMonitorConfig SystemMonitorConfig `json:"system_monitor"` +} + +type ConnectionConfig struct { + CenterServerURL string `json:"center_server_url"` + AuditServerURL string `json:"audit_server_url"` } type OfficialConfig struct { - WhitelistFiles map[string][]string `yaml:"whitelist_files"` - WhitelistProcesses []string `yaml:"whitelist_processes"` - IgnoredPaths []string `yaml:"ignored_paths"` + Version string `json:"version"` + WhiteListFiles map[string]string `json:"white_list_files"` + WhiteListProcesses []string `json:"white_list_processes"` + IgnoredPaths []string `json:"ignored_paths"` + ScanPaths []string `json:"scan_paths"` } type UserConfig struct { - AuditServerUrl string `json:"audit_server_url"` // 审计服务器地址 - // 用户补充的白名单文件 - SupplementFiles map[string][]string `json:"supplement_files"` - // 用户补充的进程列表 - // Key: 进程名, Value: 启动指令(如果为空则仅作为白名单,如果不为空则需保活) - SupplementProcesses map[string]string `json:"supplement_processes"` - IgnoredPaths []string `json:"ignored_paths"` - CheckPermPaths []string `json:"check_perm_paths"` // 检查权限的目录 - - // 邮件配置 - EmailConfig EmailConfig `json:"email_config"` + Version string `json:"version"` + Connection ConnectionConfig `json:"connection"` + Models ModelSwitches `json:"models"` + SupplementFiles map[string]string `json:"supplement_files"` + SupplementProcesses []string `json:"supplement_processes"` + MonitorConfig MonitorConfig `json:"monitor_config"` } -type EmailConfig struct { - ImapServer string `json:"imap_server"` - EmergencyMail []string `json:"emergency_mail"` +type Configuration struct { + Official OfficialConfig // 官方配置 + User UserConfig // 用户自定义配置 } -type SSHMonitor struct { - Enabled bool `yaml:"enabled"` - DisplayOnShell bool `yaml:"display_on_shell"` - AlertOnRootLogin bool `yaml:"alert_on_root_login"` +func NewDefaultUserConfig() UserConfig { + return UserConfig{ + Version: "BuildInDefault", + Connection: ConnectionConfig{ + CenterServerURL: "ws://localhost:8090/api/v1/ws", + AuditServerURL: "ws://localhost:8090/api/v1/ws", + }, + Models: ModelSwitches{ + FileScanner: false, + FileWatcher: true, + SSHMonitor: true, + SystemMonitor: true, + }, + MonitorConfig: MonitorConfig{ + SSHMonitorConfig: SSHMonitorConfig{Enabled: true}, + SystemMonitorConfig: SystemMonitorConfig{CollectInterval: "30s", CollectNetwork: true, CollectProcess: true, ProcessLimit: 10}, + }, + } } diff --git a/internal/monitor/info_monitor.go b/internal/monitor/info_monitor.go index 63c2451..47f6068 100644 --- a/internal/monitor/info_monitor.go +++ b/internal/monitor/info_monitor.go @@ -1,873 +1,873 @@ -package monitor - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - "time" - - "github.com/shirou/gopsutil/v4/cpu" - "github.com/shirou/gopsutil/v4/disk" - "github.com/shirou/gopsutil/v4/host" - "github.com/shirou/gopsutil/v4/load" - "github.com/shirou/gopsutil/v4/mem" - "github.com/shirou/gopsutil/v4/net" - "github.com/shirou/gopsutil/v4/process" -) - -// InfoMonitor 服务器信息监控器 -type InfoMonitor struct { - config *InfoMonitorConfig - logFile *os.File - stopChan chan struct{} - metricsChan chan ServerMetrics -} - -// InfoMonitorConfig 信息监控配置 -type InfoMonitorConfig struct { - Enabled bool `yaml:"enabled"` - Interval time.Duration `yaml:"interval"` // 采集间隔 - LogFilePath string `yaml:"log_file_path"` // 日志文件路径 - MaxLogSize int64 `yaml:"max_log_size"` // 最大日志大小(字节) - LogRetention int `yaml:"log_retention"` // 日志保留天数 - ProcessLimit int `yaml:"process_limit"` // 显示进程数限制 - CollectNetwork bool `yaml:"collect_network"` // 是否收集网络信息 - CollectProcess bool `yaml:"collect_process"` // 是否收集进程信息 -} - -// ServerMetrics 服务器指标 -type ServerMetrics struct { - Timestamp time.Time `json:"timestamp"` - CPU CPUInfo `json:"cpu"` - Memory MemoryInfo `json:"memory"` - Disk []DiskInfo `json:"disk"` - Network NetworkInfo `json:"network"` - Load LoadInfo `json:"load"` - Processes []ProcessInfo `json:"processes"` - Host HostInfo `json:"host"` - Runtime RuntimeInfo `json:"runtime"` - QuickMetrics QuickMetrics `json:"quick_metrics"` -} - -// CPUInfo CPU信息 -type CPUInfo struct { - Model string `json:"model"` - Cores int `json:"cores"` - LogicalCores int `json:"logical_cores"` - UsagePercent float64 `json:"usage_percent"` - PerCorePercent []float64 `json:"per_core_percent"` - Mhz float64 `json:"mhz"` - CacheSize int `json:"cache_size"` -} - -// MemoryInfo 内存信息 -type MemoryInfo struct { - TotalGB float64 `json:"total_gb"` - UsedGB float64 `json:"used_gb"` - AvailableGB float64 `json:"available_gb"` - UsedPercent float64 `json:"used_percent"` - SwapTotalGB float64 `json:"swap_total_gb"` - SwapUsedGB float64 `json:"swap_used_gb"` -} - -// DiskInfo 磁盘信息 -type DiskInfo struct { - Mountpoint string `json:"mountpoint"` - Device string `json:"device"` - Fstype string `json:"fstype"` - TotalGB float64 `json:"total_gb"` - UsedGB float64 `json:"used_gb"` - FreeGB float64 `json:"free_gb"` - UsedPercent float64 `json:"used_percent"` - InodesPercent float64 `json:"inodes_percent"` -} - -// NetworkInfo 网络信息 -type NetworkInfo struct { - Interfaces []NetworkInterface `json:"interfaces"` - TotalRecvMB float64 `json:"total_recv_mb"` - TotalSentMB float64 `json:"total_sent_mb"` - TCPConnections int `json:"tcp_connections"` - EstablishedConn int `json:"established_conn"` -} - -// NetworkInterface 网络接口 -type NetworkInterface struct { - Name string `json:"name"` - HardwareAddr string `json:"hardware_addr"` - IPAddresses []string `json:"ip_addresses"` -} - -// LoadInfo 负载信息 -type LoadInfo struct { - Load1 float64 `json:"load_1"` - Load5 float64 `json:"load_5"` - Load15 float64 `json:"load_15"` - RelativeLoad1 float64 `json:"relative_load_1"` - RelativeLoad5 float64 `json:"relative_load_5"` - RelativeLoad15 float64 `json:"relative_load_15"` - ProcsRunning int `json:"procs_running"` // 改为 int 类型 - ProcsTotal int `json:"procs_total"` // 改为 int 类型 -} - -// ProcessInfo 进程信息 -type ProcessInfo struct { - PID int32 `json:"pid"` - Name string `json:"name"` - Cmdline string `json:"cmdline"` - MemoryMB float64 `json:"memory_mb"` - CPUPercent float64 `json:"cpu_percent"` -} - -// HostInfo 主机信息 -type HostInfo struct { - Hostname string `json:"hostname"` - OS string `json:"os"` - Platform string `json:"platform"` - PlatformVersion string `json:"platform_version"` - KernelVersion string `json:"kernel_version"` - BootTime time.Time `json:"boot_time"` - Uptime string `json:"uptime"` - CPUCount uint64 `json:"cpu_count"` - Architecture string `json:"architecture"` - HostID string `json:"host_id"` -} - -// RuntimeInfo 运行时信息 -type RuntimeInfo struct { - GoVersion string `json:"go_version"` - GOOS string `json:"goos"` - GOARCH string `json:"goarch"` - GOROOT string `json:"goroot"` - GOMAXPROCS int `json:"gomaxprocs"` - NumCPU int `json:"num_cpu"` - NumGoroutine int `json:"num_goroutine"` -} - -// QuickMetrics 快速指标 -type QuickMetrics struct { - CPUPercent float64 `json:"cpu_percent"` - MemoryPercent float64 `json:"memory_percent"` - RootDiskPercent float64 `json:"root_disk_percent"` - AvailableMemoryGB float64 `json:"available_memory_gb"` -} - -// NewInfoMonitor 创建信息监控器 -func NewInfoMonitor(cfg *InfoMonitorConfig, metricsChan chan ServerMetrics) *InfoMonitor { - if cfg == nil { - cfg = &InfoMonitorConfig{ - Enabled: true, - Interval: 30 * time.Second, - ProcessLimit: 10, - CollectNetwork: true, - CollectProcess: true, - } - } - - if cfg.Interval == 0 { - cfg.Interval = 30 * time.Second - } - - if cfg.ProcessLimit == 0 { - cfg.ProcessLimit = 10 - } - - if cfg.MaxLogSize == 0 { - cfg.MaxLogSize = 100 * 1024 * 1024 // 100MB - } - - if cfg.LogFilePath == "" { - cfg.LogFilePath = "/var/log/sysmonitord/info_monitor.log" - } - - return &InfoMonitor{ - config: cfg, - stopChan: make(chan struct{}), - metricsChan: metricsChan, - } -} - -// Start 启动信息监控 -func (m *InfoMonitor) Start() error { - log.Println("启动服务器信息监控...") - - // 初始化日志文件 - if err := m.initLogFile(); err != nil { - return fmt.Errorf("初始化日志文件失败: %v", err) - } - - // 启动监控循环 - go m.monitorLoop() - - return nil -} - -// Stop 停止信息监控 -func (m *InfoMonitor) Stop() error { - log.Println("停止服务器信息监控...") - close(m.stopChan) - - if m.logFile != nil { - m.logFile.Close() - } - - return nil -} - -// initLogFile 初始化日志文件 -func (m *InfoMonitor) initLogFile() error { - if m.config.LogFilePath == "" { - m.config.LogFilePath = "/var/log/sysmonitord/info_monitor.log" - } - - // 创建日志目录 - logDir := filepath.Dir(m.config.LogFilePath) - if err := os.MkdirAll(logDir, 0755); err != nil { - return err - } - - // 打开日志文件 - file, err := os.OpenFile(m.config.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - - m.logFile = file - - // 启动日志轮转检查 - go m.logRotateCheck() - - return nil -} - -// logRotateCheck 日志轮转检查 -func (m *InfoMonitor) logRotateCheck() { - ticker := time.NewTicker(1 * time.Hour) - defer ticker.Stop() - - for { - select { - case <-m.stopChan: - return - case <-ticker.C: - if m.logFile != nil { - // 检查文件大小 - if info, err := m.logFile.Stat(); err == nil { - if info.Size() > m.config.MaxLogSize { - m.rotateLogFile() - } - } - } - } - } -} - -// rotateLogFile 轮转日志文件 -func (m *InfoMonitor) rotateLogFile() { - if m.logFile != nil { - m.logFile.Close() - - // 重命名旧文件 - timestamp := time.Now().Format("20060102_150405") - backupFile := fmt.Sprintf("%s.%s", m.config.LogFilePath, timestamp) - oldPath := m.config.LogFilePath - - // 重新打开日志文件 - file, err := os.OpenFile(m.config.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - log.Printf("重新打开日志文件失败: %v", err) - return - } - - m.logFile = file - - // 异步重命名旧文件 - go func() { - if err := os.Rename(oldPath, backupFile); err != nil { - log.Printf("重命名日志文件失败: %v", err) - } - }() - } -} - -// monitorLoop 监控循环 -func (m *InfoMonitor) monitorLoop() { - // 首次立即执行 - m.collectAndLogMetrics() - - ticker := time.NewTicker(m.config.Interval) - defer ticker.Stop() - - for { - select { - case <-m.stopChan: - return - case <-ticker.C: - m.collectAndLogMetrics() - } - } -} - -// collectAndLogMetrics 收集并记录指标 -func (m *InfoMonitor) collectAndLogMetrics() { - startTime := time.Now() - metrics := m.collectAllMetrics() - collectionTime := time.Since(startTime) - - log.Printf("收集指标完成,耗时: %v", collectionTime) - - // 记录到日志文件 - m.logMetrics(metrics) - - // 发送到metrics通道(如果有) - if m.metricsChan != nil { - select { - case m.metricsChan <- metrics: - default: - // 通道满,丢弃数据 - log.Printf("警告: metrics通道已满,丢弃数据") - } - } - - // 输出到控制台 - m.displayMetrics(metrics) -} - -// collectAllMetrics 收集所有指标 -func (m *InfoMonitor) collectAllMetrics() ServerMetrics { - return ServerMetrics{ - Timestamp: time.Now(), - CPU: m.getCPUInfo(), - Memory: m.getMemoryInfo(), - Disk: m.getDiskInfo(), - Network: m.getNetworkInfo(), - Load: m.getLoadInfo(), - Processes: m.getProcessInfo(), - Host: m.getHostInfo(), - Runtime: m.getRuntimeInfo(), - QuickMetrics: m.getQuickMetrics(), - } -} - -// getHostInfo 获取主机信息 -func (m *InfoMonitor) getHostInfo() HostInfo { - hostInfo, err := host.Info() - if err != nil { - log.Printf("获取主机信息失败: %v", err) - return HostInfo{} - } - - bootTime := time.Unix(int64(hostInfo.BootTime), 0) - uptime := time.Since(bootTime) - hours := int(uptime.Hours()) - minutes := int(uptime.Minutes()) % 60 - seconds := int(uptime.Seconds()) % 60 - - return HostInfo{ - Hostname: hostInfo.Hostname, - OS: hostInfo.OS, - Platform: hostInfo.Platform, - PlatformVersion: hostInfo.PlatformVersion, - KernelVersion: hostInfo.KernelVersion, - BootTime: bootTime, - Uptime: fmt.Sprintf("%d小时%d分钟%d秒", hours, minutes, seconds), - CPUCount: hostInfo.Procs, - Architecture: hostInfo.KernelArch, - HostID: hostInfo.HostID, - } -} - -// getCPUInfo 获取CPU信息 -func (m *InfoMonitor) getCPUInfo() CPUInfo { - physicalCount, _ := cpu.Counts(false) - logicalCount, _ := cpu.Counts(true) - - percent, _ := cpu.Percent(200*time.Millisecond, false) - perCorePercent, _ := cpu.Percent(200*time.Millisecond, true) - - cpuInfoList, _ := cpu.Info() - var model string - var mhz float64 - var cacheSize int - - if len(cpuInfoList) > 0 { - model = cpuInfoList[0].ModelName - mhz = cpuInfoList[0].Mhz - // 将 int32 转换为 int - cacheSize = int(cpuInfoList[0].CacheSize) - } - - usagePercent := 0.0 - if len(percent) > 0 { - usagePercent = percent[0] - } - - return CPUInfo{ - Model: model, - Cores: physicalCount, - LogicalCores: logicalCount, - UsagePercent: usagePercent, - PerCorePercent: perCorePercent, - Mhz: mhz, - CacheSize: cacheSize, - } -} - -// getMemoryInfo 获取内存信息 -func (m *InfoMonitor) getMemoryInfo() MemoryInfo { - vMem, err := mem.VirtualMemory() - if err != nil { - log.Printf("获取内存信息失败: %v", err) - return MemoryInfo{} - } - - swap, _ := mem.SwapMemory() - - return MemoryInfo{ - TotalGB: float64(vMem.Total) / (1024 * 1024 * 1024), - UsedGB: float64(vMem.Used) / (1024 * 1024 * 1024), - AvailableGB: float64(vMem.Available) / (1024 * 1024 * 1024), - UsedPercent: vMem.UsedPercent, - SwapTotalGB: float64(swap.Total) / (1024 * 1024 * 1024), - SwapUsedGB: float64(swap.Used) / (1024 * 1024 * 1024), - } -} - -// getDiskInfo 获取磁盘信息 -func (m *InfoMonitor) getDiskInfo() []DiskInfo { - partitions, err := disk.Partitions(false) - if err != nil { - log.Printf("获取磁盘分区失败: %v", err) - return nil - } - - var disks []DiskInfo - for _, partition := range partitions { - // 过滤掉特殊文件系统 - if partition.Fstype == "" || - partition.Fstype == "tmpfs" || - partition.Fstype == "devtmpfs" || - partition.Fstype == "squashfs" || - partition.Fstype == "efivarfs" || - partition.Fstype == "debugfs" || - partition.Fstype == "securityfs" || - partition.Fstype == "cgroup" || - partition.Fstype == "cgroup2" || - partition.Fstype == "pstore" || - partition.Fstype == "autofs" { - continue - } - - usage, err := disk.Usage(partition.Mountpoint) - if err != nil { - continue - } - - inodesPercent := 0.0 - if usage.InodesUsedPercent > 0 { - inodesPercent = usage.InodesUsedPercent - } - - disks = append(disks, DiskInfo{ - Mountpoint: partition.Mountpoint, - Device: partition.Device, - Fstype: partition.Fstype, - TotalGB: float64(usage.Total) / (1024 * 1024 * 1024), - UsedGB: float64(usage.Used) / (1024 * 1024 * 1024), - FreeGB: float64(usage.Free) / (1024 * 1024 * 1024), - UsedPercent: usage.UsedPercent, - InodesPercent: inodesPercent, - }) - } - - return disks -} - -// getNetworkInfo 获取网络信息 -func (m *InfoMonitor) getNetworkInfo() NetworkInfo { - if !m.config.CollectNetwork { - return NetworkInfo{} - } - - interfaces, err := net.Interfaces() - if err != nil { - log.Printf("获取网络接口失败: %v", err) - return NetworkInfo{} - } - - var netInterfaces []NetworkInterface - for _, iface := range interfaces { - if len(iface.Addrs) > 0 && iface.Name != "lo" { - var ips []string - for _, addr := range iface.Addrs { - ips = append(ips, addr.Addr) - } - - netInterfaces = append(netInterfaces, NetworkInterface{ - Name: iface.Name, - HardwareAddr: iface.HardwareAddr, - IPAddresses: ips, - }) - } - } - - // 获取网络IO统计 - ioCounters, _ := net.IOCounters(true) - var totalRecv, totalSent uint64 - for _, io := range ioCounters { - totalRecv += io.BytesRecv - totalSent += io.BytesSent - } - - // 获取TCP连接数 - tcpConns, _ := net.Connections("tcp") - established := 0 - for _, conn := range tcpConns { - if conn.Status == "ESTABLISHED" { - established++ - } - } - - return NetworkInfo{ - Interfaces: netInterfaces, - TotalRecvMB: float64(totalRecv) / (1024 * 1024), - TotalSentMB: float64(totalSent) / (1024 * 1024), - TCPConnections: len(tcpConns), - EstablishedConn: established, - } -} - -// getLoadInfo 获取负载信息 -func (m *InfoMonitor) getLoadInfo() LoadInfo { - avg, err := load.Avg() - if err != nil { - log.Printf("获取系统负载失败: %v", err) - return LoadInfo{} - } - - misc, _ := load.Misc() - logicalCount, _ := cpu.Counts(true) - - relativeLoad1 := 0.0 - relativeLoad5 := 0.0 - relativeLoad15 := 0.0 - if logicalCount > 0 { - relativeLoad1 = avg.Load1 / float64(logicalCount) - relativeLoad5 = avg.Load5 / float64(logicalCount) - relativeLoad15 = avg.Load15 / float64(logicalCount) - } - - return LoadInfo{ - Load1: avg.Load1, - Load5: avg.Load5, - Load15: avg.Load15, - RelativeLoad1: relativeLoad1, - RelativeLoad5: relativeLoad5, - RelativeLoad15: relativeLoad15, - // load.Misc() 返回的是 int 类型 - ProcsRunning: misc.ProcsRunning, - ProcsTotal: misc.ProcsTotal, - } -} - -// getProcessInfo 获取进程信息 -func (m *InfoMonitor) getProcessInfo() []ProcessInfo { - if !m.config.CollectProcess { - return nil - } - - processes, err := process.Processes() - if err != nil { - log.Printf("获取进程列表失败: %v", err) - return nil - } - - var procList []ProcessInfo - limit := m.config.ProcessLimit - if limit <= 0 { - limit = 10 // 默认显示10个进程 - } - - count := 0 - for _, p := range processes { - if count >= limit { - break - } - - name, err := p.Name() - if err != nil || name == "" || name == " " { - continue - } - - cmdline, _ := p.Cmdline() - memInfo, _ := p.MemoryInfo() - cpuPercent, _ := p.CPUPercent() - - var memMB float64 - if memInfo != nil { - memMB = float64(memInfo.RSS) / (1024 * 1024) - } - - procList = append(procList, ProcessInfo{ - PID: p.Pid, - Name: name, - Cmdline: cmdline, - MemoryMB: memMB, - CPUPercent: cpuPercent, - }) - - count++ - } - - return procList -} - -// getRuntimeInfo 获取运行时信息 -func (m *InfoMonitor) getRuntimeInfo() RuntimeInfo { - return RuntimeInfo{ - GoVersion: runtime.Version(), - GOOS: runtime.GOOS, - GOARCH: runtime.GOARCH, - GOROOT: runtime.GOROOT(), - GOMAXPROCS: runtime.GOMAXPROCS(0), - NumCPU: runtime.NumCPU(), - NumGoroutine: runtime.NumGoroutine(), - } -} - -// getQuickMetrics 获取快速指标 -func (m *InfoMonitor) getQuickMetrics() QuickMetrics { - cpuPercent, _ := cpu.Percent(100*time.Millisecond, false) - memInfo, _ := mem.VirtualMemory() - rootUsage, _ := disk.Usage("/") - - quickCPU := 0.0 - if len(cpuPercent) > 0 { - quickCPU = cpuPercent[0] - } - - memPercent := 0.0 - availableGB := 0.0 - if memInfo != nil { - memPercent = memInfo.UsedPercent - availableGB = float64(memInfo.Available) / (1024 * 1024 * 1024) - } - - rootDiskPercent := 0.0 - if rootUsage != nil { - rootDiskPercent = rootUsage.UsedPercent - } - - return QuickMetrics{ - CPUPercent: quickCPU, - MemoryPercent: memPercent, - RootDiskPercent: rootDiskPercent, - AvailableMemoryGB: availableGB, - } -} - -// logMetrics 记录指标到日志文件 -func (m *InfoMonitor) logMetrics(metrics ServerMetrics) { - if m.logFile == nil { - return - } - - // 基本指标日志 - basicLog := fmt.Sprintf("[INFO-METRIC] %s | CPU:%.2f%% | MEM:%.2f%% | Load1:%.2f | DiskRoot:%.2f%%", - metrics.Timestamp.Format("2006-01-02 15:04:05"), - metrics.QuickMetrics.CPUPercent, - metrics.QuickMetrics.MemoryPercent, - metrics.Load.Load1, - metrics.QuickMetrics.RootDiskPercent, - ) - - // 详细指标日志 - detailedLog := fmt.Sprintf("\n[INFO-DETAIL] Host: %s, Uptime: %s, Cores: %d/%d, Mem: %.2f/%.2f GB", - metrics.Host.Hostname, - metrics.Host.Uptime, - metrics.CPU.Cores, - metrics.CPU.LogicalCores, - metrics.Memory.UsedGB, - metrics.Memory.TotalGB, - ) - - logLine := basicLog + detailedLog + "\n" - - if _, err := m.logFile.WriteString(logLine); err != nil { - log.Printf("写入日志文件失败: %v", err) - } - - // 确保数据写入磁盘 - m.logFile.Sync() -} - -// displayMetrics 显示指标到控制台 -func (m *InfoMonitor) displayMetrics(metrics ServerMetrics) { - // 使用不同颜色显示不同类型的指标 - fmt.Printf("\n\x1b[36m════════════════ 服务器监控指标 [%s] ════════════════\x1b[0m\n", - metrics.Timestamp.Format("15:04:05")) - - // 主机信息 - fmt.Printf("\n\x1b[33m主机信息:\x1b[0m\n") - fmt.Printf(" \x1b[32m主机名:\x1b[0m %s\n", metrics.Host.Hostname) - fmt.Printf(" \x1b[32m运行时间:\x1b[0m %s\n", metrics.Host.Uptime) - fmt.Printf(" \x1b[32m系统:\x1b[0m %s %s\n", metrics.Host.Platform, metrics.Host.PlatformVersion) - - // CPU信息 - fmt.Printf("\n\x1b[33mCPU信息:\x1b[0m\n") - fmt.Printf(" \x1b[32m型号:\x1b[0m %s\n", metrics.CPU.Model) - fmt.Printf(" \x1b[32m核心:\x1b[0m %d物理/%d逻辑\n", metrics.CPU.Cores, metrics.CPU.LogicalCores) - - // 根据CPU使用率显示不同颜色 - cpuColor := "\x1b[32m" // 绿色 - if metrics.CPU.UsagePercent > 70 { - cpuColor = "\x1b[33m" // 黄色 - } - if metrics.CPU.UsagePercent > 90 { - cpuColor = "\x1b[31m" // 红色 - } - fmt.Printf(" \x1b[32m使用率:\x1b[0m %s%.2f%%\x1b[0m", cpuColor, metrics.CPU.UsagePercent) - - if len(metrics.CPU.PerCorePercent) > 0 { - fmt.Printf(" (") - for i, p := range metrics.CPU.PerCorePercent { - if i > 0 { - fmt.Printf(" ") - } - coreColor := "\x1b[32m" - if p > 70 { - coreColor = "\x1b[33m" - } - if p > 90 { - coreColor = "\x1b[31m" - } - fmt.Printf("%s%d:%.0f%%\x1b[0m", coreColor, i, p) - } - fmt.Printf(")") - } - fmt.Println() - - // 内存信息 - fmt.Printf("\n\x1b[33m内存信息:\x1b[0m\n") - - // 根据内存使用率显示不同颜色 - memColor := "\x1b[32m" - if metrics.Memory.UsedPercent > 70 { - memColor = "\x1b[33m" - } - if metrics.Memory.UsedPercent > 90 { - memColor = "\x1b[31m" - } - - memBar := getProgressBar(metrics.Memory.UsedPercent, 20) - fmt.Printf(" \x1b[32m使用率:\x1b[0m %s%.1f%%\x1b[0m %s\n", - memColor, metrics.Memory.UsedPercent, memBar) - fmt.Printf(" \x1b[32m总量/已用/可用:\x1b[0m %.2f/%.2f/%.2f GB\n", - metrics.Memory.TotalGB, metrics.Memory.UsedGB, metrics.Memory.AvailableGB) - - if metrics.Memory.SwapTotalGB > 0 { - fmt.Printf(" \x1b[32m交换空间:\x1b[0m %.2f GB\n", metrics.Memory.SwapTotalGB) - } - - // 磁盘信息 - if len(metrics.Disk) > 0 { - fmt.Printf("\n\x1b[33m磁盘使用情况:\x1b[0m\n") - for _, disk := range metrics.Disk { - diskColor := "\x1b[32m" - if disk.UsedPercent > 70 { - diskColor = "\x1b[33m" - } - if disk.UsedPercent > 90 { - diskColor = "\x1b[31m" - } - - diskBar := getProgressBar(disk.UsedPercent, 15) - fmt.Printf(" \x1b[32m%s:\x1b[0m %s%.1f%%\x1b[0m %s %.2f/%.2f GB\n", - disk.Mountpoint, diskColor, disk.UsedPercent, diskBar, disk.UsedGB, disk.TotalGB) - } - } - - // 负载信息 - fmt.Printf("\n\x1b[33m系统负载:\x1b[0m\n") - load1Color := "\x1b[32m" - if metrics.Load.RelativeLoad1 > 1.0 { - load1Color = "\x1b[33m" - } - if metrics.Load.RelativeLoad1 > 2.0 { - load1Color = "\x1b[31m" - } - - fmt.Printf(" \x1b[32m1/5/15分钟:\x1b[0m %s%.2f\x1b[0m/%.2f/%.2f\n", - load1Color, metrics.Load.Load1, metrics.Load.Load5, metrics.Load.Load15) - fmt.Printf(" \x1b[32m相对负载:\x1b[0m %.2f/%.2f/%.2f\n", - metrics.Load.RelativeLoad1, metrics.Load.RelativeLoad5, metrics.Load.RelativeLoad15) - fmt.Printf(" \x1b[32m进程:\x1b[0m %d运行中 / %d总计\n", - metrics.Load.ProcsRunning, metrics.Load.ProcsTotal) - - // 网络信息(如果启用了) - if m.config.CollectNetwork && len(metrics.Network.Interfaces) > 0 { - fmt.Printf("\n\x1b[33m网络信息:\x1b[0m\n") - fmt.Printf(" \x1b[32mTCP连接:\x1b[0m %d (已建立: %d)\n", - metrics.Network.TCPConnections, metrics.Network.EstablishedConn) - fmt.Printf(" \x1b[32m流量:\x1b[0m 接收:%.2f MB 发送:%.2f MB\n", - metrics.Network.TotalRecvMB, metrics.Network.TotalSentMB) - } - - // 进程信息 - if len(metrics.Processes) > 0 { - fmt.Printf("\n\x1b[33mTOP进程 (按内存排序):\x1b[0m\n") - for i, proc := range metrics.Processes { - if i >= 5 { // 只显示前5个 - break - } - procColor := "\x1b[36m" - if proc.CPUPercent > 10 { - procColor = "\x1b[33m" - } - if proc.CPUPercent > 30 { - procColor = "\x1b[31m" - } - - // 截断过长的命令行 - cmdDisplay := proc.Cmdline - if len(cmdDisplay) > 50 { - cmdDisplay = cmdDisplay[:47] + "..." - } - - fmt.Printf(" %s%5d\x1b[0m %-20s %s%.1f%%\x1b[0m %.1fMB %s\n", - procColor, proc.PID, proc.Name, procColor, proc.CPUPercent, - proc.MemoryMB, cmdDisplay) - } - } - - fmt.Printf("\n\x1b[36m══════════════════════════════════════════════════════\x1b[0m\n") -} - -// getProgressBar 获取进度条 -func getProgressBar(percent float64, width int) string { - filled := int((percent / 100.0) * float64(width)) - if filled > width { - filled = width - } - - bar := "[" - for i := 0; i < width; i++ { - if i < filled { - // 根据填充量使用不同颜色 - if i < width/3 { - bar += "\x1b[32m=\x1b[0m" // 绿色 - } else if i < width*2/3 { - bar += "\x1b[33m=\x1b[0m" // 黄色 - } else { - bar += "\x1b[31m=\x1b[0m" // 红色 - } - } else { - bar += " " - } - } - bar += "]" - - return bar -} +package monitor + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/load" + "github.com/shirou/gopsutil/v4/mem" + "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" +) + +// InfoMonitor 服务器信息监控器 +type InfoMonitor struct { + config *InfoMonitorConfig + logFile *os.File + stopChan chan struct{} + metricsChan chan ServerMetrics +} + +// InfoMonitorConfig 信息监控配置 +type InfoMonitorConfig struct { + Enabled bool `yaml:"enabled"` + Interval time.Duration `yaml:"interval"` // 采集间隔 + LogFilePath string `yaml:"log_file_path"` // 日志文件路径 + MaxLogSize int64 `yaml:"max_log_size"` // 最大日志大小(字节) + LogRetention int `yaml:"log_retention"` // 日志保留天数 + ProcessLimit int `yaml:"process_limit"` // 显示进程数限制 + CollectNetwork bool `yaml:"collect_network"` // 是否收集网络信息 + CollectProcess bool `yaml:"collect_process"` // 是否收集进程信息 +} + +// ServerMetrics 服务器指标 +type ServerMetrics struct { + Timestamp time.Time `json:"timestamp"` + CPU CPUInfo `json:"cpu"` + Memory MemoryInfo `json:"memory"` + Disk []DiskInfo `json:"disk"` + Network NetworkInfo `json:"network"` + Load LoadInfo `json:"load"` + Processes []ProcessInfo `json:"processes"` + Host HostInfo `json:"host"` + Runtime RuntimeInfo `json:"runtime"` + QuickMetrics QuickMetrics `json:"quick_metrics"` +} + +// CPUInfo CPU信息 +type CPUInfo struct { + Model string `json:"model"` + Cores int `json:"cores"` + LogicalCores int `json:"logical_cores"` + UsagePercent float64 `json:"usage_percent"` + PerCorePercent []float64 `json:"per_core_percent"` + Mhz float64 `json:"mhz"` + CacheSize int `json:"cache_size"` +} + +// MemoryInfo 内存信息 +type MemoryInfo struct { + TotalGB float64 `json:"total_gb"` + UsedGB float64 `json:"used_gb"` + AvailableGB float64 `json:"available_gb"` + UsedPercent float64 `json:"used_percent"` + SwapTotalGB float64 `json:"swap_total_gb"` + SwapUsedGB float64 `json:"swap_used_gb"` +} + +// DiskInfo 磁盘信息 +type DiskInfo struct { + Mountpoint string `json:"mountpoint"` + Device string `json:"device"` + Fstype string `json:"fstype"` + TotalGB float64 `json:"total_gb"` + UsedGB float64 `json:"used_gb"` + FreeGB float64 `json:"free_gb"` + UsedPercent float64 `json:"used_percent"` + InodesPercent float64 `json:"inodes_percent"` +} + +// NetworkInfo 网络信息 +type NetworkInfo struct { + Interfaces []NetworkInterface `json:"interfaces"` + TotalRecvMB float64 `json:"total_recv_mb"` + TotalSentMB float64 `json:"total_sent_mb"` + TCPConnections int `json:"tcp_connections"` + EstablishedConn int `json:"established_conn"` +} + +// NetworkInterface 网络接口 +type NetworkInterface struct { + Name string `json:"name"` + HardwareAddr string `json:"hardware_addr"` + IPAddresses []string `json:"ip_addresses"` +} + +// LoadInfo 负载信息 +type LoadInfo struct { + Load1 float64 `json:"load_1"` + Load5 float64 `json:"load_5"` + Load15 float64 `json:"load_15"` + RelativeLoad1 float64 `json:"relative_load_1"` + RelativeLoad5 float64 `json:"relative_load_5"` + RelativeLoad15 float64 `json:"relative_load_15"` + ProcsRunning int `json:"procs_running"` // 改为 int 类型 + ProcsTotal int `json:"procs_total"` // 改为 int 类型 +} + +// ProcessInfo 进程信息 +type ProcessInfo struct { + PID int32 `json:"pid"` + Name string `json:"name"` + Cmdline string `json:"cmdline"` + MemoryMB float64 `json:"memory_mb"` + CPUPercent float64 `json:"cpu_percent"` +} + +// HostInfo 主机信息 +type HostInfo struct { + Hostname string `json:"hostname"` + OS string `json:"os"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + KernelVersion string `json:"kernel_version"` + BootTime time.Time `json:"boot_time"` + Uptime string `json:"uptime"` + CPUCount uint64 `json:"cpu_count"` + Architecture string `json:"architecture"` + HostID string `json:"host_id"` +} + +// RuntimeInfo 运行时信息 +type RuntimeInfo struct { + GoVersion string `json:"go_version"` + GOOS string `json:"goos"` + GOARCH string `json:"goarch"` + GOROOT string `json:"goroot"` + GOMAXPROCS int `json:"gomaxprocs"` + NumCPU int `json:"num_cpu"` + NumGoroutine int `json:"num_goroutine"` +} + +// QuickMetrics 快速指标 +type QuickMetrics struct { + CPUPercent float64 `json:"cpu_percent"` + MemoryPercent float64 `json:"memory_percent"` + RootDiskPercent float64 `json:"root_disk_percent"` + AvailableMemoryGB float64 `json:"available_memory_gb"` +} + +// NewInfoMonitor 创建信息监控器 +func NewInfoMonitor(cfg *InfoMonitorConfig, metricsChan chan ServerMetrics) *InfoMonitor { + if cfg == nil { + cfg = &InfoMonitorConfig{ + Enabled: true, + Interval: 30 * time.Second, + ProcessLimit: 10, + CollectNetwork: true, + CollectProcess: true, + } + } + + if cfg.Interval == 0 { + cfg.Interval = 30 * time.Second + } + + if cfg.ProcessLimit == 0 { + cfg.ProcessLimit = 10 + } + + if cfg.MaxLogSize == 0 { + cfg.MaxLogSize = 100 * 1024 * 1024 // 100MB + } + + if cfg.LogFilePath == "" { + cfg.LogFilePath = "/var/log/sysmonitord/info_monitor.log" + } + + return &InfoMonitor{ + config: cfg, + stopChan: make(chan struct{}), + metricsChan: metricsChan, + } +} + +// Start 启动信息监控 +func (m *InfoMonitor) Start() error { + log.Println("启动服务器信息监控...") + + // 初始化日志文件 + if err := m.initLogFile(); err != nil { + return fmt.Errorf("初始化日志文件失败: %v", err) + } + + // 启动监控循环 + go m.monitorLoop() + + return nil +} + +// Stop 停止信息监控 +func (m *InfoMonitor) Stop() error { + log.Println("停止服务器信息监控...") + close(m.stopChan) + + if m.logFile != nil { + m.logFile.Close() + } + + return nil +} + +// initLogFile 初始化日志文件 +func (m *InfoMonitor) initLogFile() error { + if m.config.LogFilePath == "" { + m.config.LogFilePath = "/var/log/sysmonitord/info_monitor.log" + } + + // 创建日志目录 + logDir := filepath.Dir(m.config.LogFilePath) + if err := os.MkdirAll(logDir, 0755); err != nil { + return err + } + + // 打开日志文件 + file, err := os.OpenFile(m.config.LogFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return err + } + + m.logFile = file + + // 启动日志轮转检查 + go m.logRotateCheck() + + return nil +} + +// logRotateCheck 日志轮转检查 +func (m *InfoMonitor) logRotateCheck() { + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() + + for { + select { + case <-m.stopChan: + return + case <-ticker.C: + if m.logFile != nil { + // 检查文件大小 + if info, err := m.logFile.Stat(); err == nil { + if info.Size() > m.config.MaxLogSize { + m.rotateLogFile() + } + } + } + } + } +} + +// rotateLogFile 轮转日志文件 +func (m *InfoMonitor) rotateLogFile() { + if m.logFile != nil { + m.logFile.Close() + + // 重命名旧文件 + timestamp := time.Now().Format("20060102_150405") + backupFile := fmt.Sprintf("%s.%s", m.config.LogFilePath, timestamp) + oldPath := m.config.LogFilePath + + // 重新打开日志文件 + file, err := os.OpenFile(m.config.LogFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + log.Printf("重新打开日志文件失败: %v", err) + return + } + + m.logFile = file + + // 异步重命名旧文件 + go func() { + if err := os.Rename(oldPath, backupFile); err != nil { + log.Printf("重命名日志文件失败: %v", err) + } + }() + } +} + +// monitorLoop 监控循环 +func (m *InfoMonitor) monitorLoop() { + // 首次立即执行 + m.collectAndLogMetrics() + + ticker := time.NewTicker(m.config.Interval) + defer ticker.Stop() + + for { + select { + case <-m.stopChan: + return + case <-ticker.C: + m.collectAndLogMetrics() + } + } +} + +// collectAndLogMetrics 收集并记录指标 +func (m *InfoMonitor) collectAndLogMetrics() { + startTime := time.Now() + metrics := m.collectAllMetrics() + collectionTime := time.Since(startTime) + + log.Printf("收集指标完成,耗时: %v", collectionTime) + + // 记录到日志文件 + m.logMetrics(metrics) + + // 发送到metrics通道(如果有) + if m.metricsChan != nil { + select { + case m.metricsChan <- metrics: + default: + // 通道满,丢弃数据 + log.Printf("警告: metrics通道已满,丢弃数据") + } + } + + // 输出到控制台 + m.displayMetrics(metrics) +} + +// collectAllMetrics 收集所有指标 +func (m *InfoMonitor) collectAllMetrics() ServerMetrics { + return ServerMetrics{ + Timestamp: time.Now(), + CPU: m.getCPUInfo(), + Memory: m.getMemoryInfo(), + Disk: m.getDiskInfo(), + Network: m.getNetworkInfo(), + Load: m.getLoadInfo(), + Processes: m.getProcessInfo(), + Host: m.getHostInfo(), + Runtime: m.getRuntimeInfo(), + QuickMetrics: m.getQuickMetrics(), + } +} + +// getHostInfo 获取主机信息 +func (m *InfoMonitor) getHostInfo() HostInfo { + hostInfo, err := host.Info() + if err != nil { + log.Printf("获取主机信息失败: %v", err) + return HostInfo{} + } + + bootTime := time.Unix(int64(hostInfo.BootTime), 0) + uptime := time.Since(bootTime) + hours := int(uptime.Hours()) + minutes := int(uptime.Minutes()) % 60 + seconds := int(uptime.Seconds()) % 60 + + return HostInfo{ + Hostname: hostInfo.Hostname, + OS: hostInfo.OS, + Platform: hostInfo.Platform, + PlatformVersion: hostInfo.PlatformVersion, + KernelVersion: hostInfo.KernelVersion, + BootTime: bootTime, + Uptime: fmt.Sprintf("%d小时%d分钟%d秒", hours, minutes, seconds), + CPUCount: hostInfo.Procs, + Architecture: hostInfo.KernelArch, + HostID: hostInfo.HostID, + } +} + +// getCPUInfo 获取CPU信息 +func (m *InfoMonitor) getCPUInfo() CPUInfo { + physicalCount, _ := cpu.Counts(false) + logicalCount, _ := cpu.Counts(true) + + percent, _ := cpu.Percent(200*time.Millisecond, false) + perCorePercent, _ := cpu.Percent(200*time.Millisecond, true) + + cpuInfoList, _ := cpu.Info() + var model string + var mhz float64 + var cacheSize int + + if len(cpuInfoList) > 0 { + model = cpuInfoList[0].ModelName + mhz = cpuInfoList[0].Mhz + // 将 int32 转换为 int + cacheSize = int(cpuInfoList[0].CacheSize) + } + + usagePercent := 0.0 + if len(percent) > 0 { + usagePercent = percent[0] + } + + return CPUInfo{ + Model: model, + Cores: physicalCount, + LogicalCores: logicalCount, + UsagePercent: usagePercent, + PerCorePercent: perCorePercent, + Mhz: mhz, + CacheSize: cacheSize, + } +} + +// getMemoryInfo 获取内存信息 +func (m *InfoMonitor) getMemoryInfo() MemoryInfo { + vMem, err := mem.VirtualMemory() + if err != nil { + log.Printf("获取内存信息失败: %v", err) + return MemoryInfo{} + } + + swap, _ := mem.SwapMemory() + + return MemoryInfo{ + TotalGB: float64(vMem.Total) / (1024 * 1024 * 1024), + UsedGB: float64(vMem.Used) / (1024 * 1024 * 1024), + AvailableGB: float64(vMem.Available) / (1024 * 1024 * 1024), + UsedPercent: vMem.UsedPercent, + SwapTotalGB: float64(swap.Total) / (1024 * 1024 * 1024), + SwapUsedGB: float64(swap.Used) / (1024 * 1024 * 1024), + } +} + +// getDiskInfo 获取磁盘信息 +func (m *InfoMonitor) getDiskInfo() []DiskInfo { + partitions, err := disk.Partitions(false) + if err != nil { + log.Printf("获取磁盘分区失败: %v", err) + return nil + } + + var disks []DiskInfo + for _, partition := range partitions { + // 过滤掉特殊文件系统 + if partition.Fstype == "" || + partition.Fstype == "tmpfs" || + partition.Fstype == "devtmpfs" || + partition.Fstype == "squashfs" || + partition.Fstype == "efivarfs" || + partition.Fstype == "debugfs" || + partition.Fstype == "securityfs" || + partition.Fstype == "cgroup" || + partition.Fstype == "cgroup2" || + partition.Fstype == "pstore" || + partition.Fstype == "autofs" { + continue + } + + usage, err := disk.Usage(partition.Mountpoint) + if err != nil { + continue + } + + inodesPercent := 0.0 + if usage.InodesUsedPercent > 0 { + inodesPercent = usage.InodesUsedPercent + } + + disks = append(disks, DiskInfo{ + Mountpoint: partition.Mountpoint, + Device: partition.Device, + Fstype: partition.Fstype, + TotalGB: float64(usage.Total) / (1024 * 1024 * 1024), + UsedGB: float64(usage.Used) / (1024 * 1024 * 1024), + FreeGB: float64(usage.Free) / (1024 * 1024 * 1024), + UsedPercent: usage.UsedPercent, + InodesPercent: inodesPercent, + }) + } + + return disks +} + +// getNetworkInfo 获取网络信息 +func (m *InfoMonitor) getNetworkInfo() NetworkInfo { + if !m.config.CollectNetwork { + return NetworkInfo{} + } + + interfaces, err := net.Interfaces() + if err != nil { + log.Printf("获取网络接口失败: %v", err) + return NetworkInfo{} + } + + var netInterfaces []NetworkInterface + for _, iface := range interfaces { + if len(iface.Addrs) > 0 && iface.Name != "lo" { + var ips []string + for _, addr := range iface.Addrs { + ips = append(ips, addr.Addr) + } + + netInterfaces = append(netInterfaces, NetworkInterface{ + Name: iface.Name, + HardwareAddr: iface.HardwareAddr, + IPAddresses: ips, + }) + } + } + + // 获取网络IO统计 + ioCounters, _ := net.IOCounters(true) + var totalRecv, totalSent uint64 + for _, io := range ioCounters { + totalRecv += io.BytesRecv + totalSent += io.BytesSent + } + + // 获取TCP连接数 + tcpConns, _ := net.Connections("tcp") + established := 0 + for _, conn := range tcpConns { + if conn.Status == "ESTABLISHED" { + established++ + } + } + + return NetworkInfo{ + Interfaces: netInterfaces, + TotalRecvMB: float64(totalRecv) / (1024 * 1024), + TotalSentMB: float64(totalSent) / (1024 * 1024), + TCPConnections: len(tcpConns), + EstablishedConn: established, + } +} + +// getLoadInfo 获取负载信息 +func (m *InfoMonitor) getLoadInfo() LoadInfo { + avg, err := load.Avg() + if err != nil { + log.Printf("获取系统负载失败: %v", err) + return LoadInfo{} + } + + misc, _ := load.Misc() + logicalCount, _ := cpu.Counts(true) + + relativeLoad1 := 0.0 + relativeLoad5 := 0.0 + relativeLoad15 := 0.0 + if logicalCount > 0 { + relativeLoad1 = avg.Load1 / float64(logicalCount) + relativeLoad5 = avg.Load5 / float64(logicalCount) + relativeLoad15 = avg.Load15 / float64(logicalCount) + } + + return LoadInfo{ + Load1: avg.Load1, + Load5: avg.Load5, + Load15: avg.Load15, + RelativeLoad1: relativeLoad1, + RelativeLoad5: relativeLoad5, + RelativeLoad15: relativeLoad15, + // load.Misc() 返回的是 int 类型 + ProcsRunning: misc.ProcsRunning, + ProcsTotal: misc.ProcsTotal, + } +} + +// getProcessInfo 获取进程信息 +func (m *InfoMonitor) getProcessInfo() []ProcessInfo { + if !m.config.CollectProcess { + return nil + } + + processes, err := process.Processes() + if err != nil { + log.Printf("获取进程列表失败: %v", err) + return nil + } + + var procList []ProcessInfo + limit := m.config.ProcessLimit + if limit <= 0 { + limit = 10 // 默认显示10个进程 + } + + count := 0 + for _, p := range processes { + if count >= limit { + break + } + + name, err := p.Name() + if err != nil || name == "" || name == " " { + continue + } + + cmdline, _ := p.Cmdline() + memInfo, _ := p.MemoryInfo() + cpuPercent, _ := p.CPUPercent() + + var memMB float64 + if memInfo != nil { + memMB = float64(memInfo.RSS) / (1024 * 1024) + } + + procList = append(procList, ProcessInfo{ + PID: p.Pid, + Name: name, + Cmdline: cmdline, + MemoryMB: memMB, + CPUPercent: cpuPercent, + }) + + count++ + } + + return procList +} + +// getRuntimeInfo 获取运行时信息 +func (m *InfoMonitor) getRuntimeInfo() RuntimeInfo { + return RuntimeInfo{ + GoVersion: runtime.Version(), + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GOROOT: runtime.GOROOT(), + GOMAXPROCS: runtime.GOMAXPROCS(0), + NumCPU: runtime.NumCPU(), + NumGoroutine: runtime.NumGoroutine(), + } +} + +// getQuickMetrics 获取快速指标 +func (m *InfoMonitor) getQuickMetrics() QuickMetrics { + cpuPercent, _ := cpu.Percent(100*time.Millisecond, false) + memInfo, _ := mem.VirtualMemory() + rootUsage, _ := disk.Usage("/") + + quickCPU := 0.0 + if len(cpuPercent) > 0 { + quickCPU = cpuPercent[0] + } + + memPercent := 0.0 + availableGB := 0.0 + if memInfo != nil { + memPercent = memInfo.UsedPercent + availableGB = float64(memInfo.Available) / (1024 * 1024 * 1024) + } + + rootDiskPercent := 0.0 + if rootUsage != nil { + rootDiskPercent = rootUsage.UsedPercent + } + + return QuickMetrics{ + CPUPercent: quickCPU, + MemoryPercent: memPercent, + RootDiskPercent: rootDiskPercent, + AvailableMemoryGB: availableGB, + } +} + +// logMetrics 记录指标到日志文件 +func (m *InfoMonitor) logMetrics(metrics ServerMetrics) { + if m.logFile == nil { + return + } + + // 基本指标日志 + basicLog := fmt.Sprintf("[INFO-METRIC] %s | CPU:%.2f%% | MEM:%.2f%% | Load1:%.2f | DiskRoot:%.2f%%", + metrics.Timestamp.Format("2006-01-02 15:04:05"), + metrics.QuickMetrics.CPUPercent, + metrics.QuickMetrics.MemoryPercent, + metrics.Load.Load1, + metrics.QuickMetrics.RootDiskPercent, + ) + + // 详细指标日志 + detailedLog := fmt.Sprintf("\n[INFO-DETAIL] Host: %s, Uptime: %s, Cores: %d/%d, Mem: %.2f/%.2f GB", + metrics.Host.Hostname, + metrics.Host.Uptime, + metrics.CPU.Cores, + metrics.CPU.LogicalCores, + metrics.Memory.UsedGB, + metrics.Memory.TotalGB, + ) + + logLine := basicLog + detailedLog + "\n" + + if _, err := m.logFile.WriteString(logLine); err != nil { + log.Printf("写入日志文件失败: %v", err) + } + + // 确保数据写入磁盘 + m.logFile.Sync() +} + +// displayMetrics 显示指标到控制台 +func (m *InfoMonitor) displayMetrics(metrics ServerMetrics) { + // 使用不同颜色显示不同类型的指标 + fmt.Printf("\n\x1b[36m════════════════ 服务器监控指标 [%s] ════════════════\x1b[0m\n", + metrics.Timestamp.Format("15:04:05")) + + // 主机信息 + fmt.Printf("\n\x1b[33m主机信息:\x1b[0m\n") + fmt.Printf(" \x1b[32m主机名:\x1b[0m %s\n", metrics.Host.Hostname) + fmt.Printf(" \x1b[32m运行时间:\x1b[0m %s\n", metrics.Host.Uptime) + fmt.Printf(" \x1b[32m系统:\x1b[0m %s %s\n", metrics.Host.Platform, metrics.Host.PlatformVersion) + + // CPU信息 + fmt.Printf("\n\x1b[33mCPU信息:\x1b[0m\n") + fmt.Printf(" \x1b[32m型号:\x1b[0m %s\n", metrics.CPU.Model) + fmt.Printf(" \x1b[32m核心:\x1b[0m %d物理/%d逻辑\n", metrics.CPU.Cores, metrics.CPU.LogicalCores) + + // 根据CPU使用率显示不同颜色 + cpuColor := "\x1b[32m" // 绿色 + if metrics.CPU.UsagePercent > 70 { + cpuColor = "\x1b[33m" // 黄色 + } + if metrics.CPU.UsagePercent > 90 { + cpuColor = "\x1b[31m" // 红色 + } + fmt.Printf(" \x1b[32m使用率:\x1b[0m %s%.2f%%\x1b[0m", cpuColor, metrics.CPU.UsagePercent) + + if len(metrics.CPU.PerCorePercent) > 0 { + fmt.Printf(" (") + for i, p := range metrics.CPU.PerCorePercent { + if i > 0 { + fmt.Printf(" ") + } + coreColor := "\x1b[32m" + if p > 70 { + coreColor = "\x1b[33m" + } + if p > 90 { + coreColor = "\x1b[31m" + } + fmt.Printf("%s%d:%.0f%%\x1b[0m", coreColor, i, p) + } + fmt.Printf(")") + } + fmt.Println() + + // 内存信息 + fmt.Printf("\n\x1b[33m内存信息:\x1b[0m\n") + + // 根据内存使用率显示不同颜色 + memColor := "\x1b[32m" + if metrics.Memory.UsedPercent > 70 { + memColor = "\x1b[33m" + } + if metrics.Memory.UsedPercent > 90 { + memColor = "\x1b[31m" + } + + memBar := getProgressBar(metrics.Memory.UsedPercent, 20) + fmt.Printf(" \x1b[32m使用率:\x1b[0m %s%.1f%%\x1b[0m %s\n", + memColor, metrics.Memory.UsedPercent, memBar) + fmt.Printf(" \x1b[32m总量/已用/可用:\x1b[0m %.2f/%.2f/%.2f GB\n", + metrics.Memory.TotalGB, metrics.Memory.UsedGB, metrics.Memory.AvailableGB) + + if metrics.Memory.SwapTotalGB > 0 { + fmt.Printf(" \x1b[32m交换空间:\x1b[0m %.2f GB\n", metrics.Memory.SwapTotalGB) + } + + // 磁盘信息 + if len(metrics.Disk) > 0 { + fmt.Printf("\n\x1b[33m磁盘使用情况:\x1b[0m\n") + for _, disk := range metrics.Disk { + diskColor := "\x1b[32m" + if disk.UsedPercent > 70 { + diskColor = "\x1b[33m" + } + if disk.UsedPercent > 90 { + diskColor = "\x1b[31m" + } + + diskBar := getProgressBar(disk.UsedPercent, 15) + fmt.Printf(" \x1b[32m%s:\x1b[0m %s%.1f%%\x1b[0m %s %.2f/%.2f GB\n", + disk.Mountpoint, diskColor, disk.UsedPercent, diskBar, disk.UsedGB, disk.TotalGB) + } + } + + // 负载信息 + fmt.Printf("\n\x1b[33m系统负载:\x1b[0m\n") + load1Color := "\x1b[32m" + if metrics.Load.RelativeLoad1 > 1.0 { + load1Color = "\x1b[33m" + } + if metrics.Load.RelativeLoad1 > 2.0 { + load1Color = "\x1b[31m" + } + + fmt.Printf(" \x1b[32m1/5/15分钟:\x1b[0m %s%.2f\x1b[0m/%.2f/%.2f\n", + load1Color, metrics.Load.Load1, metrics.Load.Load5, metrics.Load.Load15) + fmt.Printf(" \x1b[32m相对负载:\x1b[0m %.2f/%.2f/%.2f\n", + metrics.Load.RelativeLoad1, metrics.Load.RelativeLoad5, metrics.Load.RelativeLoad15) + fmt.Printf(" \x1b[32m进程:\x1b[0m %d运行中 / %d总计\n", + metrics.Load.ProcsRunning, metrics.Load.ProcsTotal) + + // 网络信息(如果启用了) + if m.config.CollectNetwork && len(metrics.Network.Interfaces) > 0 { + fmt.Printf("\n\x1b[33m网络信息:\x1b[0m\n") + fmt.Printf(" \x1b[32mTCP连接:\x1b[0m %d (已建立: %d)\n", + metrics.Network.TCPConnections, metrics.Network.EstablishedConn) + fmt.Printf(" \x1b[32m流量:\x1b[0m 接收:%.2f MB 发送:%.2f MB\n", + metrics.Network.TotalRecvMB, metrics.Network.TotalSentMB) + } + + // 进程信息 + if len(metrics.Processes) > 0 { + fmt.Printf("\n\x1b[33mTOP进程 (按内存排序):\x1b[0m\n") + for i, proc := range metrics.Processes { + if i >= 5 { // 只显示前5个 + break + } + procColor := "\x1b[36m" + if proc.CPUPercent > 10 { + procColor = "\x1b[33m" + } + if proc.CPUPercent > 30 { + procColor = "\x1b[31m" + } + + // 截断过长的命令行 + cmdDisplay := proc.Cmdline + if len(cmdDisplay) > 50 { + cmdDisplay = cmdDisplay[:47] + "..." + } + + fmt.Printf(" %s%5d\x1b[0m %-20s %s%.1f%%\x1b[0m %.1fMB %s\n", + procColor, proc.PID, proc.Name, procColor, proc.CPUPercent, + proc.MemoryMB, cmdDisplay) + } + } + + fmt.Printf("\n\x1b[36m══════════════════════════════════════════════════════\x1b[0m\n") +} + +// getProgressBar 获取进度条 +func getProgressBar(percent float64, width int) string { + filled := int((percent / 100.0) * float64(width)) + if filled > width { + filled = width + } + + bar := "[" + for i := 0; i < width; i++ { + if i < filled { + // 根据填充量使用不同颜色 + if i < width/3 { + bar += "\x1b[32m=\x1b[0m" // 绿色 + } else if i < width*2/3 { + bar += "\x1b[33m=\x1b[0m" // 黄色 + } else { + bar += "\x1b[31m=\x1b[0m" // 红色 + } + } else { + bar += " " + } + } + bar += "]" + + return bar +} diff --git a/internal/network/client.go b/internal/network/client.go index 754a76b..d7661f4 100644 --- a/internal/network/client.go +++ b/internal/network/client.go @@ -1,141 +1,141 @@ -package network - -import ( - "encoding/json" - "log" - "sync" - "time" - - "github.com/gorilla/websocket" -) - -type ClientConfig struct { - ServerURL string - SendInterval time.Duration - BufferSize int -} - -type WSClient struct { - config ClientConfig - conn *websocket.Conn - sendChan chan Packet - mu sync.Mutex - isConnected bool - stopChan chan struct{} -} - -func NewWSClient(cfg ClientConfig) *WSClient { - if cfg.BufferSize == 0 { - cfg.BufferSize = 100 - } - return &WSClient{ - config: cfg, - sendChan: make(chan Packet, cfg.BufferSize), - stopChan: make(chan struct{}), - } -} - -func (c *WSClient) Start() { - go c.connectionLoop() - go c.sendLoop() -} - -func (c *WSClient) SendQueue(packet Packet) { - select { - case c.sendChan <- packet: - default: - log.Printf("[网络] 发送队列已满,丢弃消息: %s", packet.Type) - } -} - -func (c *WSClient) Stop() { - close(c.stopChan) - if c.conn != nil { - c.conn.Close() - } -} - -func (c *WSClient) sendLoop() { - for { - select { - case <-c.stopChan: - return - case packet := <-c.sendChan: - c.sendRaw(packet) - } - } -} - -func (c *WSClient) sendRaw(packet Packet) { - c.mu.Lock() - defer c.mu.Unlock() - if !c.isConnected || c.conn == nil { - log.Printf("[网络] 无连接,无法发送消息: %s", packet.Type) - return - } - - c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) - - payload, _ := json.Marshal(packet) - if err := c.conn.WriteMessage(websocket.TextMessage, payload); err != nil { - log.Printf("[网络] 发送消息失败: %v", err) - c.isConnected = false - } -} - -func (c *WSClient) connectionLoop() { - for { - select { - case <-c.stopChan: - return - default: - if !c.isConnected { - if err := c.connect(); err != nil { - log.Printf("[网络] 连接 %s 失败: %v. 5秒后重试...", c.config.ServerURL, err) - time.Sleep(5 * time.Second) - continue - } - } - - _, _, err := c.conn.ReadMessage() - if err != nil { - log.Printf("[网络] 连接断开: %v", err) - c.closeConn() - } - - // TODO: 处理服务器消息 - } - } -} - -func (c *WSClient) closeConn() { - c.mu.Lock() - defer c.mu.Unlock() - if c.conn != nil { - c.conn.Close() - c.conn = nil - } - c.isConnected = false -} - -func (c *WSClient) connect() error { - conn, _, err := websocket.DefaultDialer.Dial(c.config.ServerURL, nil) - if err != nil { - return err - } - - c.mu.Lock() - defer c.mu.Unlock() - - select { - case <-c.stopChan: - conn.Close() - return nil - default: - } - - c.conn = conn - c.isConnected = true - log.Printf("[网络] 成功连接到服务器: %s", c.config.ServerURL) - return nil -} +package network + +import ( + "encoding/json" + "log" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +type ClientConfig struct { + ServerURL string + SendInterval time.Duration + BufferSize int +} + +type WSClient struct { + config ClientConfig + conn *websocket.Conn + sendChan chan Packet + mu sync.Mutex + isConnected bool + stopChan chan struct{} +} + +func NewWSClient(cfg ClientConfig) *WSClient { + if cfg.BufferSize == 0 { + cfg.BufferSize = 100 + } + return &WSClient{ + config: cfg, + sendChan: make(chan Packet, cfg.BufferSize), + stopChan: make(chan struct{}), + } +} + +func (c *WSClient) Start() { + go c.connectionLoop() + go c.sendLoop() +} + +func (c *WSClient) SendQueue(packet Packet) { + select { + case c.sendChan <- packet: + default: + log.Printf("[网络] 发送队列已满,丢弃消息: %s", packet.Type) + } +} + +func (c *WSClient) Stop() { + close(c.stopChan) + if c.conn != nil { + c.conn.Close() + } +} + +func (c *WSClient) sendLoop() { + for { + select { + case <-c.stopChan: + return + case packet := <-c.sendChan: + c.sendRaw(packet) + } + } +} + +func (c *WSClient) sendRaw(packet Packet) { + c.mu.Lock() + defer c.mu.Unlock() + if !c.isConnected || c.conn == nil { + log.Printf("[网络] 无连接,无法发送消息: %s", packet.Type) + return + } + + c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) + + payload, _ := json.Marshal(packet) + if err := c.conn.WriteMessage(websocket.TextMessage, payload); err != nil { + log.Printf("[网络] 发送消息失败: %v", err) + c.isConnected = false + } +} + +func (c *WSClient) connectionLoop() { + for { + select { + case <-c.stopChan: + return + default: + if !c.isConnected { + if err := c.connect(); err != nil { + log.Printf("[网络] 连接 %s 失败: %v. 5秒后重试...", c.config.ServerURL, err) + time.Sleep(5 * time.Second) + continue + } + } + + _, _, err := c.conn.ReadMessage() + if err != nil { + log.Printf("[网络] 连接断开: %v", err) + c.closeConn() + } + + // TODO: 处理服务器消息 + } + } +} + +func (c *WSClient) closeConn() { + c.mu.Lock() + defer c.mu.Unlock() + if c.conn != nil { + c.conn.Close() + c.conn = nil + } + c.isConnected = false +} + +func (c *WSClient) connect() error { + conn, _, err := websocket.DefaultDialer.Dial(c.config.ServerURL, nil) + if err != nil { + return err + } + + c.mu.Lock() + defer c.mu.Unlock() + + select { + case <-c.stopChan: + conn.Close() + return nil + default: + } + + c.conn = conn + c.isConnected = true + log.Printf("[网络] 成功连接到服务器: %s", c.config.ServerURL) + return nil +} diff --git a/internal/network/loader.go b/internal/network/loader.go index fa03cbe..644e2c5 100644 --- a/internal/network/loader.go +++ b/internal/network/loader.go @@ -1,49 +1,49 @@ -package network - -import ( - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/wuko233/sysmonitord/internal/config" -) - -type ConfigLoader struct { - client *http.Client -} - -func NewConfigLoader() *ConfigLoader { - return &ConfigLoader{ - client: &http.Client{ - Timeout: 10 * time.Second, - }, - } -} - -func (l *ConfigLoader) LoadConfigs(urls ConfigUrls) (config.OfficialConfig, config.UserConfig, error) { - var official config.OfficialConfig - var user config.UserConfig - // 1. 下载官方配置 - if err := l.fetchJSON(urls.OfficialConfigUrl, &official); err != nil { - return official, user, fmt.Errorf("下载官方配置失败: %v", err) - } - // 2. 下载用户配置 - if err := l.fetchJSON(urls.UserConfigUrl, &user); err != nil { - return official, user, fmt.Errorf("下载用户配置失败: %v", err) - } - return official, user, nil -} - -func (l *ConfigLoader) fetchJSON(url string, target interface{}) error { - resp, err := l.client.Get(url) - if err != nil { - return fmt.Errorf("请求失败: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("非200响应: %d", resp.StatusCode) - } - return json.NewDecoder(resp.Body).Decode(target) -} +package network + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/wuko233/sysmonitord/internal/config" +) + +type ConfigLoader struct { + client *http.Client +} + +func NewConfigLoader() *ConfigLoader { + return &ConfigLoader{ + client: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +func (l *ConfigLoader) LoadConfigs(urls ConfigUrls) (config.OfficialConfig, config.UserConfig, error) { + var official config.OfficialConfig + var user config.UserConfig + // 1. 下载官方配置 + if err := l.fetchJSON(urls.OfficialConfigUrl, &official); err != nil { + return official, user, fmt.Errorf("下载官方配置失败: %v", err) + } + // 2. 下载用户配置 + if err := l.fetchJSON(urls.UserConfigUrl, &user); err != nil { + return official, user, fmt.Errorf("下载用户配置失败: %v", err) + } + return official, user, nil +} + +func (l *ConfigLoader) fetchJSON(url string, target interface{}) error { + resp, err := l.client.Get(url) + if err != nil { + return fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("非200响应: %d", resp.StatusCode) + } + return json.NewDecoder(resp.Body).Decode(target) +} diff --git a/internal/network/types.go b/internal/network/types.go index 987a605..dcc9bd7 100644 --- a/internal/network/types.go +++ b/internal/network/types.go @@ -1,22 +1,22 @@ -package network - -import "time" - -type Packet struct { - Type string `json:"type"` - Timestamp int64 `json:"timestamp"` - Payload interface{} `json:"payload"` -} - -func NewPacket(msgType string, payload interface{}) Packet { - return Packet{ - Type: msgType, - Timestamp: time.Now().Unix(), - Payload: payload, - } -} - -type ConfigUrls struct { - OfficialConfigUrl string - UserConfigUrl string -} +package network + +import "time" + +type Packet struct { + Type string `json:"type"` + Timestamp int64 `json:"timestamp"` + Payload interface{} `json:"payload"` +} + +func NewPacket(msgType string, payload interface{}) Packet { + return Packet{ + Type: msgType, + Timestamp: time.Now().Unix(), + Payload: payload, + } +} + +type ConfigUrls struct { + OfficialConfigUrl string + UserConfigUrl string +} diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index b68459d..04163d4 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -1,133 +1,133 @@ -package scanner - -import ( - "log" - "os" - "path/filepath" - "time" - - "github.com/shirou/gopsutil/v4/cpu" - "github.com/wuko233/sysmonitord/internal/network" - "github.com/wuko233/sysmonitord/internal/whitelist" -) - -type Scanner struct { - wlManager *whitelist.Manager - client *network.WSClient - cpuLimit float64 - scanPaths []string - stopChan chan struct{} -} - -func NewScanner(wl *whitelist.Manager, client *network.WSClient) *Scanner { - return &Scanner{ - wlManager: wl, - client: client, - cpuLimit: 50.0, - scanPaths: []string{"/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/tmp", "/home"}, - stopChan: make(chan struct{}), - } -} - -func (s *Scanner) Start() { - log.Println("[扫描器] 启动文件完整性扫描...") - go s.scanLoop() -} - -func (s *Scanner) scanLoop() { - ticker := time.NewTicker(10 * time.Minute) - defer ticker.Stop() - for { - select { - case <-s.stopChan: - return - case <-ticker.C: - s.performScan() - } - } -} - -func (s *Scanner) performScan() { - log.Println("[扫描器] 开始新一轮全盘扫描") - fileCount := 0 - - for _, root := range s.scanPaths { - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - - select { - case <-s.stopChan: - return filepath.SkipDir - default: - } - - if err != nil { - log.Printf("[扫描器] 访问错误: %v", err) - return nil - } - - fileCount++ - - if fileCount%100 == 0 { - s.checkCPUAndSleep() - } - - if info.IsDir() { - if s.wlManager.IsPathIgnored(path) { - return filepath.SkipDir - } - - return nil - } - - isWhitelisted, isHashMatch, err := s.wlManager.CheckFileStatus(path) - if err != nil { - log.Printf("[扫描器] 检查文件状态失败: %v", err) - return nil - } - - if !isWhitelisted { - log.Printf("[扫描器] 发现未在白名单文件: %s", path) - s.reportFile(path, "NON_WHITELISTED_FILE") - } else if !isHashMatch { - log.Printf("[扫描器] 警告!文件Hash不匹配(可能被篡改): %s", path) - s.reportFile(path, "FILE_HASH_MISMATCH") - } - - return nil - }) - if err != nil { - log.Printf("[扫描器] 扫描目录 %s 出错: %v", root, err) - } - } -} - -func (s *Scanner) checkCPUAndSleep() { - percent, err := cpu.Percent(200*time.Millisecond, false) - if err != nil || len(percent) == 0 { - log.Printf("[扫描器] 获取CPU使用率失败: %v", err) - return - } - - if percent[0] > s.cpuLimit { - log.Printf("[扫描器] CPU使用率过高 (%.2f%%),暂停扫描5秒", percent[0]) - time.Sleep(5 * time.Second) - } - - time.Sleep(10 * time.Millisecond) -} - -func (s *Scanner) reportFile(path string, alertType string) { - payload := map[string]interface{}{ - "filepath": path, - "status": "detected", - } - - packet := network.NewPacket(alertType, payload) - - s.client.SendQueue(packet) -} - -func (s *Scanner) Stop() { - log.Println("[扫描器] 停止文件完整性扫描...") - close(s.stopChan) -} +package scanner + +import ( + "log" + "os" + "path/filepath" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/wuko233/sysmonitord/internal/network" + "github.com/wuko233/sysmonitord/internal/whitelist" +) + +type Scanner struct { + wlManager *whitelist.Manager + client *network.WSClient + cpuLimit float64 + scanPaths []string + stopChan chan struct{} +} + +func NewScanner(wl *whitelist.Manager, client *network.WSClient) *Scanner { + return &Scanner{ + wlManager: wl, + client: client, + cpuLimit: 50.0, + scanPaths: []string{"/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/tmp", "/home"}, + stopChan: make(chan struct{}), + } +} + +func (s *Scanner) Start() { + log.Println("[扫描器] 启动文件完整性扫描...") + go s.scanLoop() +} + +func (s *Scanner) scanLoop() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + for { + select { + case <-s.stopChan: + return + case <-ticker.C: + s.performScan() + } + } +} + +func (s *Scanner) performScan() { + log.Println("[扫描器] 开始新一轮全盘扫描") + fileCount := 0 + + for _, root := range s.scanPaths { + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + + select { + case <-s.stopChan: + return filepath.SkipDir + default: + } + + if err != nil { + log.Printf("[扫描器] 访问错误: %v", err) + return nil + } + + fileCount++ + + if fileCount%100 == 0 { + s.checkCPUAndSleep() + } + + if info.IsDir() { + if s.wlManager.IsPathIgnored(path) { + return filepath.SkipDir + } + + return nil + } + + isWhitelisted, isHashMatch, err := s.wlManager.CheckFileStatus(path) + if err != nil { + log.Printf("[扫描器] 检查文件状态失败: %v", err) + return nil + } + + if !isWhitelisted { + log.Printf("[扫描器] 发现未在白名单文件: %s", path) + s.reportFile(path, "NON_WHITELISTED_FILE") + } else if !isHashMatch { + log.Printf("[扫描器] 警告!文件Hash不匹配(可能被篡改): %s", path) + s.reportFile(path, "FILE_HASH_MISMATCH") + } + + return nil + }) + if err != nil { + log.Printf("[扫描器] 扫描目录 %s 出错: %v", root, err) + } + } +} + +func (s *Scanner) checkCPUAndSleep() { + percent, err := cpu.Percent(200*time.Millisecond, false) + if err != nil || len(percent) == 0 { + log.Printf("[扫描器] 获取CPU使用率失败: %v", err) + return + } + + if percent[0] > s.cpuLimit { + log.Printf("[扫描器] CPU使用率过高 (%.2f%%),暂停扫描5秒", percent[0]) + time.Sleep(5 * time.Second) + } + + time.Sleep(10 * time.Millisecond) +} + +func (s *Scanner) reportFile(path string, alertType string) { + payload := map[string]interface{}{ + "filepath": path, + "status": "detected", + } + + packet := network.NewPacket(alertType, payload) + + s.client.SendQueue(packet) +} + +func (s *Scanner) Stop() { + log.Println("[扫描器] 停止文件完整性扫描...") + close(s.stopChan) +} diff --git a/internal/scanner/watcher.go b/internal/scanner/watcher.go index 6e2e5bf..1af3edd 100644 --- a/internal/scanner/watcher.go +++ b/internal/scanner/watcher.go @@ -1,119 +1,119 @@ -package scanner - -import ( - "log" - "os" - "time" - - "github.com/fsnotify/fsnotify" - "github.com/wuko233/sysmonitord/internal/network" - "github.com/wuko233/sysmonitord/internal/whitelist" -) - -type Watcher struct { - wlManager *whitelist.Manager - client *network.WSClient - watcher *fsnotify.Watcher - stopChan chan struct{} - watchPaths []string -} - -func NewWatcher(wl *whitelist.Manager, client *network.WSClient) (*Watcher, error) { - fsWatch, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - - return &Watcher{ - wlManager: wl, - client: client, - watcher: fsWatch, - stopChan: make(chan struct{}), - - // TODO: 当前仅实现对主目录的监控,后续实现递归监控子目录 - watchPaths: []string{ - "/bin", "/sbin", "/usr/bin", "/etc/init.d", "/tmp", - }, - }, nil -} - -func (w *Watcher) Start() { - log.Println("[监听器] 启动实时文件监控...") - - for _, path := range w.watchPaths { - if _, err := os.Stat(path); err == nil { - if err := w.watcher.Add(path); err != nil { - log.Printf("[监听器] 无法监控路径 %s: %v", path, err) - } else { - log.Printf("[监听器] 开始监控路径: %s", path) - } - } - } - - go w.eventLoop() -} - -func (w *Watcher) eventLoop() { - for { - select { - case <-w.stopChan: - return - case event, ok := <-w.watcher.Events: - if !ok { - return - } - - if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) { - if w.wlManager.IsPathIgnored(event.Name) { - continue - } - - go w.handleFileChange(event.Name, event.Op.String()) - } - case err, ok := <-w.watcher.Errors: - if !ok { - return - } - log.Printf("[监听器] 错误: %v", err) - } - } -} - -func (w *Watcher) handleFileChange(path string, op string) { - - time.Sleep(200 * time.Millisecond) // 等待文件写入完成 - - if _, err := os.Stat(path); os.IsNotExist(err) { - return - } - - isWhitelisted, isHashMatch, err := w.wlManager.CheckFileStatus(path) - if err != nil { - return - } - - if !isWhitelisted { - log.Printf("[监听器] 实时拦截:检测到非白名单文件变动 (%s): %s", op, path) - w.reportEvent(path, "REALTIME_FILE_ALERT", op) - } else if !isHashMatch { - log.Printf("[监听器] 实时拦截:检测到白名单文件被篡改 (%s): %s", op, path) - w.reportEvent(path, "REALTIME_HASH_MISMATCH", op) - } -} - -func (w *Watcher) reportEvent(path, alertType, op string) { - payload := map[string]interface{}{ - "filepath": path, - "operation": op, - "time": time.Now(), - } - - packet := network.NewPacket(alertType, payload) - w.client.SendQueue(packet) -} - -func (w *Watcher) Stop() { - log.Println("[监听器] 停止实时文件监控...") - close(w.stopChan) - w.watcher.Close() -} +package scanner + +import ( + "log" + "os" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/wuko233/sysmonitord/internal/network" + "github.com/wuko233/sysmonitord/internal/whitelist" +) + +type Watcher struct { + wlManager *whitelist.Manager + client *network.WSClient + watcher *fsnotify.Watcher + stopChan chan struct{} + watchPaths []string +} + +func NewWatcher(wl *whitelist.Manager, client *network.WSClient) (*Watcher, error) { + fsWatch, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + + return &Watcher{ + wlManager: wl, + client: client, + watcher: fsWatch, + stopChan: make(chan struct{}), + + // TODO: 当前仅实现对主目录的监控,后续实现递归监控子目录 + watchPaths: []string{ + "/bin", "/sbin", "/usr/bin", "/etc/init.d", "/tmp", + }, + }, nil +} + +func (w *Watcher) Start() { + log.Println("[监听器] 启动实时文件监控...") + + for _, path := range w.watchPaths { + if _, err := os.Stat(path); err == nil { + if err := w.watcher.Add(path); err != nil { + log.Printf("[监听器] 无法监控路径 %s: %v", path, err) + } else { + log.Printf("[监听器] 开始监控路径: %s", path) + } + } + } + + go w.eventLoop() +} + +func (w *Watcher) eventLoop() { + for { + select { + case <-w.stopChan: + return + case event, ok := <-w.watcher.Events: + if !ok { + return + } + + if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) { + if w.wlManager.IsPathIgnored(event.Name) { + continue + } + + go w.handleFileChange(event.Name, event.Op.String()) + } + case err, ok := <-w.watcher.Errors: + if !ok { + return + } + log.Printf("[监听器] 错误: %v", err) + } + } +} + +func (w *Watcher) handleFileChange(path string, op string) { + + time.Sleep(200 * time.Millisecond) // 等待文件写入完成 + + if _, err := os.Stat(path); os.IsNotExist(err) { + return + } + + isWhitelisted, isHashMatch, err := w.wlManager.CheckFileStatus(path) + if err != nil { + return + } + + if !isWhitelisted { + log.Printf("[监听器] 实时拦截:检测到非白名单文件变动 (%s): %s", op, path) + w.reportEvent(path, "REALTIME_FILE_ALERT", op) + } else if !isHashMatch { + log.Printf("[监听器] 实时拦截:检测到白名单文件被篡改 (%s): %s", op, path) + w.reportEvent(path, "REALTIME_HASH_MISMATCH", op) + } +} + +func (w *Watcher) reportEvent(path, alertType, op string) { + payload := map[string]interface{}{ + "filepath": path, + "operation": op, + "time": time.Now(), + } + + packet := network.NewPacket(alertType, payload) + w.client.SendQueue(packet) +} + +func (w *Watcher) Stop() { + log.Println("[监听器] 停止实时文件监控...") + close(w.stopChan) + w.watcher.Close() +} diff --git a/internal/whitelist/manager.go b/internal/whitelist/manager.go index a205f2b..a4af5b1 100644 --- a/internal/whitelist/manager.go +++ b/internal/whitelist/manager.go @@ -1,135 +1,135 @@ -package whitelist - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "sync" - - "github.com/wuko233/sysmonitord/internal/config" -) - -type Manager struct { - mu sync.RWMutex - official config.OfficialConfig - user config.UserConfig - mergedIgnore []string -} - -func NewManager() *Manager { - return &Manager{ - official: config.OfficialConfig{ - WhitelistFiles: make(map[string][]string), - }, - user: config.UserConfig{ - SupplementFiles: make(map[string][]string), - SupplementProcesses: make(map[string]string), - }, - } -} - -func (m *Manager) UpdateConfig(official config.OfficialConfig, user config.UserConfig) { - m.mu.Lock() - defer m.mu.Unlock() - - m.official = official - m.user = user - - m.mergedIgnore = append([]string{}, m.official.IgnoredPaths...) - m.mergedIgnore = append(m.mergedIgnore, m.user.IgnoredPaths...) -} - -func (m *Manager) IsPathIgnored(path string) bool { - m.mu.RLock() - defer m.mu.RUnlock() - - return m.IsPathIgnoredUnsafe(path) -} - -func (m *Manager) IsPathIgnoredUnsafe(path string) bool { - path = filepath.Clean(path) - for _, ignore := range m.mergedIgnore { - if strings.HasPrefix(path, filepath.Clean(ignore)) { - return true - } - } - return false -} - -// CheckFileStatus 检查文件状态 -// 返回: isWhitelisted(是否在白名单), isValid(Hash是否匹配), err -func (m *Manager) CheckFileStatus(path string) (bool, bool, error) { - - m.mu.RLock() - defer m.mu.RUnlock() - - if m.IsPathIgnoredUnsafe((path)) { - return true, true, nil - } - - hashes, exists := m.official.WhitelistFiles[path] - if !exists { - hashes, exists = m.user.SupplementFiles[path] - } - if !exists { - return false, false, nil - } - - fileHash, err := CalculateFileHash(path) - if err != nil { - return true, false, fmt.Errorf("计算文件哈希失败: %v", err) - } - - for _, h := range hashes { - if strings.EqualFold(h, fmt.Sprintf("%v", fileHash)) { - return true, true, nil - } - } - - return true, false, nil -} - -func CalculateFileHash(filePath string) (string, error) { - file, err := os.Open(filePath) - - if err != nil { - return "", err - } - - defer file.Close() - - hash := sha256.New() - if _, err := io.Copy(hash, file); err != nil { - return "", err - } - - return hex.EncodeToString(hash.Sum(nil)), nil -} - -func (m *Manager) IsProcessAllowed(procName string, cmdLine string) bool { - m.mu.RLock() - defer m.mu.RUnlock() - - for _, p := range m.official.WhitelistProcesses { - if p == procName { - return true - } - } - - if _, ok := m.user.SupplementProcesses[procName]; ok { - return true - } - - return false -} - -func (m *Manager) GetAuditServerUrl() string { - m.mu.RLock() - defer m.mu.RUnlock() - - return m.user.AuditServerUrl -} +package whitelist + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/wuko233/sysmonitord/internal/config" +) + +type Manager struct { + mu sync.RWMutex + official config.OfficialConfig + user config.UserConfig + mergedIgnore []string +} + +func NewManager() *Manager { + return &Manager{ + official: config.OfficialConfig{ + WhitelistFiles: make(map[string][]string), + }, + user: config.UserConfig{ + SupplementFiles: make(map[string][]string), + SupplementProcesses: make(map[string]string), + }, + } +} + +func (m *Manager) UpdateConfig(official config.OfficialConfig, user config.UserConfig) { + m.mu.Lock() + defer m.mu.Unlock() + + m.official = official + m.user = user + + m.mergedIgnore = append([]string{}, m.official.IgnoredPaths...) + m.mergedIgnore = append(m.mergedIgnore, m.user.IgnoredPaths...) +} + +func (m *Manager) IsPathIgnored(path string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.IsPathIgnoredUnsafe(path) +} + +func (m *Manager) IsPathIgnoredUnsafe(path string) bool { + path = filepath.Clean(path) + for _, ignore := range m.mergedIgnore { + if strings.HasPrefix(path, filepath.Clean(ignore)) { + return true + } + } + return false +} + +// CheckFileStatus 检查文件状态 +// 返回: isWhitelisted(是否在白名单), isValid(Hash是否匹配), err +func (m *Manager) CheckFileStatus(path string) (bool, bool, error) { + + m.mu.RLock() + defer m.mu.RUnlock() + + if m.IsPathIgnoredUnsafe((path)) { + return true, true, nil + } + + hashes, exists := m.official.WhitelistFiles[path] + if !exists { + hashes, exists = m.user.SupplementFiles[path] + } + if !exists { + return false, false, nil + } + + fileHash, err := CalculateFileHash(path) + if err != nil { + return true, false, fmt.Errorf("计算文件哈希失败: %v", err) + } + + for _, h := range hashes { + if strings.EqualFold(h, fmt.Sprintf("%v", fileHash)) { + return true, true, nil + } + } + + return true, false, nil +} + +func CalculateFileHash(filePath string) (string, error) { + file, err := os.Open(filePath) + + if err != nil { + return "", err + } + + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "", err + } + + return hex.EncodeToString(hash.Sum(nil)), nil +} + +func (m *Manager) IsProcessAllowed(procName string, cmdLine string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + + for _, p := range m.official.WhitelistProcesses { + if p == procName { + return true + } + } + + if _, ok := m.user.SupplementProcesses[procName]; ok { + return true + } + + return false +} + +func (m *Manager) GetAuditServerUrl() string { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.user.AuditServerUrl +}