feat: 初始实现SSH服务器监控系统

- 核心功能:监控SSH登录事件,检测root登录
- 日志解析:集成systemd journal实时监控
- 告警系统:支持root登录告警和通道处理
- 项目结构:规范的Go项目布局
- 文档:完善README使用说明"
This commit is contained in:
wk233 2026-01-06 21:21:45 +08:00
parent 91a22e79d9
commit e6d4a8303c
9 changed files with 338 additions and 40 deletions

View File

@ -1,6 +1,8 @@
# sysmonitord # sysmonitord
推翻重做的VigilGoGuard更符合工程规范 重写VigilGoGuard更符合工程规范
多线程支持、更加模块化,以及...
## ToDo ## ToDo

70
cmd/sysmonitord/main.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"log"
"os"
"time"
"github.com/wuko233/sysmonitord/internal/config"
"github.com/wuko233/sysmonitord/internal/monitor"
)
func main() {
log.Println("启动sysmonitord...")
cfg := &config.SSHMonitor{
Enabled: true,
DisplayOnShell: true,
AlertOnRootLogin: true,
}
log.Printf("加载SSH监控配置: %+v\n", cfg)
alertChan := make(chan monitor.Alert, 100)
log.Println("初始化SSH监控器...")
sshMonitor := monitor.NewSSHMonitor(cfg, alertChan)
log.Println("启用告警处理...")
go handleAlerts(alertChan)
go func() {
if err := sshMonitor.Start(); err != nil {
log.Fatalf("启动SSH监控器失败: %v", err)
}
}()
time.Sleep(3 * time.Second)
log.Println("启动sysmonitord完成.")
log.Println("sysmonitord正在运行...")
log.Println("按Ctrl+C退出...")
stopChan := make(chan os.Signal, 1)
<-stopChan
log.Println("停止SSH监控器...")
if err := sshMonitor.Stop(); err != nil {
log.Fatalf("停止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: 接入发信接口
}
}
}

7
go.mod Normal file
View File

@ -0,0 +1,7 @@
module github.com/wuko233/sysmonitord
go 1.24.3
require (
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=

View File

@ -0,0 +1,7 @@
package config
type SSHMonitor struct {
Enabled bool `yaml:"enabled"`
DisplayOnShell bool `yaml:"display_on_shell"`
AlertOnRootLogin bool `yaml:"alert_on_root_login"`
}

View File

@ -0,0 +1,172 @@
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("════════════════════════════════")
}

36
internal/monitor/types.go Normal file
View File

@ -0,0 +1,36 @@
package monitor
import (
"time"
"github.com/coreos/go-systemd/sdjournal"
"github.com/wuko233/sysmonitord/internal/config"
)
type Alert struct {
Type string `json:"type"` // 告警类型
Level string `json:"level"` // 告警级别
Message string `json:"message"` // 告警消息
Timestamp time.Time `json:"timestamp"` // 时间戳
Data interface{} `json:"data"` // 附加数据
}
type SSHMonitor struct {
config *config.SSHMonitor
alertChan chan<- Alert
stopChan chan struct{}
journal *sdjournal.Journal
}
type SSHLoginEvent struct {
Timestamp time.Time `json:"timestamp"`
Hostname string `json:"hostname"` // 主机名
Username string `json:"username"` // 用户名
Method string `json:"method"` // 登录方式password/publickey
SourceIP string `json:"source_ip"` // 来源IP
Port string `json:"port"` // 端口
Service string `json:"service"` // 服务名
PID string `json:"pid"` // 进程ID
Message string `json:"message"` // 原始日志消息
}

BIN
sysmonitord Normal file

Binary file not shown.