diff --git a/cmd/start/start.go b/cmd/start/start.go index da5c96e..a9e4a89 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -9,6 +9,7 @@ import ( "sysmonitord/internal/monitor/detector" "sysmonitord/internal/monitor/timer" "sysmonitord/internal/monitor/watcher" + "sysmonitord/internal/notifier" "sysmonitord/internal/scanner/file" "sysmonitord/internal/scanner/process" "sysmonitord/internal/storage" @@ -107,6 +108,10 @@ var StartCmd = &cobra.Command{ procScheduler := timer.NewScheduler(time.Duration(cfg.Scanner.Process.Interval)*time.Second, procDetector) procScheduler.Start() + // ====== 启动告警管理器 ====== + alerter := notifier.NewAlerter(cfg.Notification) + alerter.Start() + logger.Log.Info("系统监控守护服务已启动,正在监控系统变化...") quit := make(chan os.Signal, 1) @@ -126,6 +131,14 @@ var StartCmd = &cobra.Command{ fileDetector.HandleEvent(event.Path, event.Op.String()) } + // test + alerter.PushAlert(notifier.AlertEvent{ + Type: "File", + Path: event.Path, + Reason: event.Op.String(), + Details: "To test", + }) + case err := <-mon.Errors(): logger.Log.Error("文件监听错误", zap.Error(err)) diff --git a/config.yaml b/config.yaml.example similarity index 100% rename from config.yaml rename to config.yaml.example diff --git a/internal/config/config.go b/internal/config/config.go index 4a9fdfa..27a27c1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,10 +1,28 @@ package config type Config struct { - Log LogConfig `yaml:"log"` - Audit AuditConfig `yaml:"audit"` - Scanner ScannerConfig `yaml:"scanner"` - Storage StorageConfig `yaml:"storage"` + Log LogConfig `yaml:"log"` + Audit AuditConfig `yaml:"audit"` + Scanner ScannerConfig `yaml:"scanner"` + Storage StorageConfig `yaml:"storage"` + Notification NotificationConfig `yaml:"notification"` +} + +type NotificationConfig struct { + Email EmailConfig `yaml:"email"` +} + +type EmailConfig struct { + Enabled bool `yaml:"enabled"` + Recipients []string `yaml:"recipients"` + SMTP SMTPConfig `yaml:"smtp"` +} + +type SMTPConfig struct { + Server string `yaml:"server"` + Port int `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` } type LogConfig struct { diff --git a/internal/notifier/alert_manager.go b/internal/notifier/alert_manager.go new file mode 100644 index 0000000..5f7e47b --- /dev/null +++ b/internal/notifier/alert_manager.go @@ -0,0 +1,97 @@ +package notifier + +import ( + "fmt" + "sync" + "sysmonitord/internal/config" + "sysmonitord/internal/notifier/mail" + "sysmonitord/pkg/logger" + "time" + + "go.uber.org/zap" +) + +type AlertEvent struct { + Type string + Path string + Reason string + Details string +} + +type Alerter struct { + cfg config.NotificationConfig + mailer *mail.Mailer + eventChan chan AlertEvent + buffer []AlertEvent + mu sync.Mutex + timer *time.Timer + interval time.Duration +} + +func NewAlerter(cfg config.NotificationConfig) *Alerter { + return &Alerter{ + cfg: cfg, + mailer: mail.NewMailer(cfg.Email), + eventChan: make(chan AlertEvent, 100), + buffer: make([]AlertEvent, 0), + interval: 1 * time.Minute, // Todo: 可配置化 + } +} + +func (a *Alerter) Start() { + go a.loop() +} + +func (a *Alerter) PushAlert(event AlertEvent) { + select { + case a.eventChan <- event: + logger.Log.Debug("[notifier] 推送告警事件", zap.String("path", event.Path), zap.String("reason", event.Reason)) + default: + logger.Log.Warn("[notifier] 告警事件通道已满,丢弃告警", zap.String("path", event.Path), zap.String("reason", event.Reason)) + } +} + +func (a *Alerter) loop() { + a.timer = time.NewTimer(a.interval) + + for { + select { + case event := <-a.eventChan: + a.mu.Lock() + a.buffer = append(a.buffer, event) + a.mu.Unlock() + logger.Log.Debug("[notifier] 收到告警,加入待发送序列", zap.String("path", event.Path)) + + case <-a.timer.C: + a.mu.Lock() + if len(a.buffer) > 0 { + a.sendAlert() + a.buffer = make([]AlertEvent, 0) + } + a.mu.Unlock() + + a.timer.Reset(a.interval) + } + } +} + +func (a *Alerter) sendAlert() { + if len(a.buffer) == 0 { + return + } + + subject := fmt.Sprintf("【Sysmonitor】新增 %d 个告警", len(a.buffer)) + body := "以下是最近的告警事件:\n\n" + + for _, event := range a.buffer { + body += fmt.Sprintf("- [%s] %s: %s (%s)\n", event.Type, event.Path, event.Reason, event.Details) + } + + body += "\n请及时关注系统安全状况。" + + if err := a.mailer.Send(subject, body); err != nil { + logger.Log.Error("[notifier] 发送告警邮件失败", zap.Error(err)) + } else { + logger.Log.Debug("[notifier] 告警邮件发送成功", zap.Int("count", len(a.buffer))) + } +} diff --git a/internal/notifier/mail/mailer.go b/internal/notifier/mail/mailer.go new file mode 100644 index 0000000..d76fa05 --- /dev/null +++ b/internal/notifier/mail/mailer.go @@ -0,0 +1,50 @@ +package mail + +import ( + "fmt" + "net/smtp" + "sysmonitord/internal/config" + "sysmonitord/pkg/logger" + + "go.uber.org/zap" +) + +type Mailer struct { + cfg config.EmailConfig +} + +func NewMailer(cfg config.EmailConfig) *Mailer { + return &Mailer{cfg: cfg} +} + +func (m *Mailer) Send(subject, body string) error { + if !m.cfg.Enabled { + logger.Log.Debug("[notifier] 未启用邮件通知,跳过....") + return nil + } + + headers := make(map[string]string) + headers["From"] = m.cfg.SMTP.Username + headers["To"] = m.cfg.Recipients[0] + headers["Subject"] = subject + + message := "" + for k, v := range headers { + message += fmt.Sprintf("%s: %s\r\n", k, v) + } + message += "\r\n" + body + + auth := smtp.PlainAuth("", m.cfg.SMTP.Username, m.cfg.SMTP.Password, m.cfg.SMTP.Server) + addr := fmt.Sprintf("%s:%d", m.cfg.SMTP.Server, m.cfg.SMTP.Port) + + logger.Log.Info("[notifier] 发送邮件通知", zap.String("subject", subject), zap.String("to", m.cfg.Recipients[0])) + + err := smtp.SendMail(addr, auth, m.cfg.SMTP.Username, m.cfg.Recipients, []byte(message)) + if err != nil { + logger.Log.Error("[notifier] 发送邮件失败", zap.Error(err)) + return err + } + + logger.Log.Info("[notifier] 邮件发送成功") + return nil +}