mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-02 02:43:50 +08:00
134 lines
5.8 KiB
Go
134 lines
5.8 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/Autumn-27/artex/db"
|
|
)
|
|
|
|
// taskAssetMapping starts a visible topology-mapping conversation for the task.
|
|
// The asset relationship graph remains sourced directly from the database and
|
|
// does not depend on this conversation. The conversation is the audit trail for
|
|
// the two human-readable reports (asset relationships + network topology).
|
|
func (s *Server) taskAssetMapping(w http.ResponseWriter, r *http.Request) {
|
|
t, ok := s.m.Task(r.PathValue("id"))
|
|
if !ok {
|
|
writeErr(w, http.StatusNotFound, "task not found")
|
|
return
|
|
}
|
|
if s.m.pg == nil || s.chatAgentRef() == nil {
|
|
writeErr(w, http.StatusServiceUnavailable, s.chatUnavailableReason())
|
|
return
|
|
}
|
|
if agentInfo, err := s.m.pg.GetAgentByKey("asset_mapper"); err != nil || agentInfo == nil || !agentInfo.Enabled {
|
|
writeErr(w, http.StatusServiceUnavailable, "网络拓扑梳理 Agent 未启用")
|
|
return
|
|
}
|
|
taskID, _ := strconv.ParseInt(t.ID, 10, 64)
|
|
message := fmt.Sprintf(`请重新梳理任务 #%d 的网络拓扑,并在同一次会话中输出两份相互独立的结果。
|
|
|
|
三层区域是本任务拓扑的硬约束,不是可忽略的排版:
|
|
- internet(互联网区):公网根域、子域名、公共 IP 和外部入口。
|
|
- boundary(边界区):对外暴露的服务、网关、Web 应用或端点;只有存在公开主机/URL证据时才归入此层。
|
|
- internal(内网区):私有 IP、回环/链路本地地址及其承载的业务应用、端点和数据库。
|
|
节点摘要中的 zone/zone_reason 是后端根据真实地址和资产证据计算的权威归类;不得仅按 kind 改写区域。没有 zone 或证据不足时必须标记“待确认”,不能臆造跨区连接。
|
|
|
|
输出必须严格使用下面两个一级标题,不能合并成一份:
|
|
## 资产关系结果
|
|
- 只根据当前任务及其直接关联任务的真实资产数据,保留资产库中的层级和归属关系。
|
|
- 这部分是资产关系图的说明,不要声称本次 Agent 改写了资产关系,也不要补充摘要中没有的资产。
|
|
|
|
## 网络拓扑结果
|
|
- 只整理互联网入口、边界服务、内网业务三层网络关系,并说明每个节点所属区域。
|
|
- 只展示有真实资产证据的连接;数据库节点标注可识别的数据库类型。
|
|
|
|
两份结果都只能使用当前任务及其直接关联任务的真实资产数据;不要探测目标、不要修改资产库,也不要把没有证据的连接描述成已验证通路。`, taskID)
|
|
if as := s.m.Assets(); as != nil {
|
|
if graph, err := as.BuildCoverageGraph(taskID, t.ExpID); err == nil {
|
|
type nodeSample struct {
|
|
Key, Kind, Label, Domain, IP, Zone, ZoneReason string
|
|
Port int
|
|
Tested, InScope bool
|
|
}
|
|
counts := map[string]int{}
|
|
sample := make([]nodeSample, 0, 60)
|
|
for _, node := range graph.Nodes {
|
|
counts[node.Kind]++
|
|
if len(sample) < 60 {
|
|
sample = append(sample, nodeSample{
|
|
Key: node.Key, Kind: node.Kind, Label: node.Label, Domain: node.Domain,
|
|
IP: node.IP, Port: node.Port, Tested: node.Tested, InScope: node.InScope,
|
|
Zone: node.Zone, ZoneReason: node.ZoneReason,
|
|
})
|
|
}
|
|
}
|
|
edges := graph.Edges
|
|
truncatedEdges := len(edges) > 80
|
|
if truncatedEdges {
|
|
edges = edges[:80]
|
|
}
|
|
snapshot, _ := json.Marshal(map[string]any{
|
|
"nodes_total": len(graph.Nodes), "edges_total": len(graph.Edges),
|
|
"counts_by_kind": counts, "node_sample": sample, "edge_sample": edges,
|
|
"truncated": len(graph.Nodes) > len(sample) || truncatedEdges,
|
|
})
|
|
message += "\n\n页面刚读取的任务资产图真实摘要如下;只依据摘要和任务范围解释,避免把全局资产误当成本任务资产。未列出的节点不要逐一补写:\n" + string(snapshot)
|
|
}
|
|
}
|
|
title := fmt.Sprintf("资产与网络梳理 · 任务 #%d", taskID)
|
|
conversation, err := s.m.pg.CreateConversation("asset_mapper", title, nil)
|
|
if err != nil || conversation == nil {
|
|
writeErr(w, http.StatusInternalServerError, "创建拓扑梳理会话失败")
|
|
return
|
|
}
|
|
if _, err := s.m.pg.AppendConvActivity(conversation.ID, db.Activity{
|
|
Worker: "asset_mapper", Kind: "user", Summary: title, Detail: message,
|
|
}); err != nil {
|
|
writeErr(w, http.StatusInternalServerError, "写入拓扑梳理会话失败")
|
|
return
|
|
}
|
|
busyKey := s.convBusyKey(conversation.ID)
|
|
s.chatMu.Lock()
|
|
s.chatBusy[busyKey] = true
|
|
s.chatMu.Unlock()
|
|
s.runConversation(conversation, message, busyKey, message)
|
|
writeJSON(w, http.StatusAccepted, map[string]any{
|
|
"status": "accepted", "agent_key": "asset_mapper", "conversation_id": conversation.ID,
|
|
})
|
|
}
|
|
|
|
func (s *Server) taskAssetMappingStatus(w http.ResponseWriter, r *http.Request) {
|
|
t, ok := s.m.Task(r.PathValue("id"))
|
|
if !ok {
|
|
writeErr(w, http.StatusNotFound, "task not found")
|
|
return
|
|
}
|
|
conversations, err := s.m.pg.ListConversations()
|
|
if err != nil {
|
|
writeErr(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
title := fmt.Sprintf("资产与网络梳理 · 任务 #%s", t.ID)
|
|
legacyTitle := fmt.Sprintf("资产梳理 · 任务 #%s", t.ID)
|
|
for _, conversation := range conversations {
|
|
if conversation.AgentKey != "asset_mapper" || (conversation.Title != title && conversation.Title != legacyTitle) {
|
|
continue
|
|
}
|
|
busyKey := s.convBusyKey(conversation.ID)
|
|
s.chatMu.Lock()
|
|
running := s.chatBusy[busyKey]
|
|
s.chatMu.Unlock()
|
|
// A completed conversation is historical evidence, not an active run.
|
|
// Keep looking so a newer run is not hidden by an older one, and avoid
|
|
// making the client poll a completed conversation after page reload.
|
|
if running {
|
|
writeJSON(w, http.StatusOK, map[string]any{"running": true, "conversation_id": conversation.ID})
|
|
return
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"running": false})
|
|
}
|