mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-16 00:47:29 +02:00
228 lines
7.6 KiB
Go
228 lines
7.6 KiB
Go
package handler
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"cyberstrike-ai/internal/database"
|
||
"cyberstrike-ai/internal/robot"
|
||
"cyberstrike-ai/internal/security"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
type updateVulnerabilityAlertRequest struct {
|
||
Enabled bool `json:"enabled"`
|
||
MinSeverity string `json:"min_severity"`
|
||
}
|
||
|
||
func (h *VulnerabilityHandler) GetMyAlertSubscription(c *gin.Context) {
|
||
session, ok := security.CurrentSession(c)
|
||
if !ok {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||
return
|
||
}
|
||
sub, err := h.db.GetVulnerabilityAlertSubscription(session.UserID)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
bindings, _ := h.db.ListRobotUserBindings(session.UserID)
|
||
deliveryReady := false
|
||
for _, binding := range bindings {
|
||
if binding.Enabled && robot.SupportsProactive(binding.Platform) {
|
||
deliveryReady = true
|
||
break
|
||
}
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"subscription": sub, "bindings": bindings, "delivery_ready": deliveryReady})
|
||
}
|
||
|
||
func (h *VulnerabilityHandler) UpdateMyAlertSubscription(c *gin.Context) {
|
||
session, ok := security.CurrentSession(c)
|
||
if !ok {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
||
return
|
||
}
|
||
var req updateVulnerabilityAlertRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
sub, err := h.db.UpsertVulnerabilityAlertSubscription(session.UserID, req.Enabled, req.MinSeverity)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||
return
|
||
}
|
||
if h.audit != nil {
|
||
h.audit.RecordOK(c, "vulnerability", "alert_subscription_update", "更新漏洞提醒订阅", "user", session.UserID, map[string]interface{}{
|
||
"enabled": sub.Enabled, "min_severity": sub.MinSeverity,
|
||
})
|
||
}
|
||
c.JSON(http.StatusOK, sub)
|
||
}
|
||
|
||
func vulnerabilityAlertSeverityLabel(value string) string {
|
||
switch value {
|
||
case "critical":
|
||
return "严重"
|
||
case "high":
|
||
return "高危"
|
||
case "medium":
|
||
return "中危"
|
||
case "low":
|
||
return "低危"
|
||
default:
|
||
return "信息"
|
||
}
|
||
}
|
||
|
||
func formatVulnerabilityRobotAlert(v *database.Vulnerability) string {
|
||
return fmt.Sprintf("🚨 新漏洞提醒\n\n标题:%s\n严重程度:%s(%s)\n目标:%s\n影响:%s\n修复建议:%s\n漏洞 ID:%s\n\n请核实目标版本与实际暴露情况。",
|
||
strings.TrimSpace(v.Title), vulnerabilityAlertSeverityLabel(v.Severity), v.Severity,
|
||
fallbackAlertText(v.Target), fallbackAlertText(v.Impact), fallbackAlertText(v.Recommendation), v.ID)
|
||
}
|
||
|
||
func fallbackAlertText(value string) string {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
return "未填写"
|
||
}
|
||
if len([]rune(value)) > 300 {
|
||
return string([]rune(value)[:300]) + "…"
|
||
}
|
||
return value
|
||
}
|
||
|
||
// NotifyNewVulnerability pushes an alert to every eligible bound robot identity.
|
||
// A failure on one platform never blocks vulnerability creation or other recipients.
|
||
func (h *RobotHandler) NotifyNewVulnerability(v *database.Vulnerability) {
|
||
if h == nil || h.db == nil || v == nil {
|
||
return
|
||
}
|
||
recipients, err := h.db.ListVulnerabilityAlertRecipients(v)
|
||
if err != nil {
|
||
h.logger.Warn("查询漏洞提醒接收人失败", zap.String("vulnerability_id", v.ID), zap.Error(err))
|
||
return
|
||
}
|
||
eligible := recipients[:0]
|
||
for _, recipient := range recipients {
|
||
if robot.SupportsProactive(recipient.Platform) {
|
||
eligible = append(eligible, recipient)
|
||
}
|
||
}
|
||
if err := h.db.EnqueueVulnerabilityAlertDeliveries(v.ID, eligible); err != nil {
|
||
h.logger.Warn("写入漏洞提醒投递队列失败", zap.String("vulnerability_id", v.ID), zap.Error(err))
|
||
return
|
||
}
|
||
select {
|
||
case h.alertWake <- struct{}{}:
|
||
default:
|
||
}
|
||
}
|
||
|
||
// RunVulnerabilityAlertWorker drains the durable outbox. Failed sends use
|
||
// exponential backoff and remain retryable across application restarts.
|
||
func (h *RobotHandler) RunVulnerabilityAlertWorker(ctx context.Context) {
|
||
if h == nil || h.db == nil {
|
||
return
|
||
}
|
||
ticker := time.NewTicker(30 * time.Second)
|
||
defer ticker.Stop()
|
||
for {
|
||
h.processVulnerabilityAlertDeliveries(ctx)
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
case <-h.alertWake:
|
||
}
|
||
}
|
||
}
|
||
|
||
func (h *RobotHandler) processVulnerabilityAlertDeliveries(ctx context.Context) {
|
||
deliveries, err := h.db.ListDueVulnerabilityAlertDeliveries(50)
|
||
if err != nil {
|
||
h.logger.Warn("读取漏洞提醒投递队列失败", zap.Error(err))
|
||
return
|
||
}
|
||
for _, delivery := range deliveries {
|
||
sendCtx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||
err := robot.SendProactive(sendCtx, h.config.Robots, delivery.Platform, delivery.ExternalUserID, formatVulnerabilityRobotAlert(delivery.Vulnerability))
|
||
cancel()
|
||
if err != nil {
|
||
attempts := delivery.Attempts + 1
|
||
_ = h.db.MarkVulnerabilityAlertDeliveryFailed(delivery.ID, attempts, err)
|
||
h.logger.Warn("发送漏洞机器人提醒失败", zap.String("platform", delivery.Platform), zap.String("user_id", delivery.UserID), zap.String("vulnerability_id", delivery.Vulnerability.ID), zap.Int("attempt", attempts), zap.Error(err))
|
||
continue
|
||
}
|
||
_ = h.db.MarkVulnerabilityAlertDeliverySent(delivery.ID)
|
||
h.logger.Info("漏洞机器人提醒已发送", zap.String("platform", delivery.Platform), zap.String("user_id", delivery.UserID), zap.String("vulnerability_id", delivery.Vulnerability.ID))
|
||
}
|
||
}
|
||
|
||
func (h *RobotHandler) cmdVulnerabilityAlerts(platform, externalUserID, arg string) string {
|
||
access, err := h.resolveRobotAccess(platform, externalUserID)
|
||
if err != nil {
|
||
return h.robotAccessDeniedMessage(platform)
|
||
}
|
||
arg = strings.ToLower(strings.TrimSpace(arg))
|
||
if arg == "" || arg == "status" || arg == "状态" {
|
||
sub, err := h.db.GetVulnerabilityAlertSubscription(access.User.ID)
|
||
if err != nil {
|
||
return "读取漏洞提醒设置失败,请稍后重试。"
|
||
}
|
||
state := "已关闭"
|
||
if sub.Enabled {
|
||
state = "已开启"
|
||
}
|
||
return fmt.Sprintf("漏洞提醒%s;最低提醒级别:%s(%s)。\n可在 Web「漏洞管理」页面同步修改。", state, vulnerabilityAlertSeverityLabel(sub.MinSeverity), sub.MinSeverity)
|
||
}
|
||
enabled, severity := true, ""
|
||
switch arg {
|
||
case "开启", "开", "on", "enable":
|
||
severity = "high"
|
||
case "关闭", "关", "off", "disable":
|
||
enabled, severity = false, "high"
|
||
case "仅严重", "严重", "critical":
|
||
severity = "critical"
|
||
case "高危以上", "高危", "high":
|
||
severity = "high"
|
||
case "中危以上", "中危", "medium":
|
||
severity = "medium"
|
||
case "低危以上", "低危", "low":
|
||
severity = "low"
|
||
case "全部", "all", "info":
|
||
severity = "info"
|
||
default:
|
||
return "用法:漏洞提醒 开启|关闭|仅严重|高危以上|中危以上"
|
||
}
|
||
current, _ := h.db.GetVulnerabilityAlertSubscription(access.User.ID)
|
||
if (arg == "开启" || arg == "开" || arg == "on" || arg == "enable" || !enabled) && current != nil {
|
||
severity = current.MinSeverity
|
||
}
|
||
sub, err := h.db.UpsertVulnerabilityAlertSubscription(access.User.ID, enabled, severity)
|
||
if err != nil {
|
||
return "更新漏洞提醒失败,请稍后重试。"
|
||
}
|
||
if !sub.Enabled {
|
||
return "已关闭漏洞提醒。Web 端设置已同步。"
|
||
}
|
||
bindings, _ := h.db.ListRobotUserBindings(access.User.ID)
|
||
deliveryReady := false
|
||
for _, binding := range bindings {
|
||
if binding.Enabled && robot.SupportsProactive(binding.Platform) {
|
||
deliveryReady = true
|
||
break
|
||
}
|
||
}
|
||
if !deliveryReady {
|
||
return fmt.Sprintf("已保存漏洞提醒:%s及以上。当前没有支持主动推送的已绑定账号;请绑定企业微信、飞书、Telegram、Slack 或 Discord。", vulnerabilityAlertSeverityLabel(sub.MinSeverity))
|
||
}
|
||
return fmt.Sprintf("已开启漏洞提醒:%s及以上漏洞将通过已绑定机器人推送。Web 端设置已同步。", vulnerabilityAlertSeverityLabel(sub.MinSeverity))
|
||
}
|