sysmonitord/internal/monitor/ssh_monitor.go
wk233 e6d4a8303c feat: 初始实现SSH服务器监控系统
- 核心功能:监控SSH登录事件,检测root登录
- 日志解析:集成systemd journal实时监控
- 告警系统:支持root登录告警和通道处理
- 项目结构:规范的Go项目布局
- 文档:完善README使用说明"
2026-01-06 21:21:45 +08:00

173 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package monitor
import (
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/coreos/go-systemd/sdjournal"
"github.com/wuko233/sysmonitord/internal/config"
)
func NewSSHMonitor(cfg *config.SSHMonitor, alertChan chan<- Alert) *SSHMonitor {
return &SSHMonitor{
config: cfg,
alertChan: alertChan,
stopChan: make(chan struct{}),
}
}
func (m *SSHMonitor) Start() error {
log.Println("启动SSH监控...")
return m.startJournalMonitor()
}
func (m *SSHMonitor) Stop() error {
log.Println("停止SSH监控...")
close(m.stopChan)
if m.journal != nil {
m.journal.Close()
}
return nil
}
func (m *SSHMonitor) startJournalMonitor() error {
journal, err := sdjournal.NewJournal()
if err != nil {
return fmt.Errorf("打开日志失败: %v", err)
}
m.journal = journal
filters := []string{
"_TRANSPORT=syslog",
"SYSLOG_IDENTIFIER=sshd",
}
for _, filter := range filters {
if err := journal.AddMatch(filter); err != nil {
return fmt.Errorf("添加日志过滤器%s失败: %v", filter, err)
}
}
if err := journal.SeekTail(); err != nil {
return fmt.Errorf("跳转到日志末尾失败: %v", err)
}
m.monitorJournal()
return nil
}
func (m *SSHMonitor) monitorJournal() {
for {
select {
case <-m.stopChan:
return
default:
count, err := m.journal.Next()
if err != nil {
log.Printf("读取日志错误: %v", err)
time.Sleep(time.Second)
continue
}
if count == 0 {
time.Sleep(100 * time.Millisecond)
continue
}
entry, err := m.journal.GetEntry()
if err != nil {
log.Printf("获取日志条目错误: %v", err)
continue
}
m.processLogEntry(entry.Fields)
}
}
}
func (m *SSHMonitor) processLogEntry(field map[string]string) {
message, exists := field[sdjournal.SD_JOURNAL_FIELD_MESSAGE]
if !exists {
return
}
if strings.Contains(message, "Accepted") ||
strings.Contains(message, "authentication failure") ||
strings.Contains(message, "Failed password") {
event := m.parseSSHEvent(field, message)
if event != nil {
m.handleSSHEvent(event)
}
}
}
func (m *SSHMonitor) parseSSHEvent(field map[string]string, message string) *SSHLoginEvent {
var event *SSHLoginEvent
if strings.Contains(message, "Accepted") {
re := regexp.MustCompile(`Accepted (\S+) for (\S+) from ([\d\.:a-fA-F]+) port (\d+)`)
// \S+ 匹配非空白字符,[\d\.:a-fA-F]+ 匹配IP地址
matches := re.FindStringSubmatch(message)
if matches != nil {
// timestamp := time.Unix(0, int64(field[sdjournal.SD_JOURNAL_FIELD_REALTIME_TIMESTAMP])*int64(time.Microsecond))
timestamp := time.Now()
event = &SSHLoginEvent{
Timestamp: timestamp,
Hostname: field["_HOSTNAME"],
Username: matches[2],
Method: matches[1],
SourceIP: matches[3],
Port: matches[4],
Service: field["SYSLOG_IDENTIFIER"],
PID: field["_PID"],
Message: message,
}
}
}
// Todo处理其他情况
return event
}
func (m *SSHMonitor) handleSSHEvent(event *SSHLoginEvent) {
if m.config.DisplayOnShell {
// 在终端显示事件
m.displayEventOnShell(event)
}
if event.Username == "root" && m.config.AlertOnRootLogin {
alert := Alert{
Type: "SSH_ROOT_LOGIN",
Level: "HIGH",
Message: fmt.Sprintf("检测到来自%s的root登录", event.SourceIP),
Timestamp: time.Now(),
Data: event,
}
m.alertChan <- alert
}
log.Printf("SSH登录事件: 用户=%s, 来源IP=%s, 方式=%s",
event.Username, event.SourceIP, event.Method)
}
func (m *SSHMonitor) displayEventOnShell(event *SSHLoginEvent) {
fmt.Println("════════ SSH登录事件 ════════")
fmt.Printf("时间: %s\n", event.Timestamp.Format("2006-01-02 15:04:05"))
fmt.Printf("主机: %s\n", event.Hostname)
fmt.Printf("用户: %s\n", event.Username)
fmt.Printf("方式: %s\n", event.Method)
fmt.Printf("来源IP: %s\n", event.SourceIP)
fmt.Printf("端口: %s\n", event.Port)
fmt.Printf("服务: %s (PID: %s)\n", event.Service, event.PID)
fmt.Printf("消息: %s\n", event.Message)
fmt.Println("════════════════════════════════")
}