fix: 规范开发文档,规范配置文件

This commit is contained in:
wuko233 2026-03-22 12:15:59 +08:00
parent 1dcb60fa14
commit bf45cd54a2
9 changed files with 1806 additions and 1780 deletions

View File

@ -1,277 +1,277 @@
# 消息交互 # 消息交互
## 数据包格式 ## 数据包格式
### 通用数据包结构 ### 通用数据包结构
```json ```json
{ {
"type": "消息类型", "type": "消息类型",
"timestamp": 1612345678901, "timestamp": 1612345678901,
"payload": { "payload": {
// 根据消息类型的具体数据结构 // 根据消息类型的具体数据结构
} }
} }
``` ```
### 数据类型定义 ### 数据类型定义
```go ```go
type Packet struct { type Packet struct {
Type string `json:"type"` // 消息类型 Type string `json:"type"` // 消息类型
Timestamp int64 `json:"timestamp"` // Unix时间戳 Timestamp int64 `json:"timestamp"` // Unix时间戳
Payload interface{} `json:"payload"` // 消息载荷 Payload interface{} `json:"payload"` // 消息载荷
} }
``` ```
## 消息类型及数据结构 ## 消息类型及数据结构
### 1. 系统状态更新 (`STATUS_UPDATE`) ### 1. 系统状态更新 (`STATUS_UPDATE`)
**描述**: 定期发送的系统性能指标 **描述**: 定期发送的系统性能指标
**推送频率**: 每30秒一次 **推送频率**: 每30秒一次
**Payload 结构**: `ServerMetrics` **Payload 结构**: `ServerMetrics`
```json ```json
{ {
"timestamp": "2024-01-15T10:30:00Z", "timestamp": "2024-01-15T10:30:00Z",
"cpu": { "cpu": {
"model": "Intel(R) Xeon(R) CPU E5-2680 v4", "model": "Intel(R) Xeon(R) CPU E5-2680 v4",
"cores": 14, "cores": 14,
"logical_cores": 28, "logical_cores": 28,
"usage_percent": 45.67, "usage_percent": 45.67,
"per_core_percent": [23.4, 45.6, 12.3, ...], "per_core_percent": [23.4, 45.6, 12.3, ...],
"mhz": 2400.5, "mhz": 2400.5,
"cache_size": 35840 "cache_size": 35840
}, },
"memory": { "memory": {
"total_gb": 128.0, "total_gb": 128.0,
"used_gb": 64.5, "used_gb": 64.5,
"available_gb": 63.5, "available_gb": 63.5,
"used_percent": 50.4, "used_percent": 50.4,
"swap_total_gb": 16.0, "swap_total_gb": 16.0,
"swap_used_gb": 2.3 "swap_used_gb": 2.3
}, },
"disk": [ "disk": [
{ {
"mountpoint": "/", "mountpoint": "/",
"device": "/dev/sda1", "device": "/dev/sda1",
"fstype": "ext4", "fstype": "ext4",
"total_gb": 500.0, "total_gb": 500.0,
"used_gb": 250.0, "used_gb": 250.0,
"free_gb": 250.0, "free_gb": 250.0,
"used_percent": 50.0, "used_percent": 50.0,
"inodes_percent": 12.3 "inodes_percent": 12.3
} }
], ],
"network": { "network": {
"interfaces": [ "interfaces": [
{ {
"name": "eth0", "name": "eth0",
"hardware_addr": "00:11:22:33:44:55", "hardware_addr": "00:11:22:33:44:55",
"ip_addresses": ["192.168.1.100", "fe80::211:22ff:fe33:4455"] "ip_addresses": ["192.168.1.100", "fe80::211:22ff:fe33:4455"]
} }
], ],
"total_recv_mb": 1234.56, "total_recv_mb": 1234.56,
"total_sent_mb": 987.65, "total_sent_mb": 987.65,
"tcp_connections": 245, "tcp_connections": 245,
"established_conn": 128 "established_conn": 128
}, },
"load": { "load": {
"load_1": 2.34, "load_1": 2.34,
"load_5": 2.12, "load_5": 2.12,
"load_15": 1.89, "load_15": 1.89,
"relative_load_1": 0.83, "relative_load_1": 0.83,
"relative_load_5": 0.76, "relative_load_5": 0.76,
"relative_load_15": 0.68, "relative_load_15": 0.68,
"procs_running": 132, "procs_running": 132,
"procs_total": 456 "procs_total": 456
}, },
"processes": [ "processes": [
{ {
"pid": 1234, "pid": 1234,
"name": "nginx", "name": "nginx",
"cmdline": "nginx: master process", "cmdline": "nginx: master process",
"memory_mb": 125.6, "memory_mb": 125.6,
"cpu_percent": 12.3 "cpu_percent": 12.3
} }
], ],
"host": { "host": {
"hostname": "server01", "hostname": "server01",
"os": "linux", "os": "linux",
"platform": "ubuntu", "platform": "ubuntu",
"platform_version": "20.04", "platform_version": "20.04",
"kernel_version": "5.4.0-42-generic", "kernel_version": "5.4.0-42-generic",
"boot_time": "2024-01-15T08:00:00Z", "boot_time": "2024-01-15T08:00:00Z",
"uptime": "2小时30分钟45秒", "uptime": "2小时30分钟45秒",
"cpu_count": 28, "cpu_count": 28,
"architecture": "x86_64", "architecture": "x86_64",
"host_id": "abcdef12-3456-7890-abcd-ef1234567890" "host_id": "abcdef12-3456-7890-abcd-ef1234567890"
}, },
"runtime": { "runtime": {
"go_version": "go1.21.0", "go_version": "go1.21.0",
"goos": "linux", "goos": "linux",
"goarch": "amd64", "goarch": "amd64",
"goroot": "/usr/local/go", "goroot": "/usr/local/go",
"gomaxprocs": 28, "gomaxprocs": 28,
"num_cpu": 28, "num_cpu": 28,
"num_goroutine": 42 "num_goroutine": 42
}, },
"quick_metrics": { "quick_metrics": {
"cpu_percent": 45.67, "cpu_percent": 45.67,
"memory_percent": 50.4, "memory_percent": 50.4,
"root_disk_percent": 50.0, "root_disk_percent": 50.0,
"available_memory_gb": 63.5 "available_memory_gb": 63.5
} }
} }
``` ```
### 2. SSH登录告警 (`SSH_ALERT`) ### 2. SSH登录告警 (`SSH_ALERT`)
**描述**: SSH登录安全告警特别是root登录 **描述**: SSH登录安全告警特别是root登录
**触发条件**: SSH登录事件当检测到root登录时触发HIGH级别告警 **触发条件**: SSH登录事件当检测到root登录时触发HIGH级别告警
**Payload 结构**: `Alert` **Payload 结构**: `Alert`
```json ```json
{ {
"type": "SSH_ROOT_LOGIN", "type": "SSH_ROOT_LOGIN",
"level": "HIGH", "level": "HIGH",
"message": "检测到来自192.168.1.50的root登录", "message": "检测到来自192.168.1.50的root登录",
"timestamp": "2024-01-15T10:31:15Z", "timestamp": "2024-01-15T10:31:15Z",
"data": { "data": {
"timestamp": "2024-01-15T10:31:15Z", "timestamp": "2024-01-15T10:31:15Z",
"hostname": "server01", "hostname": "server01",
"username": "root", "username": "root",
"method": "publickey", "method": "publickey",
"source_ip": "192.168.1.50", "source_ip": "192.168.1.50",
"port": "22", "port": "22",
"service": "sshd", "service": "sshd",
"pid": "12345", "pid": "12345",
"message": "Accepted publickey for root from 192.168.1.50 port 22" "message": "Accepted publickey for root from 192.168.1.50 port 22"
} }
} }
``` ```
### 3. 文件完整性告警 ### 3. 文件完整性告警
#### 3.1 非白名单文件告警 (`NON_WHITELISTED_FILE`) #### 3.1 非白名单文件告警 (`NON_WHITELISTED_FILE`)
**描述**: 扫描发现不在白名单中的文件 **描述**: 扫描发现不在白名单中的文件
**触发条件**: 定期扫描中发现未在白名单中注册的文件 **触发条件**: 定期扫描中发现未在白名单中注册的文件
**Payload 结构**: **Payload 结构**:
```json ```json
{ {
"type": "NON_WHITELISTED_FILE", "type": "NON_WHITELISTED_FILE",
"timestamp": 1612345678901, "timestamp": 1612345678901,
"payload": { "payload": {
"filepath": "/tmp/suspicious_file.bin", "filepath": "/tmp/suspicious_file.bin",
"status": "detected" "status": "detected"
} }
} }
``` ```
#### 3.2 文件Hash不匹配告警 (`FILE_HASH_MISMATCH`) #### 3.2 文件Hash不匹配告警 (`FILE_HASH_MISMATCH`)
**描述**: 白名单文件被篡改Hash值不匹配 **描述**: 白名单文件被篡改Hash值不匹配
**触发条件**: 文件hash与白名单记录不符 **触发条件**: 文件hash与白名单记录不符
**Payload 结构**: **Payload 结构**:
```json ```json
{ {
"type": "FILE_HASH_MISMATCH", "type": "FILE_HASH_MISMATCH",
"timestamp": 1612345678901, "timestamp": 1612345678901,
"payload": { "payload": {
"filepath": "/usr/bin/ls", "filepath": "/usr/bin/ls",
"status": "detected" "status": "detected"
} }
} }
``` ```
### 4. 实时文件监控告警 ### 4. 实时文件监控告警
#### 4.1 实时文件变动告警 (`REALTIME_FILE_ALERT`) #### 4.1 实时文件变动告警 (`REALTIME_FILE_ALERT`)
**描述**: 监控目录中检测到非白名单文件的创建或修改 **描述**: 监控目录中检测到非白名单文件的创建或修改
**触发条件**: 使用fsnotify监控到文件系统事件 **触发条件**: 使用fsnotify监控到文件系统事件
**Payload 结构**: **Payload 结构**:
```json ```json
{ {
"type": "REALTIME_FILE_ALERT", "type": "REALTIME_FILE_ALERT",
"timestamp": 1612345678901, "timestamp": 1612345678901,
"payload": { "payload": {
"filepath": "/tmp/new_suspicious_file", "filepath": "/tmp/new_suspicious_file",
"operation": "CREATE", "operation": "CREATE",
"time": "2024-01-15T10:32:00Z" "time": "2024-01-15T10:32:00Z"
} }
} }
``` ```
#### 4.2 实时Hash不匹配告警 (`REALTIME_HASH_MISMATCH`) #### 4.2 实时Hash不匹配告警 (`REALTIME_HASH_MISMATCH`)
**描述**: 监控到白名单文件被实时篡改 **描述**: 监控到白名单文件被实时篡改
**Payload 结构**: **Payload 结构**:
```json ```json
{ {
"type": "REALTIME_HASH_MISMATCH", "type": "REALTIME_HASH_MISMATCH",
"timestamp": 1612345678901, "timestamp": 1612345678901,
"payload": { "payload": {
"filepath": "/etc/passwd", "filepath": "/etc/passwd",
"operation": "WRITE", "operation": "WRITE",
"time": "2024-01-15T10:33:00Z" "time": "2024-01-15T10:33:00Z"
} }
} }
``` ```
## 配置接口 ## 配置接口
### 1. 配置下载接口 ### 1. 配置下载接口
Agent 启动时会通过 HTTP 下载两份配置: Agent 启动时会通过 HTTP 下载两份配置:
#### 官方配置 (GET) #### 官方配置 (GET)
- **URL**: `http://localhost:8090/api/v1/configs/official.json` - **URL**: `http://localhost:8090/api/v1/configs/official.json`
- **响应格式**: 符合 `OfficialConfig` 结构 - **响应格式**: 符合 `OfficialConfig` 结构
#### 用户配置 (GET) #### 用户配置 (GET)
- **URL**: `http://localhost:8090/api/v1/configs/user.json` - **URL**: `http://localhost:8090/api/v1/configs/user.json`
- **响应格式**: 符合 `UserConfig` 结构 - **响应格式**: 符合 `UserConfig` 结构
### 2. 配置数据结构 ### 2. 配置数据结构
#### OfficialConfig #### OfficialConfig
```json ```json
{ {
"whitelist_files": { "whitelist_files": {
"/usr/bin/ls": ["hash1", "hash2"], "/usr/bin/ls": ["hash1", "hash2"],
"/bin/bash": ["hash3"] "/bin/bash": ["hash3"]
}, },
"whitelist_processes": ["sshd", "nginx", "docker"], "whitelist_processes": ["sshd", "nginx", "docker"],
"ignored_paths": ["/proc", "/sys", "/dev"] "ignored_paths": ["/proc", "/sys", "/dev"]
} }
``` ```
#### UserConfig #### UserConfig
```json ```json
{ {
"audit_server_url": "ws://audit.example.com:8090/api/v1/ws", "audit_server_url": "ws://audit.example.com:8090/api/v1/ws",
"supplement_files": { "supplement_files": {
"/opt/myapp/bin/app": ["user_hash1"] "/opt/myapp/bin/app": ["user_hash1"]
}, },
"supplement_processes": { "supplement_processes": {
"myapp": "/opt/myapp/bin/app start", "myapp": "/opt/myapp/bin/app start",
"custom_service": "" "custom_service": ""
}, },
"ignored_paths": ["/mnt/temp"], "ignored_paths": ["/mnt/temp"],
"check_perm_paths": ["/etc/sudoers", "/etc/shadow"], "check_perm_paths": ["/etc/sudoers", "/etc/shadow"],
"email_config": { "email_config": {
"imap_server": "imap.example.com", "imap_server": "imap.example.com",
"emergency_mail": ["admin@example.com", "security@example.com"] "emergency_mail": ["admin@example.com", "security@example.com"]
} }
} }
``` ```

