Add files via upload

This commit is contained in:
公明
2025-12-25 22:12:41 +08:00
committed by GitHub
parent 99cf5e78a9
commit 025704cbf7
11 changed files with 1683 additions and 28 deletions
+54 -16
View File
@@ -20,18 +20,19 @@ import (
// Agent AI代理
type Agent struct {
openAIClient *openai.Client
config *config.OpenAIConfig
agentConfig *config.AgentConfig
memoryCompressor *MemoryCompressor
mcpServer *mcp.Server
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
logger *zap.Logger
maxIterations int
resultStorage ResultStorage // 结果存储
largeResultThreshold int // 大结果阈值(字节)
mu sync.RWMutex // 添加互斥锁以支持并发更新
toolNameMapping map[string]string // 工具名称映射:OpenAI格式 -> 原始格式(用于外部MCP工具)
openAIClient *openai.Client
config *config.OpenAIConfig
agentConfig *config.AgentConfig
memoryCompressor *MemoryCompressor
mcpServer *mcp.Server
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
logger *zap.Logger
maxIterations int
resultStorage ResultStorage // 结果存储
largeResultThreshold int // 大结果阈值(字节)
mu sync.RWMutex // 添加互斥锁以支持并发更新
toolNameMapping map[string]string // 工具名称映射:OpenAI格式 -> 原始格式(用于外部MCP工具)
currentConversationID string // 当前对话ID(用于自动传递给工具)
}
// ResultStorage 结果存储接口(直接使用 storage 包的类型)
@@ -301,11 +302,20 @@ type ProgressCallback func(eventType, message string, data interface{})
// AgentLoop 执行Agent循环
func (a *Agent) AgentLoop(ctx context.Context, userInput string, historyMessages []ChatMessage) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, nil)
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil)
}
// AgentLoopWithProgress 执行Agent循环(带进度回调
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, callback ProgressCallback) (*AgentLoopResult, error) {
// AgentLoopWithConversationID 执行Agent循环(带对话ID
func (a *Agent) AgentLoopWithConversationID(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil)
}
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback) (*AgentLoopResult, error) {
// 设置当前对话ID
a.mu.Lock()
a.currentConversationID = conversationID
a.mu.Unlock()
// 发送进度更新
sendProgress := func(eventType, message string, data interface{}) {
if callback != nil {
@@ -388,7 +398,19 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
5. 如果确实无法使用某个工具,向用户说明问题,并建议替代方案或手动操作
6. 不要因为单个工具失败就停止整个测试流程,尝试其他方法继续完成任务
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。`
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。
漏洞记录要求:
- 当你发现有效漏洞时,必须使用 record_vulnerability 工具记录漏洞详情
- 漏洞记录应包含:标题、描述、严重程度、类型、目标、证明(POC)、影响和修复建议
- 严重程度评估标准:
* critical(严重):可导致系统完全被控制、数据泄露、服务中断等
* high(高):可导致敏感信息泄露、权限提升、重要功能被绕过等
* medium(中):可导致部分信息泄露、功能受限、需要特定条件才能利用等
* low(低):影响较小,难以利用或影响范围有限
* info(信息):安全配置问题、信息泄露但不直接可利用等
- 确保漏洞证明(proof)包含足够的证据,如请求/响应、截图、命令输出等
- 在记录漏洞后,继续测试以发现更多问题`
messages := []ChatMessage{
{
@@ -1112,6 +1134,22 @@ func (a *Agent) executeToolViaMCP(ctx context.Context, toolName string, args map
zap.Any("args", args),
)
// 如果是record_vulnerability工具,自动添加conversation_id
if toolName == "record_vulnerability" {
a.mu.RLock()
conversationID := a.currentConversationID
a.mu.RUnlock()
if conversationID != "" {
args["conversation_id"] = conversationID
a.logger.Debug("自动添加conversation_id到record_vulnerability工具",
zap.String("conversation_id", conversationID),
)
} else {
a.logger.Warn("record_vulnerability工具调用时conversation_id为空")
}
}
var result *mcp.ToolResult
var executionID string
var err error
+203
View File
@@ -77,6 +77,9 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
// 注册工具
executor.RegisterTools(mcpServer)
// 注册漏洞记录工具
registerVulnerabilityTool(mcpServer, db, log.Logger)
if cfg.Auth.GeneratedPassword != "" {
config.PrintGeneratedPasswordWarning(cfg.Auth.GeneratedPassword, cfg.Auth.GeneratedPasswordPersisted, cfg.Auth.GeneratedPasswordPersistErr)
cfg.Auth.GeneratedPassword = ""
@@ -237,6 +240,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
groupHandler := handler.NewGroupHandler(db, log.Logger)
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
// 如果知识库已启用,设置知识库工具注册器,以便在ApplyConfig时重新注册知识库工具
if cfg.Knowledge.Enabled && knowledgeRetriever != nil && knowledgeManager != nil {
@@ -261,6 +265,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
externalMCPHandler,
attackChainHandler,
knowledgeHandler,
vulnerabilityHandler,
mcpServer,
authManager,
)
@@ -330,6 +335,7 @@ func setupRoutes(
externalMCPHandler *handler.ExternalMCPHandler,
attackChainHandler *handler.AttackChainHandler,
knowledgeHandler *handler.KnowledgeHandler,
vulnerabilityHandler *handler.VulnerabilityHandler,
mcpServer *mcp.Server,
authManager *security.AuthManager,
) {
@@ -417,6 +423,14 @@ func setupRoutes(
protected.POST("/knowledge/search", knowledgeHandler.Search)
}
// 漏洞管理
protected.GET("/vulnerabilities", vulnerabilityHandler.ListVulnerabilities)
protected.GET("/vulnerabilities/stats", vulnerabilityHandler.GetVulnerabilityStats)
protected.GET("/vulnerabilities/:id", vulnerabilityHandler.GetVulnerability)
protected.POST("/vulnerabilities", vulnerabilityHandler.CreateVulnerability)
protected.PUT("/vulnerabilities/:id", vulnerabilityHandler.UpdateVulnerability)
protected.DELETE("/vulnerabilities/:id", vulnerabilityHandler.DeleteVulnerability)
// MCP端点
protected.POST("/mcp", func(c *gin.Context) {
mcpServer.HandleHTTP(c.Writer, c.Request)
@@ -433,6 +447,195 @@ func setupRoutes(
})
}
// registerVulnerabilityTool 注册漏洞记录工具到MCP服务器
func registerVulnerabilityTool(mcpServer *mcp.Server, db *database.DB, logger *zap.Logger) {
tool := mcp.Tool{
Name: "record_vulnerability",
Description: "记录发现的漏洞详情到漏洞管理系统。当发现有效漏洞时,使用此工具记录漏洞信息,包括标题、描述、严重程度、类型、目标、证明、影响和建议等。",
ShortDescription: "记录发现的漏洞详情到漏洞管理系统",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"title": map[string]interface{}{
"type": "string",
"description": "漏洞标题(必需)",
},
"description": map[string]interface{}{
"type": "string",
"description": "漏洞详细描述",
},
"severity": map[string]interface{}{
"type": "string",
"description": "漏洞严重程度:critical(严重)、high(高)、medium(中)、low(低)、info(信息)",
"enum": []string{"critical", "high", "medium", "low", "info"},
},
"vulnerability_type": map[string]interface{}{
"type": "string",
"description": "漏洞类型,如:SQL注入、XSS、CSRF、命令注入等",
},
"target": map[string]interface{}{
"type": "string",
"description": "受影响的目标(URL、IP地址、服务等)",
},
"proof": map[string]interface{}{
"type": "string",
"description": "漏洞证明(POC、截图、请求/响应等)",
},
"impact": map[string]interface{}{
"type": "string",
"description": "漏洞影响说明",
},
"recommendation": map[string]interface{}{
"type": "string",
"description": "修复建议",
},
},
"required": []string{"title", "severity"},
},
}
handler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
// 从参数中获取conversation_id(由Agent自动添加)
conversationID, _ := args["conversation_id"].(string)
if conversationID == "" {
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: "错误: conversation_id 未设置。这是系统错误,请重试。",
},
},
IsError: true,
}, nil
}
title, ok := args["title"].(string)
if !ok || title == "" {
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: "错误: title 参数必需且不能为空",
},
},
IsError: true,
}, nil
}
severity, ok := args["severity"].(string)
if !ok || severity == "" {
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: "错误: severity 参数必需且不能为空",
},
},
IsError: true,
}, nil
}
// 验证严重程度
validSeverities := map[string]bool{
"critical": true,
"high": true,
"medium": true,
"low": true,
"info": true,
}
if !validSeverities[severity] {
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("错误: severity 必须是 critical、high、medium、low 或 info 之一,当前值: %s", severity),
},
},
IsError: true,
}, nil
}
// 获取可选参数
description := ""
if d, ok := args["description"].(string); ok {
description = d
}
vulnType := ""
if t, ok := args["vulnerability_type"].(string); ok {
vulnType = t
}
target := ""
if t, ok := args["target"].(string); ok {
target = t
}
proof := ""
if p, ok := args["proof"].(string); ok {
proof = p
}
impact := ""
if i, ok := args["impact"].(string); ok {
impact = i
}
recommendation := ""
if r, ok := args["recommendation"].(string); ok {
recommendation = r
}
// 创建漏洞记录
vuln := &database.Vulnerability{
ConversationID: conversationID,
Title: title,
Description: description,
Severity: severity,
Status: "open",
Type: vulnType,
Target: target,
Proof: proof,
Impact: impact,
Recommendation: recommendation,
}
created, err := db.CreateVulnerability(vuln)
if err != nil {
logger.Error("记录漏洞失败", zap.Error(err))
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("记录漏洞失败: %v", err),
},
},
IsError: true,
}, nil
}
logger.Info("漏洞记录成功",
zap.String("id", created.ID),
zap.String("title", created.Title),
zap.String("severity", created.Severity),
zap.String("conversation_id", conversationID),
)
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("漏洞已成功记录!\n\n漏洞ID: %s\n标题: %s\n严重程度: %s\n状态: %s\n\n你可以在漏洞管理页面查看和管理此漏洞。", created.ID, created.Title, created.Severity, created.Status),
},
},
IsError: false,
}, nil
}
mcpServer.RegisterTool(tool, handler)
logger.Info("漏洞记录工具注册成功")
}
// corsMiddleware CORS中间件
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
+27
View File
@@ -170,6 +170,25 @@ func (db *DB) initTables() error {
UNIQUE(conversation_id, group_id)
);`
// 创建漏洞表
createVulnerabilitiesTable := `
CREATE TABLE IF NOT EXISTS vulnerabilities (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
severity TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
vulnerability_type TEXT,
target TEXT,
proof TEXT,
impact TEXT,
recommendation TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);`
// 创建索引
createIndexes := `
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
@@ -189,6 +208,10 @@ func (db *DB) initTables() error {
CREATE INDEX IF NOT EXISTS idx_conversation_group_mappings_conversation ON conversation_group_mappings(conversation_id);
CREATE INDEX IF NOT EXISTS idx_conversation_group_mappings_group ON conversation_group_mappings(group_id);
CREATE INDEX IF NOT EXISTS idx_conversations_pinned ON conversations(pinned);
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_conversation_id ON vulnerabilities(conversation_id);
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_severity ON vulnerabilities(severity);
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_status ON vulnerabilities(status);
CREATE INDEX IF NOT EXISTS idx_vulnerabilities_created_at ON vulnerabilities(created_at);
`
if _, err := db.Exec(createConversationsTable); err != nil {
@@ -231,6 +254,10 @@ func (db *DB) initTables() error {
return fmt.Errorf("创建conversation_group_mappings表失败: %w", err)
}
if _, err := db.Exec(createVulnerabilitiesTable); err != nil {
return fmt.Errorf("创建vulnerabilities表失败: %w", err)
}
// 为已有表添加新字段(如果不存在)- 必须在创建索引之前
if err := db.migrateConversationsTable(); err != nil {
db.logger.Warn("迁移conversations表失败", zap.Error(err))
+246
View File
@@ -0,0 +1,246 @@
package database
import (
"database/sql"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
)
// Vulnerability 漏洞
type Vulnerability struct {
ID string `json:"id"`
ConversationID string `json:"conversation_id"`
Title string `json:"title"`
Description string `json:"description"`
Severity string `json:"severity"` // critical, high, medium, low, info
Status string `json:"status"` // open, confirmed, fixed, false_positive
Type string `json:"type"`
Target string `json:"target"`
Proof string `json:"proof"`
Impact string `json:"impact"`
Recommendation string `json:"recommendation"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateVulnerability 创建漏洞
func (db *DB) CreateVulnerability(vuln *Vulnerability) (*Vulnerability, error) {
if vuln.ID == "" {
vuln.ID = uuid.New().String()
}
if vuln.Status == "" {
vuln.Status = "open"
}
now := time.Now()
if vuln.CreatedAt.IsZero() {
vuln.CreatedAt = now
}
vuln.UpdatedAt = now
query := `
INSERT INTO vulnerabilities (
id, conversation_id, title, description, severity, status,
vulnerability_type, target, proof, impact, recommendation,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
_, err := db.Exec(
query,
vuln.ID, vuln.ConversationID, vuln.Title, vuln.Description,
vuln.Severity, vuln.Status, vuln.Type, vuln.Target,
vuln.Proof, vuln.Impact, vuln.Recommendation,
vuln.CreatedAt, vuln.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("创建漏洞失败: %w", err)
}
return vuln, nil
}
// GetVulnerability 获取漏洞
func (db *DB) GetVulnerability(id string) (*Vulnerability, error) {
var vuln Vulnerability
query := `
SELECT id, conversation_id, title, description, severity, status,
vulnerability_type, target, proof, impact, recommendation,
created_at, updated_at
FROM vulnerabilities
WHERE id = ?
`
err := db.QueryRow(query, id).Scan(
&vuln.ID, &vuln.ConversationID, &vuln.Title, &vuln.Description,
&vuln.Severity, &vuln.Status, &vuln.Type, &vuln.Target,
&vuln.Proof, &vuln.Impact, &vuln.Recommendation,
&vuln.CreatedAt, &vuln.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("漏洞不存在")
}
return nil, fmt.Errorf("获取漏洞失败: %w", err)
}
return &vuln, nil
}
// ListVulnerabilities 列出漏洞
func (db *DB) ListVulnerabilities(limit, offset int, conversationID, severity, status string) ([]*Vulnerability, error) {
query := `
SELECT id, conversation_id, title, description, severity, status,
vulnerability_type, target, proof, impact, recommendation,
created_at, updated_at
FROM vulnerabilities
WHERE 1=1
`
args := []interface{}{}
if conversationID != "" {
query += " AND conversation_id = ?"
args = append(args, conversationID)
}
if severity != "" {
query += " AND severity = ?"
args = append(args, severity)
}
if status != "" {
query += " AND status = ?"
args = append(args, status)
}
query += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
args = append(args, limit, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("查询漏洞列表失败: %w", err)
}
defer rows.Close()
var vulnerabilities []*Vulnerability
for rows.Next() {
var vuln Vulnerability
err := rows.Scan(
&vuln.ID, &vuln.ConversationID, &vuln.Title, &vuln.Description,
&vuln.Severity, &vuln.Status, &vuln.Type, &vuln.Target,
&vuln.Proof, &vuln.Impact, &vuln.Recommendation,
&vuln.CreatedAt, &vuln.UpdatedAt,
)
if err != nil {
db.logger.Warn("扫描漏洞记录失败", zap.Error(err))
continue
}
vulnerabilities = append(vulnerabilities, &vuln)
}
return vulnerabilities, nil
}
// UpdateVulnerability 更新漏洞
func (db *DB) UpdateVulnerability(id string, vuln *Vulnerability) error {
vuln.UpdatedAt = time.Now()
query := `
UPDATE vulnerabilities
SET title = ?, description = ?, severity = ?, status = ?,
vulnerability_type = ?, target = ?, proof = ?, impact = ?,
recommendation = ?, updated_at = ?
WHERE id = ?
`
_, err := db.Exec(
query,
vuln.Title, vuln.Description, vuln.Severity, vuln.Status,
vuln.Type, vuln.Target, vuln.Proof, vuln.Impact,
vuln.Recommendation, vuln.UpdatedAt, id,
)
if err != nil {
return fmt.Errorf("更新漏洞失败: %w", err)
}
return nil
}
// DeleteVulnerability 删除漏洞
func (db *DB) DeleteVulnerability(id string) error {
_, err := db.Exec("DELETE FROM vulnerabilities WHERE id = ?", id)
if err != nil {
return fmt.Errorf("删除漏洞失败: %w", err)
}
return nil
}
// GetVulnerabilityStats 获取漏洞统计
func (db *DB) GetVulnerabilityStats(conversationID string) (map[string]interface{}, error) {
stats := make(map[string]interface{})
// 总漏洞数
var totalCount int
query := "SELECT COUNT(*) FROM vulnerabilities"
args := []interface{}{}
if conversationID != "" {
query += " WHERE conversation_id = ?"
args = append(args, conversationID)
}
err := db.QueryRow(query, args...).Scan(&totalCount)
if err != nil {
return nil, fmt.Errorf("获取总漏洞数失败: %w", err)
}
stats["total"] = totalCount
// 按严重程度统计
severityQuery := "SELECT severity, COUNT(*) FROM vulnerabilities"
if conversationID != "" {
severityQuery += " WHERE conversation_id = ?"
}
severityQuery += " GROUP BY severity"
rows, err := db.Query(severityQuery, args...)
if err != nil {
return nil, fmt.Errorf("获取严重程度统计失败: %w", err)
}
defer rows.Close()
severityStats := make(map[string]int)
for rows.Next() {
var severity string
var count int
if err := rows.Scan(&severity, &count); err != nil {
continue
}
severityStats[severity] = count
}
stats["by_severity"] = severityStats
// 按状态统计
statusQuery := "SELECT status, COUNT(*) FROM vulnerabilities"
if conversationID != "" {
statusQuery += " WHERE conversation_id = ?"
}
statusQuery += " GROUP BY status"
rows, err = db.Query(statusQuery, args...)
if err != nil {
return nil, fmt.Errorf("获取状态统计失败: %w", err)
}
defer rows.Close()
statusStats := make(map[string]int)
for rows.Next() {
var status string
var count int
if err := rows.Scan(&status, &count); err != nil {
continue
}
statusStats[status] = count
}
stats["by_status"] = statusStats
return stats, nil
}
+3 -3
View File
@@ -117,8 +117,8 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
h.logger.Error("保存用户消息失败", zap.Error(err))
}
// 执行Agent Loop,传入历史消息
result, err := h.agent.AgentLoop(c.Request.Context(), req.Message, agentHistoryMessages)
// 执行Agent Loop,传入历史消息和对话ID
result, err := h.agent.AgentLoopWithConversationID(c.Request.Context(), req.Message, agentHistoryMessages, conversationID)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
@@ -492,7 +492,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断
sendEvent("progress", "正在分析您的请求...", nil)
result, err := h.agent.AgentLoopWithProgress(taskCtx, req.Message, agentHistoryMessages, progressCallback)
result, err := h.agent.AgentLoopWithProgress(taskCtx, req.Message, agentHistoryMessages, conversationID, progressCallback)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
cause := context.Cause(baseCtx)
+212
View File
@@ -0,0 +1,212 @@
package handler
import (
"net/http"
"strconv"
"cyberstrike-ai/internal/database"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// VulnerabilityHandler 漏洞处理器
type VulnerabilityHandler struct {
db *database.DB
logger *zap.Logger
}
// NewVulnerabilityHandler 创建新的漏洞处理器
func NewVulnerabilityHandler(db *database.DB, logger *zap.Logger) *VulnerabilityHandler {
return &VulnerabilityHandler{
db: db,
logger: logger,
}
}
// CreateVulnerabilityRequest 创建漏洞请求
type CreateVulnerabilityRequest struct {
ConversationID string `json:"conversation_id" binding:"required"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
Severity string `json:"severity" binding:"required"`
Status string `json:"status"`
Type string `json:"type"`
Target string `json:"target"`
Proof string `json:"proof"`
Impact string `json:"impact"`
Recommendation string `json:"recommendation"`
}
// CreateVulnerability 创建漏洞
func (h *VulnerabilityHandler) CreateVulnerability(c *gin.Context) {
var req CreateVulnerabilityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
vuln := &database.Vulnerability{
ConversationID: req.ConversationID,
Title: req.Title,
Description: req.Description,
Severity: req.Severity,
Status: req.Status,
Type: req.Type,
Target: req.Target,
Proof: req.Proof,
Impact: req.Impact,
Recommendation: req.Recommendation,
}
created, err := h.db.CreateVulnerability(vuln)
if err != nil {
h.logger.Error("创建漏洞失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, created)
}
// GetVulnerability 获取漏洞
func (h *VulnerabilityHandler) GetVulnerability(c *gin.Context) {
id := c.Param("id")
vuln, err := h.db.GetVulnerability(id)
if err != nil {
h.logger.Error("获取漏洞失败", zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "漏洞不存在"})
return
}
c.JSON(http.StatusOK, vuln)
}
// ListVulnerabilities 列出漏洞
func (h *VulnerabilityHandler) ListVulnerabilities(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "50")
offsetStr := c.DefaultQuery("offset", "0")
conversationID := c.Query("conversation_id")
severity := c.Query("severity")
status := c.Query("status")
limit, _ := strconv.Atoi(limitStr)
offset, _ := strconv.Atoi(offsetStr)
if limit <= 0 || limit > 100 {
limit = 50
}
vulnerabilities, err := h.db.ListVulnerabilities(limit, offset, conversationID, severity, status)
if err != nil {
h.logger.Error("获取漏洞列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, vulnerabilities)
}
// UpdateVulnerabilityRequest 更新漏洞请求
type UpdateVulnerabilityRequest struct {
Title string `json:"title"`
Description string `json:"description"`
Severity string `json:"severity"`
Status string `json:"status"`
Type string `json:"type"`
Target string `json:"target"`
Proof string `json:"proof"`
Impact string `json:"impact"`
Recommendation string `json:"recommendation"`
}
// UpdateVulnerability 更新漏洞
func (h *VulnerabilityHandler) UpdateVulnerability(c *gin.Context) {
id := c.Param("id")
var req UpdateVulnerabilityRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 获取现有漏洞
existing, err := h.db.GetVulnerability(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "漏洞不存在"})
return
}
// 更新字段
if req.Title != "" {
existing.Title = req.Title
}
if req.Description != "" {
existing.Description = req.Description
}
if req.Severity != "" {
existing.Severity = req.Severity
}
if req.Status != "" {
existing.Status = req.Status
}
if req.Type != "" {
existing.Type = req.Type
}
if req.Target != "" {
existing.Target = req.Target
}
if req.Proof != "" {
existing.Proof = req.Proof
}
if req.Impact != "" {
existing.Impact = req.Impact
}
if req.Recommendation != "" {
existing.Recommendation = req.Recommendation
}
if err := h.db.UpdateVulnerability(id, existing); err != nil {
h.logger.Error("更新漏洞失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回更新后的漏洞
updated, err := h.db.GetVulnerability(id)
if err != nil {
h.logger.Error("获取更新后的漏洞失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, updated)
}
// DeleteVulnerability 删除漏洞
func (h *VulnerabilityHandler) DeleteVulnerability(c *gin.Context) {
id := c.Param("id")
if err := h.db.DeleteVulnerability(id); err != nil {
h.logger.Error("删除漏洞失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}
// GetVulnerabilityStats 获取漏洞统计
func (h *VulnerabilityHandler) GetVulnerabilityStats(c *gin.Context) {
conversationID := c.Query("conversation_id")
stats, err := h.db.GetVulnerabilityStats(conversationID)
if err != nil {
h.logger.Error("获取漏洞统计失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
+333
View File
@@ -5645,3 +5645,336 @@ header {
.context-submenu-item.add-group-item:hover {
background: rgba(0, 102, 255, 0.1);
}
/* 漏洞管理页面样式 */
.vulnerability-dashboard {
margin-bottom: 24px;
}
.dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
text-align: center;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
}
.stat-card:hover {
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.stat-card.stat-critical {
border-left: 4px solid #dc3545;
}
.stat-card.stat-high {
border-left: 4px solid #fd7e14;
}
.stat-card.stat-medium {
border-left: 4px solid #ffc107;
}
.stat-card.stat-low {
border-left: 4px solid #20c997;
}
.stat-card.stat-info {
border-left: 4px solid #6c757d;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 8px;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
}
.vulnerability-controls {
margin-bottom: 24px;
}
.vulnerability-filters {
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: flex-end;
}
.vulnerability-filters label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.875rem;
color: var(--text-secondary);
}
.vulnerability-filters input,
.vulnerability-filters select {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
min-width: 150px;
}
.vulnerabilities-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.vulnerability-card {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow-sm);
transition: all 0.2s ease;
}
.vulnerability-card:hover {
box-shadow: var(--shadow-md);
}
.vulnerability-card.severity-critical {
border-left: 4px solid #dc3545;
}
.vulnerability-card.severity-high {
border-left: 4px solid #fd7e14;
}
.vulnerability-card.severity-medium {
border-left: 4px solid #ffc107;
}
.vulnerability-card.severity-low {
border-left: 4px solid #20c997;
}
.vulnerability-card.severity-info {
border-left: 4px solid #6c757d;
}
.vulnerability-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0;
padding: 4px 0;
transition: background-color 0.2s ease;
}
.vulnerability-header:hover {
background-color: var(--bg-secondary);
border-radius: 4px;
padding: 4px 8px;
margin: -4px -8px;
}
.vulnerability-title-section {
flex: 1;
}
.vulnerability-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
flex: 1;
}
.vulnerability-expand-icon {
color: var(--text-secondary);
transition: transform 0.2s ease, color 0.2s ease;
}
.vulnerability-header:hover .vulnerability-expand-icon {
color: var(--accent-color);
}
.vulnerability-meta {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
}
.severity-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.severity-badge.severity-critical {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
.severity-badge.severity-high {
background: rgba(253, 126, 20, 0.1);
color: #fd7e14;
}
.severity-badge.severity-medium {
background: rgba(255, 193, 7, 0.1);
color: #ffc107;
}
.severity-badge.severity-low {
background: rgba(32, 201, 151, 0.1);
color: #20c997;
}
.severity-badge.severity-info {
background: rgba(108, 117, 125, 0.1);
color: #6c757d;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
background: var(--bg-tertiary);
color: var(--text-secondary);
}
.status-badge.status-open {
background: rgba(0, 102, 255, 0.1);
color: #0066ff;
}
.status-badge.status-confirmed {
background: rgba(40, 167, 69, 0.1);
color: #28a745;
}
.status-badge.status-fixed {
background: rgba(108, 117, 125, 0.1);
color: #6c757d;
}
.status-badge.status-false_positive {
background: rgba(220, 53, 69, 0.1);
color: #dc3545;
}
.vulnerability-date {
font-size: 0.75rem;
color: var(--text-muted);
}
.vulnerability-actions {
display: flex;
gap: 8px;
}
.vulnerability-content {
margin-top: 16px;
padding-left: 24px;
animation: slideDown 0.2s ease;
}
@keyframes slideDown {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 5000px;
}
}
.vulnerability-description {
margin-bottom: 16px;
color: var(--text-primary);
line-height: 1.6;
}
.vulnerability-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 16px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
}
.detail-item {
font-size: 0.875rem;
}
.detail-item strong {
color: var(--text-secondary);
margin-right: 4px;
}
.detail-item code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.8rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
.vulnerability-proof,
.vulnerability-impact,
.vulnerability-recommendation {
margin-top: 12px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
font-size: 0.875rem;
line-height: 1.6;
}
.vulnerability-proof pre {
margin-top: 8px;
padding: 12px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
overflow-x: auto;
font-size: 0.8rem;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
white-space: pre-wrap;
word-wrap: break-word;
}
.vulnerability-proof strong,
.vulnerability-impact strong,
.vulnerability-recommendation strong {
color: var(--text-primary);
display: block;
margin-bottom: 8px;
}
.empty-state {
text-align: center;
padding: 48px;
color: var(--text-secondary);
font-size: 1rem;
}
+59 -7
View File
@@ -4634,15 +4634,31 @@ async function removeConversationFromGroup(convId, groupId) {
async function loadConversationGroupMapping() {
try {
// 获取所有分组,然后获取每个分组的对话
const groups = groupsCache.length > 0 ? groupsCache : await (await apiFetch('/api/groups')).json();
let groups;
if (Array.isArray(groupsCache) && groupsCache.length > 0) {
groups = groupsCache;
} else {
const response = await apiFetch('/api/groups');
groups = await response.json();
}
// 确保groups是有效数组
if (!Array.isArray(groups)) {
console.warn('loadConversationGroupMapping: groups不是有效数组,使用空数组');
groups = [];
}
conversationGroupMappingCache = {};
for (const group of groups) {
const response = await apiFetch(`/api/groups/${group.id}/conversations`);
const conversations = await response.json();
conversations.forEach(conv => {
conversationGroupMappingCache[conv.id] = group.id;
});
// 确保conversations是有效数组
if (Array.isArray(conversations)) {
conversations.forEach(conv => {
conversationGroupMappingCache[conv.id] = group.id;
});
}
}
} catch (error) {
console.error('加载对话分组映射失败:', error);
@@ -4866,7 +4882,19 @@ async function createGroup(event) {
// 前端校验:检查名称是否已存在
try {
const groups = groupsCache.length > 0 ? groupsCache : await (await apiFetch('/api/groups')).json();
let groups;
if (Array.isArray(groupsCache) && groupsCache.length > 0) {
groups = groupsCache;
} else {
const response = await apiFetch('/api/groups');
groups = await response.json();
}
// 确保groups是有效数组
if (!Array.isArray(groups)) {
groups = [];
}
const nameExists = groups.some(g => g.name === name);
if (nameExists) {
alert('分组名称已存在,请使用其他名称');
@@ -5175,7 +5203,19 @@ async function editGroup() {
const trimmedName = newName.trim();
// 前端校验:检查名称是否已存在(排除当前分组)
const groups = groupsCache.length > 0 ? groupsCache : await (await apiFetch('/api/groups')).json();
let groups;
if (Array.isArray(groupsCache) && groupsCache.length > 0) {
groups = groupsCache;
} else {
const response = await apiFetch('/api/groups');
groups = await response.json();
}
// 确保groups是有效数组
if (!Array.isArray(groups)) {
groups = [];
}
const nameExists = groups.some(g => g.name === trimmedName && g.id !== currentGroupId);
if (nameExists) {
alert('分组名称已存在,请使用其他名称');
@@ -5273,7 +5313,19 @@ async function renameGroupFromContext() {
const trimmedName = newName.trim();
// 前端校验:检查名称是否已存在(排除当前分组)
const groups = groupsCache.length > 0 ? groupsCache : await (await apiFetch('/api/groups')).json();
let groups;
if (Array.isArray(groupsCache) && groupsCache.length > 0) {
groups = groupsCache;
} else {
const response = await apiFetch('/api/groups');
groups = await response.json();
}
// 确保groups是有效数组
if (!Array.isArray(groups)) {
groups = [];
}
const nameExists = groups.some(g => g.name === trimmedName && g.id !== groupId);
if (nameExists) {
alert('分组名称已存在,请使用其他名称');
+8 -2
View File
@@ -8,7 +8,7 @@ function initRouter() {
// 从URL hash读取页面(如果有)
const hash = window.location.hash.slice(1);
if (hash && ['chat', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings'].includes(hash)) {
if (hash && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings'].includes(hash)) {
switchPage(hash);
}
}
@@ -198,6 +198,12 @@ function initPage(pageId) {
loadToolsList(1, '');
}
break;
case 'vulnerabilities':
// 初始化漏洞管理页面
if (typeof initVulnerabilityPage === 'function') {
initVulnerabilityPage();
}
break;
case 'settings':
// 初始化设置页面(不需要加载工具列表)
if (typeof loadConfig === 'function') {
@@ -215,7 +221,7 @@ document.addEventListener('DOMContentLoaded', function() {
// 监听hash变化
window.addEventListener('hashchange', function() {
const hash = window.location.hash.slice(1);
if (hash && ['chat', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings'].includes(hash)) {
if (hash && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings'].includes(hash)) {
switchPage(hash);
}
});
+380
View File
@@ -0,0 +1,380 @@
// 漏洞管理相关功能
let currentVulnerabilityId = null;
let vulnerabilityFilters = {
conversation_id: '',
severity: '',
status: ''
};
// 初始化漏洞管理页面
function initVulnerabilityPage() {
loadVulnerabilityStats();
loadVulnerabilities();
}
// 加载漏洞统计
async function loadVulnerabilityStats() {
try {
// 检查apiFetch是否可用
if (typeof apiFetch === 'undefined') {
console.error('apiFetch未定义,请确保auth.js已加载');
throw new Error('apiFetch未定义');
}
const params = new URLSearchParams();
if (vulnerabilityFilters.conversation_id) {
params.append('conversation_id', vulnerabilityFilters.conversation_id);
}
const response = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
console.error('获取统计失败:', response.status, errorText);
throw new Error(`获取统计失败: ${response.status}`);
}
const stats = await response.json();
updateVulnerabilityStats(stats);
} catch (error) {
console.error('加载漏洞统计失败:', error);
// 统计失败不影响列表显示,只重置统计为0
updateVulnerabilityStats(null);
}
}
// 更新漏洞统计显示
function updateVulnerabilityStats(stats) {
// 处理空值情况
if (!stats) {
stats = {
total: 0,
by_severity: {},
by_status: {}
};
}
document.getElementById('stat-total').textContent = stats.total || 0;
const bySeverity = stats.by_severity || {};
document.getElementById('stat-critical').textContent = bySeverity.critical || 0;
document.getElementById('stat-high').textContent = bySeverity.high || 0;
document.getElementById('stat-medium').textContent = bySeverity.medium || 0;
document.getElementById('stat-low').textContent = bySeverity.low || 0;
document.getElementById('stat-info').textContent = bySeverity.info || 0;
}
// 加载漏洞列表
async function loadVulnerabilities() {
const listContainer = document.getElementById('vulnerabilities-list');
listContainer.innerHTML = '<div class="loading-spinner">加载中...</div>';
try {
// 检查apiFetch是否可用
if (typeof apiFetch === 'undefined') {
console.error('apiFetch未定义,请确保auth.js已加载');
throw new Error('apiFetch未定义');
}
const params = new URLSearchParams();
params.append('limit', '100');
params.append('offset', '0');
if (vulnerabilityFilters.conversation_id) {
params.append('conversation_id', vulnerabilityFilters.conversation_id);
}
if (vulnerabilityFilters.severity) {
params.append('severity', vulnerabilityFilters.severity);
}
if (vulnerabilityFilters.status) {
params.append('status', vulnerabilityFilters.status);
}
const response = await apiFetch(`/api/vulnerabilities?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
console.error('获取漏洞列表失败:', response.status, errorText);
throw new Error(`获取漏洞列表失败: ${response.status}`);
}
const vulnerabilities = await response.json();
renderVulnerabilities(vulnerabilities);
} catch (error) {
console.error('加载漏洞列表失败:', error);
listContainer.innerHTML = `<div class="error-message">加载失败: ${error.message}</div>`;
}
}
// 渲染漏洞列表
function renderVulnerabilities(vulnerabilities) {
const listContainer = document.getElementById('vulnerabilities-list');
// 处理空值情况
if (!vulnerabilities || !Array.isArray(vulnerabilities)) {
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
return;
}
if (vulnerabilities.length === 0) {
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
return;
}
const html = vulnerabilities.map(vuln => {
const severityClass = `severity-${vuln.severity}`;
const severityText = {
'critical': '严重',
'high': '高危',
'medium': '中危',
'low': '低危',
'info': '信息'
}[vuln.severity] || vuln.severity;
const statusText = {
'open': '待处理',
'confirmed': '已确认',
'fixed': '已修复',
'false_positive': '误报'
}[vuln.status] || vuln.status;
const createdDate = new Date(vuln.created_at).toLocaleString('zh-CN');
return `
<div class="vulnerability-card ${severityClass}">
<div class="vulnerability-header" onclick="toggleVulnerabilityDetails('${vuln.id}')" style="cursor: pointer;">
<div class="vulnerability-title-section">
<div style="display: flex; align-items: center; gap: 8px;">
<svg class="vulnerability-expand-icon" id="expand-icon-${vuln.id}" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="transition: transform 0.2s ease; flex-shrink: 0;">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h3 class="vulnerability-title">${escapeHtml(vuln.title)}</h3>
</div>
<div class="vulnerability-meta">
<span class="severity-badge ${severityClass}">${severityText}</span>
<span class="status-badge status-${vuln.status}">${statusText}</span>
<span class="vulnerability-date">${createdDate}</span>
</div>
</div>
<div class="vulnerability-actions" onclick="event.stopPropagation();">
<button class="btn-ghost" onclick="editVulnerability('${vuln.id}')" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="btn-ghost" onclick="deleteVulnerability('${vuln.id}')" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
<div class="vulnerability-content" id="content-${vuln.id}" style="display: none;">
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
<div class="vulnerability-details">
${vuln.type ? `<div class="detail-item"><strong>类型:</strong> ${escapeHtml(vuln.type)}</div>` : ''}
${vuln.target ? `<div class="detail-item"><strong>目标:</strong> ${escapeHtml(vuln.target)}</div>` : ''}
<div class="detail-item"><strong>会话ID:</strong> <code>${escapeHtml(vuln.conversation_id)}</code></div>
</div>
${vuln.proof ? `<div class="vulnerability-proof"><strong>证明:</strong><pre>${escapeHtml(vuln.proof)}</pre></div>` : ''}
${vuln.impact ? `<div class="vulnerability-impact"><strong>影响:</strong> ${escapeHtml(vuln.impact)}</div>` : ''}
${vuln.recommendation ? `<div class="vulnerability-recommendation"><strong>修复建议:</strong> ${escapeHtml(vuln.recommendation)}</div>` : ''}
</div>
</div>
`;
}).join('');
listContainer.innerHTML = html;
}
// 显示添加漏洞模态框
function showAddVulnerabilityModal() {
currentVulnerabilityId = null;
document.getElementById('vulnerability-modal-title').textContent = '添加漏洞';
// 清空表单
document.getElementById('vulnerability-conversation-id').value = '';
document.getElementById('vulnerability-title').value = '';
document.getElementById('vulnerability-description').value = '';
document.getElementById('vulnerability-severity').value = '';
document.getElementById('vulnerability-status').value = 'open';
document.getElementById('vulnerability-type').value = '';
document.getElementById('vulnerability-target').value = '';
document.getElementById('vulnerability-proof').value = '';
document.getElementById('vulnerability-impact').value = '';
document.getElementById('vulnerability-recommendation').value = '';
document.getElementById('vulnerability-modal').style.display = 'block';
}
// 编辑漏洞
async function editVulnerability(id) {
try {
const response = await apiFetch(`/api/vulnerabilities/${id}`);
if (!response.ok) throw new Error('获取漏洞失败');
const vuln = await response.json();
currentVulnerabilityId = id;
document.getElementById('vulnerability-modal-title').textContent = '编辑漏洞';
// 填充表单
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
document.getElementById('vulnerability-title').value = vuln.title || '';
document.getElementById('vulnerability-description').value = vuln.description || '';
document.getElementById('vulnerability-severity').value = vuln.severity || '';
document.getElementById('vulnerability-status').value = vuln.status || 'open';
document.getElementById('vulnerability-type').value = vuln.type || '';
document.getElementById('vulnerability-target').value = vuln.target || '';
document.getElementById('vulnerability-proof').value = vuln.proof || '';
document.getElementById('vulnerability-impact').value = vuln.impact || '';
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || '';
document.getElementById('vulnerability-modal').style.display = 'block';
} catch (error) {
console.error('加载漏洞失败:', error);
alert('加载漏洞失败: ' + error.message);
}
}
// 保存漏洞
async function saveVulnerability() {
const conversationId = document.getElementById('vulnerability-conversation-id').value.trim();
const title = document.getElementById('vulnerability-title').value.trim();
const severity = document.getElementById('vulnerability-severity').value;
if (!conversationId || !title || !severity) {
alert('请填写必填字段:会话ID、标题和严重程度');
return;
}
const data = {
conversation_id: conversationId,
title: title,
description: document.getElementById('vulnerability-description').value.trim(),
severity: severity,
status: document.getElementById('vulnerability-status').value,
type: document.getElementById('vulnerability-type').value.trim(),
target: document.getElementById('vulnerability-target').value.trim(),
proof: document.getElementById('vulnerability-proof').value.trim(),
impact: document.getElementById('vulnerability-impact').value.trim(),
recommendation: document.getElementById('vulnerability-recommendation').value.trim()
};
try {
const url = currentVulnerabilityId
? `/api/vulnerabilities/${currentVulnerabilityId}`
: '/api/vulnerabilities';
const method = currentVulnerabilityId ? 'PUT' : 'POST';
const response = await apiFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '保存失败');
}
closeVulnerabilityModal();
loadVulnerabilityStats();
loadVulnerabilities();
} catch (error) {
console.error('保存漏洞失败:', error);
alert('保存漏洞失败: ' + error.message);
}
}
// 删除漏洞
async function deleteVulnerability(id) {
if (!confirm('确定要删除此漏洞吗?')) {
return;
}
try {
const response = await apiFetch(`/api/vulnerabilities/${id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('删除失败');
loadVulnerabilityStats();
loadVulnerabilities();
} catch (error) {
console.error('删除漏洞失败:', error);
alert('删除漏洞失败: ' + error.message);
}
}
// 关闭漏洞模态框
function closeVulnerabilityModal() {
document.getElementById('vulnerability-modal').style.display = 'none';
currentVulnerabilityId = null;
}
// 筛选漏洞
function filterVulnerabilities() {
vulnerabilityFilters.conversation_id = document.getElementById('vulnerability-conversation-filter').value.trim();
vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter').value;
vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter').value;
loadVulnerabilityStats();
loadVulnerabilities();
}
// 清除筛选
function clearVulnerabilityFilters() {
document.getElementById('vulnerability-conversation-filter').value = '';
document.getElementById('vulnerability-severity-filter').value = '';
document.getElementById('vulnerability-status-filter').value = '';
vulnerabilityFilters = {
conversation_id: '',
severity: '',
status: ''
};
loadVulnerabilityStats();
loadVulnerabilities();
}
// 刷新漏洞
function refreshVulnerabilities() {
loadVulnerabilityStats();
loadVulnerabilities();
}
// 切换漏洞详情展开/折叠
function toggleVulnerabilityDetails(id) {
const content = document.getElementById(`content-${id}`);
const icon = document.getElementById(`expand-icon-${id}`);
if (!content || !icon) return;
if (content.style.display === 'none') {
content.style.display = 'block';
icon.style.transform = 'rotate(90deg)';
} else {
content.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
}
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 点击模态框外部关闭
window.onclick = function(event) {
const modal = document.getElementById('vulnerability-modal');
if (event.target === modal) {
closeVulnerabilityModal();
}
}
+158
View File
@@ -76,6 +76,15 @@
<span>对话</span>
</div>
</div>
<div class="nav-item" data-page="vulnerabilities">
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>漏洞管理</span>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="mcp">
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -430,6 +439,86 @@
</div>
</div>
<!-- 漏洞管理页面 -->
<div id="page-vulnerabilities" class="page">
<div class="page-header">
<h2>漏洞管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshVulnerabilities()">刷新</button>
<button class="btn-primary" onclick="showAddVulnerabilityModal()">添加漏洞</button>
</div>
</div>
<div class="page-content">
<!-- 统计看板 -->
<div class="vulnerability-dashboard" id="vulnerability-dashboard">
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-label">总漏洞数</div>
<div class="stat-value" id="stat-total">-</div>
</div>
<div class="stat-card stat-critical">
<div class="stat-label">严重</div>
<div class="stat-value" id="stat-critical">-</div>
</div>
<div class="stat-card stat-high">
<div class="stat-label">高危</div>
<div class="stat-value" id="stat-high">-</div>
</div>
<div class="stat-card stat-medium">
<div class="stat-label">中危</div>
<div class="stat-value" id="stat-medium">-</div>
</div>
<div class="stat-card stat-low">
<div class="stat-label">低危</div>
<div class="stat-value" id="stat-low">-</div>
</div>
<div class="stat-card stat-info">
<div class="stat-label">信息</div>
<div class="stat-value" id="stat-info">-</div>
</div>
</div>
</div>
<!-- 筛选和搜索 -->
<div class="vulnerability-controls">
<div class="vulnerability-filters">
<label>
会话ID
<input type="text" id="vulnerability-conversation-filter" placeholder="筛选特定会话" />
</label>
<label>
严重程度
<select id="vulnerability-severity-filter">
<option value="">全部</option>
<option value="critical">严重</option>
<option value="high">高危</option>
<option value="medium">中危</option>
<option value="low">低危</option>
<option value="info">信息</option>
</select>
</label>
<label>
状态
<select id="vulnerability-status-filter">
<option value="">全部</option>
<option value="open">待处理</option>
<option value="confirmed">已确认</option>
<option value="fixed">已修复</option>
<option value="false_positive">误报</option>
</select>
</label>
<button class="btn-secondary" onclick="filterVulnerabilities()">筛选</button>
<button class="btn-secondary" onclick="clearVulnerabilityFilters()">清除</button>
</div>
</div>
<!-- 漏洞列表 -->
<div id="vulnerabilities-list" class="vulnerabilities-list">
<div class="loading-spinner">加载中...</div>
</div>
</div>
</div>
<!-- 系统设置页面 -->
<div id="page-settings" class="page">
<div class="page-header">
@@ -952,12 +1041,81 @@
</div>
</div>
<!-- 漏洞编辑模态框 -->
<div id="vulnerability-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="vulnerability-modal-title">添加漏洞</h2>
<span class="modal-close" onclick="closeVulnerabilityModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="vulnerability-conversation-id">会话ID <span style="color: red;">*</span></label>
<input type="text" id="vulnerability-conversation-id" placeholder="输入会话ID" required />
</div>
<div class="form-group">
<label for="vulnerability-title">标题 <span style="color: red;">*</span></label>
<input type="text" id="vulnerability-title" placeholder="漏洞标题" required />
</div>
<div class="form-group">
<label for="vulnerability-description">描述</label>
<textarea id="vulnerability-description" rows="5" placeholder="漏洞详细描述"></textarea>
</div>
<div class="form-group">
<label for="vulnerability-severity">严重程度 <span style="color: red;">*</span></label>
<select id="vulnerability-severity" required>
<option value="">请选择</option>
<option value="critical">严重</option>
<option value="high">高危</option>
<option value="medium">中危</option>
<option value="low">低危</option>
<option value="info">信息</option>
</select>
</div>
<div class="form-group">
<label for="vulnerability-status">状态</label>
<select id="vulnerability-status">
<option value="open">待处理</option>
<option value="confirmed">已确认</option>
<option value="fixed">已修复</option>
<option value="false_positive">误报</option>
</select>
</div>
<div class="form-group">
<label for="vulnerability-type">漏洞类型</label>
<input type="text" id="vulnerability-type" placeholder="如:SQL注入、XSS、CSRF等" />
</div>
<div class="form-group">
<label for="vulnerability-target">目标</label>
<input type="text" id="vulnerability-target" placeholder="受影响的目标(URL、IP地址等)" />
</div>
<div class="form-group">
<label for="vulnerability-proof">证明(POC</label>
<textarea id="vulnerability-proof" rows="5" placeholder="漏洞证明,如请求/响应、截图等"></textarea>
</div>
<div class="form-group">
<label for="vulnerability-impact">影响</label>
<textarea id="vulnerability-impact" rows="3" placeholder="漏洞影响说明"></textarea>
</div>
<div class="form-group">
<label for="vulnerability-recommendation">修复建议</label>
<textarea id="vulnerability-recommendation" rows="3" placeholder="修复建议"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeVulnerabilityModal()">取消</button>
<button class="btn-primary" onclick="saveVulnerability()">保存</button>
</div>
</div>
</div>
<script src="/static/js/auth.js"></script>
<script src="/static/js/router.js"></script>
<script src="/static/js/monitor.js"></script>
<script src="/static/js/chat.js"></script>
<script src="/static/js/settings.js"></script>
<script src="/static/js/knowledge.js"></script>
<script src="/static/js/vulnerability.js?v=4"></script>
</body>
</html>