Compare commits

...

9 Commits

11 changed files with 769 additions and 136 deletions

5
.gitignore vendored
View File

@ -33,4 +33,7 @@ config.local.yaml
.env.local .env.local
# 系统文件 # 系统文件
.DS_Store .DS_Store
# 需求文件
todo.txt

View File

@ -4,153 +4,155 @@ import (
"io" "io"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"time" "time"
"github.com/wuko233/sysmonitord/internal/config" "github.com/wuko233/sysmonitord/internal/config"
"github.com/wuko233/sysmonitord/internal/monitor" "github.com/wuko233/sysmonitord/internal/monitor"
"github.com/wuko233/sysmonitord/internal/network"
"github.com/wuko233/sysmonitord/internal/scanner"
"github.com/wuko233/sysmonitord/internal/whitelist"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
) )
const (
OfficialConfigURL = "http://localhost:8080/api/configs/official.json"
UserConfigURL = "http://localhost:8080/api/configs/user.json"
CenterServerURL = "ws://localhost:8080/ws/monitor"
)
func main() { func main() {
// 设置日志输出到文件和控制台
initLogger()
log.Println("==========================================")
log.Print(`
_ _ _
(_) | | |
___ _ _ ___ _ __ ___ ___ _ __ _| |_ ___ _ __ __| |
/ __| | | / __| '_ ` + "`" + ` _ \ / _ \| '_ \| | __/ _ \| '__/ _` + "`" + ` |
\__ \ |_| \__ \ | | | | | (_) | | | | | || (_) | | | (_| |
|___/\__, |___/_| |_| |_|\___/|_| |_|_|\__\___/|_| \__,_|
__/ |
|___/
`)
log.Println("==========================================")
// 加载配置
log.Println("[启动流程] 1/6: 下载远程安全策略配置...")
cfgLoader := network.NewConfigLoader()
officialCfg, userCfg, err := cfgLoader.LoadConfigs(network.ConfigUrls{
OfficialConfigUrl: OfficialConfigURL,
UserConfigUrl: UserConfigURL,
})
if err != nil {
log.Fatalf("[启动错误]下载配置失败: %v", err)
log.Println("[启动降级] 继续使用默认空配置...")
// os.Exit(1)
}
log.Println("[启动流程] 2/6: 初始化白名单判定引擎...")
wlManager := whitelist.NewManager()
wlManager.UpdateConfig(officialCfg, userCfg)
log.Println("[启动流程] 3/6: 启动中心服务器连接...")
centerClient := network.NewWSClient(network.ClientConfig{
ServerURL: CenterServerURL,
SendInterval: 1 * time.Second,
BufferSize: 1000,
})
centerClient.Start()
auditUrl := wlManager.GetAuditServerUrl()
if auditUrl == "" {
auditUrl = CenterServerURL
}
auditClient := network.NewWSClient(network.ClientConfig{
ServerURL: auditUrl,
SendInterval: 1 * time.Second,
BufferSize: 1000,
})
auditClient.Start()
log.Println("[启动流程] 4/6: 启动文件完整性防护...")
// 扫盘器
sysScanner := scanner.NewScanner(wlManager, centerClient)
sysScanner.Start()
// 监控器
sysWatcher, err := scanner.NewWatcher(wlManager, centerClient)
if err != nil {
log.Fatalf("[启动错误] 初始化监控器失败: %v", err)
} else {
sysWatcher.Start()
}
log.Println("[启动流程] 5/6: 启动系统行为监控...")
// SSH监控
sshAlertChan := make(chan monitor.Alert, 100)
sshMon := monitor.NewSSHMonitor(&config.SSHMonitor{
Enabled: true,
AlertOnRootLogin: true,
DisplayOnShell: true,
}, sshAlertChan)
go func() {
for alert := range sshAlertChan {
packet := network.NewPactet("SSH_ALERT", alert)
auditClient.SendQueue(packet)
}
}()
go func() {
if err := sshMon.Start(); err != nil {
log.Printf("[监控错误] SSH监控遇到错误: %v", err)
}
}()
// 状态监控
metricsChan := make(chan monitor.ServerMetrics, 100)
infoMon := monitor.NewInfoMonitor(nil, metricsChan)
go func() {
for metrics := range metricsChan {
packet := network.NewPactet("STATUS_UPDATE", metrics)
centerClient.SendQueue(packet)
}
}()
go infoMon.Start()
log.Println("[启动流程] 6/6: 系统监控守护进程启动完成!")
stopChan := make(chan os.Signal, 1)
signal.Notify(stopChan, os.Interrupt, syscall.SIGTERM)
<-stopChan
log.Println("[守护进程] 接收到停止信号,正在关闭...")
if sysWatcher != nil {
sysWatcher.Stop()
}
sysScanner.Stop()
sshMon.Stop()
infoMon.Stop()
centerClient.Stop()
auditClient.Stop()
log.Println("[守护进程] 已成功停止,安全退出程序。")
}
func initLogger() {
log.SetOutput(os.Stdout) log.SetOutput(os.Stdout)
fileLogger := &lumberjack.Logger{ fileLogger := &lumberjack.Logger{
Filename: "/var/log/sysmonitord/sysmonitord.log", Filename: "/var/log/sysmonitord/sysmonitord.log",
MaxSize: 10, // MB MaxSize: 100, // MB
MaxBackups: 5, MaxBackups: 7,
MaxAge: 28, // days MaxAge: 30, // 天
Compress: true, Compress: true,
} }
log.SetOutput(io.MultiWriter(os.Stdout, fileLogger)) log.SetOutput(io.MultiWriter(os.Stdout, fileLogger))
log.Println("启动sysmonitord...")
// SSH监控配置
sshCfg := &config.SSHMonitor{
Enabled: true,
DisplayOnShell: true,
AlertOnRootLogin: true,
}
// 信息监控配置
infoCfg := &monitor.InfoMonitorConfig{
Enabled: true,
Interval: 30 * time.Second, // 每30秒采集一次
LogFilePath: "/var/log/sysmonitord/info_monitor.log",
MaxLogSize: 100 * 1024 * 1024, // 100MB
LogRetention: 30, // 保留30天
ProcessLimit: 10, // 显示10个进程
CollectNetwork: true,
CollectProcess: true,
}
alertChan := make(chan monitor.Alert, 100)
metricsChan := make(chan monitor.ServerMetrics, 100)
log.Println("初始化监控器...")
// 创建SSH监控器
sshMonitor := monitor.NewSSHMonitor(sshCfg, alertChan)
// 创建信息监控器
infoMonitor := monitor.NewInfoMonitor(infoCfg, metricsChan)
// 启动告警处理
log.Println("启动告警处理...")
go handleAlerts(alertChan)
// 启动指标处理
log.Println("启动指标处理...")
go handleMetrics(metricsChan)
// 启动SSH监控器
go func() {
log.Println("启动SSH监控器...")
if err := sshMonitor.Start(); err != nil {
log.Fatalf("启动SSH监控器失败: %v", err)
}
}()
// 启动信息监控器
go func() {
log.Println("启动信息监控器...")
if err := infoMonitor.Start(); err != nil {
log.Fatalf("启动信息监控器失败: %v", err)
}
}()
time.Sleep(3 * time.Second)
log.Println("sysmonitord监控系统已启动.")
log.Println("按Ctrl+C退出...")
stopChan := make(chan os.Signal, 1)
<-stopChan
log.Println("正在停止监控器...")
// 停止信息监控器
if err := infoMonitor.Stop(); err != nil {
log.Printf("停止信息监控器失败: %v", err)
}
// 停止SSH监控器
if err := sshMonitor.Stop(); err != nil {
log.Printf("停止SSH监控器失败: %v", err)
}
time.Sleep(1 * time.Second)
log.Println("sysmonitord已退出.")
}
func handleAlerts(alertChan <-chan monitor.Alert) {
for alert := range alertChan {
log.Printf("[告警] 类型: %s | 级别: %s | 时间: %s | 消息: %s | 数据: %+v\n",
alert.Type, alert.Level, alert.Timestamp.Format(time.RFC3339), alert.Message, alert.Data)
switch alert.Type {
case "SSH_ROOT_LOGIN":
log.Println("ROOT用户登入警告")
// Todo: 接入发信接口
}
}
}
func handleMetrics(metricsChan <-chan monitor.ServerMetrics) {
for metrics := range metricsChan {
// 这里可以处理指标数据,比如:
// 1. 存储到数据库
// 2. 发送到监控系统
// 3. 生成告警
// 示例:检查指标并生成告警
checkMetrics(&metrics)
}
}
func checkMetrics(metrics *monitor.ServerMetrics) {
// 检查CPU使用率
if metrics.CPU.UsagePercent > 90 {
log.Printf("[警告] CPU使用率过高: %.2f%%\n", metrics.CPU.UsagePercent)
}
// 检查内存使用率
if metrics.Memory.UsedPercent > 90 {
log.Printf("[警告] 内存使用率过高: %.2f%%\n", metrics.Memory.UsedPercent)
}
// 检查磁盘使用率
for _, disk := range metrics.Disk {
if disk.UsedPercent > 90 {
log.Printf("[警告] 磁盘%s使用率过高: %.2f%%\n",
disk.Mountpoint, disk.UsedPercent)
}
}
// 检查负载
if metrics.Load.RelativeLoad1 > 3.0 {
log.Printf("[警告] 系统负载过高: %.2f\n", metrics.Load.Load1)
}
} }

