#102 高:证据正文下载在全局 advisory lock 事务内 io.Copy 给 HTTP 客户端, verifyFile 又要整读一遍算 SHA-256 —— 一次大文件下载或慢客户端就会挂住 全进程的证据写入。改为事务内只解析绑定元数据(Store.Binding),出锁后再校验 与串流;blob 内容寻址且 GC 有 24h 宽限,已打开的 fd 也不受 unlink 影响。 #102 高:traffic_evidence_snapshots 的 url/req_head/resp_head 直接来自目标 原始报文,未清洗就写 PG TEXT。目标返回非 UTF-8 头(GBK filename)或 NUL 会让 RecordFindingTx 整个事务回滚,已确认的漏洞完全不落库。新增 TrafficEvidenceSnapshot.Normalize(),在算 ID 之前套用仓库既有的 utf8Clean, 保证哈希与入库字节一致。 #102 中:自动绑定默认关闭,reporter 便不会传 evidence_version, SetFindingReportVersionByNodeID 按 legacy 语义写 -1,报告永久显示「待更新」 且 UI 无处清除;老库还因 reporter_agent_seed_v1 守卫拿不到新触发文案。 触发消息改为无条件先读 get_finding_traffic(只读工具,不依赖捕获开关)并回传 版本,并加一次性迁移刷新老库中未被用户改动的默认文案。 #102 中:开关关闭时 report_finding 只要收到 traffic_refs/evidence_hint_id 就在写入前报错,模型幻觉出多余字段即丢掉一个真实漏洞。改为忽略这两个参数 照常登记,由既有的 evidence_status=not_bound 说明未绑定。 #101 中:删掉 RuleID!=0 判断后,每次模型放行都写一行带完整审计快照的 intercept_pending(单行最坏 24×8KiB 上下文 + 32KiB 输入)。开启兜底裁判的 渗透任务会灌入数百 MB,并经 task_archives 的 SELECT * 进入归档。放行行改为 只留决策元数据与执行结果,丢掉重放快照;ask/deny 路径不变。 #103 中:FindingRetestForConversation 出错时在收尾 defer 注册前就 return, 复测行永远停在 pending —— 漏洞列表常驻「复测中」,后续发起复测被去重拦下, 只有重启才能清。新增 FailPendingRetestForConversation 按会话 ID 收尾。
129 lines
8.0 KiB
Go
129 lines
8.0 KiB
Go
package agent
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
"github.com/Autumn-27/artex/db"
|
||
actool "github.com/Autumn-27/norma/tool"
|
||
)
|
||
|
||
const findingIDGuidance = "\n\n**漏洞编号约定**:finding_id 是独立漏洞记录 ID;finding_node_id 是探索节点 ID。list_findings / list_task_findings / node_detail / get_task_node_detail 的 id 保留为探索节点 ID,应从同一返回的 finding_id 读取独立编号。get_finding_traffic / bind_finding_traffic 用独立 finding_id。旧 update_finding_report 的 finding_id 参数仍传 finding_node_id。不要把 report_finding 第一行的数字用于证据工具,也不要遇到编号错误后猜测其他数字。"
|
||
|
||
// The server supplies the persisted setting. A missing setting/host is off.
|
||
// Consulted at assembly and again on writes so an already-running session
|
||
// cannot keep binding after the user switches the feature off.
|
||
var FindingTrafficBindingEnabled func() bool
|
||
|
||
func findingTrafficBindingEnabled() bool {
|
||
return FindingTrafficBindingEnabled != nil && FindingTrafficBindingEnabled()
|
||
}
|
||
|
||
// Applied after ToolResolve: user descriptions and prompts remain intact, while
|
||
// all actual reporters (including Planner and custom chat agents) see the same
|
||
// API contract. Disabled/unbound tools are never reintroduced here.
|
||
func findingWorkflowTools(agentKey string, tools []actool.CoreTool) ([]actool.CoreTool, string) {
|
||
if !findingTrafficBindingEnabled() {
|
||
out := make([]actool.CoreTool, 0, len(tools))
|
||
for _, tool := range tools {
|
||
if tool.Name() == "bind_finding_traffic" {
|
||
continue
|
||
}
|
||
if agentKey == "reporter" && (tool.Name() == "traffic_search" || tool.Name() == "traffic_get" || tool.Name() == "traffic_blob") {
|
||
continue
|
||
}
|
||
switch tool.Name() {
|
||
case "report_finding", "add_hint", "add_task_hint":
|
||
// Work on a copy: toggling back on must restore the original schema.
|
||
raw, _ := json.Marshal(tool.InputSchema())
|
||
var schema map[string]any
|
||
if json.Unmarshal(raw, &schema) == nil {
|
||
stripTrafficParameters(schema)
|
||
tool = DecorateTool(tool, tool.Description(), schema)
|
||
}
|
||
}
|
||
out = append(out, tool)
|
||
}
|
||
return out, ""
|
||
}
|
||
out := append([]actool.CoreTool(nil), tools...)
|
||
has := map[string]bool{}
|
||
for i, tool := range out {
|
||
has[tool.Name()] = true
|
||
note := ""
|
||
switch tool.Name() {
|
||
case "report_finding":
|
||
note = "\n默认由报告 Agent 在编写报告前核对并绑定流量。上报者在 evidence 中保留验证命令、关键输出、已有的真实流量 ID 及其用途,供报告 Agent 对照执行记录核实;无需为绑定额外查包。兼容显式即时绑定:traffic_refs 或 evidence_hint_id 可提交已核实的引用,后者读取本任务指定 hint 的结构化引用;任一无效则本次上报全部失败。TCP/无包不需要这些可选参数。返回 finding_id 与 finding_node_id 分别表示独立记录和探索节点。"
|
||
case "add_hint", "add_task_hint":
|
||
note = "\n交接已确认漏洞时,在对应提示的 traffic_refs 中保留已核实流量的 ID、用途、说明和顺序(单条放顶层,批量放对应 hints 元素),并在 text 中说明它证明的具体漏洞。调用方不能只交接文字而丢弃已有流量引用。未核实的候选不能作为证据传递。"
|
||
case "get_finding_traffic", "bind_finding_traffic", "list_findings", "list_task_findings", "node_detail", "get_task_node_detail", "update_finding_report":
|
||
note = findingIDGuidance
|
||
}
|
||
if note != "" {
|
||
out[i] = DecorateTool(tool, tool.Description()+note, tool.InputSchema())
|
||
}
|
||
}
|
||
guidance := ""
|
||
if has["report_finding"] || has["add_task_hint"] || has["add_hint"] {
|
||
guidance = "\n\n**流量证据交接(可选)**:自动绑定默认由报告 Agent 在漏洞入库后、编写报告前完成。上报者应在 evidence 保留验证命令、关键输出、已有真实流量 ID 及其用途,任务中带 intent_id,便于报告 Agent 追溯;不必为了绑定额外查包。Auto / Planner 代为上报时不要丢弃执行者已有的引用。add_hint / add_task_hint 可用 traffic_refs 交接;显式即时绑定仍兼容 report_finding 的 traffic_refs / evidence_hint_id。TCP 或无包时正常登记,不能猜测 ID,也不能仅为补包重复探测。"
|
||
if has["add_task_hint"] && !has["add_hint"] {
|
||
guidance += "\n平台对话没有任务上下文时,不直接调用 report_finding;通过 add_task_hint 向已有对应任务交接,由任务 Agent 登记,并用 list_task_findings 核对结果。"
|
||
}
|
||
if has["prove_goal"] || has["goal_met"] {
|
||
guidance += "\n判定目标完成前,先完成本次已有证据的上报/交接。不要在证据交接尚未完成时仅因文字漏洞已登记就结束任务、取消 Worker;无包不要求等待或强行抓包。"
|
||
}
|
||
}
|
||
if has["update_finding_report"] && has["bind_finding_traffic"] && has["get_finding_traffic"] {
|
||
guidance += "\n\n**报告前自动关联流量(已开启)**:你负责为本次触发的漏洞核对并绑定流量,再撰写报告。先从 report_finding 返回 JSON 或 get_task_node_detail / list_task_findings 取得明确的 finding_id 与 finding_node_id。读取漏洞详情、对应意图的执行记录及已有证据清单,优先使用上报者交接的真实 ID。若本次验证为 HTTP 且流量工具可用,用 traffic_search 筛选候选,再用 traffic_get 逐条核实请求/响应确实支持该漏洞;域名和时间只用于筛选,不证明归属。将确认的证据按复现顺序用 bind_finding_traffic(finding_id, traffic_refs) 关联,选择 baseline / proof / verification / supporting 并说明用途。只能操作本次漏洞,不重复创建漏洞或重新探测目标。绑定成功后重新调用 get_finding_traffic 获取最新 version,读取所需正文,再将实际读取的 version 作为 evidence_version 传给 update_finding_report(其 finding_id 参数仍用 finding_node_id)。已有绑定不必重复追加。TCP、未采集、工具不可用或没有确切匹配时,跳过自动绑定,依据文字/命令证据正常写报告并说明原因,不得为凑齐流量而猜测。绑定失败不宣称成功;保留已有证据并在报告说明未绑定原因。"
|
||
}
|
||
if guidance != "" || has["get_finding_traffic"] || has["update_finding_report"] {
|
||
guidance += findingIDGuidance
|
||
}
|
||
return out, guidance
|
||
}
|
||
|
||
func stripTrafficParameters(schema map[string]any) {
|
||
props, _ := schema["properties"].(map[string]any)
|
||
delete(props, "traffic_refs")
|
||
delete(props, "evidence_hint_id")
|
||
if required, ok := schema["required"].([]any); ok {
|
||
kept := required[:0]
|
||
for _, key := range required {
|
||
if key != "traffic_refs" && key != "evidence_hint_id" {
|
||
kept = append(kept, key)
|
||
}
|
||
}
|
||
schema["required"] = kept
|
||
}
|
||
if hints, ok := props["hints"].(map[string]any); ok {
|
||
if items, ok := hints["items"].(map[string]any); ok {
|
||
stripTrafficParameters(items)
|
||
}
|
||
}
|
||
}
|
||
|
||
// HintTrafficSchema is shared by the task-local and cross-task hint tools.
|
||
func HintTrafficSchema() map[string]any {
|
||
return map[string]any{"type": "array", "description": "可选:已核实且对应本提示中具体漏洞的流量引用,保留顺序;交接后 report_finding 可传 evidence_hint_id 携带这些引用。", "items": obj(map[string]any{"traffic_id": str("真实流量 ID"), "role": str("baseline / proof / verification / supporting"), "note": str("该流量支持什么结论")}, "traffic_id")}
|
||
}
|
||
|
||
func (t *ToolSet) findingRefsFromHint(hintID int64, explicit []db.TrafficRef) ([]db.TrafficRef, error) {
|
||
if hintID <= 0 {
|
||
return db.NormalizeTrafficRefs(explicit)
|
||
}
|
||
n, err := t.ts.GetNode(hintID) // local store only: inherited hints cannot supply evidence
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if n == nil || n.Kind != db.KindHint {
|
||
return nil, fmt.Errorf("evidence_hint_id=%d 必须是本任务的提示节点(继承提示不可直接用于绑定)", hintID)
|
||
}
|
||
var payload struct {
|
||
Refs []db.TrafficRef `json:"traffic_refs"`
|
||
}
|
||
if err := json.Unmarshal(n.Payload, &payload); err != nil {
|
||
return nil, err
|
||
}
|
||
return db.NormalizeTrafficRefs(append(append([]db.TrafficRef{}, explicit...), payload.Refs...))
|
||
}
|