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
{
"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"]
}
}
```

View File

@ -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},
},
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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
}