mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 14:07:36 +02:00
Retesting a finding creates an independent conversation branch that
snapshots the source history and model trace, so verification can run
multi-turn without writing to the original session.
- POST /api/vulnerabilities/:id/retest creates the branch and persists the
prompt, so a lost response, navigation or refresh can resume it
- new update_vulnerability_status tool closes the loop; a retest branch may
only read and update its own finding
- {{vulnerability_id}}, {{conversation_id}} and {{source_conversation_id}}
resolve on send, in the first turn and in follow-ups
- evidence appends to a read-only vulnerabilities.retest_log column instead
of retest_notes: that field is operator-authored and the edit form
rewrites it wholesale, so a routine edit used to erase the retest trail
- the retest prompt is prefilled with the operator's retest_notes, which
already states how to verify the fix
- copied messages are re-bound as time.Time. Scanning a DATETIME column into
a string yields RFC3339Nano while AddMessage writes the driver's
space-separated format, and messages sort on the raw created_at text, so a
branch rendered its new turn above the inherited history
87 lines
3.8 KiB
Go
87 lines
3.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"cyberstrike-ai/internal/audit"
|
|
"cyberstrike-ai/internal/security"
|
|
"github.com/gin-gonic/gin"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// CreateRetest creates a sibling snapshot; the chat endpoint runs the new turn.
|
|
func (h *VulnerabilityHandler) CreateRetest(c *gin.Context) {
|
|
var req struct {
|
|
Prompt string `json:"prompt" binding:"required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Prompt) == "" || len(req.Prompt) > 20000 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入复测提示词(最多 20000 字节)"})
|
|
return
|
|
}
|
|
if _, ok := security.CurrentSession(c); !ok {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
|
|
return
|
|
}
|
|
v, err := h.db.GetVulnerability(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "漏洞不存在"})
|
|
return
|
|
}
|
|
source, err := h.db.GetConversation(v.ConversationID)
|
|
if err != nil {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "原始对话已不存在,无法创建复测分支"})
|
|
return
|
|
}
|
|
if session, ok := security.CurrentSession(c); ok {
|
|
if !security.SessionHasPermission(c, "chat:read") || !security.SessionHasPermission(c, "chat:write") || !security.SessionHasPermission(c, "agent:execute") || !security.SessionHasPermission(c, "vulnerability:write") || !h.db.UserCanAccessResource(session.UserID, session.ScopeFor("vulnerability:write"), "vulnerability", v.ID) || !h.db.UserCanAccessResource(session.UserID, session.ScopeFor("chat:read"), "conversation", source.ID) || (source.ProjectID != "" && (!security.SessionHasPermission(c, "project:read") || !h.db.UserCanAccessResource(session.UserID, session.ScopeFor("project:read"), "project", source.ProjectID))) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "无权创建或执行该漏洞的复测分支"})
|
|
return
|
|
}
|
|
}
|
|
if v.ProjectID != "" && v.ProjectID != source.ProjectID {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "漏洞与原始对话所属项目不一致,请先检查项目归属"})
|
|
return
|
|
}
|
|
if h.taskState != nil {
|
|
if running, _ := h.taskState.ConversationTaskRuntimeState(source.ID); running {
|
|
c.JSON(http.StatusConflict, gin.H{"error": "原始对话仍在执行,请等待完成或停止后再复测"})
|
|
return
|
|
}
|
|
}
|
|
meta := audit.ConversationCreateMetaFromGin(c, "vulnerability_retest")
|
|
conv, err := h.db.CreateVulnerabilityRetest(v, meta)
|
|
if err != nil {
|
|
h.logger.Error("创建复测分支失败", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建复测分支失败"})
|
|
return
|
|
}
|
|
if session, ok := security.CurrentSession(c); ok {
|
|
if err = h.db.SetResourceOwner("conversation", conv.ID, session.UserID); err == nil {
|
|
err = h.db.AssignResourceToUser(session.UserID, "conversation", conv.ID)
|
|
}
|
|
if err != nil {
|
|
_ = h.db.DeleteConversation(conv.ID)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "设置分支权限失败"})
|
|
return
|
|
}
|
|
}
|
|
// Continue with the user's message unchanged apart from placeholder resolution.
|
|
// Branch linkage and tool authorization are maintained by the backend.
|
|
prompt, err := h.db.ResolveConversationPlaceholders(conv.ID, strings.TrimSpace(req.Prompt))
|
|
if err != nil {
|
|
_ = h.db.DeleteConversation(conv.ID)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "解析复测提示词失败"})
|
|
return
|
|
}
|
|
if err = h.db.SetVulnerabilityRetestPrompt(conv.ID, prompt); err != nil {
|
|
_ = h.db.DeleteConversation(conv.ID)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存复测提示词失败"})
|
|
return
|
|
}
|
|
if h.audit != nil {
|
|
h.audit.RecordOK(c, "vulnerability", "retest", "创建漏洞复测分支", "vulnerability", v.ID, map[string]interface{}{"conversation_id": conv.ID, "source_conversation_id": source.ID})
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"conversation": conv, "prompt": prompt, "vulnerability_id": v.ID, "source_conversation_id": source.ID})
|
|
}
|