diff --git a/agent/tools_insert.go b/agent/tools_insert.go index d442bf2..c1a9973 100644 --- a/agent/tools_insert.go +++ b/agent/tools_insert.go @@ -521,6 +521,45 @@ func (t *ToolSet) listAssets() actool.CoreTool { ) } +// deleteAssetsByHost hard-deletes every asset tied to one host (exact match). +func (t *ToolSet) deleteAssetsByHost() actool.CoreTool { + return writeTool( + "delete_assets_by_host", + "按 host 精确删除资产:删掉该 host 的域名/子域名,以及其下的服务(service)、接口(endpoint)。\n"+ + "host 完全匹配(小写、去空格),不是模糊/通配。\n"+ + "传根域名(如 example.com)会连带删除它的子域名及其服务/接口;传子域名(如 a.example.com)或 IP 只删该 host 自身及其服务/接口。\n"+ + "⚠️ 硬删除且作用于全局资产库(跨任务共享),不可撤销;请先用 list_assets 确认范围再删。", + obj(map[string]any{ + "host": str("要删除的 host:域名/子域名/IP。完全匹配,如 example.com 或 a.example.com 或 1.2.3.4"), + }, "host"), + func(_ context.Context, in json.RawMessage) (actool.Result, error) { + if t.as == nil { + return actool.Errorf("delete_assets_by_host 未启用: AssetStore 未初始化"), nil + } + var a struct { + Host string `json:"host"` + } + _ = json.Unmarshal(in, &a) + if strings.TrimSpace(a.Host) == "" { + return actool.Errorf("host 不能为空"), nil + } + counts, err := t.as.DeleteByHost(a.Host) + if err != nil { + return actool.Errorf("删除失败: " + err.Error()), nil + } + var total int64 + for _, n := range counts { + total += n + } + return jsonResult(map[string]any{ + "host": a.Host, + "deleted": total, + "deleted_by_type": counts, + }) + }, + ) +} + // listCompanies lets an agent enumerate companies (企业) with their scope + asset count. func (t *ToolSet) listCompanies() actool.CoreTool { return readTool( diff --git a/db/assets.go b/db/assets.go index 01241b4..fee5d94 100644 --- a/db/assets.go +++ b/db/assets.go @@ -15,16 +15,16 @@ import ( // Asset is a row in the assets table. type Asset struct { - ID int64 `json:"id"` - Type string `json:"type"` - CompanyID *int64 `json:"company_id,omitempty"` - TaskIDs []int64 `json:"task_ids"` - Domain string `json:"domain,omitempty"` - RootDomain string `json:"root_domain,omitempty"` - IP string `json:"ip,omitempty"` - CSegment string `json:"c_segment,omitempty"` - Port *int `json:"port,omitempty"` - ICP string `json:"icp,omitempty"` + ID int64 `json:"id"` + Type string `json:"type"` + CompanyID *int64 `json:"company_id,omitempty"` + TaskIDs []int64 `json:"task_ids"` + Domain string `json:"domain,omitempty"` + RootDomain string `json:"root_domain,omitempty"` + IP string `json:"ip,omitempty"` + CSegment string `json:"c_segment,omitempty"` + Port *int `json:"port,omitempty"` + ICP string `json:"icp,omitempty"` // ip fields BoundDomains []string `json:"bound_domains,omitempty"` OpenPorts []map[string]any `json:"open_ports,omitempty"` @@ -986,7 +986,6 @@ const assetSelectCols = `SELECT id, type, company_id, array_to_json(task_ids)::t COALESCE(method,''), array_to_json(params)::text, extra, last_seen::text FROM assets` - // GetByIDs returns assets with the given ids (order preserved by id array order). func (s *AssetStore) GetByIDs(ids []int64) ([]*Asset, error) { if len(ids) == 0 { @@ -1016,6 +1015,38 @@ func (s *AssetStore) DeleteByCompanyID(companyID int64) (int64, error) { return res.RowsAffected() } +// DeleteByHost hard-deletes every asset whose host exactly matches the given value: +// root_domain / subdomain / service / endpoint (they carry the host in domain or +// root_domain) plus an ip asset and its services/endpoints (ip column). The host is +// normalized the same way it is stored (DomainKey: lowercase/trim/strip trailing dot) +// so matching is exact, not fuzzy. Passing a root domain also removes its subdomains +// and their services/endpoints (they carry root_domain = that host); passing a +// subdomain/IP removes only that host's own assets. Referencing exploration_anchors +// rows are cleaned by ON DELETE CASCADE. Returns rows deleted, grouped by type. +func (s *AssetStore) DeleteByHost(host string) (map[string]int64, error) { + h := DomainKey(host) + if h == "" { + return nil, fmt.Errorf("host is required") + } + rows, err := s.db.Query(` +DELETE FROM assets +WHERE domain = $1 OR root_domain = $1 OR ip = $1 +RETURNING type`, h) + if err != nil { + return nil, err + } + defer rows.Close() + counts := map[string]int64{} + for rows.Next() { + var t string + if err := rows.Scan(&t); err != nil { + return nil, err + } + counts[t]++ + } + return counts, rows.Err() +} + // DeleteByIDs hard-deletes assets by their IDs. Returns the number of rows deleted. func (s *AssetStore) DeleteByIDs(ids []int64) (int64, error) { if len(ids) == 0 {