View File

@ -1,46 +1,72 @@
package config package config
import "time" type ModelSwitches struct {
FileScanner bool `json:"file_scanner"`
type Configuration struct { FileWatcher bool `json:"file_watcher"`
Local Localconfig // 本地配置 SSHMonitor bool `json:"ssh_monitor"`
Offical OfficialConfig // 官方配置 SystemMonitor bool `json:"system_monitor"`
User UserConfig // 用户自定义配置
} }
type Localconfig struct { type SSHMonitorConfig struct {
LogPath string `yaml:"log_path"` Enabled bool `json:"enabled"`
CheckInterval time.Duration `yaml:"check_interval"` AlertOnRootLogin bool `json:"alert_on_root_login"`
ServerUrl string `yaml:"server_url"` }
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 { type OfficialConfig struct {
WhitelistFiles map[string][]string `yaml:"whitelist_files"` Version string `json:"version"`
WhitelistProcesses []string `yaml:"whitelist_processes"` WhiteListFiles map[string]string `json:"white_list_files"`
IgnoredPaths []string `yaml:"ignored_paths"` WhiteListProcesses []string `json:"white_list_processes"`
IgnoredPaths []string `json:"ignored_paths"`
ScanPaths []string `json:"scan_paths"`
} }
type UserConfig struct { type UserConfig struct {
AuditServerUrl string `json:"audit_server_url"` // 审计服务器地址 Version string `json:"version"`
// 用户补充的白名单文件 Connection ConnectionConfig `json:"connection"`
SupplementFiles map[string][]string `json:"supplement_files"` Models ModelSwitches `json:"models"`
// 用户补充的进程列表 SupplementFiles map[string]string `json:"supplement_files"`
// Key: 进程名, Value: 启动指令(如果为空则仅作为白名单,如果不为空则需保活) SupplementProcesses []string `json:"supplement_processes"`
SupplementProcesses map[string]string `json:"supplement_processes"` MonitorConfig MonitorConfig `json:"monitor_config"`
IgnoredPaths []string `json:"ignored_paths"`
CheckPermPaths []string `json:"check_perm_paths"` // 检查权限的目录
// 邮件配置
EmailConfig EmailConfig `json:"email_config"`
} }
type EmailConfig struct { type Configuration struct {
ImapServer string `json:"imap_server"` Official OfficialConfig // 官方配置
EmergencyMail []string `json:"emergency_mail"` User UserConfig // 用户自定义配置
} }
type SSHMonitor struct { func NewDefaultUserConfig() UserConfig {
Enabled bool `yaml:"enabled"` return UserConfig{
DisplayOnShell bool `yaml:"display_on_shell"` Version: "BuildInDefault",
AlertOnRootLogin bool `yaml:"alert_on_root_login"` 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},
},
}
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,141 +1,141 @@
package network package network
import ( import (
"encoding/json" "encoding/json"
"log" "log"
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
) )
type ClientConfig struct { type ClientConfig struct {
ServerURL string ServerURL string
SendInterval time.Duration SendInterval time.Duration
BufferSize int BufferSize int
} }
type WSClient struct { type WSClient struct {
config ClientConfig config ClientConfig
conn *websocket.Conn conn *websocket.Conn
sendChan chan Packet sendChan chan Packet
mu sync.Mutex mu sync.Mutex
isConnected bool isConnected bool
stopChan chan struct{} stopChan chan struct{}
} }
func NewWSClient(cfg ClientConfig) *WSClient { func NewWSClient(cfg ClientConfig) *WSClient {
if cfg.BufferSize == 0 { if cfg.BufferSize == 0 {
cfg.BufferSize = 100 cfg.BufferSize = 100
} }
return &WSClient{ return &WSClient{
config: cfg, config: cfg,
sendChan: make(chan Packet, cfg.BufferSize), sendChan: make(chan Packet, cfg.BufferSize),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
} }
func (c *WSClient) Start() { func (c *WSClient) Start() {
go c.connectionLoop() go c.connectionLoop()
go c.sendLoop() go c.sendLoop()
} }
func (c *WSClient) SendQueue(packet Packet) { func (c *WSClient) SendQueue(packet Packet) {
select { select {
case c.sendChan <- packet: case c.sendChan <- packet:
default: default:
log.Printf("[网络] 发送队列已满,丢弃消息: %s", packet.Type) log.Printf("[网络] 发送队列已满,丢弃消息: %s", packet.Type)
} }
} }
func (c *WSClient) Stop() { func (c *WSClient) Stop() {
close(c.stopChan) close(c.stopChan)
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
} }
} }
func (c *WSClient) sendLoop() { func (c *WSClient) sendLoop() {
for { for {
select { select {
case <-c.stopChan: case <-c.stopChan:
return return
case packet := <-c.sendChan: case packet := <-c.sendChan:
c.sendRaw(packet) c.sendRaw(packet)
} }
} }
} }
func (c *WSClient) sendRaw(packet Packet) { func (c *WSClient) sendRaw(packet Packet) {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if !c.isConnected || c.conn == nil { if !c.isConnected || c.conn == nil {
log.Printf("[网络] 无连接,无法发送消息: %s", packet.Type) log.Printf("[网络] 无连接,无法发送消息: %s", packet.Type)
return return
} }
c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)) c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
payload, _ := json.Marshal(packet) payload, _ := json.Marshal(packet)
if err := c.conn.WriteMessage(websocket.TextMessage, payload); err != nil { if err := c.conn.WriteMessage(websocket.TextMessage, payload); err != nil {
log.Printf("[网络] 发送消息失败: %v", err) log.Printf("[网络] 发送消息失败: %v", err)
c.isConnected = false c.isConnected = false
} }
} }
func (c *WSClient) connectionLoop() { func (c *WSClient) connectionLoop() {
for { for {
select { select {
case <-c.stopChan: case <-c.stopChan:
return return
default: default:
if !c.isConnected { if !c.isConnected {
if err := c.connect(); err != nil { if err := c.connect(); err != nil {
log.Printf("[网络] 连接 %s 失败: %v. 5秒后重试...", c.config.ServerURL, err) log.Printf("[网络] 连接 %s 失败: %v. 5秒后重试...", c.config.ServerURL, err)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
continue continue
} }
} }
_, _, err := c.conn.ReadMessage() _, _, err := c.conn.ReadMessage()
if err != nil { if err != nil {
log.Printf("[网络] 连接断开: %v", err) log.Printf("[网络] 连接断开: %v", err)
c.closeConn() c.closeConn()
} }
// TODO: 处理服务器消息 // TODO: 处理服务器消息
} }
} }
} }
func (c *WSClient) closeConn() { func (c *WSClient) closeConn() {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
c.conn = nil c.conn = nil
} }
c.isConnected = false c.isConnected = false
} }
func (c *WSClient) connect() error { func (c *WSClient) connect() error {
conn, _, err := websocket.DefaultDialer.Dial(c.config.ServerURL, nil) conn, _, err := websocket.DefaultDialer.Dial(c.config.ServerURL, nil)
if err != nil { if err != nil {
return err return err
} }
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
select { select {
case <-c.stopChan: case <-c.stopChan:
conn.Close() conn.Close()
return nil return nil
default: default:
} }
c.conn = conn c.conn = conn
c.isConnected = true c.isConnected = true
log.Printf("[网络] 成功连接到服务器: %s", c.config.ServerURL) log.Printf("[网络] 成功连接到服务器: %s", c.config.ServerURL)
return nil return nil
} }

