First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/Autumn-27/artex/agent"
|
||||
"github.com/Autumn-27/artex/db"
|
||||
"github.com/Autumn-27/artex/traffic"
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
// 현지화 보존 판정(artex-ko): 이 파일의 중국어 문자열은 전부 에이전트(두뇌) 입력이고
|
||||
// HTTP 사용자 응답으로 나가지 않는다. BRIEF 경계 #1(두뇌는 번역하지 않는다 — 성능
|
||||
// 보존)에 따라 원문을 그대로 둔다. 이 파일에 writeErr 는 0 — 사용자 노출 경로가 없다.
|
||||
// 싱크는 세 갈래다(행 번호 대신 심볼로 가리킨다. 편집으로 밀려도 유효하게).
|
||||
//
|
||||
// (1) 도구 설명·스키마 description: report_finding 의 evidence_hint_id·hints.text,
|
||||
// bind_finding_traffic 의 wrTool 설명·finding_id strParam. LLM 이 읽는 도구
|
||||
// 정의라 두뇌 입력이다.
|
||||
// (2) 마이그레이션 매칭 문자열: seedFindingWorkflowTools 의 legacy 변수(옛 traffic_search
|
||||
// 설명)는 traffic.TrafficSearchDescription(그 자체도 중국어 도구 설명)으로 올리는
|
||||
// UPDATE … WHERE description=$2 의 비교값이다. 번역하면 기존 행과 매칭되지 않아
|
||||
// 업그레이드가 멈춘다.
|
||||
// (3) actool.Errorf 도구 결과: agentFindingTrafficAccess 의 오류들은 유일 호출처
|
||||
// toolBindFindingTraffic(이 파일)과 get_finding_traffic(finding_traffic.go) 두
|
||||
// 도구에서 모두 actool.Errorf(err.Error()) 로 감싸 에이전트에게 되돌아가고,
|
||||
// toolBindFindingTraffic 의 바인딩 비활성·빈 traffic_refs 문구는 직접 actool.Errorf 다.
|
||||
//
|
||||
// 다음 기여자가 "마저 번역"하다 벤치마크된 두뇌 입력을 바꾸지 않도록 둔다(F16 계열).
|
||||
func (s *Server) seedFindingWorkflowTools() {
|
||||
const hostSearchDescriptionFlag = "finding_workflow_tools_v3_host_search_description"
|
||||
if value, _, _ := s.m.pg.GetSetting(hostSearchDescriptionFlag); value != "true" {
|
||||
// Only replace the original built-in text. A user-edited description is
|
||||
// authoritative and must survive upgrades.
|
||||
legacy := "查询记录代理已抓取的目标流量(必须指定 host,可再按 URL 子串或正文关键词过滤)。body_contains 会在已抓取的请求/响应头与正文中做全文搜索,支持任意子串和中文(至少 3 个字符),可用来找响应里的密码、密钥、报错、内网地址等。仅返回极轻量索引(id/method/url/status/resp_len),不含任何响应内容。默认只返回 3 条、每页最多 10 条;结果多时用 page 翻页(page=0 起);要看某条的请求/响应原文用 traffic_get(id)。回看已访问资源、找端点先用它,避免重复 curl 同一 URL。"
|
||||
if _, err := s.m.pg.Exec(`UPDATE tools SET description=$1,updated_at=now() WHERE key='traffic_search' AND system AND description=$2`, traffic.TrafficSearchDescription, legacy); err != nil {
|
||||
// Log and leave the flag unset so the next startup retries; do not
|
||||
// return, or a transient error here would also skip the reporter
|
||||
// migration below — the two are independent.
|
||||
log.Printf("[evidence] upgrade traffic_search description: %v", err)
|
||||
} else {
|
||||
_ = s.m.pg.SetSetting(hostSearchDescriptionFlag, "true")
|
||||
}
|
||||
}
|
||||
const flag = "finding_workflow_tools_v2_reporter"
|
||||
if value, _, _ := s.m.pg.GetSetting(flag); value == "true" {
|
||||
return
|
||||
}
|
||||
for _, key := range []string{"report_finding", "add_hint", "add_task_hint"} {
|
||||
row, err := s.m.pg.GetTool(key)
|
||||
if err != nil {
|
||||
log.Printf("[evidence] load %s: %v", key, err)
|
||||
return
|
||||
}
|
||||
if row == nil || !row.System {
|
||||
continue
|
||||
}
|
||||
var schema map[string]any
|
||||
if err := json.Unmarshal(row.Schema, &schema); err != nil {
|
||||
log.Printf("[evidence] invalid schema for %s: %v", key, err)
|
||||
return
|
||||
}
|
||||
if schema == nil {
|
||||
log.Printf("[evidence] missing object schema for %s", key)
|
||||
return
|
||||
}
|
||||
props := objectProperty(schema, "properties")
|
||||
if key == "report_finding" {
|
||||
if _, exists := props["evidence_hint_id"]; !exists {
|
||||
props["evidence_hint_id"] = map[string]any{"type": "integer", "description": "可选:本任务中对应此漏洞的 hint ID;读取该提示保存的 traffic_refs 一并绑定,无提示时省略"}
|
||||
}
|
||||
} else {
|
||||
if _, exists := props["traffic_refs"]; !exists {
|
||||
props["traffic_refs"] = agent.HintTrafficSchema()
|
||||
}
|
||||
hints := objectProperty(props, "hints")
|
||||
if _, exists := hints["type"]; !exists {
|
||||
hints["type"] = "array"
|
||||
}
|
||||
items := objectProperty(hints, "items")
|
||||
if _, ok := items["type"]; !ok {
|
||||
items["type"] = "object"
|
||||
}
|
||||
itemProps := objectProperty(items, "properties")
|
||||
for name, value := range map[string]any{"text": strParam("提示内容"), "asset_ids": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}}, "traffic_refs": agent.HintTrafficSchema()} {
|
||||
if _, exists := itemProps[name]; !exists {
|
||||
itemProps[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
raw, _ := json.Marshal(schema)
|
||||
result, err := s.m.pg.Exec(`UPDATE tools SET schema=$2::jsonb,updated_at=now() WHERE key=$1 AND system AND schema=$3::jsonb`, key, string(raw), string(row.Schema))
|
||||
if err != nil {
|
||||
log.Printf("[evidence] upgrade %s: %v", key, err)
|
||||
return
|
||||
}
|
||||
if n, _ := result.RowsAffected(); n != 1 {
|
||||
return
|
||||
} // preserve concurrent user edits
|
||||
}
|
||||
// Upgrade only the original default binding. Customized lists and enabled
|
||||
// flags survive; the one-time flag also preserves future user unbinding.
|
||||
readers := `["worker","reporter"]`
|
||||
for _, key := range []string{"traffic_search", "traffic_get", "traffic_blob"} {
|
||||
if _, err := s.m.pg.Exec(`UPDATE tools SET agents=$2::jsonb WHERE key=$1 AND system AND (agents='["worker"]'::jsonb OR (agents @> '["worker","planner","mainagent","auto","pentest"]'::jsonb AND jsonb_array_length(agents)=5))`, key, readers); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, err := s.m.pg.Exec(`UPDATE tools SET agents=$1::jsonb WHERE key='get_finding_traffic' AND system AND agents @> '["auto","reporter"]'::jsonb AND jsonb_array_length(agents)=2`, `["auto","reporter","worker","planner","mainagent","pentest"]`); err != nil {
|
||||
return
|
||||
}
|
||||
// Replace the previous code default only; preserve customized binding lists.
|
||||
if _, err := s.m.pg.Exec(`UPDATE tools SET agents='["reporter"]'::jsonb WHERE key='bind_finding_traffic' AND system AND agents @> '["worker","planner","mainagent","auto","pentest"]'::jsonb AND jsonb_array_length(agents)=5`); err != nil {
|
||||
return
|
||||
}
|
||||
if err := s.m.pg.AddAgentToToolBinding("reporter", []string{"bind_finding_traffic"}); err != nil {
|
||||
return
|
||||
}
|
||||
_ = s.m.pg.SetSetting(flag, "true")
|
||||
}
|
||||
|
||||
func objectProperty(parent map[string]any, key string) map[string]any {
|
||||
value, ok := parent[key].(map[string]any)
|
||||
if !ok {
|
||||
value = map[string]any{}
|
||||
parent[key] = value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *Server) agentFindingTrafficAccess(ctx context.Context, id int64, write bool) error {
|
||||
if id <= 0 {
|
||||
return errors.New("finding_id 必须为独立漏洞记录 ID;不是探索节点 ID")
|
||||
}
|
||||
f, err := s.m.pg.GetFinding(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f == nil {
|
||||
return fmt.Errorf("%w:finding_id=%d。证据工具使用独立漏洞记录 ID,请从 list_task_findings / get_task_node_detail 的 finding_id 字段读取;不要传 id / finding_node_id", db.ErrFindingNotFound, id)
|
||||
}
|
||||
if ri := agent.RunInfoFrom(ctx); ri.TaskID > 0 {
|
||||
task := s.m.ResolveTask(strconv.FormatInt(ri.TaskID, 10))
|
||||
if task == nil {
|
||||
return errors.New("任务不存在")
|
||||
}
|
||||
_, inherited, allowed := findingProvenanceInTask(task, f.TaskID)
|
||||
if !allowed {
|
||||
return errors.New("当前任务不可读取该漏洞")
|
||||
}
|
||||
if write && inherited {
|
||||
return errors.New("继承漏洞的流量证据只读,请到来源任务修改")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) toolBindFindingTraffic() actool.CoreTool {
|
||||
return wrTool("bind_finding_traffic", "为已登记漏洞补绑经核实的真实 HTTP 流量。finding_id 使用独立漏洞记录 ID;不要传探索节点 ID。同批引用全部成功或全部失败,重复引用不覆盖已有说明。补绑会使已有报告标记待更新;不要为补包重新探测或重复创建漏洞。",
|
||||
objSchema(map[string]any{"finding_id": strParam("独立漏洞记录 ID,从 list_task_findings / get_task_node_detail 的 finding_id 字段读取"), "traffic_refs": agent.HintTrafficSchema()}, "finding_id", "traffic_refs"),
|
||||
func(ctx context.Context, raw json.RawMessage) (actool.Result, error) {
|
||||
if !s.m.pg.GetBool(settingAgentTrafficBinding, false) {
|
||||
return actool.Errorf("Agent 自动绑定流量已关闭;请在系统设置开启,或使用页面人工绑定。"), nil
|
||||
}
|
||||
var args struct {
|
||||
FindingID json.RawMessage `json:"finding_id"`
|
||||
Refs []db.TrafficRef `json:"traffic_refs"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &args); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
id := parseProfileID(args.FindingID)
|
||||
if err := s.agentFindingTrafficAccess(ctx, id, true); err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
if len(args.Refs) == 0 {
|
||||
return actool.Errorf("补绑需要至少一条已核实的 traffic_refs;无流量无需调用此工具"), nil
|
||||
}
|
||||
list, err := s.evidenceStore().Bind(ctx, id, args.Refs)
|
||||
if err != nil {
|
||||
return actool.Errorf(err.Error()), nil
|
||||
}
|
||||
return jsonResult(trafficSummary(list))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user