service.xpcool.com/internal/service/serversecurity/security.go
夏犀麟 4b3c00270f
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
feat(i18n): 中文化校验提示与服务层错误信息
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error
错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释
与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
2026-09-13 23:22:46 +08:00

156 lines
5.2 KiB
Go
Raw 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 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
}