156 lines
5.1 KiB
Go
156 lines
5.1 KiB
Go
// 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 implementation not registered")
|
||
}
|
||
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, "insert server security log")
|
||
}
|
||
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, "count security log")
|
||
}
|
||
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, "query security log")
|
||
}
|
||
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, "count total")
|
||
}
|
||
if st.Failed, err = base.Clone().Where("event_type", "failed_ssh").Count(); err != nil {
|
||
return nil, gerror.Wrap(err, "count failed")
|
||
}
|
||
if st.Banned, err = base.Clone().Where("event_type", "banned").Count(); err != nil {
|
||
return nil, gerror.Wrap(err, "count banned")
|
||
}
|
||
if st.Accepted, err = base.Clone().Where("event_type", "accepted_ssh").Count(); err != nil {
|
||
return nil, gerror.Wrap(err, "count accepted")
|
||
}
|
||
// 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, "query top ips")
|
||
}
|
||
// 按类型分布。
|
||
if err = base.Clone().Fields("event_type, COUNT(*) AS cnt").
|
||
Group("event_type").Scan(&st.ByType); err != nil {
|
||
return nil, gerror.Wrap(err, "query by type")
|
||
}
|
||
for i := range st.ByType {
|
||
st.ByType[i].EventName = dto.SecurityEventName(st.ByType[i].EventType)
|
||
}
|
||
return st, nil
|
||
}
|