feat:add notice manager

This commit is contained in:
samwaf
2025-11-25 09:07:21 +08:00
parent 1a5ff12eca
commit 14ee020e88
25 changed files with 1366 additions and 0 deletions

View File

@@ -47,6 +47,9 @@ type APIGroup struct {
WafSystemMonitorApi
WafCaServerInfoApi
WafSqlQueryApi
WafNotifyChannelApi
WafNotifySubscriptionApi
WafNotifyLogApi
}
var APIGroupAPP = new(APIGroup)

View File

@@ -11,6 +11,7 @@ import (
"SamWaf/model/common/response"
"SamWaf/model/request"
response2 "SamWaf/model/response"
"SamWaf/service/waf_service"
"SamWaf/utils"
"fmt"
"github.com/gin-gonic/gin"
@@ -152,6 +153,16 @@ func (w *WafLoginApi) LoginApi(c *gin.Context) {
}
global.GQEQUE_LOG_DB.Enqueue(&wafSysLog)
// 发送用户登录通知
go func() {
title, content := waf_service.WafNotifySenderServiceApp.FormatUserLoginMessage(
bean.LoginAccount,
clientIP,
time.Now().Format("2006-01-02 15:04:05"),
)
waf_service.WafNotifySenderServiceApp.SendNotification(model.MSG_TYPE_USER_LOGIN, title, content)
}()
response.OkWithDetailed(response2.LoginRep{
AccessToken: tokenInfo.AccessToken,
}, "登录成功", c)

116
api/waf_notify_channel.go Normal file
View File

@@ -0,0 +1,116 @@
package api
import (
"SamWaf/model/common/response"
"SamWaf/model/request"
"SamWaf/service/waf_service"
"errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type WafNotifyChannelApi struct{}
var wafNotifyChannelService = waf_service.WafNotifyChannelServiceApp
// AddApi 添加通知渠道
func (w *WafNotifyChannelApi) AddApi(c *gin.Context) {
var req request.WafNotifyChannelAddReq
err := c.ShouldBindJSON(&req)
if err == nil {
err = wafNotifyChannelService.CheckIsExistApi(req)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = wafNotifyChannelService.AddApi(req)
if err == nil {
response.OkWithMessage("添加成功", c)
} else {
response.FailWithMessage("添加失败", c)
}
return
} else {
response.FailWithMessage("当前通知渠道已经存在", c)
return
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetDetailApi 获取通知渠道详情
func (w *WafNotifyChannelApi) GetDetailApi(c *gin.Context) {
var req request.WafNotifyChannelDetailReq
err := c.ShouldBind(&req)
if err == nil {
bean := wafNotifyChannelService.GetDetailApi(req)
response.OkWithDetailed(bean, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetListApi 获取通知渠道列表
func (w *WafNotifyChannelApi) GetListApi(c *gin.Context) {
var req request.WafNotifyChannelSearchReq
err := c.ShouldBindJSON(&req)
if err == nil {
beans, total, _ := wafNotifyChannelService.GetListApi(req)
response.OkWithDetailed(response.PageResult{
List: beans,
Total: total,
PageIndex: req.PageIndex,
PageSize: req.PageSize,
}, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// DelApi 删除通知渠道
func (w *WafNotifyChannelApi) DelApi(c *gin.Context) {
var req request.WafNotifyChannelDelReq
err := c.ShouldBind(&req)
if err == nil {
err = wafNotifyChannelService.DelApi(req)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("请检测参数", c)
} else if err != nil {
response.FailWithMessage("发生错误", c)
} else {
response.OkWithMessage("删除成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// ModifyApi 修改通知渠道
func (w *WafNotifyChannelApi) ModifyApi(c *gin.Context) {
var req request.WafNotifyChannelEditReq
err := c.ShouldBindJSON(&req)
if err == nil {
err = wafNotifyChannelService.ModifyApi(req)
if err != nil {
response.FailWithMessage("编辑发生错误", c)
} else {
response.OkWithMessage("编辑成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// TestApi 测试通知渠道
func (w *WafNotifyChannelApi) TestApi(c *gin.Context) {
var req request.WafNotifyChannelTestReq
err := c.ShouldBindJSON(&req)
if err == nil {
err = wafNotifyChannelService.TestChannelApi(req)
if err != nil {
response.FailWithMessage("测试失败: "+err.Error(), c)
} else {
response.OkWithMessage("测试成功,请检查是否收到通知", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}

61
api/waf_notify_log.go Normal file
View File

@@ -0,0 +1,61 @@
package api
import (
"SamWaf/model/common/response"
"SamWaf/model/request"
"SamWaf/service/waf_service"
"errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type WafNotifyLogApi struct{}
var wafNotifyLogService = waf_service.WafNotifyLogServiceApp
// GetListApi 获取通知日志列表
func (w *WafNotifyLogApi) GetListApi(c *gin.Context) {
var req request.WafNotifyLogSearchReq
err := c.ShouldBindJSON(&req)
if err == nil {
beans, total, _ := wafNotifyLogService.GetListApi(req)
response.OkWithDetailed(response.PageResult{
List: beans,
Total: total,
PageIndex: req.PageIndex,
PageSize: req.PageSize,
}, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetDetailApi 获取通知日志详情
func (w *WafNotifyLogApi) GetDetailApi(c *gin.Context) {
var req request.WafNotifyLogDetailReq
err := c.ShouldBind(&req)
if err == nil {
bean := wafNotifyLogService.GetDetailApi(req)
response.OkWithDetailed(bean, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// DelApi 删除通知日志
func (w *WafNotifyLogApi) DelApi(c *gin.Context) {
var req request.WafNotifyLogDelReq
err := c.ShouldBind(&req)
if err == nil {
err = wafNotifyLogService.DelApi(req)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("请检测参数", c)
} else if err != nil {
response.FailWithMessage("发生错误", c)
} else {
response.OkWithMessage("删除成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}

View File

@@ -0,0 +1,100 @@
package api
import (
"SamWaf/model/common/response"
"SamWaf/model/request"
"SamWaf/service/waf_service"
"errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type WafNotifySubscriptionApi struct{}
var wafNotifySubscriptionService = waf_service.WafNotifySubscriptionServiceApp
// AddApi 添加通知订阅
func (w *WafNotifySubscriptionApi) AddApi(c *gin.Context) {
var req request.WafNotifySubscriptionAddReq
err := c.ShouldBindJSON(&req)
if err == nil {
err = wafNotifySubscriptionService.CheckIsExistApi(req)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = wafNotifySubscriptionService.AddApi(req)
if err == nil {
response.OkWithMessage("添加成功", c)
} else {
response.FailWithMessage("添加失败", c)
}
return
} else {
response.FailWithMessage("该渠道已订阅此消息类型", c)
return
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetDetailApi 获取通知订阅详情
func (w *WafNotifySubscriptionApi) GetDetailApi(c *gin.Context) {
var req request.WafNotifySubscriptionDetailReq
err := c.ShouldBind(&req)
if err == nil {
bean := wafNotifySubscriptionService.GetDetailApi(req)
response.OkWithDetailed(bean, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// GetListApi 获取通知订阅列表
func (w *WafNotifySubscriptionApi) GetListApi(c *gin.Context) {
var req request.WafNotifySubscriptionSearchReq
err := c.ShouldBindJSON(&req)
if err == nil {
beans, total, _ := wafNotifySubscriptionService.GetListApi(req)
response.OkWithDetailed(response.PageResult{
List: beans,
Total: total,
PageIndex: req.PageIndex,
PageSize: req.PageSize,
}, "获取成功", c)
} else {
response.FailWithMessage("解析失败", c)
}
}
// DelApi 删除通知订阅
func (w *WafNotifySubscriptionApi) DelApi(c *gin.Context) {
var req request.WafNotifySubscriptionDelReq
err := c.ShouldBind(&req)
if err == nil {
err = wafNotifySubscriptionService.DelApi(req)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
response.FailWithMessage("请检测参数", c)
} else if err != nil {
response.FailWithMessage("发生错误", c)
} else {
response.OkWithMessage("删除成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}
// ModifyApi 修改通知订阅
func (w *WafNotifySubscriptionApi) ModifyApi(c *gin.Context) {
var req request.WafNotifySubscriptionEditReq
err := c.ShouldBindJSON(&req)
if err == nil {
err = wafNotifySubscriptionService.ModifyApi(req)
if err != nil {
response.FailWithMessage("编辑发生错误", c)
} else {
response.OkWithMessage("编辑成功", c)
}
} else {
response.FailWithMessage("解析失败", c)
}
}

21
model/notify_channel.go Normal file
View File

@@ -0,0 +1,21 @@
package model
import (
"SamWaf/model/baseorm"
)
/*
*
通知渠道配置
*/
type NotifyChannel struct {
baseorm.BaseOrm
Name string `json:"name"` // 渠道名称
Type string `json:"type"` // 渠道类型dingtalk, feishu, wechat, email等
WebhookURL string `json:"webhook_url"` // Webhook地址
Secret string `json:"secret"` // 密钥(用于签名验证)
AccessToken string `json:"access_token"` // 访问令牌
ConfigJSON string `json:"config_json"` // 额外配置JSON格式
Status int `json:"status"` // 状态1启用0禁用
Remarks string `json:"remarks"` // 备注
}

22
model/notify_log.go Normal file
View File

@@ -0,0 +1,22 @@
package model
import (
"SamWaf/model/baseorm"
)
/*
*
通知发送日志
*/
type NotifyLog struct {
baseorm.BaseOrm
ChannelId string `json:"channel_id"` // 渠道ID
ChannelName string `json:"channel_name"` // 渠道名称
ChannelType string `json:"channel_type"` // 渠道类型
MessageType string `json:"message_type"` // 消息类型
MessageTitle string `json:"message_title"` // 消息标题
MessageContent string `json:"message_content"` // 消息内容
Status int `json:"status"` // 发送状态1成功0失败
ErrorMsg string `json:"error_msg"` // 错误信息
SendTime string `json:"send_time"` // 发送时间
}

View File

@@ -0,0 +1,28 @@
package model
import (
"SamWaf/model/baseorm"
)
/*
*
通知订阅配置
*/
type NotifySubscription struct {
baseorm.BaseOrm
ChannelId string `json:"channel_id"` // 关联的渠道ID
MessageType string `json:"message_type"` // 消息类型user_login, attack_info, weekly_report等
Status int `json:"status"` // 状态1启用0禁用
FilterJSON string `json:"filter_json"` // 过滤条件JSON格式
Remarks string `json:"remarks"` // 备注
}
// 消息类型常量
const (
MSG_TYPE_USER_LOGIN = "user_login" // 用户登录
MSG_TYPE_ATTACK_INFO = "attack_info" // 攻击信息
MSG_TYPE_WEEKLY_REPORT = "weekly_report" // 周报
MSG_TYPE_SSL_EXPIRE = "ssl_expire" // SSL证书过期
MSG_TYPE_SYSTEM_ERROR = "system_error" // 系统错误
MSG_TYPE_IP_BAN = "ip_ban" // IP封禁
)

View File

@@ -0,0 +1,50 @@
package request
// 添加通知渠道请求
type WafNotifyChannelAddReq struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
WebhookURL string `json:"webhook_url"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
ConfigJSON string `json:"config_json"`
Status int `json:"status"`
Remarks string `json:"remarks"`
}
// 编辑通知渠道请求
type WafNotifyChannelEditReq struct {
Id string `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
WebhookURL string `json:"webhook_url"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
ConfigJSON string `json:"config_json"`
Status int `json:"status"`
Remarks string `json:"remarks"`
}
// 查询通知渠道详情请求
type WafNotifyChannelDetailReq struct {
Id string `form:"id" binding:"required"`
}
// 搜索通知渠道请求
type WafNotifyChannelSearchReq struct {
PageIndex int `json:"pageIndex"`
PageSize int `json:"pageSize"`
Name string `json:"name"`
Type string `json:"type"`
Status int `json:"status"`
}
// 删除通知渠道请求
type WafNotifyChannelDelReq struct {
Id string `form:"id" binding:"required"`
}
// 测试通知渠道请求
type WafNotifyChannelTestReq struct {
Id string `json:"id" binding:"required"`
}

View File

@@ -0,0 +1,22 @@
package request
// 搜索通知日志请求
type WafNotifyLogSearchReq struct {
PageIndex int `json:"pageIndex"`
PageSize int `json:"pageSize"`
ChannelId string `json:"channel_id"`
MessageType string `json:"message_type"`
Status int `json:"status"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
// 删除通知日志请求
type WafNotifyLogDelReq struct {
Id string `form:"id" binding:"required"`
}
// 查询通知日志详情请求
type WafNotifyLogDetailReq struct {
Id string `form:"id" binding:"required"`
}

View File

@@ -0,0 +1,39 @@
package request
// 添加通知订阅请求
type WafNotifySubscriptionAddReq struct {
ChannelId string `json:"channel_id" binding:"required"`
MessageType string `json:"message_type" binding:"required"`
Status int `json:"status"`
FilterJSON string `json:"filter_json"`
Remarks string `json:"remarks"`
}
// 编辑通知订阅请求
type WafNotifySubscriptionEditReq struct {
Id string `json:"id" binding:"required"`
ChannelId string `json:"channel_id" binding:"required"`
MessageType string `json:"message_type" binding:"required"`
Status int `json:"status"`
FilterJSON string `json:"filter_json"`
Remarks string `json:"remarks"`
}
// 查询通知订阅详情请求
type WafNotifySubscriptionDetailReq struct {
Id string `form:"id" binding:"required"`
}
// 搜索通知订阅请求
type WafNotifySubscriptionSearchReq struct {
PageIndex int `json:"pageIndex"`
PageSize int `json:"pageSize"`
ChannelId string `json:"channel_id"`
MessageType string `json:"message_type"`
Status int `json:"status"`
}
// 删除通知订阅请求
type WafNotifySubscriptionDelReq struct {
Id string `form:"id" binding:"required"`
}

View File

@@ -45,6 +45,9 @@ type ApiGroup struct {
WafSystemMonitorRouter
WafCaServerInfoRouter
SqlQueryRouter
NotifyChannelRouter
NotifySubscriptionRouter
NotifyLogRouter
}
type PublicApiGroup struct {
LoginRouter

View File

@@ -0,0 +1,20 @@
package router
import (
"SamWaf/api"
"github.com/gin-gonic/gin"
)
type NotifyChannelRouter struct {
}
func (receiver *NotifyChannelRouter) InitNotifyChannelRouter(group *gin.RouterGroup) {
api := api.APIGroupAPP.WafNotifyChannelApi
router := group.Group("")
router.POST("/samwaf/notify/channel/list", api.GetListApi)
router.GET("/samwaf/notify/channel/detail", api.GetDetailApi)
router.POST("/samwaf/notify/channel/add", api.AddApi)
router.GET("/samwaf/notify/channel/del", api.DelApi)
router.POST("/samwaf/notify/channel/edit", api.ModifyApi)
router.POST("/samwaf/notify/channel/test", api.TestApi)
}

17
router/waf_notify_log.go Normal file
View File

@@ -0,0 +1,17 @@
package router
import (
"SamWaf/api"
"github.com/gin-gonic/gin"
)
type NotifyLogRouter struct {
}
func (receiver *NotifyLogRouter) InitNotifyLogRouter(group *gin.RouterGroup) {
api := api.APIGroupAPP.WafNotifyLogApi
router := group.Group("")
router.POST("/samwaf/notify/log/list", api.GetListApi)
router.GET("/samwaf/notify/log/detail", api.GetDetailApi)
router.GET("/samwaf/notify/log/del", api.DelApi)
}

View File

@@ -0,0 +1,19 @@
package router
import (
"SamWaf/api"
"github.com/gin-gonic/gin"
)
type NotifySubscriptionRouter struct {
}
func (receiver *NotifySubscriptionRouter) InitNotifySubscriptionRouter(group *gin.RouterGroup) {
api := api.APIGroupAPP.WafNotifySubscriptionApi
router := group.Group("")
router.POST("/samwaf/notify/subscription/list", api.GetListApi)
router.GET("/samwaf/notify/subscription/detail", api.GetDetailApi)
router.POST("/samwaf/notify/subscription/add", api.AddApi)
router.GET("/samwaf/notify/subscription/del", api.DelApi)
router.POST("/samwaf/notify/subscription/edit", api.ModifyApi)
}

View File

@@ -0,0 +1,151 @@
package waf_service
import (
"SamWaf/common/uuid"
"SamWaf/customtype"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/request"
"SamWaf/wafnotify/dingtalk"
"SamWaf/wafnotify/feishu"
"errors"
"time"
)
type WafNotifyChannelService struct{}
var WafNotifyChannelServiceApp = new(WafNotifyChannelService)
// AddApi 添加通知渠道
func (receiver *WafNotifyChannelService) AddApi(req request.WafNotifyChannelAddReq) error {
var bean = &model.NotifyChannel{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
Name: req.Name,
Type: req.Type,
WebhookURL: req.WebhookURL,
Secret: req.Secret,
AccessToken: req.AccessToken,
ConfigJSON: req.ConfigJSON,
Status: req.Status,
Remarks: req.Remarks,
}
return global.GWAF_LOCAL_DB.Create(bean).Error
}
// CheckIsExistApi 检查是否存在
func (receiver *WafNotifyChannelService) CheckIsExistApi(req request.WafNotifyChannelAddReq) error {
return global.GWAF_LOCAL_DB.First(&model.NotifyChannel{}, "name = ? ", req.Name).Error
}
// ModifyApi 修改通知渠道
func (receiver *WafNotifyChannelService) ModifyApi(req request.WafNotifyChannelEditReq) error {
editMap := map[string]interface{}{
"Name": req.Name,
"Type": req.Type,
"WebhookURL": req.WebhookURL,
"Secret": req.Secret,
"AccessToken": req.AccessToken,
"ConfigJSON": req.ConfigJSON,
"Status": req.Status,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
return global.GWAF_LOCAL_DB.Model(model.NotifyChannel{}).Where("id = ?", req.Id).Updates(editMap).Error
}
// GetDetailApi 获取详情
func (receiver *WafNotifyChannelService) GetDetailApi(req request.WafNotifyChannelDetailReq) model.NotifyChannel {
var bean model.NotifyChannel
global.GWAF_LOCAL_DB.Where("id=?", req.Id).Find(&bean)
return bean
}
// GetListApi 获取列表
func (receiver *WafNotifyChannelService) GetListApi(req request.WafNotifyChannelSearchReq) ([]model.NotifyChannel, int64, error) {
var list []model.NotifyChannel
var total int64 = 0
var whereField = ""
var whereValues []interface{}
if len(req.Name) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " name like ? "
whereValues = append(whereValues, "%"+req.Name+"%")
}
if len(req.Type) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " type = ? "
whereValues = append(whereValues, req.Type)
}
if req.Status > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " status = ? "
whereValues = append(whereValues, req.Status)
}
global.GWAF_LOCAL_DB.Model(&model.NotifyChannel{}).Where(whereField, whereValues...).Limit(req.PageSize).Offset(req.PageSize * (req.PageIndex - 1)).Find(&list)
global.GWAF_LOCAL_DB.Model(&model.NotifyChannel{}).Where(whereField, whereValues...).Count(&total)
return list, total, nil
}
// DelApi 删除
func (receiver *WafNotifyChannelService) DelApi(req request.WafNotifyChannelDelReq) error {
var bean model.NotifyChannel
err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&bean).Error
if err != nil {
return err
}
// 删除关联的订阅
global.GWAF_LOCAL_DB.Where("channel_id = ?", req.Id).Delete(&model.NotifySubscription{})
// 删除渠道
return global.GWAF_LOCAL_DB.Where("id = ?", req.Id).Delete(&model.NotifyChannel{}).Error
}
// TestChannelApi 测试通知渠道
func (receiver *WafNotifyChannelService) TestChannelApi(req request.WafNotifyChannelTestReq) error {
var channel model.NotifyChannel
err := global.GWAF_LOCAL_DB.Where("id = ?", req.Id).First(&channel).Error
if err != nil {
return errors.New("通知渠道不存在")
}
title := "SamWAF 测试通知"
content := "这是一条测试消息,发送时间:" + time.Now().Format("2006-01-02 15:04:05")
switch channel.Type {
case "dingtalk":
notifier := dingtalk.NewDingTalkNotifier(channel.WebhookURL, channel.Secret)
return notifier.SendMarkdown(title, content)
case "feishu":
notifier := feishu.NewFeishuNotifier(channel.WebhookURL, channel.Secret)
return notifier.SendMarkdown(title, content)
default:
return errors.New("不支持的通知类型")
}
}
// GetAllChannels 获取所有启用的通知渠道
func (receiver *WafNotifyChannelService) GetAllChannels() []model.NotifyChannel {
var channels []model.NotifyChannel
global.GWAF_LOCAL_DB.Where("status = ?", 1).Find(&channels)
return channels
}

View File

@@ -0,0 +1,96 @@
package waf_service
import (
"SamWaf/common/uuid"
"SamWaf/customtype"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/request"
"time"
)
type WafNotifyLogService struct{}
var WafNotifyLogServiceApp = new(WafNotifyLogService)
// AddLog 添加通知日志
func (receiver *WafNotifyLogService) AddLog(channelId, channelName, channelType, messageType, messageTitle, messageContent string, status int, errorMsg string) error {
var bean = &model.NotifyLog{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
ChannelId: channelId,
ChannelName: channelName,
ChannelType: channelType,
MessageType: messageType,
MessageTitle: messageTitle,
MessageContent: messageContent,
Status: status,
ErrorMsg: errorMsg,
SendTime: time.Now().Format("2006-01-02 15:04:05"),
}
return global.GWAF_LOCAL_LOG_DB.Create(bean).Error
}
// GetListApi 获取列表
func (receiver *WafNotifyLogService) GetListApi(req request.WafNotifyLogSearchReq) ([]model.NotifyLog, int64, error) {
var list []model.NotifyLog
var total int64 = 0
var whereField = ""
var whereValues []interface{}
if len(req.ChannelId) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " channel_id = ? "
whereValues = append(whereValues, req.ChannelId)
}
if len(req.MessageType) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " message_type = ? "
whereValues = append(whereValues, req.MessageType)
}
if req.Status > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " status = ? "
whereValues = append(whereValues, req.Status)
}
if len(req.StartTime) > 0 && len(req.EndTime) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " send_time >= ? and send_time <= ? "
whereValues = append(whereValues, req.StartTime, req.EndTime)
}
global.GWAF_LOCAL_LOG_DB.Model(&model.NotifyLog{}).Where(whereField, whereValues...).Order("create_time desc").Limit(req.PageSize).Offset(req.PageSize * (req.PageIndex - 1)).Find(&list)
global.GWAF_LOCAL_LOG_DB.Model(&model.NotifyLog{}).Where(whereField, whereValues...).Count(&total)
return list, total, nil
}
// GetDetailApi 获取详情
func (receiver *WafNotifyLogService) GetDetailApi(req request.WafNotifyLogDetailReq) model.NotifyLog {
var bean model.NotifyLog
global.GWAF_LOCAL_LOG_DB.Where("id=?", req.Id).Find(&bean)
return bean
}
// DelApi 删除
func (receiver *WafNotifyLogService) DelApi(req request.WafNotifyLogDelReq) error {
return global.GWAF_LOCAL_LOG_DB.Where("id = ?", req.Id).Delete(&model.NotifyLog{}).Error
}

View File

@@ -0,0 +1,128 @@
package waf_service
import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/model"
"SamWaf/wafnotify/dingtalk"
"SamWaf/wafnotify/feishu"
"fmt"
)
type WafNotifySenderService struct{}
var WafNotifySenderServiceApp = new(WafNotifySenderService)
// SendNotification 发送通知
func (receiver *WafNotifySenderService) SendNotification(messageType, title, content string) {
// 获取订阅
subscriptions := WafNotifySubscriptionServiceApp.GetSubscriptionsByMessageType(messageType)
if len(subscriptions) == 0 {
zlog.Debug(fmt.Sprintf("没有找到消息类型 %s 的订阅", messageType))
return
}
// 遍历订阅,发送通知
for _, subscription := range subscriptions {
// 获取渠道信息
var channel model.NotifyChannel
err := receiver.getChannelById(subscription.ChannelId, &channel)
if err != nil {
zlog.Error(fmt.Sprintf("获取渠道信息失败: %v", err))
continue
}
// 发送通知
go receiver.sendToChannel(channel, messageType, title, content)
}
}
// getChannelById 根据ID获取渠道
func (receiver *WafNotifySenderService) getChannelById(channelId string, channel *model.NotifyChannel) error {
return global.GWAF_LOCAL_DB.Where("id = ? and status = ?", channelId, 1).First(channel).Error
}
// sendToChannel 发送到具体渠道
func (receiver *WafNotifySenderService) sendToChannel(channel model.NotifyChannel, messageType, title, content string) {
var err error
status := 1
errorMsg := ""
switch channel.Type {
case "dingtalk":
notifier := dingtalk.NewDingTalkNotifier(channel.WebhookURL, channel.Secret)
err = notifier.SendMarkdown(title, content)
case "feishu":
notifier := feishu.NewFeishuNotifier(channel.WebhookURL, channel.Secret)
err = notifier.SendMarkdown(title, content)
default:
err = fmt.Errorf("不支持的通知类型: %s", channel.Type)
}
if err != nil {
status = 0
errorMsg = err.Error()
zlog.Error(fmt.Sprintf("发送通知失败: %v", err))
}
// 记录日志
logErr := WafNotifyLogServiceApp.AddLog(
channel.Id,
channel.Name,
channel.Type,
messageType,
title,
content,
status,
errorMsg,
)
if logErr != nil {
zlog.Error(fmt.Sprintf("记录通知日志失败: %v", logErr))
}
}
// FormatUserLoginMessage 格式化用户登录消息
func (receiver *WafNotifySenderService) FormatUserLoginMessage(username, ip, time string) (string, string) {
title := "用户登录通知"
content := fmt.Sprintf("**用户:** %s\n\n**IP地址:** %s\n\n**登录时间:** %s", username, ip, time)
return title, content
}
// FormatAttackInfoMessage 格式化攻击信息消息
func (receiver *WafNotifySenderService) FormatAttackInfoMessage(attackType, url, ip, time string) (string, string) {
title := "攻击告警通知"
content := fmt.Sprintf("**攻击类型:** %s\n\n**URL:** %s\n\n**攻击IP:** %s\n\n**攻击时间:** %s", attackType, url, ip, time)
return title, content
}
// FormatWeeklyReportMessage 格式化周报消息
func (receiver *WafNotifySenderService) FormatWeeklyReportMessage(totalRequests, blockedRequests int64, weekRange string) (string, string) {
title := "WAF周报"
content := fmt.Sprintf("**周期:** %s\n\n**总请求数:** %d\n\n**拦截请求数:** %d\n\n**拦截率:** %.2f%%",
weekRange,
totalRequests,
blockedRequests,
float64(blockedRequests)/float64(totalRequests)*100)
return title, content
}
// FormatSSLExpireMessage 格式化SSL证书过期消息
func (receiver *WafNotifySenderService) FormatSSLExpireMessage(domain string, expireTime string, daysLeft int) (string, string) {
title := "SSL证书即将过期通知"
content := fmt.Sprintf("**域名:** %s\n\n**过期时间:** %s\n\n**剩余天数:** %d天", domain, expireTime, daysLeft)
return title, content
}
// FormatSystemErrorMessage 格式化系统错误消息
func (receiver *WafNotifySenderService) FormatSystemErrorMessage(errorType, errorMsg, time string) (string, string) {
title := "系统错误通知"
content := fmt.Sprintf("**错误类型:** %s\n\n**错误信息:** %s\n\n**发生时间:** %s", errorType, errorMsg, time)
return title, content
}
// FormatIPBanMessage 格式化IP封禁消息
func (receiver *WafNotifySenderService) FormatIPBanMessage(ip, reason, time string, duration int) (string, string) {
title := "IP封禁通知"
content := fmt.Sprintf("**IP地址:** %s\n\n**封禁原因:** %s\n\n**封禁时长:** %d分钟\n\n**封禁时间:** %s", ip, reason, duration, time)
return title, content
}

View File

@@ -0,0 +1,109 @@
package waf_service
import (
"SamWaf/common/uuid"
"SamWaf/customtype"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/request"
"time"
)
type WafNotifySubscriptionService struct{}
var WafNotifySubscriptionServiceApp = new(WafNotifySubscriptionService)
// AddApi 添加通知订阅
func (receiver *WafNotifySubscriptionService) AddApi(req request.WafNotifySubscriptionAddReq) error {
var bean = &model.NotifySubscription{
BaseOrm: baseorm.BaseOrm{
Id: uuid.GenUUID(),
USER_CODE: global.GWAF_USER_CODE,
Tenant_ID: global.GWAF_TENANT_ID,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
},
ChannelId: req.ChannelId,
MessageType: req.MessageType,
Status: req.Status,
FilterJSON: req.FilterJSON,
Remarks: req.Remarks,
}
return global.GWAF_LOCAL_DB.Create(bean).Error
}
// CheckIsExistApi 检查是否存在
func (receiver *WafNotifySubscriptionService) CheckIsExistApi(req request.WafNotifySubscriptionAddReq) error {
return global.GWAF_LOCAL_DB.First(&model.NotifySubscription{}, "channel_id = ? and message_type = ? ", req.ChannelId, req.MessageType).Error
}
// ModifyApi 修改通知订阅
func (receiver *WafNotifySubscriptionService) ModifyApi(req request.WafNotifySubscriptionEditReq) error {
editMap := map[string]interface{}{
"ChannelId": req.ChannelId,
"MessageType": req.MessageType,
"Status": req.Status,
"FilterJSON": req.FilterJSON,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
return global.GWAF_LOCAL_DB.Model(model.NotifySubscription{}).Where("id = ?", req.Id).Updates(editMap).Error
}
// GetDetailApi 获取详情
func (receiver *WafNotifySubscriptionService) GetDetailApi(req request.WafNotifySubscriptionDetailReq) model.NotifySubscription {
var bean model.NotifySubscription
global.GWAF_LOCAL_DB.Where("id=?", req.Id).Find(&bean)
return bean
}
// GetListApi 获取列表
func (receiver *WafNotifySubscriptionService) GetListApi(req request.WafNotifySubscriptionSearchReq) ([]model.NotifySubscription, int64, error) {
var list []model.NotifySubscription
var total int64 = 0
var whereField = ""
var whereValues []interface{}
if len(req.ChannelId) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " channel_id = ? "
whereValues = append(whereValues, req.ChannelId)
}
if len(req.MessageType) > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " message_type = ? "
whereValues = append(whereValues, req.MessageType)
}
if req.Status > 0 {
if len(whereField) > 0 {
whereField = whereField + " and "
}
whereField = whereField + " status = ? "
whereValues = append(whereValues, req.Status)
}
global.GWAF_LOCAL_DB.Model(&model.NotifySubscription{}).Where(whereField, whereValues...).Limit(req.PageSize).Offset(req.PageSize * (req.PageIndex - 1)).Find(&list)
global.GWAF_LOCAL_DB.Model(&model.NotifySubscription{}).Where(whereField, whereValues...).Count(&total)
return list, total, nil
}
// DelApi 删除
func (receiver *WafNotifySubscriptionService) DelApi(req request.WafNotifySubscriptionDelReq) error {
return global.GWAF_LOCAL_DB.Where("id = ?", req.Id).Delete(&model.NotifySubscription{}).Error
}
// GetSubscriptionsByMessageType 根据消息类型获取订阅
func (receiver *WafNotifySubscriptionService) GetSubscriptionsByMessageType(messageType string) []model.NotifySubscription {
var subscriptions []model.NotifySubscription
global.GWAF_LOCAL_DB.Where("message_type = ? and status = ?", messageType, 1).Find(&subscriptions)
return subscriptions
}

View File

@@ -164,6 +164,29 @@ func RunCoreDBMigrations(db *gorm.DB) error {
return dropCoreIndexes(tx)
},
},
// 迁移3: 创建通知管理相关表
{
ID: "202511240001_add_notify_tables",
Migrate: func(tx *gorm.DB) error {
zlog.Info("迁移 202511240001: 创建通知管理表")
// 创建通知渠道和订阅表
if err := tx.AutoMigrate(
&model.NotifyChannel{},
&model.NotifySubscription{},
); err != nil {
return fmt.Errorf("创建通知管理表失败: %w", err)
}
zlog.Info("通知管理表创建成功")
return nil
},
Rollback: func(tx *gorm.DB) error {
zlog.Info("回滚 202511240001: 删除通知管理表")
return tx.Migrator().DropTable(
&model.NotifyChannel{},
&model.NotifySubscription{},
)
},
},
})
// 执行迁移

View File

@@ -87,6 +87,27 @@ func RunLogDBMigrations(db *gorm.DB) error {
return dropLogIndexes(tx)
},
},
// 迁移3: 创建通知日志表
{
ID: "202511240001_add_notify_log_table",
Migrate: func(tx *gorm.DB) error {
zlog.Info("迁移 202511240001: 创建通知日志表")
// 创建通知日志表
if err := tx.AutoMigrate(
&model.NotifyLog{},
); err != nil {
return fmt.Errorf("创建通知日志表失败: %w", err)
}
zlog.Info("通知日志表创建成功")
return nil
},
Rollback: func(tx *gorm.DB) error {
zlog.Info("回滚 202511240001: 删除通知日志表")
return tx.Migrator().DropTable(
&model.NotifyLog{},
)
},
},
})
// 执行迁移

View File

@@ -87,6 +87,9 @@ func (web *WafWebManager) initRouter(r *gin.Engine) {
router.ApiGroupApp.InitWafSystemMonitorRouter(RouterGroup)
router.ApiGroupApp.InitWafCaServerInfoRouter(RouterGroup)
router.ApiGroupApp.InitSqlQueryRouter(RouterGroup)
router.ApiGroupApp.InitNotifyChannelRouter(RouterGroup)
router.ApiGroupApp.InitNotifySubscriptionRouter(RouterGroup)
router.ApiGroupApp.InitNotifyLogRouter(RouterGroup)
}
if global.GWAF_RELEASE == "true" {

View File

@@ -0,0 +1,134 @@
package dingtalk
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"
)
// DingTalkNotifier 钉钉通知器
type DingTalkNotifier struct {
WebhookURL string
Secret string
}
// NewDingTalkNotifier 创建钉钉通知器
func NewDingTalkNotifier(webhookURL, secret string) *DingTalkNotifier {
return &DingTalkNotifier{
WebhookURL: webhookURL,
Secret: secret,
}
}
// DingTalkMessage 钉钉消息结构
type DingTalkMessage struct {
MsgType string `json:"msgtype"`
Markdown map[string]interface{} `json:"markdown,omitempty"`
Text map[string]interface{} `json:"text,omitempty"`
}
// SendMarkdown 发送Markdown消息
func (d *DingTalkNotifier) SendMarkdown(title, content string) error {
message := DingTalkMessage{
MsgType: "markdown",
Markdown: map[string]interface{}{
"title": title,
"text": content,
},
}
return d.send(message)
}
// SendText 发送文本消息
func (d *DingTalkNotifier) SendText(content string) error {
message := DingTalkMessage{
MsgType: "text",
Text: map[string]interface{}{
"content": content,
},
}
return d.send(message)
}
// send 发送消息
func (d *DingTalkNotifier) send(message DingTalkMessage) error {
// 构建URL包含签名
urlWithSign, err := d.buildURL()
if err != nil {
return fmt.Errorf("构建URL失败: %v", err)
}
// 序列化消息
payload, err := json.Marshal(message)
if err != nil {
return fmt.Errorf("序列化消息失败: %v", err)
}
// 发送HTTP请求
resp, err := http.Post(urlWithSign, "application/json", bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
// 检查响应码
if errCode, ok := result["errcode"].(float64); ok && errCode != 0 {
return fmt.Errorf("钉钉返回错误: %v", result["errmsg"])
}
return nil
}
// buildURL 构建包含签名的URL
func (d *DingTalkNotifier) buildURL() (string, error) {
if d.Secret == "" {
return d.WebhookURL, nil
}
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
sign, err := d.sign(timestamp)
if err != nil {
return "", err
}
u, err := url.Parse(d.WebhookURL)
if err != nil {
return "", err
}
query := u.Query()
query.Set("timestamp", timestamp)
query.Set("sign", sign)
u.RawQuery = query.Encode()
return u.String(), nil
}
// sign 计算签名
func (d *DingTalkNotifier) sign(timestamp string) (string, error) {
stringToSign := timestamp + "\n" + d.Secret
h := hmac.New(sha256.New, []byte(d.Secret))
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return url.QueryEscape(signature), nil
}

152
wafnotify/feishu/feishu.go Normal file
View File

@@ -0,0 +1,152 @@
package feishu
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
// FeishuNotifier 飞书通知器
type FeishuNotifier struct {
WebhookURL string
Secret string
}
// NewFeishuNotifier 创建飞书通知器
func NewFeishuNotifier(webhookURL, secret string) *FeishuNotifier {
return &FeishuNotifier{
WebhookURL: webhookURL,
Secret: secret,
}
}
// FeishuMessage 飞书消息结构
type FeishuMessage struct {
Timestamp string `json:"timestamp,omitempty"`
Sign string `json:"sign,omitempty"`
MsgType string `json:"msg_type"`
Content map[string]interface{} `json:"content,omitempty"`
Card map[string]interface{} `json:"card,omitempty"`
}
// SendText 发送文本消息
func (f *FeishuNotifier) SendText(content string) error {
message := FeishuMessage{
MsgType: "text",
Content: map[string]interface{}{
"text": content,
},
}
return f.send(message)
}
// SendRichText 发送富文本消息
func (f *FeishuNotifier) SendRichText(title, content string) error {
message := FeishuMessage{
MsgType: "post",
Content: map[string]interface{}{
"post": map[string]interface{}{
"zh_cn": map[string]interface{}{
"title": title,
"content": [][]map[string]interface{}{
{
{
"tag": "text",
"text": content,
},
},
},
},
},
},
}
return f.send(message)
}
// SendMarkdown 发送Markdown消息交互式卡片
func (f *FeishuNotifier) SendMarkdown(title, content string) error {
message := FeishuMessage{
MsgType: "interactive",
Card: map[string]interface{}{
"config": map[string]interface{}{
"wide_screen_mode": true,
},
"header": map[string]interface{}{
"title": map[string]interface{}{
"tag": "plain_text",
"content": title,
},
"template": "blue",
},
"elements": []map[string]interface{}{
{
"tag": "markdown",
"content": content,
},
},
},
}
return f.send(message)
}
// send 发送消息
func (f *FeishuNotifier) send(message FeishuMessage) error {
// 如果有密钥,添加签名
if f.Secret != "" {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
sign, err := f.sign(timestamp)
if err != nil {
return fmt.Errorf("生成签名失败: %v", err)
}
message.Timestamp = timestamp
message.Sign = sign
}
// 序列化消息
payload, err := json.Marshal(message)
if err != nil {
return fmt.Errorf("序列化消息失败: %v", err)
}
// 发送HTTP请求
resp, err := http.Post(f.WebhookURL, "application/json", bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("发送HTTP请求失败: %v", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %v", err)
}
// 解析响应
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
// 检查响应码
if code, ok := result["code"].(float64); ok && code != 0 {
return fmt.Errorf("飞书返回错误: %v", result["msg"])
}
return nil
}
// sign 计算签名
func (f *FeishuNotifier) sign(timestamp string) (string, error) {
stringToSign := timestamp + "\n" + f.Secret
h := hmac.New(sha256.New, []byte(stringToSign))
h.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
return signature, nil
}

View File

@@ -4,6 +4,8 @@ import (
"SamWaf/common/zlog"
"SamWaf/global"
"SamWaf/innerbean"
"SamWaf/model"
"SamWaf/service/waf_service"
"SamWaf/wafipban"
"SamWaf/waftask"
"strconv"
@@ -60,6 +62,21 @@ func ProcessLogDequeEngine() {
}
}
}
// 检查攻击日志并发送通知
for _, log := range webLogArray {
if log.RULE != "" && log.STATUS_CODE >= 400 {
// 异步发送攻击通知
go func(attackLog *innerbean.WebLog) {
title, content := waf_service.WafNotifySenderServiceApp.FormatAttackInfoMessage(
attackLog.RULE,
attackLog.URL,
attackLog.SRC_IP,
attackLog.CREATE_TIME,
)
waf_service.WafNotifySenderServiceApp.SendNotification(model.MSG_TYPE_ATTACK_INFO, title, content)
}(log)
}
}
if global.GCONFIG_LOG_PERSIST_ENABLED == 1 {
global.GWAF_LOCAL_LOG_DB.CreateInBatches(webLogArray, len(webLogArray))
}