Compare commits

..
8 changed files with 1104 additions and 316 deletions

No files matched your search

+18 -28
View File
@@ -23,21 +23,20 @@ type DBFinding struct {
ReportEvidenceVersion int64 ReportEvidenceVersion int64
TrafficBindings []FindingTrafficBinding // populated only for export TrafficBindings []FindingTrafficBinding // populated only for export
ID int64 ID int64
TaskID *int64 TaskID *int64
NodeID *int64 NodeID *int64
VulnClass string VulnClass string
Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass
Severity string Severity string
Summary string Summary string
Evidence string Evidence string
Worker string Worker string
AssetIDs []int64 AssetIDs []int64
Status string Status string
Report string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带 Report string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带
ReportConversationID *int64 `json:"report_conversation_id,omitempty"` CreatedAt time.Time
CreatedAt time.Time TaskDescription string // populated via LEFT JOIN on tasks
TaskDescription string // populated via LEFT JOIN on tasks
} }
// Finding triage states (findings.status). // Finding triage states (findings.status).
@@ -107,9 +106,9 @@ func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, name, severity, summary
// findingSelectCols is the column list (with task_description join) every finding // findingSelectCols is the column list (with task_description join) every finding
// list query selects, so scanFinding stays in sync across callers. // list query selects, so scanFinding stays in sync across callers.
const findingSelectCols = `f.id, f.task_id, f.node_id, f.vulnclass, COALESCE(f.name, ''), f.severity, f.summary, const findingSelectCols = `f.id, f.task_id, f.node_id, f.vulnclass, COALESCE(f.name, ''), f.severity, f.summary,
f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at, f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at,
COALESCE(t.description, '') AS task_description, f.evidence_version, f.report_evidence_version, COALESCE(t.description, '') AS task_description, f.evidence_version, f.report_evidence_version,
(SELECT count(*) FROM finding_traffic_bindings b WHERE b.finding_id=f.id)` (SELECT count(*) FROM finding_traffic_bindings b WHERE b.finding_id=f.id)`
// scanFindings materializes rows selected via findingSelectCols. // scanFindings materializes rows selected via findingSelectCols.
func scanFindings(rows interface { func scanFindings(rows interface {
@@ -662,14 +661,13 @@ func (d *DB) FindingStats() (*FindingStats, error) {
func (d *DB) GetFinding(id int64) (*DBFinding, error) { func (d *DB) GetFinding(id int64) (*DBFinding, error) {
f := &DBFinding{} f := &DBFinding{}
var aidsJSON string var aidsJSON string
var reportConv sql.NullInt64 err := d.QueryRow(`SELECT `+findingSelectCols+`, COALESCE(f.report, '')
err := d.QueryRow(`SELECT `+findingSelectCols+`, COALESCE(f.report, ''), f.report_conversation_id
FROM findings f FROM findings f
LEFT JOIN tasks t ON f.task_id = t.id LEFT JOIN tasks t ON f.task_id = t.id
WHERE f.id = $1`, id).Scan( WHERE f.id = $1`, id).Scan(
&f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Name, &f.Severity, &f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Name, &f.Severity,
&f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt, &f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt,
&f.TaskDescription, &f.EvidenceVersion, &f.ReportEvidenceVersion, &f.TrafficCount, &f.Report, &reportConv) &f.TaskDescription, &f.EvidenceVersion, &f.ReportEvidenceVersion, &f.TrafficCount, &f.Report)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, nil return nil, nil
} }
@@ -677,17 +675,9 @@ func (d *DB) GetFinding(id int64) (*DBFinding, error) {
return nil, err return nil, err
} }
_ = json.Unmarshal([]byte(aidsJSON), &f.AssetIDs) _ = json.Unmarshal([]byte(aidsJSON), &f.AssetIDs)
if reportConv.Valid {
f.ReportConversationID = &reportConv.Int64
}
return f, nil return f, nil
} }
func (d *DB) SetFindingReportConversationID(findingID, conversationID int64) error {
_, err := d.Exec(`UPDATE findings SET report_conversation_id=$2 WHERE id=$1`, findingID, conversationID)
return err
}
// DeleteFinding removes a finding entirely: the standalone findings row and its // DeleteFinding removes a finding entirely: the standalone findings row and its
// originating exploration node (kind='finding'), so it disappears from the findings // originating exploration node (kind='finding'), so it disappears from the findings
// list, the per-task 发现 Tab, and the exploration graph alike. Deleting the node // list, the per-task 发现 Tab, and the exploration graph alike. Deleting the node
-2
View File
@@ -1162,13 +1162,11 @@ CREATE TABLE IF NOT EXISTS findings (
status TEXT NOT NULL DEFAULT 'pending', status TEXT NOT NULL DEFAULT 'pending',
-- 漏洞详细报告(Markdown);默认空,仅详情页读取/展示,不进列表接口以免 payload 膨胀。 -- 漏洞详细报告(Markdown);默认空,仅详情页读取/展示,不进列表接口以免 payload 膨胀。
report TEXT NOT NULL DEFAULT '', report TEXT NOT NULL DEFAULT '',
report_conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now() created_at TIMESTAMPTZ NOT NULL DEFAULT now()
); );
ALTER TABLE findings ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending'; ALTER TABLE findings ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE findings ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT ''; ALTER TABLE findings ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';
ALTER TABLE findings ADD COLUMN IF NOT EXISTS report TEXT NOT NULL DEFAULT ''; ALTER TABLE findings ADD COLUMN IF NOT EXISTS report TEXT NOT NULL DEFAULT '';
ALTER TABLE findings ADD COLUMN IF NOT EXISTS report_conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_findings_task ON findings(task_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_findings_task ON findings(task_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_findings_time ON findings(created_at DESC); CREATE INDEX IF NOT EXISTS idx_findings_time ON findings(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status, created_at DESC);
+1 -9
View File
@@ -701,14 +701,6 @@ func (s *Server) readTriggerBehavior(agentKey string) triggerBehavior {
// merging by task / all / none); parallel → run each fire in its own concurrent // merging by task / all / none); parallel → run each fire in its own concurrent
// conversation up to trigger_max_parallel. Distinct agents always run concurrently. // conversation up to trigger_max_parallel. Distinct agents always run concurrently.
func (s *Server) StartTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string) { func (s *Server) StartTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string) {
s.startTriggeredRun(agentKey, title, message, taskID, mergeable, taskDesc, taskGoal, 0)
}
func (s *Server) StartTriggeredRunWithConversation(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string, conversationID int64) {
s.startTriggeredRun(agentKey, title, message, taskID, mergeable, taskDesc, taskGoal, conversationID)
}
func (s *Server) startTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string, conversationID int64) {
if s.m.pg == nil || s.chatAgentRef() == nil { if s.m.pg == nil || s.chatAgentRef() == nil {
return return
} }
@@ -722,7 +714,7 @@ func (s *Server) startTriggeredRun(agentKey, title, message string, taskID int64
cfg := s.readTriggerBehavior(agentKey) // DB read BEFORE the lock (never under queueMu) cfg := s.readTriggerBehavior(agentKey) // DB read BEFORE the lock (never under queueMu)
s.queueMu.Lock() s.queueMu.Lock()
s.triggerCfg[agentKey] = cfg s.triggerCfg[agentKey] = cfg
s.triggerQ[agentKey] = append(s.triggerQ[agentKey], triggeredRun{conversationID: conversationID, agentKey: agentKey, title: title, message: message, taskID: taskID, taskDesc: taskDesc, taskGoal: taskGoal, mergeable: mergeable}) s.triggerQ[agentKey] = append(s.triggerQ[agentKey], triggeredRun{agentKey: agentKey, title: title, message: message, taskID: taskID, taskDesc: taskDesc, taskGoal: taskGoal, mergeable: mergeable})
s.pumpLocked(agentKey) s.pumpLocked(agentKey)
s.queueMu.Unlock() s.queueMu.Unlock()
} }
+2 -12
View File
@@ -58,30 +58,20 @@ func (s *Server) generateFindingReport(w http.ResponseWriter, r *http.Request) {
} }
} }
message := manualFindingReportMessage(f, taskID) message := manualFindingReportMessage(f, taskID)
conversation, err := s.m.pg.CreateConversation("reporter", fmt.Sprintf("漏洞报告 · finding#%d", f.ID), nil) s.StartTriggeredRun(
if err != nil {
writeErr(w, http.StatusInternalServerError, "创建报告对话失败: "+err.Error())
return
}
if err := s.m.pg.SetFindingReportConversationID(f.ID, conversation.ID); err != nil {
writeErr(w, http.StatusInternalServerError, "关联报告对话失败: "+err.Error())
return
}
s.StartTriggeredRunWithConversation(
"reporter", "reporter",
fmt.Sprintf("人工生成步骤与报告 · finding#%d", f.ID), fmt.Sprintf("人工生成步骤与报告 · finding#%d", f.ID),
message, message,
taskID, taskID,
false, false,
taskDesc, taskDesc,
taskGoal, conversation.ID, taskGoal,
) )
writeJSON(w, http.StatusAccepted, map[string]any{ writeJSON(w, http.StatusAccepted, map[string]any{
"queued": true, "queued": true,
"agent_key": "reporter", "agent_key": "reporter",
"finding_id": f.ID, "finding_id": f.ID,
"finding_node_id": *f.NodeID, "finding_node_id": *f.NodeID,
"conversation_id": conversation.ID,
}) })
} }
@@ -424,7 +424,6 @@ function FindingDetailInner() {
agentLabel="报告撰写 Agent" agentLabel="报告撰写 Agent"
promptPlaceholder="描述要如何调整漏洞报告…" promptPlaceholder="描述要如何调整漏洞报告…"
modal modal
initialConversationId={finding.report_conversation_id}
open={reportChatOpen} open={reportChatOpen}
onOpenChange={setReportChatOpen} onOpenChange={setReportChatOpen}
/> />
File diff suppressed because it is too large. Load diff
+1 -7
View File
@@ -26,7 +26,6 @@ export function FindingAutoChat({
agentLabel = "Auto Agent", agentLabel = "Auto Agent",
promptPlaceholder = "询问这个漏洞…", promptPlaceholder = "询问这个漏洞…",
modal = false, modal = false,
initialConversationId,
open, open,
onOpenChange, onOpenChange,
}: { }: {
@@ -36,7 +35,6 @@ export function FindingAutoChat({
agentLabel?: string; agentLabel?: string;
promptPlaceholder?: string; promptPlaceholder?: string;
modal?: boolean; modal?: boolean;
initialConversationId?: number;
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}) { }) {
@@ -72,10 +70,6 @@ export function FindingAutoChat({
React.useEffect(() => { React.useEffect(() => {
if (!open || conversation || manualEmpty) return; if (!open || conversation || manualEmpty) return;
if (initialConversationId && initialConversationId > 0) {
setConversation({ id: initialConversationId, agent_key: agentKey, title: `漏洞 #${findingId} · ${safeTitle}`, created_at: "", updated_at: "" });
return;
}
const storedID = Number(window.localStorage.getItem(storageKey)); const storedID = Number(window.localStorage.getItem(storageKey));
if (Number.isSafeInteger(storedID) && storedID > 0) { if (Number.isSafeInteger(storedID) && storedID > 0) {
setConversation({ setConversation({
@@ -88,7 +82,7 @@ export function FindingAutoChat({
return; return;
} }
void createConversation(); void createConversation();
}, [conversation, createConversation, initialConversationId, manualEmpty, open, safeTitle, storageKey]); }, [conversation, createConversation, manualEmpty, open, safeTitle, storageKey]);
React.useEffect(() => { React.useEffect(() => {
if (!historyOpen && !running) return; if (!historyOpen && !running) return;
-1
View File
@@ -529,7 +529,6 @@ export interface Finding {
summary: string; summary: string;
evidence: string; evidence: string;
report?: string; // 详细报告(Markdown);仅详情接口返回,列表为空 report?: string; // 详细报告(Markdown);仅详情接口返回,列表为空
report_conversation_id?: number;
intent_id?: string; intent_id?: string;
param_id?: string; param_id?: string;
task_id?: string; task_id?: string;