5
go.mod
View File

@ -4,6 +4,11 @@ go 1.24.3
require github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf require github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
)
require ( require (
github.com/ebitengine/purego v0.9.1 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect

4
go.sum
View File

@ -4,9 +4,13 @@ github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5z
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=

View File

@ -1,5 +1,44 @@
package config package config
import "time"
type Configuration struct {
Local Localconfig // 本地配置
Offical OfficialConfig // 官方配置
User UserConfig // 用户自定义配置
}
type Localconfig struct {
LogPath string `yaml:"log_path"`
CheckInterval time.Duration `yaml:"check_interval"`
ServerUrl string `yaml:"server_url"`
}
type OfficialConfig struct {
WhitelistFiles map[string][]string `yaml:"whitelist_files"`
WhitelistProcesses []string `yaml:"whitelist_processes"`
IgnoredPaths []string `yaml:"ignored_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"`
}
type EmailConfig struct {
ImapServer string `json:"imap_server"`
EmergencyMail []string `json:"emergency_mail"`
}
type SSHMonitor struct { type SSHMonitor struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
DisplayOnShell bool `yaml:"display_on_shell"` DisplayOnShell bool `yaml:"display_on_shell"`

133
internal/network/client.go Normal file
View File

@ -0,0 +1,133 @@
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() any {
c.mu.Lock()
defer c.mu.Unlock()
conn, _, err := websocket.DefaultDialer.Dial(c.config.ServerURL, nil)
if err != nil {
return err
}
c.conn = conn
c.isConnected = true
log.Printf("[网络] 成功连接到服务器: %s", c.config.ServerURL)
return nil
}

View File

@ -0,0 +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)
}

22
internal/network/types.go Normal file
View File

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

127
internal/scanner/scanner.go Normal file
View File

@ -0,0 +1,127 @@
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("[扫描器] 开始新一轮全盘扫描")
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 {
return nil
}
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(time.Second, 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.NewPactet(alertType, payload)
s.client.SendQueue(packet)
}
func (s *Scanner) Stop() {
log.Println("[扫描器] 停止文件完整性扫描...")
close(s.stopChan)
}

119
internal/scanner/watcher.go Normal file
View File

@ -0,0 +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.NewPactet(alertType, payload)
w.client.SendQueue(packet)
}
func (w *Watcher) Stop() {
log.Println("[监听器] 停止实时文件监控...")
close(w.stopChan)
w.watcher.Close()
}

View File

@ -0,0 +1,130 @@
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()
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.Lock()
defer m.mu.Unlock()
if m.IsPathIgnored((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
}