Files
CyberStrikeAI/internal/handler/vulnerability_alert.go
T
2026-07-13 19:16:51 +08:00

228 lines
7.6 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 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))
}