diff --git a/agent/tools.go b/agent/tools.go index a0bd49e..f6adf62 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -696,6 +696,39 @@ func (t *ToolSet) addFinding() actool.CoreTool { }) } +// updateFindingReport sets/overwrites a finding's detailed Markdown report +// (findings.report), addressed by the id report_finding returned. +func (t *ToolSet) updateFindingReport() actool.CoreTool { + return writeTool("update_finding_report", + "为已登记的漏洞补充/更新【详细报告】。report 传 Markdown 全文(建议含:漏洞概述、影响、复现步骤、证据/PoC、修复建议)。finding_id 传 report_finding 返回的那个 id。整段覆盖该漏洞已有的报告。", + obj(map[string]any{ + "finding_id": idp("目标漏洞 id(report_finding 返回的 id)"), + "report": str("详细报告全文,Markdown 格式"), + }, "finding_id", "report"), + func(_ context.Context, in json.RawMessage) (actool.Result, error) { + var a struct { + FindingID json.RawMessage `json:"finding_id"` + Report string `json:"report"` + } + _ = json.Unmarshal(in, &a) + nodeID := pid(a.FindingID) + if nodeID <= 0 { + return actool.Errorf("finding_id 无效"), nil + } + if t.ts == nil { + return actool.Errorf("update_finding_report 需要任务上下文(exploration store 未初始化)"), nil + } + n, err := t.ts.SetFindingReport(nodeID, a.Report) + if err != nil { + return actool.Errorf(err.Error()), nil + } + if n == 0 { + return actool.Errorf(fmt.Sprintf("未找到 finding_id=%d 对应的漏洞记录(先用 report_finding 登记)", nodeID)), nil + } + return actool.Text(fmt.Sprintf("finding %d report updated (%d chars)", nodeID, len(a.Report))), nil + }) +} + // recordFact writes a general exploration RESULT/conclusion (not a vuln, not a // new asset) into the EXPLORATION graph, chained to the intent that produced it. // This is the home for observations and — importantly — negative results @@ -1250,7 +1283,7 @@ func (t *ToolSet) PlannerTools() []actool.CoreTool { t.getWorkerOutput(), t.getWorkerTrace(), t.searchAllWorkerTraces(), t.listGoals(), t.addIntent(), t.proveGoal(), t.goalMet(), t.killWorkTool(), t.steerWorkTool(), // report_finding:规划态势研判时若自身已确证漏洞,可直接登记(与 worker 同工具)。 - t.addFinding(), + t.addFinding(), t.updateFindingReport(), // list_companies:查看企业列表 + scope + 资产数(拿 company_id / 理解归属范围)。 t.listCompanies(), // add_task_scope:主动把整根域/整公司/某子域/IP 纳入本任务测试范围(覆盖度分母)。 diff --git a/agent/tools_insert.go b/agent/tools_insert.go index c808988..a0109e7 100644 --- a/agent/tools_insert.go +++ b/agent/tools_insert.go @@ -582,7 +582,7 @@ func splitLines(s string) []string { func (t *ToolSet) WorkerTools() []actool.CoreTool { return []actool.CoreTool{ t.listFindings(), t.listFacts(), t.nodeDetail(), - t.addFinding(), t.recordFact(), + t.addFinding(), t.updateFindingReport(), t.recordFact(), // cross-work retrieval t.searchAllWorkerTraces(), t.listWorkerTraces(), t.getWorkerTrace(), // asset management (handlers guard nil store internally) @@ -599,7 +599,7 @@ func (t *ToolSet) MainAgentTools() []actool.CoreTool { t.setGoals(), // asset management (handlers guard nil store internally) t.insertAssets(), t.addCompanyScope(), t.listAssets(), - t.addFinding(), t.recordFact(), + t.addFinding(), t.updateFindingReport(), t.recordFact(), t.addTaskScope(), // list_untested_assets:按需查本任务范围内未测资产(类型+分页),自行决定补测。 t.listUntestedAssets(), diff --git a/db/exploration.go b/db/exploration.go index 00f6024..c3f70a5 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -889,3 +889,9 @@ FROM activity WHERE exploration_id=$1 AND kind<>'thinking' AND id IN (`+strings. func (s *ExplorationStore) AddStandaloneFinding(taskID, nodeID int64, vulnclass, name, severity, summary, evidence, worker string, assetIDs []int64) (int64, error) { return s.db.AddFinding(taskID, nodeID, vulnclass, name, severity, summary, evidence, worker, assetIDs) } + +// SetFindingReport sets a finding's Markdown detail report, addressed by its +// finding node id (the id report_finding returns). Returns rows affected. +func (s *ExplorationStore) SetFindingReport(nodeID int64, report string) (int64, error) { + return s.db.SetFindingReportByNodeID(nodeID, report) +} diff --git a/db/findings.go b/db/findings.go index 1549e06..b77ac3f 100644 --- a/db/findings.go +++ b/db/findings.go @@ -277,6 +277,17 @@ func (d *DB) SetFindingStatus(id int64, status string) (int64, error) { return res.RowsAffected() } +// SetFindingReportByNodeID sets the Markdown report on the standalone finding row +// whose node_id matches — report_finding returns that node id, so an agent tool +// can address the finding it just created. Returns rows affected (0 when no row). +func (d *DB) SetFindingReportByNodeID(nodeID int64, report string) (int64, error) { + res, err := d.Exec(`UPDATE findings SET report=$1 WHERE node_id=$2`, report, nodeID) + if err != nil { + return 0, err + } + return res.RowsAffected() +} + // SetFindingSeverity updates one finding's severity in the standalone table AND // keeps the originating exploration node's payload in sync (the per-task view // reads severity from the node payload, not this table). Returns rows affected