mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-30 21:20:12 +02:00
Add files via upload
This commit is contained in:
9 files changed
+1276
-272
No files matched your search
+3
-4
@@ -60,8 +60,8 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
|||||||
return nil, fmt.Errorf("初始化数据库失败: %w", err)
|
return nil, fmt.Errorf("初始化数据库失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建MCP服务器
|
// 创建MCP服务器(带数据库持久化)
|
||||||
mcpServer := mcp.NewServer(log.Logger)
|
mcpServer := mcp.NewServerWithStorage(log.Logger, db)
|
||||||
|
|
||||||
// 创建安全工具执行器
|
// 创建安全工具执行器
|
||||||
executor := security.NewExecutor(&cfg.Security, mcpServer, log.Logger)
|
executor := security.NewExecutor(&cfg.Security, mcpServer, log.Logger)
|
||||||
@@ -91,7 +91,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
|||||||
|
|
||||||
// 创建处理器
|
// 创建处理器
|
||||||
agentHandler := handler.NewAgentHandler(agent, db, log.Logger)
|
agentHandler := handler.NewAgentHandler(agent, db, log.Logger)
|
||||||
monitorHandler := handler.NewMonitorHandler(mcpServer, executor, log.Logger)
|
monitorHandler := handler.NewMonitorHandler(mcpServer, executor, db, log.Logger)
|
||||||
conversationHandler := handler.NewConversationHandler(db, log.Logger)
|
conversationHandler := handler.NewConversationHandler(db, log.Logger)
|
||||||
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
|
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
|
||||||
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, log.Logger)
|
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, log.Logger)
|
||||||
@@ -188,7 +188,6 @@ func setupRoutes(
|
|||||||
protected.GET("/monitor", monitorHandler.Monitor)
|
protected.GET("/monitor", monitorHandler.Monitor)
|
||||||
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
|
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
|
||||||
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
protected.GET("/monitor/stats", monitorHandler.GetStats)
|
||||||
protected.GET("/monitor/vulnerabilities", monitorHandler.GetVulnerabilities)
|
|
||||||
|
|
||||||
// 配置管理
|
// 配置管理
|
||||||
protected.GET("/config", configHandler.GetConfig)
|
protected.GET("/config", configHandler.GetConfig)
|
||||||
|
|||||||
@@ -75,12 +75,41 @@ func (db *DB) initTables() error {
|
|||||||
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
||||||
);`
|
);`
|
||||||
|
|
||||||
|
// 创建工具执行记录表
|
||||||
|
createToolExecutionsTable := `
|
||||||
|
CREATE TABLE IF NOT EXISTS tool_executions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
tool_name TEXT NOT NULL,
|
||||||
|
arguments TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
result TEXT,
|
||||||
|
error TEXT,
|
||||||
|
start_time DATETIME NOT NULL,
|
||||||
|
end_time DATETIME,
|
||||||
|
duration_ms INTEGER,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);`
|
||||||
|
|
||||||
|
// 创建工具统计表
|
||||||
|
createToolStatsTable := `
|
||||||
|
CREATE TABLE IF NOT EXISTS tool_stats (
|
||||||
|
tool_name TEXT PRIMARY KEY,
|
||||||
|
total_calls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
success_calls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
failed_calls INTEGER NOT NULL DEFAULT 0,
|
||||||
|
last_call_time DATETIME,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);`
|
||||||
|
|
||||||
// 创建索引
|
// 创建索引
|
||||||
createIndexes := `
|
createIndexes := `
|
||||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at);
|
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at);
|
||||||
CREATE INDEX IF NOT EXISTS idx_process_details_message_id ON process_details(message_id);
|
CREATE INDEX IF NOT EXISTS idx_process_details_message_id ON process_details(message_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_process_details_conversation_id ON process_details(conversation_id);
|
CREATE INDEX IF NOT EXISTS idx_process_details_conversation_id ON process_details(conversation_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_executions_tool_name ON tool_executions(tool_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_executions_start_time ON tool_executions(start_time);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_tool_executions_status ON tool_executions(status);
|
||||||
`
|
`
|
||||||
|
|
||||||
if _, err := db.Exec(createConversationsTable); err != nil {
|
if _, err := db.Exec(createConversationsTable); err != nil {
|
||||||
@@ -95,6 +124,14 @@ func (db *DB) initTables() error {
|
|||||||
return fmt.Errorf("创建process_details表失败: %w", err)
|
return fmt.Errorf("创建process_details表失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := db.Exec(createToolExecutionsTable); err != nil {
|
||||||
|
return fmt.Errorf("创建tool_executions表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := db.Exec(createToolStatsTable); err != nil {
|
||||||
|
return fmt.Errorf("创建tool_stats表失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := db.Exec(createIndexes); err != nil {
|
if _, err := db.Exec(createIndexes); err != nil {
|
||||||
return fmt.Errorf("创建索引失败: %w", err)
|
return fmt.Errorf("创建索引失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/mcp"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveToolExecution 保存工具执行记录
|
||||||
|
func (db *DB) SaveToolExecution(exec *mcp.ToolExecution) error {
|
||||||
|
argsJSON, err := json.Marshal(exec.Arguments)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Warn("序列化执行参数失败", zap.Error(err))
|
||||||
|
argsJSON = []byte("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultJSON sql.NullString
|
||||||
|
if exec.Result != nil {
|
||||||
|
resultBytes, err := json.Marshal(exec.Result)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Warn("序列化执行结果失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
resultJSON = sql.NullString{String: string(resultBytes), Valid: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorText sql.NullString
|
||||||
|
if exec.Error != "" {
|
||||||
|
errorText = sql.NullString{String: exec.Error, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var endTime sql.NullTime
|
||||||
|
if exec.EndTime != nil {
|
||||||
|
endTime = sql.NullTime{Time: *exec.EndTime, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var durationMs sql.NullInt64
|
||||||
|
if exec.Duration > 0 {
|
||||||
|
durationMs = sql.NullInt64{Int64: exec.Duration.Milliseconds(), Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT OR REPLACE INTO tool_executions
|
||||||
|
(id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err = db.Exec(query,
|
||||||
|
exec.ID,
|
||||||
|
exec.ToolName,
|
||||||
|
string(argsJSON),
|
||||||
|
exec.Status,
|
||||||
|
resultJSON,
|
||||||
|
errorText,
|
||||||
|
exec.StartTime,
|
||||||
|
endTime,
|
||||||
|
durationMs,
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Error("保存工具执行记录失败", zap.Error(err), zap.String("executionId", exec.ID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadToolExecutions 加载所有工具执行记录
|
||||||
|
func (db *DB) LoadToolExecutions() ([]*mcp.ToolExecution, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms
|
||||||
|
FROM tool_executions
|
||||||
|
ORDER BY start_time DESC
|
||||||
|
LIMIT 1000
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var executions []*mcp.ToolExecution
|
||||||
|
for rows.Next() {
|
||||||
|
var exec mcp.ToolExecution
|
||||||
|
var argsJSON string
|
||||||
|
var resultJSON sql.NullString
|
||||||
|
var errorText sql.NullString
|
||||||
|
var endTime sql.NullTime
|
||||||
|
var durationMs sql.NullInt64
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&exec.ID,
|
||||||
|
&exec.ToolName,
|
||||||
|
&argsJSON,
|
||||||
|
&exec.Status,
|
||||||
|
&resultJSON,
|
||||||
|
&errorText,
|
||||||
|
&exec.StartTime,
|
||||||
|
&endTime,
|
||||||
|
&durationMs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Warn("加载执行记录失败", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析参数
|
||||||
|
if err := json.Unmarshal([]byte(argsJSON), &exec.Arguments); err != nil {
|
||||||
|
db.logger.Warn("解析执行参数失败", zap.Error(err))
|
||||||
|
exec.Arguments = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析结果
|
||||||
|
if resultJSON.Valid && resultJSON.String != "" {
|
||||||
|
var result mcp.ToolResult
|
||||||
|
if err := json.Unmarshal([]byte(resultJSON.String), &result); err != nil {
|
||||||
|
db.logger.Warn("解析执行结果失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
exec.Result = &result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置错误
|
||||||
|
if errorText.Valid {
|
||||||
|
exec.Error = errorText.String
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置结束时间
|
||||||
|
if endTime.Valid {
|
||||||
|
exec.EndTime = &endTime.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置持续时间
|
||||||
|
if durationMs.Valid {
|
||||||
|
exec.Duration = time.Duration(durationMs.Int64) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
executions = append(executions, &exec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetToolExecution 根据ID获取单条工具执行记录
|
||||||
|
func (db *DB) GetToolExecution(id string) (*mcp.ToolExecution, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms
|
||||||
|
FROM tool_executions
|
||||||
|
WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
row := db.QueryRow(query, id)
|
||||||
|
|
||||||
|
var exec mcp.ToolExecution
|
||||||
|
var argsJSON string
|
||||||
|
var resultJSON sql.NullString
|
||||||
|
var errorText sql.NullString
|
||||||
|
var endTime sql.NullTime
|
||||||
|
var durationMs sql.NullInt64
|
||||||
|
|
||||||
|
err := row.Scan(
|
||||||
|
&exec.ID,
|
||||||
|
&exec.ToolName,
|
||||||
|
&argsJSON,
|
||||||
|
&exec.Status,
|
||||||
|
&resultJSON,
|
||||||
|
&errorText,
|
||||||
|
&exec.StartTime,
|
||||||
|
&endTime,
|
||||||
|
&durationMs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(argsJSON), &exec.Arguments); err != nil {
|
||||||
|
db.logger.Warn("解析执行参数失败", zap.Error(err))
|
||||||
|
exec.Arguments = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultJSON.Valid && resultJSON.String != "" {
|
||||||
|
var result mcp.ToolResult
|
||||||
|
if err := json.Unmarshal([]byte(resultJSON.String), &result); err != nil {
|
||||||
|
db.logger.Warn("解析执行结果失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
exec.Result = &result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorText.Valid {
|
||||||
|
exec.Error = errorText.String
|
||||||
|
}
|
||||||
|
|
||||||
|
if endTime.Valid {
|
||||||
|
exec.EndTime = &endTime.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
if durationMs.Valid {
|
||||||
|
exec.Duration = time.Duration(durationMs.Int64) * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
return &exec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveToolStats 保存工具统计信息
|
||||||
|
func (db *DB) SaveToolStats(toolName string, stats *mcp.ToolStats) error {
|
||||||
|
var lastCallTime sql.NullTime
|
||||||
|
if stats.LastCallTime != nil {
|
||||||
|
lastCallTime = sql.NullTime{Time: *stats.LastCallTime, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT OR REPLACE INTO tool_stats
|
||||||
|
(tool_name, total_calls, success_calls, failed_calls, last_call_time, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := db.Exec(query,
|
||||||
|
toolName,
|
||||||
|
stats.TotalCalls,
|
||||||
|
stats.SuccessCalls,
|
||||||
|
stats.FailedCalls,
|
||||||
|
lastCallTime,
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Error("保存工具统计信息失败", zap.Error(err), zap.String("toolName", toolName))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadToolStats 加载所有工具统计信息
|
||||||
|
func (db *DB) LoadToolStats() (map[string]*mcp.ToolStats, error) {
|
||||||
|
query := `
|
||||||
|
SELECT tool_name, total_calls, success_calls, failed_calls, last_call_time
|
||||||
|
FROM tool_stats
|
||||||
|
`
|
||||||
|
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
stats := make(map[string]*mcp.ToolStats)
|
||||||
|
for rows.Next() {
|
||||||
|
var stat mcp.ToolStats
|
||||||
|
var lastCallTime sql.NullTime
|
||||||
|
|
||||||
|
err := rows.Scan(
|
||||||
|
&stat.ToolName,
|
||||||
|
&stat.TotalCalls,
|
||||||
|
&stat.SuccessCalls,
|
||||||
|
&stat.FailedCalls,
|
||||||
|
&lastCallTime,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Warn("加载统计信息失败", zap.Error(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastCallTime.Valid {
|
||||||
|
stat.LastCallTime = &lastCallTime.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
stats[stat.ToolName] = &stat
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateToolStats 更新工具统计信息(累加模式)
|
||||||
|
func (db *DB) UpdateToolStats(toolName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error {
|
||||||
|
var lastCallTimeSQL sql.NullTime
|
||||||
|
if lastCallTime != nil {
|
||||||
|
lastCallTimeSQL = sql.NullTime{Time: *lastCallTime, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT INTO tool_stats (tool_name, total_calls, success_calls, failed_calls, last_call_time, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(tool_name) DO UPDATE SET
|
||||||
|
total_calls = total_calls + ?,
|
||||||
|
success_calls = success_calls + ?,
|
||||||
|
failed_calls = failed_calls + ?,
|
||||||
|
last_call_time = COALESCE(?, last_call_time),
|
||||||
|
updated_at = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := db.Exec(query,
|
||||||
|
toolName, totalCalls, successCalls, failedCalls, lastCallTimeSQL, time.Now(),
|
||||||
|
totalCalls, successCalls, failedCalls, lastCallTimeSQL, time.Now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
db.logger.Error("更新工具统计信息失败", zap.Error(err), zap.String("toolName", toolName))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+40
-36
@@ -4,6 +4,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"cyberstrike-ai/internal/database"
|
||||||
"cyberstrike-ai/internal/mcp"
|
"cyberstrike-ai/internal/mcp"
|
||||||
"cyberstrike-ai/internal/security"
|
"cyberstrike-ai/internal/security"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -14,57 +15,66 @@ import (
|
|||||||
type MonitorHandler struct {
|
type MonitorHandler struct {
|
||||||
mcpServer *mcp.Server
|
mcpServer *mcp.Server
|
||||||
executor *security.Executor
|
executor *security.Executor
|
||||||
|
db *database.DB
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
vulns []security.Vulnerability
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMonitorHandler 创建新的监控处理器
|
// NewMonitorHandler 创建新的监控处理器
|
||||||
func NewMonitorHandler(mcpServer *mcp.Server, executor *security.Executor, logger *zap.Logger) *MonitorHandler {
|
func NewMonitorHandler(mcpServer *mcp.Server, executor *security.Executor, db *database.DB, logger *zap.Logger) *MonitorHandler {
|
||||||
return &MonitorHandler{
|
return &MonitorHandler{
|
||||||
mcpServer: mcpServer,
|
mcpServer: mcpServer,
|
||||||
executor: executor,
|
executor: executor,
|
||||||
|
db: db,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
vulns: []security.Vulnerability{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonitorResponse 监控响应
|
// MonitorResponse 监控响应
|
||||||
type MonitorResponse struct {
|
type MonitorResponse struct {
|
||||||
Executions []*mcp.ToolExecution `json:"executions"`
|
Executions []*mcp.ToolExecution `json:"executions"`
|
||||||
Stats map[string]*mcp.ToolStats `json:"stats"`
|
Stats map[string]*mcp.ToolStats `json:"stats"`
|
||||||
Vulnerabilities []security.Vulnerability `json:"vulnerabilities"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Report map[string]interface{} `json:"report"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitor 获取监控信息
|
// Monitor 获取监控信息
|
||||||
func (h *MonitorHandler) Monitor(c *gin.Context) {
|
func (h *MonitorHandler) Monitor(c *gin.Context) {
|
||||||
// 获取所有执行记录
|
executions := h.loadExecutions()
|
||||||
executions := h.mcpServer.GetAllExecutions()
|
stats := h.loadStats()
|
||||||
|
|
||||||
// 分析执行结果,提取漏洞
|
|
||||||
for _, exec := range executions {
|
|
||||||
if exec.Status == "completed" && exec.Result != nil {
|
|
||||||
vulns := h.executor.AnalyzeResults(exec.ToolName, exec.Result)
|
|
||||||
h.vulns = append(h.vulns, vulns...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取统计信息
|
|
||||||
stats := h.mcpServer.GetStats()
|
|
||||||
|
|
||||||
// 生成报告
|
|
||||||
report := h.executor.GetVulnerabilityReport(h.vulns)
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, MonitorResponse{
|
c.JSON(http.StatusOK, MonitorResponse{
|
||||||
Executions: executions,
|
Executions: executions,
|
||||||
Stats: stats,
|
Stats: stats,
|
||||||
Vulnerabilities: h.vulns,
|
Timestamp: time.Now(),
|
||||||
Report: report,
|
|
||||||
Timestamp: time.Now(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *MonitorHandler) loadExecutions() []*mcp.ToolExecution {
|
||||||
|
if h.db == nil {
|
||||||
|
return h.mcpServer.GetAllExecutions()
|
||||||
|
}
|
||||||
|
|
||||||
|
executions, err := h.db.LoadToolExecutions()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("从数据库加载执行记录失败,回退到内存数据", zap.Error(err))
|
||||||
|
return h.mcpServer.GetAllExecutions()
|
||||||
|
}
|
||||||
|
return executions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MonitorHandler) loadStats() map[string]*mcp.ToolStats {
|
||||||
|
if h.db == nil {
|
||||||
|
return h.mcpServer.GetStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := h.db.LoadToolStats()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Warn("从数据库加载统计信息失败,回退到内存数据", zap.Error(err))
|
||||||
|
return h.mcpServer.GetStats()
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// GetExecution 获取特定执行记录
|
// GetExecution 获取特定执行记录
|
||||||
func (h *MonitorHandler) GetExecution(c *gin.Context) {
|
func (h *MonitorHandler) GetExecution(c *gin.Context) {
|
||||||
id := c.Param("id")
|
id := c.Param("id")
|
||||||
@@ -80,13 +90,7 @@ func (h *MonitorHandler) GetExecution(c *gin.Context) {
|
|||||||
|
|
||||||
// GetStats 获取统计信息
|
// GetStats 获取统计信息
|
||||||
func (h *MonitorHandler) GetStats(c *gin.Context) {
|
func (h *MonitorHandler) GetStats(c *gin.Context) {
|
||||||
stats := h.mcpServer.GetStats()
|
stats := h.loadStats()
|
||||||
c.JSON(http.StatusOK, stats)
|
c.JSON(http.StatusOK, stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVulnerabilities 获取漏洞列表
|
|
||||||
func (h *MonitorHandler) GetVulnerabilities(c *gin.Context) {
|
|
||||||
report := h.executor.GetVulnerabilityReport(h.vulns)
|
|
||||||
c.JSON(http.StatusOK, report)
|
|
||||||
}
|
|
||||||
|
|
||||||
+284
-145
@@ -15,14 +15,25 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MonitorStorage 监控数据存储接口
|
||||||
|
type MonitorStorage interface {
|
||||||
|
SaveToolExecution(exec *ToolExecution) error
|
||||||
|
LoadToolExecutions() ([]*ToolExecution, error)
|
||||||
|
GetToolExecution(id string) (*ToolExecution, error)
|
||||||
|
SaveToolStats(toolName string, stats *ToolStats) error
|
||||||
|
LoadToolStats() (map[string]*ToolStats, error)
|
||||||
|
UpdateToolStats(toolName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error
|
||||||
|
}
|
||||||
|
|
||||||
// Server MCP服务器
|
// Server MCP服务器
|
||||||
type Server struct {
|
type Server struct {
|
||||||
tools map[string]ToolHandler
|
tools map[string]ToolHandler
|
||||||
toolDefs map[string]Tool // 工具定义
|
toolDefs map[string]Tool // 工具定义
|
||||||
executions map[string]*ToolExecution
|
executions map[string]*ToolExecution
|
||||||
stats map[string]*ToolStats
|
stats map[string]*ToolStats
|
||||||
prompts map[string]*Prompt // 提示词模板
|
prompts map[string]*Prompt // 提示词模板
|
||||||
resources map[string]*Resource // 资源
|
resources map[string]*Resource // 资源
|
||||||
|
storage MonitorStorage // 可选的持久化存储
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
@@ -32,6 +43,11 @@ type ToolHandler func(ctx context.Context, args map[string]interface{}) (*ToolRe
|
|||||||
|
|
||||||
// NewServer 创建新的MCP服务器
|
// NewServer 创建新的MCP服务器
|
||||||
func NewServer(logger *zap.Logger) *Server {
|
func NewServer(logger *zap.Logger) *Server {
|
||||||
|
return NewServerWithStorage(logger, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServerWithStorage 创建新的MCP服务器(带持久化存储)
|
||||||
|
func NewServerWithStorage(logger *zap.Logger, storage MonitorStorage) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
tools: make(map[string]ToolHandler),
|
tools: make(map[string]ToolHandler),
|
||||||
toolDefs: make(map[string]Tool),
|
toolDefs: make(map[string]Tool),
|
||||||
@@ -39,6 +55,7 @@ func NewServer(logger *zap.Logger) *Server {
|
|||||||
stats: make(map[string]*ToolStats),
|
stats: make(map[string]*ToolStats),
|
||||||
prompts: make(map[string]*Prompt),
|
prompts: make(map[string]*Prompt),
|
||||||
resources: make(map[string]*Resource),
|
resources: make(map[string]*Resource),
|
||||||
|
storage: storage,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +256,6 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建执行记录
|
|
||||||
executionID := uuid.New().String()
|
executionID := uuid.New().String()
|
||||||
execution := &ToolExecution{
|
execution := &ToolExecution{
|
||||||
ID: executionID,
|
ID: executionID,
|
||||||
@@ -253,10 +269,12 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
s.executions[executionID] = execution
|
s.executions[executionID] = execution
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
// 更新统计
|
if s.storage != nil {
|
||||||
s.updateStats(req.Name, false)
|
if err := s.storage.SaveToolExecution(execution); err != nil {
|
||||||
|
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 执行工具
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
handler, exists := s.tools[req.Name]
|
handler, exists := s.tools[req.Name]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
@@ -266,6 +284,19 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
execution.Error = "Tool not found"
|
execution.Error = "Tool not found"
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
execution.EndTime = &now
|
execution.EndTime = &now
|
||||||
|
execution.Duration = now.Sub(execution.StartTime)
|
||||||
|
|
||||||
|
if s.storage != nil {
|
||||||
|
if err := s.storage.SaveToolExecution(execution); err != nil {
|
||||||
|
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.executions, executionID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.updateStats(req.Name, true)
|
||||||
|
|
||||||
return &Message{
|
return &Message{
|
||||||
ID: msg.ID,
|
ID: msg.ID,
|
||||||
Type: MessageTypeError,
|
Type: MessageTypeError,
|
||||||
@@ -274,7 +305,6 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同步执行所有工具,确保错误能正确返回
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -284,24 +314,63 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
)
|
)
|
||||||
|
|
||||||
result, err := handler(ctx, req.Arguments)
|
result, err := handler(ctx, req.Arguments)
|
||||||
|
now := time.Now()
|
||||||
|
var failed bool
|
||||||
|
var finalResult *ToolResult
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
now := time.Now()
|
|
||||||
execution.EndTime = &now
|
execution.EndTime = &now
|
||||||
execution.Duration = now.Sub(execution.StartTime)
|
execution.Duration = now.Sub(execution.StartTime)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
execution.Status = "failed"
|
execution.Status = "failed"
|
||||||
execution.Error = err.Error()
|
execution.Error = err.Error()
|
||||||
s.updateStats(req.Name, true)
|
failed = true
|
||||||
s.mu.Unlock()
|
} else if result != nil && result.IsError {
|
||||||
|
execution.Status = "failed"
|
||||||
|
if len(result.Content) > 0 {
|
||||||
|
execution.Error = result.Content[0].Text
|
||||||
|
} else {
|
||||||
|
execution.Error = "工具执行返回错误结果"
|
||||||
|
}
|
||||||
|
execution.Result = result
|
||||||
|
failed = true
|
||||||
|
} else {
|
||||||
|
execution.Status = "completed"
|
||||||
|
if result == nil {
|
||||||
|
result = &ToolResult{
|
||||||
|
Content: []Content{
|
||||||
|
{Type: "text", Text: "工具执行完成,但未返回结果"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
execution.Result = result
|
||||||
|
failed = false
|
||||||
|
}
|
||||||
|
|
||||||
|
finalResult = execution.Result
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.storage != nil {
|
||||||
|
if err := s.storage.SaveToolExecution(execution); err != nil {
|
||||||
|
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.updateStats(req.Name, failed)
|
||||||
|
|
||||||
|
if s.storage != nil {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.executions, executionID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
s.logger.Error("工具执行失败",
|
s.logger.Error("工具执行失败",
|
||||||
zap.String("toolName", req.Name),
|
zap.String("toolName", req.Name),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 返回错误结果
|
|
||||||
errorResult, _ := json.Marshal(CallToolResponse{
|
errorResult, _ := json.Marshal(CallToolResponse{
|
||||||
Content: []Content{
|
Content: []Content{
|
||||||
{Type: "text", Text: fmt.Sprintf("工具执行失败: %v", err)},
|
{Type: "text", Text: fmt.Sprintf("工具执行失败: %v", err)},
|
||||||
@@ -316,23 +385,25 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查result是否为错误
|
if finalResult != nil && finalResult.IsError {
|
||||||
if result != nil && result.IsError {
|
s.logger.Warn("工具执行返回错误结果",
|
||||||
execution.Status = "failed"
|
zap.String("toolName", req.Name),
|
||||||
if len(result.Content) > 0 {
|
)
|
||||||
execution.Error = result.Content[0].Text
|
|
||||||
}
|
|
||||||
s.updateStats(req.Name, true)
|
|
||||||
} else {
|
|
||||||
execution.Status = "completed"
|
|
||||||
execution.Result = result
|
|
||||||
s.updateStats(req.Name, false)
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
// 返回执行结果
|
errorResult, _ := json.Marshal(CallToolResponse{
|
||||||
if result == nil {
|
Content: finalResult.Content,
|
||||||
result = &ToolResult{
|
IsError: true,
|
||||||
|
})
|
||||||
|
return &Message{
|
||||||
|
ID: msg.ID,
|
||||||
|
Type: MessageTypeResponse,
|
||||||
|
Version: "2.0",
|
||||||
|
Result: errorResult,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if finalResult == nil {
|
||||||
|
finalResult = &ToolResult{
|
||||||
Content: []Content{
|
Content: []Content{
|
||||||
{Type: "text", Text: "工具执行完成,但未返回结果"},
|
{Type: "text", Text: "工具执行完成,但未返回结果"},
|
||||||
},
|
},
|
||||||
@@ -340,13 +411,13 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resultJSON, _ := json.Marshal(CallToolResponse{
|
resultJSON, _ := json.Marshal(CallToolResponse{
|
||||||
Content: result.Content,
|
Content: finalResult.Content,
|
||||||
IsError: result.IsError,
|
IsError: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
s.logger.Info("工具执行完成",
|
s.logger.Info("工具执行完成",
|
||||||
zap.String("toolName", req.Name),
|
zap.String("toolName", req.Name),
|
||||||
zap.Bool("isError", result.IsError),
|
zap.Bool("isError", finalResult.IsError),
|
||||||
)
|
)
|
||||||
|
|
||||||
return &Message{
|
return &Message{
|
||||||
@@ -359,6 +430,25 @@ func (s *Server) handleCallTool(msg *Message) *Message {
|
|||||||
|
|
||||||
// updateStats 更新统计信息
|
// updateStats 更新统计信息
|
||||||
func (s *Server) updateStats(toolName string, failed bool) {
|
func (s *Server) updateStats(toolName string, failed bool) {
|
||||||
|
now := time.Now()
|
||||||
|
if s.storage != nil {
|
||||||
|
totalCalls := 1
|
||||||
|
successCalls := 0
|
||||||
|
failedCalls := 0
|
||||||
|
if failed {
|
||||||
|
failedCalls = 1
|
||||||
|
} else {
|
||||||
|
successCalls = 1
|
||||||
|
}
|
||||||
|
if err := s.storage.UpdateToolStats(toolName, totalCalls, successCalls, failedCalls, &now); err != nil {
|
||||||
|
s.logger.Warn("保存统计信息到数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
if s.stats[toolName] == nil {
|
if s.stats[toolName] == nil {
|
||||||
s.stats[toolName] = &ToolStats{
|
s.stats[toolName] = &ToolStats{
|
||||||
ToolName: toolName,
|
ToolName: toolName,
|
||||||
@@ -367,7 +457,6 @@ func (s *Server) updateStats(toolName string, failed bool) {
|
|||||||
|
|
||||||
stats := s.stats[toolName]
|
stats := s.stats[toolName]
|
||||||
stats.TotalCalls++
|
stats.TotalCalls++
|
||||||
now := time.Now()
|
|
||||||
stats.LastCallTime = &now
|
stats.LastCallTime = &now
|
||||||
|
|
||||||
if failed {
|
if failed {
|
||||||
@@ -377,34 +466,122 @@ func (s *Server) updateStats(toolName string, failed bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExecution 获取执行记录
|
// GetExecution 获取执行记录(先从内存查找,再从数据库查找)
|
||||||
func (s *Server) GetExecution(id string) (*ToolExecution, bool) {
|
func (s *Server) GetExecution(id string) (*ToolExecution, bool) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
|
||||||
exec, exists := s.executions[id]
|
exec, exists := s.executions[id]
|
||||||
return exec, exists
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return exec, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.storage != nil {
|
||||||
|
exec, err := s.storage.GetToolExecution(id)
|
||||||
|
if err == nil {
|
||||||
|
return exec, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllExecutions 获取所有执行记录
|
// loadHistoricalData 从数据库加载历史数据
|
||||||
|
func (s *Server) loadHistoricalData() {
|
||||||
|
if s.storage == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载历史执行记录(最近1000条)
|
||||||
|
executions, err := s.storage.LoadToolExecutions()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("加载历史执行记录失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
s.mu.Lock()
|
||||||
|
for _, exec := range executions {
|
||||||
|
// 只加载最近1000条,避免内存占用过大
|
||||||
|
if len(s.executions) < 1000 {
|
||||||
|
s.executions[exec.ID] = exec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
s.logger.Info("加载历史执行记录", zap.Int("count", len(executions)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载历史统计信息
|
||||||
|
stats, err := s.storage.LoadToolStats()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("加载历史统计信息失败", zap.Error(err))
|
||||||
|
} else {
|
||||||
|
s.mu.Lock()
|
||||||
|
for k, v := range stats {
|
||||||
|
s.stats[k] = v
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
s.logger.Info("加载历史统计信息", zap.Int("count", len(stats)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllExecutions 获取所有执行记录(合并内存和数据库)
|
||||||
func (s *Server) GetAllExecutions() []*ToolExecution {
|
func (s *Server) GetAllExecutions() []*ToolExecution {
|
||||||
|
if s.storage != nil {
|
||||||
|
dbExecutions, err := s.storage.LoadToolExecutions()
|
||||||
|
if err == nil {
|
||||||
|
execMap := make(map[string]*ToolExecution)
|
||||||
|
for _, exec := range dbExecutions {
|
||||||
|
if _, exists := execMap[exec.ID]; !exists {
|
||||||
|
execMap[exec.ID] = exec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.RLock()
|
||||||
|
for id, exec := range s.executions {
|
||||||
|
if _, exists := execMap[id]; !exists {
|
||||||
|
execMap[id] = exec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]*ToolExecution, 0, len(execMap))
|
||||||
|
for _, exec := range execMap {
|
||||||
|
result = append(result, exec)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
s.logger.Warn("从数据库加载执行记录失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
executions := make([]*ToolExecution, 0, len(s.executions))
|
|
||||||
|
memExecutions := make([]*ToolExecution, 0, len(s.executions))
|
||||||
for _, exec := range s.executions {
|
for _, exec := range s.executions {
|
||||||
executions = append(executions, exec)
|
memExecutions = append(memExecutions, exec)
|
||||||
}
|
}
|
||||||
return executions
|
return memExecutions
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats 获取统计信息
|
// GetStats 获取统计信息(合并内存和数据库)
|
||||||
func (s *Server) GetStats() map[string]*ToolStats {
|
func (s *Server) GetStats() map[string]*ToolStats {
|
||||||
|
if s.storage != nil {
|
||||||
|
dbStats, err := s.storage.LoadToolStats()
|
||||||
|
if err == nil {
|
||||||
|
return dbStats
|
||||||
|
}
|
||||||
|
s.logger.Warn("从数据库加载统计信息失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
stats := make(map[string]*ToolStats)
|
|
||||||
|
memStats := make(map[string]*ToolStats)
|
||||||
for k, v := range s.stats {
|
for k, v := range s.stats {
|
||||||
stats[k] = v
|
statCopy := *v
|
||||||
|
memStats[k] = &statCopy
|
||||||
}
|
}
|
||||||
return stats
|
|
||||||
|
return memStats
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllTools 获取所有已注册的工具(用于Agent动态获取工具列表)
|
// GetAllTools 获取所有已注册的工具(用于Agent动态获取工具列表)
|
||||||
@@ -443,30 +620,73 @@ func (s *Server) CallTool(ctx context.Context, toolName string, args map[string]
|
|||||||
s.executions[executionID] = execution
|
s.executions[executionID] = execution
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
// 更新统计
|
if s.storage != nil {
|
||||||
s.updateStats(toolName, false)
|
if err := s.storage.SaveToolExecution(execution); err != nil {
|
||||||
|
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 执行工具
|
|
||||||
result, err := handler(ctx, args)
|
result, err := handler(ctx, args)
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
execution.EndTime = &now
|
execution.EndTime = &now
|
||||||
execution.Duration = now.Sub(execution.StartTime)
|
execution.Duration = now.Sub(execution.StartTime)
|
||||||
|
var failed bool
|
||||||
|
var finalResult *ToolResult
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
execution.Status = "failed"
|
execution.Status = "failed"
|
||||||
execution.Error = err.Error()
|
execution.Error = err.Error()
|
||||||
s.updateStats(toolName, true)
|
failed = true
|
||||||
s.mu.Unlock()
|
} else if result != nil && result.IsError {
|
||||||
return nil, executionID, err
|
execution.Status = "failed"
|
||||||
|
if len(result.Content) > 0 {
|
||||||
|
execution.Error = result.Content[0].Text
|
||||||
|
} else {
|
||||||
|
execution.Error = "工具执行返回错误结果"
|
||||||
|
}
|
||||||
|
execution.Result = result
|
||||||
|
failed = true
|
||||||
|
finalResult = result
|
||||||
} else {
|
} else {
|
||||||
execution.Status = "completed"
|
execution.Status = "completed"
|
||||||
|
if result == nil {
|
||||||
|
result = &ToolResult{
|
||||||
|
Content: []Content{
|
||||||
|
{Type: "text", Text: "工具执行完成,但未返回结果"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
execution.Result = result
|
execution.Result = result
|
||||||
s.updateStats(toolName, false)
|
finalResult = result
|
||||||
s.mu.Unlock()
|
failed = false
|
||||||
return result, executionID, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if finalResult == nil {
|
||||||
|
finalResult = execution.Result
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.storage != nil {
|
||||||
|
if err := s.storage.SaveToolExecution(execution); err != nil {
|
||||||
|
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.updateStats(toolName, failed)
|
||||||
|
|
||||||
|
if s.storage != nil {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.executions, executionID)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, executionID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalResult, executionID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDefaultPrompts 初始化默认提示词模板
|
// initDefaultPrompts 初始化默认提示词模板
|
||||||
@@ -691,111 +911,31 @@ func (s *Server) generateResourceContent(resource *Resource) ResourceContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateToolDocumentation 生成工具文档
|
// generateToolDocumentation 生成工具文档
|
||||||
|
// 注意:硬编码的工具文档已移除,现在只使用工具定义中的信息
|
||||||
func (s *Server) generateToolDocumentation(toolName string, resource *Resource) string {
|
func (s *Server) generateToolDocumentation(toolName string, resource *Resource) string {
|
||||||
// 获取工具定义以获取更详细的信息
|
// 获取工具定义以获取更详细的信息
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
tool, hasTool := s.toolDefs[toolName]
|
tool, hasTool := s.toolDefs[toolName]
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
||||||
// 为常见工具生成详细文档
|
// 使用工具定义中的描述信息
|
||||||
switch toolName {
|
if hasTool {
|
||||||
case "nmap":
|
doc := fmt.Sprintf("%s\n\n", resource.Description)
|
||||||
return `Nmap (Network Mapper) 是一个强大的网络扫描工具。
|
if tool.InputSchema != nil {
|
||||||
|
if props, ok := tool.InputSchema["properties"].(map[string]interface{}); ok {
|
||||||
主要功能:
|
doc += "参数说明:\n"
|
||||||
- 端口扫描:发现目标主机开放的端口
|
for paramName, paramInfo := range props {
|
||||||
- 服务识别:识别运行在端口上的服务
|
if paramMap, ok := paramInfo.(map[string]interface{}); ok {
|
||||||
- 版本检测:检测服务版本信息
|
if desc, ok := paramMap["description"].(string); ok {
|
||||||
- 操作系统检测:识别目标操作系统
|
doc += fmt.Sprintf("- %s: %s\n", paramName, desc)
|
||||||
|
|
||||||
常用命令:
|
|
||||||
- nmap -sT target # TCP连接扫描
|
|
||||||
- nmap -sV target # 版本检测
|
|
||||||
- nmap -sC target # 默认脚本扫描
|
|
||||||
- nmap -p 1-1000 target # 扫描指定端口范围
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- target: 目标IP地址或域名(必需)
|
|
||||||
- ports: 端口范围,例如: 1-1000(可选)`
|
|
||||||
|
|
||||||
case "sqlmap":
|
|
||||||
return `SQLMap 是一个自动化的SQL注入检测和利用工具。
|
|
||||||
|
|
||||||
主要功能:
|
|
||||||
- 自动检测SQL注入漏洞
|
|
||||||
- 数据库指纹识别
|
|
||||||
- 数据提取
|
|
||||||
- 文件系统访问
|
|
||||||
|
|
||||||
常用命令:
|
|
||||||
- sqlmap -u "http://target.com/page?id=1" # 检测URL参数
|
|
||||||
- sqlmap -u "http://target.com" --forms # 检测表单
|
|
||||||
- sqlmap -u "http://target.com" --dbs # 列出数据库
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- url: 目标URL(必需)`
|
|
||||||
|
|
||||||
case "nikto":
|
|
||||||
return `Nikto 是一个Web服务器扫描工具。
|
|
||||||
|
|
||||||
主要功能:
|
|
||||||
- Web服务器漏洞扫描
|
|
||||||
- 检测过时的服务器软件
|
|
||||||
- 检测危险文件和程序
|
|
||||||
- 检测服务器配置问题
|
|
||||||
|
|
||||||
常用命令:
|
|
||||||
- nikto -h target # 扫描目标主机
|
|
||||||
- nikto -h target -p 80,443 # 扫描指定端口
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- target: 目标URL(必需)`
|
|
||||||
|
|
||||||
case "dirb":
|
|
||||||
return `Dirb 是一个Web内容扫描器。
|
|
||||||
|
|
||||||
主要功能:
|
|
||||||
- 扫描Web目录和文件
|
|
||||||
- 发现隐藏的目录和文件
|
|
||||||
- 支持自定义字典
|
|
||||||
|
|
||||||
常用命令:
|
|
||||||
- dirb url # 扫描目标URL
|
|
||||||
- dirb url -w wordlist.txt # 使用自定义字典
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- target: 目标URL(必需)`
|
|
||||||
|
|
||||||
case "exec":
|
|
||||||
return `Exec 工具用于执行系统命令。
|
|
||||||
|
|
||||||
⚠️ 警告:此工具可以执行任意系统命令,请谨慎使用!
|
|
||||||
|
|
||||||
参数说明:
|
|
||||||
- command: 要执行的系统命令(必需)
|
|
||||||
- shell: 使用的shell,默认为sh(可选)
|
|
||||||
- workdir: 工作目录(可选)`
|
|
||||||
|
|
||||||
default:
|
|
||||||
// 对于其他工具,使用工具定义中的描述信息
|
|
||||||
if hasTool {
|
|
||||||
doc := fmt.Sprintf("%s\n\n", resource.Description)
|
|
||||||
if tool.InputSchema != nil {
|
|
||||||
if props, ok := tool.InputSchema["properties"].(map[string]interface{}); ok {
|
|
||||||
doc += "参数说明:\n"
|
|
||||||
for paramName, paramInfo := range props {
|
|
||||||
if paramMap, ok := paramInfo.(map[string]interface{}); ok {
|
|
||||||
if desc, ok := paramMap["description"].(string); ok {
|
|
||||||
doc += fmt.Sprintf("- %s: %s\n", paramName, desc)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return doc
|
|
||||||
}
|
}
|
||||||
return resource.Description
|
return doc
|
||||||
}
|
}
|
||||||
|
return resource.Description
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSamplingRequest 处理采样请求
|
// handleSamplingRequest 处理采样请求
|
||||||
@@ -908,4 +1048,3 @@ func (s *Server) sendError(w http.ResponseWriter, id interface{}, code int, mess
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(response)
|
json.NewEncoder(w).Encode(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,76 +722,12 @@ type Vulnerability struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AnalyzeResults 分析工具执行结果,提取漏洞信息
|
// AnalyzeResults 分析工具执行结果,提取漏洞信息
|
||||||
|
// 注意:硬编码的漏洞解析逻辑已移除,此函数现在返回空数组
|
||||||
|
// 漏洞检测应该由工具本身或专门的漏洞扫描工具来完成
|
||||||
func (e *Executor) AnalyzeResults(toolName string, result *mcp.ToolResult) []Vulnerability {
|
func (e *Executor) AnalyzeResults(toolName string, result *mcp.ToolResult) []Vulnerability {
|
||||||
vulnerabilities := []Vulnerability{}
|
// 不再进行硬编码的漏洞解析
|
||||||
|
// 漏洞检测应该由工具本身(如sqlmap、nmap等)的输出结果来体现
|
||||||
if result.IsError {
|
return []Vulnerability{}
|
||||||
return vulnerabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分析输出内容
|
|
||||||
for _, content := range result.Content {
|
|
||||||
if content.Type == "text" {
|
|
||||||
vulns := e.parseToolOutput(toolName, content.Text)
|
|
||||||
vulnerabilities = append(vulnerabilities, vulns...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vulnerabilities
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseToolOutput 解析工具输出
|
|
||||||
func (e *Executor) parseToolOutput(toolName, output string) []Vulnerability {
|
|
||||||
vulnerabilities := []Vulnerability{}
|
|
||||||
|
|
||||||
// 简单的漏洞检测逻辑
|
|
||||||
outputLower := strings.ToLower(output)
|
|
||||||
|
|
||||||
// SQL注入检测
|
|
||||||
if strings.Contains(outputLower, "sql injection") || strings.Contains(outputLower, "sqli") {
|
|
||||||
vulnerabilities = append(vulnerabilities, Vulnerability{
|
|
||||||
ID: fmt.Sprintf("sql-%d", time.Now().Unix()),
|
|
||||||
Type: "SQL Injection",
|
|
||||||
Severity: "high",
|
|
||||||
Title: "SQL注入漏洞",
|
|
||||||
Description: "检测到潜在的SQL注入漏洞",
|
|
||||||
FoundAt: time.Now(),
|
|
||||||
Details: output,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// XSS检测
|
|
||||||
if strings.Contains(outputLower, "xss") || strings.Contains(outputLower, "cross-site scripting") {
|
|
||||||
vulnerabilities = append(vulnerabilities, Vulnerability{
|
|
||||||
ID: fmt.Sprintf("xss-%d", time.Now().Unix()),
|
|
||||||
Type: "XSS",
|
|
||||||
Severity: "medium",
|
|
||||||
Title: "跨站脚本攻击漏洞",
|
|
||||||
Description: "检测到潜在的XSS漏洞",
|
|
||||||
FoundAt: time.Now(),
|
|
||||||
Details: output,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开放端口检测
|
|
||||||
if toolName == "nmap" {
|
|
||||||
lines := strings.Split(output, "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.Contains(line, "open") && strings.Contains(line, "port") {
|
|
||||||
vulnerabilities = append(vulnerabilities, Vulnerability{
|
|
||||||
ID: fmt.Sprintf("port-%d", time.Now().Unix()),
|
|
||||||
Type: "Open Port",
|
|
||||||
Severity: "low",
|
|
||||||
Title: "开放端口",
|
|
||||||
Description: fmt.Sprintf("发现开放端口: %s", line),
|
|
||||||
FoundAt: time.Now(),
|
|
||||||
Details: line,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vulnerabilities
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVulnerabilityReport 生成漏洞报告
|
// GetVulnerabilityReport 生成漏洞报告
|
||||||
|
|||||||
+306
-14
@@ -97,28 +97,55 @@ header {
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-btn {
|
.header-actions {
|
||||||
background: transparent;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
||||||
color: white;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s;
|
gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-btn:hover {
|
.header-actions button svg {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-btn svg {
|
|
||||||
stroke: currentColor;
|
stroke: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-color: rgba(255, 255, 255, 0.35);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-btn {
|
||||||
|
color: #8cc4ff;
|
||||||
|
border-color: rgba(0, 102, 255, 0.35);
|
||||||
|
background: rgba(0, 102, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-btn:hover {
|
||||||
|
background: rgba(0, 102, 255, 0.25);
|
||||||
|
border-color: rgba(0, 102, 255, 0.45);
|
||||||
|
color: #cfe4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-btn {
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 侧边栏样式 */
|
/* 侧边栏样式 */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 280px;
|
width: 280px;
|
||||||
@@ -1625,3 +1652,268 @@ header {
|
|||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border-color: var(--accent-color);
|
border-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.monitor-modal-content {
|
||||||
|
max-width: 1080px;
|
||||||
|
width: 95%;
|
||||||
|
max-height: 92vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 0 0 16px 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-sections {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-section {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-section .section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-section .section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-section .section-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-section .section-actions select {
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-stat-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid rgba(0, 102, 255, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-stat-card h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-stat-value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-stat-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-table-container {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-table th,
|
||||||
|
.monitor-table td {
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-table thead {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-table tbody tr:hover {
|
||||||
|
background: rgba(0, 102, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-status-chip.completed {
|
||||||
|
background: rgba(40, 167, 69, 0.12);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-status-chip.running {
|
||||||
|
background: rgba(0, 102, 255, 0.12);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-status-chip.failed {
|
||||||
|
background: rgba(220, 53, 69, 0.12);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-execution-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-execution-actions button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-vuln-container {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-vuln-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-counter {
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid rgba(0, 102, 255, 0.12);
|
||||||
|
min-width: 140px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-counter strong {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-counter span {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-list {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-severity {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-severity.critical {
|
||||||
|
background: rgba(108, 0, 150, 0.15);
|
||||||
|
color: #bf00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-severity.high {
|
||||||
|
background: rgba(220, 53, 69, 0.12);
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-severity.medium {
|
||||||
|
background: rgba(255, 193, 7, 0.15);
|
||||||
|
color: #b8860b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vuln-severity.low {
|
||||||
|
background: rgba(40, 167, 69, 0.12);
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitor-error {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
color: var(--error-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: rgba(220, 53, 69, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
+240
-2
@@ -1654,13 +1654,17 @@ function closeSettings() {
|
|||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
const settingsModal = document.getElementById('settings-modal');
|
const settingsModal = document.getElementById('settings-modal');
|
||||||
const mcpModal = document.getElementById('mcp-detail-modal');
|
const mcpModal = document.getElementById('mcp-detail-modal');
|
||||||
|
const monitorModal = document.getElementById('monitor-modal');
|
||||||
|
|
||||||
if (event.target == settingsModal) {
|
if (event.target === settingsModal) {
|
||||||
closeSettings();
|
closeSettings();
|
||||||
}
|
}
|
||||||
if (event.target == mcpModal) {
|
if (event.target === mcpModal) {
|
||||||
closeMCPDetail();
|
closeMCPDetail();
|
||||||
}
|
}
|
||||||
|
if (event.target === monitorModal) {
|
||||||
|
closeMonitorPanel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
@@ -1913,3 +1917,237 @@ async function changePassword() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 监控面板状态
|
||||||
|
const monitorState = {
|
||||||
|
executions: [],
|
||||||
|
stats: {},
|
||||||
|
lastFetchedAt: null
|
||||||
|
};
|
||||||
|
|
||||||
|
function openMonitorPanel() {
|
||||||
|
const modal = document.getElementById('monitor-modal');
|
||||||
|
if (!modal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.style.display = 'block';
|
||||||
|
|
||||||
|
// 重置显示状态
|
||||||
|
const statsContainer = document.getElementById('monitor-stats');
|
||||||
|
const execContainer = document.getElementById('monitor-executions');
|
||||||
|
if (statsContainer) {
|
||||||
|
statsContainer.innerHTML = '<div class="monitor-empty">加载中...</div>';
|
||||||
|
}
|
||||||
|
if (execContainer) {
|
||||||
|
execContainer.innerHTML = '<div class="monitor-empty">加载中...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusFilter = document.getElementById('monitor-status-filter');
|
||||||
|
if (statusFilter) {
|
||||||
|
statusFilter.value = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshMonitorPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMonitorPanel() {
|
||||||
|
const modal = document.getElementById('monitor-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshMonitorPanel() {
|
||||||
|
const statsContainer = document.getElementById('monitor-stats');
|
||||||
|
const execContainer = document.getElementById('monitor-executions');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiFetch('/api/monitor', { method: 'GET' });
|
||||||
|
const result = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.error || '获取监控数据失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorState.executions = Array.isArray(result.executions) ? result.executions : [];
|
||||||
|
monitorState.stats = result.stats || {};
|
||||||
|
monitorState.lastFetchedAt = new Date();
|
||||||
|
|
||||||
|
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
|
||||||
|
renderMonitorExecutions(monitorState.executions);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('刷新监控面板失败:', error);
|
||||||
|
if (statsContainer) {
|
||||||
|
statsContainer.innerHTML = `<div class="monitor-error">无法加载统计信息:${escapeHtml(error.message)}</div>`;
|
||||||
|
}
|
||||||
|
if (execContainer) {
|
||||||
|
execContainer.innerHTML = `<div class="monitor-error">无法加载执行记录:${escapeHtml(error.message)}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMonitorFilters() {
|
||||||
|
const statusFilter = document.getElementById('monitor-status-filter');
|
||||||
|
const status = statusFilter ? statusFilter.value : 'all';
|
||||||
|
renderMonitorExecutions(monitorState.executions, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
|
||||||
|
const container = document.getElementById('monitor-stats');
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.values(statsMap);
|
||||||
|
if (entries.length === 0) {
|
||||||
|
container.innerHTML = '<div class="monitor-empty">暂无统计数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算总体汇总
|
||||||
|
const totals = entries.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc.total += item.totalCalls || 0;
|
||||||
|
acc.success += item.successCalls || 0;
|
||||||
|
acc.failed += item.failedCalls || 0;
|
||||||
|
const lastCall = item.lastCallTime ? new Date(item.lastCallTime) : null;
|
||||||
|
if (lastCall && (!acc.lastCallTime || lastCall > acc.lastCallTime)) {
|
||||||
|
acc.lastCallTime = lastCall;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ total: 0, success: 0, failed: 0, lastCallTime: null }
|
||||||
|
);
|
||||||
|
|
||||||
|
const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0';
|
||||||
|
const lastUpdatedText = lastFetchedAt ? lastFetchedAt.toLocaleString('zh-CN') : 'N/A';
|
||||||
|
const lastCallText = totals.lastCallTime ? totals.lastCallTime.toLocaleString('zh-CN') : '暂无调用';
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<div class="monitor-stat-card">
|
||||||
|
<h4>总调用次数</h4>
|
||||||
|
<div class="monitor-stat-value">${totals.total}</div>
|
||||||
|
<div class="monitor-stat-meta">成功 ${totals.success} / 失败 ${totals.failed}</div>
|
||||||
|
</div>
|
||||||
|
<div class="monitor-stat-card">
|
||||||
|
<h4>成功率</h4>
|
||||||
|
<div class="monitor-stat-value">${successRate}%</div>
|
||||||
|
<div class="monitor-stat-meta">统计自全部工具调用</div>
|
||||||
|
</div>
|
||||||
|
<div class="monitor-stat-card">
|
||||||
|
<h4>最近一次调用</h4>
|
||||||
|
<div class="monitor-stat-value" style="font-size:1rem;">${lastCallText}</div>
|
||||||
|
<div class="monitor-stat-meta">最后刷新时间:${lastUpdatedText}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 显示最多前4个工具的统计
|
||||||
|
const topTools = entries
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
|
||||||
|
.slice(0, 4);
|
||||||
|
|
||||||
|
topTools.forEach(tool => {
|
||||||
|
const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0';
|
||||||
|
html += `
|
||||||
|
<div class="monitor-stat-card">
|
||||||
|
<h4>${escapeHtml(tool.toolName || '未知工具')}</h4>
|
||||||
|
<div class="monitor-stat-value">${tool.totalCalls || 0}</div>
|
||||||
|
<div class="monitor-stat-meta">
|
||||||
|
成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = `<div class="monitor-stats-grid">${html}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||||
|
const container = document.getElementById('monitor-executions');
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(executions) || executions.length === 0) {
|
||||||
|
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedStatus = statusFilter === 'all' ? null : statusFilter;
|
||||||
|
const filtered = normalizedStatus
|
||||||
|
? executions.filter(exec => (exec.status || '').toLowerCase() === normalizedStatus)
|
||||||
|
: executions;
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
container.innerHTML = '<div class="monitor-empty">当前筛选条件下暂无记录</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = filtered
|
||||||
|
.slice(0, 25)
|
||||||
|
.map(exec => {
|
||||||
|
const status = (exec.status || 'unknown').toLowerCase();
|
||||||
|
const statusClass = `monitor-status-chip ${status}`;
|
||||||
|
const statusLabel = getStatusText(status);
|
||||||
|
const startTime = exec.startTime ? new Date(exec.startTime).toLocaleString('zh-CN') : '未知';
|
||||||
|
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
|
||||||
|
const toolName = escapeHtml(exec.toolName || '未知工具');
|
||||||
|
const executionId = escapeHtml(exec.id || '');
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${toolName}</td>
|
||||||
|
<td><span class="${statusClass}">${statusLabel}</span></td>
|
||||||
|
<td>${startTime}</td>
|
||||||
|
<td>${duration}</td>
|
||||||
|
<td>
|
||||||
|
<div class="monitor-execution-actions">
|
||||||
|
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">查看详情</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="monitor-table-container">
|
||||||
|
<table class="monitor-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>工具</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>开始时间</th>
|
||||||
|
<th>耗时</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function formatExecutionDuration(start, end) {
|
||||||
|
if (!start) {
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
const startTime = new Date(start);
|
||||||
|
const endTime = end ? new Date(end) : new Date();
|
||||||
|
if (Number.isNaN(startTime.getTime()) || Number.isNaN(endTime.getTime())) {
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
const diffMs = Math.max(0, endTime - startTime);
|
||||||
|
const seconds = Math.floor(diffMs / 1000);
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${seconds} 秒`;
|
||||||
|
}
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) {
|
||||||
|
const remain = seconds % 60;
|
||||||
|
return remain > 0 ? `${minutes} 分 ${remain} 秒` : `${minutes} 分`;
|
||||||
|
}
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const remainMinutes = minutes % 60;
|
||||||
|
return remainMinutes > 0 ? `${hours} 小时 ${remainMinutes} 分` : `${hours} 小时`;
|
||||||
|
}
|
||||||
@@ -37,12 +37,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<p class="header-subtitle">安全测试平台</p>
|
<p class="header-subtitle">安全测试平台</p>
|
||||||
<button class="settings-btn" onclick="openSettings()" title="设置">
|
<div class="header-actions">
|
||||||
|
<button class="monitor-btn" onclick="openMonitorPanel()" title="MCP 监控面板">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<span>监控</span>
|
||||||
|
</button>
|
||||||
|
<button class="settings-btn" onclick="openSettings()" title="设置">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -155,6 +163,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 监控面板模态框 -->
|
||||||
|
<div id="monitor-modal" class="modal">
|
||||||
|
<div class="modal-content monitor-modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>MCP 监控面板</h2>
|
||||||
|
<span class="modal-close" onclick="closeMonitorPanel()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="monitor-modal-body">
|
||||||
|
<div class="monitor-sections">
|
||||||
|
<section class="monitor-section monitor-overview">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>执行统计</h3>
|
||||||
|
<button class="btn-secondary" onclick="refreshMonitorPanel()">刷新</button>
|
||||||
|
</div>
|
||||||
|
<div id="monitor-stats" class="monitor-stats-grid">
|
||||||
|
<div class="monitor-empty">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="monitor-section monitor-executions">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>最新执行记录</h3>
|
||||||
|
<div class="section-actions">
|
||||||
|
<label>
|
||||||
|
状态筛选
|
||||||
|
<select id="monitor-status-filter" onchange="applyMonitorFilters()">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
<option value="completed">已完成</option>
|
||||||
|
<option value="running">执行中</option>
|
||||||
|
<option value="failed">失败</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="monitor-executions" class="monitor-table-container">
|
||||||
|
<div class="monitor-empty">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- MCP调用详情模态框 -->
|
<!-- MCP调用详情模态框 -->
|
||||||
<div id="mcp-detail-modal" class="modal">
|
<div id="mcp-detail-modal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
Reference in new issue
Block a user