Files
pika/internal/service/ssh_login_service.go
T
2026-08-12 18:59:29 +08:00

245 lines
6.7 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"github.com/pika-monitor/pika/internal/models"
"github.com/pika-monitor/pika/internal/protocol"
"github.com/pika-monitor/pika/internal/repo"
"github.com/pika-monitor/pika/internal/websocket"
"go.uber.org/zap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
// SSHLoginService SSH登录服务
type SSHLoginService struct {
logger *zap.Logger
SSHLoginEventRepo *repo.SSHLoginEventRepo
agentRepo *repo.AgentRepo
wsManager *websocket.Manager
geoIPSvc *GeoIPService
notificationSvc *NotificationService
}
// NewSSHLoginService 创建服务
func NewSSHLoginService(logger *zap.Logger, db *gorm.DB, wsManager *websocket.Manager, geoIPSvc *GeoIPService, notificationSvc *NotificationService) *SSHLoginService {
return &SSHLoginService{
logger: logger,
SSHLoginEventRepo: repo.NewSSHLoginEventRepo(db),
agentRepo: repo.NewAgentRepo(db),
wsManager: wsManager,
geoIPSvc: geoIPSvc,
notificationSvc: notificationSvc,
}
}
// === 配置管理 ===
// GetConfig 获取探针的配置
func (s *SSHLoginService) GetConfig(ctx context.Context, agentID string) (*models.SSHLoginConfigData, error) {
agent, err := s.agentRepo.FindById(ctx, agentID)
if err != nil {
return nil, err
}
config := agent.SSHLoginConfig.Data()
return &config, nil
}
// UpdateConfig 更新配置并下发到 Agent
// 返回: config - 配置对象, error - 错误信息
func (s *SSHLoginService) UpdateConfig(ctx context.Context, agentID string, req *models.SSHLoginConfigData) error {
// 保存配置到数据库
config := models.SSHLoginConfigData{
Enabled: req.Enabled,
IPWhitelist: req.IPWhitelist,
ApplyStatus: "pending",
}
var agentForUpdate = models.Agent{
ID: agentID,
SSHLoginConfig: datatypes.NewJSONType(config),
}
if err := s.agentRepo.UpdateById(ctx, &agentForUpdate); err != nil {
return err
}
// 下发配置到 Agent
go func() {
if err := s.sendConfigToAgent(agentID, config.Enabled); err != nil {
s.logger.Error("下发SSH登录监控配置到 Agent 失败", zap.String("agentId", agentID), zap.Error(err))
}
}()
return nil
}
// sendConfigToAgent 下发配置到 Agent
func (s *SSHLoginService) sendConfigToAgent(agentID string, enabled bool) error {
configData := protocol.SSHLoginConfig{
Enabled: enabled,
}
message := protocol.OutboundMessage{
Type: protocol.MessageTypeSSHLoginConfig,
Data: configData,
}
msgBytes, err := json.Marshal(message)
if err != nil {
return fmt.Errorf("序列化消息失败: %w", err)
}
return s.wsManager.SendToClient(agentID, msgBytes)
}
// HandleConfigResult 处理 Agent 上报的配置应用结果
func (s *SSHLoginService) HandleConfigResult(ctx context.Context, agentID string, result protocol.SSHLoginConfigResult) error {
// 获取现有配置
config, err := s.GetConfig(ctx, agentID)
if err != nil {
return fmt.Errorf("获取配置失败: %w", err)
}
// 更新配置应用状态
status := "success"
if !result.Success {
status = "failed"
}
config.ApplyStatus = status
config.ApplyMessage = result.Message
var agentForUpdate = models.Agent{
ID: agentID,
SSHLoginConfig: datatypes.NewJSONType(*config),
}
return s.agentRepo.UpdateById(ctx, &agentForUpdate)
}
// === 事件处理 ===
// HandleEvent 处理 Agent 上报的事件
func (s *SSHLoginService) HandleEvent(ctx context.Context, agentID string, eventData protocol.SSHLoginEvent) error {
// 检查是否启用监控
config, err := s.GetConfig(ctx, agentID)
if err != nil {
return err
}
// 如果未启用,忽略事件
if config == nil || !config.Enabled {
s.logger.Debug("SSH登录监控未启用,忽略事件", zap.String("agentId", agentID))
return nil
}
ipLocation := ""
if s.geoIPSvc != nil && eventData.IP != "" {
ipLocation = s.geoIPSvc.LookupIP(eventData.IP)
}
// 保存事件到数据库
event := &models.SSHLoginEvent{
ID: uuid.NewString(),
AgentID: agentID,
Username: eventData.Username,
IP: eventData.IP,
IPLocation: ipLocation,
Port: eventData.Port,
Status: eventData.Status,
TTY: eventData.TTY,
SessionID: eventData.SessionID,
Timestamp: eventData.Timestamp,
CreatedAt: time.Now().UnixMilli(),
}
if err := s.SSHLoginEventRepo.Create(ctx, event); err != nil {
s.logger.Error("保存SSH登录事件失败", zap.Error(err))
return err
}
s.logger.Info("SSH登录事件已记录",
zap.String("agentId", agentID),
zap.String("username", eventData.Username),
zap.String("ip", eventData.IP),
zap.String("status", eventData.Status))
if config.IsIPWhitelisted(eventData.IP) {
s.logger.Info("IP在白名单中,忽略事件", zap.String("agentId", agentID), zap.String("ip", eventData.IP))
return nil
}
s.sendLoginSuccessNotification(ctx, agentID, eventData, ipLocation)
return nil
}
func (s *SSHLoginService) sendLoginSuccessNotification(ctx context.Context, agentID string, eventData protocol.SSHLoginEvent, ipLocation string) {
if s.notificationSvc == nil {
return
}
agent, err := s.agentRepo.FindById(ctx, agentID)
if err != nil {
s.logger.Error("获取探针信息失败", zap.String("agentId", agentID), zap.Error(err))
return
}
firedAt := eventData.Timestamp
if firedAt == 0 {
firedAt = time.Now().UnixMilli()
}
sourceIP := eventData.IP
if s.notificationSvc != nil {
if maskIP, err := s.notificationSvc.IsMaskIPEnabled(ctx); err == nil && maskIP {
sourceIP = maskIPAddress(sourceIP)
}
}
sourceAddr := sourceIP
if eventData.Port != "" {
sourceAddr = fmt.Sprintf("%s:%s", sourceIP, eventData.Port)
}
locationText := ipLocation
if locationText == "" {
locationText = "未知"
}
record := &models.AlertRecord{
AgentID: agentID,
AgentName: agent.Name,
AlertType: "ssh_login",
Message: fmt.Sprintf("SSH登录成功:用户 %s,来源 %s,归属地 %s,终端 %s,会话 %s", eventData.Username, sourceAddr, locationText, eventData.TTY, eventData.SessionID),
Threshold: 0,
ActualValue: 0,
Level: "warning",
Status: "notice",
FiredAt: firedAt,
CreatedAt: firedAt,
}
go func(record *models.AlertRecord, agent *models.Agent) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.notificationSvc.SendAlertNotification(ctx, NotificationTypeSSHLogin, record, agent); err != nil {
s.logger.Error("发送SSH登录成功通知失败",
zap.String("agentId", agentID),
zap.Error(err),
)
}
}(record, &agent)
}
// === 事件查询 ===
// DeleteEventsByAgentID 删除探针的所有事件
func (s *SSHLoginService) DeleteEventsByAgentID(ctx context.Context, agentID string) error {
return s.SSHLoginEventRepo.DeleteEventsByAgentID(ctx, agentID)
}