// Package serversecurity 提供服务器安全监控日志领域服务(采集上报/查询/统计)。 package serversecurity import ( "context" "strings" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gtime" "service.xpcool.com/internal/consts" "service.xpcool.com/internal/dao" "service.xpcool.com/internal/library/response" "service.xpcool.com/internal/model/dto" ) // ISecurity 服务器安全日志领域服务接口。 type ISecurity interface { Report(context.Context, string, []dto.SecurityLogInput) (int, error) List(context.Context, dto.SecurityLogFilter) ([]dto.SecurityLogVO, int, error) Stats(context.Context, string, string) (*dto.SecurityStats, error) } type security struct{} var localSecurity ISecurity // New 创建安全日志服务实现。 func New() ISecurity { return &security{} } // Security 返回已注册的安全日志服务实现。 func Security() ISecurity { if localSecurity == nil { panic("Security 实现未注册") } return localSecurity } // RegisterServerSecurity 注册安全日志服务实现。 func RegisterServerSecurity(i ISecurity) { localSecurity = i } // Report 校验内部令牌并把采集到的日志批量入库(幂等:按 log_time+src_ip+event_type 去重)。 func (s *security) Report(ctx context.Context, token string, items []dto.SecurityLogInput) (int, error) { // 校验内部上报令牌(配置 internalToken,来自环境变量 INTERNAL_TOKEN)。 expect := g.Cfg().MustGet(ctx, "internalToken").String() if expect == "" || token != expect { return 0, response.Error(consts.CodeUnauthorized, "内部上报令牌无效") } if len(items) == 0 { return 0, nil } rows := make([]map[string]interface{}, 0, len(items)) for _, it := range items { // 事件时间兜底:非法/空则用当前时间。 logTime := strings.TrimSpace(it.LogTime) if logTime == "" { logTime = gtime.Now().Format("Y-m-d H:i:s") } srcIp := strings.TrimSpace(it.SrcIp) if len(srcIp) > 45 { srcIp = srcIp[:45] } detail := strings.TrimSpace(it.Detail) if len(detail) > 500 { detail = detail[:500] } rows = append(rows, map[string]interface{}{ "log_time": logTime, "src_ip": srcIp, "src_port": it.SrcPort, "dest_port": it.DestPort, "event_type": strings.TrimSpace(it.EventType), "detail": detail, }) } if _, err := dao.ServerSecurityLog.Ctx(ctx).Data(rows).Insert(); err != nil { return 0, gerror.Wrap(err, "写入服务器安全日志失败") } return len(rows), nil } // List 分页查询安全日志(时间倒序)。 func (s *security) List(ctx context.Context, f dto.SecurityLogFilter) ([]dto.SecurityLogVO, int, error) { m := dao.ServerSecurityLog.Ctx(ctx) if f.SrcIp != "" { m = m.Where("src_ip LIKE ?", "%"+f.SrcIp+"%") } if f.EventType != "" { m = m.Where("event_type", f.EventType) } if f.DestPort > 0 { m = m.Where("dest_port", f.DestPort) } if f.DateFrom != "" { m = m.WhereGTE("log_time", f.DateFrom) } if f.DateTo != "" { m = m.WhereLTE("log_time", f.DateTo) } total, err := m.Clone().Count() if err != nil { return nil, 0, gerror.Wrap(err, "统计安全日志总数失败") } var list []dto.SecurityLogVO if err = m.Clone().Page(f.Page, f.Size).OrderDesc("log_time").OrderDesc("id").Scan(&list); err != nil { return nil, 0, gerror.Wrap(err, "查询安全日志列表失败") } for i := range list { list[i].EventName = dto.SecurityEventName(list[i].EventType) } return list, total, nil } // Stats 统计:总数/爆破/封禁/成功登录 + TOP 攻击源 + 按类型分布。 // 时间范围为闭区间,空则默认最近 24 小时。 func (s *security) Stats(ctx context.Context, dateFrom, dateTo string) (*dto.SecurityStats, error) { if dateFrom == "" { dateFrom = gtime.Now().AddDate(0, 0, -1).Format("Y-m-d H:i:s") } if dateTo == "" { dateTo = gtime.Now().Format("Y-m-d H:i:s") } base := dao.ServerSecurityLog.Ctx(ctx).Where("log_time BETWEEN ? AND ?", dateFrom, dateTo) st := &dto.SecurityStats{} var err error if st.Total, err = base.Clone().Count(); err != nil { return nil, gerror.Wrap(err, "统计事件总数失败") } if st.Failed, err = base.Clone().Where("event_type", "failed_ssh").Count(); err != nil { return nil, gerror.Wrap(err, "统计爆破次数失败") } if st.Banned, err = base.Clone().Where("event_type", "banned").Count(); err != nil { return nil, gerror.Wrap(err, "统计封禁次数失败") } if st.Accepted, err = base.Clone().Where("event_type", "accepted_ssh").Count(); err != nil { return nil, gerror.Wrap(err, "统计成功登录次数失败") } // TOP 攻击源(爆破+封禁优先)。 if err = base.Clone().WhereIn("event_type", g.Slice{"failed_ssh", "banned"}). Fields("src_ip, COUNT(*) AS cnt"). Group("src_ip").OrderDesc("cnt").Limit(10).Scan(&st.TopIps); err != nil { return nil, gerror.Wrap(err, "查询 TOP 攻击源失败") } // 按类型分布。 if err = base.Clone().Fields("event_type, COUNT(*) AS cnt"). Group("event_type").Scan(&st.ByType); err != nil { return nil, gerror.Wrap(err, "按事件类型统计失败") } for i := range st.ByType { st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType) } return st, nil }