mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-21 06:51:25 +02:00
163 lines
6.2 KiB
Go
163 lines
6.2 KiB
Go
package project
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"cyberstrike-ai/internal/config"
|
|
"cyberstrike-ai/internal/database"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func TestProjectResumeSurvivesRestartAndStaysProjectScoped(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "resume.db")
|
|
db, err := database.NewDB(path, zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { db.Close() }()
|
|
p, err := db.CreateProject(&database.Project{Name: "resume"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := db.CreateProject(&database.Project{Name: "other"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conv, err := db.CreateConversation("yesterday", database.ConversationCreateMeta{ProjectID: p.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, f := range []*database.ProjectFact{
|
|
{ProjectID: p.ID, FactKey: "artifact/parser", Category: "note", Summary: "已验证解析脚本", Body: "路径 artifacts/parser.py;调用 python parser.py --input capture.txt;验证输出 PASS", Confidence: "confirmed", SourceConversationID: conv.ID},
|
|
{ProjectID: p.ID, FactKey: "test/login-query", Category: "note", Summary: "已测参数未发现异常", Body: "POST /login 参数 q 匿名:重复三次响应相同;仅在该入口有效;版本变化再测", Confidence: "confirmed"},
|
|
{ProjectID: p.ID, FactKey: "artifact/obsolete", Category: "note", Summary: "废弃旧脚本", Body: "obsolete-body", Confidence: "deprecated"},
|
|
{ProjectID: other.ID, FactKey: "artifact/private", Category: "note", Summary: "other-project-secret", Body: "private-body", Confidence: "confirmed"},
|
|
} {
|
|
if _, err := db.UpsertProjectFact(f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
for _, v := range []*database.Vulnerability{
|
|
{ProjectID: p.ID, Title: "known finding", Severity: "medium", Status: "fixed", Target: "https://example.test/login"},
|
|
{ProjectID: other.ID, Title: "private-vulnerability", Severity: "high"},
|
|
} {
|
|
if _, err := db.CreateVulnerability(v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := db.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db, err = database.NewDB(path, zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Both a reopened conversation and a fresh conversation in the same project
|
|
// receive saved results without relying on any chat history.
|
|
for _, title := range []string{"reopened", "today"} {
|
|
if _, err := db.CreateConversation(title, database.ConversationCreateMeta{ProjectID: p.ID}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
block, err := BuildProjectBlackboardBlock(db, p.ID, config.ProjectConfig{Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{"artifacts/parser.py", "python parser.py", "POST /login", "版本变化再测", "known finding", "fixed", conv.ID, "先恢复成果", "默认跳过"} {
|
|
if !strings.Contains(block, want) {
|
|
t.Errorf("missing %q in %s", want, block)
|
|
}
|
|
}
|
|
for _, secret := range []string{"other-project-secret", "private-body", "private-vulnerability", "obsolete-body"} {
|
|
if strings.Contains(block, secret) {
|
|
t.Errorf("unexpected %q", secret)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResumeSupplementBudgetAndIndependentResultQuotas(t *testing.T) {
|
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "budget.db"), zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
p, err := db.CreateProject(&database.Project{Name: "budget"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var facts []*database.ProjectFact
|
|
for i := 0; i < 25; i++ {
|
|
facts = append(facts, &database.ProjectFact{FactKey: fmt.Sprintf("artifact/%d", i), Summary: strings.Repeat("大", 2000), Body: strings.Repeat("文", 10000), Confidence: "confirmed"})
|
|
if _, err := db.CreateVulnerability(&database.Vulnerability{ProjectID: p.ID, Title: strings.Repeat("漏洞", 500), Severity: "high"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
facts = append(facts, &database.ProjectFact{FactKey: "test/negative", Summary: "bounded-negative", Body: "已测条件下未发现", Confidence: "confirmed"})
|
|
block := buildResumeSection(db, p.ID, facts)
|
|
if n := len([]rune(block)); n > resumeSupplementMaxRunes {
|
|
t.Fatalf("budget exceeded: %d", n)
|
|
}
|
|
if !strings.Contains(block, "test/negative") || (!strings.Contains(block, "/ 25") || strings.Contains(block, "artifact: 展示 0")) || !strings.Contains(block, "get_vulnerability") {
|
|
t.Fatalf("missing independent result quota or retrieval: %s", block)
|
|
}
|
|
}
|
|
|
|
func TestProjectResumeWithoutFactsStillListsVulnerabilities(t *testing.T) {
|
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "empty.db"), zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
p, err := db.CreateProject(&database.Project{Name: "empty"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.CreateVulnerability(&database.Vulnerability{ProjectID: p.ID, Title: "finding-without-facts", Severity: "low"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
block, err := BuildFactIndexBlock(db, p.ID, config.ProjectConfig{Enabled: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(block, "finding-without-facts") {
|
|
t.Fatal(block)
|
|
}
|
|
disabled, err := BuildFactIndexBlock(db, p.ID, config.ProjectConfig{})
|
|
if err != nil || disabled != "" {
|
|
t.Fatalf("disabled=%q, err=%v", disabled, err)
|
|
}
|
|
}
|
|
|
|
func TestResumeSupplementRespectsMergedIndexBudget(t *testing.T) {
|
|
db, err := database.NewDB(filepath.Join(t.TempDir(), "resume-cap.db"), zap.NewNop())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
p, err := db.CreateProject(&database.Project{Name: "budget"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.UpsertProjectFact(&database.ProjectFact{ProjectID: p.ID, FactKey: "artifact/saved", Category: "note", Summary: "saved script", Body: "artifacts/saved.py " + FactIndexSectionEndMarker, Confidence: "confirmed"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, cap := range []int{30, 100, 300, 700, 3500, 6500, 12000} {
|
|
block, err := BuildFactIndexBlock(db, p.ID, config.ProjectConfig{Enabled: true, FactIndexMaxRunes: cap})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len([]rune(block)); n > cap {
|
|
t.Fatalf("index has %d runes for cap %d", n, cap)
|
|
}
|
|
if block != "" && (strings.Count(block, FactIndexSectionStartMarker) != 1 || strings.Count(block, FactIndexSectionEndMarker) != 1) {
|
|
t.Fatalf("broken host markers: %s", block)
|
|
}
|
|
if cap == 12000 && !strings.Contains(block, "artifacts/saved.py") {
|
|
t.Fatal("selected artifact body was not recovered from the index projection")
|
|
}
|
|
}
|
|
}
|