mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 10:16:32 +02:00
Add files via upload
This commit is contained in:
+54
-16
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()">×</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user