fix: preserve attack-chain summary provenance and evidence access

This commit is contained in:
Sycun committed 2026-10-12 20:58:45 +08:00
1 parent 983c981b92
commit 2b76b31355
15 files changed
+1022 -66

No files matched your search

+5 -5
View File
@@ -136,11 +136,6 @@ func (h *AttackChainHandler) RegenerateAttackChain(c *gin.Context) {
return
}
// 删除旧的攻击链
if err := h.db.DeleteAttackChain(conversationID); err != nil {
h.logger.Warn("删除旧攻击链失败", zap.Error(err))
}
// 使用锁机制防止并发生成
lockInterface, _ := h.generatingLocks.LoadOrStore(conversationID, &sync.Mutex{})
lock := lockInterface.(*sync.Mutex)
@@ -153,6 +148,11 @@ func (h *AttackChainHandler) RegenerateAttackChain(c *gin.Context) {
}
defer lock.Unlock()
// 只有持锁的生成请求可以删除旧图;返回 409 的请求不得修改缓存。
if err := h.db.DeleteAttackChain(conversationID); err != nil {
h.logger.Warn("删除旧攻击链失败", zap.Error(err))
}
// 生成新的攻击链
h.logger.Info("重新生成攻击链", zap.String("conversationId", conversationID))
@@ -0,0 +1,58 @@
package handler
import (
"net/http"
"net/http/httptest"
"path/filepath"
"sync"
"testing"
"cyberstrike-ai/internal/database"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
func TestBusyAttackChainRegenerationPreservesCachedGraph(t *testing.T) {
db, err := database.NewDB(filepath.Join(t.TempDir(), "chain.db"), zap.NewNop())
if err != nil {
t.Fatal(err)
}
defer db.Close()
conv, err := db.CreateConversation("cached", database.ConversationCreateMeta{})
if err != nil {
t.Fatal(err)
}
for _, id := range []string{"target", "action"} {
if err := db.SaveAttackChainNode(conv.ID, id, id, "saved "+id, "", `{}`, 0); err != nil {
t.Fatal(err)
}
}
if err := db.SaveAttackChainEdge(conv.ID, "edge", "target", "action", "leads_to", 1); err != nil {
t.Fatal(err)
}
h := NewAttackChainHandler(db, nil, zap.NewNop())
lock := &sync.Mutex{}
lock.Lock() // Another generation is still writing this conversation's graph.
defer lock.Unlock()
h.generatingLocks.Store(conv.ID, lock)
router := gin.New()
router.POST("/api/attack-chain/:conversationId/regenerate", h.RegenerateAttackChain)
for i := 0; i < 2; i++ {
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodPost, "/api/attack-chain/"+conv.ID+"/regenerate", nil))
if recorder.Code != http.StatusConflict {
t.Fatalf("busy regeneration returned %d: %s", recorder.Code, recorder.Body.String())
}
nodes, err := db.LoadAttackChainNodes(conv.ID)
if err != nil {
t.Fatal(err)
}
edges, err := db.LoadAttackChainEdges(conv.ID)
if err != nil {
t.Fatal(err)
}
if len(nodes) != 2 || len(edges) != 1 || edges[0].Source != "target" || edges[0].Target != "action" {
t.Fatalf("409 request modified the graph being generated: nodes=%+v edges=%+v", nodes, edges)
}
}
}