View File

@ -1,49 +1,49 @@
package network package network
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"github.com/wuko233/sysmonitord/internal/config" "github.com/wuko233/sysmonitord/internal/config"
) )
type ConfigLoader struct { type ConfigLoader struct {
client *http.Client client *http.Client
} }
func NewConfigLoader() *ConfigLoader { func NewConfigLoader() *ConfigLoader {
return &ConfigLoader{ return &ConfigLoader{
client: &http.Client{ client: &http.Client{
Timeout: 10 * time.Second, Timeout: 10 * time.Second,
}, },
} }
} }
func (l *ConfigLoader) LoadConfigs(urls ConfigUrls) (config.OfficialConfig, config.UserConfig, error) { func (l *ConfigLoader) LoadConfigs(urls ConfigUrls) (config.OfficialConfig, config.UserConfig, error) {
var official config.OfficialConfig var official config.OfficialConfig
var user config.UserConfig var user config.UserConfig
// 1. 下载官方配置 // 1. 下载官方配置
if err := l.fetchJSON(urls.OfficialConfigUrl, &official); err != nil { if err := l.fetchJSON(urls.OfficialConfigUrl, &official); err != nil {
return official, user, fmt.Errorf("下载官方配置失败: %v", err) return official, user, fmt.Errorf("下载官方配置失败: %v", err)
} }
// 2. 下载用户配置 // 2. 下载用户配置
if err := l.fetchJSON(urls.UserConfigUrl, &user); err != nil { if err := l.fetchJSON(urls.UserConfigUrl, &user); err != nil {
return official, user, fmt.Errorf("下载用户配置失败: %v", err) return official, user, fmt.Errorf("下载用户配置失败: %v", err)
} }
return official, user, nil return official, user, nil
} }
func (l *ConfigLoader) fetchJSON(url string, target interface{}) error { func (l *ConfigLoader) fetchJSON(url string, target interface{}) error {
resp, err := l.client.Get(url) resp, err := l.client.Get(url)
if err != nil { if err != nil {
return fmt.Errorf("请求失败: %v", err) return fmt.Errorf("请求失败: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("非200响应: %d", resp.StatusCode) return fmt.Errorf("非200响应: %d", resp.StatusCode)
} }
return json.NewDecoder(resp.Body).Decode(target) return json.NewDecoder(resp.Body).Decode(target)
} }

View File

@ -1,22 +1,22 @@
package network package network
import "time" import "time"
type Packet struct { type Packet struct {
Type string `json:"type"` Type string `json:"type"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
Payload interface{} `json:"payload"` Payload interface{} `json:"payload"`
} }
func NewPacket(msgType string, payload interface{}) Packet { func NewPacket(msgType string, payload interface{}) Packet {
return Packet{ return Packet{
Type: msgType, Type: msgType,
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
Payload: payload, Payload: payload,
} }
} }
type ConfigUrls struct { type ConfigUrls struct {
OfficialConfigUrl string OfficialConfigUrl string
UserConfigUrl string UserConfigUrl string
} }

View File

@ -1,133 +1,133 @@
package scanner package scanner
import ( import (
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/cpu"
"github.com/wuko233/sysmonitord/internal/network" "github.com/wuko233/sysmonitord/internal/network"
"github.com/wuko233/sysmonitord/internal/whitelist" "github.com/wuko233/sysmonitord/internal/whitelist"
) )
type Scanner struct { type Scanner struct {
wlManager *whitelist.Manager wlManager *whitelist.Manager
client *network.WSClient client *network.WSClient
cpuLimit float64 cpuLimit float64
scanPaths []string scanPaths []string
stopChan chan struct{} stopChan chan struct{}
} }
func NewScanner(wl *whitelist.Manager, client *network.WSClient) *Scanner { func NewScanner(wl *whitelist.Manager, client *network.WSClient) *Scanner {
return &Scanner{ return &Scanner{
wlManager: wl, wlManager: wl,
client: client, client: client,
cpuLimit: 50.0, cpuLimit: 50.0,
scanPaths: []string{"/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/tmp", "/home"}, scanPaths: []string{"/bin", "/sbin", "/usr/bin", "/usr/sbin", "/etc", "/tmp", "/home"},
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
} }
} }
func (s *Scanner) Start() { func (s *Scanner) Start() {
log.Println("[扫描器] 启动文件完整性扫描...") log.Println("[扫描器] 启动文件完整性扫描...")
go s.scanLoop() go s.scanLoop()
} }
func (s *Scanner) scanLoop() { func (s *Scanner) scanLoop() {
ticker := time.NewTicker(10 * time.Minute) ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-s.stopChan: case <-s.stopChan:
return return
case <-ticker.C: case <-ticker.C:
s.performScan() s.performScan()
} }
} }
} }
func (s *Scanner) performScan() { func (s *Scanner) performScan() {
log.Println("[扫描器] 开始新一轮全盘扫描") log.Println("[扫描器] 开始新一轮全盘扫描")
fileCount := 0 fileCount := 0
for _, root := range s.scanPaths { for _, root := range s.scanPaths {
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
select { select {
case <-s.stopChan: case <-s.stopChan:
return filepath.SkipDir return filepath.SkipDir
default: default:
} }
if err != nil { if err != nil {
log.Printf("[扫描器] 访问错误: %v", err) log.Printf("[扫描器] 访问错误: %v", err)
return nil return nil
} }
fileCount++ fileCount++
if fileCount%100 == 0 { if fileCount%100 == 0 {
s.checkCPUAndSleep() s.checkCPUAndSleep()
} }
if info.IsDir() { if info.IsDir() {
if s.wlManager.IsPathIgnored(path) { if s.wlManager.IsPathIgnored(path) {
return filepath.SkipDir return filepath.SkipDir
} }
return nil return nil
} }
isWhitelisted, isHashMatch, err := s.wlManager.CheckFileStatus(path) isWhitelisted, isHashMatch, err := s.wlManager.CheckFileStatus(path)
if err != nil { if err != nil {
log.Printf("[扫描器] 检查文件状态失败: %v", err) log.Printf("[扫描器] 检查文件状态失败: %v", err)
return nil return nil
} }
if !isWhitelisted { if !isWhitelisted {
log.Printf("[扫描器] 发现未在白名单文件: %s", path) log.Printf("[扫描器] 发现未在白名单文件: %s", path)
s.reportFile(path, "NON_WHITELISTED_FILE") s.reportFile(path, "NON_WHITELISTED_FILE")
} else if !isHashMatch { } else if !isHashMatch {
log.Printf("[扫描器] 警告文件Hash不匹配(可能被篡改): %s", path) log.Printf("[扫描器] 警告文件Hash不匹配(可能被篡改): %s", path)
s.reportFile(path, "FILE_HASH_MISMATCH") s.reportFile(path, "FILE_HASH_MISMATCH")
} }
return nil return nil
}) })
if err != nil { if err != nil {
log.Printf("[扫描器] 扫描目录 %s 出错: %v", root, err) log.Printf("[扫描器] 扫描目录 %s 出错: %v", root, err)
} }
} }
} }
func (s *Scanner) checkCPUAndSleep() { func (s *Scanner) checkCPUAndSleep() {
percent, err := cpu.Percent(200*time.Millisecond, false) percent, err := cpu.Percent(200*time.Millisecond, false)
if err != nil || len(percent) == 0 { if err != nil || len(percent) == 0 {
log.Printf("[扫描器] 获取CPU使用率失败: %v", err) log.Printf("[扫描器] 获取CPU使用率失败: %v", err)
return return
} }
if percent[0] > s.cpuLimit { if percent[0] > s.cpuLimit {
log.Printf("[扫描器] CPU使用率过高 (%.2f%%)暂停扫描5秒", percent[0]) log.Printf("[扫描器] CPU使用率过高 (%.2f%%)暂停扫描5秒", percent[0])
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
} }
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
func (s *Scanner) reportFile(path string, alertType string) { func (s *Scanner) reportFile(path string, alertType string) {
payload := map[string]interface{}{ payload := map[string]interface{}{
"filepath": path, "filepath": path,
"status": "detected", "status": "detected",
} }
packet := network.NewPacket(alertType, payload) packet := network.NewPacket(alertType, payload)
s.client.SendQueue(packet) s.client.SendQueue(packet)
} }
func (s *Scanner) Stop() { func (s *Scanner) Stop() {
log.Println("[扫描器] 停止文件完整性扫描...") log.Println("[扫描器] 停止文件完整性扫描...")
close(s.stopChan) close(s.stopChan)
} }

View File

@ -1,119 +1,119 @@
package scanner package scanner
import ( import (
"log" "log"
"os" "os"
"time" "time"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/wuko233/sysmonitord/internal/network" "github.com/wuko233/sysmonitord/internal/network"
"github.com/wuko233/sysmonitord/internal/whitelist" "github.com/wuko233/sysmonitord/internal/whitelist"
) )
type Watcher struct { type Watcher struct {
wlManager *whitelist.Manager wlManager *whitelist.Manager
client *network.WSClient client *network.WSClient
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
stopChan chan struct{} stopChan chan struct{}
watchPaths []string watchPaths []string
} }
func NewWatcher(wl *whitelist.Manager, client *network.WSClient) (*Watcher, error) { func NewWatcher(wl *whitelist.Manager, client *network.WSClient) (*Watcher, error) {
fsWatch, err := fsnotify.NewWatcher() fsWatch, err := fsnotify.NewWatcher()
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Watcher{ return &Watcher{
wlManager: wl, wlManager: wl,
client: client, client: client,
watcher: fsWatch, watcher: fsWatch,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
// TODO: 当前仅实现对主目录的监控,后续实现递归监控子目录 // TODO: 当前仅实现对主目录的监控,后续实现递归监控子目录
watchPaths: []string{ watchPaths: []string{
"/bin", "/sbin", "/usr/bin", "/etc/init.d", "/tmp", "/bin", "/sbin", "/usr/bin", "/etc/init.d", "/tmp",
}, },
}, nil }, nil
} }
func (w *Watcher) Start() { func (w *Watcher) Start() {
log.Println("[监听器] 启动实时文件监控...") log.Println("[监听器] 启动实时文件监控...")
for _, path := range w.watchPaths { for _, path := range w.watchPaths {
if _, err := os.Stat(path); err == nil { if _, err := os.Stat(path); err == nil {
if err := w.watcher.Add(path); err != nil { if err := w.watcher.Add(path); err != nil {
log.Printf("[监听器] 无法监控路径 %s: %v", path, err) log.Printf("[监听器] 无法监控路径 %s: %v", path, err)
} else { } else {
log.Printf("[监听器] 开始监控路径: %s", path) log.Printf("[监听器] 开始监控路径: %s", path)
} }
} }
} }
go w.eventLoop() go w.eventLoop()
} }
func (w *Watcher) eventLoop() { func (w *Watcher) eventLoop() {
for { for {
select { select {
case <-w.stopChan: case <-w.stopChan:
return return
case event, ok := <-w.watcher.Events: case event, ok := <-w.watcher.Events:
if !ok { if !ok {
return return
} }
if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) { if event.Has(fsnotify.Create) || event.Has(fsnotify.Write) {
if w.wlManager.IsPathIgnored(event.Name) { if w.wlManager.IsPathIgnored(event.Name) {
continue continue
} }
go w.handleFileChange(event.Name, event.Op.String()) go w.handleFileChange(event.Name, event.Op.String())
} }
case err, ok := <-w.watcher.Errors: case err, ok := <-w.watcher.Errors:
if !ok { if !ok {
return return
} }
log.Printf("[监听器] 错误: %v", err) log.Printf("[监听器] 错误: %v", err)
} }
} }
} }
func (w *Watcher) handleFileChange(path string, op string) { func (w *Watcher) handleFileChange(path string, op string) {
time.Sleep(200 * time.Millisecond) // 等待文件写入完成 time.Sleep(200 * time.Millisecond) // 等待文件写入完成
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return return
} }
isWhitelisted, isHashMatch, err := w.wlManager.CheckFileStatus(path) isWhitelisted, isHashMatch, err := w.wlManager.CheckFileStatus(path)
if err != nil { if err != nil {
return return
} }
if !isWhitelisted { if !isWhitelisted {
log.Printf("[监听器] 实时拦截:检测到非白名单文件变动 (%s): %s", op, path) log.Printf("[监听器] 实时拦截:检测到非白名单文件变动 (%s): %s", op, path)
w.reportEvent(path, "REALTIME_FILE_ALERT", op) w.reportEvent(path, "REALTIME_FILE_ALERT", op)
} else if !isHashMatch { } else if !isHashMatch {
log.Printf("[监听器] 实时拦截:检测到白名单文件被篡改 (%s): %s", op, path) log.Printf("[监听器] 实时拦截:检测到白名单文件被篡改 (%s): %s", op, path)
w.reportEvent(path, "REALTIME_HASH_MISMATCH", op) w.reportEvent(path, "REALTIME_HASH_MISMATCH", op)
} }
} }
func (w *Watcher) reportEvent(path, alertType, op string) { func (w *Watcher) reportEvent(path, alertType, op string) {
payload := map[string]interface{}{ payload := map[string]interface{}{
"filepath": path, "filepath": path,
"operation": op, "operation": op,
"time": time.Now(), "time": time.Now(),
} }
packet := network.NewPacket(alertType, payload) packet := network.NewPacket(alertType, payload)
w.client.SendQueue(packet) w.client.SendQueue(packet)
} }
func (w *Watcher) Stop() { func (w *Watcher) Stop() {
log.Println("[监听器] 停止实时文件监控...") log.Println("[监听器] 停止实时文件监控...")
close(w.stopChan) close(w.stopChan)
w.watcher.Close() w.watcher.Close()
} }

View File

@ -1,135 +1,135 @@
package whitelist package whitelist
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"github.com/wuko233/sysmonitord/internal/config" "github.com/wuko233/sysmonitord/internal/config"
) )
type Manager struct { type Manager struct {
mu sync.RWMutex mu sync.RWMutex
official config.OfficialConfig official config.OfficialConfig
user config.UserConfig user config.UserConfig
mergedIgnore []string mergedIgnore []string
} }
func NewManager() *Manager { func NewManager() *Manager {
return &Manager{ return &Manager{
official: config.OfficialConfig{ official: config.OfficialConfig{
WhitelistFiles: make(map[string][]string), WhitelistFiles: make(map[string][]string),
}, },
user: config.UserConfig{ user: config.UserConfig{
SupplementFiles: make(map[string][]string), SupplementFiles: make(map[string][]string),
SupplementProcesses: make(map[string]string), SupplementProcesses: make(map[string]string),
}, },
} }
} }
func (m *Manager) UpdateConfig(official config.OfficialConfig, user config.UserConfig) { func (m *Manager) UpdateConfig(official config.OfficialConfig, user config.UserConfig) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
m.official = official m.official = official
m.user = user m.user = user
m.mergedIgnore = append([]string{}, m.official.IgnoredPaths...) m.mergedIgnore = append([]string{}, m.official.IgnoredPaths...)
m.mergedIgnore = append(m.mergedIgnore, m.user.IgnoredPaths...) m.mergedIgnore = append(m.mergedIgnore, m.user.IgnoredPaths...)
} }
func (m *Manager) IsPathIgnored(path string) bool { func (m *Manager) IsPathIgnored(path string) bool {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
return m.IsPathIgnoredUnsafe(path) return m.IsPathIgnoredUnsafe(path)
} }
func (m *Manager) IsPathIgnoredUnsafe(path string) bool { func (m *Manager) IsPathIgnoredUnsafe(path string) bool {
path = filepath.Clean(path) path = filepath.Clean(path)
for _, ignore := range m.mergedIgnore { for _, ignore := range m.mergedIgnore {
if strings.HasPrefix(path, filepath.Clean(ignore)) { if strings.HasPrefix(path, filepath.Clean(ignore)) {
return true return true
} }
} }
return false return false
} }
// CheckFileStatus 检查文件状态 // CheckFileStatus 检查文件状态
// 返回: isWhitelisted(是否在白名单), isValid(Hash是否匹配), err // 返回: isWhitelisted(是否在白名单), isValid(Hash是否匹配), err
func (m *Manager) CheckFileStatus(path string) (bool, bool, error) { func (m *Manager) CheckFileStatus(path string) (bool, bool, error) {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
if m.IsPathIgnoredUnsafe((path)) { if m.IsPathIgnoredUnsafe((path)) {
return true, true, nil return true, true, nil
} }
hashes, exists := m.official.WhitelistFiles[path] hashes, exists := m.official.WhitelistFiles[path]
if !exists { if !exists {
hashes, exists = m.user.SupplementFiles[path] hashes, exists = m.user.SupplementFiles[path]
} }
if !exists { if !exists {
return false, false, nil return false, false, nil
} }
fileHash, err := CalculateFileHash(path) fileHash, err := CalculateFileHash(path)
if err != nil { if err != nil {
return true, false, fmt.Errorf("计算文件哈希失败: %v", err) return true, false, fmt.Errorf("计算文件哈希失败: %v", err)
} }
for _, h := range hashes { for _, h := range hashes {
if strings.EqualFold(h, fmt.Sprintf("%v", fileHash)) { if strings.EqualFold(h, fmt.Sprintf("%v", fileHash)) {
return true, true, nil return true, true, nil
} }
} }
return true, false, nil return true, false, nil
} }
func CalculateFileHash(filePath string) (string, error) { func CalculateFileHash(filePath string) (string, error) {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
return "", err return "", err
} }
defer file.Close() defer file.Close()
hash := sha256.New() hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil { if _, err := io.Copy(hash, file); err != nil {
return "", err return "", err
} }
return hex.EncodeToString(hash.Sum(nil)), nil return hex.EncodeToString(hash.Sum(nil)), nil
} }
func (m *Manager) IsProcessAllowed(procName string, cmdLine string) bool { func (m *Manager) IsProcessAllowed(procName string, cmdLine string) bool {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
for _, p := range m.official.WhitelistProcesses { for _, p := range m.official.WhitelistProcesses {
if p == procName { if p == procName {
return true return true
} }
} }
if _, ok := m.user.SupplementProcesses[procName]; ok { if _, ok := m.user.SupplementProcesses[procName]; ok {
return true return true
} }
return false return false
} }
func (m *Manager) GetAuditServerUrl() string { func (m *Manager) GetAuditServerUrl() string {
m.mu.RLock() m.mu.RLock()
defer m.mu.RUnlock() defer m.mu.RUnlock()
return m.user.AuditServerUrl return m.user.AuditServerUrl
} }